ASCIS 2024 - HACK ME! (REV)
Solution to the challenge HACK ME! in ASCIS 2024.
ASCIS Overview
Last weekend, I participated in ASEAN Students Contest on Information Security (ASCIS) with my university team, and we got top 3 in the Jeopardy Finals. For me, I think I did not do well in the CTF, as I only solved one Reverse Engineering challenge by the end of the contest day.
Who created the cursed first challenge, I wonder?
Challenge Information
- Challenge name: HACK ME!
- Given files: Get it here!
Overview
At first, only CHALL_RE.ZIP
, HACKME.EXE.ZIP
and CHECKSUM.TXT
were given.
CHALL_RE.ZIP
contains flag
file and HACKME.EXE
binary.
HACKME.EXE.ZIP
contains HACKME.EXE
binary (I guess it is the same binary).
I will explain the four hints throughout this writeup.
Main binary
We are given an executable that reads from a file named flag
and prompts us for a password.
At this point I was thinking if I should crack the zip that contains the flag
file, but honestly what’s the point of doing that in a reverse challenge?
Luckily, they gave the flag
file later on (this was the first hint).
|
|
First, it reads 32
bytes from the flag
file, and stores the content at &flag
.
Looking into smth()
, we can see that it loads the buffer at &flag
and call sub_140001834(v2)
. The function looks like it is implementing kind of a VM, so v2
must be the context of the VM.
We can clearly see that this challenge uses structs to store things, so let’s start creating custom structs to beautify this.
View > Open Subviews > Local types
or press Shift + F1
. Right click in the Local types
tab and choose Insert
.A quick analysis into the VM function also helps us define the struct entries.
|
|
It is clearly shown that *(_DWORD *)(a1 + 0x48)
should be the VM pointer, *(_QWORD *)(a1 + 0x58)
is the VM bytecodes buffer, and *(_BYTE *)a1
is the condition for that while loop. Lastly, *(_QWORD *)(a1 + 0x60)
looks like the length of the VM bytecodes list.
Let’s start defining our custom struct!
|
|
Above is our first struct, with some unknown fields that I will cover later on.
|
|
After inserting the struct and changing the type of a1
to object* a1
, we got this beautiful piece of code.
|
|
Back to the smth()
function, we can see that v2->padding4[1]
is also the VM length, along with v2->padding2[0x10]
being another pointer that could possibly be used afterward.
At this point, the VM bytecodes buffer should look like this: flag (32 bytes) + actual VM bytecodes
Let’s update our struct once again!
|
|
Let’s look at the function inside case 0x20
(because it has %d
, which could be the input function).
|
|
From this, we can see 2 things: &a1->padding2[]
is the memory of the VM, and each section of the memory will be 4 bytes. The input will be a 4-byte number.
Below is the new struct, that has padding2
replaced by memory
.
|
|
Another function that we should try to look at is the one in case 0x0E
, since it looks like it is doing conditional jump.
|
|
From this, we know that a1->padding3[1]
might be a bool value to check something, so let’s edit our struct to define it.
|
|
Interpreter
At this point, the VM is already easy to solve statically, for example (case 0x18
):
|
|
This can be rewrote into Python like this:
|
|
From this point, I started to write an interpreter for the VM, and got the trace.
|
|
|
|
From the trace, we can see that the executable wants four numbers instead of one, and how those numbers are checked (even though some lines are useless).
Solver
The challenge can be modeled into Z3 like this:
|
|
Unfortunately, this gave infinite results 😭
Let’s take one example and see the trace of the VM - [1073741824, 1480589314, 3780755636, 2332472249]
|
|
Our input will be used to make a key to RC4-decrypting the encrypted flag, so we need the EXACT result from the Z3 script above and I’m pretty sure this is infeasible.
Luckily, the author made three hints that had the first three values of the password - 544047215, 369586174, 635441828
.
Running this updated script yields the final flag:
|
|
Flag is: ASCIS{Just_a_simple_machine}