This page looks best with JavaScript enabled

ICMTC Reverse

All Reverse Engineering challenges write-ups. ⭐⭐⭐⭐⭐

 ·  ☕ 24 min read  ·  👻 Ahmed Raof

Oriris

Let’s begin by loading the challenge onto IDA. The program prompts the user for input and passes it to the check_flag() function. If the return value is 1, it displays the message Correct Flag :). Otherwise, it will print Wrong Flag :( message.


code

Upon examining the check_flag() function, we can observe multiple conditional checks. These conditions can be translated into constraints in the Z3 solver to obtain the flag. Notably, the largest index being checked is user_input[41], indicating that the flag length is 42.

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
60
61
62
63
64
65
66
67
68
69
70
71
from z3 import *

rol = lambda val, r_bits, max_bits=8: \
    (val << r_bits%max_bits) & (2**max_bits-1) | \
    ((val & (2**max_bits-1)) >> (max_bits-(r_bits%max_bits)))
 
# Rotate right. Set max_bits to 8.
ror = lambda val, r_bits, max_bits=8: \
    ((val & (2**max_bits-1)) >> r_bits%max_bits) | \
    (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))

solver = z3.Solver()
FLAG_LEN = 42

flag = [BitVec("c_%i" % i ,32)  for i in range(FLAG_LEN)]

# CHARACTERS MUST BE PRINTABLE
for char in flag:
    solver.add(char >= 0 , char <= 127)

solver.add(ror(rol(flag[0], 48), 51) == 0xA8)
solver.add(rol(rol(flag[1], 16), 15) == 0xA3)
solver.add((rol(flag[2], 68) ^ 0x3A) == 14)
solver.add(ror(flag[3] ^ 0x43, 47) == 12)
solver.add(ror(flag[4] ^ 0xC, 47) == 0xBC)
solver.add((rol(flag[5], 20) ^ 0x11) == 84)
solver.add(rol(flag[6] ^ 0x12, 42) == 0xA5)
solver.add(rol(ror(flag[7], 56), 68) == 83)
solver.add(ror(flag[8] ^ 0x38, 19) == 97)
solver.add((ror(flag[9], 67) ^ 0x1F) == 121)
solver.add(ror(ror(flag[10], 38), 60) == 0xD7)
solver.add(rol(ror(flag[11], 51), 51) == 49)
solver.add(rol(rol(flag[12], 28), 21) == 0xCC)
solver.add(ror(flag[13] ^ 0x3A, 42) == 89)
solver.add(ror(ror(flag[14], 16), 42) == 94)
solver.add(ror(rol(flag[15], 48), 24) == 48)
solver.add((ror(flag[16], 39) ^ 0x20) == 0xCA)
solver.add(ror(ror(flag[17], 35), 43) == 125)
solver.add(rol(ror(flag[18], 19), 62) == 35)
solver.add(rol(ror(flag[19], 58), 12) == 0xCC)
solver.add((rol(flag[20], 13) ^ 0x13) == 0xB5)
solver.add(ror(flag[21] ^ 0x27, 21) == 0xA0)
solver.add((flag[22] ^ 0x33) == 97)
solver.add((ror(flag[23], 38) ^ 0x19) == 0xC0)
solver.add(rol(flag[24] ^ 0x1B, 34) == 0xA0)
solver.add(ror(ror(flag[25], 36), 44) == 95)
solver.add((flag[26] ^ 0x79) == 14)
solver.add(rol(ror(flag[27], 20), 10) == 26)
solver.add((rol(flag[28], 69) ^ 0x3A) == 0xBC)
solver.add((rol(flag[29], 18) ^ 0x3F) == 0xE3)
solver.add(rol(flag[30] ^ 0x33, 46) == 27)
solver.add(rol(rol(flag[31], 39), 44) == 27)
solver.add((ror(flag[32], 33) ^ 0x19) == 1)
solver.add(ror(ror(flag[33], 38), 60) == 91)
solver.add((flag[34] ^ 0x3D) == 14)
solver.add(ror(flag[35] ^ 0x3E, 19) == 97)
solver.add(rol(ror(flag[36], 35), 24) == 0xEB)
solver.add(ror(rol(flag[37], 49), 55) == 0xB9)
solver.add(ror(ror(flag[38], 43), 11) == 0xCC)
solver.add((flag[39] ^ 0x33) == 75)
solver.add(flag[40] == 0x37)
solver.add(ror(rol(flag[41], 12), 10) == 0xF5)

if solver.check() == sat:
    model = solver.model()
    for char in range(len(flag)):
        flag[char] = model[flag[char]].as_long()
    
    print(''.join([chr(x) for x in flag]))
else:
    print("no solution found")

Registry

If we open the file sample.reg in any text editor, we can observe that

  • the script adds an entry in the RunOnce key of the current user’s registry. This entry contains a startup command that is executed once when the user logs in.

  • The command launches a hidden PowerShell window and performs a series of actions.

  • First, the PowerShell command searches for a registry file (.reg) within the C:\ drive and its subdirectories. It looks for a file with a size of 0x00026A83 bytes, which is the same size as our file sample.reg [Size: 158,339 bytes].

    • Once it finds a file matching the size criterion, it retrieves the full path of the file.
  • Next, the script creates a temporary batch file named tmpreg.bat in the user’s temporary folder (%temp%). It then copies the selected registry file to the location of the temporary batch file. The content of the tmpreg.bat file is read as a byte array.

  • The script iterates over each byte in the array and performs an XOR operation with the value 0x77.

  • After encryption, a path is generated for a new executable file within the user’s temporary folder.


    sample reg


Alright, let’s XOR the contents of the sample.reg file with the key 0x77 to obtain the executable binary.

sample reg xor
Download the XORed file from CyberChef and reformat it to obtain the binary file.

binary

Let’s load the binary into IDA and begin the analysis. Open the strings tab and, we come across three interesting strings egcert_%d, COMPUTERNAME, checkmate. Let’s jump to the function that contain egcert_%d

strings

This particular function appears to be responsible for storing the FLAG if a specific conditions happend. The Destination variable will holds the value that will be stored in the file. Initially, it concatenates the Source value, which is the word EGCERT{ with the Destination variable. Then, it enters a loop where we notice that the condition is always true. However, there is an if condition that checks if v1 [The counter] equals 8. If this condition is met, it stores the value of Destination in the file and exits.

During debugging, I observed that the function sub_7FF684BABA20() stores the value egcert_0 when v1 is 0 into VarName variable and then attempts to retrieve the value of an environment variable with the same name using getenv(). If the variable with the name egcert_0 exists in the environment variable, the function compares it with qword_7FF684BB8040[0]. If they are equal, it increments v1 [The counter], add the value of qword_7FF684BB8040[0] to the Destination variable and adds an underscore ‘_’ to Destination.

From the above analysis, we can deduce that qword_7FF684BB8040 must be an array with 8 values, representing the flag. To explore this further, let’s examine the Xrefs for this function.

store the flag

As mentioned earlier, a specific condition must be met to execute the get_flag() function. This condition requires that our COMPUTERNAME variable matches the string checkmate for the get_flag() function to be executed.

store the flag

check the env name

We can patch the code to execute get_flag() function. Let’s examine the Xrefs of this function and set a breakpoint on it.

add a break point

SOLUTION

Get The Flag
1
2
var = ['iesr3v3r', 'gn', '_', 'g3r', '_', 's3l1f', '_', 'htiw', '_', 'd3dd3bm3', '_', '3lbatuc3xe', '_', '3d0c', '_', '3db6bcc9']
print("EGCERT{" + "".join(var[::-1])[::-1] + "}", end='')

CppFusion

1
2
ahmed@DESKTOP-FCBJDP8:~$ CppFusion.exe
no

Our main focus is to determine the source of the no message. To begin our analysis, let’s load the program into IDA. If we look at strings we won’t find anything. Examining the main() we will find an interesting part, let’s check the result value and what is function sub_439A94()

sample reg

  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
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
int __usercall sub_439A94@<eax>(int a1@<esi>, int a2, int a3)
{
  _DWORD *v3; // eax
  _DWORD *v4; // eax
  _DWORD *v5; // eax
  _DWORD *v6; // eax
  int v7; // ecx
  _DWORD *v8; // eax
  _DWORD *v9; // eax
  int v10; // eax
  const char *v11; // eax
  char *Buffer; // [esp+0h] [ebp-98h] BYREF
  int v14; // [esp+14h] [ebp-84h]
  int v15; // [esp+18h] [ebp-80h]
  char v16[4]; // [esp+1Ch] [ebp-7Ch] BYREF
  int v17; // [esp+20h] [ebp-78h]
  int (__cdecl *v18)(int, int, int, int, int, int); // [esp+34h] [ebp-64h]
  int *v19; // [esp+38h] [ebp-60h]
  int *v20; // [esp+3Ch] [ebp-5Ch]
  void *v21; // [esp+40h] [ebp-58h]
  char **p_Buffer; // [esp+44h] [ebp-54h]
  char v23; // [esp+57h] [ebp-41h] BYREF
  int v24; // [esp+58h] [ebp-40h] BYREF
  int v25; // [esp+5Ch] [ebp-3Ch] BYREF
  int v26[3]; // [esp+60h] [ebp-38h] BYREF
  char v27[8]; // [esp+6Ch] [ebp-2Ch] BYREF
  int v28; // [esp+74h] [ebp-24h] BYREF
  int v29; // [esp+78h] [ebp-20h] BYREF
  int v30; // [esp+7Ch] [ebp-1Ch]
  int *v31; // [esp+88h] [ebp-10h]
  int savedregs; // [esp+98h] [ebp+0h] BYREF

  v31 = &a2;
  v15 = a2;
  v18 = sub_439190;
  v14 = a3;
  v19 = dword_43A7E8;
  v20 = &savedregs;
  v21 = &loc_439D2D;
  p_Buffer = &Buffer;
  sub_41A0B0(v16);
  sub_418630();
  v24 = 0;
  v23 = 0;
  v25 = *(_DWORD *)(v14 + 4);
  if ( v15 != 2 )
    goto LABEL_3;
  v26[0] = 3;
  v23 = 1;
  memset(&v26[1], 0, 8u);
  v28 = 0;
  v29 = 0;
  memset(v27, 0, sizeof(v27));
  v17 = 6;
  v3 = (_DWORD *)sub_438520(8u);
  *v3 = &off_441AB4;
  v3[1] = &v24;
  sub_433F7C(v3);
  v17 = 1;
  v4 = (_DWORD *)sub_438520(0x14u);
  *v4 = &off_442E14;
  v4[1] = &v25;
  v4[3] = v26;
  v4[2] = &v24;
  v4[4] = &v23;
  sub_433F98(v4);
  v17 = 1;
  sub_42C300(&v28, a1);
  sub_43232C(&v29);
  sub_432338(&v28);
  sub_433D30(&v27[4]);
  v28 = 0;
  memset(v27, 0, sizeof(v27));
  v29 = 0;
  v30 = 0;
  v17 = 7;
  v5 = (_DWORD *)sub_438520(8u);
  *v5 = &off_442E28;
  v5[1] = &v24;
  sub_433F7C(v5);
  v17 = 2;
  v6 = (_DWORD *)sub_438520(0xCu);
  *v6 = &off_442E3C;
  v6[1] = &v23;
  v6[2] = v26;
  sub_433F98(v6);
  v17 = 2;
  sub_42C2B0(&v28, v7);
  sub_42C2DC(&v28);
  sub_433D30(&v27[4]);
  v28 = 0;
  memset(v27, 0, sizeof(v27));
  v29 = 0;
  v30 = 0;
  v17 = 8;
  v8 = (_DWORD *)sub_438520(0xCu);
  *v8 = &off_442E50;
  v8[1] = &v25;
  v8[2] = &v24;
  sub_433F7C(v8);
  v17 = 3;
  v9 = (_DWORD *)sub_438520(0xCu);
  *v9 = &off_442E64;
  v9[1] = &v23;
  v9[2] = v26;
  v10 = sub_433F98(v9);
  v17 = 3;
  sub_42C2B0(&v28, v10);
  sub_42C2DC(&v28);
  sub_433D30(&v27[4]);
  sub_433D30(&v26[2]);
  v11 = "yes";
  if ( !v23 )
LABEL_3:
    v11 = "no";
  v17 = -1;
  puts(v11);
  v15 = 0;
  sub_41A380(v16);
  return v15;
}

To understand how the program determines whether to display the yes or no message, I began debugging this function. I stepped through the code until I reached the instruction where it compares v15 with 2. If the comparison is false, it print out the no message. In this case, the condition was false cause v15 equal to 1. To proceed, I patched the zero flag (ZF) and continued execution.




As we continued our debugging process, we got an error message. 282244: The instruction at 0x282244 referenced memory at 0x0. The memory could not be read -> 00000000 (exc.code c0000005, tid 8932) and moved to this function



It appears that the value we patched, v15, is responsible for checking whether a parameter has been passed to the program. Let’s attach a parameter and set a breakpoint on the function where we encountered the memory error. This particular function seems to access our input for performing the necessary checks.



By debugging the program, we can locate our input. I added a hardware breakpoint on the input and continued. It compares the first character of our input with the first character of the flag, which is E

  

Here, we can see two images representing different functions, each cmp byte ptr [edx+eax] <single char of the flag>. These functions are located at different offsets within the binary, but the fixed difference between them is 46. We can write a Python script to extract each character of the flag.


sample reg

sample reg

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
f = open("CppFusion.exe", "rb").read()

base_addr = 0x1622
flag = "flag: "

while flag[-1] != "}":
    flag += chr(f[base_addr+3])
    base_addr -= 46

print(flag)

B14ckB0X

This challenge is protected with VMProtect. Therefore, I did not attempt to load it in IDA or even debug it 😂😂, actually i tried to encrypt a very large file, suspend the process, and attach it to x64dbg, and use VMPImportFixer.exe but this method didn’t work. Instead, I focused on alternative approaches to analyze the binary.

We just stopped the execution somewhere; part of this program already ran.


I attempted multiple encryption operations using various files.

Case 1

I utilized the following command to generate a file filled with null bytes

fsutil.exe file createNew null_byte.jpg 200
Next, let's encrypt this file and see the result
B14ckB0X.exe null_byte.jpg

we will notice that those 24 bytes 0F 41 C3 91 BF 98 0F B0 76 41 1D 98 85 46 76 68 B4 91 83 4F 40 A2 0F C2 are repeated which mean that we are dealing with a stream cipher

Case 2

Let’s try to repalce 00 with 01



Next, let’s encrypt this file and see the result

B14ckB0X.exe 01_byte.jpg

Same case but with different bytes 61 3C 52 31 B5 4C 61 F9 07 3C 34 4C BE 6A 07 FF B3 31 94 89 24 01 61 96

Case 3

Now let’s try a combination of 00 and 01



Next, let’s encrypt this file and see the result

B14ckB0X.exe 01_00_byte.jpg

Here, the results become even more interesting. We will notice that the result of encrypting 01 bytes is the same as 01_byte_enc file 61 3C 52 31 B5 4C 61 F9 07 3C 34 4C BE 6A 07 FF B3 31 94 89 24 01 61 96 and 00 is the same as null_byte_enc file 0F 41 C3 91 BF 98 0F B0 76 41 1D 98 85 46 76 68 B4 91 83 4F 40 A2 0F C2

Case 4

Let’s encrypt a single byte for example F4. The result will be

Next, try 00 and F4. We will notice that 00 is the same as the first value in null_byte file which is 0F and F4 encryption result is 9B


Lol, it’s actually depend on the index of the byte, and it’s value so, if we want to get the same result of encrypting a single byte F4 we will do the following


Summary

To decrypt the encrypted file and retrieve the original values, we need to construct our encryption table containing all possible encryption mappings. By comparing this encryption table with the encrypted file, we can determine the original values that were encrypted.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import os

pt_tb = bytearray([i for i in range(256) for _ in range(24)])
with open("pt_tb", "wb") as f: f.write( pt_tb )

os.system("B14ckB0X.exe pt_tb")

enc_tb = bytearray(open("pt_tb.joez", "rb").read())
enc_flag = bytearray(open("flag.jpg.joez", "rb").read())

dec_flag = []

for i in range(0, len(enc_flag), 24):
    for j in range(len(enc_flag[i:i+24])):
        for k in range(0, len(enc_tb), 24):
            val = enc_tb[k:k+24][j]
            if val == enc_flag[i:i+24][j]:
                dec_flag.append(pt_tb[k:k+24][j])
                break

with open("flag.jpg", "wb") as f: f.write( bytes(dec_flag) )

Nightmare [NOT DONE YET]

This challenge utilizes Control-flow Flattening as an obfuscation technique. In this technique, a control_flow value (represented by rax in this challenge) determines which block of code to execute. The control_flow value is altered, and the program loops to execute the next block, repeating this process iteratively.


sample reg

At first, the program checks whether the user has passed a parameter to it or not. It also verifies the length of the parameter to determine if it matches the expected flag length, which in this case, appears to be 48 characters. We will also noticed that the initial value of the control_flow variable is defined.

sample reg

Let’s begin the debugging process.

Eq1

control_flow = 0x871B439D3AB58593 # initial value
i ^= usr_input[31] where i equal to 0
next control_flow = 0xE77D09EC61DBFDFD

It hit the default case
i *= 0x7BA
control_flow = 0x8555347719FF1A7A

i *= 988
control_flow = 0xFEE1F097A9C92F4B

i *= 2228
control_flow = 0xA4DF404AD1AAC8EE

i += usr_input[11]
control_flow = 0xE2AF025EB8A28336

i ^= usr_input[15]
control_flow = 0xD91C08E550BE5766

i += usr_input[35]
control_flow = 0xE5825B9CD01F9C26

i -= usr_input[27]
control_flow = 0xF6DE02EB40B1F592

now it check the i value
Our mission here to get all the equations, then do some **Z3

usr_input[31] * 0x7BA * 988 * 2228 + usr_input[11] ^ usr_input[15] + usr_input[35] - usr_input[27] == 0x696E8ED357

control_flow = 0xC5D5B57C1CBC8C6E
Then he sit i = 0

Eq2

i *= 2532i64
control_flow = 0xCC01DD89959CC7DD

Again, it’s repeating itself with a different control_flow value
control_flow = 0xB55038A0BD1CC3E0
i *= 123



i ^= user_input[24]
control_flow = 0xDD4199AC325220C2


i ^= user_input[28]
control_flow = 0xEBDB474AC55BB5B4


i += user_input[16]
control_flow = 0xF58177BDE4872B30


i *= 595i64;
control_flow = 0x939AC27453EE9EC9


i -= user_input[36]
control_flow = 0xDC59BF762D22FA6C


i *= 3379i64
control_flow = 0xEB048CB33E997319


Here is the second equation

0 ^ user_input[24] ^ user_input[28] + user_input[16] * 595 - user_input[36] * 3379 == 283139926
control_flow = 0x8635B1FB0AA13299 i = 0



That’s Enough

I had a question in my mind about how many equations I needed to obtain the flag. To find out, I examined the wrong condition. Surprisingly, there were nearly 32 equations 😯😯😯.


sample reg

At this point, it became clear that we needed to start considering an automated approach to solve the challenge.

I have already written a code that steps through the program, extracting each part of the equation until it retrieves the full set of equations. However, it encounters an issue at equation number 5 where the dumpulator executes jz to be true when zf = 0, and I’m uncertain why this occurs. I may consider exploring an alternative approach in the future. If anyone has another approach or solution, feel free to reach out to me on Discord 50r4. I would greatly appreciate it.

  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
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
from dumpulator import Dumpulator
from struct import pack
import ida_lines
import idaapi
import idc
import time

dp = Dumpulator("night.dmp",  quiet=True)
ida_base = "0x7FF60E49"

jumps = ['je', 'jz', 'jne', 'jnz', 'jl', 'jge', 'jle', 'jg', 'jo', 'jno', 'jc', 'jnc', 'jbe', 'ja', 'js', 'jns', 'jp', 'jnp', 'jmp']

get_last_4_value = lambda x: hex(x)[-4:]
map_to_ida = lambda x: int(ida_base + get_last_4_value(x), 16)
map_to_x_64 = lambda x: int("0x00007FF6B1F5" + get_last_4_value(x), 16)
get_assembly = lambda x: idc.GetDisasm(int(ida_base + get_last_4_value(x), 16))

# https://reverseengineering.stackexchange.com/questions/30252/idapython-how-to-get-decompiler-output-corresponding-to-the-indirect-call
def find_parent_cinsn(cfunc, citem):
    if not citem.is_expr():
        return citem
    cinsn = None
    class cvisitor(idaapi.ctree_visitor_t):
        def __init__(self):
            super().__init__(idaapi.CV_FAST | idaapi.CV_PARENTS)

        def visit_expr(self, expr) -> int:
            nonlocal cinsn
            if expr.obj_id == citem.obj_id:
                for parent in reversed(self.parents):
                    if not parent.is_expr():
                        cinsn = parent
                        break
                return 1  # Stop enumeration
            return 0

    cvisitor().apply_to(cfunc.body, None)
    return cinsn
def pseudo_for_ea(ea):
    cfunc = idaapi.decompile(ea)
    citem = cfunc.body.find_closest_addr(ea)
    citem = find_parent_cinsn(cfunc, citem)

    if citem:
        for item in cfunc.get_pseudocode():
            if format(citem.index, "X").zfill(16) in item.line.split()[0]:
                decomp = ida_lines.tag_remove(item.line)
                return decomp.replace(" ", "").replace(";", "")

def get_addr_of_loc(instr_code):
    loc_val = instr_code.split(" ")[-1]
    current_function = idaapi.get_func(here())
    target_address = idaapi.get_name_ea(current_function.start_ea, loc_val)
    return target_address

def is_jump_taken(instr_code, eflags):
    jumps_type = instr_code.split(" ")[0]
    eflags_dic = {
        "CF": (eflags & 1),  # Carry Flag
        "PF": (eflags >> 2) & 1,  # Parity Flag
        "AF": (eflags >> 4) & 1,  # Auxiliary Carry Flag
        "ZF": (eflags >> 6) & 1,  # Zero Flag
        "SF": (eflags >> 7) & 1,  # Sign Flag
        "TF": (eflags >> 8) & 1,  # Trap Flag
        "IF": (eflags >> 9) & 1,  # Interrupt Flag
        "DF": (eflags >> 10) & 1,  # Direction Flag
        "OF": (eflags >> 11) & 1  # Overflow Flag
    }
    # http://unixwiz.net/techtips/x86-jumps.html
    if jumps_type == "ja":
        return eflags_dic["CF"] == 0 and eflags_dic["ZF"] == 0
    elif jumps_type == "jae":
        return eflags_dic["CF"] == 0
    elif jumps_type == "jb":
        return eflags_dic["CF"] == 1
    elif jumps_type == "jbe":
        return eflags_dic["CF"] == 1 or eflags_dic["ZF"] == 1
    elif jumps_type == "jc":
        return eflags_dic["CF"] == 1
    elif jumps_type == "je" or jumps_type == "jz":
        return eflags_dic["ZF"] == 1
    elif jumps_type == "jg":
        return eflags_dic["ZF"] == 0 and eflags_dic["SF"] == eflags_dic["OF"]
    elif jumps_type == "jge":
        return eflags_dic["SF"] == eflags_dic["OF"]
    elif jumps_type == "jl":
        return eflags_dic["SF"] != eflags_dic["OF"]
    elif jumps_type == "jle":
        return eflags_dic["ZF"] == 1 or eflags_dic["SF"] != eflags_dic["OF"]
    elif jumps_type == "jna":
        return eflags_dic["CF"] == 1 or eflags_dic["ZF"] == 1
    elif jumps_type == "jnae":
        return eflags_dic["CF"] == 1
    elif jumps_type == "jnb":
        return eflags_dic["CF"] == 0
    elif jumps_type == "jnbe":
        return eflags_dic["CF"] == 0 and eflags_dic["ZF"] == 0
    elif jumps_type == "jnc":
        return eflags_dic["CF"] == 0
    elif jumps_type == "jne" or jumps_type == "jnz":
        return eflags_dic["ZF"] == 0
    elif jumps_type == "jno":
        return eflags_dic["OF"] == 0
    elif jumps_type == "jnp" or jumps_type == "jpo":
        return eflags_dic["PF"] == 0
    elif jumps_type == "jns":
        return eflags_dic["SF"] == 0
    elif jumps_type == "jo":
        return eflags_dic["OF"] == 1
    elif jumps_type == "jp" or jumps_type == "jpe":
        return eflags_dic["PF"] == 1
    elif jumps_type == "js":
        return eflags_dic["SF"] == 1
    elif jumps_type == "jmp":
        return 1
    # Default case if jump type not recognized
    return False

# address from x64
main_addr = 0x00007FF6B1F51070
control_flow_start = 0x00007FF6B1F524D0
wrong_condition_addr = [0x00007FF6B1F53901, 0x00007FF6B1F53866, 0x00007FF6B1F53652, 0x00007FF6B1F5355F, 0x00007FF6B1F53350, 0x00007FF6B1F532B3, 0x00007FF6B1F53211, 0x00007FF6B1F530E1, 0x00007FF6B1F52EFE, 0x00007FF6B1F52E4C, 0x00007FF6B1F52D18, 0x00007FF6B1F52AD4, 0x00007FF6B1F5281D, 0x00007FF6B1F5279A, 0x00007FF6B1F52635, 0x00007FF6B1F52566, 0x00007FF6B1F54B66, 0x00007FF6B1F54971, 0x00007FF6B1F54865, 0x00007FF6B1F54841, 0x00007FF6B1F547BC, 0x00007FF6B1F5419A, 0x00007FF6B1F5414E, 0x00007FF6B1F53F5D, 0x00007FF6B1F53BC1, 0x00007FF6B1F53B09, 0x00007FF6B1F53A61, 0x00007FF6B1F53A46, 0x00007FF6B1F53D12, 0x00007FF6B1F53CE1, 0x00007FF6B1F53E2E, 0x00007FF6B1F54D4E]

dp.start(main_addr, control_flow_start)

solve_dict = {}
current_control_flow_value = dp.regs.rax
counter = 0
new_val_rax = 0

while True:
    instr = map_to_ida(control_flow_start)
    instr_code = get_assembly(instr)
    next_instr = idc.next_head(instr)
    eflags = int(dp.regs.eflags)
    control_flow_start = next_instr

    # it's end of the program
    if map_to_x_64(dp.regs.rip) == 0x00007FF6B1F54D4E:
        print("It's Ennnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnd")
        print(solve_dict)
        break

    # moving to a new equation
    if map_to_x_64(dp.regs.rip) in wrong_condition_addr:
        print("wronnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnng condition")
        temp = instr
        key = "eq" + str(counter)

        while True:
            if get_assembly(temp).split(" ")[0] == "jmp":
                current_control_flow_value = solve_dict[key][-1].split("=")[-1][:-4][2:]
                convert_to_little_endian = pack('<Q', int.from_bytes(bytes.fromhex(current_control_flow_value), byteorder='big'))
                new_val_rax = bytes.fromhex("48B8") + convert_to_little_endian

                control_flow_start = get_addr_of_loc(get_assembly(temp))
                break
            res = pseudo_for_ea(temp)
            if key in solve_dict:
                if res not in solve_dict[key]:
                    solve_dict[key].append(res)
            else:
                solve_dict[key] = [res]
            temp = idc.next_head(temp)

        print("equationnnnnnnnnnnnnnnnnnnnnnns Wronnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnng:", key, solve_dict[key])
        counter += 1
    
    # if the jump is taken we must change the address of the next instruction
    elif instr_code.split(" ")[0] in jumps:
        print("jump conditionnnnnnnnnnnnnnnnnnn")
        if instr_code.split(" ")[0] == "jmp":
            print("jump takennnnnnnnnnnnnnnnnn")
            control_flow_start = get_addr_of_loc(get_assembly(instr))
            continue

        yes_or_no = is_jump_taken(instr_code, dp.regs.eflags)
        
        if yes_or_no:
            print("jump takennnnnnnnnnnnnnnnnn")
            control_flow_start = get_addr_of_loc(get_assembly(instr))
        else:
            print("jump nottttttttttttttttttttttttttt taken")
    
    # add a part of the equation
    elif "cmp" not in instr_code and instr_code.split(" ")[0] not in jumps:
        print("operationnnnnnnnnnnnnnnnnnnnnnnnnnnn")
        temp = instr
        while True:
            if get_assembly(temp).split(" ")[0] == "jmp":
                current_control_flow_value = solve_dict[counter][-1].split("=")[-1][:-4][2:]
                convert_to_little_endian = pack('<Q', int.from_bytes(bytes.fromhex(current_control_flow_value), byteorder='big'))
                new_val_rax = bytes.fromhex("48B8") + convert_to_little_endian

                control_flow_start = get_addr_of_loc(get_assembly(temp))
                break
            
            elif map_to_x_64(temp) in wrong_condition_addr:
                control_flow_start = temp
                break

            res = pseudo_for_ea(temp)
            
            if counter in solve_dict:
                if res not in solve_dict[counter]:
                    solve_dict[counter].append(res)
            else:
                solve_dict[counter] = [res]
            
            temp = idc.next_head(temp)
        print("equationnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnnns:", counter, solve_dict[counter])

    print("Control Flow:", hex(dp.regs.rax))
    print("Current RIP: ", hex(instr))
    print("next RIP:", hex(control_flow_start))
    print("instr:", instr_code)
    
    print("Counter Equations:", counter)

    print("-"*20)
    dp = Dumpulator("night.dmp",  quiet=True)
    if new_val_rax != 0: # patch the value of rax
        dp.write( map_to_x_64(0x11FD),  new_val_rax)
    dp.start(main_addr, map_to_x_64(control_flow_start))
    
    if counter == 6:
        break

Astra

Get The Flag

Let’s begin by examining the main function. Initially, it prints the message Enter The Flag: and waits for our input. Afterward, it executes the function sub_401090(). You can see in the gif, we observe that this function appears to be ntdll_RtlAddVectoredExceptionHandler. The next function called is ntdll_RtlAddVectoredExceptionHandler, with two parameters being passed: 1 and check_the_flag.

Get The Flag

Let’s see ntdll_RtlAddVectoredExceptionHandler at msdn.

ntdll_RtlAddVectoredExceptionHandler

1
2
3
4
PVOID AddVectoredExceptionHandler(
  ULONG                       First,
  PVECTORED_EXCEPTION_HANDLER Handler
);

First
The order in which the handler should be called. If the parameter is nonzero, the handler is the first handler to be called. If the parameter is zero, the handler is the last handler to be called.

Hander AKA check_the_flag
A pointer to the handler to be called. For more information, see VectoredHandler.

Let’s edit the function parameters type in IDA

Get The Flag

check_the_flag

1
2
3
4
5
6
PVECTORED_EXCEPTION_HANDLER PvectoredExceptionHandler;

LONG PvectoredExceptionHandler(
  [in] _EXCEPTION_POINTERS *ExceptionInfo
)
{...}

A pointer to an EXCEPTION_POINTERS structure that receives the exception record.

Note that the _EXCEPTION_POINTERS contains two struct


Get The Flag

Summary

Get The Flag

First Case

Set the register ecx with value 0x15 (which is used in check_the_flag to determine which case will be taken), then it divide by zero so that the excpetion will happend and call check_the_flag function.

The instruction v29->Eip += 2; -> is used to escape the idiv and avoid getting trapped in a continuous loop, and then it set the memory


Get The Flag

Second Case

Move `dword`` of our input to the eax register. Set ecx 0xA (which is used in check_the_flag to determine which case will be taken), then it divide by zero so that the excpetion will happend and call check_the_flag function

v23 = ContextRecord->Eax; -> which is a dword of our input


Get The Flag

Afterwards, the program stores the resulting value at a specific memory location.

Get The Flag

For example, if the op_code is equal to 0x30210, the movzx operation will only consider 0x10 and evaluate which op_code satisfies the condition: dword_7D7370[(Ecx >> 8) & 7] = v23;. In this case, the operation is performed on 0x30210, resulting in (0x30210 >> 8) & 7 = 2.


Get The Flag

Now, our next step is to extract all the op_codes and addresses where they access the user input. This information will be necessary for emulation using dumpulator

 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
import idaapi
import idc

get_assembly = lambda x: idc.GetDisasm(x)

def dump_op(start, end):
    temp = start
    val = {"start": [], "end": [], "ecx": []}

    while temp != end:
        instr_code = get_assembly(temp)
        if "mov     ecx" in instr_code and "[ebp+var_4]" not in instr_code:
            hex_val = instr_code.split(",")[-1].replace(" ", "").replace("h", "")
            
            val['start'].append(idc.prev_head(temp) if "mov     eax" in get_assembly(idc.prev_head(temp)) else "")
            val['end'].append(idc.next_head(temp))
            val['ecx'].append(hex_val)

        temp = idc.next_head(temp)
    return val

start_main = 0x00B91DE0 # start address of the main
end_main   = 0x00B94CC6 # end address of the main

get_all_op = dump_op(start_main, end_main)
print(get_all_op)

Now that we have obtained the opcodes, the next step is to acquire the dump file (dmp) required for the emulation process.

Get The Flag

Here is the VM disassembler and the result

VM disassembler

 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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
from op_value import *
from dumpulator import Dumpulator

split_hex = lambda hex_ecx: [int("0x" + hex_ecx[i:i+2], 16) for i in range(0, len(hex_ecx), 2)]
to_hex = lambda val: int("0x"+val, 16)
convert_arr = lambda lst: ''.join([hex(num)[2:] for num in lst])
map_to_x64 = lambda addr, base: int("0x" + base + hex(addr)[2:][-4:], 16)

format_4 = lambda val: ["".join((val[i:i+4])) for i in range(0, len(val), 4)]
hex_arr = lambda lst: [f"%.2x" % num for num in lst]

base = "38" # if the ida base is different from your dump base, change it here
memory = []

with open("astra.txt", "w") as f:
    for i, ecx in enumerate(op['ecx']):
        if ecx[-2:] == "15": # set memory to 0
            memory = [0 for i in range(32)] # 7 * 4(dword) = 28 -> 32 bytes increase the size to esacpe the index out of range error in case "12"
            f.write(f"{ecx[-2:]}: setting memory to 0 ->\n")
            f.write(f"{format_4(hex_arr(memory[:16]))}\n")
            f.write(f"{format_4(hex_arr(memory[16:]))}\n\n")

        elif ecx[-2:] == "0A": # mov memory, eax_val
            dp = Dumpulator("astra.dmp", quiet=True)
            dp.start(map_to_x64(op['start'][i], base), map_to_x64(op['end'][i], base))
            
            eax_val = hex(dp.regs.eax)[2:]
            store_mem_index = (to_hex(ecx) >> 8) & 7
            
            memory[store_mem_index*4:store_mem_index*4+4] = split_hex(eax_val)

            f.write(f"{ecx[-2:]}: mov memory[{store_mem_index*4}:{store_mem_index*4+4}]_index_{store_mem_index}, eax_value_{eax_val} ->\n")
            f.write(f"{format_4(hex_arr(memory[:16]))}\n")
            f.write(f"{format_4(hex_arr(memory[16:]))}\n\n")
        
        elif ecx[-2:] == "16": # mov memory, memory
            index_v24 = (to_hex(ecx) >> 0x10) & 7
            value_v24 = memory[index_v24*4:index_v24*4+4]

            store_mem_index = (to_hex(ecx) >> 8) & 7
            memory[store_mem_index*4:store_mem_index*4+4] = value_v24

            f.write(f"{ecx[-2:]}: mov memory[{store_mem_index*4}:{store_mem_index*4+4}]_index_{store_mem_index}, memory_at_index_{index_v24}_{convert_arr(memory[index_v24*4:index_v24*4+4])} ->\n")
            f.write(f"{format_4(hex_arr(memory[:16]))}\n")
            f.write(f"{format_4(hex_arr(memory[16:]))}\n\n")

        if ecx[-2:] == "10": # xor operation
            get_index_1 = (to_hex(ecx) >> 8) & 7
            get_val_1 = memory[get_index_1*4:get_index_1*4+4]

            get_index_2 = (to_hex(ecx) >> 0x10) & 7
            get_val_2 = memory[get_index_2*4:get_index_2*4+4]

            xor_res = [get_val_1[i] ^ get_val_2[i] for i in range(4)]

            memory[get_index_1*4:get_index_1*4+4] = xor_res

            f.write(f"{ecx[-2:]}: xor memory[{get_index_1*4}:{get_index_1*4+4}]_index_{get_index_1}, memory_at_index_{get_index_2}_{convert_arr(get_val_2)} ->\n")
            f.write(f"{format_4(hex_arr(memory[:16]))}\n")
            f.write(f"{format_4(hex_arr(memory[16:]))}\n\n")
        
        if ecx[-2:] == "12": # or operation
            get_index_1 = (to_hex(ecx) >> 8) & 7
            get_val_1 = memory[get_index_1*4:get_index_1*4+4]

            get_index_2 = (to_hex(ecx) >> 0x10) & 7
            get_val_2 = memory[get_index_2*4:get_index_2*4+4]

            or_res = [get_val_1[i] | get_val_2[i] for i in range(4)]

            memory[get_index_1*4:get_index_1*4+4] = or_res

            f.write(f"{ecx[-2:]}: or memory[{get_index_1*4}:{get_index_1*4+4}]_index_{get_index_1}, memory_at_index_{get_index_2}_{convert_arr(get_val_2)} ->\n")
            f.write(f"{format_4(hex_arr(memory[:16]))}\n")
            f.write(f"{format_4(hex_arr(memory[16:]))}\n\n")
        
        if ecx[-2:] == "13": # check the flag -> if a dword is equal to zero then it's the correct flag
            f.write(f"check the flag, does the last 4 bytes is zero or not?\n")
            f.write(f"{format_4(hex_arr(memory[:16]))}\n")
            f.write(f"{format_4(hex_arr(memory[16:]))}\n\n")

Result

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
15: setting memory to 0 ->
['00000000', '00000000', '00000000', '00000000']
['00000000', '00000000', '00000000', '00000000']

0A: mov memory[0:4]_index_0, eax_value_33323130 ->
['33323130', '00000000', '00000000', '00000000']
['00000000', '00000000', '00000000', '00000000']

0A: mov memory[4:8]_index_1, eax_value_37363534 ->
['33323130', '37363534', '00000000', '00000000']
['00000000', '00000000', '00000000', '00000000']

0A: mov memory[8:12]_index_2, eax_value_33323130 ->
['33323130', '37363534', '33323130', '00000000']
['00000000', '00000000', '00000000', '00000000']

0A: mov memory[12:16]_index_3, eax_value_37363534 ->
['33323130', '37363534', '33323130', '37363534']
['00000000', '00000000', '00000000', '00000000']
                ................
                ................
                ................

Now, you can see the instructions generated by our VM disassembler, and solve the challenge 🙂

Another amazing solution made by my friend


Share on

Ahmed Raof. AKA 50r4.
WRITTEN BY
Ahmed Raof
📚Learner🤓Nerd🔫reverse engineering👾malware analysis🔒cryptography