Join the discord

hackthebox.eu/reversing: vmcrack

11 Jul, 2021 12:27
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!

Guest 12 Jul, 2024 13:29
the chall has been retired please publish writeup
© nullsecurity.org 2011-2024 | legal | terms & rules | contacts