Hexagon

TL;DR

  1. Reverse program with Cutter
  2. Crash program through gdb to get register dump
  3. Try different inputs until we get the flag

Solution

(This challenged was solved with the help of my team mate s3np41k1r1t0)

Confusion (at first)

We are provided with a single file (challenge) and at first we might try to open in using Ghidra, but it cannot get the language to decompile, so we can't properly use it. So, we tried to open it in Cutter instead - and it opened :). After looking at the code, we saw there were some hexX functions defined and we tried to understand what was going on but it was too hard, that assembly is just insane. And because of this, we followed another approach, however, we figured that it reads 8 chars from stdin.

Manual Fuzzing

Since we couldn't properly reverse it, we would run it. After running the file command, we can see that the architecture for this ELF is QUALCOMM DSP6, which is commercially known as Qualcomm Hexagon. We can't run this natively in our computer, so we installed qemu-hexagon which emulates the instructions of this processor and allows us to run the challenge. Also, by passing the flag -g with a port we should be able to step by step the program in GDB and take a look at the registers.

Well, we couldn't do it step by step... BUT we got the dump of the register contents, which is a good thing, kinda. If we try to run the program step by step, we would receive a dump, and it would happend also if we put some breakpoints and continue execution. Since we looked at the code, and partially understood what was going on in the check_flag function, we put a breakpoint in the address of cmp.eq (R5:R4, R3:R2) and tried many inputs, so we could understand what was happening, and we found that R5:R4 would contain the value we have to equal, since it didn't change, and R3:R2 the value after operating on our input.

With our dynamic analysis we found that:

  • The 1st byte of our input would change the rightmost byte of R3
  • The 2nd byte of our input would change the second rightmost byte of R3
  • The 3rd byte of our input would change the second leftmost byte of R3
  • The 4th byte of our input would change the leftmost byte of R3
  • The 5th byte of our input would change the rightmost bytes of R3 and R2
  • The 6th byte of our input would change the second rightmost byte of R3 and R2
  • The 7th byte of our input would change the second leftmost byte of R3 and R2
  • The 8th byte of our input would change the leftmost byte of R3 and R2

Defining our target

With this information, we can define a strategy: since the last 4 bytes would change both register, we need to guess them first and after that we need to guess the first ones, because they only change 1 register and so we can guess the value easily. Ideally we would script this, but since neither of us knew how to program GDB through pwntools and it would take a while to learn how to do it, we just did it by hand.

HOWEVER, for the purpose of this writeup, we will script it :)

We want our gdbscript to: first set the breakpoint to the desired instruction (since the binary doesn't have PIE, its addresses are static) and then resume execution, so the code is something like

break *0x00020384
continue

Next, we should try every possible printable character at every position, put the program waiting for GDB (with the magic flag) and send our gdbscript to it. Then we just have to send our guess and get the dump to check how far/near we are from our target. Using pwntools, we can interact with GDB in 2 ways: attaching to a running process or start a new process with GDB attached. I couldn't use the latter option because we would have 2 gdbservers running, and its a mess. So, going with the former option, we get something like this:

GUESS = [b"a"] * 8

commands = """
break *0x00020384
continue
"""

for i in range(4,8):
  for c in string.printable:
    GUESS[i] = c.encode()
    io = process(["qemu-hexagon", "-g", "1234", "./challenge"])
    debugger = gdb.attach(target=("localhost", 1234), gdbscript=commands, exe="./challenge", api=True)[1] # We use API just to close the gdb and not reach the maximum number of pipes
    io.recvline() # To clean the first message
    io.sendline(b"".join(GUESS))
    dump = io.clean().split(b"\n")
    io.close()
    debugger.quit()
    R2 = ... # We just get the byte we want
    if R4 == R2: # Also with only the bytes we want
      break

And we would do the same for the other part. Since this isn't an usual use case for pwntools (the usage of a lot of gdb instances) this may look bad, but hey, it works 🙃 !

Since this would take a lot of time, I decided to parallelize it and it took about 310s with 4 Python threads. The guess was IDigVLIW, which makes sense, since this processor has a VLIW ISA allowing for instruction parallelism. With this input, we get the message Congratulations! Flag is 'CTF{XXX}' where XXX is your input., so our flag becomes CTF{IDigVLIW}.

Here is the full exploit:

from pwn import *
import string
import threading

def split(val):
  return [val[i:i+2] for i in range(0, len(val), 2)]
def split_pos(val, pos):
  return split(val)[pos]

R4 = split(b"6d80bf97")
R5 = split(b"9d450e3d")

GUESS = [b"a"] * 8

def brute(pos):
  context.terminal = ["alacritty", "-e"] # This is needed so GDB can be spawned
  context.log_level = "FATAL" # This is just to get less output, due to the huge amount of spawned processes
  commands = """
  break *0x00020384
  continue
  """

  # Last dword
  for c in string.printable:
    GUESS[pos+4] = c.encode()
    io = process(["qemu-hexagon", "-g", str(1234 + pos), "./challenge"])
    debugger = gdb.attach(target=("localhost", 1234 + pos), gdbscript=commands, exe="./challenge", api=True)[1]
    io.recvline()
    io.sendline(b"".join(GUESS))
    dump = io.clean().split(b"\n")
    io.close()
    debugger.quit()
    R2 = split_pos(dump[5].strip().split(b"0x")[1], 3-pos)
    if R4[3-pos] == R2:
      break

  print(f"Found {GUESS[pos+4]} at {pos+4}")

  # First dword
  for c in string.printable:
    GUESS[pos] = c.encode()
    io = process(["qemu-hexagon", "-g", str(1234 + pos), "./challenge"])
    debugger = gdb.attach(target=("localhost", 1234 + pos), gdbscript=commands, exe="./challenge", api=True)[1]
    io.recvline()
    io.sendline(b"".join(GUESS))
    dump = io.clean().split(b"\n")
    io.close()
    debugger.quit()
    R3 = split_pos(dump[6].strip().split(b"0x")[1], 3-pos)
    if R5[3-pos] == R3:
      break

  print(f"Found {GUESS[pos]} at {pos}")

threads = []
for i in range(4):
  threads.append(threading.Thread(target=brute, args=(i,)))
  threads[i].start()

for i in range(4):
  threads[i].join()

print("GUESS:", b"".join(GUESS).encode())
io = process(["qemu-hexagon", "./challenge"])
io.sendline(b"".join(GUESS))
print(io.clean())