Haachama cooking [RE]
A simple Golang binary reversing challenge with some AES. Second RE challenge from CTF.SG 2021.
Challenge Description
I lost the description, but it didn’t contain any useful information anyway.
We are given a binary
Solution
I couldn’t get this binary to run, so I gave up eventually and went straight to static analysis.
Opening this binary in Ghidra, we find no entry
as we normally would with a C
binary. We can see hints of Golang from the fmt
functions, so the main is actually at main.main
.
One skill you need to have when analyzing Golang binaries is to ignore certain part of code, for instance:
void main.main(void)
{
pppuVar1 = (undefined ***)(*(int *)(*in_GS_OFFSET + -4) + 8);
ppiVar2 = (int **)register0x00000010;
if (*pppuVar1 <= &local_2c && &local_2c != (undefined ***)*pppuVar1) {
...
while (true) {
...
if (next_index_x16 < (uint)((int)index * 0x10)) {
LAB_080dac9c:
runtime.panicSliceB();
break;
}
...
if ((int *)0x3 < local_6c) {
runtime.panicIndex();
goto LAB_080dac9c;
}
result[(int)local_6c] = local_84;
index = local_70;
}
runtime.panicSliceAlen();
}
*(undefined4 *)((undefined *)ppiVar2 + -4) = 0x80dacb2;
runtime.morestack_noctxt();
main.main();
return;
}
These code do nothing related to the main logic of the program, it just allocates stack space for the executable. We’ll jump straight into the important parts.
After the input, there is a while loop that, upon closer inspection, each iteration seems to operate on 16 bytes of our input.
Looking at this, we are given iv
and key
, and if we trace the main.encryptPart()
call, we end up in main.encryptPart.func1()
and see that it calls main.aesEncrypt()
. From this, I just blindly assumed that it takes the given iv
and key
to encrypt our 16 byte segments.
Going back to main.main
, we can look into the code that runs after we encrypt the 4 segments. I’ll be excluding the parameters and only look at the function calls (this is sufficient in understanding the program, and this is what I did to solve it).
main.merge();
runtime.makeslice();
encoding/hex.Encode();
runtime.slicebytetostring();
fmt.Fprintln();
runtime.convTstring();
fmt.Fprintln();
if (local_74 == (undefined **)&DAT_00000080) {
runtime.memequal();
if ((char)local_a0 != '\0') {
fmt.Fprintln();
return;
}
}
fmt.Fprintln();
return;
If we look into main.merge()
, it takes the 4 segments (after encryption) and merges them back into 64 bytes (converting them to a literal string, i.e. 0x2a03
-> “2a03”), then compares it to some string using runtime.memequal()
.
So the final solution is to take 128 characters of this string, because AES outputs in 128 bit = 16 byte = 32 characters, 4 such outputs concat to 128 characters. Then we use that string and decrypt using AES with the given iv
and key
.
Final Script
from Crypto.Cipher import AES
iv = b'mysupersecureiv\x00'
key = b'mysupersecurekey'
def decrypt_flag(key: str, iv: str, ciphertext: str):
# Decrypt flag
ciphertext = bytes.fromhex(ciphertext)
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = cipher.decrypt(ciphertext)
return plaintext.decode('utf-8')
ct = ['20d91f642406ce17432107a0f61a5405', 'c3b45ec744d07c2d3a19649f5ed2c5ba', 'ff4d15473b92c1d00916790dd14deec7', '7a9d413a1e2fe83f0775bd7d3c984c4c']
for text in ct:
print(decrypt_flag(key, iv, text), end='')
print()
Flag: CTFSG{t0d@y_1_1E@rnT_hum@ns_c@nt_multit@sk_BUT_c0mput3rs_c@n_d0}