Hexagon
TL;DR
- Reverse program with
Cutter
- Crash program through
gdb
to get register dump - 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())