Baylen Orr

File Analysis - 2023:02:07

Network indicators on this page are defanged (e.g. example[.]com) so they can't be clicked by accident.

Samples can be downloaded here

Files

  1. AFFAERENTRI.exe.7z (19294a8f3982807f6119e9720e8fde7c21fdc5c13be427f2c6fd130a7b532bec)
  2. shellode.bin (9715179183b1c151f87eb6e32c0dcfec152568bf34e99093606f79b8498c3c8e)

SUMMARY

The sample AFFAERENTRI.exe is a Visual Basic wrapper that contains embedded shellcode which is responsible for loading and decrypting a payload into a new memory region and then passing execution to the decrypted payload. This decrypted payload is a GuLoader loader. We reverse engineered some of the sophisticated anti-analysis routines present in GuLoader to develop TTPs for identifying and bypassing these defenses against malware analysts.

AFFAERENTRI.exe (Stage-0)

Stage-0 is a Visual Basic (v6) wrapper executable that unpacks and passes execution to a GuLoader shellcode loader which we extract and identify below. Retrieving GuLoader from Stage-0 can be tricky and requires the use of a debugger in this sample. Stage-0 decodes GuLoader shellcode into a region of allocated memory. Due to the nature of the Visual Basic virtual machine, setting breakpoints on VirtualAlloc from the beginning is not feasible, because the Visual Basic virtual machine will make calls to VirtualAlloc very many times to set itself up during execution.

For this particular sample, we set a breakpoint on VirtualProtect instead and waited for the program to self terminate. This happens as the shellcode contains many anti-VM and anti-analysis functions. To locate the beginning of the shellcode, the following heuristic may help:

  1. Breakpoint on VirtualProtect
  2. Execute until program self terminates, counting the number of times (N) VirtualProtect was hit
  3. Restart debugging session
  4. Breakpoint on VirtualProtect
  5. Continue execution, and set a breakpoint on VirtualAlloc at N-1 VirtualProtect hit break points.

For our sample, the above heuristic let up wait until after the second hit on VirtualProtect. After the second hit on VirtualProtect we set a breakpoint for VirtualAlloc and monitored memory regions until we saw one populated.

Unpacking GuLoader

Following the heuristic above to retrieve the shellcode, a quick observation will reveal the dumped data is not legitimate shellcode. Stage-0 contains its own shellcode to decode the GuLoader shellcode before executing it. These instructions are padded with 0xff bytes and begin at 0x2e35 in the Stage-0 binary. The structure of the Stage-0 shellcode is made up of several nonsense instructions (ie: mov esi,esi), a purposeful instruction, and a JMP instruction to the next block. The following excerpt contains the embedded shellcode in Stage-0 minus the 0xff bytes.

;Begin Loop:
    00403E8B   | 89F6             | mov esi,esi
    00403E8D   | EB 2E            | jmp guLoader.00403EBD

    ;Decrypt 4 bytes:
    00403EBD   | 89C9             | mov ecx,ecx
    00403EBF   | 89F6             | mov esi,esi
    00403EC1   | 813408 739832D3  | xor dword ptr ds:[eax+ecx],D3329873
    00403EC8   | 90               | nop
    00403EC9   | EB 2D            | jmp guLoader.00403EF8

    ;Increment Counter:
    00403EF8   | 89C9             | mov ecx,ecx
    00403EFA   | 89F6             | mov esi,esi
    00403EFC   | 41               | inc ecx
    00403EFD   | 90               | nop
    00403EFE   | EB 40            | jmp guLoader.00403F40

    ;Nonsense Jump:
    00403F40   | 89C9             | mov ecx,ecx
    00403F42   | 90               | nop
    00403F43   | 90               | nop
    00403F44   | EB 40            | jmp guLoader.00403F86

    ;Increment Counter again:
    00403F86   | 41               | inc ecx
    00403F87   | 89C9             | mov ecx,ecx
    00403F89   | 89DB             | mov ebx,ebx
    00403F8B   | 89FF             | mov edi,edi
    00403F8D   | EB 03            | jmp guLoader.00403F86

    ;Increment Counter again:
    00403F86   | 41               | inc ecx
    00403F87   | 89C9             | mov ecx,ecx
    00403F89   | 89DB             | mov ebx,ebx
    00403F8B   | 89FF             | mov edi,edi
    00403F8D   | EB 03            | jmp guLoader.00403FB3

    ;Increment Counter again, check the size against
    ; ebp+94 (which in our case points to 0x0019f77c,
    ; which holds the value 0x8000):
    00403FB3   | 41               | inc ecx
    00403FB4   | 3B8D 94000000    | cmp ecx,dword ptr ss:[ebp+94]
    00403FBA   | 0F85 CBFEFFFF    | jne guLoader.00403E8B
    00403FC0   | 90               | nop
    00403FC1   | EB 37            | jmp guLoader.00403FFA

    ;Jump to shellcode entry (jmp EAX). In this case it
    ; is the beginning of the dumped shellcode.
    00403FFA   | 89C9             | mov ecx,ecx
    00403FFC   | 89FF             | mov edi,edi
    00403FFE   | 89F6             | mov esi,esi
    00404000   | FFE0             | jmp eax

(Figure 1)

Shellcode Entry

The shellcode is unpacked in the following manner before execution is passed to the GuLoader entry at 0x00404000. By breaking at the NOP instruction at 0x00403FC0 you can easily single step into the entry following the JNZ instruction that signifies the end of the decryption loop. Note that the XOR key for this sample is 0xD3329873.

GULOADER ANTI-ANALYSIS ROUTINES

The GuLoader designers added effective stack manipulation instructions to confuse disassembly tools such as Ghidra or Ida. After loading the dumped shellcode into Ghidra, many portions are incorrectly analyzed as code when they should be strings. This report does not make mention of all of these, as there are too many to cover and GuLoader's stack manipulation effectiveness will vary against different disassembly tools. For examples discussed here, see Figures A and B at the end of this report (Note: We used Ghdira during our analysis of this sample).

Heaven's Gate Check

At 0x2bf7 the shellcode does a Heaven's Gate check to ensure the code isn't running in a 32-bit process. This will break the debugging session because the x32dbg process is 32-bit, as the initial Visual Basic application is 32-bit. To continue debugging, modify the value in EBX before the CMP instruction at 0x2c01 is executed.

API Obfuscation

A routine that handles both module and API hashes is located at 0x2312. There is no standard calling convention for passing arguments to this routine. Before the routine is called, the API and module string are moved into the EDX and ECX registers respectively. A command switch is loaded into the EAX register. If the switch is 0x539, the function only attempts to locate the API without walking the PEB. Otherwise, the function iterates though the PEB to resolve Kernel33!LoadLibraryA where a handle is moved into a variable and then subsequently called via CALL dword ptr [ebp + n].

/**** FUN_00002312 Caller ****/
    000026bf     MOV      EAX, 0x539          ; Switch
    000026c4     MOV      ECX, dword ptr [EBP + 0x1c]  ; Module string
    000026c7     MOV      EDX, 0x8802edac     ; API hash
    000026cc     CALL     FUN_00002312

    /**** FUN_00002312 Epilogue ****/
    00002312     PUSH     ESI
    00002313     PUSH     EDX         ; API hash onto stack
    00002314     PUSH     ECX         ; Module string onto stack
    00002315     TEST     BL, BL
    00002317     TEST     CH, 0xa8
    0000231a     CMP      EAX, 0x539

(Figure 2)

The discrete PUSH instructions in the epilogue later set the stack up for a successful call to compare hashes from the function at 0x2378.

The function at 0x2378 calls a DJB hash routine which is easily identifiable via the known constants 0x1505 (5381 decimal) and 0x5. Strings used in the shellcode have had the DJB hashes precomputed and hard-coded into the shellcode in lieu of strings. The shellcode will walk the PEB and hash the string names of modules, and compare the hash with the argument passed.

Anti-Virtual Machine

An anti-VM check is made early on during GuLoader's execution. This happens at a jump to 0x1d9e. This function then passes 8 hashes to a child function. The disassembly appears as seen in Figure C, Appendices. This routine is easily identifiable by the 8 PUSH instructions followed by a CALL instruction.

To check for VM related strings, GuLoader first calls ZwQueryVirtualMemory() via the aforementioned hash routine. This gets a handle to ZwQueryVirtualMemory() and places it into a variable at 0x26e5. This happens via a MOV dword ptr [EDI + local], EAX, where EAX contains the result of GetProcAddress(main_hash_wrapping_function(0x8802edac)).

The function then iterates through all regions of memory and parses the structure returned by ZwQueryVirtualMemory(). The full disassembly is complicated and a portion has been annotated in Figure D, Appendices. The flow of the function however is simple, and contains multiple nested loops. The important take away is that to continue debugging, the most effective measure seems to be to change the hash values on the stack so that there will never be a match. This is because the loops exit via jump instructions to other loops. If a match is found and GuLoader decides to initiate the self-termination sequence, it simply fails to branch out of the last loop to the caller loop, and continues executing the remaining instructions from 0x27c0 near the end of the function.

CPU and Timing Checks

GuLoader employs multiple uses of CPUID and RDTSC instructions in a series of loops and complex checks to thwart sandbox timing defenses and any debugging efforts. The parent routine for these checks begins with a CALL FUN_00002558 at address 0x10b. This function calls a subroutine numerous times, probably most likely to fool automated sandboxes. The subroutine (located at 0x000025be) recursively calls itself. In this manner numerous calls to CPUID and RDTSC are made. If a timing condition fails, the function branches to a program termination sequence.

000025be     CALL     rdtsc_check          ; routine to do RDTSC
    000025c3     MOV      ESI, EDX             ; copy return to ESI
    000025c5     PUSHAD

    000025c6     MOV      EAX, 0x1             ; argument for CPUID
    000025cb     CPUID
    000025cd     BT       ECX, 0x1f
                                               ; Modify JC to NOP instructions
    000025d1     JC       FUN_00000281::KILL_PROGRAM
    000025d7     POPAD
    000025d8     CALL     rdtsc_check
    000025dd     SUB      EDX, ESI
    000025df     CMP      EDX, 0x0
    000025e2     JLE      rdtsc_check_wrapper  ; repeat routine
    000025e4     RET

(Figure 3)

By changing the bytes located at offsets 0x25D1 to 0x25d6 to x90 we can continue debugging out of the CPU and timing checks meant to thwart analysis.

> Analyst Note: There may be another patch necessary while debugging. Before the assembly in Figure 3 is executed, there is a call to a function that is structured much in the same way, although the x86 calls are to CPUID instead of RDTSC. There is a JGE at 0x2610 that compares the execution (stored in EDI) against 0xe4e1c0 (15000000 nanoseconds). If for some reason it took longer, you can end up in an infinite loop. The system time is retrieved in a subroutine that calls CPUID and RDTSC at 0x263f. See the Ghidra file for more information. Some names have been renamed.

It is important to recap that the anti-analysis routines above are all contained in the same wrapper function at 0xcf (see Figure C, Appendices).

Anti-Debugging

After the above anti-analysis checks are passed, a routine at 0x1b5 begins to both check for an attached Debugger as well and patch the Ntdll!DgbBreakPoint() and Ntdll!DbgRemoteUiBreakin() APIs. The value 0x82962c8 is passed to the main_hash_wrapping_function and returns ZwProtectVirtualMemory(). The ZwProtectVirtualMemory API is passed to a routine at 0x1e2 which is responsible for patching NTDLL. We renamed this routine to anti_debug_patch. This routine begins at offset 0x28ce in our sample. Within the anti_debug_patch() function the aforementioned Debug APIs are resolved via the same main_hash_wrapping_function and their addresses are stored in local variables. Eventually a subroutine is called at 0x2965 and takes the argument 0x40 to change the permissions to "WCX".

00002963     PUSH     0x40     ; Protections Argument  ; WCX permsisions
    00002965     CALL     change_ntdll.text_protections    ; Subroutine to change protections
        /**
        * Omitted for brevity
        **/
    00002988     MOV      EAX, dword ptr [ESP + param_6]   ; pointer to ntdll.DbgBreakPoint
    0000298c     MOV      byte ptr [EAX], 0x90             ; Change ntdll.DbgBreakPoint to NOP
    0000298f     TEST     CL, 0x4a
    00002992     MOV      EAX, dword ptr [ESP + param_7]   ; pointer to DbgRemoteUiBreakin
    00002996     MOV      byte ptr [EAX], 0x6a             ; Next three instructions patch with fake opcodes to
                                                           ; cause a crash
    00002999     MOV      byte ptr [EAX + 0x1], 0x0
    0000299d     MOV      byte ptr [EAX + 0x2], 0xb8
    000029a1     TEST     CX, CX
    000029a4     TEST     DX, AX
    000029a7     MOV      EDX, dword ptr [EBP + 0x138]     ; Kernel32.ExitProcess
    000029ad     MOV      dword ptr [EAX + 0x3], EDX       ; Patch ntdll.DbgBreakPoint with Kernel32.ExitProcess
    000029b0     MOV      byte ptr [EAX + 0x7], 0xff
    

(Figure 4)

Anti-Endpoint Detection

The anti_debug_patch routine is also responsible for traversing NTDLL and looking for any hooks and either restoring them to the system original or replacing the with NOP instructions. Most Endpoint Detection systems employ numerous hooks on functions in NTDLL and KERNEL32 to observe behaviors. In Figure 5 we see a scan for the common byte sequence for hooking. In the accompanying Ghidra file you may observe similar loops that implement a MOV instruction to restore values to the system defaults. The condition at 0x00002b27 will jump to the NOP loop if satisfied.

00002b21     TEST     DL, 0x20
    00002b24     INC      EBX
    00002b25     CMP      EBX, EAX
    00002b27     JZ       LAB_00002b85
    00002b29     CMP      byte ptr [EBX], 0xb8
    00002b2c     JNZ      LAB_00002b21
    00002b2e     CMP      dword ptr [EBX + 0x1], 0x0
    00002b32     JNZ      LAB_00002b21
    00002b34     CMP      byte ptr [EBX + 0x5], 0xb9
    00002b38     JNZ      LAB_00002b21

(Figure 5)

APPENDICES

Figures A and B demonstrating results of stack manipulation to confuse reverse engineering tools.

00002c22     CALL     FUN_00002c08
    00002c27     PUSH     EAX
    00002c28     POP      EAX
    00002c29     PUSH     EDX
    00002c2a     INC      ECX
    00002c2b     PUSH     EAX
    00002c2c     INC      ECX
    00002c2d     PUSH     ECX
    00002c2e     PUSH     EAX
    00002c2f     POP      EAX

    /**** Code bytes cleared reveals a string ****/

    00002c22     CALL     FUN_00002c08
    00002c27     ??       50h    P
    00002c28     ??       58h    X
    00002c29     ??       52h    R
    00002c2a     ??       41h    A
    00002c2b     ??       50h    P
    00002c2c     ??       41h    A
    00002c2d     ??       51h    Q
    00002c2e     ??       50h    P

(Figure A)

                     LAB_00001d9e
    00001d9e     ??       E8h
    00001d9f     ??       2Ch    ,
    00001da0     ??       E3h
    00001da1     ??       FFh
    00001da2     ??       FFh
    00001da3     ds       "ntdll"
                       LAB_00001da9

    00001da9     ??       E8h
    00001daa     ??       0Ah
    00001dab     ??       F7h
    00001dac     ??       FFh
    00001dad     ??       FFh
    00001dae     unicode  u"windir="

    /* By manually disassembing the code bytes
    * we can see there is are function calls
    * preluding the strings.
    */

                       LAB_00001d9e
    00001d9e     CALL     FUN_000000cf
    00001da3     ds       "ntdll"
                       LAB_00001da9

    00001da9     CALL     FUN_000014b8
    00001dae     unicode  u"windir="

(Figure B)

000000cf     POP      ECX
    000000d0     MOV      dword ptr [EBP + 0x1c], ECX
    000000d3     PUSH     0x0=>local_res0
    000000d5     PUSH     0xb314751d
    000000da     PUSH     0xa7c53f01
    000000df     TEST     BH, CH
    000000e1     PUSH     0x7f21185b
    000000e6     PUSH     0x3e17ade6
    000000eb     CMP      EAX, EBX
    000000ed     PUSH     0xf21fd920
    000000f2     PUSH     0x27aa3188
    000000f7     PUSH     0xdfcb8f12
    000000fc     PUSH     0x2d9cc76c
    00000101     TEST     CH, AH
    00000103     CALL     scan_mem_for_vm_strings

(Figure C)

0000277f     TEST     DL, AL
    00002781     INC      ECX
    00002782     CMP      ECX, EBX
    00002784     JGE      000026b8                      ;Upper loop (not shown)
    0000278a     CMP      byte ptr [ESI + ECX*0x1], 0x0
    0000278e     JZ       LAB_0000277f
    00002790     PUSH     EBX
    00002791     PUSH     ECX
    00002792     PUSH     ESI
    00002793     TEST     CL, 0xd2
    00002796     PUSH     ESI
    00002797     ADD      dword ptr [ESP], ECX
    0000279a     CALL     djb_hash
    0000279f     MOV      EDX, EAX
    000027a1     POP      ESI
    000027a2     POP      ECX
    000027a3     TEST     CX, CX
    000027a6     POP      EBX
    000027a7     TEST     DX, AX
    000027aa     XOR      EAX, EAX

    ; Compare hash loop

    000027ac     ADD      EAX, 0x4
    000027af     CMP      dword ptr [ESP + EAX*0x1], 0x0
    000027b3     JZ       0000277f                        ;middle loop
    000027b5     TEST     EDI, 0x76916f06
    000027bb     CMP      dword ptr [ESP + EAX*0x1], EDX
    000027be     JNZ      compare_hashes

(Figure D)