Insomni’hack 2024 CTF Teaser – Cache Cache

WriteUp 10个月前 admin
50 0 0

Last year, for the Insomni’hack 2023 CTF Teaser, I created a challenge based on a logic bug in a Windows RPC server. I was pleased with the result, so I renewed the experience. Besides, I already knew what type of bug to tackle for this new edition. Insomni'hack 2024 CTF Teaser - Cache Cache
去年,在 Insomni’hack 2023 CTF 预告片中,我根据 Windows RPC 服务器中的逻辑错误创建了一个挑战。我对结果很满意,所以我更新了体验。此外,我已经知道这个新版本要解决什么类型的错误。 Insomni'hack 2024 CTF Teaser - Cache Cache

Personal thoughts 个人想法

Like my previous write-up, I will begin with some thoughts about the difficulties of creating a challenge and facing inevitable criticism.
就像我之前的文章一样,我将从一些关于创造挑战和面对不可避免的批评的困难开始。

This CTF has become so notorious over the years that creating a challenge for it is a big responsibility, and also a challenge in itself. Ideally, we want to come up with original ideas, and somehow implement them within a limited time without making mistakes that would result in unintended solves. In a perfect world, this should result in something that is difficult enough for the most experienced teams, but does not leave beginners behind. No need to say, it is a very delicate balance to find.
多年来,这个CTF已经变得如此臭名昭著,以至于为它创造一个挑战是一个很大的责任,而且本身就是一个挑战。理想情况下,我们希望提出原创的想法,并在有限的时间内以某种方式实施它们,而不会犯会导致意外解决的错误。在一个完美的世界里,这应该会导致一些对于最有经验的团队来说足够困难的事情,但不会让初学者落后。不用说,这是一个非常微妙的平衡。

One of the consequences is that it is virtually impossible to please everyone. And the harder the challenge is, the more likely you are to face frustrated players, amongst which some will definitely let you know about their feelings.
后果之一是几乎不可能取悦所有人。挑战越难,你就越有可能面对沮丧的玩家,其中有些人肯定会让你知道他们的感受。

Throughout the event, I received two complaints remarks. The first one was that the teams who had previously worked on (or even solved) my previous challenge had a huge advantage, compared to other teams who had to start from scratch, and figure out how to communicate with the remote server. In the same vein, some other players asked why a skeleton code snippet for the RPC client initialization was not provided, at least to get people started.
在整个活动期间,我收到了两封投诉。第一个是,与其他必须从头开始并弄清楚如何与远程服务器通信的团队相比,以前参与(甚至解决)我之前挑战的团队具有巨大的优势。同样,其他一些玩家也问为什么没有提供用于 RPC 客户端初始化的框架代码片段,至少是为了让人们入门。

My answer to that is relatively simple. Since I had already published a detailed write-up for the previous challenge, I thought it would provide a sufficient head start that would offset the initial difficulty for the teams that were new to those concepts. On top of that, in addition to the reverse engineering methodology, it provided code snippets that showed precisely how to connect to the server. All it required was a quick search. With keywords such as “windows rpc ctf”, which are not even that specific, my blog post is the 7th result (at the time of writing).
我的回答相对简单。由于我已经为上一个挑战发表了一篇详细的文章,我认为这将提供一个足够的领先优势,以抵消那些不熟悉这些概念的团队的初始困难。最重要的是,除了逆向工程方法外,它还提供了代码片段,精确地显示了如何连接到服务器。它所需要的只是快速搜索。使用诸如“windows rpc ctf”之类的关键字,甚至不是那么具体,我的博客文章是第 7 个结果(在撰写本文时)。

Insomni'hack 2024 CTF Teaser - Cache CacheGoogle search with the keywords “windows rpc ctf”
使用关键字“windows rpc ctf”进行 Google 搜索

Write-up 文章写

The challenge 挑战

The description of the challenge is similar to the previous one. The target is a Windows service that we can reach through port 80. The server’s executable is provided so that players can reverse it and test it offline.
挑战的描述与上一个类似。目标是我们可以通过端口 80 访问的 Windows 服务。提供了服务器的可执行文件,以便玩家可以反转它并离线测试它。

Insomni'hack 2024 CTF Teaser - Cache CacheChallenge description 挑战描述

Initial analysis 初步分析

I won’t go into the details of how to reverse engineer the server as I already did that in the previous write-up. The methodology is exactly the same. The first goal was to reconstruct the IDL file. Below is the original file I extracted from the sources of the project.
我不会详细介绍如何对服务器进行逆向工程,因为我在上一篇文章中已经这样做了。方法完全相同。第一个目标是重建 IDL 文件。以下是我从项目源代码中提取的原始文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[
    uuid (9b5cb5a7-624d-4ae2-ab79-529fbb2f3072),
    version(1.0),
    pointer_default(unique)
]
interface winternals3
{
    typedef struct _PLAYER_CONTEXT
    {
        wchar_t wszPlayerName[64];
        wchar_t wszPlayerLocation[64];
        int bPlayerFound;
    } PLAYER_CONTEXT, * PPLAYER_CONTEXT;

    typedef [context_handle] void* PCONTEXT_HANDLE_TYPE;

    long HsCreatePlayer([in] handle_t binding_h, [out] PCONTEXT_HANDLE_TYPE* pphContext, [in, string] wchar_t* pwszName); // 0
    long HsGetPlayerName([in] handle_t binding_h, [in] PCONTEXT_HANDLE_TYPE phContext, [out, string][ref] wchar_t** ppwszName); // 1
    long HsCallReady([in] handle_t binding_h, [in, string] wchar_t* pwszMessage, [out, string][ref] wchar_t** ppwszResponse); // 2
    long HsHidePlayer([in] handle_t binding_h, [in] PCONTEXT_HANDLE_TYPE phContext, [in, string] wchar_t* pwszLocation); // 3
    long HsGetPlayerLocation([in] handle_t binding_h, [in] PCONTEXT_HANDLE_TYPE phContext, [out, string][ref] wchar_t** ppwszLocation); // 4
    long HsSeekPlayer([in] handle_t binding_h, [in] PCONTEXT_HANDLE_TYPE phContext); // 5
    long HsGetFlag([in] handle_t binding_h, [in] PCONTEXT_HANDLE_TYPE phContext, [out, string][ref] wchar_t** ppwszFlag); // 6
    long HsClose([in] handle_t binding_h, [in, out] PCONTEXT_HANDLE_TYPE* pphContext); // 7
}

Through reverse engineering, you should have found a similar result, without the names of the two custom types and the function parameters. The procedure names were provided in log messages.
通过逆向工程,您应该已经找到了类似的结果,没有两个自定义类型的名称和函数参数。过程名称在日志消息中提供。

First contact 初次接触

From there, if you tried to invoke any of the procedures, there is a chance you got only “Access Denied” errors. If so, you probably missed a key aspect of this RPC server.
从那里,如果您尝试调用任何过程,则有可能只收到“拒绝访问”错误。如果是这样,您可能错过了此 RPC 服务器的一个关键方面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...
  Log(L"INIT > Registering protocol sequence: %ws:%ws\r\n");
  RVar1 = RpcServerUseProtseqEpW(
    (RPC_WSTR)L"ncacn_http", 10, (RPC_WSTR)L"8000", (void *)0x0
  );
  if (RVar1 == 0) {
    Log(L"INIT > Registering authentication information\r\n");
    RVar1 = RpcServerRegisterAuthInfoW(
        (RPC_WSTR)0x0, 10, (RPC_AUTH_KEY_RETRIEVAL_FN)0x0, (void *)0x0
    );
    if (RVar1 == 0) {
      Log(L"INIT > Registering interface\r\n");
      RVar1 = RpcServerRegisterIf2(
        &winternals3___RpcServerInterface, // RPC_IF_HANDLE IfSpec
        (UUID *)0x0,            // UUID *MgrTypeUuid
        (void *)0x0,            // RPC_MGR_EPV *MgrEpv
        0,                      // unsigned int Flags
        0x4d2,                  // unsigned int MaxCalls
        0xffffffff,             // unsigned int MaxRpcSize
        ServerSecurityCallback  // RPC_IF_CALLBACK_FN *IfCallbackFn
      );
// ...

Note that, in my case, Ghidra automatically imported the PDB file. That’s why some symbols are shown here, but it was possible to guess them.
请注意,就我而言,Ghidra 自动导入了 PDB 文件。这就是为什么这里显示一些符号的原因,但可以猜到它们。

The thing to notice here was that the last argument of RpcServerRegisterIf2 is not null, which means that a security-callback function is implemented. In the documentation, you can read that “specifying a security-callback function allows the server application to restrict access to its interfaces on an individual client basis”.
这里需要注意的是,最后一个 RpcServerRegisterIf2 参数不是 null,这意味着实现了安全回调函数。在文档中,您可以读到“指定安全回调函数允许服务器应用程序在单个客户端上限制对其接口的访问”。

Now, I have no idea why, but if you relied on the pseudo-code generated by Ghidra, you would have been out of luck because it does not show the most important part of the function, as highlighted on the screenshot below. This was not intentional from my part.
现在,我不知道为什么,但如果你依赖 Ghidra 生成的伪代码,你就不走运了,因为它没有显示函数最重要的部分,如下面的屏幕截图所示。这不是我故意的。

Insomni'hack 2024 CTF Teaser - Cache CacheAnalysis of the security callback with Ghidra
使用 Ghidra 分析安全回调

IDA, on the other hand, does a way better job with it. It was able to generate a pseudo-code that is very close to the source, even with the free version I used for this next screenshot.
另一方面,IDA在这方面做得更好。它能够生成一个非常接近源代码的伪代码,即使我用于下一个屏幕截图的免费版本也是如此。

Insomni'hack 2024 CTF Teaser - Cache CacheAnalysis of the security callback with IDA Free
使用 IDA Free 分析安全回调

For the comparison, below is the original source code.
为了进行比较,以下是原始源代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
RPC_STATUS ServerSecurityCallback(RPC_IF_HANDLE InterfaceUuid, void* Context)
{
    Log(L"CALLBACK > Callback start\r\n");

    RPC_STATUS status = E_UNEXPECTED, authorization = RPC_S_ACCESS_DENIED;
    RPC_CALL_ATTRIBUTES_V2_W RpcCallAttributes;
    USHORT opnum;
    DWORD al;
    SECURITY_IMPERSONATION_LEVEL il = SecurityAnonymous;

    ZeroMemory(&RpcCallAttributes, sizeof(RpcCallAttributes));
    RpcCallAttributes.Version = 2;
    RpcCallAttributes.Flags = 0;

    status = RpcServerInqCallAttributesW(Context, &RpcCallAttributes);
    if (status != RPC_S_OK) {
        Log(L"RpcServerInqCallAttributesW() err: %d - 0x%08x\r\n", status, status);
        goto cleanup;
    }

    opnum = RpcCallAttributes.OpNum;
    al = RpcCallAttributes.AuthenticationLevel;
    GetImpersonationLevel(Context, &il);

    if (il == SecurityIdentification) {
        if (al == RPC_C_AUTHN_LEVEL_PKT_INTEGRITY) {
            if (opnum == 2) { // HsCallReady
                authorization = RPC_S_OK;
            }
        } else if (al == RPC_C_AUTHN_LEVEL_PKT_PRIVACY) {
            if (opnum == 3) { // HsSeekPlayer
                authorization = RPC_S_OK;
            }
        }
    } else if (il == SecurityImpersonation) {
        if (al == RPC_C_AUTHN_LEVEL_PKT_PRIVACY) {
            if (opnum == 42) {
                authorization = RPC_S_OK;
            }
        }
    }

cleanup:
    Log(L"CALLBACK > Callback end\r\n");

    return authorization;
}

Anyway, what you had to figure out is that the security callback function takes a decision as to whether it should authorize a client’s call based on three pieces of information:
无论如何,您必须弄清楚的是,安全回调函数会根据三条信息来决定是否应该授权客户端的调用:

  • the OpNum of the procedure invoked by the client;
    OpNum 客户端调用的过程;
  • the Authentication Level associated to the client’s binding;
    与客户端绑定关联的身份验证级别;
  • the Impersonation Level associated to the client’s binding.
    与客户端绑定关联的模拟级别。

For instance, authorization would be granted for the procedure with the OpNum 2 (HsCallReady) only if the impersonation level is SecurityIdentification and the authentication level is PKT_INTEGRITY, which are two parameters a client can set when initializing its binding handle. Other similar checks are performed when invoking the procedures with the OpNum 3 and 42. If you try to invoke other procedures, the function will always return RPC_S_ACCESS_DENIED.
例如,仅当模拟级别为 SecurityIdentification 且身份验证级别为 PKT_INTEGRITY 时,才会授予使用 OpNum 2 ( HsCallReady ) 的过程的授权,这是客户端在初始化其绑定句柄时可以设置的两个参数。使用 OpNum 3 和 42 调用过程时,将执行其他类似的检查。如果尝试调用其他过程,该函数将始终返回 RPC_S_ACCESS_DENIED 。

Now, if you analyzed all the procedures, you should have found that they pretty much all need to be invoked, with the appropriate values, in order to obtain the proper server-side context that will allow you to eventually get the flag. But, as we’ve seen, some of those procedures are unreachable because of the security callback. At this point, your conclusion should be that the problem is impossible to solve. Unless there is a trick…
现在,如果您分析了所有过程,您应该已经发现它们几乎都需要使用适当的值进行调用,以便获得正确的服务器端上下文,从而最终获得标志。但是,正如我们所看到的,由于安全回调,其中一些过程是无法访问的。在这一点上,你的结论应该是问题不可能解决。除非有诀窍……

A caching issue 缓存问题

Of course, there was a trick! The name of the challenge was supposed to hint towards the solution. With a quick search including the keywords “windows rpc cache”, the very first result should have been this one (at least at the time of writing).
当然,有一个诀窍!挑战的名称应该暗示解决方案。通过快速搜索,包括关键字“windows rpc cache”,第一个结果应该是这个(至少在撰写本文时)。

Insomni'hack 2024 CTF Teaser - Cache CacheGoogle search with the keywords “windows rpc cache”
使用关键字“windows rpc cache”进行 Google 搜索

In the blog post Cold Hard Cache – Bypassing RPC Interface Security with Cache Abuse, Ben Barnea and Stiv Kupchik discussed a very interesting topic I wasn’t aware of before this publication. Essentially, they explain that the result of a security callback can be cached, either per interface, or per call, which can lead to trick logic bugs if not handled correctly by the developers.
在博客文章 Cold Hard Cache – Bypassing RPC Interface Security with Cache Abuse 中,Ben Barnea 和 Stiv Kupchik 讨论了一个非常有趣的话题,我在这篇文章发表之前并不知道。从本质上讲,他们解释说,安全回调的结果可以按接口或按调用进行缓存,如果开发人员未正确处理,可能会导致欺骗逻辑错误。

Let’s say we have an RPC server with one interface and two procedures A and B. This server wants to grant access to low-privileged users to procedure A, but not B, using a security callback. If a client connects to the server and invokes A, the request is served. However, if the same client connects and invokes B, the access is denied.
假设我们有一个 RPC 服务器,它有一个接口和两个过程 A 和 B。此服务器希望使用安全回调向低特权用户授予对过程 A 的访问权限,而不是向过程 B 授予访问权限。如果客户端连接到服务器并调用 A,则为请求提供服务。但是,如果同一客户端连接并调用 B,则访问将被拒绝。

Though, because of the interface-based caching mechanism, if a client were to connect and invoke procedure A, the authorization would be cached by the RPC runtime. Therefore, if the client reuses the same binding to invoke B, the security callback is not invoked, and the request is served. This is exactly the type of behavior we need to exploit here.
但是,由于基于接口的缓存机制,如果客户端要连接并调用过程 A,则授权将由 RPC 运行时缓存。因此,如果客户端重用相同的绑定来调用 B,则不会调用安全回调,而是处理请求。这正是我们需要在这里利用的行为类型。

There is still one thing to know though, which is not explicitly mentioned in the blog post. Whenever a client alters its binding, the server does not use the cache, so the security callback is invoked again.
不过,还有一件事需要知道,这在博客文章中没有明确提及。每当客户端更改其绑定时,服务器都不会使用缓存,因此会再次调用安全回调。

Solving the maze 解开迷宫

The ultimate goal was to generate the appropriate server-side state represented by the structure PLAYER_CONTEXT. More specifically, the flag bPlayerFound had to be set to 1, so that the procedure HsGetFlag could be invoked.
最终目标是生成由结构 PLAYER_CONTEXT 表示的适当服务器端状态。更具体地说,必须将标志 bPlayerFound 设置为 1,以便可以调用该过程 HsGetFlag 。

To do that, the idea was to solve a kind of maze, starting from the exit, and working your way out to the entry point as follows.
要做到这一点,我们的想法是解决一种迷宫,从出口开始,然后一路走到入口点,如下所示。

1. "HsGetFlag" call requires:
    - Impersonation Level = "IMPERSONATION"
    - Authentication Level = "PRIVACY"
    - Context->found = true
2. "HsGetFlag" authorization granted through:
    - An RPC call with the opnum 42
3. "Context->found = true" requires:
    - An RPC call to "HsSeekPlayer"
    - Context->name = "Alice"
    - Context->location = "Wonderland"
4. "Context->location = Wonderland" requires:
    - An RPC call to "HsHidePlayer"
    - An RPC call to "HsSeekPlayer"
5. "HsSeekPlayer" call requires:
    - Impersonation level = "IDENTIFICATION"
    - Authentication level = "PRIVACY"
6. "HsSeekPlayer" authorization granted through:
    - An RPC call to "HsHidePlayer"
7. "Context->name = Alice" requires
    - An RPC call to "HsCreatePlayer"
8. "HsCreatePlayer" authorization granted through:
    - An RPC call to "HsCallReady"
9. "HsCallReady" call requires:
    - Impersonation level = "IDENTIFICATION"
    - Authentication level = "INTEGRITY"

From there, the exploit consisted in implementing all the steps in reverse order. The only thing to know here is that the impersonation and authentication levels could be set using the API RpcBindingSetAuthInfoEx(A/W). The authentication level is the third parameter. The impersonation level can be set through the structure RPC_SECURITY_QOS, which is passed as the last argument.
从那里开始,漏洞利用包括以相反的顺序实现所有步骤。这里唯一要知道的是,可以使用 API RpcBindingSetAuthInfoEx(A/W) 设置模拟和身份验证级别。身份验证级别是第三个参数。模拟级别可以通过结构设置,该结构 RPC_SECURITY_QOS 作为最后一个参数传递。

As for the check for the OpNum 42, the RPC interface has only 8 procedures, so there is obviously no procedure with the OpNum 42. Nevertheless, this value is also controlled by the client. Personally, I simply added non-existent procedure entries in my client-side IDL file such as long HsNotUsed8();, until I reached long HsNotUsed42();. This way the MIDL compiler generates all the stubs for you.
至于 OpNum 42 的检查,RPC 接口只有 8 个过程,所以 OpNum 42 显然没有过程。但是,此值也由客户端控制。就个人而言,我只是在客户端 IDL 文件中添加了不存在的过程条目,例如 long HsNotUsed8(); ,直到我到达 long HsNotUsed42(); .这样,MIDL 编译器将为你生成所有存根。

When trying to invoke the procedure HsNotUsed42 though, you just have to expect the client-side RPC runtime to throw an exception with the error code returned by the remote server. In that case, it would be 1745 - RPC_S_PROCNUM_OUT_OF_RANGE.
但是,在尝试调用该过程 HsNotUsed42 时,您只需要期望客户端 RPC 运行时引发异常,并显示远程服务器返回的错误代码。在这种情况下,它将是 1745 - RPC_S_PROCNUM_OUT_OF_RANGE .

1
2
3
4
5
6
7
8
9
__try {
    // We need to call HsNotUsed42 to pass and cache the authorization, but
    // the procedure number is not defined, an exception will be thrown. This
    // is expected.
    wprintf(L"[*] 5) IMPERSONATION + PRIVACY + HsNotUsed42() -> Authorization cached\r\n");
    ret = HsNotUsed42(BindingHandle);
} __except (EXCEPTION_EXECUTE_HANDLER) {
    wprintf(L"[*] RPC runtime exception: %d - 0x%08x (this exception is expected).\r\n", RpcExceptionCode(), RpcExceptionCode());
}

And finally… Here is my exploit code in action!
最后……这是我的漏洞利用代码!

Insomni'hack 2024 CTF Teaser - Cache CacheFinal exploit 最终漏洞利用

Conclusion 结论

Last year, InsoBug was solved by only 3 teams. This year, Cache Cache was solved by a total of 8 teams. Congratulations to them! Insomni'hack 2024 CTF Teaser - Cache Cache
去年, InsoBug 只有3个团队解决了这个问题。今年, Cache Cache 共有8支队伍破解。恭喜他们! Insomni'hack 2024 CTF Teaser - Cache Cache

Insomni'hack 2024 CTF Teaser - Cache CacheFirst three teams who solved the challenge
前三名完成挑战的团队

This was supposed to be a hard challenge, and I’m glad so many people chose to grapple with it. Obviously, not all the players were able to reach the end, even after spending hours on it, which can be understandably frustrating, but that’s what you sign up for when you participate in CTFs I guess. Insomni'hack 2024 CTF Teaser - Cache Cache
这本来是一个艰巨的挑战,我很高兴有这么多人选择与之抗争。显然,并不是所有的玩家都能到达终点,即使花了几个小时,这可能是可以理解的令人沮丧的,但我想这就是你参加 CTF 时所注册的。 Insomni'hack 2024 CTF Teaser - Cache Cache

A last word about the challenge’s name. First, the word “Cache” was intended to hint towards the solution, as I mentioned earlier. Second, the name “Cache Cache” is French for “Hide-and-Seek”. Although French-speaking people were more likely to get the joke/pun, and the references in the procedures’ names, it was definitely not a requirement to solve the challenge. Insomni'hack 2024 CTF Teaser - Cache Cache
关于挑战名称的最后一句话。首先,正如我之前提到的,“缓存”一词旨在暗示解决方案。其次,“Cache Cache”这个名字在法语中是“捉迷藏”的意思。虽然说法语的人更有可能得到笑话/双关语,以及程序名称中的引用,但这绝对不是解决挑战的必要条件。 Insomni'hack 2024 CTF Teaser - Cache Cache

Links & Resources 链接和资源

原文始发于itm4n:Insomni’hack 2024 CTF Teaser – Cache Cache

版权声明:admin 发表于 2024年1月22日 下午6:59。
转载请注明:Insomni’hack 2024 CTF Teaser – Cache Cache | CTF导航

相关文章