DynELF的应用

前言

原文:http://bobao.360.cn/learning/detail/3298.html

1
在没有目标系统libc文件的情况下,我们可以使用pwntools的DynELF模块来泄漏地址信息,从而获取到shell。本文针对linux下的puts和write,分别给出了实现DynELF关键函数leak的方法,并通过3道CTF题目介绍了这些方法的具体应用情况。

DynELF

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
DynELF是pwntools中专门用来应对无libc情况的漏洞利用模块,其基本代码框架如下。

p = process('./xxx')


def leak(address):


#各种预处理


payload = "xxxxxxxx" + address + "xxxxxxxx"


p.send(payload)


#各种处理


data = p.recv(4)


log.debug("%#x => %s" % (address, (data or '').encode('hex')))


return data


d = DynELF(leak, elf=ELF("./xxx")) #初始化DynELF模块


systemAddress = d.lookup('system', 'libc') #在libc文件中搜索system函数的地址

需要使用者进行的工作主要集中在leak函数的具体实现上,上面的代码只是个模板。其中,address就是leak函数要泄漏信息的所在地址,而payload就是触发目标程序泄漏address处信息的攻击代码。

使用条件

1
2
3
4
5
6
不管有没有libc文件,要想获得目标系统的system函数地址,首先都要求目标二进制程序中存在一个能够泄漏目标系统内存中libc空间内信息的漏洞。同时,由于我们是在对方内存中不断搜索地址信息,故我们需要这样的信息泄露漏洞能够被反复调用。以下是大致归纳的主要使用条件:
1)目标程序存在可以泄露libc空间信息的漏洞,如read@got就指向libc地址空间内;

2)目标程序中存在的信息泄露漏洞能够反复触发,从而可以不断泄露libc地址空间内的信息。

当然,以上仅仅是实现利用的基本条件,不同的目标程序和运行环境都会有一些坑需要绕过。接下来,我们主要针对write和puts这两个普遍用来泄漏信息的函数在实际配合DynELF工作时可能遇到的问题,给出相应的解决方法。

write函数

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
write函数原型是write(fd, addr, len),即将addr作为起始地址,读取len字节的数据到文件流fd(0表示标准输入流stdin、1表示标准输出流stdout)。write函数的优点是可以读取任意长度的内存信息,即它的打印长度只受len参数控制,缺点是需要传递3个参数,特别是在x64环境下,可能会带来一些困扰。
在x64环境下,函数的参数是通过寄存器传递的,rdi对应第一个参数,rsi对应第二个参数,rdx对应第三个参数,往往凑不出类似“pop rdi; ret”、“pop rsi; ret”、“pop rdx; ret”等3个传参的gadget。此时,可以考虑使用__libc_csu_init函数的通用gadget,具体原理请参见文章。简单的说,就是通过__libc_csu_init函数的两段代码来实现3个参数的传递,这两段代码普遍存在于x64二进制程序中,只不过是间接地传递参数,而不像原来,是通过pop指令直接传递参数。

.text:000000000040075A pop rbx #需置为0,为配合第二段代码的call指令寻址


.text:000000000040075B pop rbp #需置为1


.text:000000000040075C pop r12 #需置为要调用的函数地址,注意是got地址而不是plt地址,因为第二段代码中是call指令


.text:000000000040075E pop r13 #write函数的第三个参数


.text:0000000000400760 pop r14 #write函数的第二个参数


.text:0000000000400762 pop r15 #write函数的第一个参数


.text:0000000000400764 retn

第二段代码如下

.text:0000000000400740 mov rdx, r13


.text:0000000000400743 mov rsi, r14


.text:0000000000400746 mov edi, r15d


.text:0000000000400749 call qword ptr [r12+rbx*8]

这两段代码运行后,会将栈顶指针移动56字节,我们在栈中布置56个字节即可。

这样,我们便解决了write函数在leak信息中存在的问题,具体的应用会放到后面的3道题目中讲。

puts函数

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
puts的原型是puts(addr),即将addr作为起始地址输出字符串,直到遇到“\x00”字符为止。也就是说,puts函数输出的数据长度是不受控的,只要我们输出的信息中包含\x00截断符,输出就会终止,且会自动将“\n”追加到输出字符串的末尾,这是puts函数的缺点,而优点就是需要的参数少,只有1个,无论在x32还是x64环境下,都容易调用。
为了克服输入不受控这一缺点,我们考虑利用puts函数输出的字符串最后一位为“\n“这一特点,分两种情况来解决。

(1)puts输出完后就没有其他输出,在这种情况下的leak函数可以这么写。

def leak(address):


count = 0


data = ''


payload = xxx


p.send(payload)


print p.recvuntil('xxx\n') #一定要在puts前释放完输出


up = ""


while True:


#由于接收完标志字符串结束的回车符后,就没有其他输出了,故先等待1秒钟,如果确实接收不到了,就说明输出结束了


#以便与不是标志字符串结束的回车符(0x0A)混淆,这也利用了recv函数的timeout参数,即当timeout结束后仍得不到输出,则直接返回空字符串””


c = p.recv(numb=1, timeout=1)


count += 1


if up == '\n' and c == "": #接收到的上一个字符为回车符,而当前接收不到新字符,则


buf = buf[:-1] #删除puts函数输出的末尾回车符


buf += "\x00"


break


else:


buf += c


up = c


data = buf[:4] #取指定字节数


log.info("%#x => %s" % (address, (data or '').encode('hex')))


return 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
52
53
54
55
56
57
58
59
60
puts输出完后还有其他输出,在这种情况下的leak函数可以这么写。

def leak(address):


count = 0


data = ""


payload = xxx


p.send(payload)


print p.recvuntil("xxx\n")) #一定要在puts前释放完输出


up = ""


while True:


c = p.recv(1)


count += 1


if up == '\n' and c == "x": #一定要找到泄漏信息的字符串特征


data = buf[:-1]


data += "\x00"


break


else:


buf += c


up = c


data = buf[:4]


log.info("%#x => %s" % (address, (data or '').encode('hex')))


return data

其他需要注意的

1
在信息泄露过程中,由于循环制造溢出,故可能会导致栈结构发生不可预料的变化,可以尝试调用目标二进制程序的_start函数来重新开始程序以恢复栈
0%