Scope
The text that came as description with the CTF was the following:
I got this binary from a friend. Can you help me find the correct flag?
Not really helpful…
The only file involved is a binary called ‘indirect’.
Walkthrough
There are not a lot of functions, and only three non-standard ones are used in main
.
Custom Functions
char* func1(char* str, int32_t len)
char* xored_str = malloc(bytes: sx.q(len + 1))
for (int32_t i = 0; i s< len; i += 1)
xored_str[sx.q(i)] = str[sx.q(i)] ^ 0x1a
return xored_str
→ Every character in a string will be XOR’d with 0x1a.
char* func2(char* str, int32_t len)
char* xored_str = malloc(bytes: sx.q(len + 1))
for (int32_t i = 0; i s< len; i += 1)
xored_str[sx.q(i)] = str[sx.q(i)] ^ i.b
return xored_str
→ Every character in a string will be xor’d with the current index.
Plus a wrapper around dlsym()
which searches for a symbol in a shared library object.
Main Function
A lot of string initialisation with random encoded strings, followed by searches for symbols in a shared library object. These symbols are invoked as functions with the other strings.
First, I searched for all the symbols in use by decoding the strings, i.e.
XOR-ing all characters with 0x1A. This results in the shared library, called
lib.so.6, and the first symbol (function) being memfrob
, an obfuscation function
that XORs n bytes with a static value (default is 0x1A).
Further decoding the strings with memfrob
, the symbols in order are:
memcmp
strlen
printf
exit
memfrob
This makes the most interesting part of the challenge clearer:
if (fgets(&input, 31, stdin) != 0 && strlen(&input) == 0x1e)
char* rax_13 = xor_str_with_i(
str: xor_str_with_0x1A(str: &input, len: 0x1e), len: 0x1e)
memfrob(rax_13, 0x1e)
if (memcmp(str5, rax_13, 30) == 0)
printf(&var_a10)
exit(0)
Whereas str5
is the string “CESZFVBQ” which is only 8 bytes long, although 30
bytes are being compared. This means that we have to inspect the memory at
run-time at the location where str5
is stored, in order to retrieve the full
30 bytes.
When debugging (with Binja, GDB or whatever), looking at the address where
str5
points, the 30 bytes that we want to decode are:
"CESZFVBQC\x08T_\rO\r\\T~A\x17HI\x13xI\x10\x12\x1fGP\x00"
Okay, with that in mind we can look at the encoding logic and reverse it.
Decoding Analysis
What the code block does in short:
- It reads 30 bytes from input.
- Applies two transformations:
- XOR each byte with 0x1A
- XOR each byte with its index
- Applies
memfrob
- Compares the result (30 bytes) to the “CESZFVBQ…"-string
The Transformation Chain
Let INPUT be the string provided by the user, and let’s call the custom functions by the following names:
xor_str_with_0x1A(char* str, int len)
xor_str_with_i(char* str, int len)
The logic then becomes:
xor_str_with_0x1A(INPUT, 30)
->INPUT[i] ^ 0x1A
xor_str_with_i(result, 30)
->(INPUT[i] ^ 0x1A) ^ i
memfrob(result, 30)
->((INPUT[i] ^ 0x1A) ^ i) ^ 0x2A
- Compare result to “CESZFVBQ…”
Let’s call the encoded string that starts with ‘CESZFVBQ…’ ‘TARGET’.
For each byte i (0..29), the process is as follows:
((INPUT[i] ^ 0x1A) ^ i) ^ 0x2A == TARGET[i]
Reversing the Transformations
Let’s solve for INPUT[i]
and let’s denote: X = INPUT[i]
XOR is associative and commutative, so we can rearrange:
1. ((X ^ 0x1A) ^ i) = TARGET[i] ^ 0x2A
2. (X ^ 0x1A) = (TARGET[i] ^ 0x2A) ^ i
3. X = ((TARGET[i] ^ 0x2A) ^ i) ^ 0x1A
So the formula for the input is:
INPUT[i] = ((TARGET[i] ^ 0x2A) ^ i) ^ 0x1A
Flag retrieval
With this information we can create a simple python script, which reverses the found bytes to the flag:
target = b"CESZFVBQC\x08T_\rO\r\\T~A\x17HI\x13xI\x10\x12\x1fGP\x00"
flag = bytearray()
for i in range(len(target)):
c = target[i] ^ 0x2A
c ^= i
c ^= 0x1A
flag.append(c)
print(flag.decode('utf-8'))
Yay :)