BreizhCTF 2024 - New world 2 (misc)
BreizhCTF 2024 - New world 2 (misc)
The challenge consists in a tcp service that sends you 10 ELF binaries which call custom syscalls and ask you to provide the binary execution output.
The following instructions are given but you don’t even need to read it. Or at least, at the end of the challenge.
# New World - Partie 2
## Informations de base
- L'architecture du CPU est amd64.
- Les binaires ELF sont envoyés par le serveur en base64
- Votre objectif est de déterminer la sortie du binaire sur la machine distante dix fois à la suite.
- La sortie attendue est donnée si votre entrée est incorrecte.
- Vous trouverez en dessous la documentation trouvée pour les syscalls à implémenter
- Vous n'avez que quelques secondes pour répondre
## Syscalls New World
### Custom mmap - numéro 31337
Créer une zone mémoire à l'adresse 0x13370000 de taille 0x10000
### md5sum - numéro 31338
Réalise le condensat MD5 d'une zone mémoire.
Paramètres :
- x64 : rdi - zone sur laquelle est réalisée le condensat
- x64 : rsi - zone de destination du condensat
- x64 : rbx - longueur à condenser
### sha1sum - numéro 31339
Réalise le condensat SHA1 d'une zone mémoire.
Paramètres :
- x64 : rdi - zone sur laquelle est réalisée le condensat
- x64 : rsi - zone de destination du condensat
- x64 : rbx - longueur à condenser
### sha256sum - numéro 31340
Réalise le condensat SHA256 d'une zone mémoire.
Paramètres :
- x64 : rdi - zone sur laquelle est réalisée le condensat
- x64 : rsi - zone de destination du condensat
- x64 : rbx - longueur à condenser
### write modifié - numéro 31341
Réalise un affichage sur la sortie avec un encodage en fonction d'un index passé en paramètre.
Paramètres :
- rdi - zone à afficher
- rsi - longueur de la zone prise en compte pour l'affichage (avant encodage)
- rbx - index pour l'encodage
- 1 : base64
- 2 : base32
- 3 : base16
- 4 : base85Looking at the first binary sent show only one section called
.shellcode that hold the instructions to execute.
$ objdump -d -s .shellcode <binary> -M intel
Disassembly of section .shellcode:
0000000000401000 <.shellcode>:
401000: 48 c7 c0 69 7a 00 00 mov rax,0x7a69
401007: 0f 05 syscall
401009: 48 c7 c2 00 00 37 13 mov rdx,0x13370000
401010: 48 be 88 ec 82 60 48 movabs rsi,0x46ccb4486082ec88
401017: b4 cc 46
40101a: 48 89 32 mov QWORD PTR [rdx],rsi
40101d: 48 83 c2 08 add rdx,0x8
401021: 48 be 4f 3c 09 ff a7 movabs rsi,0xd49ee9a7ff093c4f
401028: e9 9e d4
40102b: 48 89 32 mov QWORD PTR [rdx],rsi
40102e: 48 83 c2 08 add rdx,0x8
401032: 48 be 23 5d d3 01 5c movabs rsi,0x79afe65c01d35d23
401039: e6 af 79
40103c: 48 89 32 mov QWORD PTR [rdx],rsi
40103f: 48 83 c2 08 add rdx,0x8
401043: 48 be 97 d2 0a 90 3f movabs rsi,0x9ba1a73f900ad297
40104a: a7 a1 9b
40104d: 48 89 32 mov QWORD PTR [rdx],rsi
401050: 48 83 c2 08 add rdx,0x8
401054: 48 be f2 ca 40 bf 5c movabs rsi,0x8dbfd45cbf40caf2
40105b: d4 bf 8d
40105e: 48 89 32 mov QWORD PTR [rdx],rsi
401061: 48 83 c2 08 add rdx,0x8
401065: 48 be bc 55 1b a1 fc movabs rsi,0x462596fca11b55bc
[...]
40164a: 0f 05 syscall
40164c: 48 83 c6 04 add rsi,0x4
401650: 48 c7 c0 6b 7a 00 00 mov rax,0x7a6b
401657: 0f 05 syscall
401659: 48 83 c6 04 add rsi,0x4
40165d: 48 c7 c0 6a 7a 00 00 mov rax,0x7a6a
401664: 0f 05 syscall
401666: 48 83 c6 04 add rsi,0x4
40166a: 48 c7 c0 6c 7a 00 00 mov rax,0x7a6c
401671: 0f 05 syscall
401673: 48 83 c6 04 add rsi,0x4
401677: 48 c7 c0 6c 7a 00 00 mov rax,0x7a6c
40167e: 0f 05 syscall
401680: 48 83 c6 04 add rsi,0x4
401684: 48 c7 c0 6a 7a 00 00 mov rax,0x7a6a
40168b: 0f 05 syscall
40168d: 48 83 c6 04 add rsi,0x4
401691: 48 c7 c0 6b 7a 00 00 mov rax,0x7a6b
401698: 0f 05 syscall
40169a: 48 83 c6 04 add rsi,0x4
40169e: 48 c7 c7 00 50 37 13 mov rdi,0x13375000
4016a5: 48 c7 c6 00 04 00 00 mov rsi,0x400
4016ac: 48 c7 c3 03 00 00 00 mov rbx,0x3
4016b3: 48 c7 c0 6d 7a 00 00 mov rax,0x7a6d
4016ba: 0f 05 syscall
4016bc: 48 c7 c0 3c 00 00 00 mov rax,0x3c
4016c3: 0f 05 syscall
...The challenge maker want us to implement some custom syscalls that perform hash digest and baseXX encoding.
The others instructions of the binary are just here to put data in the stack and in some memory space.
Syscalls are just fancies functions calls in kernel land. However for this kind of challenge it would be overkill to implement it.
The most straight forward idea I got was to emulate the whole binary and when a syscall is made just handle it within the emulation program. For this I went with Unicorn Engine
The first part was to extract the .shellcode section
from the ELF format. For this I used my python lib https://github.com/0xswitch/Hellf
:
from Hellf import ELF
from pwn import remote
p = remote("challenge.ctf.bzh", 30342)
p.recvuntil(" :")
chall = base64.b64decode(p.recvuntil("----").split(b"\n")[1])
helf = ELF(chall)
BYTECODE = helf.get_section_by_name(".shellcode").dataThen we can just run it without any syscall handling to observe its behavior.
from unicorn import *
from unicorn.x86_const import *
from Hellf import ELF
BASE = 0x4001000 # base addr for the execution
STACK_ADDR = 0x0 # stack will be at 0 because why not
STACK_SIZE = 1024 * 1024
mu = Uc(UC_ARCH_X86, UC_MODE_64)
mu.mem_map(BASE, 1024 * 1024) # always need to be divisble by 1024
helf = ELF("./elf")
BYTECODE = helf.get_section_by_name(".shellcode").data
mu.mem_write(BASE, BYTECODE) # writing our instructions in memory
mu.reg_write(UC_X86_REG_EBP, STACK_ADDR) # setup the stack
mu.reg_write(UC_X86_REG_ESP, STACK_ADDR)
def hook_code(mu, address, size, user_data): # hook every instructions to display every PC
op = mu.mem_read(address, size)
print('[ 0x%x ] size = 0x%x' %(address, size))
mu.hook_add(UC_HOOK_CODE, hook_code)
mu.emu_start(BASE, BASE + len(BYTECODE))The first execution yeet this error :
$ python xploit.py
[ 0x4001000 ] size = 0x7
[ 0x4001007 ] size = 0x2
[ 0x4001009 ] size = 0x7
[ 0x4001010 ] size = 0xa
[ 0x400101a ] size = 0x3
Traceback (most recent call last):
File "/home/switch/bzhctf/new-world/wu.py", line 127, in <module>
mu.emu_start(BASE, BASE + len(BYTECODE))
File "/usr/lib/python3.11/site-packages/unicorn/unicorn.py", line 547, in emu_start
raise UcError(status)
unicorn.unicorn.UcError: Invalid memory write (UC_ERR_WRITE_UNMAPPED)The instruction responsible of this failure is
mov QWORD PTR [rdx],rsi when rdx is
0x13370000. This happen because we did not tell to Unicorn
to map this memory. Than can be fixed with :
mu.mem_map(0x13370000, 1024 * 1024)With this modification the next execution is flawless. But without the syscalls implementations, the binary will miss the hash and encoding operations. Let’s implement it.
The easiest way is to hook every instruction and check if a syscall
is made (opcode \x0f\x05) and check rax value
for the syscall number.
def hook_code(mu, address, size, user_data):
op = mu.mem_read(address, size)
print('[ 0x%x ] size = 0x%x' %(address, size))
if op == b"\x0f\x05":
syscall_number = mu.reg_read(UC_X86_REG_EAX)
if syscall_number == 31337:
print("syscall: 31337")
elif syscall_number == 31338:
print("syscall: 31338")
syscall_hash(mu, hashlib.md5)
elif syscall_number == 31339:
print("syscall: 31339")
syscall_hash(mu, hashlib.sha1)
elif syscall_number == 31340:
print("syscall: 31340")
syscall_hash(mu, hashlib.sha256)
elif syscall_number == 31341:
print("syscall: 31341")
s31341(mu)
elif syscall_number == 60:
print("EXIT")
p.sendline(output)
mu.emu_stop()
else:
print(syscall_number)
print("WTF")
exit()The implementation of syscall 31333 is not need as its
only purpose is to map 0x13370000 which is already done
when we initialize unicorn.
Syscalls 31338, 31339 and
31340 are the same as the used registers are identical,
only the called function will differs :
def syscall_hash(mu, hash_func):
rdi = mu.reg_read(UC_X86_REG_RDI) # getting the registers value
rsi = mu.reg_read(UC_X86_REG_RSI)
rbx = mu.reg_read(UC_X86_REG_RBX)
print(hex(rdi), hex(rsi), hex(rbx))
data = mu.mem_read(rdi, rbx) # reading the value to hash (rdi is the address and rsi its size as stated in the doc)
res = hash_func(data).digest() # call the digest
mu.mem_write(rsi, res) # write the result to the address in rbxThat’s all, the final syscall is 31341 which consists in
a base encoding on some memory :
def s31341(mu):
rdi = mu.reg_read(UC_X86_REG_RDI) # address to read
rsi = mu.reg_read(UC_X86_REG_RSI) # size to read
rbx = mu.reg_read(UC_X86_REG_RBX) # encoding to use
print(hex(rdi), hex(rsi), hex(rbx))
data = mu.mem_read(rdi, rsi)
if rbx == 1:
func = base64.b64encode
if rbx == 2:
func = base64.b32encode
if rbx == 3:
func = base64.b16encode
if rbx == 4:
func = base64.b85encode
output = func(data)The execution will produce something like that should be sent to the server :
CA1A29CACA1A29CACA1A29CA35392C6735392C67CA1A29CACA1A29CAC6BBCA7BCA1A29CA35392C67CA1A29CACA1A29CA35392C6735392C67C6BBCA7B35392C67C6BBCA7BCA1A29CA35392C67CA1A29CA35392C67C6BBCA7BC6BBCA7BC6BBCA7B35392C67CA1A29CA35392C67CA1A29CA35392C67C6BBCA7B35392C6735392C67CA1A29CAC6BBCA7BC6BBCA7BCA1A29CA35392C67CA1A29CA35392C67CA1A29CACA1A29CA35392C67C6BBCA7BC6BBCA7BC6BBCA7BC6BBCA7BC6BBCA7BCA1A29CACA1A29CA35392C67CA1A29CAC6BBCA7BC6BBCA7BCA1A29CA35392C679FD5F3EE033A71A4532C770F00B7F8684BD7DAC200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Sending this to the server 10 times and get the flag !
Full code :
import hashlib
import base64
from unicorn import *
from unicorn.x86_const import *
from pwn import remote
from Hellf import ELF
BASE = 0x4001000
STACK_ADDR = 0x0
STACK_SIZE = 1024 * 1024
p = remote("challenge.ctf.bzh", 30342)
output = ""
def hook_code(mu, address, size, user_data):
op = mu.mem_read(address, size)
# print('[ 0x%x ] size = 0x%x' %(address, size))
if op == b"\x0f\x05":
syscall_number = mu.reg_read(UC_X86_REG_EAX)
if syscall_number == 31337:
print("syscall: 31337")
elif syscall_number == 31338:
print("syscall: 31338")
syscall_hash(mu, hashlib.md5)
elif syscall_number == 31339:
print("syscall: 31339")
syscall_hash(mu, hashlib.sha1)
elif syscall_number == 31340:
print("syscall: 31340")
syscall_hash(mu, hashlib.sha256)
elif syscall_number == 31341:
print("syscall: 31341")
output = s31341(mu)
print(output)
elif syscall_number == 60:
print("EXIT")
p.sendline(output)
p.interactive()
mu.emu_stop()
else:
print(syscall_number)
print("WTF")
exit()
def syscall_hash(mu, hash_func):
rdi = mu.reg_read(UC_X86_REG_RDI)
rsi = mu.reg_read(UC_X86_REG_RSI)
rbx = mu.reg_read(UC_X86_REG_RBX)
print(hex(rdi), hex(rsi), hex(rbx))
data = mu.mem_read(rdi, rbx)
res = hash_func(data).digest()
mu.mem_write(rsi, res)
def s31341(mu):
rdi = mu.reg_read(UC_X86_REG_RDI)
rsi = mu.reg_read(UC_X86_REG_RSI)
rbx = mu.reg_read(UC_X86_REG_RBX)
print(hex(rdi), hex(rsi), hex(rbx))
data = mu.mem_read(rdi, rsi)
if rbx == 1:
func = base64.b64encode
if rbx == 2:
func = base64.b32encode
if rbx == 3:
func = base64.b16encode
if rbx == 4:
func = base64.b85encode
return func(data)
while True:
mu = Uc(UC_ARCH_X86, UC_MODE_64)
mu.mem_map(BASE, 1024 * 1024) # always need to be divisble by 1024
mu.mem_map(0x13370000, 1024 * 1024) # always need to be divisble by 1024
p.recvuntil(" :")
chall = base64.b64decode(p.recvuntil("----").split(b"\n")[1])
helf = ELF(chall)
BYTECODE = helf.get_section_by_name(".shellcode").data
mu.mem_write(BASE, BYTECODE)
mu.reg_write(UC_X86_REG_EBP, STACK_ADDR)
mu.reg_write(UC_X86_REG_ESP, STACK_ADDR)
mu.hook_add(UC_HOOK_CODE, hook_code)
mu.emu_start(BASE, BASE + len(BYTECODE))
10955