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 : base85
Looking at the first binary sent show only one section called
.shellcode
that hold the instructions to execute.
-d -s .shellcode <binary> -M intel
$ objdump
.shellcode:
Disassembly of section
>:
0000000000401000 <.shellcode69 7a 00 00 mov rax,0x7a69
401000: 48 c7 c0 syscall
401007: 0f 05 00 00 37 13 mov rdx,0x13370000
401009: 48 c7 c2 88 ec 82 60 48 movabs rsi,0x46ccb4486082ec88
401010: 48 be 46
401017: b4 cc mov QWORD PTR [rdx],rsi
40101a: 48 89 32 08 add rdx,0x8
40101d: 48 83 c2 4f 3c 09 ff a7 movabs rsi,0xd49ee9a7ff093c4f
401021: 48 be 9e d4
401028: e9 mov QWORD PTR [rdx],rsi
40102b: 48 89 32 08 add rdx,0x8
40102e: 48 83 c2 23 5d d3 01 5c movabs rsi,0x79afe65c01d35d23
401032: 48 be 79
401039: e6 af mov QWORD PTR [rdx],rsi
40103c: 48 89 32 08 add rdx,0x8
40103f: 48 83 c2 97 d2 0a 90 3f movabs rsi,0x9ba1a73f900ad297
401043: 48 be 9b
40104a: a7 a1 mov QWORD PTR [rdx],rsi
40104d: 48 89 32 08 add rdx,0x8
401050: 48 83 c2 40 bf 5c movabs rsi,0x8dbfd45cbf40caf2
401054: 48 be f2 ca 8d
40105b: d4 bf mov QWORD PTR [rdx],rsi
40105e: 48 89 32 08 add rdx,0x8
401061: 48 83 c2 55 1b a1 fc movabs rsi,0x462596fca11b55bc
401065: 48 be bc
[...]syscall
40164a: 0f 05 04 add rsi,0x4
40164c: 48 83 c6 6b 7a 00 00 mov rax,0x7a6b
401650: 48 c7 c0 syscall
401657: 0f 05 04 add rsi,0x4
401659: 48 83 c6 6a 7a 00 00 mov rax,0x7a6a
40165d: 48 c7 c0 syscall
401664: 0f 05 04 add rsi,0x4
401666: 48 83 c6 6c 7a 00 00 mov rax,0x7a6c
40166a: 48 c7 c0 syscall
401671: 0f 05 04 add rsi,0x4
401673: 48 83 c6 6c 7a 00 00 mov rax,0x7a6c
401677: 48 c7 c0 syscall
40167e: 0f 05 04 add rsi,0x4
401680: 48 83 c6 6a 7a 00 00 mov rax,0x7a6a
401684: 48 c7 c0 syscall
40168b: 0f 05 04 add rsi,0x4
40168d: 48 83 c6 6b 7a 00 00 mov rax,0x7a6b
401691: 48 c7 c0 syscall
401698: 0f 05 04 add rsi,0x4
40169a: 48 83 c6 00 50 37 13 mov rdi,0x13375000
40169e: 48 c7 c7 00 04 00 00 mov rsi,0x400
4016a5: 48 c7 c6 03 00 00 00 mov rbx,0x3
4016ac: 48 c7 c3 6d 7a 00 00 mov rax,0x7a6d
4016b3: 48 c7 c0 syscall
4016ba: 0f 05 3c 00 00 00 mov rax,0x3c
4016bc: 48 c7 c0 syscall
4016c3: 0f 05 ...
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
= remote("challenge.ctf.bzh", 30342)
p " :")
p.recvuntil(= base64.b64decode(p.recvuntil("----").split(b"\n")[1])
chall
= ELF(chall)
helf = helf.get_section_by_name(".shellcode").data BYTECODE
Then 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
= 0x4001000 # base addr for the execution
BASE = 0x0 # stack will be at 0 because why not
STACK_ADDR = 1024 * 1024
STACK_SIZE
= Uc(UC_ARCH_X86, UC_MODE_64)
mu
1024 * 1024) # always need to be divisble by 1024
mu.mem_map(BASE,
= ELF("./elf")
helf
= helf.get_section_by_name(".shellcode").data
BYTECODE
# writing our instructions in memory
mu.mem_write(BASE, BYTECODE) # setup the stack
mu.reg_write(UC_X86_REG_EBP, STACK_ADDR)
mu.reg_write(UC_X86_REG_ESP, STACK_ADDR)
def hook_code(mu, address, size, user_data): # hook every instructions to display every PC
= mu.mem_read(address, size)
op print('[ 0x%x ] size = 0x%x' %(address, size))
mu.hook_add(UC_HOOK_CODE, hook_code)+ len(BYTECODE)) mu.emu_start(BASE, BASE
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 :
0x13370000, 1024 * 1024) mu.mem_map(
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):
= mu.mem_read(address, size)
op print('[ 0x%x ] size = 0x%x' %(address, size))
if op == b"\x0f\x05":
= mu.reg_read(UC_X86_REG_EAX)
syscall_number
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):
= mu.reg_read(UC_X86_REG_RDI) # getting the registers value
rdi = mu.reg_read(UC_X86_REG_RSI)
rsi = mu.reg_read(UC_X86_REG_RBX)
rbx
print(hex(rdi), hex(rsi), hex(rbx))
= mu.mem_read(rdi, rbx) # reading the value to hash (rdi is the address and rsi its size as stated in the doc)
data = hash_func(data).digest() # call the digest
res # write the result to the address in rbx mu.mem_write(rsi, res)
That’s all, the final syscall is 31341
which consists in
a base encoding on some memory :
def s31341(mu):
= mu.reg_read(UC_X86_REG_RDI) # address to read
rdi = mu.reg_read(UC_X86_REG_RSI) # size to read
rsi = mu.reg_read(UC_X86_REG_RBX) # encoding to use
rbx
print(hex(rdi), hex(rsi), hex(rbx))
= mu.mem_read(rdi, rsi)
data
if rbx == 1:
= base64.b64encode
func if rbx == 2:
= base64.b32encode
func if rbx == 3:
= base64.b16encode
func if rbx == 4:
= base64.b85encode
func
= func(data) output
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
= 0x4001000
BASE = 0x0
STACK_ADDR = 1024 * 1024
STACK_SIZE
= remote("challenge.ctf.bzh", 30342)
p = ""
output
def hook_code(mu, address, size, user_data):
= mu.mem_read(address, size)
op # print('[ 0x%x ] size = 0x%x' %(address, size))
if op == b"\x0f\x05":
= mu.reg_read(UC_X86_REG_EAX)
syscall_number
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)
output 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):
= mu.reg_read(UC_X86_REG_RDI)
rdi = mu.reg_read(UC_X86_REG_RSI)
rsi = mu.reg_read(UC_X86_REG_RBX)
rbx
print(hex(rdi), hex(rsi), hex(rbx))
= mu.mem_read(rdi, rbx)
data = hash_func(data).digest()
res
mu.mem_write(rsi, res)
def s31341(mu):
= mu.reg_read(UC_X86_REG_RDI)
rdi = mu.reg_read(UC_X86_REG_RSI)
rsi = mu.reg_read(UC_X86_REG_RBX)
rbx
print(hex(rdi), hex(rsi), hex(rbx))
= mu.mem_read(rdi, rsi)
data
if rbx == 1:
= base64.b64encode
func if rbx == 2:
= base64.b32encode
func if rbx == 3:
= base64.b16encode
func if rbx == 4:
= base64.b85encode
func
return func(data)
while True:
= Uc(UC_ARCH_X86, UC_MODE_64)
mu
1024 * 1024) # always need to be divisble by 1024
mu.mem_map(BASE, 0x13370000, 1024 * 1024) # always need to be divisble by 1024
mu.mem_map(
" :")
p.recvuntil(= base64.b64decode(p.recvuntil("----").split(b"\n")[1])
chall
= ELF(chall)
helf = helf.get_section_by_name(".shellcode").data
BYTECODE
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)+ len(BYTECODE)) mu.emu_start(BASE, BASE