Lifting Binaries, Part 0: Devirtualizing VMProtect and Themida: It's Just Flattening?
Table Of Contents
- Table Of Contents
- Intro
- Day 0
- Failed attempts
- Whiplash
- Challenges
- Action
- Whats next?
- Special thanks to
- Thanks for reading!
- Useful resources:
Intro
This is going to be the first part of multipart series in which I discuss about using compiler techniques for reverse engineering purposes. I want this first post to be more of a blog about why I decided on this approach and why I developed Mergen rather than focusing on the more technical aspects.
Let’s go back to day 0, where everything started.
Day 0
Commercial VM based obfuscators like VMProtect and Themida are considered as industry standards because of their black box implementation. Even though these solutions have been in this position for a long time and there are public projects that are able to deobfuscate a particular solution or a particular version of a solution, there are (or were) no public projects that can deobfuscate multiple versions or multiple solutions.
So bored enough to learn a new topic, stupid enough to make it wildly ambitious, I started by creating a basic executable that just returns the sum of two numbers, applied VMProtect to it, and started to mess around. When I looked at the function in IDA, I saw the function was getting replaced with a jump to VMProtect generated .???0 section with a weird looking code block.
.???0:000000014000AE7B push r10
.???0:000000014000AE7D pushfq
.???0:000000014000AE7E push rsi
.???0:000000014000AE7F bsf r10, r10
.???0:000000014000AE83 bts r10w, di
.???0:000000014000AE88 push r13
.???0:000000014000AE8A test cx, r15w
.???0:000000014000AE8E btc r10w, sp
.???0:000000014000AE93 cmc
.???0:000000014000AE94 push r8
.???0:000000014000AE96 stc
.???0:000000014000AE97 bswap r10
.???0:000000014000AE9A push rbx
.???0:000000014000AE9B bswap bx
.???0:000000014000AE9E sbb r10b, sil
.???0:000000014000AEA1 cmc
.???0:000000014000AEA2 push rdx
.???0:000000014000AEA3 btr bx, dx
.???0:000000014000AEA7 push r11
.???0:000000014000AEA9 clc
.???0:000000014000AEAA movzx r10d, bp
.???0:000000014000AEAE push rax
.???0:000000014000AEAF push r15
.???0:000000014000AEB1 sal rax, 5Ah
.???0:000000014000AEB5 push rbp
.???0:000000014000AEB6 push r9
.???0:000000014000AEB8 push rdi
.???0:000000014000AEB9 and ebp, 38D937E3h
.???0:000000014000AEBF push rcx
.???0:000000014000AEC0 push r14
.???0:000000014000AEC2 bts r9d, r9d
.???0:000000014000AEC6 btc rax, 57h ; 'W'
.???0:000000014000AECB push r12
.???0:000000014000AECD and r9b, 0CCh
.???0:000000014000AED1 shld r10w, r8w, 3Ah
.???0:000000014000AED7 add bpl, al
.???0:000000014000AEDA mov rax, 0
.???0:000000014000AEE4 push rax
.???0:000000014000AEE5 bswap rbx
.???0:000000014000AEE8 bsf bx, r14w
.???0:000000014000AEED mov rdi, [rsp+88h+arg_0]
.???0:000000014000AEF5 cmc
.???0:000000014000AEF6 shr bpl, cl
.???0:000000014000AEF9 bswap edi
.???0:000000014000AEFB add edi, 66931E79h
.???0:000000014000AF01 movzx rbp, r12w
.???0:000000014000AF05 or r11b, bpl
.???0:000000014000AF08 adc bl, r14b
.???0:000000014000AF0B bswap edi
.???0:000000014000AF0D inc r9b
.???0:000000014000AF10 sub edi, 6573517Fh
.???0:000000014000AF16 add rdi, rax
.???0:000000014000AF19 mov r10, 100000000h
.???0:000000014000AF23 bts r11, rdi
.???0:000000014000AF27 add rdi, r10
.???0:000000014000AF2A movsx r11, bx
.???0:000000014000AF2E mov rbx, rsp
.???0:000000014000AF31 movsx ebp, bp
.???0:000000014000AF34 btc r11w, r8w
.???0:000000014000AF39 mov bpl, r15b
.???0:000000014000AF3C sub rsp, 180h
.???0:000000014000AF43 bsf r9d, r15d
.???0:000000014000AF47 and bpl, 0D0h
.???0:000000014000AF4B movzx bp, r13b
.???0:000000014000AF50 and rsp, 0FFFFFFFFFFFFFFF0h
.???0:000000014000AF57
.???0:000000014000AF57 loc_14000AF57: ; CODE XREF: .???0:loc_14001D58B↓j
.???0:000000014000AF57 ; .???0:000000014001FFEC↓j ...
.???0:000000014000AF57 mov rbp, rdi
.???0:000000014000AF5A rcr r11w, 7Fh
.???0:000000014000AF5F xor r11w, 0E0Ch
.???0:000000014000AF65 xadd r9, r9
.???0:000000014000AF69 mov r9, 0
.???0:000000014000AF73 sub rbp, r9
.???0:000000014000AF76 dec r11b
.???0:000000014000AF79 or r11b, r11b
.???0:000000014000AF7C mov r9b, 0C2h
.???0:000000014000AF7F
.???0:000000014000AF7F loc_14000AF7F: ; DATA XREF: sub_14000AE7B:loc_14000AF7F↓o
.???0:000000014000AF7F lea r11, loc_14000AF7F
.???0:000000014000AF86 sar r9b, cl
.???0:000000014000AF89 bt r9, rdi
.???0:000000014000AF8D sub rdi, 4
.???0:000000014000AF94 ror r9b, cl
.???0:000000014000AF97 mov r9d, [rdi]
.???0:000000014000AF9A jmp loc_140049977
We can make sense of some instructions in this block, such as the red ones, which simply pushes the code into the (virtual) stack. So I debugged it in hopes of finding our numbers and sum of our numbers, and voila, we have the block that we do the addition operation. This is the block that does the addition:
.???0:0000000140020893 mov eax, [rbx]
.???0:0000000140020895 mov rsi, 42DB0376h
.???0:000000014002089C mov edx, [rbx+4]
.???0:000000014002089F xor si, r15w
.???0:00000001400208A3 test dl, cl
.???0:00000001400208A5 sub rbx, 4
.???0:00000001400208AC dec esi
.???0:00000001400208AE add eax, edx
.???0:00000001400208B0 movsx esi, bx
.???0:00000001400208B3 mov [rbx+8], eax
.???0:00000001400208B6 not si
.???0:00000001400208B9 xchg si, si
.???0:00000001400208BC bswap esi
.???0:00000001400208BE pushfq
.???0:00000001400208BF mov rsi, 6F022F06h
.???0:00000001400208C6 sal sil, cl
.???0:00000001400208C9 cmp r10b, sil
.???0:00000001400208CC pop qword ptr [rbx]
.???0:00000001400208CE xor sil, dl
.???0:00000001400208D1 sar si, cl
.???0:00000001400208D4 sub rdi, 4
.???0:00000001400208DB shl sil, 0E5h
.???0:00000001400208DF mov esi, [rdi]
.???0:00000001400208E1 stc
.???0:00000001400208E2 xor esi, ebp
.???0:00000001400208E4 jmp loc_14007A020
Red instruction loads the first operand, blue instruction loads the second, and purple will do the addition and store it in the slot. So essentially the handle is just this:
.???0:0000000140020893 mov eax, [rbx]
.???0:000000014002089C mov edx, [rbx+4]
.???0:00000001400208AE add eax, edx
.???0:00000001400208B3 mov [rbx+8], eax
You could also imagine that it kind of looks like this in high level implementation:
int arg1 = stack.top();
stack.pop();
int arg2 = stack.top();
stack.push_back(arg1 + arg2);
However, we don’t always have the luxury of attaching a debugger, tracing it, and analyzing each handler manually. Even though this was a good learning experience, we need to come up with a solution that requires less manual work.
Failed attempts
If we wanted to patch or change the behavior of the program, a good solution would be creating our own function. Initially, I had different approaches and different projects such as using Triton to uncover the control flow and apply existing optimizations in Triton and just pasting the code somewhere else in the binary. However, generating the output takes quite a while, and the quality of the output is not very good.
So I decided to use unicorn engine and apply my own optimizations. You can guess that it takes quite a while to implement optimization passes when you have 0 experience with compilers, so that project was also scratched.
I remember finding retdec and playing with it; while playing with it, I found a good friend that was also was interested into the project. However, retdec wasn’t sufficent for our purposes, and I wasn’t experienced enough to make the neccesary improvements to it.
Whiplash
The failed attempts made me even more determined to work on it. So I decided to get more experience and started working on a project that will lift assembly to LLVM IR. The idea was to create something like a tracer and optimize it with LLVM passes like the last project where I used unicorn engine, but instead of writing our own optimizations, LLVM would take care of it.
I also realized I could just use LLVM passes to find the next block, so we didn’t need to use unicorn engine either.
Using LLVM would introduce me to SSA (single static assignment) format, which allows us to read the output easier than asm because every variable is only defined once, making control flow and data dependencies easier to follow. Also, LLVM is maintained by other people, and it’s already a widely used project.
Assembly
add rax, rcx
sub rdx, rbx
sub rax, rdx
LLVM IR (SSA format)
%rax2 = add i64 %rax, %rcx
%rdx2 = sub i64 %rdx, %rbx
%rax3 = sub i64 %rax2, %rdx2
So the ability to produce assembly code, passes, SSA and active community made using LLVM a no-brainer.
Unlike other devirtualization oriented projects, we are going to create a generic approach that would work with everything. So no special VIP, VSP or any vm register will be tracked; we will take the assembly code and directly lift it to LLVM IR and apply optimizations. Our only assumption is the CPU is able to execute this code, so we keep track of the memory, registers and additionally assumptions that come with branches.
Challenges
In order for our project to “trace” our virtualized function correctly, there were several challenges such as:
- Partial reads between memory slots could not be resolved.
- Storing a value with higher bits symbolized and truncating again would confuse value-tracking.
This is because LLVM is a compiler, not a deobfuscator, a normal program wouldn’t use memory slots like this.
We need to implement these solutions:
- Custom aliasing system
- Pattern matching for memory tracking
I also encountered LLVM’s KnownBits class; basically, it tracks one’s and zero’s in a value and allows us to concretize values more easily. The idea is simple: we have KnownOnes and KnownZeros.
??01
represents 4 bits, KnownOnes is 0001
and KnownZeros is 0010
, so we know the lower two bits and the rest is unknown. We can keep tracking the value if its bits get and’d with 0, or’d with 1, etc.
Action
After solving these challenges and implementing 60+ instructions, we could get the trace of instructions and optimize them; it was a huge breakthrough, even though it was taking 200+ seconds to trace a simple function.
This bottleneck was caused because we were creating a clone of our module and applying optimizations each time we come across a jump. So I came up with a solution that would optimize the instructions while lifting and making sure we don’t invalidate any necessary value. After doing this, our runtime dropped to 500~ ms, 400x speed up, an incredible milestone for our project!
The idea was to treat each instruction as a unique instruction. This gives us more room to analyze and modify the “original” function. Here, these graphs explain the overall idea of a VM.
If we flatten the control flow and reorder the blocks sequentially, its much easier to analyze.
Before jumping into VMProtect or Themida examples, let’s see how our lifter performs on this toy example.
#include <array>
#include <iostream>
#define VM_MAX_OPCODES 10
#define STACK_SIZE 10
enum VMOpcodes {
VM_NOP = 0,
VM_PUSH,
VM_POP,
VM_ADD,
VM_SUB,
VM_XOR,
VM_EXIT,
Invalid
};
struct VMopcode {
VMOpcodes opc;
int *v;
VMopcode(VMOpcodes opc = VM_NOP, int *v = nullptr) : opc(opc), v(v){};
};
class VMRegister {
public:
int value = 0;
VMRegister() : value(0){};
VMRegister(int v) : value(v) {}
VMRegister operator+(const VMRegister &RHS) const {
return VMRegister(this->value + RHS.value);
}
VMRegister operator-(const VMRegister &RHS) const {
return VMRegister(this->value - RHS.value);
}
VMRegister operator^(const VMRegister &RHS) const {
return VMRegister(this->value ^ RHS.value);
}
};
class VM {
public:
std::array<VMopcode, VM_MAX_OPCODES> &program;
int vsp = 0;
int vip = 0;
int vzf = 0;
VMRegister stack[STACK_SIZE]; // could be rewritten with std::stack
VM() = delete;
explicit VM(std::array<VMopcode, VM_MAX_OPCODES> &vmopcodes)
: program(vmopcodes){};
void execute() {
while (vip < VM_MAX_OPCODES) {
auto opcode = program[vip].opc;
auto arg = program[vip++].v;
switch (opcode) {
case VM_PUSH: {
stack[vsp++] = VMRegister(*arg);
break;
}
case VM_POP: {
*arg = stack[--vsp].value;
break;
}
case VM_ADD: {
auto RHS = stack[--vsp];
auto LHS = stack[--vsp];
stack[vsp++] = LHS + RHS;
break;
}
case VM_SUB: {
auto RHS = stack[--vsp];
auto LHS = stack[--vsp];
stack[vsp++] = LHS - RHS;
break;
}
case VM_XOR: {
auto RHS = stack[--vsp];
auto LHS = stack[--vsp];
stack[vsp++] = LHS ^ RHS;
break;
}
case VM_NOP: {
break;
}
case VM_EXIT: {
return; // Exit the execution loop
}
case Invalid:
break;
}
}
}
};
int callvm(int a, int b) {
// return (a ^ b) + b;
int v1;
std::array<VMopcode, VM_MAX_OPCODES> vmopcodes = {{{VM_PUSH, &a},
{VM_PUSH, &b},
{VM_XOR, nullptr},
{VM_PUSH, &b},
{VM_ADD, nullptr},
{VM_POP, &v1},
{VM_EXIT, nullptr}}};
VM ourvm(vmopcodes);
ourvm.execute();
return v1;
}
If we call callvm
control flow of this program somewhat looks like this:
and we just reorder the blocks to this:
The result after compiling -O3 and custom passes:
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %rsp, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr nocapture readnone %TEB, ptr nocapture readnone %memory) local_unnamed_addr #0 {
entry:
%0 = trunc i64 %rdx to i32
%realxor-5368715542-84 = xor i64 %rdx, %rcx
%realxor-5368715542- = trunc i64 %realxor-5368715542-84 to i32
%realadd-5368715382- = add i32 %realxor-5368715542-, %0
%1 = zext i32 %realadd-5368715382- to i64
ret i64 %1
}
We can also throw this into a decompiler, but I believe LLVM IR format is easier to read because we don’t need to fix arguments. If we wanted to see this in IDA, it would look like this:
int __fastcall main(
__int64 rax,
__int64 rcx,
__int64 rdx,
__int64 rbx,
__int64 rsp,
__int64 rbp,
__int64 rsi,
__int64 rdi,
__int64 r8,
__int64 r9,
__int64 r10,
__int64 r11,
__int64 r12,
__int64 r13,
__int64 r14,
__int64 r15)
{
return (rdx ^ rcx) + rdx;
}
After that, I just needed to implement more instructions and lift flags appropriately for supporting VMProtect 3.8 and Themida.
For comparison, here is how a function protected by VMProtect 3.8 and Themida differ in terms of control flow and results.
int foo(int a, int b) {
return a + b;
}
VMProtect 3.8 control flow (39894 non-unique instructions):
Result after -O3:
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %rsp, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr nocapture readnone %TEB, ptr nocapture writeonly %memory) local_unnamed_addr #0 {
entry:
%0 = trunc i64 %rdx to i32
%1 = trunc i64 %rcx to i32
%realadd-5370932149- = add i32 %0, %1
%3 = zext i32 %realadd-5370932149- to i64
ret i64 %3
}
Themida(Fish White) control flow (45138 non-unique instructions):
Result after -O3:
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %rsp, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr nocapture readnone %TEB, ptr writeonly %memory) local_unnamed_addr #0 {
entry:
%0 = trunc i64 %rdx to i32
%1 = inttoptr i64 1375416 to ptr
store i32 %0, ptr %1, align 4
%2 = trunc i64 %rcx to i32
%3 = inttoptr i64 1375408 to ptr
store i32 %2, ptr %3, align 4
%4 = inttoptr i64 5368817293 to ptr
store i32 1, ptr %4, align 4
%5 = inttoptr i64 5368817402 to ptr
store i64 5368709120, ptr %5, align 4
%6 = inttoptr i64 5368817438 to ptr
store i64 4114, ptr %6, align 4
%7 = inttoptr i64 5368817092 to ptr
store i64 5368817438, ptr %7, align 4
%8 = inttoptr i64 5368817228 to ptr
store i64 0, ptr %8, align 4
%9 = inttoptr i64 5368817430 to ptr
store i64 5374802999, ptr %9, align 4
%10 = inttoptr i64 5368817305 to ptr
%11 = inttoptr i64 5368763106 to ptr
store i64 5368833026, ptr %11, align 4
%12 = inttoptr i64 5368763114 to ptr
store i64 5368833031, ptr %12, align 4
%13 = inttoptr i64 5368763122 to ptr
store i64 5369155158, ptr %13, align 4
%14 = inttoptr i64 5368763130 to ptr
store i64 5369155163, ptr %14, align 4
%15 = inttoptr i64 5368763138 to ptr
store i64 5368756062, ptr %15, align 4
%16 = inttoptr i64 5368763146 to ptr
store i64 5368756067, ptr %16, align 4
%17 = inttoptr i64 5368763154 to ptr
store i64 5369076960, ptr %17, align 4
%18 = inttoptr i64 5368763162 to ptr
store i64 5369076965, ptr %18, align 4
%19 = inttoptr i64 5368763170 to ptr
store i64 5368832102, ptr %19, align 4
%20 = inttoptr i64 5368763178 to ptr
store i64 5368832107, ptr %20, align 4
%21 = inttoptr i64 5368763186 to ptr
store i64 5368817820, ptr %21, align 4
%22 = inttoptr i64 5368763194 to ptr
store i64 5368817825, ptr %22, align 4
%23 = inttoptr i64 5368763202 to ptr
store i64 5369076312, ptr %23, align 4
%24 = inttoptr i64 5368763210 to ptr
store i64 5369076317, ptr %24, align 4
%25 = inttoptr i64 5368763218 to ptr
store i64 5369155148, ptr %25, align 4
%26 = inttoptr i64 5368763226 to ptr
store i64 5369155153, ptr %26, align 4
%27 = inttoptr i64 5368763234 to ptr
store i64 5368817454, ptr %27, align 4
%28 = inttoptr i64 5368763242 to ptr
store i64 5368817459, ptr %28, align 4
%29 = inttoptr i64 5368763250 to ptr
store i64 5368790594, ptr %29, align 4
%30 = inttoptr i64 5368763258 to ptr
store i64 5368790599, ptr %30, align 4
%31 = inttoptr i64 5368763266 to ptr
store i64 5368832092, ptr %31, align 4
%32 = inttoptr i64 5368763274 to ptr
store i64 5368832097, ptr %32, align 4
%33 = inttoptr i64 5368763282 to ptr
store i64 5368908672, ptr %33, align 4
%34 = inttoptr i64 5368763290 to ptr
store i64 5368908677, ptr %34, align 4
%35 = inttoptr i64 5368763298 to ptr
store i64 5369075736, ptr %35, align 4
%36 = inttoptr i64 5368763306 to ptr
store i64 5369075741, ptr %36, align 4
%37 = inttoptr i64 5368763314 to ptr
store i64 5368832032, ptr %37, align 4
%38 = inttoptr i64 5368763322 to ptr
store i64 5368832037, ptr %38, align 4
%39 = inttoptr i64 5368763330 to ptr
store i64 5369154176, ptr %39, align 4
%40 = inttoptr i64 5368763338 to ptr
store i64 5369154181, ptr %40, align 4
%41 = inttoptr i64 5368763346 to ptr
store i64 5368820454, ptr %41, align 4
%42 = inttoptr i64 5368763354 to ptr
store i64 5368820459, ptr %42, align 4
%43 = inttoptr i64 5368763362 to ptr
store i64 5368861334, ptr %43, align 4
%44 = inttoptr i64 5368763370 to ptr
store i64 5368861339, ptr %44, align 4
%45 = inttoptr i64 5368763378 to ptr
store i64 5368827548, ptr %45, align 4
%46 = inttoptr i64 5368763386 to ptr
store i64 5368827553, ptr %46, align 4
%47 = inttoptr i64 5368763394 to ptr
store i64 5368820444, ptr %47, align 4
%48 = inttoptr i64 5368763402 to ptr
store i64 5368820449, ptr %48, align 4
%49 = inttoptr i64 5368763410 to ptr
store i64 5368826458, ptr %49, align 4
%50 = inttoptr i64 5368763418 to ptr
store i64 5368826463, ptr %50, align 4
%51 = inttoptr i64 5368763426 to ptr
store i64 5368778594, ptr %51, align 4
%52 = inttoptr i64 5368763434 to ptr
store i64 5368778599, ptr %52, align 4
%53 = inttoptr i64 5368763442 to ptr
store i64 5369142368, ptr %53, align 4
%54 = inttoptr i64 5368763450 to ptr
store i64 5369142373, ptr %54, align 4
%55 = inttoptr i64 5368763458 to ptr
store i64 5368818708, ptr %55, align 4
%56 = inttoptr i64 5368763466 to ptr
store i64 5368818713, ptr %56, align 4
%57 = inttoptr i64 5368763474 to ptr
store i64 5368835506, ptr %57, align 4
%58 = inttoptr i64 5368763482 to ptr
store i64 5368835511, ptr %58, align 4
%59 = inttoptr i64 5368763490 to ptr
store i64 5368934020, ptr %59, align 4
%60 = inttoptr i64 5368763498 to ptr
store i64 5368934025, ptr %60, align 4
%61 = inttoptr i64 5368763506 to ptr
store i64 5369145336, ptr %61, align 4
%62 = inttoptr i64 5368763514 to ptr
store i64 5369145341, ptr %62, align 4
%63 = inttoptr i64 5368763522 to ptr
store i64 5368835496, ptr %63, align 4
%64 = inttoptr i64 5368763530 to ptr
store i64 5368835501, ptr %64, align 4
%65 = inttoptr i64 5368763538 to ptr
store i64 5369142890, ptr %65, align 4
%66 = inttoptr i64 5368763546 to ptr
store i64 5369142895, ptr %66, align 4
%67 = inttoptr i64 5368763554 to ptr
store i64 5369078460, ptr %67, align 4
%68 = inttoptr i64 5368763562 to ptr
store i64 5369078465, ptr %68, align 4
%69 = inttoptr i64 5368763570 to ptr
store i64 5368756212, ptr %69, align 4
%70 = inttoptr i64 5368763578 to ptr
store i64 5368756217, ptr %70, align 4
%71 = inttoptr i64 5368763586 to ptr
store i64 5368758102, ptr %71, align 4
%72 = inttoptr i64 5368763594 to ptr
store i64 5368758107, ptr %72, align 4
%73 = inttoptr i64 5368763602 to ptr
store i64 5368760802, ptr %73, align 4
%74 = inttoptr i64 5368763610 to ptr
store i64 5368760807, ptr %74, align 4
%75 = inttoptr i64 5368763618 to ptr
store i64 5368832062, ptr %75, align 4
%76 = inttoptr i64 5368763626 to ptr
store i64 5368832067, ptr %76, align 4
%77 = inttoptr i64 5368763634 to ptr
store i64 5369017510, ptr %77, align 4
%78 = inttoptr i64 5368763642 to ptr
store i64 5369017515, ptr %78, align 4
%79 = inttoptr i64 5368763650 to ptr
store i64 5368820904, ptr %79, align 4
%80 = inttoptr i64 5368763658 to ptr
store i64 5368820909, ptr %80, align 4
%81 = inttoptr i64 5368763666 to ptr
store i64 5368832052, ptr %81, align 4
%82 = inttoptr i64 5368763674 to ptr
store i64 5368832057, ptr %82, align 4
%83 = inttoptr i64 5368763682 to ptr
store i64 5368957214, ptr %83, align 4
%84 = inttoptr i64 5368763690 to ptr
store i64 5368957219, ptr %84, align 4
%85 = inttoptr i64 5368763698 to ptr
store i64 5368820924, ptr %85, align 4
%86 = inttoptr i64 5368763706 to ptr
store i64 5368820929, ptr %86, align 4
%87 = inttoptr i64 5368763714 to ptr
store i64 5368832082, ptr %87, align 4
%88 = inttoptr i64 5368763722 to ptr
store i64 5368832087, ptr %88, align 4
%89 = inttoptr i64 5368763730 to ptr
store i64 5369142770, ptr %89, align 4
%90 = inttoptr i64 5368763738 to ptr
store i64 5369142775, ptr %90, align 4
%91 = inttoptr i64 5368763746 to ptr
store i64 5368832042, ptr %91, align 4
%92 = inttoptr i64 5368763754 to ptr
store i64 5368832047, ptr %92, align 4
%93 = inttoptr i64 5368763762 to ptr
store i64 5368755982, ptr %93, align 4
%94 = inttoptr i64 5368763770 to ptr
store i64 5368755987, ptr %94, align 4
%95 = inttoptr i64 5368763778 to ptr
store i64 5368820914, ptr %95, align 4
%96 = inttoptr i64 5368763786 to ptr
store i64 5368820919, ptr %96, align 4
%97 = inttoptr i64 5368763794 to ptr
store i64 5368832072, ptr %97, align 4
%98 = inttoptr i64 5368763802 to ptr
store i64 5368832077, ptr %98, align 4
%99 = inttoptr i64 5368763810 to ptr
store i64 5369077210, ptr %99, align 4
%100 = inttoptr i64 5368763818 to ptr
store i64 5369077215, ptr %100, align 4
%101 = inttoptr i64 5368763826 to ptr
store i64 5369144346, ptr %101, align 4
%102 = inttoptr i64 5368763834 to ptr
store i64 5369144351, ptr %102, align 4
%103 = inttoptr i64 5368763842 to ptr
store i64 5368820464, ptr %103, align 4
%104 = inttoptr i64 5368763850 to ptr
store i64 5368820469, ptr %104, align 4
%105 = inttoptr i64 5368763858 to ptr
store i64 5369145426, ptr %105, align 4
%106 = inttoptr i64 5368763866 to ptr
store i64 5369145431, ptr %106, align 4
%107 = inttoptr i64 5368763874 to ptr
store i64 5368820804, ptr %107, align 4
%108 = inttoptr i64 5368763882 to ptr
store i64 5368820809, ptr %108, align 4
%109 = inttoptr i64 5368763890 to ptr
store i64 5369016970, ptr %109, align 4
%110 = inttoptr i64 5368763898 to ptr
store i64 5369016975, ptr %110, align 4
%111 = inttoptr i64 5368763906 to ptr
store i64 5369144830, ptr %111, align 4
%112 = inttoptr i64 5368763914 to ptr
store i64 5369144835, ptr %112, align 4
%113 = inttoptr i64 5368763922 to ptr
store i64 5368818798, ptr %113, align 4
%114 = inttoptr i64 5368763930 to ptr
store i64 5368818803, ptr %114, align 4
%115 = inttoptr i64 5368763938 to ptr
store i64 5368789714, ptr %115, align 4
%116 = inttoptr i64 5368763946 to ptr
store i64 5368789719, ptr %116, align 4
%117 = inttoptr i64 5368763954 to ptr
store i64 5368957264, ptr %117, align 4
%118 = inttoptr i64 5368763962 to ptr
store i64 5368957269, ptr %118, align 4
%119 = inttoptr i64 5368763970 to ptr
store i64 5368835566, ptr %119, align 4
%120 = inttoptr i64 5368763978 to ptr
store i64 5368835571, ptr %120, align 4
%121 = inttoptr i64 5368763986 to ptr
store i64 5368818110, ptr %121, align 4
%122 = inttoptr i64 5368763994 to ptr
store i64 5368818115, ptr %122, align 4
%123 = inttoptr i64 5368764002 to ptr
store i64 5369145276, ptr %123, align 4
%124 = inttoptr i64 5368764010 to ptr
store i64 5369145281, ptr %124, align 4
%125 = inttoptr i64 5368764018 to ptr
store i64 5368835576, ptr %125, align 4
%126 = inttoptr i64 5368764026 to ptr
store i64 5368835581, ptr %126, align 4
%127 = inttoptr i64 5368764034 to ptr
store i64 5368818350, ptr %127, align 4
%128 = inttoptr i64 5368764042 to ptr
store i64 5368818355, ptr %128, align 4
%129 = inttoptr i64 5368764050 to ptr
store i64 5368806124, ptr %129, align 4
%130 = inttoptr i64 5368764058 to ptr
store i64 5368806129, ptr %130, align 4
%131 = inttoptr i64 5368764066 to ptr
store i64 5369076710, ptr %131, align 4
%132 = inttoptr i64 5368764074 to ptr
store i64 5369076715, ptr %132, align 4
%133 = inttoptr i64 5368764082 to ptr
store i64 5369153390, ptr %133, align 4
%134 = inttoptr i64 5368764090 to ptr
store i64 5369153395, ptr %134, align 4
%135 = inttoptr i64 5368764098 to ptr
store i64 5369079940, ptr %135, align 4
%136 = inttoptr i64 5368764106 to ptr
store i64 5369079945, ptr %136, align 4
%137 = inttoptr i64 5368764114 to ptr
store i64 5369153380, ptr %137, align 4
%138 = inttoptr i64 5368764122 to ptr
store i64 5369153385, ptr %138, align 4
%139 = inttoptr i64 5368764130 to ptr
store i64 5368827868, ptr %139, align 4
%140 = inttoptr i64 5368764138 to ptr
store i64 5368827873, ptr %140, align 4
%141 = inttoptr i64 5368764146 to ptr
store i64 5368998858, ptr %141, align 4
%142 = inttoptr i64 5368764154 to ptr
store i64 5368998863, ptr %142, align 4
%143 = inttoptr i64 5368764162 to ptr
store i64 5369140266, ptr %143, align 4
%144 = inttoptr i64 5368764170 to ptr
store i64 5369140271, ptr %144, align 4
%145 = inttoptr i64 5368764178 to ptr
store i64 5368756022, ptr %145, align 4
%146 = inttoptr i64 5368764186 to ptr
store i64 5368756027, ptr %146, align 4
%147 = inttoptr i64 5368764194 to ptr
store i64 5368756052, ptr %147, align 4
%148 = inttoptr i64 5368764202 to ptr
store i64 5368756057, ptr %148, align 4
%149 = inttoptr i64 5368764210 to ptr
store i64 5369076630, ptr %149, align 4
%150 = inttoptr i64 5368764218 to ptr
store i64 5369076635, ptr %150, align 4
%151 = inttoptr i64 5368764226 to ptr
store i64 5368756012, ptr %151, align 4
%152 = inttoptr i64 5368764234 to ptr
store i64 5368756017, ptr %152, align 4
%153 = inttoptr i64 5368764242 to ptr
store i64 5368756042, ptr %153, align 4
%154 = inttoptr i64 5368764250 to ptr
store i64 5368756047, ptr %154, align 4
%155 = inttoptr i64 5368764258 to ptr
store i64 5369154586, ptr %155, align 4
%156 = inttoptr i64 5368764266 to ptr
store i64 5369154591, ptr %156, align 4
store i64 5368763106, ptr %10, align 4
%157 = inttoptr i64 5368817370 to ptr
%158 = inttoptr i64 5368817076 to ptr
%159 = inttoptr i64 5368817376 to ptr
store i32 0, ptr %159, align 4
%160 = inttoptr i64 5368817175 to ptr
%161 = inttoptr i64 5368817080 to ptr
%162 = inttoptr i64 5368817146 to ptr
%163 = inttoptr i64 5368817236 to ptr
store i16 0, ptr %163, align 2
%164 = inttoptr i64 5368817401 to ptr
%165 = inttoptr i64 5368817154 to ptr
%166 = inttoptr i64 5368817150 to ptr
%167 = inttoptr i64 5368817352 to ptr
%168 = inttoptr i64 5368817329 to ptr
%169 = inttoptr i64 5368817268 to ptr
%170 = inttoptr i64 5368817220 to ptr
%171 = inttoptr i64 5368817180 to ptr
%172 = inttoptr i64 5368817100 to ptr
%173 = inttoptr i64 5368817251 to ptr
%174 = inttoptr i64 5368817362 to ptr
store i64 %r8, ptr %174, align 4
%175 = inttoptr i64 5368817126 to ptr
store i64 %r9, ptr %175, align 4
%176 = inttoptr i64 5368817134 to ptr
store i64 %r10, ptr %176, align 4
%177 = inttoptr i64 5368817344 to ptr
store i64 %r11, ptr %177, align 4
%178 = inttoptr i64 5368817269 to ptr
store i64 %r12, ptr %178, align 4
%179 = inttoptr i64 5368817252 to ptr
store i64 %r13, ptr %179, align 4
%180 = inttoptr i64 5368817068 to ptr
store i64 %r14, ptr %180, align 4
%181 = inttoptr i64 5368817321 to ptr
store i64 %r15, ptr %181, align 4
%182 = inttoptr i64 5368817212 to ptr
store i64 %rdi, ptr %182, align 4
%183 = inttoptr i64 5368817393 to ptr
store i64 %rsi, ptr %183, align 4
%184 = inttoptr i64 5368817084 to ptr
store i64 0, ptr %184, align 4
%185 = inttoptr i64 5368817155 to ptr
store i64 %rbx, ptr %185, align 4
%186 = inttoptr i64 5368817332 to ptr
store i64 %rdx, ptr %186, align 4
%187 = inttoptr i64 5368817200 to ptr
%188 = inttoptr i64 5368817297 to ptr
%189 = inttoptr i64 5368817422 to ptr
%190 = inttoptr i64 5368817163 to ptr
%191 = inttoptr i64 5368817060 to ptr
%192 = inttoptr i64 5368817243 to ptr
%193 = inttoptr i64 5368817179 to ptr
%194 = inttoptr i64 5368817238 to ptr
%195 = inttoptr i64 5368817188 to ptr
%196 = inttoptr i64 5368817313 to ptr
%197 = inttoptr i64 5368817301 to ptr
store i64 1702573061, ptr %191, align 4
store i64 1375408, ptr %195, align 4
%198 = and i64 %rcx, 4294967295
%realadd-5369249311- = shl nuw nsw i64 %198, 1
%lsb656 = and i64 %realadd-5369249311-, 254
%pf1657 = mul nuw i64 %lsb656, 72340172838076673
%pf2658 = and i64 %pf1657, -9205322385119247872
%pf3659 = urem i64 %pf2658, 511
%pf4660 = shl nuw nsw i64 %pf3659, 2
%199 = and i64 %pf4660, 4
%200 = shl i64 %rcx, 1
%createrflag2-667 = and i64 %200, 16
%zeroflag669 = icmp eq i64 %198, 0
%createrflag2-670 = select i1 %zeroflag669, i64 64, i64 0
%201 = or disjoint i64 %createrflag2-670, %createrflag2-667
%202 = or disjoint i64 %201, %199
%creatingrflag672 = xor i64 %202, 518
%realadd-5369259651- = add nuw nsw i64 %creatingrflag672, 44
%realxor-5369259850- = xor i64 %realadd-5369259651-, 1136612388
%203 = inttoptr i64 5368817204 to ptr
%realadd-5368752762- = add nuw nsw i64 %realxor-5369259850-, 17999817424
%realadd-5368880833- = add i32 %0, %2
%add_cf1733 = icmp ult i32 %realadd-5368880833-, %2
%204 = zext i1 %add_cf1733 to i64
%lsb735 = and i32 %realadd-5368880833-, 255
%205 = zext nneg i32 %lsb735 to i64
%pf1736 = mul nuw i64 %205, 72340172838076673
%pf2737 = and i64 %pf1736, -9205322385119247871
%pf3738 = urem i64 %pf2737, 511
%pf4739 = shl nuw nsw i64 %pf3738, 2
%206 = and i64 %pf4739, 4
%lvalLowerNibble743 = and i32 %2, 15
%rvalLowerNibble = and i32 %0, 15
%add_sumLowerNibble744 = add nuw nsw i32 %rvalLowerNibble, %lvalLowerNibble743
%add_af745 = icmp ugt i32 %add_sumLowerNibble744, 15
%createrflag2-746 = select i1 %add_af745, i64 16, i64 0
%zeroflag748 = icmp eq i32 %realadd-5368880833-, 0
%createrflag2-749 = select i1 %zeroflag748, i64 64, i64 0
%207 = lshr i32 %realadd-5368880833-, 24
%208 = and i32 %207, 128
%createrflag2-751.masked = zext nneg i32 %208 to i64
%ofadd754 = xor i32 %realadd-5368880833-, %2
%ofadd1 = xor i32 %realadd-5368880833-, %0
%ofadd2 = and i32 %ofadd754, %ofadd1
%209 = lshr i32 %ofadd2, 20
%210 = and i32 %209, 2048
%createrflag2-755 = zext nneg i32 %210 to i64
%211 = or disjoint i64 %createrflag2-746, %204
%212 = or disjoint i64 %211, %createrflag2-751.masked
%213 = or disjoint i64 %212, %createrflag2-755
%214 = or disjoint i64 %213, %206
%215 = or disjoint i64 %214, %createrflag2-749
%creatingrflag756 = xor i64 %215, 518
%216 = zext i32 %realadd-5368880833- to i64
store i32 %realadd-5368880833-, ptr %187, align 4
store i32 0, ptr %203, align 4
store i64 %creatingrflag756, ptr %189, align 4
%realxor-5368894493- = xor i64 %realadd-5368752762-, 6126736
store i8 -3, ptr %193, align 1
store i8 101, ptr %194, align 1
store i64 5369144346, ptr %190, align 4
%realadd-5369240414- = add nsw i64 %216, -3404397706
%realsub-5369240475- = add nsw i64 %realadd-5369240414-, %realxor-5368894493-
store i64 %realsub-5369240475-, ptr %192, align 4
store i32 -2008313946, ptr %166, align 4
%realadd-5369249311-936 = shl nuw nsw i64 %216, 1
%lsb940 = and i64 %realadd-5369249311-936, 254
%pf1941 = mul nuw i64 %lsb940, 72340172838076673
%pf2942 = and i64 %pf1941, -9205322385119247872
%pf3943 = urem i64 %pf2942, 511
%pf4944 = shl nuw nsw i64 %pf3943, 2
%217 = and i64 %pf4944, 4
%createrflag2-951 = and i64 %realadd-5369249311-936, 16
%218 = or disjoint i64 %createrflag2-951, %217
%219 = or disjoint i64 %218, %createrflag2-749
%creatingrflag956 = xor i64 %219, 518
%realsub-5369249432-967 = add nuw nsw i64 %216, 1591862189
store i64 %realsub-5369249432-967, ptr %196, align 4
%realadd-5369259651-987 = add nuw nsw i64 %creatingrflag956, 44
%realxor-5369259850-989 = xor i64 %realadd-5369259651-987, 1136612388
%realadd-5369259926-994 = add nuw nsw i64 %realxor-5369259850-989, %realxor-5368894493-
store i32 %realadd-5368880833-, ptr %188, align 4
store i32 0, ptr %197, align 4
%realsub-5369033485-1831 = add nuw nsw i64 %realadd-5369259926-994, -49619084162
store i8 88, ptr %164, align 1
store i32 -1509841899, ptr %162, align 4
store i32 480435818, ptr %161, align 4
store i32 1317788444, ptr %160, align 4
store i8 53, ptr %168, align 1
store i8 59, ptr %169, align 1
store i8 0, ptr %173, align 1
store i64 4377670473, ptr %170, align 4
%realxor-5369031128-1967 = xor i64 %r8, 4506609098
%realadd-5369031141-1968 = add i64 %realsub-5369033485-1831, %realxor-5369031128-1967
store i64 %realadd-5369031141-1968, ptr %172, align 4
store i32 -2056377491, ptr %158, align 4
store i64 1375224, ptr %167, align 4
store i8 -35, ptr %165, align 1
%realsub-5369033485-2000 = add nuw nsw i64 %realadd-5369259926-994, -66205754248
store i64 %realsub-5369033485-2000, ptr %171, align 4
store i32 -410453815, ptr %157, align 4
store i64 5374748856, ptr %9, align 4
store i64 0, ptr %6, align 4
store i32 0, ptr %4, align 4
ret i64 %216
}
After a little manual clean-up (cleaning up excess stores to .data section):
define range(i64 0, 4294967296) i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %rsp, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr readnone captures(none) %TEB, ptr readnone captures(none) %memory) local_unnamed_addr #0 {
%0 = trunc i64 %rdx to i32
%1 = trunc i64 %rcx to i32
%realadd-5368880833- = add i32 %0, %1
%2 = zext i32 %realadd-5368880833- to i64
ret i64 %2
}
VM based obfuscators do not provide significantly stronger protection against static analysis compared to control flow flattening, as both techniques primarily aim to obscure control flow without altering the original program logic. However, bin2bin VM based solutions face additional challenges, such as reliably translating assembly instructions, reconstructing jump tables, and handling exceptions, which can introduce complexity without necessarily enhancing protection against static analysis. Additionally, the added layers of abstraction in VM based obfuscation often result in increased runtime overhead, making the protected program slower and less efficient.
Even though VM based obfuscation is more advanced than flattening, if a VM based obfuscator did not use dispatchers, and simply executed the handlers in sequence, it would be trivial to analyze it even without any specialized tools. That said, when combined with other protection mechanisms, VM based obfuscation remains a unique and valuable tool in software protection.
Whats next?
So, we’ve learned it’s possible to create a generic deobfuscator that even works with commercial VM based obfuscations. Even though there are several challenges, such as MBA (Mixed Boolean Arithmetics) and unrollable loops. Hopefully, we will talk about them in next posts.
In the next post, we will dive deeper into the technical details mentioned here and demonstrate devirtualization using Tigress examples.
Special thanks to
phage
sneed
Thanks for reading!
If you found this post helpful, consider supporting me on Github Sponsors!
Useful resources:
https://www.youtube.com/watch?v=9EgnstyACKA
https://www.msreverseengineering.com/blog/2014/6/23/vmprotect-part-0-basics
https://0xnobody.github.io/devirtualization-intro/
https://secret.club/2021/09/08/vmprotect-llvm-lifting-1.html
https://github.com/JonathanSalwan/VMProtect-devirtualization
https://blog.back.engineering/17/05/2021/
https://blog.thalium.re/posts/llvm-powered-devirtualization/