File Analysis - 2023:02:07
example[.]com) so they can't be clicked by accident.Samples can be downloaded here
Files
- AFFAERENTRI.exe.7z (19294a8f3982807f6119e9720e8fde7c21fdc5c13be427f2c6fd130a7b532bec)
- 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:
- Breakpoint on
VirtualProtect - Execute until program self terminates, counting the number of times (N)
VirtualProtectwas hit - Restart debugging session
- Breakpoint on
VirtualProtect - Continue execution, and set a breakpoint on
VirtualAllocat N-1VirtualProtecthit 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
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
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
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
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
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
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="
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
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