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.

$ 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").data

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

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 rbx

That’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))