image

I try my best to explain how I solve the 4 RE challenges from this years CTF, some of it relied on trial and error / recognising patterns and therefore may not be as technically accurate as i’d like

NimCrackMe1

SHA 256: 47d7fa30cfeeba6cc42e75e97382ab05002a6cd0ebb4d622156a6af84fda7d5e

Execution flow: Main > NimMain > NimMainInner > NimMainModule > main__crackme_u20

buildEncodedFlag__crackme_u18 gets called at 140012c02, followed by xorStrings__crackme_u3 at 140012c6b

Dynamic Approach: Set a breakpoint at 140012c6b, step over the function call, review the data in the R11 register image

image

image

Static Approach: The buildEncodedFlag__crackme_u18 function builds a Nim String of 0x26 (38) bytes in length.

image

The, one byte is written at a time, resulting in the hexadecimal code: 0x28, 0x05, 0x0C, 0x47, 0x12, 0x4B, 0x15, 0x5C, 0x09, 0x12, 0x17, 0x55, 0x09, 0x4B, 0x42, 0x08, 0x55, 0x5A, 0x45, 0x58, 0x44, 0x57, 0x45, 0x77, 0x5D, 0x54, 0x44, 0x5C, 0x45, 0x13, 0x59, 0x5B, 0x47, 0x42, 0x5E, 0x59, 0x16, 0x5D

This result is stored in var_28

image

And soon, var_98, which is passed to xorStrings__crackme_u3 as the 2nd argument

image

The first argument is the result, the second is the encoded flag, and the third is the XOR key (length (var_a8) and key (var0_1))

var_a0_1 = &TM__cGo7QGde1ZstH4i7xlaOag_4 TM__cGo7QGde1ZstH4i7xlaOag_4 is a global variable for “Nim is not for malware!”

image

Rust Tickler

SHA 256: df95140548732f34d8cf11b6b9dd7addb31480fab871b7004c7c1e09acfd920b

Finding main: Entry point > FUN_140005424 > FUN_140001790 > FUN_1400011f0

Towards the end of this large function is an undefined function (sub_140001740).

image

image

image

We can see a string being XOR’d with 0x51

Replicating this will result in the flag

image

image

Rust Tickler 2

SHA 256: 47d7fa30cfeeba6cc42e75e97382ab05002a6cd0ebb4d622156a6af84fda7d5e

Main > 0x140001350

image

Set a breakpoint for this address in x64dbg:

image

A few instructions into this function, data gets moved to RDX, and the length of data is moved into RAX

image

Jump over this instruction in a debugger and right click the RDX register > Follow in dump to see the data

image

image

At 1400013ae, an XOR key is moved into XMM0

image

An XOR operation then occurs using this key towards i_3 (The data in RDX)

image

This XOR operation is performed in the following loop:

image

Partial decrypted data in RDX after the first iteration shows a HNTS header:

image

Set a breakpoint at 1400013E5 and hit it to complete the decryption loop, revealing the decrypted data structure in RDX

image

This decrypted data structure gets passed to function 140003ea0

image

This function is essentially the HNTS data parser, it checks that the data is as expected by checking the magic bytes and creates an indexed array for later lookup

The parsed data structure is moved into the RDX prior to the call to function 140003de0

image

image

0xAAAAAAAA is then moved into the R8 register

image

These values are used as arguments for the call to function 140003de0

image

After passing this function in a debugger, a string is returned:

image

A similar set up occurs later, where 0xAAAA is moved into R8 and returns another string after a call to 140003de0

image

Which returns the string Bingus

image

So the values being moved to R8 prior to the function call return different strings. They are acting as IDs within the HNTS data structure and return different output depending on the ID provided.

Looking at the HNTS data structure, the IDs are formatted in a pretty recognisable way:

image

We already know that AAAAAAAA and AAAA are valid IDs, AAAAA is also later called in the code. The rest of the IDs highlighted in this “zone” are also valid due to their offsets within the structure.

Modifying the R8 register to one of these IDs prior to the function call changes the result

image

The 7F structure ID will return the flag:

image

Rust Tickler 3

SHA 256: a4a5b64d72540552c691293f9e988e189674275f6e4743b8d61f299bd6f31fc7

Main function of interest: 1400011f0

This challenge initially follows a similar format to Rust Tickler 2 where an ID is moved into R8 prior to a function call which results in a different string being returned, for example:

image

This results in:

image

When we get to ​​1400013C2, there is a conditional jump, where either ID 1338 or 1339 is used

image

image

Trial and error tells us that 1338 is the failure, and 1339 is the success, so we’ll set our RIP / patch the ZF / binary to follow that execution path.

Failure / success is determined based on if the provided input is equal to the result of ID 133A

image

Modifying one of the IDs in R8 prior to the 1423ED7D0 function call will reveal the “answer”:

image

Continuing to follow execution, we see a path being built: (ID 1348 = Exodus)

image

image

The binary will terminate shortly after this is seen if this directory doesn’t exist.

If the correct “answer” is provided and the above path exists, a file (filename created from ID 1369)

So all we need to do is create this directory and supply the hash when prompted by the executable.

image

Stage 2 performs a memcmp to compare supplied input to a known value and if that value matches, the success path is executed.

However, patching the binary to get a success just reveals the message:

image

And there is not much change in execution flow as seen:

image

The answer lies between 140001354 and 1400013c3. This is AES ciphertext, AES key, and AES IV.

image

image

I believe this data is then passed through further cryptographic functions prior to being used by the memcmp function.

Data_14037d0f8 = cb584b62035d138f77bc9810f00f1a2020700f8fbf0d75dca3fd71085f1467cde9d05f1f83bbc76b7d9beb42f7510095

Data_14037d128 = d4c39486fdf04283f5d96436ba68ea1c4f4194796af82d0f8eed7c12f53fa07c

Data_14037d148 = 539fb31e1cc13442420d039397e91777

These data variables follow that of AES 256, where the cipher is 48 bytes, followed by a 32 byte key and 16 byte IV.

image