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.
if ( v6 == *(_DWORD *)(v31 + 36) )
{
for ( j = 0; j < (int)v8; ++j )
{
v33 = sub_140068B90(a1);
sub_140069850(v33);
}
sub_140069810();
}
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
try {
nw.Window.get().evalNWBin(null, 'app.bin');
setTimeout(() => {
try {
const win = nw.Window.get();
win.showDevTools('', () => {
console.log('DevTools opened successfully');
});
} catch (devToolsError) {
console.error('DevTools error:', devToolsError);
}
}, 1000);
} catch (error) {
console.error('Failed to load binary:', error);
}
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.
async function simulateHumanTyping(inp) {
const input = passwordInput;
input.focus();
input.value = '';
const text = inp.repeat(24);
for (let i = 0; i < text.length; i++) {
const char = text[i];
await new Promise(resolve => setTimeout(resolve, Math.random() * 100 + 80));
['keydown', 'keypress'].forEach(eventType => {
input.dispatchEvent(new KeyboardEvent(eventType, {
key: char,
code: `Key${char.toUpperCase()}`,
keyCode: char.charCodeAt(0),
which: char.charCodeAt(0),
bubbles: true,
cancelable: true
}));
});
const pos = input.selectionStart;
input.value = input.value.slice(0, pos) + char + input.value.slice(input.selectionEnd);
input.setSelectionRange(pos + 1, pos + 1);
input.dispatchEvent(new Event('input', { bubbles: true }));
await new Promise(resolve => setTimeout(resolve, Math.random() * 30 + 20));
input.dispatchEvent(new KeyboardEvent('keyup', {
key: char,
code: `Key${char.toUpperCase()}`,
keyCode: char.charCodeAt(0),
bubbles: true
}));
}
console.log(`inp['${inp}'] = [${correctPositions}]`);
correctPositions = [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false];
}
const characters = "D_!kh89fear4c305W@p172db".split('');
for (var i = 0; i < characters.length; i++) {
await simulateHumanTyping(characters[i]);
}
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.
key = 'D_!kh89fear4c305W@p172db'
inp = {}
inp['D'] = [False,False,False,False,False,False,True,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['_'] = [False,False,False,False,False,False,False,False,False,False,True,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['!'] = [False,False,False,False,False,False,False,False,False,True,False,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['k'] = [False,False,False,False,True,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['h'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['8'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,True]
inp['9'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,True,False]
inp['f'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,True,False,False,False,False,False,False,False,False]
inp['e'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,True,False,False,False,False,False]
inp['a'] = [False,False,False,False,False,False,False,False,False,False,False,False,True,False,False,False,False,False,False,False,False,False,False,False]
inp['r'] = [False,False,True,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['4'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,True,False,False,False,False,False,False,False,False,False]
inp['c'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,True,False,False,False,False,False,False]
inp['3'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,True,False,False,False,False]
inp['0'] = [False,False,False,False,False,True,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['5'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,True,False,False,False]
inp['W'] = [False,False,False,False,False,False,False,False,True,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['@'] = [False,True,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['p'] = [False,False,False,False,False,False,False,True,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['1'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,True,False,False,False,False,False,False,False,False,False,False]
inp['7'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,True,False,False]
inp['2'] = [False,False,False,False,False,False,False,False,False,False,False,True,False,False,False,False,False,False,False,False,False,False,False,False]
inp['d'] = [False,False,False,True,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False]
inp['b'] = [False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,False,True,False,False,False,False,False,False,False]
nice = ['?' for _ in range(24)]
for i in key:
tmp = inp[i]
for j in range(len(tmp)):
if tmp[j] == True:
nice[j] = i
nice[0] = 'h'
print(''.join(nice))
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.
struct DynamicArray
{
void *vtable;
void *data;
int count;
int capacity;
int field_18;
int stats_counter;
};
struct OpcodeLookupTable
{
void *vtable;
void *opcodes_array;
int count;
int opcode_0;
int opcode_1;
int opcode_2;
int opcode_3;
int opcode_4;
int opcode_5;
int opcode_6;
int opcode_7;
};
struct VMContext
{
char padding_00[16];
struct DynamicArray *bytecode;
struct OpcodeLookupTable *opcodes;
struct DynamicArray *mem;
struct DynamicArray *stack;
char padding_30[72];
int program_counter;
char padding_7C[4];
};
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.
__int64 __fastcall maybe_main_vm(VMContext *a1)
{
struct DynamicArray *bytecode; // rcx
__int64 program_counter; // rax
unsigned int count; // r8d
unsigned __int64 data; // rcx
int v6; // esi
__int64 v7; // rdx
unsigned int v8; // edi
struct OpcodeLookupTable *opcodes; // rcx
struct DynamicArray *stack_pointer_1; // rcx
_DWORD *stack_data_1; // rdx
unsigned int v12; // eax
struct OpcodeLookupTable *v13; // rcx
struct OpcodeLookupTable *v14; // rcx
struct OpcodeLookupTable *v15; // rcx
struct DynamicArray *stack_pointer_2; // rcx
_DWORD *stack_data_2; // rdx
unsigned int v18; // eax
struct OpcodeLookupTable *v19; // rax
_DWORD *opcodes_array; // rax
signed int i; // ebp
int v22; // eax
struct DynamicArray *mem_pointer_1; // rcx
_DWORD *mem_data_1; // rdx
unsigned int v25; // r8d
struct OpcodeLookupTable *v26; // rcx
signed int j; // ebp
unsigned __int16 v28; // ax
struct OpcodeLookupTable *v29; // rcx
struct DynamicArray *mem; // rcx
int v31; // edx
struct DynamicArray *stack_pointer_3; // rcx
_DWORD *stack_data_3; // rax
unsigned int v34; // r8d
struct OpcodeLookupTable *v35; // rcx
struct DynamicArray *mem_pointer_2; // rcx
_DWORD *mem_data_2; // rdx
unsigned int v38; // eax
int v39; // ecx
if ( a1->program_counter < a1->bytecode->count )// length 0xdb6
{
do
{
bytecode = a1->bytecode;
program_counter = (unsigned int)a1->program_counter;
count = bytecode->count;
if ( (unsigned int)program_counter >= count )
goto LABEL_58;
data = (unsigned __int64)bytecode->data;
if ( (unsigned int)program_counter >= *(_DWORD *)(data + 8) )
goto LABEL_59;
v6 = *(_DWORD *)(data + 4 * program_counter + 16);
v7 = (unsigned int)(a1->program_counter + 1);
if ( (unsigned int)v7 >= count )
goto LABEL_58;
if ( (unsigned int)v7 >= *(_DWORD *)(data + 8) )
goto LABEL_59;
v8 = *(_DWORD *)(data + 4 * v7 + 16);
opcodes = a1->opcodes;
if ( !opcodes->count )
goto LABEL_58;
data = (unsigned __int64)opcodes->opcodes_array;
if ( !*(_DWORD *)(data + 8) )
goto LABEL_59;
if ( v6 == *(_DWORD *)(data + 16) )
{
stack_pointer_1 = a1->stack;
++stack_pointer_1->capacity;
stack_data_1 = stack_pointer_1->data;
v12 = stack_pointer_1->count;
if ( stack_data_1[2] <= v12 )
{
push_a2_to_a1((__int64)stack_pointer_1, v8);
}
else
{
stack_pointer_1->count = v12 + 1;
stack_data_1[v12 + 4] = v8;
}
}
v13 = a1->opcodes;
if ( v13->count <= 1u )
goto LABEL_58;
data = (unsigned __int64)v13->opcodes_array;
if ( *(_DWORD *)(data + 8) <= 1u )
goto LABEL_59;
if ( v6 == *(_DWORD *)(data + 20) )
table_lookup(a1, v8);
v14 = a1->opcodes;
if ( v14->count <= 2u )
goto LABEL_58;
data = (unsigned __int64)v14->opcodes_array;
if ( *(_DWORD *)(data + 8) <= 2u )
goto LABEL_59;
if ( v6 == *(_DWORD *)(data + 24) && (unsigned int)stack_pop(a1) )
a1->program_counter = 2 * v8 - 2;
v15 = a1->opcodes;
if ( v15->count <= 3u )
goto LABEL_58;
data = (unsigned __int64)v15->opcodes_array;
if ( *(_DWORD *)(data + 8) <= 3u )
goto LABEL_59;
if ( v6 == *(_DWORD *)(data + 28) )
{
a1->program_counter = a1->bytecode->count;
stack_pointer_2 = a1->stack;
++stack_pointer_2->capacity;
stack_data_2 = stack_pointer_2->data;
v18 = stack_pointer_2->count;
if ( stack_data_2[2] <= v18 )
{
push_a2_to_a1((__int64)stack_pointer_2, v8);
}
else
{
stack_pointer_2->count = v18 + 1;
data = v18;
stack_data_2[v18 + 4] = v8;
}
}
v19 = a1->opcodes;
if ( v19->count <= 4u )
goto LABEL_58;
opcodes_array = v19->opcodes_array;
if ( opcodes_array[2] <= 4u )
goto LABEL_59;
if ( v6 == opcodes_array[8] )
{
for ( i = 0; i < (int)v8; ++i )
{
v22 = System_Console_System_Console__Read();
if ( v22 == -1 )
break;
mem_pointer_1 = a1->mem;
++mem_pointer_1->capacity;
mem_data_1 = mem_pointer_1->data;
v25 = mem_pointer_1->count;
if ( mem_data_1[2] <= v25 )
{
push_a2_to_a1((__int64)mem_pointer_1, v22);
}
else
{
mem_pointer_1->count = v25 + 1;
mem_data_1[v25 + 4] = v22;
}
}
}
v26 = a1->opcodes;
if ( v26->count <= 5u )
goto LABEL_58;
data = (unsigned __int64)v26->opcodes_array;
if ( *(_DWORD *)(data + 8) <= 5u )
goto LABEL_59;
if ( v6 == *(_DWORD *)(data + 36) )
{
for ( j = 0; j < (int)v8; ++j ) // easy nvm
{
v28 = stack_pop(a1);
write_onebyte(v28);
}
write_newline();
}
v29 = a1->opcodes;
if ( v29->count <= 6u )
LABEL_58:
exception_1();
data = (unsigned __int64)v29->opcodes_array;
if ( *(_DWORD *)(data + 8) <= 6u )
goto LABEL_59;
if ( v6 == *(_DWORD *)(data + 40) )
{
mem = a1->mem;
if ( v8 >= mem->count )
goto LABEL_58;
data = (unsigned __int64)mem->data;
if ( v8 >= *(_DWORD *)(data + 8) )
goto LABEL_59;
v31 = *(_DWORD *)(data + 4LL * v8 + 16);
stack_pointer_3 = a1->stack;
++stack_pointer_3->capacity;
stack_data_3 = stack_pointer_3->data;
v34 = stack_pointer_3->count;
if ( stack_data_3[2] <= v34 )
{
push_a2_to_a1((__int64)stack_pointer_3, v31);
}
else
{
stack_pointer_3->count = v34 + 1;
stack_data_3[v34 + 4] = v31; // input copied here
}
}
v35 = a1->opcodes;
if ( v35->count <= 7u )
goto LABEL_58;
data = (unsigned __int64)v35->opcodes_array;
if ( *(_DWORD *)(data + 8) <= 7u )
LABEL_59:
S_P_CoreLib_Internal_NativeFormat_NativePrimitiveDecoder__ThrowBadImageFormatException_14(data);
if ( v6 == *(_DWORD *)(data + 44) )
{
mem_pointer_2 = a1->mem;
++mem_pointer_2->capacity;
mem_data_2 = mem_pointer_2->data;
v38 = mem_pointer_2->count;
if ( mem_data_2[2] <= v38 )
{
push_a2_to_a1((__int64)mem_pointer_2, v8);
}
else
{
mem_pointer_2->count = v38 + 1;
mem_data_2[v38 + 4] = v8;
}
}
v39 = a1->program_counter + 2;
a1->program_counter = v39;
}
while ( v39 < a1->bytecode->count );
}
return stack_pop(a1);
}
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_table
function. 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
_DWORD *__fastcall maybe_table_lookup(VMContext *a1, int a2)
{
int val1; // edi
__int64 val2; // rcx
__int64 v6; // r8
signed int v7; // r10d
__int64 v8; // rdx
__int64 v9; // rax
__int64 v10; // rdx
__int64 v11; // rdx
int processed_val; // edx
struct DynamicArray *stack_pointer; // rcx
_DWORD *stack_data; // rax
unsigned int count; // r8d
val1 = stack_pop(a1); // 0x41
val2 = (unsigned int)stack_pop(a1); // 0x2b
v6 = *(_QWORD *)a1->padding_30;
v7 = *(_DWORD *)(v6 + 16);
v8 = (unsigned int)(a2 >> 31);
LODWORD(v8) = a2 % v7;
if ( a2 % v7 >= (unsigned int)v7 )
goto LABEL_8;
v9 = *(_QWORD *)(v6 + 8);
if ( (unsigned int)v8 >= *(_DWORD *)(v9 + 8) )
goto LABEL_9;
v10 = *(_QWORD *)(v9 + 8 * v8 + 16);
val2 = (unsigned int)(val1 + ((_DWORD)val2 << 8));// 0x2b41
if ( (unsigned int)val2 >= *(_DWORD *)(v10 + 16) )
LABEL_8:
exception_1();
v11 = *(_QWORD *)(v10 + 8);
if ( (unsigned int)val2 >= *(_DWORD *)(v11 + 8) )
LABEL_9:
S_P_CoreLib_Internal_NativeFormat_NativePrimitiveDecoder__ThrowBadImageFormatException_14(val2);
processed_val = *(_DWORD *)(v11 + 4 * val2 + 16);// data[0x2b41] == 0xea
stack_pointer = a1->stack;
++stack_pointer->capacity;
stack_data = stack_pointer->data;
count = stack_pointer->count;
if ( stack_data[2] <= count )
return (_DWORD *)push_a2_to_a1((__int64)stack_pointer, processed_val);
stack_pointer->count = count + 1;
stack_data[count + 4] = processed_val;
return stack_data;
}
It can be seen that the lookup table implements the following operations
stack_push(data[(val2 << 8) + val1])
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
import idaapi
import idc
def memdump(ea, size, file):
data = idaapi.get_bytes(ea, size)
with open(file, "wb") as fp:
fp.write(data)
print("Memdump Success!")
all_edi = []
all_ecx = []
all_edx = []
all_rdx = []
list_edi = []
list_ecx = []
list_edx = []
list_rdx = []
list_eax = []
for i in range(864):
print(i)
rv = idaapi.regval_t()
idaapi.get_reg_val('rip', rv)
rip = rv.ival
inst = idc.print_insn_mnem(rip)
if inst == 'test':
rv = idaapi.regval_t()
idaapi.get_reg_val('eax', rv)
eax = rv.ival
list_eax.append(eax)
rv = idaapi.regval_t()
rv.ival = 0x0
idaapi.set_reg_val("eax", rv)
all_ecx.append(list_ecx)
all_edx.append(list_edx)
all_edi.append(list_edi)
all_rdx.append(list_rdx)
list_edi = []
list_ecx = []
list_edx = []
list_rdx = []
idaapi.continue_process()
idaapi.wait_for_next_event(idaapi.WFNE_SUSP, -1)
elif inst == 'call':
break
else:
rv = idaapi.regval_t()
idaapi.get_reg_val('ecx', rv)
ecx = rv.ival
list_ecx.append(ecx)
rv = idaapi.regval_t()
idaapi.get_reg_val('edi', rv)
edi = rv.ival
list_edi.append(edi)
idaapi.continue_process()
idaapi.wait_for_next_event(idaapi.WFNE_SUSP, -1)
rv = idaapi.regval_t()
idaapi.get_reg_val('rdx', rv)
rdx = rv.ival
list_rdx.append(rdx)
idaapi.step_into()
idaapi.wait_for_next_event(idaapi.WFNE_SUSP, -1)
rv = idaapi.regval_t()
idaapi.get_reg_val('edx', rv)
edx = rv.ival
list_edx.append(edx)
idaapi.continue_process()
idaapi.wait_for_next_event(idaapi.WFNE_SUSP, -1)
print(all_edi)
print(all_ecx)
print(all_edx)
print(all_rdx)
print(list_eax)
uniq_rdx = []
for i in all_rdx:
for j in i:
if j not in uniq_rdx:
uniq_rdx.append(j)
print(uniq_rdx)
for i in range(len(uniq_rdx)):
address = uniq_rdx[i]
size = 262140 + 0x20 # add little junk
file = f"C:\\Users\\Intel NUC\\ctf\\itsec\\re\\nvmhard\\dump{i}.bin"
memdump(address, size, file)
Next, we can check the values that were validated and we get the following pattern.
from Crypto.Util.number import *
import string
from itertools import product
all_edi = <output from ida>
all_ecx = <output from ida>
all_edx = [<output from ida>
used_dump = <output from ida>
index_dump = <output from ida>
all_dumps = []
for i in range(len(index_dump)):
f = open(f"dump/dump{i}.bin", "rb").read()
f = f[0x10:]
data = []
for j in range(0, len(f), 4):
data.append(bytes_to_long(f[j:j+4][::-1]))
all_dumps.append(data)
inp = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL"
all_mapped = []
for i in all_edi:
mapped_index = []
for j in range(0, len(i) - 2, 2):
mapped_index.append(inp.index(i[j]))
assert len(mapped_index) == 8
all_mapped.append(mapped_index)
for i in all_mapped:
print(i)
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
from Crypto.Util.number import *
import string
from itertools import product
all_edi = <output from ida>
all_ecx = <output from ida>
all_edx = <output from ida>
used_dump = <output from ida>
index_dump = <output from ida>
all_dumps = []
for i in range(len(index_dump)):
f = open(f"dump/dump{i}.bin", "rb").read()
f = f[0x10:]
data = []
for j in range(0, len(f), 4):
data.append(bytes_to_long(f[j:j+4][::-1]))
all_dumps.append(data)
inp = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL"
all_mapped = []
for i in all_edi:
mapped_index = []
for j in range(0, len(i) - 2, 2):
mapped_index.append(inp.index(i[j]))
assert len(mapped_index) == 8
all_mapped.append(mapped_index)
ori = b"\x00" * 48
ori = list(ori)
known = b"ITSEC{"
for i in range(len(known)):
ori[i] = known[i]
list_char = string.printable[:-6].encode()
list_char = list(list_char)
poss = [ori]
for i in range(len(all_mapped)):
new_poss = []
for tmp_inp in poss:
unknown = []
for unk in all_mapped[i]:
if tmp_inp[unk] == 0:
unknown.append(unk)
length_unknown = len(unknown)
for bf in product(list_char, repeat=length_unknown):
inp = tmp_inp[:]
for k in range(length_unknown):
inp[unknown[k]] = bf[k]
init = all_ecx[i][0]
counter = 0
for j in range(len(all_mapped[i])):
init += inp[all_mapped[i][j]]
data = all_dumps[index_dump.index(used_dump[i][counter])]
counter += 1
init = data[init]
init <<= 8
init += all_edi[i][j*2 + 1]
data = all_dumps[index_dump.index(used_dump[i][counter])]
counter += 1
init = data[init]
init <<= 8
data = all_dumps[index_dump.index(used_dump[i][counter])]
init += all_edi[i][16]
init = data[init]
if init == 0:
new_poss.append(inp)
poss = new_poss[:]
for i in poss:
print(bytes(i))
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
while ( (unsigned __int8)sub_42B360(v19[2]) )
{
sub_403F80(v19);
sub_404180(v6, v10);
}
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.
struct MineCell
{
char has_mine;
char is_revealed;
char is_flagged;
char padding;
int adjacent_mine_count;
};
struct GameContext
{
unsigned int grid_width;
unsigned int grid_height;
unsigned int unknown_08;
unsigned int multiplier;
MineCell *mine_field_ptr;
unsigned int unknown_14[10];
unsigned int constant_1;
unsigned int rng_seed;
unsigned int grid_param1;
unsigned int grid_param2;
unsigned int unknown_48;
unsigned int queue_head;
unsigned int queue_tail;
unsigned int *queue_begin;
unsigned int *queue_end;
};
struct MineSweeper
{
void *unknown_0[2];
void *handler_ptr;
char unknown_12[12];
char ui_data[284];
char game_data[183];
GameContext *game_context;
unsigned __int8 game_over;
unsigned __int8 win_state_initialized;
char win_message[100];
int grid_width;
int grid_height;
char unknown_500[8];
void *mine_field;
char unknown_512[56];
int remaining_count;
char unknown_572[12];
unsigned __int8 state_flag;
char unknown_585[100];
};
And following is the decompiled results of mine_detection
after we rename and implement the struct
int __thiscall mine_detection(GameContext *game_context, int x, int y)
{
MineCell *result; // eax
int v4; // ebx
unsigned int v5; // esi
signed int v6; // edi
MineCell *v7; // ebx
int v8; // edi
signed int v9; // esi
MineCell *v10; // ebx
MineCell *v11; // ebx
int v12; // edi
signed int v13; // esi
int v14; // edx
signed int v15; // edi
int v16; // edx
signed int v17; // [esp+Ch] [ebp-Ch]
signed int v18; // [esp+10h] [ebp-8h]
int v19; // [esp+10h] [ebp-8h]
int v20; // [esp+14h] [ebp-4h]
if ( x >= 0 )
{
v4 = y;
if ( y >= 0 && x < (signed int)game_context->grid_width && y < (signed int)game_context->grid_height )
{
result = game_context->mine_field_ptr;
v5 = y + x * game_context->grid_width;
if ( !result[v5].is_revealed && !result[v5].is_flagged )
{
result[v5].is_revealed = 1;
v6 = game_context->rng_seed * game_context->grid_param1 + 1;
v18 = v4 + x * game_context->grid_width;
if ( (signed int)game_context->constant_1 % v6
* ((signed int)game_context->constant_1 % v6 * (v18 % v6) % v6 % v6)
% v6 < 48 )
{
result = game_context->mine_field_ptr;
result[v18].has_mine = 1;
LOBYTE(result) = 1;
return (int)result;
}
v19 = -1;
v20 = x - 1;
while ( 1 )
{
v7 = &game_context->mine_field_ptr[v4 + x * game_context->grid_width];
v8 = (signed int)game_context->constant_1
% (signed int)(game_context->rng_seed * game_context->grid_param1 + 1);
v9 = game_context->rng_seed * game_context->grid_param1 + 1;
v7->adjacent_mine_count += v8 * (v8 * ((signed int)(v20 * game_context->grid_width + y - 1) % v9) % v9 % v9)
% v9 < 48;
if ( v19 )
{
v17 = game_context->rng_seed * game_context->grid_param1 + 1;
v10 = &game_context->mine_field_ptr[y + x * game_context->grid_width];
v10->adjacent_mine_count += (signed int)game_context->constant_1 % v17
* ((signed int)game_context->constant_1 % v17
* ((signed int)(y + v20 * game_context->grid_width) % v17)
% v17
% v17)
% v17 < 48;
}
v11 = &game_context->mine_field_ptr[y + x * game_context->grid_width];
v12 = (signed int)game_context->constant_1
% (signed int)(game_context->rng_seed * game_context->grid_param1 + 1);
v13 = game_context->rng_seed * game_context->grid_param1 + 1;
v14 = v12 * (v12 * ((signed int)(v20 * game_context->grid_width + y + 1) % v13) % v13 % v13) % v13;
++v20;
v11->adjacent_mine_count += v14 < 48;
if ( ++v19 > 1 )
break;
v4 = y;
}
v15 = game_context->rng_seed * game_context->grid_param1 + 1;
v16 = (signed int)game_context->constant_1 % v15 * ((signed int)(y + x * game_context->grid_width) % v15) % v15;
result = (MineCell *)game_context->queue_tail;
y = v16;
if ( result != (MineCell *)game_context->queue_begin )
{
*(_DWORD *)&result->has_mine = v16;
game_context->queue_tail += 4;
LOBYTE(result) = 0;
return (int)result;
}
result = (MineCell *)sub_6D25F0((const void **)&game_context->queue_head, result, &y);
}
}
}
LOBYTE(result) = 0;
return (int)result;
}
The next step is to convert the code to get a map of the mine
so we can find out which coordinates have mines.
import sys
from typing import List, Tuple, Optional
class MinePredictor:
def __init__(self, grid_width: int, grid_height: int, rng_seed: int,
grid_param1: int, grid_param2: int, constant: int):
self.grid_width = grid_width
self.grid_height = grid_height
self.rng_seed = rng_seed
self.grid_param1 = grid_param1
self.grid_param2 = grid_param2
self.constant = constant
self.modulus = rng_seed * grid_param1 + 1
self.rng_multiplier = constant % self.modulus
self.MINE_THRESHOLD = 48
def is_mine_at(self, x: int, y: int) -> bool:
if x < 0 or y < 0 or x >= self.grid_width or y >= self.grid_height:
return False
position = y + x * self.grid_width
modulus = self.rng_seed * self.grid_param1 + 1
multiplier = self.constant % modulus
pos_mod = position % modulus
inner_calc = (multiplier * pos_mod) % modulus
inner_calc = inner_calc % modulus # Double modulus
rng_result = (multiplier * inner_calc) % modulus
return rng_result < self.MINE_THRESHOLD
def print_mine_map(self, show_numbers: bool = False):
print()
print(" ", end="")
for x in range(self.grid_width):
print(f"{x:2d}", end="")
print()
# Print rows
nice = []
for y in range(self.grid_height):
print(f"{y:2d} ", end="")
for x in range(self.grid_width):
if self.is_mine_at(x, y):
print(" *", end="")
else:
print(" .", end="")
nice.append([x, y])
print()
print()
print(f"coordinates = {nice}")
width = 8
height = 8
seed = 8
param1 = 8
param2 = 8
constant = 88403651
predictor = MinePredictor(width, height, seed, param1, param2, constant)
predictor.print_mine_map()
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
).
void *__userpurge validate_and_decrypt@<eax>(GameContext *a1@<ecx>, int a2@<ebp>, void *a3)
{
GameContext *v3; // edx
unsigned int *p_queue_head; // edi
char *v5; // ecx
int v6; // eax
int v7; // edx
int v8; // esi
_DWORD *v9; // ecx
int v10; // kr00_4
unsigned int v11; // eax
int v12; // ecx
int multiplier; // esi
int v14; // edi
unsigned int v15; // ecx
int v16; // edi
unsigned __int64 v17; // kr08_8
int v18; // eax
int v19; // eax
void *index_3; // eax
int v21; // edi
int v22; // ecx
int v23; // esi
int v24; // edx
_BYTE *v25; // eax
_BYTE *v26; // edx
int i; // ecx
GameContext *queue_end; // esi
int v29; // edi
bool v30; // zf
int v31; // edi
int v32; // eax
unsigned int v33; // edi
unsigned int v34; // edx
unsigned int v35; // ecx
GameContext *v36; // esi
unsigned int j; // eax
int v38; // edx
int v39; // edx
int v40; // edx
int v41; // edx
int v42; // edi
GameContext *v43; // eax
int index_1; // [esp-148h] [ebp-154h]
int v45; // [esp-144h] [ebp-150h]
int v46; // [esp-140h] [ebp-14Ch]
int v47; // [esp-128h] [ebp-134h]
int v48; // [esp-128h] [ebp-134h]
_DWORD v49[3]; // [esp-124h] [ebp-130h] BYREF
__int128 v50; // [esp-118h] [ebp-124h]
__int16 v51; // [esp-108h] [ebp-114h]
GameContext *v52; // [esp-104h] [ebp-110h]
int index_0; // [esp-100h] [ebp-10Ch]
int v54; // [esp-FCh] [ebp-108h]
int v55; // [esp-C8h] [ebp-D4h]
int v56; // [esp-C4h] [ebp-D0h]
_BYTE v57[64]; // [esp-B8h] [ebp-C4h]
int v58; // [esp-78h] [ebp-84h]
_BYTE v59[32]; // [esp-70h] [ebp-7Ch] BYREF
_BYTE v60[36]; // [esp-50h] [ebp-5Ch] BYREF
__int128 v61; // [esp-2Ch] [ebp-38h]
int v62; // [esp-1Ch] [ebp-28h] BYREF
_DWORD v63[5]; // [esp-18h] [ebp-24h] BYREF
int v64; // [esp-4h] [ebp-10h]
int v65; // [esp+0h] [ebp-Ch]
void *v66; // [esp+4h] [ebp-8h]
int v67; // [esp+8h] [ebp-4h] BYREF
void *retaddr; // [esp+Ch] [ebp+0h]
v65 = a2;
v66 = retaddr;
v64 = -1;
v63[4] = &loc_770F1D;
v63[3] = NtCurrentTeb()->NtTib.ExceptionList;
v63[2] = &v67;
v3 = a1;
v52 = a1;
p_queue_head = &a1->queue_head;
v5 = 0;
v6 = (int)(p_queue_head[1] - *p_queue_head) >> 2;
index_0 = 0;
v63[0] = p_queue_head;
if ( v6 )
{
do
{
v7 = 0;
v54 = (int)(v5 + 4);
v8 = (int)v5;
if ( !__OFSUB__(v5, v5 + 4) )
{
do
{
v9 = (_DWORD *)(*p_queue_head + 4 * v8++);
v7 = *v9 | (v7 << 8);
*v9 = (unsigned __int8)*v9;
}
while ( v8 < v54 );
v5 = (char *)index_0;
}
v10 = (int)v5;
v5 = (char *)v54;
*((_DWORD *)&v61 + v10 / 4) = v7;
v11 = (int)(p_queue_head[1] - *p_queue_head) >> 2;
index_0 = (int)v5;
}
while ( (unsigned int)v5 < v11 );
v3 = v52;
}
v12 = 10;
multiplier = v3->multiplier;
v14 = v3->rng_seed * v3->grid_param1 + 1;
v45 = v14;
v47 = (signed int)v3->constant_1 % v14;
do
{
v46 = v12 - 1;
v15 = 0xABFDFFBD;
do
{
multiplier = v47 * (multiplier % v14) % v14;
--v15;
}
while ( v15 );
index_0 = DWORD2(v61);
v17 = *(_QWORD *)((char *)&v61 + 4);
LODWORD(v50) = HIDWORD(v17);
v16 = v17;
v18 = calc_1(SDWORD1(v61), multiplier);
index_1 = v61 ^ v18;
DWORD1(v50) = v61 ^ v18;
v54 = (int)(v61
- 1123996597
* (((int)((unsigned __int64)(512866991LL * (int)v61) >> 32) >> 27)
+ ((unsigned int)((unsigned __int64)(512866991LL * (int)v61) >> 32) >> 31))) >> 31;
DWORD2(v50) = (int)(index_0
- 1123996597
* (((int)((unsigned __int64)(512866991LL * index_0) >> 32) >> 27)
+ ((unsigned int)((unsigned __int64)(512866991LL * index_0) >> 32) >> 31)))
* (__int64)(int)(v61
- 1123996597
* (((int)((unsigned __int64)(512866991LL * (int)v61) >> 32) >> 27)
+ ((unsigned int)((unsigned __int64)(512866991LL * (int)v61) >> 32) >> 31)))
% 1123996597
* ((v16 + (__int64)SHIDWORD(v61)) % 1123996597)
% 1123996597;
v19 = calc_1(index_0, multiplier);
index_3 = (void *)(DWORD1(v61) ^ v19);
v12 = v46;
HIDWORD(v50) = index_3;
v61 = v50;
v14 = v45;
}
while ( v46 );
if ( index_0 == 342066253 && index_1 == 686880041 && DWORD2(v50) == 828922036 && index_3 == (void *)509868183 )
{
v21 = v63[0];
v22 = 0;
v54 = 0;
do
{
v23 = v52->grid_param1 * v52->rng_seed + 1;
v24 = (signed int)v52->constant_1 % v23 * (*(_DWORD *)(v22 + *(_DWORD *)v21) % v23) % v23;
v25 = *(_BYTE **)(v21 + 4);
v63[0] = v24;
if ( v25 == *(_BYTE **)(v21 + 8) )
{
sub_6D25F0((const void **)v21, v25, v63);
}
else
{
*(_DWORD *)v25 = v24;
*(_DWORD *)(v21 + 4) += 4;
}
v22 = v54 + 4;
v54 = v22;
}
while ( v22 < 64 );
v26 = *(_BYTE **)v21;
for ( i = 0; i < 32; i += 4 )
{
v60[i] = v26[4 * i];
v60[i + 1] = v26[4 * i + 4];
v60[i + 2] = v26[4 * i + 8];
v60[i + 3] = v26[4 * i + 12];
}
v62 = 915433456;
v63[0] = 392537330;
*(_QWORD *)((char *)&v61 + 4) = 0LL;
v49[0] = 2078981252;
v49[1] = 406946880;
v49[2] = -607402235;
*(_QWORD *)&v50 = 0x9185733C62E8F422uLL;
*((_QWORD *)&v50 + 1) = 0x4DCD4172D214DB83LL;
v51 = 200;
HIDWORD(v61) = 0;
v52 = 0;
index_0 = (int)operator new(0x1Eu);
queue_end = (GameContext *)index_0;
DWORD1(v61) = index_0;
v54 = index_0 + 30;
HIDWORD(v61) = index_0 + 30;
memmove((void *)index_0, v49, 0x1Eu);
DWORD2(v61) = v54;
v29 = v54;
v52 = queue_end;
v64 = 0;
sub_6D13A0(v60, &v62);
v31 = v29 - (_DWORD)v52;
v30 = v31 == 0;
v32 = 64;
v48 = v31;
v58 = 64;
v55 = 0;
v56 = 0;
v33 = 0;
if ( !v30 )
{
v34 = v48;
v35 = 64;
v36 = v52;
do
{
if ( v35 >= 0x40 )
{
sub_6D1530(v59);
for ( j = 0; j < 0x40; j += 16 )
{
v38 = *(_DWORD *)&v59[j];
*(_WORD *)&v57[j] = v38;
v57[j + 2] = BYTE2(v38);
v57[j + 3] = HIBYTE(v38);
v39 = *(_DWORD *)&v59[j + 4];
*(_WORD *)&v57[j + 4] = v39;
v57[j + 6] = BYTE2(v39);
v57[j + 7] = HIBYTE(v39);
v40 = *(_DWORD *)&v59[j + 8];
*(_WORD *)&v57[j + 8] = v40;
v57[j + 10] = BYTE2(v40);
v57[j + 11] = HIBYTE(v40);
v41 = *(_DWORD *)&v59[j + 12];
*(_WORD *)&v57[j + 12] = v41;
v57[j + 14] = BYTE2(v41);
v57[j + 15] = HIBYTE(v41);
}
v34 = v48;
v32 = 0;
v58 = 0;
}
*((_BYTE *)&v36->grid_width + v33++) ^= v57[v32];
v32 = v58 + 1;
v58 = v32;
v35 = v32;
}
while ( v33 < v34 );
queue_end = (GameContext *)index_0;
}
v42 = v54 - (_DWORD)queue_end;
memcpy(a3, queue_end, v54 - (_DWORD)queue_end);
index_3 = a3;
*((_BYTE *)a3 + v42) = 0;
if ( queue_end )
{
v43 = queue_end;
if ( (unsigned int)(HIDWORD(v61) - (_DWORD)queue_end) >= 0x1000 )
{
queue_end = (GameContext *)queue_end[-1].queue_end;
if ( (unsigned int)((char *)v43 - (char *)queue_end - 4) > 0x1F )
invalid_parameter_noinfo_noreturn();
}
return (void *)sub_76F7E3(queue_end);
}
}
return index_3;
}
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
v15 = 0xABFDFFBD;
do
{
multiplier = v47 * (multiplier % v14) % v14;
--v15;
}
while ( v15 );
We can optimize
this piece of code into the following equation
((v47^v15 % v14) * multiplier) % v14
Next there is the calc_1
function which looks like it has been obfuscated
.
__int64 __stdcall calc_1(int a1, int a2)
{
unsigned int v2; // ecx
v2 = 1123996597
* (((int)((unsigned __int64)(512866991LL * a2) >> 32) >> 27)
+ ((unsigned int)((unsigned __int64)(512866991LL * a2) >> 32) >> 31));
return ((int)(a2 - v2)
* (__int64)(int)(a1
- 1123996597
* (((int)((unsigned __int64)(512866991LL * a1) >> 32) >> 27)
+ ((unsigned int)((unsigned __int64)(512866991LL * a1) >> 32) >> 31)))
+ a2
- 1123996597
* ((int)(a2 - v2)
* (__int64)(int)(a1
- 1123996597
* (((int)((unsigned __int64)(512866991LL * a1) >> 32) >> 27)
+ ((unsigned int)((unsigned __int64)(512866991LL * a1) >> 32) >> 31)))
/ 1123996597))
% 1123996597;
}
So we need to convert the above operations, here are the results
import ctypes
def sub_6D1B60( a1: int, a2: int) -> int:
MAGIC_DIVISOR = 512866991
LARGE_PRIME = 1123996597
a1 = ctypes.c_int32(a1).value
a2 = ctypes.c_int32(a2).value
mul_result_a2 = (MAGIC_DIVISOR * a2) & 0xFFFFFFFFFFFFFFFF
high_32_a2 = (mul_result_a2 >> 32) & 0xFFFFFFFF
term1_a2 = ctypes.c_int32(high_32_a2).value >> 27
term2_a2 = (high_32_a2 >> 31) & 1
v2 = LARGE_PRIME * (term1_a2 + term2_a2)
mul_result_a1 = (MAGIC_DIVISOR * a1) & 0xFFFFFFFFFFFFFFFF
high_32_a1 = (mul_result_a1 >> 32) & 0xFFFFFFFF
term1_a1 = ctypes.c_int32(high_32_a1).value >> 27
term2_a1 = (high_32_a1 >> 31) & 1
adjusted_a1 = a1 - LARGE_PRIME * (term1_a1 + term2_a1)
adjusted_a2 = a2 - v2
product = adjusted_a2 * adjusted_a1
division_result = product // LARGE_PRIME
reduction_term = LARGE_PRIME * division_result
result = (product + a2 - reduction_term) % LARGE_PRIME
result = result & 0xFFFFFFFF
return result
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
a1_mod = a1 % MOD
a2_mod = a2 % MOD
result = (a2_mod * a1_mod + a2) % MOD
Continue to the next code
DWORD2(v50) = (int)(index_0
- 1123996597
* (((int)((unsigned __int64)(512866991LL * index_0) >> 32) >> 27)
+ ((unsigned int)((unsigned __int64)(512866991LL * index_0) >> 32) >> 31)))
* (__int64)(int)(v61
- 1123996597
* (((int)((unsigned __int64)(512866991LL * (int)v61) >> 32) >> 27)
+ ((unsigned int)((unsigned __int64)(512866991LL * (int)v61) >> 32) >> 31)))
% 1123996597
* ((v16 + (__int64)SHIDWORD(v61)) % 1123996597)
% 1123996597;
In the same way we can simplify
it to the following operation
index_1_mod = index_1 % MOD
v60_low_mod = v60_low % MOD
sum_mod = (v16 + v60_high) % MOD
result = (index_1_mod * v60_low_mod % MOD * sum_mod) % MOD
Next we reconstruct the entire function to produce the values that are compared in the program.
def calc_1(a1, a2):
MOD = 1123996597
a1_mod = a1 % MOD
a2_mod = a2 % MOD
result = (a2_mod * a1_mod + a2) % MOD
return result & 0xFFFFFFFF
def calc_2(index_1, v60_low, v60_high, v16):
MOD = 1123996597
index_1_mod = index_1 % MOD
v60_low_mod = v60_low % MOD
sum_mod = (v16 + v60_high) % MOD
result = (index_1_mod * v60_low_mod % MOD * sum_mod) % MOD
return result
def encrypt(game_context, vector_data):
v13 = game_context['constant']
v14 = game_context['grid_param1'] * game_context['grid_param2'] + 1
v47 = game_context['rng_seed'] % v14
state = vector_data
current_seed = v13
multiplier = 0xb
v15 = 0x0ABFDFFBD
for round_num in range(10):
current_seed = (pow(multiplier, v15, v14) * current_seed) % v14
temp1 = state[0] ^ calc_1(state[1], current_seed)
v49 = calc_2(state[2], state[0], state[1], state[3])
temp2 = calc_1(state[2], current_seed)
new_state = [
state[2], # done
temp1,
v49,
state[1] ^ temp2 # done
]
state = new_state
return state
vector_data = [0x3A231C05,0x2F28113B,0x341D4029,0x35222E17,0x0B0D0DAB]
game_context = {
'rng_seed': 8,
'grid_param1': 8,
'grid_param2': 8,
'constant': 88403651
}
print(encrypt(game_context, vector_data))
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.
new_state = [
state[2],
temp1,
v49,
state[1] ^ temp2
]
From the resulting state, we can return the value to the previous state
.
old_state[2] = new_state[0]
old_state[1] = new_state[3] ^ calc_1(old_state[2], current_seed)
old_state[0] = new_state[1] ^ calc_1(old_state[1], current_seed)
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].
index_1_mod = index_1 % MOD
v60_low_mod = v60_low % MOD
tmp = index_1_mod * v60_low_mod
tmp %= MOD
tmp2 = (inverse(tmp, MOD) * result) % MOD
tmp2 -= v60_high
tmp2 %= MOD # old_state[3]
The next step is to create a decrypt function, here is the script we created
from Crypto.Util.number import *
def rev_calc_2(index_1, v60_low, v60_high, result):
MOD = 1123996597
index_1_mod = index_1 % MOD
v60_low_mod = v60_low % MOD
tmp = index_1_mod * v60_low_mod
tmp %= MOD
tmp2 = (inverse(tmp, MOD) * result) % MOD
tmp2 -= v60_high
tmp2 %= MOD
return tmp2
def calc_1(a1, a2):
MOD = 1123996597
a1_mod = a1 % MOD
a2_mod = a2 % MOD
result = (a2_mod * a1_mod + a2) % MOD
return result & 0xFFFFFFFF
def calc_2(index_1, v60_low, v60_high, v16):
MOD = 1123996597
index_1_mod = index_1 % MOD
v60_low_mod = v60_low % MOD
sum_mod = (v16 + v60_high) % MOD
result = (index_1_mod * v60_low_mod % MOD * sum_mod) % MOD
return result
def encrypt(game_context, vector_data):
v13 = game_context['constant']
v14 = game_context['grid_param1'] * game_context['grid_param2'] + 1
v47 = game_context['rng_seed'] % v14
state = vector_data
current_seed = v13
multiplier = 0xb
v15 = 0x0ABFDFFBD
for round_num in range(10):
current_seed = (pow(multiplier, v15, v14) * current_seed) % v14
temp1 = state[0] ^ calc_1(state[1], current_seed)
v49 = calc_2(state[2], state[0], state[1], state[3])
temp2 = calc_1(state[2], current_seed)
new_hash = [
state[2], # done
temp1,
v49,
state[1] ^ temp2 # done
]
state = new_hash
return state
def decrypt(game_context, last_state):
v13 = game_context['constant']
v14 = game_context['grid_param1'] * game_context['grid_param2'] + 1
v47 = game_context['rng_seed'] % v14
current_seeds = []
current_seed = v13
v15 = 0x0ABFDFFBD
multiplier = 0xb
for round_num in range(10):
current_seed = (pow(multiplier, v15, v14) * current_seed) % v14
current_seeds.append(current_seed)
state = last_state
for round_num in range(9, -1, -1):
current_seed = current_seeds[round_num]
old_2 = state[0]
old_1 = state[3] ^ calc_1(old_2, current_seed)
old_1 = old_1 & 0xFFFFFFFF
old_0 = state[1] ^ calc_1(old_1, current_seed)
old_0 = old_0 & 0xFFFFFFFF
old_3 = rev_calc_2(old_2, old_0, old_1, state[2])
state = [old_0, old_1, old_2, old_3]
return state
vector_data = [0x3A231C05,0x2F28113B,0x341D4029,0x35222E17,0x0B0D0DAB]
game_context = {
'rng_seed': 8,
'grid_param1': 8,
'grid_param2': 8,
'constant': 88403651
}
last_state = encrypt(game_context, vector_data)
print("check", vector_data == decrypt(game_context, last_state))
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
state = [0x2dca345e, 0xf342d60, 0x2b6a3410, 0x2824d71f]
print(hex(calc_2(state[2], state[0], state[1], state[3]))) # 0x3863bc89
state = [0x2dca345e, 0xf342d60, 0x2b6a3410, 0x6b23aad4]
print(hex(calc_2(state[2], state[0], state[1], state[3]))) # 0x3863bc89
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.
from Crypto.Util.number import *
def rev_calc_2(index_1, v60_low, v60_high, result):
MOD = 1123996597
index_1_mod = index_1 % MOD
v60_low_mod = v60_low % MOD
tmp = index_1_mod * v60_low_mod
tmp %= MOD
tmp2 = (inverse(tmp, MOD) * result) % MOD
tmp2 -= v60_high
tmp2 %= MOD
return tmp2
def calc_1(a1, a2):
MOD = 1123996597
a1_mod = a1 % MOD
a2_mod = a2 % MOD
result = (a2_mod * a1_mod + a2) % MOD
return result & 0xFFFFFFFF
def decrypt(game_context, last_state):
v13 = game_context['constant']
v14 = game_context['grid_param1'] * game_context['grid_param2'] + 1
v47 = game_context['rng_seed'] % v14
current_seeds = []
current_seed = v13
v15 = 0x0ABFDFFBD
multiplier = 0xb
for round_num in range(10):
current_seed = (pow(multiplier, v15, v14) * current_seed) % v14
current_seeds.append(current_seed)
state = last_state
for round_num in range(9, -1, -1):
current_seed = current_seeds[round_num]
old_2 = state[0]
old_1 = state[3] ^ calc_1(old_2, current_seed)
old_1 = old_1 & 0xFFFFFFFF
old_0 = state[1] ^ calc_1(old_1, current_seed)
old_0 = old_0 & 0xFFFFFFFF
old_3 = rev_calc_2(old_2, old_0, old_1, state[2])
state = [old_0, old_1, old_2, old_3]
return state
def rev_conv(game_context, hash_value):
grid_width = 8
grid_height = 8
grid_param1 = game_context['grid_param1']
rng_seed = game_context['rng_seed']
constant = game_context['constant']
modulus = rng_seed * grid_param1 + 1
multiplier = constant % modulus
inv = inverse(multiplier, modulus)
tmp = (inv * hash_value) % modulus
x = tmp // grid_width
y = tmp % grid_width
return [x, y]
game_context = {
'rng_seed': 8,
'grid_param1': 8,
'grid_param2': 8,
'constant': 88403651
}
target_state = [
342066253,
686880041,
828922036,
509868183
]
old_state = decrypt(game_context, target_state)
maps = []
for i in old_state:
tmp = long_to_bytes(i)
for j in tmp:
maps.append(rev_conv(game_context, j))
print(maps)
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
0xda0000
during debugging.Set a breakpoint at
00DA440A
.Set RIP to
0DA4420
.Copy the ecx value from the
00DA1BF0
function call and place it in helper1.py.Run helper1.py.
Set breakpoints at
0DA1CED
and0DA1D0E
.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).
import ida_dbg
func_addr = 0x00DA2220
maps = [[1, 7], [2, 7], [2, 0], [7, 3], [0, 1], [3, 6], [1, 1], [6, 3], [5, 5], [2, 6], [3, 5], [7, 2], [5, 4], [1, 0], [6, 4], [4, 5]]
for i in maps:
args = [0x00FBFBD0, i[0], i[1]]
prototype = "int __thiscall func(void*, int, int);"
idc.SetType(func_addr, prototype)
func_callable = ida_idd.Appcall.proto(func_addr, prototype)
result = func_callable(args[0], args[1], args[2])
print(i, hex(result))
import idaapi
import idc
arr_mul = [56, 31, 16, 46, 51, 41, 61, 21, 36, 6]
start = 0
list_eax = []
for i in range(10 - start):
rv = idaapi.regval_t()
rv.ival = 0x1
idaapi.set_reg_val("ecx", rv)
idaapi.continue_process()
idaapi.wait_for_next_event(idaapi.WFNE_SUSP, -1)
rv = idaapi.regval_t()
rv.ival = arr_mul[start + i]
idaapi.set_reg_val("esi", rv)
idaapi.continue_process()
idaapi.wait_for_next_event(idaapi.WFNE_SUSP, -1)
rv = idaapi.regval_t()
idaapi.get_reg_val('eax', rv)
eax = rv.ival
list_eax.append(hex(eax))
idaapi.continue_process()
idaapi.wait_for_next_event(idaapi.WFNE_SUSP, -1)
print(list_eax)
Flag: ITSEC{P3mbEr51H_R4nJ4U_HanD4L}
Last updated