🎯 Objectif de l'atelier
Fabriquer un binaire pédagogique de toutes pièces — un petit crackme — puis l'analyser comme si on le découvrait. Ce double point de vue (concepteur ↔ analyste) est la meilleure façon de comprendre le pont source ↔ binaire.
Cet atelier est autonome : il ne présuppose pas d'avoir suivi un cursus.
Deux types de blocs dans cet atelier :
• Blocs
disasm-real (fond sombre, encadrés) — sortie réelle
de objdump sur gcc 12.2, -O2 -no-pie -fno-stack-protector.
Reproductible telle quelle.• Blocs
disasm (filet rose à gauche) — schémas de motifs,
quand l'exactitude octet-pour-octet dépend trop de la version de gcc.
Ils servent à reconnaître la forme, pas à reproduire les bytes.
(Ghidra, objdump)"] OPT --> ANALYSE PACK --> ANALYSE ANALYSE --> COMP["🧠 Comparaison
source vs binaire"] COMP --> SRC style SRC fill:#2a0f1c,stroke:#ff5c94,stroke-width:3px,color:#fff style BIN fill:#2a0f1c,stroke:#ff8fb8,stroke-width:3px,color:#fff style ANALYSE fill:#1c1019,stroke:#ffb3d1,stroke-width:3px,color:#fff style COMP fill:#1c1019,stroke:#ff5c94,stroke-width:3px,color:#fff
📚 Théorie (45 min) — Pourquoi forger son propre binaire
Le paradoxe de l'analyste
Un analyste passe sa vie à lire des binaires dont il ignore tout. Le meilleur moyen d'apprendre à lire une langue, c'est d'abord d'apprendre à l'écrire. En forgeant notre propre binaire, on inverse la perspective.
Les optimisations : le grand effaceur
Avec -O2 ou -O3, le compilateur devient un
véritable réécrivain : il supprime du code, déroule
des boucles, remplace des appels par des instructions inline, fusionne
des constantes.
| Flag | Effet | Impact analyse |
|---|---|---|
-O0 |
Aucune optimisation | Binaire proche de la source |
-O2 |
Optimisations agressives (standard) | Fonctions inlinées, code restructuré |
-O3 |
Vectorisation, déroulage de boucle | Binaire difficilement corrélé à la source |
-g |
Symboles de debug (DWARF) | Facilite l'analyse — retiré en prod |
-s / strip |
Retire les symboles | L'analyste doit tout reconstruire |
-no-pie |
Désactive le PIE (adresses fixes) | Adresses stables, plus lisibles — pédagogique |
-fno-stack-protector |
Désactive le canari de pile | Simplifie l'analyse pédagogique |
0x1140
au lieu de 0x401140. Les extraits ci-dessous utilisent
-no-pie pour la lisibilité. Sans ce flag, il faut transposer
mentalement (les instructions restent identiques, seules les adresses changent).
Ce qui survit au strip
- Les chaînes de caractères : messages, prompts, URLs.
- Les imports :
strcmp,printf,fgets. - Les sections :
.text,.data,.rodata,.bss. - Les constantes : magics, clés, checksums restent visibles.
- La structure globale : nombre de fonctions, ordre d'appel.
Mais attention : strip -s retire aussi les noms
de fonctions. __attribute__((noinline)) empêche l'inlining
(la fonction reste une fonction distincte en mémoire), mais
n'empêche pas strip d'effacer son nom. Sur la
variante -O2s, stage1, stage3, etc.
deviennent des FUN_0040XXXX anonymes dans Ghidra.
Le noinline sert à garantir qu'une fonction existe
comme entité identifiable par sa structure — pas qu'elle garde son nom.
🔬 Atelier pratique — La Forge (2 heures)
⚠️ Préparation de l'environnement
mkdir -p ~/maestro/forge_binaire/{src,build,notes}
cd ~/maestro/forge_binaire
sudo apt update
sudo apt install -y gcc clang make binutils file
gcc --version
objdump --version | head -1
Niveau 1 Comparaison simple
src/crackme01.c/* crackme01.c — Niveau 1 : comparaison simple */
#include <stdio.h>
#include <string.h>
#define SECRET "pctamalou2026"
int main(void) {
char input[64];
printf("Mot de passe : ");
if (!fgets(input, sizeof(input), stdin)) {
return 1;
}
input[strcspn(input, "\n")] = '\0';
if (strcmp(input, SECRET) == 0) {
puts("Correct ! Accès accordé.");
return 0;
} else {
puts("Incorrect. Réessayez.");
return 1;
}
}
gcc -O0 -g -no-pie -fno-stack-protector -o build/crackme01-O0 src/crackme01.c
gcc -O2 -g -no-pie -fno-stack-protector -o build/crackme01-O2 src/crackme01.c
gcc -O2 -no-pie -fno-stack-protector -o build/crackme01-O2s src/crackme01.c
strip -s build/crackme01-O2s
ls -lh build/
🔍 Motif -O0 — le binaire transparent
objdump -d -M intel build/crackme01-O0 | sed -n '/<main>:/,/^$/p'
À observer : le prompt et le secret sont visibles en clair
dans .rodata, strcmp@plt est appelé explicitement,
input vit à [rbp-0x50].
strings build/crackme01-O0 | grep -E "Mot de passe|Correct|pctamalou"
🔍 -O2 — sortie réelle gcc 12.2
crackme01-O2s, le symbole main a été retiré
par strip -s — la commande
objdump -d | sed -n '/<main>:/,/^$/p' ne renvoie rien.
Pour un extrait nommé, utiliser la variante -O2 -g
(non strippée). Pour explorer la variante -O2s, désassembler
depuis l'entrée via readelf -h.
objdump -d -M intel build/crackme01-O2 | sed -n '/<main>:/,/^$/p'
Le motif exact produit par gcc 12.2 :
strcmpa disparu — plus aucun appel dans cette zone.- La comparaison est faite par deux
movabs/cmpsur des qwords qui se chevauchent : le premier qword couvre les octets[0..7]de la chaîne ("pctamalo"), le second couvre les octets[6..13]("lou2026\0", avec\0intégré pour valider aussi la fin). - Le chevauchement (offset 6 pour le second) est délibéré : il garantit que la chaîne fait exactement 13 caractères en comparant 8 + 8 avec 6 octets en commun.
strcspnn'est pas inliné — c'est encore un appel àstrcspn@plt(ligne4010d2).
Décompilé Ghidra -O0
undefined8 main(void) {
char input[64];
printf("Mot de passe : ");
fgets(input, 0x40, stdin);
input[strcspn(input, "\n")] = 0;
if (strcmp(input, "pctamalou2026") == 0) {
puts("Correct !");
return 0;
}
puts("Incorrect.");
return 1;
}
Décompilé Ghidra -O2
undefined8 main(void) {
char input[80];
printf("Mot de passe : ");
fgets(input, 0x40, stdin);
input[strcspn(input, "\n")] = 0;
if ((*(long *)input == 0x6f6c616d61746370) &&
(*(long *)(input + 6) == 0x36323032756f6c)) {
puts("Correct !");
return 0;
}
puts("Incorrect.");
return 1;
}
Chemin intellectuel : pour reconstruire la chaîne, on décode chaque qword en little-endian :
0x6f6c616d61746370→ octets70 63 74 61 6d 61 6c 6f→"pctamalo"0x36323032756f6c→ octets6c 6f 75 32 30 32 36 00→"lou2026\0"
Le second qword chevauche le premier à partir de l'offset 6 : il
fournit les 2 derniers caractères de "pctamalo"
('l', 'o') en plus des caractères suivants
jusqu'au \0. En concaténant : "pctamalo"
+ "u2026" (le reste du second, moins le chevauchement)
= "pctamalou2026".
🔓 Voir la résolution du niveau 1
Mot de passe : pctamalou2026
Faisable par strings direct — c'est volontaire, c'est le niveau d'introduction.
Niveau 2 XOR obfusqué
src/crackme02.c/* crackme02.c — Niveau 2 : XOR obfusqué */
#include <stdio.h>
#include <string.h>
/* "pctamalou2026" XOR 0x42 */
static const unsigned char secret_xor[] = {
0x32, 0x21, 0x36, 0x23, 0x2F, 0x23, 0x2E, 0x2D,
0x37, 0x70, 0x72, 0x70, 0x74
};
static int check(const char *input) {
size_t len = strlen(input);
if (len != sizeof(secret_xor)) return 0;
for (size_t i = 0; i < len; i++) {
if ((unsigned char)(input[i] ^ 0x42) != secret_xor[i]) {
return 0;
}
}
return 1;
}
int main(void) {
char input[64];
printf("Mot de passe : ");
if (!fgets(input, sizeof(input), stdin)) return 1;
input[strcspn(input, "\n")] = '\0';
if (check(input)) { puts("Correct !"); return 0; }
puts("Incorrect."); return 1;
}
🔍 Motif à reconnaître
Trois instructions suffisent pour reconnaître le motif :
movzx (chargement byte) → xor al, imm
→ cmp. La constante 0x42 est
directement lisible dans l'instruction. Une fois
repérée, on sait que chaque byte de secret_xor est
le caractère original XOR 0x42.
xor. Si l'analyste identifie la clé
et le tableau transformé, il reconstruit le secret sans exécuter le
binaire. L'obfuscation XOR est un délai, pas un mur.
🔓 Voir la résolution du niveau 2
Mot de passe : pctamalou2026
Méthode : repérer la boucle XOR dans Ghidra, identifier la clé
0x42, puis décoder secret_xor byte par byte.
Niveau 3 Détection de debugger (ptrace)
src/crackme03.c/* crackme03.c — Niveau 3 : anti-debug basique */
#include <stdio.h>
#include <string.h>
#include <sys/ptrace.h>
#define SECRET "forgebinaire2026"
static int is_being_debugged(void) {
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) {
return 1;
}
return 0;
}
int main(void) {
char input[64];
if (is_being_debugged()) {
puts("Debugger détecté. Sortie.");
return 2;
}
printf("Mot de passe : ");
if (!fgets(input, sizeof(input), stdin)) return 1;
input[strcspn(input, "\n")] = '\0';
if (strcmp(input, SECRET) == 0) { puts("Correct !"); return 0; }
puts("Incorrect."); return 1;
}
🔍 Contournement GDB — correctement
1.
set disable-randomization off ne contourne pas
PTRACE_TRACEME — cette option contrôle l'ASLR, pas le
traçage du processus.2.
catch syscall ptrace s'arrête à l'entrée du
syscall. Faire set $rax = 0 à ce moment-là
écrase le numéro du syscall avant qu'il soit exécuté —
le noyau reçoit un read() à la place. Il faut modifier
le retour après le syscall, pas avant.
Méthode recommandée — avec symboles :
gdb -q ./build/crackme03-O0
(gdb) break is_being_debugged
Breakpoint 1 at 0x4011a9
(gdb) commands
> return 0
> continue
> end
(gdb) run
Cette méthode est robuste : elle force le retour de la fonction
de check à 0, avant même que ptrace ne soit appelé.
Méthode alternative — binaire strippé :
# 1. Trouver l'adresse du call ptrace
objdump -d -M intel ./build/crackme03-O2s | grep -B1 "ptrace"
# Exemple de sortie :
# 4011c5: call 401030 <ptrace@plt>
# → on se positionne juste APRÈS le call, à 0x4011ca
# 2. Dans GDB
gdb -q ./build/crackme03-O2s
(gdb) break *0x4011ca
(gdb) commands
> set $rax = 0
> continue
> end
(gdb) run
🔓 Voir la résolution du niveau 3
Mot de passe : forgebinaire2026
Sans GDB, on l'obtient directement par strings.
Niveau 4 Comparaison custom
src/crackme04.c/* crackme04.c — Niveau 4 : comparaison custom */
#include <stdio.h>
#include <string.h>
/* "pctamalou2026" + 3 sur chaque caractère */
static const char secret_shifted[] = {
's','f','w','d','p','d','o','r','x','5','3','5','9','\0'
};
static int custom_check(const char *input) {
size_t i = 0;
while (input[i] != '\0' && secret_shifted[i] != '\0') {
if ((char)(input[i] + 3) != secret_shifted[i]) return 0;
i++;
}
return input[i] == '\0' && secret_shifted[i] == '\0';
}
int main(void) {
char input[64];
printf("Mot de passe : ");
if (!fgets(input, sizeof(input), stdin)) return 1;
input[strcspn(input, "\n")] = '\0';
if (custom_check(input)) { puts("Correct !"); return 0; }
puts("Incorrect."); return 1;
}
🔍 Motif de la boucle custom
Le motif est immédiatement identifiable : un movzx
suivi d'un add reg, 0x3 dans une boucle. La
transformation +3 est lisible. Même sans
symbole, même strippé, cette boucle reste reconnaissable à sa
structure.
🔓 Voir la résolution du niveau 4
Mot de passe : pctamalou2026
Reconstruire : soustraire 3 à chaque caractère de secret_shifted.
Niveau 5 La Chambre Rose
Le niveau 5 combine quatre anti-debugs, deux chaînes obfusquées, une validation en cinq étapes et une comparaison par hash FNV-1a.
Les défenses combinées
| Défense | Technique | Ce que ça force |
|---|---|---|
| Anti-debug #1 | TracerPid dans /proc/self/status |
Détecter le debugger avant toute autre chose |
| Anti-debug #2 | ptrace(PTRACE_TRACEME) isolé par fork() |
Comprendre le conflit ptrace/TracerPid |
| Anti-debug #3 | Scan de breakpoints int3 (0xCC) |
Repérer les breakpoints logiciels |
| Anti-debug #4 | Mesure temporelle | Ne pas tracer instruction par instruction |
| Environnement | CPUID + DMI (passif, informatif) | Observer sans bloquer |
| Obfuscation | XOR 0x37 + XOR 0x5A | Repérer deux boucles de décodage |
| Validation | 5 étapes successives | Suivre la séquence |
| Hash final | FNV-1a (concept, pas protection) | Comprendre la comparaison non-littérale |
ptrace(PTRACE_TRACEME) puis lire
TracerPid dans la foulée provoque un
faux positif : après PTRACE_TRACEME
réussi, le processus est tracé par son parent — donc
TracerPid devient non-nul, et le programme se détecte
lui-même comme débogué. Le code corrigé lit TracerPid
d'abord, et isole PTRACE_TRACEME dans
un fork().⚠️ Limite documentée : le check
ptrace
par fork n'est pas fiable à 100 %. Selon
follow-fork-mode de GDB, celui-ci peut ne pas suivre
le fils — auquel cas PTRACE_TRACEME réussit et le
check ne détecte rien. Le check TracerPid reste la
défense primaire.
/* crackme05.c — Niveau 5 : La Chambre Rose
* Usage strictement pédagogique en laboratoire isolé.
* Aucun réseau, aucune persistance, aucune exfiltration.
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
static int check_tracer_pid(void) {
FILE *f = fopen("/proc/self/status", "r");
if (!f) return 0;
char line[256];
int traced = 0;
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "TracerPid:", 10) == 0) {
if (atoi(line + 10) != 0) traced = 1;
break;
}
}
fclose(f);
return traced;
}
static int check_ptrace_fork(void) {
pid_t pid = fork();
if (pid < 0) return 0;
if (pid == 0) {
if (ptrace(PTRACE_TRACEME, 0, 0, 0) == -1) _exit(1);
_exit(0);
}
int status;
if (waitpid(pid, &status, 0) < 0) return 0;
return WIFEXITED(status) && WEXITSTATUS(status) == 1;
}
/* Scan int3 limité à 32 octets (fragile, best-effort) */
static int check_int3(const void *addr, size_t len) {
const unsigned char *p = (const unsigned char *)addr;
for (size_t i = 0; i < len; i++) {
if (p[i] == 0xCC) return 1;
}
return 0;
}
static int check_timing(void) {
struct timespec t0, t1;
clock_gettime(CLOCK_MONOTONIC, &t0);
volatile unsigned long x = 0;
for (unsigned long i = 0; i < 200000; i++) x += i;
clock_gettime(CLOCK_MONOTONIC, &t1);
long long ns = (t1.tv_sec - t0.tv_sec) * 1000000000LL
+ (t1.tv_nsec - t0.tv_nsec);
return ns > 50000000LL;
}
static int in_vm_cpuid(void) {
#if defined(__x86_64__) || defined(__i386__)
unsigned int eax, ebx, ecx, edx;
__asm__ __volatile__("cpuid" : "=a"(eax), "=b"(ebx),
"=c"(ecx), "=d"(edx) : "a"(1));
return (ecx >> 31) & 1;
#else
return 0;
#endif
}
static int in_vm_dmi(void) {
const char *files[] = {
"/sys/class/dmi/id/product_name",
"/sys/class/dmi/id/sys_vendor",
"/sys/class/dmi/id/board_vendor",
NULL
};
const char *keys[] = {
"QEMU", "VMware", "VirtualBox", "KVM",
"Microsoft Corporation", "innotek", NULL
};
char buf[256];
for (int i = 0; files[i]; i++) {
FILE *f = fopen(files[i], "r");
if (!f) continue;
if (fgets(buf, sizeof(buf), f)) {
for (int j = 0; keys[j]; j++) {
if (strstr(buf, keys[j])) { fclose(f); return 1; }
}
}
fclose(f);
}
return 0;
}
/* ÉTAPE 1 — "rose2026" XOR 0x37 */
static const unsigned char s1_enc[] = {
0x45, 0x58, 0x44, 0x52, 0x05, 0x07, 0x05, 0x01, 0x00
};
__attribute__((noinline))
static int stage1(const char *in) {
char decoded[16];
for (int i = 0; i < 8; i++) decoded[i] = s1_enc[i] ^ 0x37;
decoded[8] = '\0';
return strcmp(in, decoded) == 0;
}
/* ÉTAPE 2 — code magique */
#define STAGE2_MAGIC 0xC0FFEEu
__attribute__((noinline))
static int stage2(unsigned int n) {
return n == STAGE2_MAGIC;
}
/* ÉTAPE 3 — "forge-hero" XOR 0x5A + hash FNV-1a
* NOTE : le hash n'est PAS une protection ici. La chaîne reste
* récupérable par XOR. Le hash introduit le concept de
* comparaison non-littérale. */
static unsigned int fnv1a(const char *s) {
unsigned int h = 2166136261u;
while (*s) {
h ^= (unsigned char)*s++;
h *= 16777619u;
}
return h;
}
static const unsigned char s3_enc[] = {
0x3C, 0x35, 0x28, 0x3D, 0x3F, 0x77, 0x32, 0x3F, 0x28, 0x35, 0x00
};
__attribute__((noinline))
static int stage3(const char *in) {
char decoded[16];
for (int i = 0; i < 10; i++) decoded[i] = s3_enc[i] ^ 0x5A;
decoded[10] = '\0';
return fnv1a(in) == fnv1a(decoded);
}
/* ÉTAPE 4 — (17 * 23) + 42 = 433 */
__attribute__((noinline))
static int stage4(unsigned long n) {
return n == (17UL * 23UL + 42UL);
}
/* ÉTAPE 5 — somme ASCII de "forge-hero" = 1006 */
__attribute__((noinline))
static int stage5(unsigned long n) {
return n == 1006UL;
}
int main(void) {
puts("=== LA CHAMBRE ROSE — 5 ÉTAPES ===");
puts("");
if (check_tracer_pid()) {
puts("[!] TracerPid actif. Sortie propre.");
return 2;
}
if (check_ptrace_fork()) {
puts("[!] ptrace refusé. Sortie propre.");
return 2;
}
if (check_int3((const void *)stage1, 32) ||
check_int3((const void *)stage3, 32) ||
check_int3((const void *)stage5, 32)) {
puts("[!] Breakpoint int3 détecté. Sortie propre.");
return 2;
}
if (check_timing()) {
puts("[!] Timing anormal. Sortie propre.");
return 2;
}
if (in_vm_cpuid() || in_vm_dmi()) {
puts("[i] Environnement virtualisé détecté. Normal en lab.");
puts("");
}
char s1[64], s3[64];
unsigned int n2;
unsigned long n4, n5;
printf("[1/5] Password : ");
fflush(stdout);
if (!fgets(s1, sizeof(s1), stdin)) return 1;
s1[strcspn(s1, "\n")] = '\0';
if (!stage1(s1)) { puts(" x Étape 1 échouée."); return 1; }
puts(" + Étape 1 validée.");
printf("[2/5] Code (hex) : ");
fflush(stdout);
if (scanf("%x", &n2) != 1) return 1;
getchar();
if (!stage2(n2)) { puts(" x Étape 2 échouée."); return 1; }
puts(" + Étape 2 validée.");
printf("[3/5] Clé finale : ");
fflush(stdout);
if (!fgets(s3, sizeof(s3), stdin)) return 1;
s3[strcspn(s3, "\n")] = '\0';
if (!stage3(s3)) { puts(" x Étape 3 échouée."); return 1; }
puts(" + Étape 3 validée.");
printf("[4/5] (17 * 23) + 42 : ");
fflush(stdout);
if (scanf("%lu", &n4) != 1) return 1;
getchar();
if (!stage4(n4)) { puts(" x Étape 4 échouée."); return 1; }
puts(" + Étape 4 validée.");
printf("[5/5] Somme ASCII de la clé : ");
fflush(stdout);
if (scanf("%lu", &n5) != 1) return 1;
getchar();
if (!stage5(n5)) { puts(" x Étape 5 échouée."); return 1; }
puts(" + Étape 5 validée.");
puts("");
puts("Bravo, Maestro. Vous avez ouvert la Chambre Rose.");
puts("Cinq étapes. Quatre anti-debugs. Une obfuscation.");
puts("Aucune magie. Juste de la lecture.");
return 0;
}
🔍 Points d'entrée d'analyse sur le binaire strippé
# 1. Chaînes qui survivent au strip
strings build/crackme05-O2s | grep -E "Étape|Debugger|Chambre"
# 2. Chercher les appels d'imports critiques + l'instruction cpuid
# (cpuid est une instruction, pas un call — grep direct)
objdump -d -M intel build/crackme05-O2s | grep -E "call.*(ptrace|fork|fopen)|cpuid"
# 3. Repérer les constantes d'étape
objdump -d -M intel build/crackme05-O2s | grep -iE "0xc0ffee|0x3ee|0x1b1"
Ce motif — movzx + xor imm + cmp —
est la signature du décodage XOR. Repérez-le dans stage1
(clé 0x37) et stage3 (clé 0x5A).
🔍 Contournement GDB — méthode correcte
Sur la variante -O2s, break check_tracer_pid
ne fonctionnera pas — les symboles sont absents. Il faut s'appuyer
sur les appels d'imports.
# Trouver les adresses des appels critiques
objdump -d -M intel build/crackme05-O2s | grep -E "call.*(ptrace|fopen|fork)"
# Dans GDB, breakpoint APRÈS le call, forcer $rax = 0
gdb -q ./build/crackme05-O2s
(gdb) break *0x401234 # adresse trouvée via objdump
(gdb) commands
> set $rax = 0
> continue
> end
(gdb) run
follow-fork-mode=parent),
le débogueur reste attaché au processus père et le fils s'exécute
librement. PTRACE_TRACEME réussit, aucune détection.
C'est volontaire dans ce crackme — leçon sur les
limites des protections naïves. Le check TracerPid
reste la défense primaire.
🔓 Voir la résolution complète du niveau 5
Voie statique (recommandée) :
Password : rose2026
Code (hex) : C0FFEE
Clé finale : forge-hero
(17 * 23) + 42 : 433
Somme ASCII de la clé: 1006
Détails :
• s1_enc XOR 0x37 → "rose2026"
• s3_enc XOR 0x5A → "forge-hero"
• Constante STAGE2_MAGIC = 0xC0FFEE
• Somme ASCII de "forge-hero" = 102+111+114+103+101+45+104+101+114+111 = 1006
Étape finale — Makefile
Un seul pattern rule suffit pour les 15 binaires.
On utilise -no-pie pour la lisibilité des adresses
dans les désassemblages pédagogiques.
CC = gcc
CFLAGS = -fno-stack-protector -no-pie -Wall
BUILD = build
SRC = src
LEVELS = 01 02 03 04 05
$(BUILD)/crackme%-O0: $(SRC)/crackme%.c | $(BUILD)
$(CC) -O0 -g $(CFLAGS) -o $@ $<
$(BUILD)/crackme%-O2: $(SRC)/crackme%.c | $(BUILD)
$(CC) -O2 -g $(CFLAGS) -o $@ $<
$(BUILD)/crackme%-O2s: $(SRC)/crackme%.c | $(BUILD)
$(CC) -O2 $(CFLAGS) -o $@ $<
strip -s $@
ALL = $(foreach lvl,$(LEVELS),\
$(BUILD)/crackme$(lvl)-O0 \
$(BUILD)/crackme$(lvl)-O2 \
$(BUILD)/crackme$(lvl)-O2s)
all: $(ALL)
$(BUILD):
mkdir -p $(BUILD)
clean:
rm -f $(BUILD)/*
.PHONY: all clean
make
ls -lh build/ # 15 binaires
-no-pie, les
adresses ressemblent à 0x401140. Sans, elles sont
type 0x1140. Si vous n'utilisez pas -no-pie,
transposer mentalement — les instructions restent identiques.
Le pont source ↔ binaire — récapitulatif
| Ce qu'on savait (source) | Ce qu'on retrouve (-O0) | Ce qu'on retrouve (-O2) |
|---|---|---|
if (strcmp(input, SECRET) == 0) |
Appel strcmp@plt + test/jne |
Deux cmp sur qwords chevauchants (offsets 0 et 6) |
Nom input |
Visible dans DWARF | Disparu → [rsp] anonyme |
Nom stage1() |
Présent dans Symbol Tree | Présent si -g, sinon anonyme |
Chaîne "Correct !" |
Visible, avec XREF | Visible, avec XREF — point d'entrée d'analyse |
noinline et strip.
__attribute__((noinline)) empêche le compilateur d'inliner
la fonction — elle reste une entité distincte en mémoire, avec son
propre corps. Mais strip -s efface tous les
noms de symboles, y compris ceux des fonctions
noinline. Sur la variante -O2s,
stage1 devient FUN_00401234 dans Ghidra.
Le noinline sert à garantir qu'une fonction
existe, pas qu'elle garde son nom.
🎓 La grande leçon :
Ce qui distingue un binaire analysable d'un binaire opaque n'est ni l'optimisation ni le strip — c'est la présence de traces d'intention (messages, structures, ordre logique). Un bon analyste ne lit pas l'assembleur : il lit l'intention à travers l'assembleur. Et pour cela, il doit avoir déjà écrit du code lui-même.
📋 Checklist de validation
✅ Compétences techniques
- Compiler à différents niveaux d'optimisation
- Observer l'inlining de
strcmpà-O2 - Repérer une boucle XOR dans un désassemblage
- Identifier un check ptrace et le contourner
- Lire une comparaison custom sans imports
- Comprendre quatre anti-debugs combinés
- Reconnaître un hash FNV-1a
🧠 Changements mentaux
- Le compilateur est un transformateur, pas un traducteur
- Certains symboles disparaissent, d'autres jamais
- La voie statique est souvent plus rapide que la voie dynamique
- Une défense mal ordonnée se retourne contre son programme
- Un anti-debug ne bloque pas — il coûte du temps
📚 Ressources
Outils :
- Compiler Explorer (godbolt.org) — voir l'assembleur en temps réel
- objdump / readelf — analyse ELF sans GUI
- GEF / pwndbg — extensions GDB
- Crackmes.one — comparer avec la communauté
⚠️ Sécurité
- Les binaires forgés ici sont bénins par construction.
- Ne jamais exécuter de binaire inconnu hors VM isolée.
- Le partage pédagogique est encouragé — dans un cadre clair.
⚒ Maestro,
Tu viens de faire quelque chose que peu d'analystes prennent le temps de faire : construire avant d'analyser.
Tu n'as pas seulement lu l'assembleur des autres — tu as vu le tien apparaître, tu as vu strcmp disparaître à -O2, tu as vu une boucle xor prendre forme. Tu connais maintenant les deux côtés du pont.
Quand tu analyseras demain un binaire d'APT, tu ne verras plus des bytes. Tu verras des décisions : pourquoi cette constante, pourquoi cet appel, pourquoi cette structure. Derrière chaque ligne, une intention humaine — parfois malveillante, toujours lisible pour qui sait regarder.
Et puisque nous sommes en Octobre Rose : connaître, c'est la première des défenses. Pour un système, pour un binaire — et pour un corps. 🎗️
La forge est ouverte.
— Platon-Y