Du vendredi 4 Juin à 20h jusqu’au samedi 1h du matin, j’ai remporté avec mon équipe Arn’Hack le S.H.I.E.L.D.S. CTF, organisé par une association étudiante venant du Pôle Sup’ La Salle.
Durant ce CTF très sympathique j’ai pu entre autre flag tous les challenges de reverse (sauf 1 que j’ai flag 8 minutes après la fin du CTF…) et je me suis dis que ça serait sympa de les repasser en revue !
Cela me permettra également de faire une petite introduction au reverse Android ainsi qu’au module python claripy, utilisé ici avec angr.
Fichiers des challenges
TryHackTheEmpire – 1 solve, first et unique blood
(==> Sur un serveur distant car trop gros pour être uploadé ici)
Un nouveau Reverse – 100 pts
Ce challenge est très simple mais nous permettra d’introduire l’utilisation d’argc et argv pour ceux qui débutent vraiment en reverse 😉.
Comme d’habitude, on commence par faire un file pour récupérer quelques informations sur le binaire.
$ file unNouveauReverse
unNouveauReverse: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=81155e4dc8dce8387b894dedbce4ff4417e5075f, for GNU/Linux 3.2.0, not stripped
On a donc affaire à un ELF x86_64 non strippé. On peut alors ouvrir le binaire dans notre désassembleur favori (ce sera IDA64 pour moi) et commencer le reverse engineering de la fonction main. Le flowchart nous donne directement une idée de l’algorithme.
En général c’est bon signe, ca ressemble très fortement à de la comparaison caractère par caractère. Regardons le code assembleur de la fonction.
Si on suit la calling convention Linux en x86_64, on voit que le premier argument d’une fonction est passée via RDI, le 2ème par RSI, 3ème par RDX etc (cf x86_calling_conventions). On aura donc argc (le nombre d’arguments + 1, correspondant donc également à la taille d’argv) dans [rbp-0x14] et argv (Un tableau de chaines de caractères contenant les valeurs des arguments, argv[0] étant le path du binaire) dans [rbp-0x20].
On voit donc le premier bloc qui compare argc à 3. Si la comparaison retourne 0 (false) alors le programme quitte : il faut donc passer 2 arguments au programme.
Le deuxième bloc va ensuite prendre argv, le mettre dans RAX et ajouter 8 pour passer de argv[0] à argv[1] (argv contenant des pointeurs et un pointeur étant codé sur 8 octets en x86_64).
argv[1] sera ensuite passé à la fonction atoi pour convertir sa valeur en entier (ex: “12” sera alors convertie en 12). Cette valeur sera alors comparée à 0x49 = 73 : le premier argument devra donc être 73 🙂.
Le bloc suivant de la fonction main va récupérer argv[0] (toujours dans [rbp-0x20]) puis ajouter 0x10 == 16 opur passer à argv[2] et donc récupérer le 2ème argument qui sera alors passer dans la fonction strlen. Le résultat sera comparé à 0x20 == 32 et si ce n’est pas égal, le programme quitte – notre flag passé en 2ème argument devra donc avoir une longueur de 32 caractères.
Le bloc suivant vérifie via la fonction strncmp que les 8 premiers caractères du flag sont “SHIELDS{” et le bloc d’après que le flag se termine par “}”.
Ensuite, comme nous l’avions deviné plus tard, le programme va itérer sur tout le flag dans l’ordre et comparer la valeur de chaque caractère avec une valeur hardcodée. On voit sur cette image que le flag commence par “SHIELDS{baby_”. On récupère alors toutes ces valeurs et on arrive au flag “SHIELDS{baby_yoda_likes_reverse}”.
On peut alors le tester via le programme sans oublier de passer 73 comme premier argument !
$ ./chall1 73 SHIELDS{baby_yoda_likes_reverse}
You got the flag: SHIELDS{baby_yoda_likes_reverse}
Le reverse contre attaque – 196 pts
Encore une fois, on commence par faire un file sur le binaire pour récupérer des informations basiques sur le binaire.
$ file leReverseContreAttaque
leReverseContreAttaque: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2b8cc361a05dcc339304299e3c8c599bc8c7e7f9, for GNU/Linux 3.2.0, stripped
On a encore une fois un ELF x86_64, strippé pour cette fois. On peut alors ouvrir le binaire dans un désassembleur (encore IDA64 ici) et commencer le reverse engineering de la fonction main.
Je ne vais pas re-détailler la récupération d’argc et argv mais pour résumer argc doit valoir 2, c’est-à-dire que le programme doit être lancé avec un argument, et argv[1] qui sera notre flag vérifié sera passé en argument à une fonction que j’ai ici appelée “check_flag”.
Si le résultat de cette fonction est 0 alors nous avons le bon flag et le message “You got the flag: SHIELDS{%s}” où le format %s est remplacé par notre input. Sinon, la fonction quitte avec -2 en code d’erreur. Le code intéressant se trouve donc dans la fonction “check_flag”.
Par flemme j’ai ici “décompilé” le code pour essayer d’avoir un pseudo-code compréhensible. Après renommage des variables pour y voir un peu plus clair je suis arrivé à ce résultat.
_BOOL8 __usercall check_flag@(__int64 a1@, int *INPUT@)
{
int v2; // ST1C_4
int v4; // [rsp-1Ch] [rbp-1Ch]
int i; // [rsp-18h] [rbp-18h]
__asm { endbr64 }
v4 = 0;
char_INPUT = (__int64)&ptr1_INPUT;
ptr1_INPUT = *INPUT;
memcpy(©_INPUT, INPUT, 19uLL);
ptr2_INPUT = *INPUT;
RETURN = 0;
for ( i = 0; i < 120; ++i )
{
if ( BUFFER_CHELOU[v4] )
{
if ( BUFFER_CHELOU[v4] == 1 )
{
--*(_BYTE *)char_INPUT;
++v4;
}
else if ( BUFFER_CHELOU[v4] == 2 )
{
if ( *(char *)char_INPUT != (unsigned __int8)MAYBE_ENCRYPTED[(signed int)(char_INPUT
- (unsigned __int64)&ptr1_INPUT)] )
RETURN = 1;
++v4;
}
}
else
{
v2 = v4 + 1;
char_INPUT = (__int64)&ptr1_INPUT + BUFFER_CHELOU[v2];
v4 = v2 + 1;
}
}
return RETURN != 0;
}
Ce que j’ai renommé “BUFFER_CHELOU” et “MAYBE_ENCRYPTED” valaient ceci dans la section .data du binaire :
Après m’être attardé un petit peu sur le code décompilé de la fonction je remarque qu’IDA a clairement oublié quelques choses car par exemple ptr2_INPUT n’est jamais utilisé, ptr1_INPUT n’est jamais incrémenté alors que dans la logique le programme devrait itérer sur tout le flag et pareil pour char_INPUT dont sa valeur est décrémentée dans une branche du programme mais qui ne passe jamais au caractère suivant.
Ayant la flemme de reverse l’assembleur et de m’attarder plus que ca sur le challenge, je décidé de sortir l’artillerie lourde et d’aller faire des choses pas très catholiques au binaire avec angr 😋.
Pour limiter au maximum le path explosion (le fait qu’angr explore beaucoup trop de chemins possibles), on va devoir trouver une adresse atteinte le plus tôt possible synonyme de l’échec de la vérification du flag ainsi qu’une synonyme d’une réussite de la vérification du flag.
Dans le code de la fonction “check_flag”, nous avons vu que la variable que j’ai appelée RETURN se met à 1 dès que la comparaison avec une valeur du MAYBE_ENCRYPTED retourne false.
else if ( BUFFER_CHELOU[v4] == 2 )
{
if ( *(char *)char_INPUT != (unsigned __int8)MAYBE_ENCRYPTED[(signed int)(char_INPUT
- (unsigned __int64)&ptr1_INPUT)] )
RETURN = 1;
++v4;
}
Cette valeur ne pouvant pas retourner à la valeur 0, c’est donc dès ce moment qu’on saura si notre flag n’est pas valid. On récupère son adresse.
On va ensuite chercher une instruction décidant d’un bon flag. La première instruction indiquant un bon flag se trouve à la fin de la fonction “check_flag” lorsque la variable RETURN sera comparée à 0 via l’instruction “test eax, eax”. Suite à cette comparaison, si RETURN vaut bien zéro alors la fonction retourne 0 (c’est le bon flag) et sinon elle retourne 1 (ce n’est pas le bon flag).
On récupère alors l’adresse du mov eax, 0.
On va alors construire un programme qui va chercher à atteindre l’adresse 0x12C8 et à éviter l’adresse 0x129F.
import angr
import claripy
# Ajout de 0x400000, adresse à laquelle angr
# map les binaires par défaut
win = 0x12C8 + 0x400000
fail = 0x129F + 0x400000
proj = angr.Project('./leReverseContreAttaque')
# Création d'un vecteur de 32 octets (taille inconnue)
# On prévoit large --> ce sera au pire paddé avec des 0
arg = claripy.BVS('arg', 8*0x20)
# Notre vecteur sera passé en argument au programme
state = proj.factory.entry_state(args=['./chall2', arg])
simgr = proj.factory.simulation_manager(state)
simgr.explore(find=win, avoid=fail)
if len(simgr.found) > 0:
s = simgr.found[0]
print(f"argv[1] = {s.solver.eval(arg, cast_to=bytes)}")
On lance alors notre programme et on récupère le flag après quelques minutes pour ma part (ma VM Linux étant un fait un Docker, pas très performant…).
$ python angry_v2.py
WARNING | 2021-06-05 22:19:36,396 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.
argv[1] = b'_starwars_vm_rocks_\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
On peut alors essayer de donner cet input au programme !
$ ./chall2 starwars_vm_rocks
You got the flag: SHIELDS{_starwars_vm_rocks_}
TryHackTheEmpire – 200 pts
Dans ce challenge on nous donne un APK et on nous indique que notre mision est de découvrir le mot de passe de l’utilisateur “lorddarthvador” !
On commence alors par décompiler l’APK : je le ferais ici avec un outil appelé jadx que vous trouverez sur github : https://github.com/skylot/jadx. On peut alors regarder la MainActivity.
Le code décompilé donne alors ceci :
package com.notdarthvador.tryhacktheempire;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
public static final String TAG = "TryHackTheEmpire";
/* access modifiers changed from: protected */
@Override // androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, androidx.appcompat.app.AppCompatActivity, androidx.fragment.app.FragmentActivity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(C0648R.layout.activity_main);
((Button) findViewById(C0648R.C0651id.button)).setOnClickListener(this);
}
public void doToast(String msg) {
Toast.makeText(this, msg, 0).show();
}
public void onClick(View v) {
doToast(WebGrabber.checkLogin(((EditText) findViewById(C0648R.C0651id.etUser)).getText().toString(), ((EditText) findViewById(C0648R.C0651id.etPass)).getText().toString()));
}
}
On voit ici que lors du clic du bouton, la méthode WebGrabber.checkLogin va être appelée avec comme paramètres le nom d’utilisateur et le password entrés. On va alors regarder cette classe WebGrabber et cette fonction checkLogin.
package com.notdarthvador.tryhacktheempire;
import android.util.Log;
public class WebGrabber {
private static boolean doCheckEmu = false;
private static boolean doCheckRoot = false;
private static boolean wasFirstLaunched = false;
private final MainActivity activity;
public static native String checkLogin(String str, String str2);
public static native String string2FromJNI();
public static native String stringFromJNI();
static {
Log.e(MainActivity.TAG, "NativeServices ==>loading lib");
System.loadLibrary("native-lib");
}
public WebGrabber(MainActivity a) {
this.activity = a;
}
}
On voit alors que la fonction checkLogin vient d’une librairie native appelée “native-lib”. On va alors aller chercher cette librairie et reverse sa fonction checkLogin. Pour ceci on décompresse l’APK comme un zip.
$ unzip tryhacktheempire.apk
Archive: tryhacktheempire.apk
inflating: res/color/material_on_surface_disabled.xml
inflating: res/layout/test_toolbar.xml
inflating: res/anim/design_snackbar_in.xml
inflating: res/interpolator/btn_checkbox_checked_mtrl_animation_interpolator_0.xml
extracting: res/drawable-hdpi-v4/abc_list_longpressed_holo.9.png
extracting: res/drawable-xxhdpi-v4/abc_ic_star_half_black_16dp.png
[...]
$ ls -l
total 15920
-rw-r--r--@ 1 Julien staff 2704 1 jan 1981 AndroidManifest.xml
drwxr-xr-x@ 32 Julien staff 1024 5 jui 23:29 META-INF
-rw-r--r--@ 1 Julien staff 4043236 1 jan 1981 classes.dex
-rw-r--r--@ 1 Julien staff 417184 1 jan 1981 classes2.dex
drwxr-xr-x@ 6 Julien staff 192 5 jui 23:29 lib
drwxr-xr-x@ 40 Julien staff 1280 5 jui 23:29 res
-rw-r--r--@ 1 Julien staff 443472 1 jan 1981 resources.arsc
-rw-r--r--@ 1 Julien staff 3231850 5 jui 15:35 tryhacktheempire.apk
$ ls -lR
total 0
drwxr-xr-x@ 3 Julien staff 96 5 jui 23:29 arm64-v8a
drwxr-xr-x@ 3 Julien staff 96 5 jui 23:29 armeabi-v7a
drwxr-xr-x@ 3 Julien staff 96 5 jui 23:29 x86
drwxr-xr-x@ 3 Julien staff 96 5 jui 23:29 x86_64
./arm64-v8a:
total 424
-rw-r--r--@ 1 Julien staff 215240 1 jan 1981 libnative-lib.so
./armeabi-v7a:
total 224
-rw-r--r--@ 1 Julien staff 112316 1 jan 1981 libnative-lib.so
./x86:
total 424
-rw-r--r--@ 1 Julien staff 214764 1 jan 1981 libnative-lib.so
./x86_64:
total 472
-rw-r--r--@ 1 Julien staff 240088 1 jan 1981 libnative-lib.so
On a donc la libraire ‘libnative.so” en ARMv7 (32 bits), ARMv8 (64 bits), x86 (32 bits) et x86_64 (64 bits) : c’est classique en android, la même libraire pour diverses architectures. On choisit par exemple la version x86 et on l’ouvre dans un désassembleur et sélectionne la fonction “checkLogin”.
Par flemme je vais encore une fois utiliser le decompilo d’IDA pour avoir un pseudo-code C du code de la fonction. Après renommage des variables on obtient un résultat très clair.
int __cdecl checkLogin(void *ptr, int a2, int USERNAME, int PASSWORD){
v14 = __readgsdword(20u);
v13 = 0;
ptr_USERNAME = _JNIEnv::GetStringChars(ptr, USERNAME, &v13);
length_USERNAME = _JNIEnv::GetStringLength(ptr, USERNAME);
ptr_PASSWORD = _JNIEnv::GetStringChars(ptr, PASSWORD, &v13);
length_PASSWORD = _JNIEnv::GetStringLength(ptr, PASSWORD);
length = max_len(length_PASSWORD, length_USERNAME);
buffer = malloc(2 * length);
for(i=0; i < length; i++){
buffer[i] = (*(ptr_USERNAME + 2 * i) ^ 0x420420) % 26 + 81;
if(buffer[i] != *(ptr_PASSWORD + 2 * i)){
return_string = _JNIEnv::NewStringUTF(ptr, "Wrong login/password");
goto LABEL_7;
}
}
return_string = _JNIEnv::NewStringUTF(ptr, "Login verified, welcome!");
LABEL_7:
free(buffer);
_JNIEnv::ReleaseStringChars(ptr, USERNAME, ptr_USERNAME);
_JNIEnv::ReleaseStringChars(ptr, PASSWORD, ptr_PASSWORD);
if ( __readgsdword(0x14u) != v14 )
JUMPOUT(loc_9641);
return return_string;
}
On peut alors réimplémenter l’algorithme en python pour récupérer le mot de passe correspondant à l’utilisateur “lorddarthvador”.
user = "lorddarthvador"
password = ""
for i in user:
password += chr( (ord(i) ^ 0x420420) % 26 + 81 )
print("[+] FLAG : SHIELDS{" + password + "}")
On exécute alors le programme et on récupère le flag !
$ python apk.py
[+] FLAG : SHIELDS{iRUaa^UWeY^aRU}
On peut alors aller, pour la beauté du geste, lancer l’APK sur notre téléphone et essayer de rentrer l’username “lorddarthvador” avec le mot de passe “iRUaa^UWeY^aRU”.
Le retour du reverse – 200 pts
Encore une fois, on commence par faire un file sur le binaire pour voir à quoi on a affaire.
$ file leRetourDuReverse
leRetourDuReverse: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=ec20e6a00846b62a61a0b0b5e8a7620308901b61, for GNU/Linux 3.2.0, stripped
Sans plus tarder on lance notre désassembleur préféré et on commence à regarder la fonction main.
Encore une fois il faut passer 1 argument au programme qui sera alors notre flag. Ce flag potentiel sera passé dans la fonction que j’ai appelée “encrypt_input” et le résultat sera comparé à un buffer “unk_4020”.
On peut alors aller regarder cette fonction “encrypt_input”. J’ai ici mâché un peu le travail en remplaçant les variables locales de la forme [rbp-0x8] par des noms beaucoup plus parlants : INPUT pour le flag modifié par la fonction, i et j pour les compteurs de boucle et enfin WEIRD_BUFFER pour une sorte de sbox :
On voit ici une boucle for sur 255 dont le compteur j ne sera pas utilisé. Dans chaque tour de cette boucle, une boucle for de 30 sera exécutée. On peut alors très facilement réimplémenter ça en python et décoder notre “unk_4020”.
FLAG = [
0x82, 0xCB, 0x1D, 0x22, 0x28, 0x03, 0x82, 0x70, 0xAE, 0x4B,
0xAE, 0x81, 0x3C, 0x71, 0x58, 0x3C, 0x77, 0xFC, 0x58, 0xDE,
0x77, 0x4E, 0x45, 0x31, 0xFE, 0xFE, 0x44, 0xFC, 0xD6, 0xDC, 0x47
]
WEIRD_BUFFER = [
9, 0x81, 0x94, 0x87, 0xA0, 0x11, 0xEF, 0xCE, 0x3E, 0x21, 0xD, 0xBF,
0x82, 0xE5, 0x2E, 0x4E, 0x77, 0x95, 0x1A, 0xB, 0xA7, 0xA5, 0x8C, 0x50,
0xF7, 0x9F, 0xF6, 0x63, 0x37, 0x3D, 0x10, 0x43, 0x44, 5, 0xD6, 0x6B,
3, 0xE3, 0x7E, 0xF2, 0xD4, 0x12, 0x7B, 0x26, 0xCB, 0x7C, 0xDB, 0xF4,
0x23, 0x60, 0x25, 0x66, 0x5A, 0x4A, 0xCF, 0xDF, 0x67, 0x5D, 0x2C, 0x65,
0x32, 0xD5, 7, 0x5F, 0x8D, 0x55, 0x33, 0xD7, 0xC2, 0x6E, 0x6A, 0x1E,
0xD1, 0x46, 0xB7, 0xFC, 0xE, 0xC, 0xEC, 0x5E, 0x74, 0x78, 0x4D, 0x52,
0xB3, 0x2F, 0xA9, 6, 0x27, 0xDE, 0xA1, 0x24, 0x20, 0x34, 0xCA, 0xAE,
0xB1, 0xB9, 0x97, 0xBB, 0x36, 0x61, 0xD0, 0xB2, 0xF3, 0x8B, 0xF9,
0x16, 0x9E, 0x1B, 0x2A, 0x0BC, 0xE1, 0xA8, 0x68, 0xC3, 0xED, 0xA,
0x69, 0xC1, 0xA2, 0xF0, 0x13, 0x22, 0xE8, 0xA6, 0xAC, 0x59, 0x98,
0x45, 0xC8, 0x2B, 0xB0, 0x5B, 0xEA, 0x7F, 0xC4, 4, 0x80, 0x30, 0xF1,
0xD9, 0x4F, 0x38, 0x64, 0x96, 0x4B, 0x35, 2, 0xC5, 0x89, 0x40, 0x73,
0xAD, 0, 8, 0x18, 0x56, 0xE6, 0xD8, 0xBD, 0x84, 0x17, 0xD2, 0xB6,
0x93, 0x3F, 0x6D, 0x57, 0xFA, 0x15, 0x70, 0x19, 0x7A, 0x58, 0xCD, 0xC7,
0x9B, 0xB5, 0x76, 0xF, 0x48, 0x90, 0xA3, 0x4C, 0xFF, 0x2D, 0x6C, 0x29,
0xDA, 0xBE, 0x1C, 0xFD, 0x9D, 0x14, 0x7D, 0xFE, 0x31, 0xB8, 0x41,
0x8E, 0x28, 0xEE, 0xDD, 0x83, 0xF5, 0xE7, 0x8F, 0x88, 0x9A, 0xAA,
0x5C, 0x79, 0x51, 0xAB, 0x1F, 0x75, 0x39, 0x3B, 0x86, 0x3A, 0xCC, 0xD3,
0xBA, 0xC9, 0x6F, 0x54, 0xE9, 0x42, 0xAF, 0x85, 0xEB, 0xE0, 0xFB,
0xB4, 0x71, 0x47, 0x49, 0x9C, 0xDC, 0xC0, 0x8A, 0x62, 1, 0x3C, 0xA4,
0x53, 0xE4, 0xF8, 0x1D, 0x91, 0x92, 0xC6, 0xE2, 0x72, 0x99
]
for j in range(256):
for i in range(31):
FLAG[i] = WEIRD_BUFFER.index(FLAG[i])
print("[+] FLAG : " + ''.join(chr(i) for i in FLAG))
On exécute le script et on récupère le flag :
$ python chall3.py
[+] FLAG : SHIELDS{everyday_iam_shuffling}
C’est tout pour cette catégorie reverse engineering du SHIELDS ! C’était très sympa, hâte d’être à la prochaine édition 😁