THIS ARTICLE IS A SPOILER!
TlsCallback_0()
The execution starts with four TLS callbacks.
The first one looks like this:
TlsCallback_0()void __stdcall TlsCallback_0(PVOID DllHandle, DWORD dwReason, int a3) {
MACRO_IMAGE pNativeMachine;
MACRO_IMAGE pProcessMachine;
if ( dwReason == DLL_PROCESS_ATTACH ) {
pNativeMachine = IMAGE_FILE_MACHINE_UNKNOWN;
pProcessMachine = IMAGE_FILE_MACHINE_UNKNOWN;
if ( IsWow64Process2(-1, &pProcessMachine, &pNativeMachine) ) {
if ( pProcessMachine != IMAGE_FILE_MACHINE_I386 ) {
puts("The device is responding negatively to the amount of power the flux charger delivers."
"Quickly, decrease it to a 32-bit process!");
exit(1);
}
if ( pNativeMachine != IMAGE_FILE_MACHINE_AMD64 ) {
puts("The device seems to stream more data than our flux core can process!"
"We have to use a 64-bit flux core processor!");
exit(1);
}
*(_DWORD *)&byte_41A5E8[2] = puts;
*(_DWORD *)&byte_41A39D[2] = exit;
}
}
}
This one simply verifies if the machine and OS are 64bit.
Initially I had issues running the sample on my Windows 7, then moved to Windows 10 VM and had the same issues.
It turned out that IsWow64Process2() is introduced in Windows 10, build 1511, so I had to update my VM.
TlsCallback_1()
Moving to the second TLS Callback here:
TlsCallback_1()void __stdcall TlsCallback_1(int a1, int a2, int a3) {
// trimmed
if ( a2 == 1 ) {
v3 = (int (__stdcall *)(int, int *))sub_4027F0(2, &unk_406464, 193);
v4 = 0;
if ( v3(-1, &v4) ) {
if ( v4 ) {
dword_41A521 = (int)&unk_42B030;
dword_41A530 = -87097621;
memset(&unk_41B000, 0, 0x1002Cu);
dword_41B028 = 0x10000;
sub_405350(&unk_41B000, &unk_41A508);
memset(&unk_41B000, 0, 0x1002Cu);
dword_41B028 = 0x10000;
sub_405350(&unk_41B000, &unk_41A38C);
}
}
}
}
As the crackme name hints, we are about to deal with a virtual machine implementation, and this is the first encounter of it.
But before getting to the VM code, let's analyse this little piece of code, as it will shed a lot of light for the rest of the challenge.
The first anonymous procedure sub_4027F0() suggest that this is a GetProcAddress() implementation, because the result of it is called as a procedure.
Let's get inside:
sub_4027F0()void *__stdcall sub_4027F0(int a1, byte *data, byte key) {
// trimmed
v3 = (int **)*((_DWORD *)NtCurrentPeb()->ImageBaseAddress + 5);
do { // find the requested library by its order
v3 = (int **)*v3;
v5 = v3[4];
--a1;
} while ( a1 );
data_ = data;
for ( i = 0; data[i]; ++i ) // strlen
;
v8 = alloca(i);
v14 = i;
v13 = (char *)v5 + *(_DWORD *)((char *)v5 + *((_DWORD *)v5 + 0xF) + 0x78);
v9 = v15;
do { // XOR decrypt data using key
b = *data_++;
*v9++ = key ^ b;
--i;
} while ( i );
v11 = 0;
while ( memcmp(v15, (char *)v5 + *(_DWORD *)((char *)v5 + 4 * v11 + v13[8]), v14) ) { // iterate exports
if ( ++v11 == v13[6] )
return 0;
}
return (char *)v5 + *(_DWORD *)((char *)v5 + 4 * *(unsigned __int16 *)((char *)v5 + 2 * v11 + v13[9]) + v13[7]);
}
Basically, the buffer from the second argument is XOR-ed with the key from the third.
The rest of the code is pulling library from the PEB and iterating through its exports, until the correct one is found.
I've checked the code references to this procedure.
It turned out they are only there and after decrypting them, they are CheckRemoteDebuggerPresent, NtQueryInformationProcess and NtSetInformationThread.
Back to the TLS callback.
The resolved API procedure in here is CheckRemoteDebuggerPresent, so if the debugger is found, this code inside the if statement will run:
debugger detected code// part 1
dword_41A521 = (int)&unk_42B030;
dword_41A530 = 0xFACEFEEB;
memset(&unk_41B000, 0, 0x1002Cu);
dword_41B028 = 0x10000;
sub_405350(&unk_41B000, &unk_41A508);
// part 2
memset(&unk_41B000, 0, 0x1002Cu);
dword_41B028 = 0x10000;
sub_405350(&unk_41B000, &unk_41A38C);
This TLS callback is obviously an anti-debugging check, and I should avoid falling into that.
However, figuring out how this code here works now, will save me a lot of analysis time in future, so let's see what's going on when a debugger is detected.
The code can be easily split in two similar pieces.
Reversing the first one, will surely help understand the second, so let's get started with sub_405350():
sub_405350()int __usercall sub_405350@(int a1@<eax>, int a2@<edx>, int a3@<ecx>, int a4@<ebp>, int a5@<edi>, int a6@<esi>, _DWORD *a7, int a8) {
// trimmed locals
a7[1] = a1;
a7[2] = a3;
a7[3] = a2;
a7[4] = v9;
a7[5] = &retaddr;
a7[6] = a4;
a7[7] = a6;
a7[8] = a5;
return sub_405340((int)off_408018, (unsigned __int8 *)a8);
}
The variable a7 comes from a constant named unk_41B000, that was initially zeroed (all 0x1002C bytes of it, actually) and by the looks of it, the beginning ten-ish dwords of it are being populated by the current registers.
This obviously initializes a
CONTEXT-like structure that I defined in IDA like so:
STRUCT_VM_CONTEXT structurestruct STRUCT_VM_CONTEXT {
int u1; // unknown
int r_eax;
int r_ecx;
int r_edx;
int r_ebx;
int r_esp;
int r_ebp;
int r_esi;
int r_edi;
int u2; // unknown
int u3; // unknown
byte u4[65536]; // unknown
};
I don't know the purpose of u1 to u4 yet, but u3 was set to 0x10000 before entering that procedure, and u4 size is exactly that big.
This will get clear shortly, so bare with me.
After initializing the VM context structure, the procedure sub_405340() is called with two parameters, so let's check that out:
sub_405340()int __usercall sub_405340@<eax>(int a1@<edi>, unsigned __int8 *a2@<esi>) {
return (*(int (**)(void))(a1 + 4 * *a2))();
}
Nice, so the first argument is a pointer to a vftable, and the second one is the index in that vftable.
I wrote a small parser to list the entire vftable and its indexes for me:
vtable_parse.ida.pyva = 0x00408018
for i in range(0xFF):
proc_va = get_wide_dword(va + i * 4)
if proc_va == 0:
break
print("%02X -> %08X" % (i, proc_va))
This gave me a list of 66 procedures.
I've take a peak at some of them, but the IDA's decompiler yield junky results like this:
sub_404C60() ID:0x22void __usercall sub_404C60(int a1@<ebx@>) {
sub_404040(a1, (int)&loc_404C6F);
sub_404040(a1, (int)sub_4042EF);
JUMPOUT(0x404280);
}
Tracing the code backwards, i know that EBX holds a pointer to the STRUCT_VM_CONTEXT structure, so I've changed that in IDA:
sub_404C60() ID:0x22void __usercall sub_404C60(STRUCT_VM_CONTEXT *context@<ebx>) {
sub_404040(context, sub_404C6F);
sub_404040(context, sub_4042EF);
JUMPOUT(0x404280);
}
And moving inside sub_404040:
sub_404040()void __userpurge sub_404040(STRUCT_VM_CONTEXT *context@<ebx>, void *value) {
context->u3 -= 4;
*(_DWORD *)<context->u4[context->u3] = value;
}
That's not much but helps me understand couple of things.
First of all, since u3 is decremented by 4 and then a field pointed by it in u4 is set with a pointer to a procedure, u4 and u3 are possibly virtual stack related fields.
I can safely assume that sub_404040() is pushing stuff to the virtual stack, so i renamed that procedure to VM_PUSH().
Here It seems like a return chain is getting built, where the the first entry is set to sub_404C6F and the second is set to sub_4042EF.
So what did i learn so far?
I've resolved the virtual machine context structure, and I found a vftable that seems to be the virtual instruction set.
The only thing that I need now is some opcodes.
Let's get back to sub_405350().
This was the place where the virtual context structure was populated before calling sub_405340 (the virtual instruction processor).
I've renamed sub_405350() to vm_process() and sub_405340() to vm_instruction_process().
That makes the code a lot clearer:
vm_process()int __usercall vm_process@<eax>(
void *r_eax@<eax>,
void *r_edx@<edx>,
void *r_ecx@<ecx>,
int r_ebp@<ebp>,
void *r_edi@<edi>,
void *r_esi@<esi>,
STRUCT_VM_CONTEXT *context,
byte *opcode) {
// trimmed
context->r_eax = r_eax;
context->r_ecx = r_ecx;
context->r_edx = r_edx;
context->r_ebx = r_ebx;
context->r_esp = &r_esp;
context->r_ebp = r_ebp;
context->r_esi = r_esi;
context->r_edi = r_edi;
return vm_instruction_process(vm_instruction_set, opcode);
}
Now, going one step back, where this procedure was called here:
TlsCallback_1()dword_41A521 = (int)&unk_42B030;
dword_41A530 = 0xFACEFEEB;
v6 = memset(&context, 0, sizeof(context));
context.stack_ptr = 0x10000;
vm_process(v6, v7, v8, (int)&savedregs, (void *)edi0, (void *)esi0, &context, byte_41A508);
i can get to the opcode buffer, stored at byte_41A508:
opcode.pcode:0041A508 ; byte unk_41A508[25]
.pcode:0041A508 unk_41A508 db 1Dh ; DATA XREF: .text:00402049↑o
.pcode:0041A508 ; .text:004020F4↑o ...
.pcode:0041A509 db 1
.pcode:0041A50A db 3
.pcode:0041A50B db 18h
.pcode:0041A50C db 1Ah
.pcode:0041A50D db 1
.pcode:0041A50E db 3
.pcode:0041A50F db 18h
.pcode:0041A510 db 1
.pcode:0041A511 db 3
.pcode:0041A512 db 14h
.pcode:0041A513 db 35h ; 5
.pcode:0041A514 db 8
.pcode:0041A515 db 0
.pcode:0041A516 db 1Ah
It looks like nothing now, but atleast I have the first opcode index - 0x1D.
Reffering to the vftable from before, 0x1D corresponds to address 00404B90, where sub_404B90() is, so let's take a look inside:
sub_404B90() - Opcode 0x1Dint __usercall sub_404B90@<eax>(STRUCT_VM_CONTEXT *context@<ebx>, byte *opcode@<esi>) {
// trimmed
VM_PUSH(context, &loc_404B9F);
VM_PUSH(context, sub_4042EF);
v2 = *opcode;
v3 = (int *)(opcode + 1);
if ( v2 == 3 ) {
context->u1 = *v3;
} else {
VM_PUSH(context, (void *)*(unsigned __int8 *)v3);
v5 = v4 - 1;
if ( !v5 ) {
VM_PUSH(context, &loc_4042C9);
JUMPOUT(0x404130);
}
if ( v5 == 1 ) {
VM_PUSH(context, &loc_4042C9);
JUMPOUT(0x404160);
}
sub_404060((int)context);
}
v6 = (int (*)(void))sub_404060((int)context);
return v6();
}
This looks a bit sketchy in its decompiled form, but it shows another anonymous function sub_404060(), that I can check:
sub_404060() int __usercall sub_404060@<eax>(STRUCT_VM_CONTEXT *context@<ebx>) {
byte *v1; // edx
v1 = &context->stack[context->stack_ptr];
context->stack_ptr += 4;
return *(_DWORD *)v1;
}
Nice - it's a POP implementation, that I can safely rename to VM_POP().
That doesn't help me much on understanding the opcode parser at sub_404B90(), so let's take a look at it in assembly:
sub_404B90() - Opcode 0x1D.vm:00404B90 sub_404B90 proc near
.vm:00404B90
.vm:00404B90 push offset loc_404B9F ;/ PUSH loc_404B9F to the virtual stack
.vm:00404B95 call VM_PUSH ;\
.vm:00404B9A jmp loc_4042E0
; ...
.vm:004042E0 push offset sub_4042EF
.vm:004042E5 call VM_PUSH ; PUSH sub_4042EF to the virtual stack
.vm:004042EA jmp loc_404280
; ...
.vm:00404280 lodsb ;/ load byte from the opcode buffer
.vm:00404281 movzx ecx, al ;|
.vm:00404284 cmp ecx, 3 ;| compare it with 3
.vm:00404287 jz short loc_4042BD ;\ if the byte is == 3, jump to loc_4042BD
.vm:00404289 lodsb ;/ if the byte wasn't 3, load the next byte
.vm:0040428A movzx eax, al ;|
.vm:0040428D push eax ;|
.vm:0040428E call VM_PUSH ;\ and push it to the virtual stack
.vm:00404293 dec cl ;/ CL--
.vm:00404295 jz short loc_40429F ;\ is CL == 0? then jump to loc_40429F
.vm:00404297 dec cl ;/ CL--
.vm:00404299 jz short loc_4042AE ;\ is CL == 0? then jump to loc_4042AE
.vm:0040429B xor eax, eax
.vm:0040429D jmp short loc_4042C9
.vm:0040429F push offset loc_4042C9 ;/ PUSH loc_4042C9 to the virtual stack
.vm:004042A4 call VM_PUSH ;\
.vm:004042A9 jmp loc_404130
.vm:004042AE push offset loc_4042C9 ;/ PUSH loc_4042C9 to the virtual stack
.vm:004042B3 call VM_PUSH ;\
.vm:004042B8 jmp loc_404160
.vm:004042BD lodsd ;/ load a DWORD from the virtual stack
.vm:004042BE mov [ebx], eax ;| store it to a pointer in EBX
.vm:004042C0 lea eax, [ebx] ;\ and get the pointer to that value in EAX
.vm:004042C2 mov edx, 3
.vm:004042C7 jmp short loc_4042D2
.vm:004042C9 push eax ;/ preserve EAX
.vm:004042CA call VM_POP ;| pop the value from the top of the stack
.vm:004042CF mov edx, eax ;| and store it in EDX
.vm:004042D1 pop eax ;\ restore EAX
.vm:004042D2 push eax ;/ preserve EAX
.vm:004042D3 call VM_POP ;| pop the value from the top of the stack
.vm:004042D8 mov ebp, eax ;| and store it in EBP
.vm:004042DA pop eax ;\ restore EAX
.vm:004042DB jmp ebp ; jump to address stored at EBP
As-is, that's still not super clear, so let's get a bunch of opcode data and apply it to this procedure logic.
I'll take these 8 bytes from the beginning of the opcode buffer:
Opcode data1D 01 03 18 1A 01 03 18
I already know that 0x1D is the opcode of the instruction, that points to sub_404B90(), so I can discard this byte.
Inside sub_404B90() the code pushes loc_404B9F and sub_4042EF to the virtual stack (stored in reverse order).
Then I have to read one byte, which is 0x01 and compare it to 3. Well, it's not 3, so I have to continue to 00404289.
Here I have to take the next byte - 0x03 and store it at the top of the stack.
The stack now looks like this:
virtual stack; top of the stack
stack00 0x03
stack04 sub_4042EF
stack08 loc_404B9F
; bottom of the stack
Now, at 00404293 CL is getting decremented by 1, until it becomes zero.
This will determine if the code will continue from 0040429F (CL==1), 004042AE (CL==2) or 004042C9 (CL > 3).
This whole code looks weird but it's basically a 3 cased switch.
In my case ECX was 1, so the code will jump to 0040429F.
There, loc_4042C9 will be pushed to the top of the stack, and the code will do a unconditional jump to 00404130.
Let's get there now:
loc_404130.vm:00404130 lodsb ;/ read next byte from the opcode buffer
.vm:00404131 movzx eax, al ;|
.vm:00404134 push eax ;|
.vm:00404135 call VM_PUSH ;\ and push it to the virtual stack
.vm:0040413A push offset sub_404149 ;/
.vm:0040413F call VM_PUSH ;\ push sub_404149 to the virtual stack
.vm:00404144 jmp loc_404080
; ...
.vm:00404080 push 4 ;/
.vm:00404082 call sub_404020 ;| unknown procedure
.vm:00404087 lea eax, [ebx+eax] ;\ but its result is used as offset to EBX
.vm:0040408A push eax ;/ preserve EAX
.vm:0040408B call VM_POP ;| pop the top of the stack value
.vm:00404090 mov ebp, eax ;| and store it to EBP
.vm:00404092 pop eax ;\ restore EAX
.vm:00404093 push 4 ;/
.vm:00404095 call sub_404000 ;\ no idea what this is
.vm:0040409A jmp ebp ; jump to EBP
The first chunk of the code is pretty simple, and it pushes the next opcode byte (0x18) and sub_404149 to the stack.
At that point the stack now looks like this:
virtual stack; top of the stack
stack00 sub_404149
stack04 0x18
stack08 loc_4042C9
stack0C 0x03
stack10 sub_4042EF
stack14 loc_404B9F
; bottom of the stack
From 00404080 and below there are two anonymous functions - sub_404020 and sub_404000 that I'll have to check before continuing:
sub_404020()int __userpurge sub_404020@<eax>(STRUCT_VM_CONTEXT *context@<ebx>, DWORD index) {
return *(_DWORD *)&context->stack[context->stack_ptr + index];
}
This one takes the stack value at specified index, so I'll name it VM_STACK_GET.
The second one:
sub_404000()void __userpurge sub_404000(STRUCT_VM_CONTEXT *context@<ebx>, DWORD size) {
context->stack_ptr += size;
}
updates the stack pointer, so that's basically a POP without retrieving the value, and I'll rename it to VM_STACK_ADJUST().
With that information, I can continue from 00404080, where the stack value at index 4 is retrieved.
At stack0 I currently have sub_404149, at stack4 - 0x18, stack8 - 0x03 and so on, so here it takes 0x18 and uses it as index to EBX.
EBX is a pointer to STRUCT_VM_CONTEXT, the field at 0x18 in STRUCT_VM_CONTEXT is r_ebp, so in the end, EAX receives a pointer to STRUCT_VM_CONTEXT.r_ebp
After that, the top of the stack is pop-ed (sub_404149) to EBP and VM_STACK_ADJUST() is called to discard the next top of the stack value - 0x18.
This is basically implementing a SUB ESP, 0x04 that we have after __cdecl calls, to adjust the stack (removing the procedure variables).
By executing this code, my stack looks like this:
virtual stack; top of the stack
stack00 loc_4042C9
stack04 0x03
stack08 sub_4042EF
stack0C loc_404B9F
; bottom of the stack
Finally the code jumps to EBP, which right now holds sub_404149:
sub_404149.vm:00404149 push eax ;/ preserve EAX
.vm:0040414A call VM_POP ;| / pop the top of the stack value
.vm:0040414F mov ebp, eax ;| \ and store it to EBP
.vm:00404151 pop eax ;\ restore EAX
.vm:00404152 jmp ebp ; jump to EBP
I can name this code VM_RET because it basically implements a RET - pop the last stack value - loc_4042C9 and jump to it.
The return address goes to loc_4042C9:
loc_4042C9.vm:004042C9 push eax ;/ preserve EAX
.vm:004042CA call VM_POP ;| / pop the top of the stack
.vm:004042CF mov edx, eax ;| \ and store it to EDX
.vm:004042D1 pop eax ;/ restore EAX
.vm:004042D2 push eax ;/ preserve EAX
.vm:004042D3 call VM_POP ;| / pop the top of the stack
.vm:004042D8 mov ebp, eax ;| \ and store it to EBP
.vm:004042DA pop eax ;/ restore EAX
.vm:004042DB jmp ebp ; jump to EBP
The top of the stack - 0x03 is stored to EDX, and the next stack entry - sub_4042EF, is the return address to the next code.
I can name this code block as VM_POP_EDX and my stack is now holding only one entry:
virtual stack; top of the stack
stack00 loc_404B9F
; bottom of the stack
Continuing to the final code chunk - sub_4042EF():
sub_4042EF().vm:004042EF push eax ;/ preserve EAX
.vm:004042F0 call VM_POP ;|/ pop the top of the stack
.vm:004042F5 mov ebp, eax ;|\ and store it to EBP
.vm:004042F7 pop eax ;\ restore EAX
.vm:004042F8 push offset vm_instruction_process ;/
.vm:004042FD call VM_PUSH ;\ push vm_instruction_process to the top of the stack
.vm:00404302 jmp ebp ; jump to EBP
IDA is already helping me, but showing a procedure name here - vm_instruction_process.
So this code pops the last stack value - loc_404B9F to EBP, but before jumping there it pushes vm_instruction_process to the stack.
My stack now holds only this entry:
virtual stack; top of the stack
stack00 vm_instruction_process
; bottom of the stack
At loc_404B9F we have this code:
loc_404B9F.vm:00404B9F sub edx, 2 ;/ if EDX == 2
.vm:00404BA2 jz short loc_404BAB ;\ jump to loc_404BAB
.vm:00404BA4 dec edx ;/ if EDX == 3
.vm:00404BA5 jz short loc_404BB4 ;\ jump to loc_404BB4
.vm:00404BA7 xor eax, eax ;/ if EDX == 1
.vm:00404BA9 div eax ;\
.vm:00404BAB loc_404BAB:
.vm:00404BAB push small word ptr [eax] ;/ push a WORD from pointer stored in EAX
.vm:00404BAE sub dword ptr [ebx+14h], 2 ;\ subtract 2 from STRUCT_VM_CONTEXT.r_esp
.vm:00404BB2 jmp short loc_404BBA
.vm:00404BB4 loc_404BB4:
.vm:00404BB4 push dword ptr [eax] ;/ push a DWORD from pointer stored in EAX
.vm:00404BB6 sub dword ptr [ebx+14h], 4 ;\ subtract 4 from STRUCT_VM_CONTEXT.r_esp
.vm:00404BBA loc_404BBA:
.vm:00404BBA push eax ;/ preserve EAX
.vm:00404BBB call VM_POP ;|/ pop the top of the stack
.vm:00404BC0 mov ebp, eax ;|\ and store it to EBP
.vm:00404BC2 pop eax ;\ restore EAX
.vm:00404BC3 jmp ebp ; jump to EBP
In my case EDX was 3, so it will push a DWORD from pointer stored in EAX to the real stack.
EAX is still holding a pointer to STRUCT_VM_CONTEXT.r_ebp, and since this is still the first instruction of the virtual opcode, r_ebp holds the real EBP.
By the end of this code chunk, the virtual stack will hold nothing (the last value - vm_instruction_process will be executed by jumping to EBP), but the top of the real stack will hold the initial EBP.
Since the final JUMP is going to the instruction process again, this completes the execution chain of the first virtual instruction - 0x1D.
What did I learn from this, tho?
So far I read only 4 bytes from the virtual opcode buffer:
virtual instruction 0x1D opcodes1D 01 03 18
0x1D - The first byte is the instruction.
0x01 - Is the type. In my case 1 means it's a register
0x03 - is the size, where 3 is a DWORD, 2 is a WORD and 1 is a BYTE
0x18 - Is the offset to the virtual register from STRUCT_VM_CONTEXT.
At the end of the parser, there was a PUSH, so it seems like 0x1D is a virtual instruction PUSH, and if we translate the whole functionality in a simple x86 assembly, the opcode from above does PUSH EBP.
And that also makes sense. Preserving EBP is usually the first instruction of every standard procedure.
Continuing on with the next opcode, that starts with:
Opcode data1A 01 03 18 01 03 14 35 08 00 1A 02
0x1A is the opcode, and if we apply the rule from the previous instruction, the next bytes should be type followed by size and finally offset.
Luckily they are the same as before, so we have manipulation of EBP again (type = 1, size = 3 and offset = 0x18).
Let's take a look insise the virtual instruction parser code, that is located at 00404AA0:
00404AA0 - Opcode 0x1A.vm:00404AA0 push offset loc_404AAF ;/
.vm:00404AA5 call VM_PUSH ;\ push loc_404AAF to virtual stack
.vm:00404AAA jmp loc_404340
; ...
.vm:00404340 push offset sub_40434F ;/
.vm:00404345 call VM_PUSH ;\ push sub_40434F to virtual stack
.vm:0040434A jmp loc_404280
; ...
.vm:00404280 loc_404280:
.vm:00404280 lodsb ;/ get the instruction type
.vm:00404281 movzx ecx, al ;|
.vm:00404284 cmp ecx, 3 ;|
.vm:00404287 jz short loc_4042BD ;\ jump if type == 3
.vm:00404289 lodsb ;/
.vm:0040428A movzx eax, al ;|
.vm:0040428D push eax ;|
.vm:0040428E call VM_PUSH ;\ push the instruction size
.vm:00404293 dec cl
.vm:00404295 jz short loc_40429F
; trimmed
.vm:0040429F loc_40429F:
.vm:0040429F push offset VM_POP_EDX ;/
.vm:004042A4 call VM_PUSH ;\ push VM_POP_EDX to virtual stack
.vm:004042A9 jmp loc_404130
; trimmed
.vm:00404130 loc_404130:
.vm:00404130 lodsb ;/ get the offset
.vm:00404131 movzx eax, al ;|
.vm:00404134 push eax ;|
.vm:00404135 call VM_PUSH ;\ put it to the virtual stack
.vm:0040413A push offset VM_RET ;/
.vm:0040413F call VM_PUSH ;\
.vm:00404144 jmp loc_404080
; trimmed
.vm:00404080 push 4 ;/
.vm:00404082 call VM_STACK_GET ;| get the from the virtual stack offset
.vm:00404087 lea eax, [ebx+eax]
.vm:0040408A push eax ;/ preserve EAX
.vm:0040408B call VM_POP ;|
.vm:00404090 mov ebp, eax ;| get the return address
.vm:00404092 pop eax ;\ restore EAX
.vm:00404093 push 4 ;/
.vm:00404095 call VM_STACK_ADJUST ;\ LEAVE
.vm:0040409A jmp ebp ; go to VM_RET
; ...
.vm:00404149 VM_RET:
.vm:00404149 push eax ;/ preserve EAX
.vm:0040414A call VM_POP ;|
.vm:0040414F mov ebp, eax ;| get the return address
.vm:00404151 pop eax ;\ restore EAX
.vm:00404152 jmp ebp ; go to VM_POP_EDX
; ...
.vm:004042C9 VM_POP_EDX:
.vm:004042C9 push eax ;/ preserve EAX
.vm:004042CA call VM_POP ;|
.vm:004042CF mov edx, eax ;|
.vm:004042D1 pop eax ;\ restore EAX
.vm:004042D2 push eax ;/ preserve EAX
.vm:004042D3 call VM_POP ;|
.vm:004042D8 mov ebp, eax ;| get the return address
.vm:004042DA pop eax ;\ restore EAX
.vm:004042DB jmp ebp ; go to sub_40434F
; ...
.vm:0040434F sub_40434F
.vm:0040434F push edx ;/
.vm:00404350 call VM_PUSH ;\ push EDX (0x03) to the virtual stack
.vm:00404355 push eax ;/
.vm:00404356 call VM_PUSH ;\ push EAX (ptr to STRUCT_VM_CONTEXT.r_ebp) to the virtual stack
.vm:0040435B push offset loc_40436A ;/
.vm:00404360 call VM_PUSH ;\ push loc_40436A to the virtual stack
.vm:00404365 jmp loc_404240
; ...
.vm:00404240 loc_404240:
.vm:00404240 lodsb ;/ type
.vm:00404241 dec al ;|
.vm:00404243 jz short loc_404251 ;\ type == 1?
; trimmed
.vm:00404251 loc_404251:
.vm:00404251 push offset loc_404270 ;/
.vm:00404256 call VM_PUSH ;\ push loc_404270 to the virtual stack
.vm:0040425B jmp loc_4041A0
; trimmed
.vm:004041A0 loc_4041A0:
.vm:004041A0 lodsb ;/
.vm:004041A1 movzx ecx, al ;|
.vm:004041A4 push ecx ;|
.vm:004041A5 call VM_PUSH ;\ push size to the virtual stack
.vm:004041AA push offset sub_4041B9 ;/
.vm:004041AF call VM_PUSH ;\ push sub_4041B9 to the virtual stack
.vm:004041B4 jmp loc_404130
; trimmed
Most of this code looks basically the same as the code for PUSH.
The main difference is that instead of one set of type, size and offset, there are second one.
Again, luckily, the type and size of the second operand are the same as the first one, so i have a 32bit register again, and the offset this time points to r_esp.
The pattern is pretty obvious. Most of this code is just parser, and if we trace back the whole procedure, it seems like the first VM_PUSH pushes the actual operation code to the virtual stack.
For the previous instruction opcode - 0x1D, that address was loc_404B9F:
op_PUSH(); edx is the size
.vm:00404B9F sub edx, 2
.vm:00404BA2 jz short loc_404BAB
.vm:00404BA4 dec edx
.vm:00404BA5 jz short loc_404BB4
.vm:00404BA7 xor eax, eax
.vm:00404BA9 div eax
.vm:00404BAB loc_404BAB:
.vm:00404BAB push small word ptr [eax] ; raw instruction
.vm:00404BAE sub dword ptr [ebx+14h], 2
.vm:00404BB2 jmp short loc_404BBA
.vm:00404BB4 loc_404BB4:
.vm:00404BB4 push dword ptr [eax] ; raw instruction
.vm:00404BB6 sub dword ptr [ebx+14h], 4
.vm:00404BBA loc_404BBA:
.vm:00404BBA push eax
.vm:00404BBB call VM_POP
.vm:00404BC0 mov ebp, eax
.vm:00404BC2 pop eax
.vm:00404BC3 jmp ebp
So the raw instruction is obviously PUSH.
If I apply that logic on the second instruction opcode - 0x1A with address loc_404AAF:
op_PUSH; edx is the size
.vm:00404AAF dec edx
.vm:00404AB0 jz short loc_404ABC
.vm:00404AB2 dec edx
.vm:00404AB3 jz short loc_404AC0
.vm:00404AB5 dec edx
.vm:00404AB6 jz short loc_404AC5
.vm:00404AB8 xor eax, eax
.vm:00404ABA div eax
.vm:00404ABC loc_404ABC:
.vm:00404ABC mov [eax], cl ; raw instruction
.vm:00404ABE jmp short loc_404AC7
.vm:00404AC0 loc_404AC0:
.vm:00404AC0 mov [eax], cx ; raw instruction
.vm:00404AC3 jmp short loc_404AC7
.vm:00404AC5 loc_404AC5:
.vm:00404AC5 mov [eax], ecx ; raw instruction
.vm:00404AC7 loc_404AC7:
.vm:00404AC7 push eax
.vm:00404AC8 call VM_POP
.vm:00404ACD mov ebp, eax
.vm:00404ACF pop eax
.vm:00404AD0 jmp ebp
I can clearly see that 0x1A is actually doing a MOV.
And again, that makes sense for a second instruction in a procedure.
The first instruction was PUSH EBP, and here we have MOV EBP, ESP.
This is a classic procedure entry code!
Alright, time to write some parser for that bytecode.
But before that, there's a very easy way to get instruction opcodes and opcode sizes.
If I set a logging breakpoint at 00405340, I can log every virtual instruction and calculate the size of its operands:
This logs every instruction opcode:
opcode execution8EA508 : 1D
8EA50C : 1A
8EA513 : 35
8EA516 : 1A
8EA525 : 1A
8EA534 : 1D
8EA538 : 1D
8EA53C : 1D
8EA540 : 0E
8EA547 : 1A
8EA550 : 1A
8EA55D : 3E
8EA55F : 0A
8EA568 : 03
8EA56C : 0E
8EA573 : 1A
8EA57A : 0A
8EA583 : 1A
...
The list goes on, since there was clearly a loop in there, so I trimmed it for clarity.
A lot of 0x1A (MOV) and 0x1D (PUSH) in there, but I already know how to decode those, so let's see the code for 0x35:
Opcode 0x35.vm:00405000 lodsw ;/ load a WORD from the opcode buffer
.vm:00405002 movzx eax, ax ;| store it in EAX
.vm:00405005 sub esp, eax ;| subtract from ESP
.vm:00405007 sub [ebx+14h], eax ;\ subtract from the STRUCT_VM_CONTEXT.r_esp
.vm:0040500A jmp vm_instruction_process ; call the parser for the next instruction opcode
This is a SUB ESP, X, that allocates space for local variables.
IDA sometimes decompile this as malloc(), because that's essentially what it does.
Let's check out instruction 0x0E, that sits at 004047E0:
Opcode 0x0E.vm:004047E0 push offset loc_4047EF ; that's the worker code
.vm:004047E5 call VM_PUSH
.vm:004047EA jmp loc_404390
; trimmed parser code
.vm:004047EF loc_4047EF:
.vm:004047EF dec edx
.vm:004047F0 jz short loc_4047FC
.vm:004047F2 dec edx
.vm:004047F3 jz short loc_404801
.vm:004047F5 dec edx
.vm:004047F6 jz short loc_404807
.vm:004047F8 xor eax, eax
.vm:004047FA div eax
.vm:004047FC popf
.vm:004047FD xor [eax], cl ; raw instruction
.vm:004047FF jmp short loc_40480A
.vm:00404801 loc_404801:
.vm:00404801 popf
.vm:00404802 xor [eax], cx ; raw instruction
.vm:00404805 jmp short loc_40480A
.vm:00404807 loc_404807:
.vm:00404807 popf
.vm:00404808 xor [eax], ecx ; raw instruction
So this is XOR instruction.
By rinse and repeat, and few hours later I wrote myself a disassembler in IDApy (download link can be found at the end of this article).
Let's get back to the beginning of this whole adventure, where the virtual machine code was being executed in vm_process().
The bytecode is located at unk_41A508, so I run it through my disassembler and this is what happened:
disassembled unk_41A50800000000 1D 01 03 18 push ebp ;/ procedure epilogue
00000004 1A 01 03 18 01 03 14 mov ebp, esp ;\
0000000B 35 08 00 sub esp, 0x0008 ; allocate locals space
0000000E 1A 02 03 14 00 00 00 00 00 00 03 00 00 00 00 mov dword ptr ds:[esp+00], ref:00402009 ; previously set to unk_42B030
0000001D 1A 02 03 14 00 00 04 00 00 00 03 00 00 00 00 mov dword ptr ds:[esp+04], ref:00402013 ; previously set to 0xFACEFEEB
0000002C 1D 01 03 20 push edi ;/ preserve EDI, ESI and EBX
00000030 1D 01 03 1C push esi ;|
00000034 1D 01 03 10 push ebx ;\
00000038 0E 01 03 04 01 03 04 xor eax, eax ;/ strlen implementation
0000003F 1A 01 03 08 03 FF FF 00 00 mov ecx, 0x0000FFFF ;|
00000048 1A 01 03 20 02 03 18 00 00 F8 FF FF FF mov edi, dword ptr ds:[ebp-08] ;|
00000055 3E 01 repne scasb ;|
00000057 0A 01 03 08 03 FF FF 00 00 sub ecx, 0x0000FFFF ;|
00000060 03 01 03 08 not ecx ;\
00000064 0E 01 03 0C 01 03 0C xor edx, edx ; i = 0
0000006B 1A 01 03 1C 01 03 18 mov esi, ebp ;/ ptr to key
00000072 0A 01 03 1C 03 04 00 00 00 sub esi, 0x00000004 ;\
0000007B 1A 01 03 20 02 03 18 00 00 F8 FF FF FF mov edi, dword ptr ds:[ebp-08] ; ptr to data
00000088 0E 01 03 10 01 03 10 xor ebx, ebx ;/ i_key = 0
0000008F 10 01 03 10 03 04 00 00 00 cmp ebx, 0x00000004 ;| if i_key == 4
00000098 25 03 EA FF FF FF je 00000088 ;| if i_key is 4, jump to zero it
0000009E 1A 01 01 04 02 01 1C 10 01 00 00 00 00 mov eax, byte ptr ds:[esi+04+00+ebx] ;| key[i_key]
000000AB 01 01 03 10 inc ebx ;| i_key++
000000AF 0E 02 01 20 0C 01 00 00 00 00 01 01 04 xor byte ptr ds:[edi+04+00+edx], eax ;| data[i] ^= key[i_key]
000000BC 09 02 01 20 0C 01 00 00 00 00 03 7C 00 00 00 add byte ptr ds:[edi+04+00+edx], 0x7C ;| add 0x8C to data[i]
000000CB 01 01 03 0C inc edx ;| i++
000000CF 10 01 03 0C 01 03 08 cmp edx, ecx ;| loop check
000000D6 26 03 B3 FF FF FF jne 0000008F ;\ loop end
000000DC 1D 01 03 20 push edi ;/ decrypted string data
000000E0 32 03 00 00 00 00 call ref:0040259F ;| fputs()
000000E6 34 04 00 sub esp, 0x0004 ;\ restore __cdecl locals
000000E9 1E 01 03 10 pop ebx ;/ restore EBX, ESI and EDI
000000ED 1E 01 03 1C pop esi ;|
000000F1 1E 01 03 20 pop edi ;\
000000F5 0E 01 03 04 01 03 04 xor eax, eax ;/ procedure prologue
000000FC 34 08 00 sub esp, 0x0008 ;| clear locals space
000000FF 1E 01 03 18 pop ebp ;\ restore ebp
00000103 33 00 00 retn 0x0000
My disassembler implementation is not great, because i didn't bother to understand the whole logic behind parsing displaced addressing.
However, the code is clear enough to show that the data at unk_42B030 is being XOR-ADD decrypted using the key 0xFACEFEEB.
Let's put that to the test with that simple IDApy code:
string decryptorimport struct
def str_decrypt(va_data, key):
key = struct.pack("<L", key)
i = 0
result = ""
while True:
b = get_wide_byte(va_data + i)
if b == 0:
break
b = ((b ^ key[i % 4]) + 0x7C) & 0xFF
result += "%c" % b
i += 1
return result
Running that data i took from the second TLS callback I got the message:
"
You notice a weird sound coming out of the device, so you throw it away in fear!"
Cool, that's the message we'll get if CheckRemoteDebuggerPresent detects the debugger.
There's another virtual machine bytecode executed right after printing that message, that after disassembling looks like this:
VM exit00000000 1D 01 03 18 push ebp ;/ epilogue
00000004 1A 01 03 18 01 03 14 mov ebp, esp ;\
0000000B 1D 03 01 00 00 00 push 0x00000001 ;/ exit code
00000011 32 03 00 00 00 00 call ref:004025A9 ;| referenced to exit()
00000017 34 04 00 sub esp, 0x0004 ;\ restore __cdecl locals
0000001A 0E 01 03 04 01 03 04 xor eax, eax
00000021 1A 01 03 14 01 03 18 mov esp, ebp ;/ prologue
00000028 1E 01 03 18 pop ebp ;\
0000002C 33 00 00 retn 0x0000
Logically, if the debugger is detected, the program will terminate.
This concludes the second TLS callback. I've put up a lot of effort into figuring this out, but it helped me a lot for the rest of the challenge.
TlsCallback_2()
The next TLS callback was using NtQueryInformationProcess with ProcessDebugObjectHandle, to detect a possible debugger running.
The code is basically the same, the only thing that differs is the error message that we get if the debugger is detected.
When decrypting the data at 0042B100 using 0xBAADFC0D as a key, we get the message:
"
A bright red light emerges from the device... it is as if it is scanning us... IT MUST HAVE DETECTED US.... RUN!!!\n"
TlsCallback_3()
The final TLS callback is using NtSetInformationThread with ThreadHideFromDebugger.
If the debugger is detected, the data at 0042B088 will be decrypted using 0xC07C420D as key to print:
"
You noticed the device trying to drill itself into the ground and stopped it - the device self-destructed in defense..."
That concludes the debugging detecting in the TLS callbacks.
They can be easily bypassed by patching the if statement here:
TlsCallback_1().text:004025C0 TlsCallback_1 proc near
; trimmed
.text:004025C0 push ebp
.text:004025C1 mov ebp, esp
.text:004025C3 sub esp, 8
.text:004025C6 mov eax, ___security_cookie
.text:004025CB xor eax, ebp
.text:004025CD mov [ebp+var_4], eax
.text:004025D0 cmp [ebp+arg_4], 1 ; change that to 2
.text:004025D4 jnz loc_40266F ; or change this to JMP
; trimmed
main()
Let's get our hands dirty.
main()int __cdecl main(int argc, const char **argv, const char **envp) {
dword_41A1B3 = (int)_acrt_iob_func(1u);
dword_41A1BD = (int)fputs;
dword_41A1D0 = (int)_acrt_iob_func(0);
dword_41A1E0 = (int)fgets;
dword_41A233 = (int)&unk_419128;
dword_41A3D9 = (int)&unk_419128;
dword_41A3E8 = (int)&unk_419228;
dword_41A3F7 = 1392;
dword_41A458 = (int)sub_401D30;
dword_41A47D = (int)sub_401E00;
dword_41A4A2 = (int)sub_401ED0;
dword_41A4BF = (int)sub_401C70;
memset(&unk_419128, 0, 0x100u);
return sub_402390(argc, argv, envp);
}
Initializing some constants, but overall nothing interesting, so moving to sub_402390:
sub_402390()int __usercall sub_402390@<eax>(void *a1@<edi>, void *a2@<esi>) {
__asm { icebp } // that's something to be checked
dword_41A521 = (int)&unk_4197F8;
dword_41A530 = 0xF00DCAFE;
v2 = memset(&unk_409000, 0, 0x1002Cu);
dword_409028 = 0x10000;
vm_process(v2, v3, v4, (int)&savedregs, a1, a2, (STRUCT_VM_CONTEXT *)&unk_409000, unk_41A508);
v5 = memset(&unk_409000, 0, 0x1002Cu);
dword_409028 = 0x10000;
vm_process(v5, v6, v7, (int)&savedregs, a1, a2, (STRUCT_VM_CONTEXT *)&unk_409000, byte_41A38C);
return sub_402150();
}
This looks promising.
The virtual machine code is again printing a bad message - "
The device released a fury of permafrost, and everything within a 10 metre radius froze solid... including you..." and exits.
Let's see that but in assembly:
sub_402390().text:004023AC mov eax, ___security_cookie
.text:004023BA mov large fs:0, eax ; set exception handler procedure
.text:004023C0 mov [ebp+ms_exc.old_esp], esp
.text:004023C3 mov [ebp+ms_exc.registration.TryLevel], 0
.text:004023CA icebp ; raise exception
.text:004023CB nop ; debugger code
.text:004023CC mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:004023D3 mov eax, 1
.text:004023D8 jmp short loc_4023EC
.text:004023DA loc_4023DA: ; ScopeRecord.FilterFunc
.text:004023DA mov eax, 1
.text:004023DF retn
.text:004023E0 loc_4023E0: ; ScopeRecord.HandlerFunc
.text:004023E0 mov esp, [ebp+ms_exc.old_esp]
.text:004023E3 xor eax, eax
.text:004023E5 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:004023EC loc_4023EC:
.text:004023EC test eax, eax ; exit from the exception handler
.text:004023EE jz short loc_40245B
; bad message and program exit code
That icebp instruction is actually int1, no idea why IDA decodes it like that. Basically, there's some exception handling trickery going on here.
Exceptions are often used as debugger detection, because depending on your debugger, exceptions are either passed to debugger or passed to the code.
When the exception is passed to the debugger, the former handles it (goes to address 004023CB), so the exception handler code from the user code (code at address 004023E0) does not execute.
x64dbg can pass exceptions to the user code, so this trick here can be easily bypassed by setting a breakpoint to 004023EC, then running the code by holding the Shift key.
The exception will be handled by the user code, so at 004023EC eax will be 0, thus, jumping over the bad message.
Or I can just flip the EAX register to 0 at 004023EC...
Passing this debug check successfully, the code continues here:
sub_402150()int __usercall sub_402150@<eax>(void *a1@<edi>, void *a2@<esi>) {
// trimmed
v2 = __readeflags();
__writeeflags(v2 | 0x100); // set the trap flag
dword_41A521 = (int)&unk_4190C8; // "You notice something weird happening in the device... ITS A TRAP! ABORT MISSION! ABORT!!"
dword_41A530 = 0xDEADBEEF;
v3 = memset(&unk_409000, 0, 0x1002Cu);
dword_409028 = 0x10000;
vm_process(v3, v4, v5, (int)&savedregs, a1, a2, (STRUCT_VM_CONTEXT *)&unk_409000, unk_41A508);
v6 = memset(&unk_409000, 0, 0x1002Cu);
dword_409028 = 0x10000;
vm_process(v6, v7, v8, (int)&savedregs, a1, a2, (STRUCT_VM_CONTEXT *)&unk_409000, byte_41A38C);
v9 = sub_401FC0();
switch ( v9 ) {
case 0:
dword_41A521 = (int)&unk_419030; // "The device straight up refuses to respond to you.. THE DEBUGGING IS WEAK IN THIS ONE!"
dword_41A530 = 0xABE8C325;
LABEL_7:
v10 = memset(&unk_409000, 0, 0x1002Cu);
dword_409028 = 0x10000;
vm_process(v10, v11, v12, (int)&savedregs, a1, a2, (STRUCT_VM_CONTEXT *)&unk_409000, unk_41A508);
v13 = memset(&unk_409000, 0, 0x1002Cu);
dword_409028 = 0x10000;
vm_process(v13, v14, v15, (int)&savedregs, a1, a2, (STRUCT_VM_CONTEXT *)&unk_409000, byte_41A38C);
return 1;
case 1:
dword_41A521 = (int)&unk_419798; // "You entered a credential and the device lighted up, but nothing happened... maybe it's broken?"
dword_41A530 = 0x3E6AC524;
goto LABEL_7;
case 2:
v16 = memset(&unk_409000, 0, 0x1002Cu);
dword_409028 = 0x10000;
if ( vm_process(v16, v17, v18, (int)&savedregs, a1, a2, (STRUCT_VM_CONTEXT *)&unk_409000, byte_41A3C0) )
{
dword_41A521 = (int)&unk_419228; // this translates to garbage as-is
dword_41A530 = 0xC1C1C1C1;
v19 = memset(&unk_409000, 0, 0x1002Cu);
dword_409028 = 0x10000;
vm_process(v19, v20, v21, (int)&savedregs, a1, a2, (STRUCT_VM_CONTEXT *)&unk_409000, unk_41A508);
return 0;
}
dword_41A521 = (int)&unk_419088; // ""b̵e̸e̶e̷p̸"... hm.... it seems the machine is broken..."
dword_41A530 = 0x2C545386;
v23 = memset(&unk_409000, 0, 0x1002Cu);
dword_409028 = 0x10000;
vm_process(v23, v24, v25, (int)&savedregs, a1, a2, (STRUCT_VM_CONTEXT *)&unk_409000, unk_41A508);
break;
}
return 1;
}
I've populated some comments with various error messages.
One of them didn't decode correctly, but let's move back to the start of code.
Same as the previous debugger detection by raising an exception, here the trap flag is set.
This is easily seen in assembly:
trap flag exception raise
.text:00402188 ; __try { // __except at loc_4021A1
.text:00402188 mov [ebp+ms_exc.registration.TryLevel], 0
.text:0040218F pushf ; get flags
.text:00402190 or dword ptr [esp], 100h ; set the trap flag bit
.text:00402197 popf ; set the new flags
.text:00402198 nop
.text:00402199 jmp short loc_4021A6
.text:0040219B loc_40219B:
.text:0040219B ; __except filter // owned by 402188
.text:0040219B mov eax, 1
.text:004021A0 retn
.text:004021A1 loc_4021A1:
.text:004021A1 ; __except(loc_40219B) // owned by 402188
.text:004021A1 mov esp, [ebp+ms_exc.old_esp] ;/ exception handler will zero EAX
.text:004021A4 xor eax, eax ;\
.text:004021A4 ; } // starts at 402188
.text:004021A6 loc_4021A6:
.text:004021A6 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:004021AD test eax, eax
.text:004021AF jz short loc_40221C ; jump to the user code
; trimmed
; continue to "You notice something weird happening in the device... ITS A TRAP! ABORT MISSION! ABORT!!" message
Like before, a breakpoint to 004021AD and Shift+F9 let the user code handle the exception and zero EAX.
Continuing down, from the disassembled code I saw that the result of sub_401FC0() could be a value between 0 and 2.
If the value is 0 or 1, the error messages "
The device straight up refuses to respond to you.. THE DEBUGGING IS WEAK IN THIS ONE!" or "
You entered a credential and the device lighted up, but nothing happened... maybe it's broken?" are shown and the program terminates.
If the value is 2 however, depending on the result of another virtual machine procedure, this untranslatable message at unk_419228 or "
"b̵e̸e̶e̷p̸"... hm.... it seems the machine is broken..." will be shown.
The former is obviously not something I'd want, so this virtual procedure should return TRUE.
But first things first.
Right now i would like to know what sub_401FC0() does, and what has to be done in order to return 2:
sub_401FC0()int sub_401FC0() {
result = 0;
__debugbreak();
return result;
}
IDA's decompiler didn't help but the disassembly did:
sub_401FC0().text:00401FC0 push offset sub_401F70 ; exception handler procedure
.text:00401FC5 push large dword ptr fs:0
.text:00401FCC mov large fs:0, esp
.text:00401FD3 xor eax, eax
.text:00401FD5 int 3 ; Trap to Debugger
.text:00401FD6 pop large dword ptr fs:0
.text:00401FDD add esp, 4
.text:00401FE0 retn
The outcome depends on the code in the exception handler, so let's check it out:
sub_401F70().text:00401F70 push ebp
.text:00401F71 mov ebp, esp
.text:00401F73 push esi
.text:00401F74 mov esi, [ebp+arg_8]
.text:00401F77 test esi, esi
.text:00401F79 jz short loc_401FB4
.text:00401F7B push 1002Ch ; Size
.text:00401F80 push 0 ; Val
.text:00401F82 push offset context2 ; void *
.text:00401F87 call memset
.text:00401F8C push offset byte_41A000
.text:00401F91 push offset context2
.text:00401F96 mov ds:dword_409028, 10000h
.text:00401FA0 call vm_process
.text:00401FA5 add esp, 14h
.text:00401FA8 mov [esi+0B0h], eax
.text:00401FAE inc dword ptr [esi+0B8h]
.text:00401FB4 loc_401FB4:
.text:00401FB4 xor eax, eax
.text:00401FB6 pop esi
.text:00401FB7 pop ebp
.text:00401FB8 retn
So there's another virtual machine procedure here, and its opcode is located at byte_41A000.
Let's disassemble it:
byte_41A000 disassembly, pt100000000 31 03 0A 00 00 00 jmp 00000010
00000006 0E 01 03 04 01 03 04 xor eax, eax ;/ return 0
0000000D 33 00 00 retn 0x0000 ;\
00000010 1B 01 03 08 03 30 00 00 00 mov ecx, fs:[0x00000030] ; pointer to 32bit PEB
00000019 1A 01 03 04 02 01 08 00 00 02 00 00 00 mov eax, byte ptr ds:[ecx+02] ;/ PEB.BeingDebugged
00000026 11 01 03 04 01 03 04 test eax, eax ;|
0000002D 26 03 D3 FF FF FF jne 00000006 ;\ jump if debugger is detected
00000033 1A 01 03 04 02 03 08 00 00 68 00 00 00 mov eax, dword ptr ds:[ecx+68] ;/ PEB.NtGlobalFlag
00000040 0F 01 03 04 03 70 00 00 00 and eax, 0x00000070 ;|
00000049 10 01 03 04 03 70 00 00 00 cmp eax, 0x00000070 ;|
00000052 25 03 AE FF FF FF je 00000006 ;\ jump if debugger is detected
00000058 1A 01 03 0C 02 03 08 00 00 18 00 00 00 mov edx, dword ptr ds:[ecx+18] ; pointer to 32bit PEB.ProcessHeap
00000065 1A 01 03 04 02 03 0C 00 00 40 00 00 00 mov eax, dword ptr ds:[edx+40] ;/ PEB.ProcessHeap.Flags
00000072 11 01 03 04 03 02 00 00 00 test eax, 0x00000002 ;|
0000007B 25 03 85 FF FF FF je 00000006 ;\ jump if debugger is detected
00000081 1A 01 03 04 02 03 0C 00 00 44 00 00 00 mov eax, dword ptr ds:[edx+44] ;/ PEB.ProcessHeap.ForceFlags
0000008E 11 01 03 04 01 03 04 test eax, eax ;|
00000095 26 03 6B FF FF FF jne 00000006 ;\ jump if debugger is detected
0000009B 1C 01 03 08 03 60 00 00 00 mov ecx, gs:[0x00000060] ; pointer to 64bit PEB
000000A4 1A 01 03 04 02 01 08 00 00 02 00 00 00 mov eax, byte ptr ds:[ecx+02] ;/ PEB.BeingDebugged
000000B1 11 01 03 04 01 03 04 test eax, eax ;|
000000B8 26 03 48 FF FF FF jne 00000006 ;\ jump if debugger is detected
000000BE 1A 01 03 04 02 03 08 00 00 BC 00 00 00 mov eax, dword ptr ds:[ecx+BC] ;/ PEB.NtGlobalFlag
000000CB 0F 01 03 04 03 70 00 00 00 and eax, 0x00000070 ;|
000000D4 10 01 03 04 03 70 00 00 00 cmp eax, 0x00000070 ;|
000000DD 25 03 23 FF FF FF je 00000006 ;\ jump if debugger is detected
000000E3 1A 01 03 0C 02 03 08 00 00 30 00 00 00 mov edx, dword ptr ds:[ecx+30] ; pointer to 32bit PEB.ProcessHeap
000000F0 1A 01 03 04 02 03 0C 00 00 70 00 00 00 mov eax, dword ptr ds:[edx+70] ;/ PEB.ProcessHeap.Flags
000000FD 11 01 03 04 03 02 00 00 00 test eax, 0x00000002 ;|
00000106 25 03 FA FE FF FF je 00000006 ;\ jump if debugger is detected
0000010C 1A 01 03 04 02 03 0C 00 00 74 00 00 00 mov eax, dword ptr ds:[edx+74] ;/ PEB.ProcessHeap.ForceFlags
00000119 11 01 03 04 01 03 04 test eax, eax ;|
00000120 26 03 E0 FE FF FF jne 00000006 ;\ jump if debugger is detected
; trimmed
I've split the disassembled virtual code for clarity.
The first part is doing yet another debugger detection, this time by looking at a various places in the PEB.
If the debugger is detected, the code will return 0 and this will print the bad message "The device straight up refuses to respond to you.."
Moving on:
byte_41A000 disassembly, pt200000126 1D 01 03 1C push esi ;/ preserve ESI and EDI
0000012A 1D 01 03 20 push edi ;\
0000012E 1D 03 00 00 00 00 push 0x00000000 ;
00000134 1A 01 03 04 01 03 14 mov eax, esp ; get the current stack pointer
0000013B 1D 03 6D 66 75 6F push 0x6F75666D ;/ populate the stack with some data
00000141 1D 03 2E 39 20 3D push 0x3D20392E ;|
00000147 1D 03 20 3D 6F 29 push 0x296F3D20 ;|
0000014D 1D 03 23 3C 6F 3F push 0x3F6F3C23 ;|
00000153 1D 03 21 3B 26 2E push 0x2E263B21 ;|
00000159 1D 03 3D 2A 2B 2A push 0x2A2B2A3D ;|
0000015F 1D 03 2A 67 6D 0C push 0x0C6D672A ;|
00000165 1D 03 3C 23 2E 3B push 0x3B2E233C ;|
0000016B 1D 03 1B 3D 2E 21 push 0x212E3D1B ;|
00000171 1D 03 00 12 75 75 push 0x75751200 ;|
00000177 1D 03 2E 21 61 06 push 0x0661212E ;|
0000017D 1D 03 14 07 3A 22 push 0x223A0714 ;\
00000183 1A 01 03 08 01 03 04 mov ecx, eax ;/ calculate the size of the data
0000018A 0A 01 03 08 01 03 14 sub ecx, esp ;\ pushed to the stack
00000191 0E 02 01 14 08 01 FF FF FF FF 03 4F 00 00 00 xor byte ptr ds:[esp+04-01+ecx], 0x4F ;/ xor the data with 0x4F
000001A0 02 01 03 08 dec ecx ;|
000001A4 26 03 E7 FF FF FF jne 00000191 ;\ loop
000001AA 1A 01 03 04 01 03 14 mov eax, esp ;/ get pointer to the decrypted data
000001B1 1D 03 00 00 00 00 push ref:00402488 ;| FILE *Stream = __acrt_iob_func(1)
000001B7 1D 01 03 04 push eax ;| const char *Buffer
000001BB 32 03 00 00 00 00 call ref:00402494 ;| ref to fputs()
000001C1 34 3C 00 sub esp, 0x003C ;\ free the decrypted stack data
Here, the stack gets populated by values, then a pointer to the stack is used as a buffer that gets XOR-ed with 0x4F.
The result is then output to the console to print the entry message "
[Human.IO]::Translate("Credentials por favor"): "
Next code:
byte_41A000 disassembly, pt3000001C4 35 00 01 sub esp, 0x0100 ;/ allocate stack space
000001C7 1A 01 03 04 01 03 14 mov eax, esp ;/ use the stack as a buffer
000001CE 1D 03 00 00 00 00 push ref:004024A4 ;| FILE *Stream = __acrt_iob_func(0)
000001D4 1D 03 00 01 00 00 push 0x00000100 ;| int MaxCount = 0x100
000001DA 1D 01 03 04 push eax ;| char *Buffer
000001DE 32 03 00 00 00 00 call ref:004024B5 ;| ref to fgets()
000001E4 34 0C 00 sub esp, 0x000C ;\ __cdecl stack adjust
000001E7 0E 01 03 04 01 03 04 xor eax, eax ;/ strlen implementation
000001EE 1A 01 03 08 03 FF FF 00 00 mov ecx, 0x0000FFFF ;|
000001F7 1A 01 03 20 01 03 14 mov edi, esp ;|
000001FE 3E 01 repne scasb ;|
00000200 0A 01 03 08 03 FF FF 00 00 sub ecx, 0x0000FFFF ;|
00000209 03 01 03 08 not ecx ;|
0000020D 02 01 03 08 dec ecx ;\
00000211 1A 02 01 14 08 01 00 00 00 00 03 00 00 00 00 mov byte ptr ds:[esp+04+00+ecx], 0x00000000
00000220 1A 01 03 0C 01 03 08 mov edx, ecx
00000227 1A 01 03 1C 01 03 14 mov esi, esp
0000022E 1A 01 03 20 03 00 00 00 00 mov edi, ref:004024BA ; ref to an empty buffer
00000237 37 01 repne movsb
00000239 1A 01 03 08 01 03 0C mov ecx, edx
00000240 31 03 17 00 00 00 jmp 0000025D
00000246 1A 01 03 04 03 01 00 00 00 mov eax, 0x00000001 ;/ return 1
0000024F 34 00 01 sub esp, 0x0100 ;|
00000252 1E 01 03 20 pop edi ;|
00000256 1E 01 03 1C pop esi ;|
0000025A 33 00 00 retn 0x0000 ;\
0000025D 10 01 03 0C 03 3B 00 00 00 cmp edx, 0x0000003B ;/ user unput length check
00000266 26 03 DA FF FF FF jne 00000246 ;\
is the first user input verification.
All it does is to verify if the user entered exactly 0x3B characters.
Part 4:
byte_41A000 disassembly, pt40000026C 1A 01 03 04 02 01 14 08 01 00 00 00 00 mov eax, byte ptr ds:[esp+04+00+ecx]
00000279 01 02 01 14 08 01 FF FF FF FF inc byte ptr ds:[esp+04-01+ecx]
00000283 16 02 01 14 08 01 FF FF FF FF 03 9F 00 00 00 rol byte ptr ds:[esp+04-01+ecx], 0x0000009F
00000292 0A 02 01 14 08 01 FF FF FF FF 03 0E 00 00 00 sub byte ptr ds:[esp+04-01+ecx], 0x0000000E
000002A1 03 02 01 14 08 01 FF FF FF FF not byte ptr ds:[esp+04-01+ecx]
000002AB 0E 02 01 14 08 01 FF FF FF FF 03 C3 00 00 00 xor byte ptr ds:[esp+04-01+ecx], 0x000000C3
000002BA 04 02 01 14 08 01 FF FF FF FF neg byte ptr ds:[esp+04-01+ecx]
000002C4 09 02 01 14 08 01 FF FF FF FF 03 3E 00 00 00 add byte ptr ds:[esp+04-01+ecx], 0x0000003E
000002D3 17 02 01 14 08 01 FF FF FF FF 03 1D 00 00 00 ror byte ptr ds:[esp+04-01+ecx], 0x0000001D
000002E2 02 02 01 14 08 01 FF FF FF FF dec byte ptr ds:[esp+04-01+ecx]
000002EC 0E 02 01 14 08 01 FF FF FF FF 01 01 04 xor byte ptr ds:[esp+04-01+ecx], eax
000002F9 02 01 03 08 dec ecx
000002FD 26 03 69 FF FF FF jne 0000026C
This is pretty self explanatory, but in general, a various mathematical procedures are applied to each char of the user input.
The user code is actually processed back to forward, and the EAX at the end initially holds the string terminator byte - 0.
On each iteration EAX will be moved one character back, so if at the first iteration EAX is N, the second iteration EAX will hold N-1.
Therefore, at the last XOR, the user code byte will be XOR-ed with the previously encoded byte.
The final code piece looks like this:
byte_41A000 disassembly, pt400000303 1A 01 03 20 01 03 14 mov edi, esp ; EDI is holding the encoded user input
0000030A 1D 03 F5 ED 17 00 push 0x0017EDF5 ;/
00000310 1D 03 F5 ED F5 ED push 0xEDF5EDF5 ;|
00000316 1D 03 FE D6 D5 A5 push 0xA5D5D6FE ;|
0000031C 1D 03 FE D6 FE D6 push 0xD6FED6FE ;|
00000322 1D 03 66 91 C1 C2 push 0xC2C19166 ;|
00000328 1D 03 6E 06 32 5A push 0x5A32066E ;|
0000032E 1D 03 2D 69 29 6D push 0x6D29692D ;|
00000334 1D 03 36 72 6A 2E push 0x2E6A7236 ;|
0000033A 1D 03 65 35 09 35 push 0x35093565 ;|
00000340 1D 03 7B 53 70 8A push 0x8A70537B ;|
00000346 1D 03 DC AC F8 0F push 0x0FF8ACDC ;|
0000034C 1D 03 A8 5B 67 90 push 0x90675BA8 ;|
00000352 1D 03 1F A0 A8 F3 push 0xF3A8A01F ;|
00000358 1D 03 A7 8F C3 88 push 0x88C38FA7 ;|
0000035E 1D 03 8C 9C EB BF push 0xBFEB9C8C ;|
00000364 1A 01 03 1C 01 03 14 mov esi, esp ;\ ESI is holding e pushed data
0000036B 1A 01 03 08 01 03 0C mov ecx, edx ; size of the buffers
00000372 39 01 repne cmpsb ; compare them!
00000374 34 3C 00 sub esp, 0x003C
00000377 26 03 C9 FE FF FF jne 00000246 ; jump if the two buffers are not equal
0000037D 1A 01 03 04 03 02 00 00 00 mov eax, 0x00000002 ;/ return 2
00000386 31 03 C3 FE FF FF jmp 0000024F ;\
0000038C 1D 01 03 18 push ebp ;/ this appears to be a dead code
00000390 1A 01 03 18 01 03 14 mov ebp, esp ;|
00000397 1D 03 01 00 00 00 push 0x00000001 ;|
0000039D 32 03 00 00 00 00 call ref:004025A9 ;| ref to exit()
000003A3 34 04 00 sub esp, 0x0004 ;|
000003A6 0E 01 03 04 01 03 04 xor eax, eax ;|
000003AD 1A 01 03 14 01 03 18 mov esp, ebp ;|
000003B4 1E 01 03 18 pop ebp ;|
000003B8 33 00 00 retn 0x0000 ;\
So, in order to get 2 as a result from this virtual machine procedure, the encoded user data I enter should be equal to that last buffer.
Decoding a valid user code
The easiest way to decode a valid user data for that input that i can think of, is by bruteforicing it.
So I wrote this piece here, that should produce a valid user code:
solution.pyimport struct
rol = lambda val, r_bits: (val << r_bits%8) & 255 | ((val & 255) >> (8-(r_bits%8)))
ror = lambda val, r_bits: ((val & 255) >> r_bits%8) | (val << (8-(r_bits%8)) & 255)
def brute_char(eax, target):
for i in range(0x20, 0x7F):
b = rol((i + 1), 0x9F)
b = -(~(b - 0x0E) ^ 0xC3) + 0x3E
b = ror(b, 0x1D)
b = ((b - 1) ^ eax) & 0xFF
if b == target:
return i
return "?"
token = [0x0017EDF5, 0xEDF5EDF5, 0xA5D5D6FE, 0xD6FED6FE,\
0xC2C19166, 0x5A32066E, 0x6D29692D, 0x2E6A7236,\
0x35093565, 0x8A70537B, 0x0FF8ACDC, 0x90675BA8,\
0xF3A8A01F, 0x88C38FA7, 0xBFEB9C8C]
data = b""
for chunk in token[::-1]:
data += struct.pack("<L", chunk)
result = ""
eax = 0
for b in data:
result += "%c" % brute_char(eax, b)
eax = b
print(result[1:])
I was surprised that actually worked and produced something meaningful:
valid user code[Alien.IO]::Translate("Skrr pip pop udurak reeeee skiiiii")
I've tried it in the app, and it worked:
Well, I got the flag.
Final bits
There is a little more work left, tho.
By entering the valid user code the code checker procedure sub_401F70() returned 2.
But there was another virtual machine procedure there that also had to return TRUE, so let's check it out.
The disassembled code looks like this:
0041A3C000000000 1D 01 03 18 push ebp
00000004 1A 01 03 18 01 03 14 mov ebp, esp
0000000B 35 0C 00 sub esp, 0x000C
0000000E 1A 02 03 14 00 00 00 00 00 00 03 00 00 00 00 mov dword ptr ds:[esp+00], ref:004024C4 ; ref to copy of the encoded user code
0000001D 1A 02 03 14 00 00 04 00 00 00 03 00 00 00 00 mov dword ptr ds:[esp+04], ref:004024CE ; ref to a buffer of encrypted data
0000002C 1A 02 03 14 00 00 08 00 00 00 03 00 00 00 00 mov dword ptr ds:[esp+08], ref:004024D8 ; ref to size of the encrypted buffer (0x570)
0000003B 1D 01 03 20 push edi
0000003F 1D 01 03 1C push esi
00000043 1D 01 03 10 push ebx
00000047 0E 01 03 04 01 03 04 xor eax, eax ;/ strlen
0000004E 1A 01 03 08 03 FF FF 00 00 mov ecx, 0x0000FFFF ;|
00000057 1A 01 03 20 02 03 18 00 00 F4 FF FF FF mov edi, dword ptr ds:[ebp-0C] ;|
00000064 3E 01 repne scasb ;|
00000066 0A 01 03 08 03 FF FF 00 00 sub ecx, 0x0000FFFF ;|
0000006F 03 01 03 08 not ecx ;|
00000073 1A 01 03 10 01 03 08 mov ebx, ecx ;\ store the length in EDX
0000007A 35 10 00 sub esp, 0x0010 ;/ allocate 0x10 bytes
0000007D 1A 01 03 1C 01 03 14 mov esi, esp ;|
00000084 1D 01 03 1C push esi ;| hash
00000088 1D 01 03 10 push ebx ;| data size
0000008C 1D 02 03 18 00 00 F4 FF FF FF push dword ptr ds:[ebp-0C] ;| data
00000096 32 03 00 00 00 00 call @MD5 ;| perform MD5 hashing
0000009C 34 0C 00 sub esp, 0x000C ;\ __cdecl stack adjust
0000009F 35 20 00 sub esp, 0x0020 ;/ allocate 0x20 bytes
000000A2 1A 01 03 20 01 03 14 mov edi, esp ;|
000000A9 1D 01 03 20 push edi ;| hash
000000AD 1D 01 03 10 push ebx ;| data size
000000B1 1D 02 03 18 00 00 F4 FF FF FF push dword ptr ds:[ebp-0C] ;| data
000000BB 32 03 00 00 00 00 call @SHA256 ;| perform SHA256 hashing
000000C1 34 0C 00 sub esp, 0x000C ;\ __cdecl stack adjust
000000C4 1D 01 03 1C push esi ;/ iv
000000C8 1D 01 03 20 push edi ;| key
000000CC 1D 02 03 18 00 00 FC FF FF FF push dword ptr ds:[ebp-04] ;| data size
000000D6 1D 02 03 18 00 00 F8 FF FF FF push dword ptr ds:[ebp-08] ;| data
000000E0 32 03 00 00 00 00 call @AES ;| perform AES decrypt
000000E6 34 40 00 sub esp, 0x0040 ;\ __cdecl stack adjust
000000E9 1D 02 03 18 00 00 FC FF FF FF push dword ptr ds:[ebp-04] ;/ data size
000000F3 1D 02 03 18 00 00 F8 FF FF FF push dword ptr ds:[ebp-08] ;| data
000000FD 32 03 00 00 00 00 call @CRC32 ;| perform CRC32 hashing
00000103 34 08 00 sub esp, 0x0008 ;\ __cdecl stack adjust
00000106 1A 01 03 10 01 03 04 mov ebx, eax
0000010D 0E 01 03 04 01 03 04 xor eax, eax
00000114 10 01 03 10 03 F6 C9 A2 4D cmp ebx, 0x4DA2C9F6 ; check if the crc32 hash is correct
0000011D 26 03 09 00 00 00 jne 0000012C
00000123 1A 01 03 04 03 01 00 00 00 mov eax, 0x00000001
0000012C 1E 01 03 10 pop ebx
00000130 1E 01 03 1C pop esi
00000134 1E 01 03 20 pop edi
00000138 34 0C 00 sub esp, 0x000C
0000013B 1E 01 03 18 pop ebp
0000013F 33 00 00 retn 0x0000
This simply decrypts the "congrats" screen.
The key and iv for the AES encryption are made by MD5 and SHA256 hashes of the user code.
This is easy to verify by using the following code:
flag_secrypt.ida.pyimport struct
import hashlib
from Crypto.Cipher import AES
def str_decrypt(data, key):
key = struct.pack("<L", key)
result = ""
for i, b in enumerate(data):
b = ((b ^ key[i % 4]) + 0x7C) & 0xFF
result += "%c" % b
return result
data = get_bytes(0x00419228, 0x570)
seed = b"[Alien.IO]::Translate(\"Skrr pip pop udurak reeeee skiiiii\")"
key = hashlib.sha256(seed).digest()
iv = hashlib.md5(seed).digest()
data = AES.new(key, mode=AES.MODE_CBC, iv=iv).decrypt(data)
data = str_decrypt(data, 0xC1C1C1C1)
print(data)
and the produced result:
flag message ______ ______
/___ \___\ || /___/ ___\
//\]/\ ___ \\||// ___ /\[/\\
\\/[\// _) \/ (_ \\/]\//
\___/ _/ o o \_ \___/
_/ \_
//'VvvvvvvvvvvvvvvV'\\
( \.'^^^^^^^^^^^^^^'./ )
\____ . .. . ____/
________ \ . .''. . / ________
/______ \________)________(________/ _______\
/| \ \ / / |\
(\|____ / / \ \ ____|/)
(\_____>- \/ \/ -<_____/)
(\_____>- -<_____/)
\_____>- -<_____/
| HELLO HUMAN |
| WE ARE HERE TO RETRIEVE OUR CHILD |
| YOU KNOW HIM AS E.T. |
| |
| THANK YOU FOR REVEALING |
| OUR GOOD INTENTIONS |
| |
| HTB{V1RTU4L_M4CH1N35_G035_BRRRRRR!11!1!1!} |
|____________________________________________|
/ ) ( \
/ / \ \
/ / / /\ \ / /\ \ \ \
( ( ( ( ( ) ( ) ) ) ) )
'v'v'v'v'(_) (_)'v'v'v'v'
\) (/
All of the hashing/encryption algorithms were stored as standard non obfuscated procedures.
The MD5 and SHA256 were easy to detect due to the Windows API implementation:
MD5()int __cdecl MD5(BYTE *data, DWORD size, void *md5_hash) {
// trimmed
v3 = 0;
phProv = 0;
phHash = 0;
if ( CryptAcquireContextW(&phProv, 0, 0, PROV_RSA_AES, 0xF0000020) ) {
if ( CryptCreateHash(phProv, CALG_MD5, 0, 0, &phHash) ) {
if ( CryptHashData(phHash, data, size, 0) ) {
pdwDataLen = 16;
*(_OWORD *)Src = 0i64;
if ( CryptGetHashParam(phHash, 2u, Src, &pdwDataLen, 0) ) {
memcpy(md5_hash, Src, pdwDataLen);
v3 = 1;
}
}
CryptDestroyHash(phHash);
}
CryptReleaseContext(phProv, 0);
}
return v3;
}
The Crc32 algorithm was also very easy to spot, due to its starting polynomial 0xEDB88320:
CRC32()int __cdecl CRC32(byte *data, DWORD size) {
// trimmed
result = -1;
for ( i = 0; i < size; result = (v8 >> 1) ^ -(v8 & 1) & 0xEDB88320 ) {
v4 = data[i++];
v5 = ((v4 ^ result) >> 1) ^ -(((unsigned __int8)v4 ^ (unsigned __int8)result) & 1) & 0xEDB88320;
v6 = (((v5 >> 1) ^ -(v5 & 1) & 0xEDB88320) >> 1) ^ -(((unsigned __int8)(v5 >> 1) ^ -(v5 & 1) & 0x20) & 1) & 0xEDB88320;
v7 = (((v6 >> 1) ^ -(v6 & 1) & 0xEDB88320) >> 1) ^ -(((unsigned __int8)(v6 >> 1) ^ -(v6 & 1) & 0x20) & 1) & 0xEDB88320;
v8 = (((v7 >> 1) ^ -(v7 & 1) & 0xEDB88320) >> 1) ^ -(((unsigned __int8)(v7 >> 1) ^ -(v7 & 1) & 0x20) & 1) & 0xEDB88320;
}
return ~result;
}
Finally, I was able to detect the AES implementation by the algorithm's S-box and transformation constants.
Well, that's all folks!
Appendix
A badly written disassembler for the virtual machine opcode can be downloaded here:
http://nullsecurity.org/download/396eac60dcea4005be0c19530072048e
Comments
* You have an opinion? Let us all hear it!