In October 2021, we published the first analysis of Wslink – a unique loader likely linked to the Lazarus group. Most samples are packed and protected with an advanced virtual machine (VM) obfuscator; the samples contain no clear artifacts and we initially did not associate the obfuscation with a publicly known VM, but we later managed to connect it to CodeVirtualizer. This VM introduces several additional obfuscation techniques such as insertion of junk code, encoding of virtual operands, duplication of virtual opcodes, opaque predicates, merging of virtual instructions, and a nested VM.
Our presentation analyzes the internals of the VM and describes our semi automated approach to “see through” the obfuscation techniques in reasonable time. We demonstrate the approach on some bytecode from a protected sample and compare the results with a non-obfuscated sample, found subsequent to starting our analysis, confirming the method’s validity. Our solution is based on a known deobfuscation method that extracts the semantics of the virtual opcodes, using symbolic execution with simplifying rules. We further treat the bytecode chunks and some internal constructs of the VM as concrete values instead of as symbolic ones, enabling the known deobfuscation method to deal with the additional obfuscation techniques automatically.
3. Agenda
•Intro to VMs in general and symbolic execution
•Internals of the VM used in Wslink
•Our approach to dealing with the obfuscation
•Demonstration of the approach
15. • Prepares the next virtual instruction
• Increases VPC (virtual program counter)
• Zeroes out a register
• RBP_init is context pointer
• Instruction table at offset 0xA4
• VPC at offset 0x28
VM2: The first executed virtual instruction
16. •Instruction 2
• Zeroes out several virtual registers
•Instruction 3
• Stores RSP in a virtual register
VM2: Virtual instructions 2 and 3
18. • Multiple basic blocks:
VM2: Virtual instruction 4
• Summary of the first block – memory assignments and IRDst:
19. • Multiple basic blocks:
VM2: Virtual instruction 4
• Summary of the first block – memory assignments and IRDst:
20. • Multiple basic blocks:
VM2: Virtual instruction 4
• Summary of the first block – memory assignments and IRDst:
• Rolling decryption – encryption of operands
• Values of virtual registers at offsets 0x0B and 0x70 were
set earlier
34. VM1: Virtual instruction 4
• Instruction merging – contains, e.g., POP and PUSH
operations
• Operands decide which instruction is executed
• PUSH:
35. VM1: Virtual instruction 4
• Instruction merging – contains, e.g., POP and PUSH
operations
• Operands decide which instruction is executed
• POP:
36. • Instruction merging – contains, e.g., POP and PUSH
operations
• Operands decide which instruction is executed
• PUSH/POP
• Can be simplified by making the operands concrete
VM1: Virtual instruction 4
37. VM1: Deobfuscating bytecode chunks
• Use the same approach as in VM2:
• Build a graph from summaries
• Treat certain values as concrete
• Preserve certain values between blocks
• Process both VMs at once:
• Additionally make the entire VM2
concrete
• Ignore assignments to the VM2 context
virtual_instruction2_1(vpc)
virtual_instruction2_2(vpc)
virtual_instruction2_1(vpc) virtual_instruction2_3(vpc)
virtual_instruction2_4(vpc)
38. Resulting graph contained unexpected
branches instead of a series of POPs
VM1: Issue with opaque predicates during deobfuscation
39. VM1: Issue with opaque predicates during deobfuscation
•The branches check a known value
• We can apply the value and simplify it
• This is a sort of opaque predicates
41. Analyzing results
Bytecode block VM1 VM2
Size in bytes 695 1,145
Total number of processed virtual instructions 62 109
Total number of underlying native instructions 3,536,427 17,406
Total number of resulting IR instructions (including IRDsts) 192 307
Execution time in seconds 382 10
Subsequently to starting our analysis found a non-obfuscated sample – we weren’t motivated to fully deobfuscate the code
Since 2017 been focusing on reverse engineering challenging malware samples
Computer Science student at the Comenius University in the first years of master’s degree
BlackHat Arsenal, AVAR
Work like standard machines – machine code is bytecode here
PC is offset to the bytecode here – not necessary
Transfer of control from one virtual instruction to the next during interpretation needs to be performed by every VM. This process is generally known as dispatching
Centralized interpreter – dispatcher – called direct call threading; can be also simple switch case
Can be also inside of handlers – direct threading
Stored in sep.arate section
Need context switch
Bytecode is generated from a function
Original function starts interpreter
Interpreter is embedded in the executable
Context switch needs to be performed
Stored in a separate section
Need context switch
Bytecode is generated from a function
Original function starts interpreter
Interpreter is embedded in the executable
Context switch needs to be performed
The strength of this obfuscation technique resides in the fact that the ISA of the VM is unknown to any prospective reverse engineer – a thorough analysis of the VM, which can be very time-consuming;
Additionally, obfuscating VMs usually virtualize only certain “interesting” functions
SE in IR
IRDst 1. stands for 2. is IR instruction pointer, e.g. cmov multiple blocks
X bit value dereference
Self explainable dereference
SE engine in Miasm automatically performs common optimization techniques
The VM used in the Wslink is a variant of CV (themida) that supports multiple VMs and polymorphic layers of obfuscation techniques. We found this out subsequently to publishing our research
The right side contains a part of the function
Why 1071? They are duplicated!
Vm_init – Sync – shared virtual context in the memory
Virtual program counter (VPC) register
Base address register
Instruction table register
Zdorazni nested VM – zacneme VM2
Rolling decryption – present in a blog about VM Protect’s structure
Make values relative to the bytecode ptr concrete
We’ve just deobfuscated one of the VM1 instructions
Let’s start with CFG
Reminder – atomic operations in blocks
Laser point – These IR blocks are effects of VM2 virtual instructions, they represent one VM1 virtual instruction together
Zeroes out a register, stores the RSP, initializes rolling decryption registers
A part of the instruction. When we inspected the code, we realized what the problem is
Memory range of the VMs is known – separate section
Let’s take a closer look at one of the branches
<CLICK>
The branches check a known value
We can apply the value and simplify it
This is a sort of opaque predicates
We will show the results on the first executed bytecode, but before that show statistics
Before we look at the devirtualized bytecode, let’s see what the execution time is approximately like
Let’s look at the deobfuscated bytecode block of VM1 <CLICK>
*We discovered the sample after starting our analysis
No ARM R0! – pseudocode close to assembly
Advanced – use junk code, duplicate and merge handlers, encrypt operands
Right values – operands and registers for encryption