[翻译]Windows内核ShellCode的动态加载和调试
Windbg下Windows内核ShellCode的加载和调试。DoublePulsar ShellCode调试实战。
在本篇文章中,我将分享一个可以从文件中加载一段shellcode到内核空间并且建立内核线程运行它的windbg脚本。由于我玩过的脚本并不是很多,所以如果有一些bug请告知我。
Windng加载shellcode以及创建线程运行该shellcode的脚本
你可以从我的github上找到这段脚本:
load_code_to_kernel_memory.wdbg
脚本的运行参数:
$$>a<load_code_to_kernel_memory.wdbg <src code> <mem size> <offset start routine>
第一个参数是含有shellcode的文件的路径。第二个参数是申请的内存大小(足够分配shellcode即可)。第三个参数是被执行的shellcode的起始偏移。
注意:包含shellcode的文件的大小应该被填充至内存分页的大小。我们是使用.readmem命令来加载shellcode,该命令将会读取一个0x1000字节大小的区块。举个例子,若你的shellcode有0x2800字节,那么.readmem将只会加载0x2000字节。所以你需要使用0x800附加的垃圾数据来补全文件以便加载完整的代码。
该脚本的简明说明
我们需要劫持一个正在运行的线程一小会。将该线程的执行重定向到ExAllocatePool来为shellcode申请内存(同时需要操作被劫持线程的栈来完成这个过程,之后再来恢复)
为此,我们需要在NtCreateFile中设置一个断点(非常频繁使用的一个API)。当线程停在该断点上的时候,就可以进行操作了:
$$首先在一个频繁使用的API上设置一个断点(这里的例子是NtCreateFile)以便劫持线程
ba e1 nt!CreateFile
g
.printf"${$arg1}"
.printf"${$arg2}"
.printf"${$arg3}"
bc *
$$保存为了调用ExAllocatePool个PsCreateSystemThread而即将被修改的原始的ESP寄存器以及栈中的参数
r @$t19 = (poi esp)
r @$t18 = (poi esp+4)
r @$t17 = (poi esp+8)
r @$t16 = (poi esp+c)
r @$t15 = (poi esp+10)
r @$t14 = (poi esp+14)
r @$t13 = (poi esp+18)
r @$t12 = (poi esp+1c)
r @$t11 = esp一旦建立线程之后,修改EIP以及栈信息来执行ExAllocatePool
$$修改ExAllocatePool执行所需要的栈参数
ed (esp+4) 0
ed (esp+8) ${$arg2}
$$劫持运行在NtCreateFile上的线程执行ExAllocatePool
u nt!ExAllocatePool
dd esp
r eip = nt!ExAllocatePool
$$步入,直到发现ret指令。我们无法执行步出(gu)命令,因为会产生一个
$$0x30 bugcheck,原因如下:
$$"当从陷阱中恢复上下文时,当检测到堆栈下溢时,会发生该错误检查。"
$$"有一个错误检查来验证当前的ESP是否小于保存在陷阱帧中的ESP"
$$"current_esp 是否小于 saved_esp"
.while(1)
{
p
r @$t10 = (poi eip)
r @$t10 = @$t10 & 0x000000ff
.if (@$t10 == 0xc2)
{
.break
}
}
r @$t0 = eax
.printf “allocated mem: %x\n”, @$t0此时,我们已经为shellcode分配了足够的内存空间,加载它吧:
$$从文件中加载shellcode到申请的内存中
$$careful: .readmem 将会读取0x1000大小的块.例如,文件有0x2800字节,
$$.readmem只会读取0x2000字节
$$你需要使用0x800附加的垃圾数据来补全文件以便加载完整的代码。
.readmem ${$arg1} @$t0
$$ @$t1 = allocated mem membase, code is read
r @$t1 = @$t0现在,我们希望在shellcode + arg3处创建一个线程:
$$此时,将在 @$t1 + arg3 (membase + startroutine_offset) 处建立内核线程
$$ NTSTATUS PsCreateSystemThread(
$$ _Out_ PHANDLE ThreadHandle,
$$ _In_ ULONG DesiredAccess,
$$ _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
$$ _In_opt_ HANDLE ProcessHandle,
$$ _Out_opt_ PCLIENT_ID ClientId,
$$ _In_ PKSTART_ROUTINE StartRoutine,
$$ _In_opt_ PVOID StartContext
$$ );
ed (esp+1c) 0
ed (esp+18) @$t1+${$arg3}
ed (esp+14) 0
ed (esp+10) 0
ed (esp+c) 0
ed (esp+8) 0
$$ThreadHandle, 我们使用未使用的参数StartContext的内存地址。
ed (esp+4) (esp+1c)
$$在即将被创建的线程中设置一个断点。
ba e1 @$t1+${$arg3}
u nt!PsCreateSystemThread
dd esp
r eip = nt!PsCreateSystemThread
$$again steps until ret instruction is found
.while (1)
{
p
r @$t10 = (poi eip)
r @$t10 = @$t10 & 0x000000ff
.if (@$t10 == 0xc2)
{
.break
}
}最后,我们恢复堆栈和eip以在NtCreateFile正确地继续执行被劫持的线程,否则系统将崩溃:
$$恢复原始寄存器及栈来继续无障碍的运行 r eip = nt!NtCreateFile r esp = @$t11 ed esp @$t19 ed (esp+4) @$t18 ed (esp+8) @$t17 ed (esp+c) @$t16 ed (esp+10) @$t15 ed (esp+14) @$t14 ed (esp+18) @$t13 ed (esp+1c) @$t12 g
在此之后,windbg应该在线程启动的shellcode的偏移处的断点处停止。
使用DoublePulsar shellcode来测试脚本
我们将使用从一个worm/ransom 恶意软件 i don’t want to remember的中提取的DoublePulsar Shellcode来测试脚本。
你可以在此处下载shellcode here (rar password: infected).
文件的大小是0x3000。我并没有深入的逆向shellcode,但开始调试的好起点看起来应该是0x221偏移处(稍后我们将会知道why)。
执行脚本,下变是打印的调试跟踪:
kd> $$>a<load_code_to_kernel_memory.wdbg shellcode.bin 3000 221 Breakpoint 0 hit <- nt!NtCreateFile hit nt!ExAllocatePool: 8261e976 8bff mov edi,edi 8261e978 55 push ebp 8261e979 8bec mov ebp,esp 8261e97b 684e6f6e65 push 656E6F4Eh 8261e980 ff750c push dword ptr [ebp+0Ch] 8261e983 ff7508 push dword ptr [ebp+8] 8261e986 e87a461100 call nt!ExAllocatePoolWithTag (82733005) 8261e98b 5d pop ebp 9e867d04 826511ea 00000000 00003000 040bfb54 <- stack for ExAllocatePool allocated mem: 849f4000 <- allocated memory Reading 10000 bytes…… <- .readmem shellcode nt!PsCreateSystemThread: 8281bfb6 8bff mov edi,edi 8281bfb8 55 push ebp 8281bfb9 8bec mov ebp,esp 8281bfbb 83e4f8 and esp,0FFFFFFF8h 8281bfbe 83ec34 sub esp,34h 8281bfc1 a148da7382 mov eax,dword ptr [nt!__security_cookie (8273da48)] 8281bfc6 33c4 xor eax,esp 8281bfc8 89442430 mov dword ptr [esp+30h],eax 9e867d04 826511ea 9e867d20 00000000 00000000 <- stack for PsCreateSystemThread 9e867d14 00000000 00000000 849f4221 00000000 9e867d24 00000001 00000060 00000000 00000000 Breakpoint 0 hit <- shellcode hit 849f4221 b923000000 mov ecx,23h 0: kd> u eip 849f4221 b923000000 mov ecx,23h 849f4226 6a30 push 30h 849f4228 0fa1 pop fs 849f422a 8ed9 mov ds,cx 849f422c 8ec1 mov es,cx 849f422e 648b0d40000000 mov ecx,dword ptr fs:[40h] 849f4235 8b6104 mov esp,dword ptr [ecx+4] 849f4238 ff35fcffdfff push dword ptr ds:[0FFDFFFFCh]
SYSENTER_EIP HOOK
可以看见新线程停在了我们指定的位置:

在0x20B偏移处可以看见shellcode hook了SYSENTER_EIP。
shellcode使用了地址0xFFDFFFFC来存储 MSR[0x176]。可以执行下边的命令:
1: kd> dt nt!_kuser_shared_data ffdf0000
Nt!_kuser_shared_data结构位于0xFFDF0000处,所以我猜测shellcode是使用了和nt!_kuser_shared_data的结构之后的位于同页的内存空间来存储其需要的临时变量。
关于通过MSR来HOOK系统调用可以参考下边这篇文章(非常有趣哦):
http://resources.infosecinstitute.com/hooking-system-calls-msrs
所以,0x221偏移是HOOK SYSENTER_EIP,因此,我认为这里是一个好的调试点。
然我们继续逆向 SYSENTER_EIP HOOK的代码:

在内核领空中,FS:[0]指向_KPCR结构。在这里,可以看见shellcode是如何从_KPCR中获取一些它所需要的值以及其他的结构体指针的过程。
fs:0x40 -> _KTSS
_KTSS + 4 -> Esp0 (correct Esp for continue executing in kernel)
nt!kuser_shared_data+0x304 -> SystemCallReturn
_KPCR + 0x1C -> SelfPcr (_KPCR)
SelfPcr + 0x120 -> PrcbData (_KPRCB)
PrcbData+0x4 -> CurrentThread (_KTHREAD)
CurrentThread+0x28 -> InitialStack
初始化完成之后,shellcode调用了HOOK的主代码,但是在这之前,它又恢复了MSR[0x176] (SYSENTER_EIP)。它已经有一个来自用户模式的线程,并且可能对它的目的而言已经足够了。
SHELLCODE 主代码
我并没有深入的调试这段shellcode,但是我们将会看一下shellcode主代码从SYSENTER_EIP HOOK处执行的第一部分。

以上可以看到shellcode是如何使用IDT中的指针来获取ntoskrnl.exe的地址。
以这种遍历内存空间直到找到PE头的方式,它可以找到ntoskrnl.exe的基址。
在找到基址之后,shellcode通过API的CRC(译者注:应该是HASH,更加短小精悍,而不是CRC):ExAllocatePool ExFreePool ZwQuerySystemInformation。
接下来,shellcode使用了ZwQuerySystemInformation来枚举所有的内核模块,来查找kdcom.dll(反调试?) 以及特定的srv.sys模块:

当shellcode找到srv.sys之后,将遍历该模块的PE区段,试图查找某些数据。
该shellcode是与SMB漏洞利用相结合的。以我的观点来看,现在它试图找到exploit发送的数据的其他部分(或许是一个要加载的PE文件)
总结
我没有接着调试下去,因为这只是测试脚本,并展示如何调试shellcode而不需要执行完整的exploit(有很多有关DoublePulsar的信息,例如:https://zerosum0x0.blogspot.com.es/2017/04/doublepulsar-initial-smb-backdoor-ring.html)
有时加载shellcode和调试比安装漏洞机器要快得多,包括执行漏洞,等等。。。
我希望这个脚本以及这篇文章将会对诸位的逆向工作有所帮助。
