The LummaC2 obfuscator employs a novel control flow protection scheme tailored to the stealer’s specific components, which, along with other standard obfuscation techniques, significantly hinders binary analysis and tampering.
Obfuscators use dispatcher blocks to disrupt a function’s control flow, which contain original and obfuscator instructions, ending with an indirect jump to a dynamically chosen next block via register or memory manipulation.
It employs register-based dispatcher blocks for obfuscation, which use MOV instructions to fetch values, XOR/LEA/INC for manipulation, and a final indirect jump based on register content.
The memory-based dispatcher layout, unlike the register-based layout, leverages both registers and stack values to calculate and branch to the destination.
While less common, this layout often involves adding a stack value to a register, while in some cases, the dispatcher block may directly jump to a value stored on the stack.
The mixed-order dispatcher layout is a complex obfuscation technique that intersperses dispatcher instructions with original code unpredictably, which makes deobfuscation challenging as it requires identifying and isolating the dispatcher instructions.
The obfuscator handles conditional dispatchers by creating paired entries for each branch based on the condition’s outcome, using setcc instructions to capture original conditional jumps and employs three distinct blocks for loop logic. For syscall logic, it determines success by negating the NTSTATUS code and inspecting its sign value.
The standard conditional dispatcher type, representing 987 out of 1,063 cases, compares the conditional value to zero and a non-zero constant, which determine an index used to fetch the appropriate encoded branch target from a table.
The loop conditional dispatcher type, found 42 times in the sample, consists of four linked blocks: initialization, update, exit-check, and loop body, where the initialization block sets up the loop, the update block modifies the flag, the exit-check block evaluates the flag, and the loop body contains the specific logic.
The syscall conditional dispatcher type, found 34 times, checks the return value of LummaC2-specific function calls that make Windows system calls, which uses the NT_SUCCESS macro to determine whether the syscall was successful or failed and then jumps to the appropriate branch based on the result.
Backward slicing with Triton helps deobfuscate functions by identifying dispatcher instructions by analyzing ASTs to distinguish instructions influencing the final jump target from original code, which allows pinpointing dispatcher instructions regardless of their placement within the block.
To recover the original control flow, they use a DFS algorithm to explore all execution paths and assume that conditional jumps are always taken, but store alternative paths for later exploration, which allows to reconstruct the obfuscated control flow by systematically exploring all possible paths.
According to Mandiant, the deobfuscation process involves rebuilding functions by replacing obfuscated instructions with their original counterparts. Indirect jumps are replaced with direct jumps or conditional jumps based on the original jump type.Â
Offset relocation fixes memory references in deobfuscated code. After removing obfuscation instructions, jump targets and other references have new addresses, and the deobfuscator calculates these new offsets and updates instructions to maintain functionality.