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:

  1. It reads 30 bytes from input.
  2. Applies two transformations:
    • XOR each byte with 0x1A
    • XOR each byte with its index
  3. Applies memfrob
  4. 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:

  1. xor_str_with_0x1A(INPUT, 30) -> INPUT[i] ^ 0x1A
  2. xor_str_with_i(result, 30) -> (INPUT[i] ^ 0x1A) ^ i
  3. memfrob(result, 30) -> ((INPUT[i] ^ 0x1A) ^ i) ^ 0x2A
  4. 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 :)