Catégories
P'Hack CTF Reverse Write-ups

P’Hack CTF – Reverse Engineering

Dans cet article nous verrons la résolution des 3 challenges de reverse engineering du P’Hack CTF que j’ai tous first blood avec mon équipe Arn’Hack. Ce sont 3 challenges très simples, le CTF étant orienté débutant, mais le dernier va pouvoir nous permettre d’introduire un outil très sympa : angr (et on repassera sur z3 que l’on avait déjà vu ici) !

Fichiers des challenges

Tendu comme un slip

Comme d’habitude on commence par faire un file sur le binaire pour prendre quelques infos.

$ file tendu
tendu: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b13fe8dbd7e73b47d33fde7a56874e1180c8dab1, for GNU/Linux 3.2.0, not stripped

On a donc un ELF 64 bits non strippé (le contraire aurait été étonnant). On peut ensuite l’exécuter pour étudier son fonctionnement.

$ ./tendu MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMMMMMMMMMmyo+oooydMMMMMMMMMMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMMMMMNd//+ydNmmdhs+/+hMMMMMMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMMMm+-yNMMMMMMMMMMMMNs:oNMMMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMN+-yMMMMMMMMMMMMMMMMMMh:+NMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMy.oMMMMMMMMMMMMMMMMMMMMMMd/oMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMM+.mMMMMMMMMMMMMMMMMMMMMMMMMMd:dMMMMMMMMMMMMMMM
 MMMMMMMMMMMMN:/NMMMMMMMMMMMMMMMMMMMMMMMMMMMm-sMMMMMMMMMMMMMM
 MMMMMMMMMMMN-/MMMMMMMosMMMNMMNmMMMNyhMsymohMN:+MMMMMMMMMMMMM
 MMMMMMMMMMM:/MMMm/-/h`.:No.No` ym: :sy   /dMMMooMMMMMMMMMMMM
 MMMMMMMMMM+/NMMm` . oM/ /. o `  . oyh- /---mMMM++MMMMMMMMMMM
 MMMMMMMMMy`NMMMm :dMMM..h++++my+NhysydhMMMMMNMMM/yMMMMMMMMMM
 MMMMMMMMN`yMMmNMhNMMMMMMMdddhso+oo/:oMMMMMs` sMMM/mMMMMMMMMM
 MMMMMMMM+.NMy  /mMMMMMMs.`/o/ /y` +sMMMMMo   `MMMN/NMMMMMMMM
 MMMMMMMN`hMMo   `hMMMMh /hdM+`Nh /ydMMMMm     dMMMmyMMMMMMMM
 MMMMMMMs`MMMs    `dMMMMho+oNmoNMdMMMMMMM+     dMMMModMMMMMMM
 MMMMMMM:/MMMm     .NMMNyhNMMMMMMMMh+:/dM.    `NMMMMN/MMMMMMM
 MMMMMMM.sMMMM/     /Md`   -sdNNh+`     y     oMMMMMM/MMMMMMM
 MMMMMMM`dMMMMN-     s`                      /MMMMMMMhMMMMMMM
 MMMMMMN hMMMMMN/           `.--::::-..`    +MMMMMMMM+MMMMMMM
 MMMMMMM.oMMMMMMMy`    `.------..```````   ./+sdMMMMM+MMMMMMM
 MMMMMMMo`NMMMNs///+.               .     .MMMMMMMMMhhMMMMMMM
 MMMMMMMN`sMMMMMMMMM/     +         :`    .MMMMMMMMN/MMMMMMMM
 MMMMMMMMh hMMMMMMMMy          :.         /MMMMMMMN/NMMMMMMMM
 MMMMMMMMMy`yMMMMMMMM/         `         /NMMMMMMN/mMMMMMMMMM
 MMMMMMMMMMh`sMMMMMMMMd+-``  `./+---:/oyNMMMMMMMy+NMMMMMMMMMM
 MMMMMMMMMMMNo-yMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMm+yMMMMMMMMMMMM
 MMMMMMMMMMMMMm+.oNMMMMMMMMMMMMMMMMMMMMMMMMNs/sMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMNs:-ohMMMMMMMMMMMMMMMMMMms//yMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMMMMho:-/sdddmmmNmdds///odMMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMMMMMMMMmhyso++++osydmMMMMMMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM
 MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM 

Un dessins d’oeuf de Pâques apparait en ASCII art. Après quelques essais, le passage d’argument ne change rien au comportement du binaire. On peut alors faire un strings sur le binaire pour afficher toutes les chaines de caractères qu’il contient.

$ strings tendu
/lib64/ld-linux-x86-64.so.2
libc.so.6
fflush
puts
stdout
[...]
PHACK{s1mplY_5tr1ng5_i7}
:*3$"
GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
crtstuff.c
[...]
.init_array
.fini_array
.dynamic
.data
.bss
.comment

Ok bon nous avons donc visiblement notre flag en clair dans le binaire : PHACK{s1mplY_5tr1ng5_i7}. Si on veut comprendre ce que fait cette chaine ici, on peut ouvrir le binaire dans un désassembleur.

Code désassemblé de la fonction “main”

On voit ici que la chaine dessinant l’oeuf de Pâques mise dans [RBP-0x10] alors que notre flag est mis dans [RBP-0x8]. Or, si l’on regarde les calling conventions on remarque qu’ici la fonction “puts” prend un seul argument se trouvant dans RDI : c’est ici notre chaine d’ASCII Art. La chaine du flag est donc une variable locale qui n’est jamais utilisée dans le programme – ce qui a d’ailleurs sans doute causé un warning à la compilation.

No strings

On commence encore une fois par faire un file sur le binaire.

$ file no-string
no-string: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ff2a45b28a26851484403821cacc8abf19949c9b, for GNU/Linux 4.4.0, not stripped

Encore une fois on se retrouve avec un ELF x86_64 non strippé. On peut alors l’exécuter pour observer son comportement.

$ ./no-string
> superflag
🙅 

On peut donc penser qu’on aura l’émoji 🙅 tant que l’on entre un flag qui n’est pas le bon. Regardons donc se qui se passe dans le binaire en ouvrant le challenge dans un désassembleur.

Code désassemblé de la fonction “main”

Surprise ! Quoiqu’on rentre ici on voit que le programme va récupérer les 0x32 = 5O premiers caractères, les stocker dans un buffer puis afficher l’émoji sans faire la moindre vérification sur notre input 😢.

On se balade alors dans le binaire et on repère une fonction intéressant nommée “display_flag” qui déchiffre puis affiche le flag. A partir de là, il y a deux moyens de solve ce challenge 😁 !

Méthode 1 – Reverse de la fonction

La première méthode à laquelle on pense (et à laquelle j’ai également pensé) consiste à reverse la fonction en question et établir un script qui affichera le script pour nous. C’était sans doute la manière attendue ici, la fonction étant très simple.

Code désassemblé de la fonction “display_flag”

On voit ici que globalement la fonction va prendre une chaine de caractères et va aller piocher dans cette chaine les caractères composant le flag. On peut utiliser un decompiler pour avoir un aperçu rapide de la fonction.

unsigned __int64 display_flag(){   
   char src; // [rsp+2h] [rbp-6Eh]
   char v2; // [rsp+3h] [rbp-6Dh]
   char v3; // [rsp+4h] [rbp-6Ch]
   char v4; // [rsp+5h] [rbp-6Bh]
   // [...]
   char v29; // [rsp+1Eh] [rbp-52h]
   char v30; // [rsp+1Fh] [rbp-51h]
   const char *v31; // [rsp+20h] [rbp-50h]
   const char *v32; // [rsp+28h] [rbp-48h]
   char dest[8]; // [rsp+30h] [rbp-40h]
   __int64 v34; // [rsp+48h] [rbp-28h]
   __int64 v35; // [rsp+50h] [rbp-20h]
   __int64 v36; // [rsp+58h] [rbp-18h]
   char v37; // [rsp+60h] [rbp-10h]
   unsigned __int64 v38; // [rsp+68h] [rbp-8h]

   v38 = __readfsqword(0x28u);
   v31 = "i_t33Am4_rdRfii";
   v32 = "Ce7d_K_hH0{}nP5";
   strcpy(dest, "Well done! The is ");
   v34 = 0LL;
   v35 = 0LL;
   v36 = 0LL;
   v37 = 0;
   src = aCe7dKHh0Np5[13];
   strncat(dest, &src, 1uLL);
   v2 = v32[8];
   strncat(dest, &v2, 1uLL);
   v3 = v31[5];
   strncat(dest, &v3, 1uLL);
   v4 = *v32;
   strncat(dest, &v4, 1uLL);
   v5 = v32[5];
   strncat(dest, &v5, 1uLL);
   v6 = v32[10];
   strncat(dest, &v6, 1uLL);
   v7 = v31[9];
   strncat(dest, &v7, 1uLL);
   v8 = v31[3];
   strncat(dest, &v8, 1uLL);
   v9 = v31[7];
   strncat(dest, &v9, 1uLL);
   v10 = v31[10];
   strncat(dest, &v10, 1uLL);
   v11 = v31[1];
   strncat(dest, &v11, 1uLL);
   v12 = *v31;
   strncat(dest, &v12, 1uLL);
   v13 = v31[2];
   strncat(dest, &v13, 1uLL);
   v14 = v31[1];
   strncat(dest, &v14, 1uLL);
   v15 = v31[12];
   strncat(dest, &v15, 1uLL);
   v16 = v31[11];
   strncat(dest, &v16, 1uLL);
   v17 = v32[9];
   strncat(dest, &v17, 1uLL);
   v18 = v31[6];
   strncat(dest, &v18, 1uLL);
   v19 = v31[1];
   strncat(dest, &v19, 1uLL);
   v20 = v31[2];
   strncat(dest, &v20, 1uLL);
   v21 = v32[7];
   strncat(dest, &v21, 1uLL);
   v22 = v31[3];
   strncat(dest, &v22, 1uLL);
   v23 = v31[1];
   strncat(dest, &v23, 1uLL);
   v24 = *v31;
   strncat(dest, &v24, 1uLL);
   v25 = v32[12];
   strncat(dest, &v25, 1uLL);
   v26 = v32[14];
   strncat(dest, &v26, 1uLL);
   v27 = *v31;
   strncat(dest, &v27, 1uLL);
   v28 = v31[10];
   strncat(dest, &v28, 1uLL);
   v29 = v32[1];
   strncat(dest, &v29, 1uLL);
   v30 = v32[11];
   strncat(dest, &v30, 1uLL);
   puts(dest);
   return v38 - __readfsqword(0x28u);
 }

On peut alors facilement faire un script python qui nous affiche le flag.

v31 = "i_t33Am4_rdRfii"
v32 = "Ce7d_K_hH0{}nP5"
flag = ""

flag += v32[13]
flag += v32[8]
flag += v31[5]
flag += v32[0]
flag += v32[5]
flag += v32[10]
flag += v31[9]
flag += v31[3]
flag += v31[7]
flag += v31[10]
flag += v31[1]
flag += v31[0]
flag += v31[2]
flag += v31[1]
flag += v31[12]
flag += v31[11]
flag += v32[9]
flag += v31[6]
flag += v31[1]
flag += v31[2]
flag += v32[7]
flag += v31[3]
flag += v31[1]
flag += v31[0]
flag += v32[12]
flag += v32[14]
flag += v31[0]
flag += v31[10]
flag += v32[1]
flag += v32[11]

print(f"[+] FLAG : {flag}")

On l’exécute et on récupère le flag.

$ python solve.py 
[+] FLAG : PHACK{r34d_it_fR0m_th3_in5ide}

Méthode 2 – Patcher le binaire pour appeler la fonction

La méthode de flemmard (enfin c’est relatif) consiste à patcher le binaire pour qu’au lieu de par exemple appeler la fonction “puts”, il appelle la fonction “display_flag”.

Fonction main avant le patch

En effet on remarque que la fonction “_puts” se trouve avant la fonction “display_flag”.

Adresses des fonctions

On peut donc calculer l’offset à ajouter à l’instruction call pour appeler “display_flag” au lieu de “_puts”.

En effet l’instruction call a pour opcode 0xE8 et la valeur qui suit, sur 4 octets, est un offset signé vers la fonction à call. Ici on a donc “call 0xfffff9c5” (soit call -0x63b) et si on calcule 0x166b (adresse de l’instruction après le call) – 0x63b on retrouve bien 0x1030 soit l’adresse de “_puts”.

On peut donc calculer le nouvel offset en utilisant python.

>>> adr_puts = 0x1030
>>> adr_display_flag = 0x1179
>>> offset = adr_display_flag - adr_puts
>>> hex(0xfffff9c5 + offset)
'0xfffffb0e'         

On doit donc patcher le 0xfffff9c5 en 0xfffffb0e.

Fonction main patchée

Et boum ! On a bien notre fonction patchée qui appelle “display_flag” ! Il n’y a plus qu’à exécuter 😁.

$ ./no-strings-patched 
> Can i get the flag please ?
Well done! The is PHACK{r34d_it_fR0m_th3_in5ide}

Military Grade Password

Encore une fois on commence par faire un file sur le fichier pour savoir à quoi on a affaire.

$ file login
login: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=36b9f99ec2f6a553cb73b4dc42017ade6768cea7, for GNU/Linux 4.4.0, stripped

On a encore une fois un ELF x86_64 mais cette fois-ci strippé. On peut ensuite le lancer pour observer son comportement.

$ ./login
55th Infantry Division identify checker
Personal access ID: sup3rl0g1n
[+] ACCES REJECTED!

On a donc visiblement un input qui nous demander un ID et qui nous affiche le message “[+] ACCES REJECTED!” si on entre un mauvais ID. Voyons ce qu’il se passe en désassemblant le programme.

Code désassemblé de la fonction main

On voit donc ici que notre input sera passé en paramètre à une fonction appelée ici “sub_1530” qui se chargera de déterminer si l’input est correct ou non. Si l’input est correct avec le message “[+] ACCESS GRANTED!” sera affiché. Intéressons-nous donc à cette fonction.

Control Flow Graph de la fonction de vérification

On remarque alors une très grande fonction qui va vérifier que l’input remplit de multiples conditions. On peut alors utiliser de decompiler pour retrouver un pseudo code proche du C.

__int64 __fastcall sub_1530(char *a1)
 {
   int v1; // esi
   int v2; // er9
   int v3; // er8
   int v4; // ecx
   int v5; // edx
   _BOOL4 v6; // er10
   int v8; // er12
   int v9; // er14
   int v10; // er15
   int v11; // ebp
   int v12; // [rsp+0h] [rbp-5Ch]
   int v13; // [rsp+8h] [rbp-54h]
   int v14; // [rsp+Ch] [rbp-50h]
   int v15; // [rsp+10h] [rbp-4Ch]
   int v16; // [rsp+14h] [rbp-48h]
   int v17; // [rsp+18h] [rbp-44h]
   char v18; // [rsp+1Ch] [rbp-40h]
   int v19; // [rsp+20h] [rbp-3Ch]
   int v20; // [rsp+24h] [rbp-38h]
   int v21; // [rsp+28h] [rbp-34h]
 v1 = a1[6];
   v2 = a1[14];
   v3 = a1[12];
   v4 = a1[10];
   v5 = a1[13];
   v6 = 0;
   if ( v2 * v1 * (v5 ^ (v3 - v4)) == 16335 )
   {
     v8 = a1[7];
     v9 = a1[18];
     v12 = a1[15];
     v18 = a1[1];
     if ( ((char)(a1[18] ^ v18) ^ (v12 - v8)) == 83 )
     {
       v14 = a1[17];
       v13 = a1[16];
       v15 = a1[5];
       v16 = a1[9];
       v17 = *a1;
       if ( (v14 - v13) * (v17 ^ (v16 + v15)) == -5902 )
       {
         v19 = a1[3];
         v10 = a1[11];
         if ( v19 - v10 == 11 )
         {
           v11 = a1[4];
           v21 = a1[2];
           v20 = a1[8];
           if ( (v20 ^ (v11 + v21)) == 3
             && v20 + v12 - v11 == 176
             && (v1 ^ ((char)(v4 ^ a1[9]) - v9 - v10)) == -199
             && v21 * v13 + v18 * (char)(*a1 ^ a1[17]) == 9985
             && v5 * v2 - v8 == 2083
             && v3 + v19 - v15 == 110
             && v5 + v16 + v4 * v20 == 5630
             && v15 - v13 - v17 - v21 == -182
             && v14 * (char)(v2 ^ a1[7]) == 7200
             && v18 * v19 + v10 * v1 == 17872
             && v3 - v12 - v11 * v9 == -5408
             && v19 * v12 + v21 * v10 == 18888
             && v13 * (v15 + v5) == 15049
             && v14 * (v4 + v17) == 12150
             && (char)(v2 ^ v1) * v9 == 10080
             && v8 + v3 - v11 == 132 )
           {
             v6 = v16 * v18 + v20 == 2453;
           }
         }
       }
     }
   }
   return (unsigned int)v6;
 }

Résoudre ce challenge revient donc à résoudre un gros système d’équations. A partir de là, encore une fois, deux méthodes distinctes sont possibles pour résoudre ce challenge 😁.

Méthode 1 – Résoudre les équations avec z3

La méthode à laquelle on peut penser intuitivement est la suivante : résoudre ce système d’équations.

Pour cela, on peut pas exemple utiliser z3, une libraire dont je vous avais déjà parlé dans cet article, à l’occasion d’un challenge du CTF inter IUT. On peut alors recopier les conditions à remplir à partir du pseudo-code et remplacer les variables locales du pseudo-code par les valeurs du flag/input directement.

from z3 import *

a = [BitVec(f"flag[{i}]", 9) for i in range(19)]
s = Solver()

s.add( a[14] * a[6] * (a[13] ^ (a[12] - a[10])) == 16335 )
s.add( ((a[18] ^ a[1]) ^ (a[15] - a[7])) == 83 )
s.add( (a[17] - a[16]) * (a[0] ^ (a[9] + a[5])) == 0xE8F2 )
s.add( a[3] - a[11] == 11 )
s.add( (a[8] ^ (a[4] + a[2])) == 3 )
s.add( a[8] + a[15] - a[4] == 176 )
s.add( (a[6] ^ ((a[10] ^ a[9]) - a[18] - a[11])) == 0xFF39 )
s.add( a[2] * a[16] + a[1] * (a[0] ^ a[17]) == 9985 )
s.add( a[13] * a[14] - a[7] == 2083 )
s.add( a[12] + a[3] - a[5] == 110 )
s.add( a[13] + a[9] + a[10] * a[8] == 5630 )
s.add( a[5] - a[16] - a[0] - a[2] == 0xFF4A )
s.add( a[17] * (a[14] ^ a[7]) == 7200 )
s.add( a[1] * a[3] + a[11] * a[6] == 17872 )
s.add( a[12] - a[15] - a[4] * a[18] == 0xEAE0 )
s.add( a[3] * a[15] + a[2] * a[11] == 18888 )
s.add( a[16] * (a[5] + a[13]) == 15049 )
s.add( a[17] * (a[10] + a[0]) == 12150 )
s.add( (a[14] ^ a[6]) * a[18] == 10080 )
s.add( a[7] + a[12] - a[4] == 132 )
s.add( a[9] * a[1] + a[8] == 2453 )

print(s.check())
if(str(s.check()) == "sat"):
    print("[+] SOLUTION : ")
    print(s.model())

J’ai ici utilisé un Bitvec sur 9 bits car le fait de manipuler des valeurs négatives cause un integer overflow. J’aurais également pu utiliser ZeroExt et SignExt pour ne pas avoir de soucis avec les integers signés.

$ python military_grade_password.py 
 sat
 [+] SOLUTION : 
 [flag[16] = 101,
  flag[3] = 111,
  flag[1] = 52,
  flag[15] = 108,
  flag[8] = 113,
  flag[7] = 77,
  flag[17] = 75,
  flag[18] = 120,
  flag[11] = 100,
  flag[4] = 45,
  flag[9] = 45,
  flag[2] = 69,
  flag[5] = 101,
  flag[6] = 121,
  flag[12] = 100,
  flag[14] = 45,
  flag[0] = 113,
  flag[13] = 48,
  flag[10] = 49]

On peut ensuite copier coller le résultat dans un nouveau script pour afficher le flag.

flag[16] = 101
flag[3] = 111
flag[1] = 52
flag[15] = 108
flag[8] = 113
flag[7] = 77
flag[17] = 75
flag[18] = 120
flag[11] = 100
flag[4] = 45
flag[9] = 45
flag[2] = 69
flag[5] = 101
flag[6] = 121
flag[12] = 100
flag[14] = 45
flag[0] = 113
flag[13] = 48
flag[10] = 49

FLAG = ''.join(chr(i) for i in flag)
print("[+] FLAG : PHACK{" + FLAG + "}")
$ python solve.py
[+] FLAG : PHACK{q4Eo-eyMq-1dd0-leKx}

Et hop on récupère le flag ! Et on notera au passage que n’importe quel serial commençant par “q4Eo-eyMq-1dd0-leKx” valide le challenge.

$ ./login
55th Infantry Division identify checker
Personal access ID: q4Eo-eyMq-1dd0-leKx-SUPEREASYCHALLENGE
[+] ACCESS GRANTED!

Méthode 2 – angr goes brrrrr

Ce challenge est particulièrement facile à résoudre avec un outil comme angr qui va combiner l’analyse statique et l’analyse dynamique symbolique (ou “concolique”), dans notre cas pour trouver l’input qui affichera le message de réussite du challenge.

Pour cela, on commence par récupérer des adresses significatives d’un input valide ou non.

Repérage d’adresses caractéristiques d’une victoire ou non

On retient donc l’adresse 0x10BF pour la victoire et l’adresse 0x10E3 pour la défaite. On peut maintenant écrire un rapide script python avec angr pour résoudre automatiquement ce challenge.

import angr

def main():
    # On ajoute 0x400000 car le binaire sera mappé avec une base address de 0x400000
    win_adress = 0x10BF + 0x400000
    fail_adress = 0x10E3 + 0x400000

    p = angr.Project('./login')
    simgr = p.factory.simulation_manager(p.factory.full_init_state())
    simgr.explore(find=win_adress, avoid=fail_adress)

    print(simgr.found[0].posix.dumps(0))

if name == 'main':
    main()

On l’exécute et après un peu d’attente le flag apparait.

$ python login_angr_addresses.py
WARNING | 2021-04-08 21:26:55,921 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
b'q4Eo-eyMq-1dd0-leKx\x1a\x00L$\x17\xc21Y\x92rl\x18@\x08\x88\xc0\xa0\x89\x00a@\x08$\x01\xa2\x02\x0c\x88\xa0\xe0\x0c\x10\x90.\xa10\x08%5'

BONUS – angr v2 et one liner

Petit bonus suite à u̶n̶ ̶c̶o̶n̶c̶o̶u̶r̶s̶ ̶d̶’̶é̶g̶o une discussion avec d’autres participants au CTF, on peut remarque qu’il est possible de résoudre ce challenge avec un “one liner” (soit un script complet en une ligne). Pour cela je vais encore utiliser angr avec le script suivant.

import angr

proj = angr.Project('login')
simgr = proj.factory.simgr()
simgr.explore(find=lambda s: b"[+] ACCESS GRANTED!" in s.posix.dumps(1))

print(simgr.found[0].posix.dumps(0))

L’idée va maintenant être de rétrécir au maximum de programme en enlevant les variables intermédiaires. On peut également intégrer l’import d’angr avec la fonction __import__ builtin avec Python (dédicace à Clément avec qui j’ai pu établir ce one liner après une collaboration totalement incroyable).

$ python3 -c "print(__import__('angr').Project('login').factory.simgr().explore(find=lambda s: b'[+] ACCESS GRANTED' in s.posix.dumps(1)).found[0].posix.dumps(0))"
 WARNING | 2021-04-08 22:32:22,433 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
 b'q4Eo-eyMq-1dd0-leKx\xa0\x18\x00\x0e\x010(\x12\x12\xb0\x02\x08\x13"`@\x01\x04*\x04\'><Q\x89M\x80j\x00\x0e\xaa6\x14&\x01E\xc0\x911\x8a\x18'