ICMTC CTF Finals Reverse Engineering Challenges Writeup
Write up of ICMTC CTF Finals Reverse Engineering Challenges.
Firstly, I’m glad to have had the opportunity to participate in this exceptional CTF. Great thanks to the EG-CERT team for their unwavering dedication and for crafting such engaging and challenging tasks. I would also like to thank the Military Technical College for their meticulous organization and support.
Also, I’m thrilled to share that we, as Cyb3rTh1eveZ team, have achieved 4th place in the finals.
Now, I’ll share my solution for all reverse engineering challenges in the ICMTC CTF Finals.
OperationQak
It’s a Windows PE executable file, so let’s not waste any time and open IDA.
This is a straightforward challenge.
I’ll just debug the program and see the result of v9, which will hold the secret key.
And here is the flag:
The first part of the flag is dynamic; it’s the returned number from the GetTickCount function.
SimpleObfuscator
Here we have another Windows PE executable file. Running Detect It Easy, it’s a .NET executable.
For this, we should use dnSpy.
As the challenge name suggests, the executable is obfuscated.
This challenge can be solved in many ways; I’ll share two of them.
First method
Taking a good look at the obfuscated code, it appears there are anti-debugging techniques.
Continuing the analysis, here is the functionality of the program itself.
After building a good understanding of the program flow, we can say that our flag might be in the string variable b and is being compared to our input.
To know the value of b after decoding, we can run a debugger, set a breakpoint at the beginning of the main function, change the instruction pointer to the declaration of the variable b, then step over.
Second method
For this solution, I’ll use a tool named De4dot. It’s a very powerful command-line tool that deobfuscates many .NET obfuscations.
Just run it and provide the executable path as an argument, and it will do the rest.
Now we see the program in a much cleaner view, and we can determine how the value of b is being assigned. The flag here is also dynamic, as seen in the cy5azpmunsa() method.
Clicking on method5, we can now see our hardcoded key.
We can even directly print the flag without the key by running this line of code directly.
Doma
“Welcome to the infinity castle.”
This was the hard reverse engineering challenge in the CTF, unfortunately, I didn’t solve it during the CTF because I was busy solving DFIR challenges 😥. We are provided with another windows executable so let’s load it in IDA.
A first look at the main function looks like IDA couldn’t analyze it correctly, and it can’t generate a graph view or decompile either.
So, it looks like there is an anti-analysis or obfuscation technique being used here.
a fast solve I thought of is trying to manually create a function by using the hotkey P while pointing to the start of the function push rax instruction, but it didn’t work.
scrolling down a bit I saw a lot of mov instructions that put some hard coded values on the stack
Also here, there was a push rbp instruction, so I thought of trying to create another function and it worked
Now I can see also the “Enter The Flag:” string and how the program is building that array with constant values, also how it waits for me to enter input.
After some scrolling, I found a block of assembly that couldn’t be analyzed at all and was just some bytes.
But I noticed a very interesting thing in this block, and that’s the jmp instruction, and yes, this is not like any other jmp instruction.
It’s the Inward-pointing jump instruction [Impossible Disassembly]
A popular anti-disassembly technique mentioned in Practical Malware Analysis book
To solve this, I can patch the EB byte and convert it to nop ( 0x90 )
And here we go
There is a lot of arithmetic operations that looks like encryption or hashing algorithm, I made another function here at the inc edx instruction so IDA can decompile for a better view
If this if
statement is true, v14 will be assigned through these operations; we can simulate this to get the result of v14
And in this while loop, we see that (if v14 == 2)
that while loop will exit and sub_140009374() will be executed
Inside it, there will be another check that is probably checking for the current character index and as long as it’s less than 48 characters it will continue looping to perform that hashing algorithm
If the current index was 48, it will not take this if
and will enter the else function that will actually print the correct message.
Now after building a good understanding of the program flow, we can start solving the challenge.
We simply need to take those hashing algorithms and recreate them in Python or any other language, and create a map of all printable characters and their corresponding result, and because we already know the values that will be compared (that v10 array earlier), we can map the results to generate our flag.
and here is the script I created:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Define the operations as functions for better readability and reusability
def operation_v7(a2):
a2_plus_1 = a2 + 1
return (((a2_plus_1) ^ ((a2_plus_1) << 10)) >> 1) + ((a2_plus_1) ^ ((a2_plus_1) << 10))
def operation_v8(v7):
return ((v7 ^ (8 * v7)) >> 5) + (v7 ^ (8 * v7))
def operation_v19(v8):
part1 = ((v8 ^ (16 * v8)) >> 17) + (v8 ^ (16 * v8))
return part1 ^ (part1 << 25)
def operation_v9(v8, v19):
part1 = ((v8 ^ (16 * v8)) >> 17) + (v8 ^ (16 * v8))
part2 = (part1 >> 6) ^ (((part1 & 0x7F) << 19))
return part2 + v19
# List of given values
given_list = [
0xb99d68d8, 0x8ef8f6c3, 0x3194ec2e, 0xb99d68d8, 0x33af2d13, 0x70a549c3, 0x7f69c81e, 0xcfef5b0b, 0x030fe761, 0xdc310a37,
0xcbba9c51, 0xcbba9c51, 0xd659a5a8, 0xcbba9c51, 0xf4c73ebf, 0x930b26e3, 0xb78ac2e7, 0x45e26648, 0x70c1a0e1, 0xea1b9f56,
0xf2475372, 0x030fe761, 0xf4c73ebf, 0xdc310a37, 0x115ea782, 0x70c1a0e1, 0x70c1a0e1, 0x8288d321, 0xf2475372, 0xd659a5a8,
0xdc310a37, 0xb78ac2e7, 0xb78ac2e7, 0xf2475372, 0x9d07d8da, 0x9d07d8da, 0xcbba9c51, 0xb78ac2e7, 0xf2475372, 0x9d07d8da,
0xd659a5a8, 0xdc310a37, 0xdc310a37, 0xcfef5b0b, 0xf4c73ebf, 0x030fe761, 0xcbba9c51, 0xabef6fef
]
# Dictionary to map v9 values to their corresponding characters
v9_to_char = {}
# Iterate through all printable ASCII characters
for a2 in range(32, 127): # Printable ASCII characters range from 32 to 126
v7 = operation_v7(a2)
v8 = operation_v8(v7)
v19 = operation_v19(v8)
v9 = operation_v9(v8, v19)
# Ensure the result is only 4 bytes
v9_4_bytes = v9 & 0xFFFFFFFF
# Store the character corresponding to the v9 value
v9_to_char[v9_4_bytes] = chr(a2)
# Map the given list to the corresponding characters
final_string = ''.join(v9_to_char.get(value, '?') for value in given_list)
# Print the final flag
print(final_string)
EGCERT{6d944e4c857b10dc9abb20e9550334503e996cd4}
Crackme ( Bonus Challenge )
This challenge was released after the end of theCTF. The author, @ghostinthehive
, decided not to publish it initially.
We are provided with another windows executable file.
The challenge asks for a username and password, then checks their validity. I analyzed it with IDA.
The program takes my input, performs some operations, generates a value, and then passes it as an argument to a function.
In this function, more operations are performed, followed by an if statement that checks specific values.
Since v6
is assigned dynamically, I need to understand how, so I’ll start debugging
I set a breakpoint before entering this function to see the argument it took, and it was a single byte
That byte was used for XOR operations in a loop to generate another sequence of bytes. The result is stored in v6
, and the if statement checks if v6 != 0x62
So, now we know that v6[0]
must equal to 0x62
Since the argument was used as the XOR key, we can determine the correct argument by XORing 0x62
(the desired value) with the first byte from v4
.
Let’s try to find the correct argument
Now that we know the argument, we can take two approaches to get the flag: the lazy way and the cool way :)
Lazy solve
We can enter any value as the username and password, then change the argument while debugging the program.
Continue execution, and here is the flag :)
Cool solve
As it’s a crackme challenge, why not generate a keygen for any username
The operations is easy to understand, so I’ll just create a c code to brute force all possible passwords on my cool nickname “ELJoOker” to get my personal password 😜
And here is my code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <stdint.h>
#include <string.h>
char valid_chars[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
int num_valid_chars = sizeof(valid_chars) - 1;
char username[] = "ELJoOker";
void find_valid_password(char *password) {
for (int p1 = 0; p1 < num_valid_chars; p1++) {
for (int p2 = 0; p2 < num_valid_chars; p2++) {
for (int p3 = 0; p3 < num_valid_chars; p3++) {
for (int p4 = 0; p4 < num_valid_chars; p4++) {
for (int p5 = 0; p5 < num_valid_chars; p5++) {
for (int p6 = 0; p6 < num_valid_chars; p6++) {
for (int p7 = 0; p7 < num_valid_chars; p7++) {
for (int p8 = 0; p8 < num_valid_chars; p8++) {
password[0] = valid_chars[p1];
password[1] = valid_chars[p2];
password[2] = valid_chars[p3];
password[3] = valid_chars[p4];
password[4] = valid_chars[p5];
password[5] = valid_chars[p6];
password[6] = valid_chars[p7];
password[7] = valid_chars[p8];
password[8] = '\0';
uint8_t v6[9];
int32_t v4 = 0;
for (int i = 0; i < 9; i++) {
v6[i] = password[i] ^ username[i];
v4 += v6[i] - 4 * i;
}
if (v4 == 0xf5) {
return;
}
}
}
}
}
}
}
}
}
}
int main() {
char password[9] = {0};
find_valid_password(password);
printf("Username: %s\n", username);
printf("Password: %s\n", password);
return 0;
}