
Deobfuskation durch Optimierung unter Verwendung von LLVM IR und Parsen von Assembly.
Mergen ist ein Tool, das entwickelt wurde, um Assembler-Code in die LLVM-Zwischendarstellung (IR) zu konvertieren. Dieses Tool ist konzipiert für:
Um das Projekt zu erstellen und auszuführen, werfen Sie einen Blick auf docs/BUILDING.md.
Rewrite-Arbeiten sollten das Baseline-Regressionsgate grün halten. Das Gate erstellt fokussierte PE-Beispiele, führt lifter aus und überprüft die gelifteten IR-Ausgaben.
scripts\\rewrite\\run.cmdWir führen eine symbolische Ausführung (oder symbolisches Liften) des Ziels durch. Die Idee hierbei ist, nicht einzelne Instruktionen zu liften, sondern eine gesamte Funktion. Wir erwarten nicht, dass eine einzelne Instruktion oder ein einzelner Basisblock jedes Mal gleich verhält, sondern behandeln sie so, als ob sie jedes Mal für unterschiedliche Zwecke da sein können und sind. Wir versuchen, das generierte IR so einfach und optimierbar wie möglich zu halten. Wir haben auch andere Anforderungen als ein üblicher Compiler. Wir nutzen Analysen, um den Kontrollfluss auszuwerten. Wir können uns nicht auf LLVM für alle unsere Analysen verlassen, da sie für andere Ziele erstellt wurden und für unseren Anwendungsfall suboptimal sein könnten.

Dies ist das praktische Beispiel, um zu veranschaulichen, wie Mergen gegen virtualisierte Programme vorgeht.
Dies ist unser Zielprogramm
struct test {
int a;
int b;
int c;
};
int maths(test a, int b, int c) {
return a.a + b - c;
}


VMProtect-Einstellungen, alles ist ausgeschaltet, wir virtualisieren die Funktion auf Ultra-Einstellung. (Getestete Versionen 3.4.0-3.6.0 3.8.1)


Hier führen wir mergen aus. Das erste Argument ist der Name der Datei und das zweite Argument die Adresse der Funktion. Sehen Sie, wie einfach es auszuführen ist. Und wir können die Ausgabe kompilieren, um sie mit unserem bevorzugten Decompiler zu erkunden.

; ModuleID = 'my_lifting_module'
source_filename = "my_lifting_module"
; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: read)
define i64 @main(i64 %rax, i64 %rcx, i64 %rdx, i64 %rbx, i64 %0, i64 %rbp, i64 %rsi, i64 %rdi, i64 %r8, i64 %r9, i64 %r10, i64 %r11, i64 %r12, i64 %r13, i64 %r14, i64 %r15, ptr nocapture readonly %memory) local_unnamed_addr #0 {
entry:
%stackmemory = alloca i128, i128 13758960, align 8
%1 = trunc i64 %r8 to i32
%2 = trunc i64 %rdx to i32
%GEPLoadxd-5369456437- = getelementptr i8, ptr %memory, i64 %rcx
%3 = load i32, ptr %GEPLoadxd-5369456437-, align 4
%adc-temp-5370242400- = sub i32 %2, %1
%realnot-5369532059- = add i32 %adc-temp-5370242400-, %3
%stackmemory10243.sroa.55.1375304.insert.ext10255 = zext i32 %realnot-5369532059- to i64
ret i64 %stackmemory10243.sroa.55.1375304.insert.ext10255
}
attributes #0 = { mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: read) }
Nach dem Kompilieren:


Jetzt könnte Ihnen auffallen, dass die Register etwas falsch sind. Dies liegt daran, dass wir den Aufrufkonventionen nicht folgen. Wenn wir den Aufrufkonventionen folgen würden, würde die Funktionssignatur so aussehen:
define i64 @main(i64 %rcx, i64 %rdx, i64 %rdx, i64 %r8, i64 %r9 ...)
Also passen wir einfach die Funktionssignatur an, damit sie normal aussieht. Wenn Sie weitere Fragen zu diesem Teil haben, empfehle ich Ihnen, sich über Aufrufkonventionen und ABI zu informieren.
Nehmen wir an, wir haben diesen Code. VMs werden den untenstehenden Code nehmen und in einen indirekten Sprung umwandeln, was für den Reverse Engineer etwas unbequemer ist.
int maths(int a, int b, int c) {
if (a > b)
return a + b + c;
else
return a - b - c;
}
next_handler = xxx;
if ( a-b > 0 )
next_handler = yyy;
jump next_handler;
Wir versuchen stets, Werte zu analysieren und im Auge zu behalten. Das ermöglicht es uns, den Kontrollfluss zu verstehen. Für Springtabellen-ähnliche Verzweigungen Optimierte Ausgabe wäre ein einfaches
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 {
fake_ret:
%0 = lshr i64 %rcx, 62
%common.ret.op = and i64 %0, 2
ret i64 %common.ret.op
}
Nicht optimierte Ausgabe. (DCE zur Lesbarkeit)
source_filename = "my_lifting_module"