
Désobfuscation par optimisation à l'aide de LLVM IR et de l'analyse de l'assembleur.
Mergen est un outil conçu pour convertir du code assembleur en représentation intermédiaire LLVM (IR). Cet outil est conçu pour :
Pour construire et exécuter le projet, consultez docs/BUILDING.md.
La porte construit des échantillons PE ciblés, exécute lifter et vérifie les sorties IR levées.
scripts\\rewrite\\run.cmdNous exécutons symboliquement (ou levons symboliquement) la cible, l'idée ici n'est pas de lever des instructions individuelles, mais de lever une fonction entière. Nous ne nous attendons pas à ce qu'une instruction ou un bloc de base se comporte de la même manière à chaque fois, nous les traitons plutôt comme s'ils pouvaient l'être et le sont à des fins différentes à chaque fois. Nous essayons de garder l'IR généré aussi simple et optimisable que possible. Nous avons également des besoins différents de ceux d'un compilateur habituel. Nous utilisons l'analyse pour évaluer le flux de contrôle. Nous ne pouvons pas dépendre de LLVM pour toute notre analyse, car ils sont créés pour des objectifs différents et pourraient être sous-optimaux pour notre cas d'utilisation.

Voici l'exemple pratique pour illustrer comment Mergen résout les programmes virtualisés.
Voici notre programme cible
struct test {
int a;
int b;
int c;
};
int maths(test a, int b, int c) {
return a.a + b - c;
}


Paramètres VMProtect, tout est désactivé, nous virtualisons la fonction en réglage ultra. (Versions testées 3.4.0-3.6.0 3.8.1)


Ici, nous exécutons mergen. Le premier argument est le nom du fichier et le second argument est l'adresse de la fonction. Regardez comme c'est simple à exécuter. Et nous pouvons compiler la sortie pour l'explorer avec notre décompilateur préféré.

; 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) }
Après compilation :


Vous remarquerez peut-être que les registres sont un peu décalés. Cela vient du fait que nous ne suivons pas les conventions d'appel ; si nous les suivions, la signature de la fonction ressemblerait à ceci :
define i64 @main(i64 %rcx, i64 %rdx, i64 %rdx, i64 %r8, i64 %r9 ...)
Donc, nous ajustons simplement la signature de la fonction pour qu'elle paraisse normale. Si vous avez d'autres questions sur cette partie, je vous suggère de vous renseigner sur les conventions d'appel et l'ABI.
Donc, disons que nous avons ce code. Les machines virtuelles prendront le code ci-dessous et le transformeront en un saut indirect, ce qui est légèrement moins pratique pour le rétro-ingénieur.
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;
Nous essayons toujours d'analyser les valeurs et de les suivre. Cela nous permet de comprendre le flux de contrôle. Pour les branches de type table de sauts La sortie optimisée serait un simple
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
}
Sortie non optimisée. (DCE pour lisibilité)
source_filename = "my_lifting_module"
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 %TEB, ptr %memory) {
%lsb = and i64 %rcx, 255
%pf1 = mul i64 %lsb, 72340172838076673
%pf2 = and i64 %pf1, -9205322385119247871
%pf3 = urem i64 %pf2, 511
%pf4 = and i64 %pf3, 1
%pf5 = icmp eq i64 0, %pf4
%0 = zext i1 %pf5 to i64
%createrflag2 = shl i64 %0, 2
%creatingrflag = or i64 2, %createrflag2
%zeroflag = icmp eq i64 %rcx, 0
%1 = zext i1 %zeroflag to i64
%createrflag21 = shl i64 %1, 6
%creatingrflag2 = or i64 %creatingrflag, %createrflag21
%signflag = icmp slt i64 %rcx, 0
%2 = zext i1 %signflag to i64
%createrflag23 = shl i64 %2, 7
%creatingrflag4 = or i64 %creatingrflag2, %createrflag23
%GEPSTORE-5368713221- = getelementptr i8, ptr %memory, i64 1376032
store i64 %creatingrflag4, ptr %GEPSTORE-5368713221-, align 4
%realand-5368713229- = and i64 %creatingrflag4, 128
%shr-lshr-5368713233- = lshr i64 %realand-5368713229-, 7
%3 = mul i64 %shr-lshr-5368713233-, 4
%bvalue_indexvalue = add i64 5368713249, %3
%4 = icmp eq i64 %bvalue_indexvalue, 5368713253
%lolb- = select i1 %4, i64 5368713264, i64 5368713257
%GEPSTORE-5368713248- = getelementptr i8, ptr %memory, i64 1376032
store i64 %lolb-, ptr %GEPSTORE-5368713248-, align 4
br i1 %4, label %real_ret, label %real_ret41
real_ret: ; preds = %fake_ret
%inc-5368713273- = add i64 %shr-lshr-5368713233-, 1
ret i64 %inc-5368713273-
real_ret41: ; preds = %fake_ret
ret i64 %shr-lshr-5368713233-
}
Remarquez cette partie
%realand-5368713229- = and i64 %creatingrflag4, 128
%shr-lshr-5368713233- = lshr i64 %realand-5368713229-, 7
Nous obtenons les indicateurs, puis nous obtenons le 7e bit qui est le Sign Flag, puis nous utilisons le Sign Flag pour calculer une adresse. Grâce à l'analyse, nous déterminons que l'adresse peut être l'une des deux valeurs, 5368713257 ou 5368713264, puis nous transformons cela en une comparaison. Si l'adresse est 5368713257, prendre une branche, sinon l'autre. Ce faisant, il est également important de marquer la condition avec la valeur appropriée car plus tard, nous pourrions avoir besoin de calculer un autre saut avec exactement la même valeur.
Bien que nous résolvions les sauts indirects, les sauts avec plus de 2 emplacements possibles ne sont pas pris en charge. Cela est dû au fait que l'analyse correspondante n'est pas encore implémentée. Cela nous permet de résoudre les branches de style machine virtuelle, mais pose problème avec les tables de sauts réelles.
Notre programme cible :

Paramètres Themida (nous ne nous intéressons qu'aux machines virtuelles pour l'instant) :



Après virtualisation :

Exécution de Mergen :

Code de sortie : cliquez ici Alors, pourquoi notre résultat n'est-il pas aussi réussi que le levage d'un binaire protégé par VMP ?
Themida écrit activement sur la section .themida. Contrairement à la pile, nous ne pouvons pas ignorer ces écritures, car ces valeurs pourraient être lues par d'autres éléments plus tard.
Mais nous avons une solution temporaire à cela. Supprimer tous les stockages dans la section .themida. Comme notre programme n'écrit pas en mémoire, j'ai simplement commenté tous les stockages. Il nous reste donc ceci :
source_filename = "my_lifting_module"
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 writeonly %memory) local_unnamed_addr #0 {
%trunc = trunc i64 %r8 to i32
%trunc1 = trunc i64 %rdx to i32
%trunc2 = trunc i64 %rcx to i32
%realadd-5369771371- = add i32 %trunc1, %trunc2
%realadd-5369582686- = add i32 %realadd-5369771371-, %trunc
%trunc457139 = zext i32 %realadd-5369582686- to i64
ret i64 %trunc457139
}
attributes #0 = { mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: write) }
Rejoignez notre Serveur Discord Mergen pour échanger des idées ou simplement discuter en général.