首页
论坛
课程
招聘
[原创]【DLL注入编写与分析系列之一】x64平台SSDT HOOK之NtResumeThread注入
2022-2-7 21:29 8285

[原创]【DLL注入编写与分析系列之一】x64平台SSDT HOOK之NtResumeThread注入

2022-2-7 21:29
8285

1、前言

本文的思路、代码、调试参考了大量资料,历经艰难的调试后,现在可直接用VS2019在win7_sp1_x64系统上运行。
编写本文的目的,重点在于解释代码编写的思路,做到举一反三的目的,为软件逆向、代码HOOK打开思路,提升逆向代码编写水平。

2、前提知识

2.1 SSDT表

System Service Descript Table(SSDT):主要处理 Kernel32.dll中的系统调用,如openProcess,ReadFile等,主要在ntoskrnl.exe中实现(微软有给出 ntoskrnl源代码)
因为x64位中ssdt表是加密的,ssdt中的每一项占4个字节但并不是对应的系统服务的地址,因为x64中地址为64位而ssdt每一项只有4个字节32位所以无法直接存放服务的地址。其实际存储的4个字节的前28位表示的是对应的系统服务相对于SSDT表基地址的偏移,而后4位如果对应的服务的参数个数小于4则其值为0,不小于4则为参数个数减去4。所以我们在ssdt hook时向ssdt表项中填入的函数得在ntoskrnl.exe模块中,原因是因为函数到SSDT表基地址的偏移大小小于4个字节。所以我们选取一个ntoskrnl.exe中很少使用的函数KeBugCheckEx作为中转函数,将需要hook的ssdt项的改为KeBugCheckEx函数,然后在inlinehook KeBugCheck函数,jmp到我们的函数中进行过滤。

2.2 MSR寄存器

MSR(Model Specific Register)是x86架构中的概念,指的是在x86架构处理器中,一系列用于控制CPU运行、功能开关、调试、跟踪程序执行、监测CPU性能等方面的寄存器。
MSR寄存器的雏形开始于Intel 80386和80486处理器,到Intel Pentium处理器的时候,Intel就正式引入RDMSR和WRMSR两个指令用于读和写MSR寄存器,这个时候MSR就算被正式引入。在引入RDMSR和WRMSR指令的同时,也引入了CPUID指令,该指令用于指明具体的CPU芯片中,哪些功能是可用的,或者这些功能对应的MSR寄存器是否存在,软件可以通过CPUID指令查询某些功能是否在当前CPU上是否支持。
去AMD的官网,下载AMD芯片手册:
https://developer.amd.com/resources/developer-guides-manuals/
之后,查找MSRC000_0082,如下图:

可见,C000_0082是SYSCALL_Target_Address,在windbg里面,我们看看这个地址是什么。

1
2
3
4
5
6
7
8
9
10
11
12
5: kd> rdmsr c0000082
msr[c0000082] = fffff800`0408eec0
5: kd> uf fffff800`0408eec0
Flow analysis was incomplete, some code may be missing
nt!KiSystemCall64:
fffff800`0408eec0 0f01f8          swapgs
fffff800`0408eec3 654889242510000000 mov   qword ptr gs:[10h],rsp
fffff800`0408eecc 65488b2425a8010000 mov   rsp,qword ptr gs:[1A8h]
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
fffff800`0408eff2 4c8d1547a92300  lea  r10,[nt!KeServiceDescriptorTable (fffff800`042c9940)]
fffff800`0408eff9 4c8d1d80a92300  lea r11,[nt!KeServiceDescriptorTableShadow (fffff800`042c9980)]
fffff800`0408f000 f7830001000080000000 test dword ptr [rbx+100h],80h

KeServiceDescriptorTableShadow就是SSDT表的地址。记住这里,后面代码中要用到。

2.3 跳转指令

x64下面常用的跳转指令:
1、mov rax,addr;jmp rax
48 B8 qword_addr -> mov rax,qword_addr
FF E0 -> jmp rax
2、push / ret
68 dword_addr -> push dword_addr
c3 -> ret
注意:push的立即数是dword,但由于对齐的关系,实际占用64位。如果跳转地址超过2GB,要使用其他指令。push的立即数不能是64位。
3、jmp、call
FF15 dword_offset qword_addr -> call[qword_addr]
FF25 dword_offset qword_addr -> jmp [qword_addr]
简单说明下:
在下条指令偏移dword_offset处,是要跳转的地址qword_addr。当dword_offset=0x00000000时,qword_addr就在指令的第6个字节。
但是在x86下,:
FF15 dword_addr -> call [dword_addr]
FF25 dword_addr -> jmp [dword_addr]
没有偏移,后面直接跟着绝对地址。

2.4 shellcode执行出现movaps错误

我在写shellcode的时候,出现了movaps错误,一直以为代码有问题,后来经过排查、搜索,发现问题是由于在shellcode里面call函数之前,要保证rsp是16字节对齐的。我在这里耽误了一些时间的,你在使用的时候,尤其要注意。

2.5 为什么要自己实现GetModuleHandle和GetProcAddress

首先要明白,win7_sp1_x64下,CreateProcess的创建流程是这样的:

1
2
3
4
5
6
Kernel32!CreateProcess
Kernel32!CreateProcessW
Kernel32!CreateProcessInternalW
ntdll!NtCreateProcessEx
ntdll!NtCreateThread
ntdll!NtResumeThread

此时,主进程的大部分创建工作已经完成(尤其是ntdll.dll已经加载,尤其重要),主线程刚刚被创建,此时还没有加载除ntdll.dll之外的其他dll。而GetModuleHandle和GetProcAddress函数在kernel32.dll里,所以需要自己去实现这两个函数,获取dll的句柄和函数地址。kernel32.dll是在进程创建完成、主线程初始化输入表时才载入的。

2.6 实现GetModuleHandle和GetProcAddress概略思路

###2.6.1 GetModuleHandle实现
GetModuleHandle是为了获取dll基址,现在要获取ntdll.dll基址,是通过暴力搜索内存实现的。
主要用到的两个函数是:
ZwQueryVirtualMemory
ZwQuerySystemInformation
其中,
ZwQuerySystemInformation用到的类型是SystemEmulationBasicInformation。很重要,我找了很多资料,才找到这个参数。
ZwQueryVirtualMemory用到的类型是MemoryBasicInformation,和另外一个未文档化的参数MEMORY_SECTION_NAME。

 

此时要注意的是x64和x86获取系统的信息稍微有所不同:
x86:ZwQuerySystemInformation第一个参数是SystemBasicInformation

 

###2.6.1 GetProcAddress实现
导出表有一个结构,是IMAGE_EXPORT_DIRECTORY,主要注意里面的三个字段:
1、AddressOfNames
2、AddressOfNameOrdinals
3、AddressOfFunctions
4、NumberOfNames

代码实现的基本思路是:
循环NumberOfNames次,在AddressOfName地址里面查找targetFunc,记住找到函数时的循环次数i,通过AddressOfNameOrdinals找到函数的索引号,比如是b,最后函数地址为AddressOfFunctions[b]。
代码基本流程如下:

1
ImageExportDirectory=NtHeader.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
1
2
3
AddressOfFunctions =MappingBaseAddress + ImageExportDirectory->AddressOfFunctions);
AddressOfNames = MappingBaseAddress + ImageExportDirectory->AddressOfNames);
AddressOfNameOrdinals =MappingBaseAddress+ImageExportDirectory->AddressOfNameOrdinals);

通过循环ImageExportDirectory->NumberOfNames次,比较名字是否和NtResumeThread相等,不等于循环,等于的话,就通过AddressOfNameOrdinals[i] 得到函数索引FunctionOrdinal,然后函数地址就等于
FunctionAddress = (PVOID)((UINT8*)MappingBaseAddress + AddressOfFunctions[FunctionOrdinal])

 

这里的实现是为了得到ntdll!ldrLoadDLL函数地址。

2.7 需要干掉PageGuard

2、代码编写核心思路

Hook函数nt!NtResumeThread的SSDT表,修改为KeBugCheckEx函数地址,然后InLine HOOK到FakeResumeThead函数去执行,最后执行shellcode。

3、代码编写详细思路

3.1 获取SSDT表地址

1
2
3
4
5
6
7
8
kd> rdmsr c0000082
   msr[c0000082] = f ffff800`04038bc0
   kd> uf ffff800`04038bc0
   nt!KiSystemCall64:
   ......
   nt!KiSystemServiceRepeat:
   fffff800`03f07a72 4c8d1587be1f00  lea     r10,[nt!KeServiceDescriptorTable (fffff800`040be940)]
   fffff800`03f07a79 4c8d1d40bf1f00  lea     r11,[nt!KeServiceDescriptorTableShadow (fffff800`040be980)]

通过PUCHAR StartSearchAddress =(PUCHAR)__readmsr(0xC0000082);作为起始地址,然后在PAGE_SIZE范围搜索特征码(4c\8d\15),然后地址加上偏移就是SSDT表地址了。

3.2 获取NtResumeThread函数在SSDT表里面的索引

在win7_sp1_x64系统,反汇编ntdll可知:

 

 

从函数地址偏移4个或1个字节得到此函数在SSDT表的索引。

3.3 计算原始函数地址

原始函数地址 = SSDT基址 + 函数索引号得到的偏移地址>>4

1
2
__OldNtResumeThreadAddress =
SSDTAddress->ServiceTableBase+SSDTAddress->ServiceTableBase[_FunctionIndexInSSDT]>>4

3.4 HOOK SSDT 表

A、通过__readcr0寄存器关闭写保护
B、InlineHook:把KeBugCheck的前面14个字节保存,然后改成跳转到shellcode执行。跳转汇编:
"\xFF\x25\x00\x00\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF";
C、计算KeBugCheck在SSDT表中基于ServerTableBase的偏移值,然后填入之前NtResumeThread的索引位置。
这样,执行NtResumeThread函数,实际先执行KeBugCheck函数,又由于KeBugCheck被HOOK了,在KeBugCheck函数里面执行shellcode。

3.5 关于FakeNtResumeThread

在这个函数里,我们主要做的事情如下:
1、通过PsGetProcessImageFileName函数,获取要注入进程的名字;
2、暴力搜索内存,获得ntdll.dll的基址,通过导出表获取ldrLoadDLL地址;
2、通过KeStackAttachProcess函数附加到进程;
3、通过PsGetContextThread函数获取进程的rip
4、通过PsSetContextThread函数修改进程的rip。

3.6 关于shellcode

shellcode结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct _INJECT_DATA
{
    CHAR ShellCode[0xA0];
    /*offset = 0xA0*/PWCHAR PathToFile;//LdrLoadDll的第一个参数
    /*offset = 0xA8*/ULONG64 DllCharacteristics;
    /*offset = 0xB0*/PUNICODE_STRING pDllPath;//PUNICODE_STRING DllPath
    /*offset = 0xB8*/PHANDLE ModuleHandle; //Dll句柄
    /*offset = 0xC0*/ULONG64 AddrOfLdrLoadDll;//LdrLoadDll地址
    /*offset = 0xC8*/ULONG64 OriginalEIP;//原线程的EIP
    /*offset = 0xD0*/UNICODE_STRING usDllPath;//Dll路径
    /*offset = 0xE0*/WCHAR wDllPath[256];//Dll路径,也就是usDllPath中的Buffer
}INJECT_DATA;

汇编代码如下:

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
48
49
50
51
push rax  
push rax  
push rax  
push rbx
push rcx
push rdx
push rbp
push rsp
push rsi
push rdi
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
pushfq 
call @Next
@Next:
pop rbx
and bx,0
mov rax,qword ptr ds:[rbx+0xc8]   
xchg rax,qword ptr ds:[rsp+0x90
sub rsp,0x30           
mov r9,qword ptr ds:[rbx+0xb8]   
mov r8,qword ptr ds:[rbx+0xb0]
mov rdx,qword ptr ds:[rbx+0xa8]  
xor rcx,rcx
call [rbx+0xc0]                   
add rsp,0x30
popfq
pop r15
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rdi
pop rsi
pop rsp
pop rbp
pop rdx
pop rcx
pop rbx
pop rax
pop rax
ret

主要功能是:
1、加载要注入的dll;
2、通过ret指令返回之前线程的rip,跳转到之前线程的指令处执行;
3、重点是,call函数的时候rsp要16字节对齐,否则会出现movaps错误;
4、之所以push3个rax,一个是用于占位,一个是用于让rsp实现16字节对齐,一个是保存rax寄存器。

4、关于代码

代码暂不上传。已经把关键点说得很详细了,我相信根据看雪x86的代码,你也能够写出x64系统下的代码了。当然,贴子如果精华了,或者需要的同学很多,再上传代码。


[2022夏季班]《安卓高级研修班(网课)》月薪三万班招生中~

最后于 2022-2-9 09:50 被ExploitCN编辑 ,原因:
收藏
点赞4
打赏
分享
最新回复 (10)
雪    币: 1595
活跃值: 活跃值 (1850)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
はつゆき 活跃值 2022-2-8 08:28
2
0
写的太好了,精华!
雪    币: 5878
活跃值: 活跃值 (4977)
能力值: ( LV9,RANK:310 )
在线值:
发帖
回帖
粉丝
PlaneJun 活跃值 6 2022-2-8 08:31
3
0
写的太好了,精华!
雪    币: 654
活跃值: 活跃值 (554)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
qdjytony 活跃值 2022-2-8 11:48
4
0
写的太好了,精华!
雪    币: 54
活跃值: 活跃值 (81)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
阮光全 活跃值 2022-2-8 14:00
5
0
写的太好了,精华!
雪    币: 286
活跃值: 活跃值 (171)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
A-王浩 活跃值 2022-2-8 17:19
6
0
写的太好了,精华!
雪    币: 1355
活跃值: 活跃值 (2957)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
风中小筑V 活跃值 2022-2-8 21:13
7
0
写的太好了,精华!
雪    币: 3821
活跃值: 活跃值 (1353)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
小菜鸟一 活跃值 2022-2-9 08:47
8
0
写的太好了,精华!
雪    币: 228
活跃值: 活跃值 (1356)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
杰克王 活跃值 2022-2-11 13:04
9
0
写的太好了,精华!
雪    币: 4828
活跃值: 活跃值 (853)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ldljlzw 活跃值 2022-6-14 09:36
10
0
写的太好了,精华!
雪    币: 1904
活跃值: 活跃值 (596)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
樂樂 活跃值 2天前
11
0
写的太好了,精华!上代码了
游客
登录 | 注册 方可回帖
返回