Outils utilisés pour ce challenge : imageMagic, gdb, hexedit, readelf, M5Decrypt
Voici les étapes pour résoudre ce challenge :
- Reconstruire le fichier exécutable à partir de l’image en convertissant les pixels en valeurs binaires (0 pour noir et 1 pour blanc), puis en regroupant ces valeurs pour les transformer en octets.
- Désassembler le fichier ainsi obtenu afin d’analyser et comprendre le fonctionnement du programme.
Avant d’aller plus loin, il est nécessaire de savoir si l’image est en couleur, car nous n’avons besoin que de 2 valeurs : 0 pour le noir, 1 pour le blanc. ImageMagick peut trouver ces informations :
$ identify infiltrate.png
infiltrate.png PNG 300x350 300x350+0+0 8-bit sRGB 8629B 0.000u 0:00.001
Donc l’image est bien en couleur (sRGB). Il faudra donc la transformer en niveau de gris avant toute chose. Nous pouvons maintenant extraire le programme de l’image à l’aide de ce script python :
#!/usr/bin/env python3
from PIL import Image
import numpy as np
import sys
def extract_binary_from_image(image_path, output_path):
try:
# Charger l'image
image = Image.open(image_path)
# Convertir en niveaux de gris
grayscale_image = image.convert("L")
# Convertir les pixels en valeurs binaires (0 pour noir, 1 pour blanc)
binary_array = np.where(np.array(grayscale_image) > 128, 1, 0)
# Aplatir le tableau binaire et regrouper par octets (8 bits)
binary_flat = binary_array.flatten()
binary_strings = ["".join(map(str, binary_flat[i:i+8])) for i in range(0, len(binary_flat), 8)]
# Convertir les chaînes binaires en octets
binary_data = bytes([int(b, 2) for b in binary_strings if len(b) == 8])
# Sauvegarder dans un fichier de sortie
with open(output_path, "wb") as binary_file:
binary_file.write(binary_data)
print(f"Fichier extrait et sauvegardé dans : {output_path}")
except Exception as e:
print(f"Erreur lors de l'extraction : {e}")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Utilisation : python extractit.py <chemin_image> <chemin_fichier_sortie>")
else:
image_path = sys.argv[1]
output_path = sys.argv[2]
extract_binary_from_image(image_path, output_path)
chmod +x extractit.py
./extractit.py infiltrate.png infiltrate.bin
chmod +x infiltrate.bin
Une fois le fichier généré, si on l’éxécute, cela ne fonctionne pas. Utilisons hexedit pour étudier l’entête :
hexedit ./infiltrate.bin
00000000 04 51 81 91 55 50 89 D5 C0 E4 4D 3D 6A E7 1D ED 7F 45 4C 46 .Q..UP....M=j....ELF
Donc il y a 16 octets de trop au début du fichier, avant les “magic bytes”. Pour les éliminer :
dd if=infiltrate.bin of=infiltrate bs=1 skip=16
Vérifions :
readelf -h infiltrate
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x790
Start of program headers: 64 (bytes into file)
Start of section headers: 6792 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 29
Section header string table index: 28
On peut maintenant l’éxcuter :
./infiltrate
Hello! Entrez la clé
azerty
Mauvaise clé !
Maintenant, passons au désassemblage avec gdb :
gdb ./infiltrate
Une fois dans GDB, on liste les fonctions du programme :
>>> info functions
All defined functions:
Non-debugging symbols:
0x00000000000006e8 _init
0x0000000000000710 printf@plt
0x0000000000000720 puts@plt
0x0000000000000730 putchar@plt
0x0000000000000740 fgets@plt
0x0000000000000750 strlen@plt
0x0000000000000760 SHA1@plt
0x0000000000000770 __stack_chk_fail@plt
0x0000000000000780 __cxa_finalize@plt
0x0000000000000790 _start
0x00000000000007c0 deregister_tm_clones
0x0000000000000800 register_tm_clones
0x0000000000000850 __do_global_dtors_aux
0x0000000000000890 frame_dummy
0x000000000000089a doit
0x00000000000009f5 main
0x0000000000000a70 __libc_csu_init
0x0000000000000ae0 __libc_csu_fini
0x0000000000000ae4 _fini
On va désassembler main :
>>> disassemble main
Dump of assembler code for function main:
0x00000000000009f5 <+0>: push %rbp
0x00000000000009f6 <+1>: mov %rsp,%rbp
0x00000000000009f9 <+4>: sub $0x70,%rsp
0x00000000000009fd <+8>: mov %fs:0x28,%rax
0x0000000000000a06 <+17>: mov %rax,-0x8(%rbp)
0x0000000000000a0a <+21>: xor %eax,%eax
0x0000000000000a0c <+23>: lea 0x10e(%rip),%rdi # 0xb21
0x0000000000000a13 <+30>: call 0x720 <puts@plt>
0x0000000000000a18 <+35>: mov 0x2005f1(%rip),%rdx # 0x201010 <stdin@@GLIBC_2.2.5>
0x0000000000000a1f <+42>: lea -0x70(%rbp),%rax
0x0000000000000a23 <+46>: mov $0x7,%esi # taille chaîne user : 7 octets
0x0000000000000a28 <+51>: mov %rax,%rdi
0x0000000000000a2b <+54>: call 0x740 <fgets@plt> # fgets
0x0000000000000a30 <+59>: lea -0x70(%rbp),%rax
0x0000000000000a34 <+63>: mov %rax,%rdi
0x0000000000000a37 <+66>: call 0x89a <doit> # appel à doit
0x0000000000000a3c <+71>: mov $0xa,%edi
0x0000000000000a41 <+76>: call 0x730 <putchar@plt>
0x0000000000000a46 <+81>: mov $0x0,%eax
0x0000000000000a4b <+86>: mov -0x8(%rbp),%rcx
0x0000000000000a4f <+90>: xor %fs:0x28,%rcx
0x0000000000000a58 <+99>: je 0xa5f <main+106>
0x0000000000000a5a <+101>: call 0x770 <__stack_chk_fail@plt>
0x0000000000000a5f <+106>: leave
0x0000000000000a60 <+107>: ret
End of assembler dump.
Donc on attend une entrée de 6 caractères + un caractère pour la fin de la chaîne. Ensuite, il y a un appel à doit avec la chaîne récupérée. Regardons ce qui se passe dans la fonction doit:
>>> disassemble doit
Dump of assembler code for function doit:
0x000000000000089a <+0>: push %rbp
0x000000000000089b <+1>: mov %rsp,%rbp
0x000000000000089e <+4>: sub $0x40,%rsp
0x00000000000008a2 <+8>: mov %rdi,-0x38(%rbp)
0x00000000000008a6 <+12>: mov %fs:0x28,%rax
0x00000000000008af <+21>: mov %rax,-0x8(%rbp)
0x00000000000008b3 <+25>: xor %eax,%eax
0x00000000000008b5 <+27>: mov -0x38(%rbp),%rax
0x00000000000008b9 <+31>: mov %rax,%rdi
0x00000000000008bc <+34>: call 0x750 <strlen@plt>
0x00000000000008c1 <+39>: mov %rax,%rcx
0x00000000000008c4 <+42>: lea -0x20(%rbp),%rdx
0x00000000000008c8 <+46>: mov -0x38(%rbp),%rax
0x00000000000008cc <+50>: mov %rcx,%rsi
0x00000000000008cf <+53>: mov %rax,%rdi
0x00000000000008d2 <+56>: call 0x760 <SHA1@plt> # hachage SHA1 de chaîne user
0x00000000000008d7 <+61>: movl $0x0,-0x24(%rbp)
0x00000000000008de <+68>: movl $0x0,-0x28(%rbp)
0x00000000000008e5 <+75>: jmp 0x9a8 <doit+270>
0x00000000000008ea <+80>: movzbl -0x20(%rbp),%eax
0x00000000000008ee <+84>: cmp $0x58,%al # compare rbp-0x20 à 0x58
0x00000000000008f0 <+86>: jne 0x9a0 <doit+262>
0x00000000000008f6 <+92>: movzbl -0x1f(%rbp),%eax
0x00000000000008fa <+96>: cmp $0x23,%al # rbp-0x1f = 0x23 ?
0x00000000000008fc <+98>: jne 0x9a0 <doit+262>
0x0000000000000902 <+104>: movzbl -0x16(%rbp),%eax
0x0000000000000906 <+108>: cmp $0xa3,%al # rbp-0x16 = 0xa3 ?
0x0000000000000908 <+110>: jne 0x9a0 <doit+262>
0x000000000000090e <+116>: movzbl -0x1e(%rbp),%eax
0x0000000000000912 <+120>: cmp $0xdb,%al # rbp-0x1e = 0xdb ?
0x0000000000000914 <+122>: jne 0x9a0 <doit+262>
0x000000000000091a <+128>: movzbl -0x1d(%rbp),%eax
0x000000000000091e <+132>: cmp $0x97,%al # rbp-0x1d = 0x97 ?
0x0000000000000920 <+134>: jne 0x9a0 <doit+262>
0x0000000000000922 <+136>: movzbl -0x1a(%rbp),%eax
0x0000000000000926 <+140>: cmp $0xc4,%al # rbp-0x1a = 0xc4 ?
0x0000000000000928 <+142>: jne 0x9a0 <doit+262>
0x000000000000092a <+144>: movzbl -0x1c(%rbp),%eax
0x000000000000092e <+148>: cmp $0x68,%al # rbp-0x1c = 0x68 ?
0x0000000000000930 <+150>: jne 0x9a0 <doit+262>
0x0000000000000932 <+152>: movzbl -0x1b(%rbp),%eax
0x0000000000000936 <+156>: cmp $0x1,%al # rbp-0x1b = 0x1 ?
0x0000000000000938 <+158>: jne 0x9a0 <doit+262>
0x000000000000093a <+160>: movzbl -0xe(%rbp),%eax
0x000000000000093e <+164>: cmp $0x26,%al # rbp-0x0e = 0x26 ?
0x0000000000000940 <+166>: jne 0x9a0 <doit+262>
0x0000000000000942 <+168>: movzbl -0x19(%rbp),%eax
0x0000000000000946 <+172>: cmp $0xa0,%al # rbp-0x19 = 0xa0 ?
0x0000000000000948 <+174>: jne 0x9a0 <doit+262>
0x000000000000094a <+176>: movzbl -0x18(%rbp),%eax
0x000000000000094e <+180>: cmp $0xe2,%al # rbp-0x18 = 0xe2 ?
0x0000000000000950 <+182>: jne 0x9a0 <doit+262>
0x0000000000000952 <+184>: movzbl -0x17(%rbp),%eax
0x0000000000000956 <+188>: cmp $0xd7,%al # rbp-0x17 = 0xd7 ?
0x0000000000000958 <+190>: jne 0x9a0 <doit+262>
0x000000000000095a <+192>: movzbl -0xd(%rbp),%eax
0x000000000000095e <+196>: cmp $0x12,%al # rbp-0x0d = 0x12 ?
0x0000000000000960 <+198>: jne 0x9a0 <doit+262>
0x0000000000000962 <+200>: movzbl -0x15(%rbp),%eax
0x0000000000000966 <+204>: cmp $0x30,%al # rbp-0x15 = 0x30 ?
0x0000000000000968 <+206>: jne 0x9a0 <doit+262>
0x000000000000096a <+208>: movzbl -0x14(%rbp),%eax
0x000000000000096e <+212>: cmp $0xb2,%al # rbp-0x14 = 0xb2 ?
0x0000000000000970 <+214>: jne 0x9a0 <doit+262>
0x0000000000000972 <+216>: movzbl -0x13(%rbp),%eax
0x0000000000000976 <+220>: cmp $0xbb,%al # rbp-0x13 = 0xbb ?
0x0000000000000978 <+222>: jne 0x9a0 <doit+262>
0x000000000000097a <+224>: movzbl -0x11(%rbp),%eax
0x000000000000097e <+228>: cmp $0xfe,%al # rbp-0x11 = 0xfe ?
0x0000000000000980 <+230>: jne 0x9a0 <doit+262>
0x0000000000000982 <+232>: movzbl -0x10(%rbp),%eax
0x0000000000000986 <+236>: cmp $0x27,%al # rbp-0x10 = 0x27 ?
0x0000000000000988 <+238>: jne 0x9a0 <doit+262>
0x000000000000098a <+240>: movzbl -0x12(%rbp),%eax
0x000000000000098e <+244>: cmp $0x82,%al # rbp-0x12 = 0x82 ?
0x0000000000000990 <+246>: jne 0x9a0 <doit+262>
0x0000000000000992 <+248>: movzbl -0xf(%rbp),%eax
0x0000000000000996 <+252>: cmp $0xcc,%al # rbp-0x0f = 0xcc ?
0x0000000000000998 <+254>: jne 0x9a0 <doit+262>
0x000000000000099a <+256>: addl $0x1,-0x24(%rbp)
0x000000000000099e <+260>: jmp 0x9a4 <doit+266>
0x00000000000009a0 <+262>: subl $0x1,-0x24(%rbp)
0x00000000000009a4 <+266>: addl $0x1,-0x28(%rbp)
0x00000000000009a8 <+270>: cmpl $0x13,-0x28(%rbp)
0x00000000000009ac <+274>: jle 0x8ea <doit+80>
0x00000000000009b2 <+280>: cmpl $0x14,-0x24(%rbp)
0x00000000000009b6 <+284>: jne 0x9d2 <doit+312>
0x00000000000009b8 <+286>: mov -0x38(%rbp),%rax
0x00000000000009bc <+290>: mov %rax,%rsi
0x00000000000009bf <+293>: lea 0x12e(%rip),%rdi # 0xaf4
0x00000000000009c6 <+300>: mov $0x0,%eax
0x00000000000009cb <+305>: call 0x710 <printf@plt>
0x00000000000009d0 <+310>: jmp 0x9de <doit+324>
0x00000000000009d2 <+312>: lea 0x138(%rip),%rdi # 0xb11
0x00000000000009d9 <+319>: call 0x720 <puts@plt>
0x00000000000009de <+324>: nop
0x00000000000009df <+325>: mov -0x8(%rbp),%rax
0x00000000000009e3 <+329>: xor %fs:0x28,%rax
0x00000000000009ec <+338>: je 0x9f3 <doit+345>
0x00000000000009ee <+340>: call 0x770 <__stack_chk_fail@plt>
0x00000000000009f3 <+345>: leave
0x00000000000009f4 <+346>: ret
End of assembler dump.
La chaîne saisie par l’utilisateur est utilisée pour générer un hachage SHA1. Ensuite, la fonction compare certains octets du hachage avec d’autres, à des positions spécifiques qui ne suivent pas un ordre séquentiel. Voici un tableau qui réorganise ces octets dans le bon ordre :
Offset depuis RBP | Valeur |
---|---|
-0x20 | 0x58 |
-0x1f | 0x23 |
-0x1e | 0xdb |
-0x1d | 0x97 |
-0x1c | 0x68 |
-0x1b | 0x01 |
-0x1a | 0xc4 |
-0x19 | 0xa0 |
-0x18 | 0xe2 |
-0x17 | 0xd7 |
-0x16 | 0xa3 |
-0x15 | 0x30 |
-0x14 | 0xb2 |
-0x13 | 0xbb |
-0x12 | 0x82 |
-0x11 | 0xfe |
-0x10 | 0x27 |
-0x0f | 0xcc |
-0x0e | 0x26 |
-0x0d | 0x12 |
Ainsi, la chaîne de hachage SHA1 qui déclenchera l’affichage du flag est :
5823db976801c4a0e2d7a330b2bb82fe27cc2612
Si on tente d’inverser ce hash SHA1 à l’aide d’un outil en ligne, par exemple via M5Decrypt, on obtient :
5823db976801c4a0e2d7a330b2bb82fe27cc2612 : 401445
Testons :
./infiltrate
Hello! Entrez la clé
401445
Bravo, le flag est FCSC{401445}