Qualifications
nvm-easy
Description
-
Solution
Two files are given: nvm.exe and easy.vm.bin. Based on the two files provided, it seems this problem refers to a VM problem. Run the easy.vm.bin file with nvm.exe.
The output shows that easy.vm.bin only prints 69 characters, so the objective is to identify the characters that aren't printed. Debug and locate the function that executes the print, assuming the VM logic is in the same function or its XREF. We found function sub_140068CA0 is the one producing this output.
The code snippet above prints to the console, so to get the full output, we can change the v8 value. First, set a breakpoint at function address 140068EA0 and change the value of EDI to something larger, like 0x100. We'll end up with a stack underflow error because the exact length of the stack isn't 0x100, but we still get the flag.
Flag: ITSEC{this_one_belong_in_stego_category}
BabyV8
Description
-
Solution
Given following files
The README.md file contains a tutorial on how to run the application. So, the application can be run with NWJS, and the original JavaScript code has been compiled into native code in app.bin. To solve this challenge, we downloaded the NWJS SDK with the same version, 0.100.0. Copy all files to the NWJS SDK directory, then change loader.js to the following code
Through the data from app.bin we can see several variables such as correctPositions and secret. Lets print it through console.
As we can see the length of secret corresponds to the length of correctPositions. When we try to input 'a' 24 times, we can see the change in correctPositions.
We can see that the value changes from false to true at index 12 after we input the value 'a'. From this, we can see that the correctPositions value will change according to the input we enter. If the value at that index is true, the correctPositions value at that index will also change. Here's the script we use to dump the correctPositions for each value in the secret.
Next, copy and make a few corrections because the value of 'h' should be at index 0. Following is the solver we use to map the correct input.
Flag: ITSEC{all_th0se_v4rs_in_v8_sn4psh0t_and_forg3T5_t0_d1s4ble_F12}
nvm-hard
Description
-
Solution
Given the same executable as nvm-easy, but a different VM code file, hard.vm.bin. In the nvm-easy problem, we successfully found a function that writes to the console, but we don't yet know how it generates the string before writing it to the console.
To make the analysis easier, we need to create a simple struct based on some information that we know.
Since a1 in the sub_140068CA0 function has a VMContext data type, there aren't many functions called in sub_140068CA0. We can perform static analysis to determine their use. Rename the function, and here's the decompiled result for the sub_140068CA0 function.
From the dynamic analysis, the program flow is as follows:
Receive input via console read
Save the user value in memory
Copy the value from memory to the stack
Process the value from the stack using the
lookup_tablefunction. In thelookup_table, there are two data items processed:User input with a predetermined index
Static value
After 8 bytes of input are processed, the stack value is checked.
If the stack pop value == 0,
continue.If the stack pop value == 1,
stop.
Following is code to do the table lookup
It can be seen that the lookup table implements the following operations
The next process does the same thing: get the static value, process the input at a specific index, perform a lookup, and so on. So here we can dump all the data needed to reproduce the program's logic.
First set a breakpoint at the following address
maybe_main_vm+FE
table_lookup+47
table_lookup+57
Next, we can check the values that were validated and we get the following pattern.
It can be seen that there is a pattern for each check.
For the first check, there is an additional check byte,
index 8.For the second check, there is an additional check byte,
index 9.And so on.
Since we already have the first 6 bytes, ITSEC{, we need to bruteforce 2 bytes on the first check, but on subsequent checks we only need 1 byte. The problem is that there are multiple valid values for some of the initial checks, such as for the first 8 bytes there is more than 1 valid value. However, we can eliminate the possibilities that have been found in subsequent iterations and finally get only 1 valid value, namely flag. Following is the solver we used
And it only takes 0.09 seconds to get the whole flag
Flag: ITSEC{self_modifying_vm_bytecode_in_dot_net_aot}
minesweeper
Description
-
Solution
Given the executable file and assets, run the executable file and you'll see a Minesweeper game. Just like in a typical Minesweeper game, if you hit the wrong block, you lose.
Decompile with IDA, and we'll initiate the win message in the main function, "You win!!". Entering the sub_6D38B0 function, we'll see the asset initiation process, or the function to load assets. Next, we'll find the following code snippet
By placing a breakpoint in the code above, we can see that this piece of code represents the game's logic and will stop when a win or loss condition is reached. The sub_42B360 function checks whether a property has a value other than 0. If so, the loop continues.
In the sub_403F80 function, we can focus on one of the functions called, sub_402220, which checks whether a block is mined. sub_402220 has three arguments, the second is the x-coordinate and the third is the y-coordinate. The first argument contains game information and constants needed to detect whether a coordinate is mined. The first argument also stores a value that will later be used to validate whether we won the game. sub_404180 checks whether we lost, are still playing, or won.
If we win, the process of validating the win and decrypting the flag is performed; this function is called sub_401BF0. The sub_401BF0 function accepts the same first argument as the mine detection function. To simplify our process, let's create a struct based on the information we know.
And following is the decompiled results of mine_detection after we rename and implement the struct
The next step is to convert the code to get a map of the mine so we can find out which coordinates have mines.
Now we know which coordinates to open. However, we can't get the flag simply by opening the correct coordinates; we also need to open the coordinates in the correct order. We can determine this from the program's decrypt and validation functions, namely sub_6D1BF0 (the base address changes to 0x6D0000).
The values processed by the validate_and_decrypt function are generated by the mine_detection function and placed in a queue. In the validate_and_decrypt function, these values are stored in v61. Therefore, we can reconstruct the conversion from coordinates to values in the queue later.
The following code snippet is important to note, as it slows down the validation process
We can optimize this piece of code into the following equation
Next there is the calc_1 function which looks like it has been obfuscated.
So we need to convert the above operations, here are the results
If we look at the pattern of values, we will see that many operations are actually just wrapping to GF(p). So we can simplify the above function to
Continue to the next code
In the same way we can simplify it to the following operation
Next we reconstruct the entire function to produce the values that are compared in the program.
After validating the program and getting the same results, we need to reverse the function because the operation appears to be reversible.
First, let's look at the state generated in each round.
From the resulting state, we can return the value to the previous state.
From the above operation, only old_state[3] has not been returned. Old_state[3] is used in the calc_2 function and its value is stored in new_state[2]. So we need to reverse calc_2 using the known values, namely state[0], state[1], and state[2].
The next step is to create a decrypt function, here is the script we created
After running the script above, we get a false value, it turns out that after we checked there was a collision/there were several valid values to produce a certain state, for example the following
Luckily, when we ran the decryption script, we got valid coordinates. To get the coordinates, we also needed to reverse the function that generated the v61 value.
Once the coordinates are obtained, the next step is to automatically call the function and optimize it for authentication. Here's what you need to do:
The base address changes to
0xda0000during debugging.Set a breakpoint at
00DA440A.Set RIP to
0DA4420.Copy the ecx value from the
00DA1BF0function call and place it in helper1.py.Run helper1.py.
Set breakpoints at
0DA1CEDand0DA1D0E.Here, we'll automate the process to speed up iterations and provide the correct value for esi.
Run helper2.py.
Set a breakpoint at
0DA1E33.Here, we'll ensure the values being compared are correct.
Set a breakpoint at
0DA2182.To retrieve the address from the decrypted result.
Then disable it.
Set a breakpoint at
00DA21AD.To get all decrypted values (flags).
Flag: ITSEC{P3mbEr51H_R4nJ4U_HanD4L}
Last updated