Solution de botanicmagic pour Infiltrate

reverse

20 janvier 2025

Outils utilisés pour ce challenge : imageMagic, gdb, hexedit, readelf, M5Decrypt

Voici les étapes pour résoudre ce challenge :

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}