vous pouvez retrouver le binaire ici
Ce challenge est une version simplifiée d’un challenge nommé “chaussette”, durant la résolution de sa version compliquée j’ai appris à utiliser miasm, un framework de reverse engineering que j’ai trouvé très intéressant malgré le peu de documentation. J’ai donc décidé de proposer une solution utilisant miasm.
Découverte du challenge
Pour ce challenge, un binaire nous est donné. La commande file
permet d’en apprendre un peu plus à son sujet.
╭─user@arch-vmware ~/shared/fcsc2023/reverse/chaussette-xs
╰─➤ file chaussette-xs
chaussette-xs: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=913fe53d14998feda6a550d5a815830cf4ab132f, for GNU/Linux 3.2.0, stripped
On apprend donc que l’on a un ELF x86-64. On va donc l’ouvrir dans ghidra afin d’avoir de comprendre ce qu’il fait plus précisemment.
void FUN_00102140(undefined8 param_1,undefined8 param_2,undefined8 param_3)
{
undefined8 unaff_retaddr;
undefined auStack_8 [8];
__libc_start_main(FUN_00102000,unaff_retaddr,&stack0x00000008,FUN_00103230,FUN_00103290,param_3,
auStack_8);
do {
/* WARNING: Do nothing block with infinite loop */
} while( true );
}
On regard donc le point d’entrée du binaire afin de trouver la fonction main de l’application.
Ici, l’appel à la fonction libc_start_main
de la libc nous permet de récuperer notre fonction principale qui sera donc FUN_00102000
.
On va de suite la renommer afin d’y voir plus clair et de la retrouver plus facilement (clic droit sur la fonction -> Rename function).
On arrive donc sur le code principal de notre fonction qui est assez simple à comprendre.
undefined8 main(void)
{
int iVar1;
long lVar2;
undefined8 *puVar3;
ulong *puVar4;
ulong uVar5;
ulong *puVar6;
undefined8 *puVar7;
byte bVar8;
ulong local_18;
undefined8 local_10;
bVar8 = 0;
lVar2 = sysconf(0x1e);
if (lVar2 == 0x1000) {
puVar3 = (undefined8 *)mmap((void *)0x0,0x100000,7,0x22,-1,0);
DAT_001061b0 = puVar3;
*puVar3 = 0x1a22d2ea22d1b848;
*(undefined8 *)((long)puVar3 + 0xf9) = 0xc3f83148b12e2565;
lVar2 = (long)puVar3 - (long)(undefined8 *)((ulong)(puVar3 + 1) & 0xfffffffffffffff8);
puVar7 = (undefined8 *)((long)&DAT_001060a0 - lVar2);
puVar3 = (undefined8 *)((ulong)(puVar3 + 1) & 0xfffffffffffffff8);
for (uVar5 = (ulong)((int)lVar2 + 0x101U >> 3); uVar5 != 0; uVar5 = uVar5 - 1) {
*puVar3 = *puVar7;
puVar7 = puVar7 + (ulong)bVar8 * -2 + 1;
puVar3 = puVar3 + (ulong)bVar8 * -2 + 1;
}
__isoc99_scanf(&DAT_00104004,&local_18);
__isoc99_scanf(&DAT_00104004,&local_10);
lVar2 = (*(code *)DAT_001061b0)(local_18,local_10);
if (lVar2 == 0) {
iVar1 = mprotect(FUN_00103000,0x400,7);
if (iVar1 != -1) {
puVar4 = (ulong *)FUN_00103000;
uVar5 = local_18;
do {
*puVar4 = *puVar4 ^ uVar5;
puVar6 = puVar4 + -0x205ff;
uVar5 = uVar5 * 0x5851f42d4c957f2d + 0x14057b7ef767814f;
puVar4 = puVar4 + 1;
} while (puVar6 < (ulong *)0x1f8);
FUN_00103000(DAT_001061b0,local_18,local_10);
}
}
}
return 0;
}
Il va dans un premier temps allouer 0x100000
octets sur le tas avec les droits de lecture, d’écriture et d’execution grâce à la fonction mmap
Il va ensuite effectuer certaines opérations afin d’écrire des données dans la mémoire allouée, on ne s’attardera pas dessus.
Puis le programme va demander à l’utilisateur 2 entrées. Si l’on regard à l’addresse DAT_00104004
on peut voir qu’elle pointe vers la chaine de caractères : “%lu” ce qui signifie que la fonction scanf
va demander à l’utilisateur 2 entiers non signés de 64 bytes.
Enfin, il va appeler le code placé dans la mémoire allouée précedemment avec comme paramètres les 2 valeurs entrées par l’utilisateur.
En fonction du résultat de cette fonction il va continuer l’execution du programme ou non. On comprend donc rapidemment que notre première épreuve va être de trouver les entrées correctes afin que cette fonction retourne 0.
A la suite de cette fonction, si les valeurs entrées sont correctes le binaire va appeler la fonction mprotect()
afin d’ajouter les droits d’écriture sur la fonction FUN_00103000
et va ensuite modifier son contenu. Un fois le code de la fonction modifié, il va l’appeler comme argument l’addresse de notre mémoire précedemment allouée ainsi que nos 2 entrées utilisateur.
premier shellcode
La première étape de ce crackme va donc être de trouver les valeurs correcte afin que le code placé en mémoire retourne 0. On va donc utiliser un debugger afin de placer un point d’arrêt au moment de l’appel à la fonction et de récuperer son code.
Pour ma part j’utilise gdb.
Petite astuce d’ailleurs si vous utilisez comme moi ghidra pour décompiler votre code et gdb pour debugger, vous pouvez changer la base address dans ghidra afin que vos addresses soit les mêmes entre ghidra et votre debugger. Il suffit d’aller dans Window -> Memory map
ensuite vous cliqué sur la petite maison et changer l’addresse de la base du binaire.
on lance donc gdb, on place notre point d’arret et on lance notre binaire.
(gdb) b*0x5555555560aa
Breakpoint 1 at 0x5555555560aa
(gdb) run
Starting program: /home/user/shared/fcsc2023/reverse/chaussette-xs/chaussette-xs
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
123
123
Breakpoint 1, 0x00005555555560aa in ?? ()
(gdb) x/i $rip
=> 0x5555555560aa:
call QWORD PTR [rip+0x4100] # 0x55555555a1b0
On peut voir qu’on est bel et bien arrété avant l’appel à la fonction.
La commande x/i address
permet de décoder les opcodes présent à une addresse.
On va donc pouvoir afficher notre shellcode grâce à cette commande.
Attention, ici le code ne se trouve pas à l’addresse mais à l’addresse pointée par celle ci.
(gdb) x/gx 0x55555555a1b0
0x55555555a1b0: 0x00007ffff7cc5000
(gdb) x/100i 0x00007ffff7cc5000
0x7ffff7cc5000: movabs rax,0x33a11a22d2ea22d1
[...]
0x7ffff7cc50ed: mul rdi
0x7ffff7cc50f0: mov rdi,rax
0x7ffff7cc50f3: movabs rax,0xb12e2565d1efe9f8
0x7ffff7cc50fd: xor rax,rdi
0x7ffff7cc5100: ret
On a donc un joli code assembleur, maintenant il faut le résoudre. On remarque d’ailleurs qu’il utilise une seule des entrées utilisateur dans cette fonction. Ce sera de même tout le long du programme, la seconde entrée n’est pas utilisée.
movabs rax,0x33a11a22d2ea22d1
sub rdi,rax
dec rdi
movabs rax,0x94224bcad3296113
or rax,0x1
mul rdi
mov rdi,rax
dec rdi
movabs rax,0xd3b1bf2d4d1d294a
sub rdi,rax
movabs rax,0xbe35746c05e956a9
add rdi,rax
movabs rax,0x5819ea9fcbc8779
xor rdi,rax
dec rdi
movabs rax,0x7f7965e1baca6c8f
sub rdi,rax
neg rdi
ror rdi,0xd
movabs rax,0x4368955c512ca39b
sub rdi,rax
inc rdi
neg rdi
ror rdi,0x32
movabs rax,0x96ccb75bd569807d
sub rdi,rax
inc rdi
movabs rax,0x9f40cb3b786d6842
or rax,0x1
mul rdi
mov rdi,rax
ror rdi,0x14
neg rdi
neg rdi
movabs rax,0x1fd3e4098240e723
add rdi,rax
movabs rax,0xda3d1c27bae9522c
sub rdi,rax
rol rdi,0x36
inc rdi
inc rdi
dec rdi
rol rdi,0x21
rol rdi,0x38
dec rdi
neg rdi
movabs rax,0xc88d84433f7b14e7
or rax,0x1
mul rdi
mov rdi,rax
movabs rax,0xb12e2565d1efe9f8
xor rax,rdi
ret
Il n’est pas très compliqué et aurait pu facilement être résolu à la main, néanmoins pour cette solution on utilisera miasm.
On va dump les octets de ce code dans un fichier, gdb nous permet de le faire grâce à sa commande dump
(gdb) dump binary memory bytcodes_function 0x7ffff7cc5000 0x7ffff7cc5101
Je vous invite donc maintenant à installer miasm.
Résolution avec Miasm
Pour cette partie nous allons utiliser le framework miasm afin de résoudre ce code.
Voici mon code permettant de trouver les entrées correctes à notre fonction. J’ai commenté le code au maximum, néanmoins je vais quand même vous résumer son fonctionnement afin d’être sur que tout le monde comprenne.
Le programme va dans un premier temps lire les octets que l’on a dump précedemmemnt, les interpreter puis lancer une execution symbolique sur notre code. A la suite de cette exécution symbolique il va récuperer l’expression symbolique du registre RAX, qui est le registre qui contient notre valeur de retour et la transformer en contrainte compréhensible par z3. A partir de là on va simplifier cette expression et la résoudre.
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB
from miasm.analysis.binary import Container
from miasm.expression.expression import *
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.ir.translators.z3_ir import TranslatorZ3
from miasm.arch.x86.lifter_model_call import LifterModelCall_x86_64
from miasm.analysis.simplifier import *
from miasm.expression.simplifications import expr_simp, ExpressionSimplifier
from z3 import *
import re
def solve_shellcode(shellcode):
# on créé une nouvelle location pour notre shellcode
loc_db = LocationDB()
# on load notre shellcode (nos bytes code)
container = Container.from_string(shellcode, loc_db)
machine = Machine('x86_64')
ira = machine.lifter(loc_db)
dis_engine = machine.dis_engine(container.bin_stream, loc_db=loc_db)
start_addres = 0
# https://github.com/cea-sec/miasm/blob/master/doc/ir/lift.ipynb
asm_cfg = dis_engine.dis_multiblock(start_addres)
ira_cfg = ira.new_ircfg_from_asmcfg(asm_cfg)
"""
# on peut utiliser ce bout de code pour afficher les instructions assembleurs.
for block in asm_cfg.blocks:
print(block)
"""
init_state = {}
# on definit ici RDI qui va contenir la valeur en entrée.
init_state[ExprId("RDI", 64)] = ExprId('input', 64)
# rax qui est censé être égal à 0 à la fin du script.
init_state[ExprId("RAX", 64)] = ExprId('result', 64)
# on lance l'execution symbolique.
sb = SymbolicExecutionEngine(LifterModelCall_x86_64(loc_db) , state=init_state)
sb.run_at(ira_cfg, addr=start_addres)
# on va transformer l'expression symbolique en contraintes pour z3
trans = TranslatorZ3(loc_db=loc_db)
# solveur basique z3
s = Solver()
# on utilise miasm pour simplifier les expressions symboliques (on aurait pu faire sans pour ce chall)
expr_simp_cond = ExpressionSimplifier()
expr_simp_cond.enable_passes(ExpressionSimplifier.PASS_COND)
# récuperer l'expression symbolique (+ la simplifiée.)
expr_rax = sb.eval_expr(expr_simp(expr_simp_cond(ExprId('RAX', 64))))
# on solve avec z3
s.add(trans.from_expr(expr_rax) == trans.from_expr(ExprInt(0, 64)))
if s.check() == sat:
# print("found")
model = s.model()
# on retourne le resultat
# (j'ai honte de mon parsing mais je n'arrivais pas récuperer cette valeur correctement ...)
return int(re.findall("[0-9]+", str(model))[0])
else:
print("[-] fail")
return 0
# on ouvre le fichier contenant les octets de notre code assembleur
with open("bytcodes_function", "rb") as file:
content = file.read()
result =
solve_shellcode(content)
if result:
print(f"[+] solution found : {result}")
On va donc executer notre programme et l’on récupère bel et bien une valeur.
╭─user@arch-vmware ~/shared/fcsc2023/reverse/chaussette-xs
╰─➤ python3 solve_first_shellcode.py
[+] solution found : 14171339039947875846
On peut entrer ces valeurs dans notre binaire et effectivemment notre script passe la première condition avec succès !
Vous croyez que c’était fini ? c’est que le début.
On passe donc à la deuxième fonction. Comme précédemment, on place notre breakpoint avant l’appel à la fonction et on l’affiche.
(gdb) b*0x000055555555612c
Breakpoint 1 at 0x55555555612c
(gdb) run
Starting program: /home/user/shared/fcsc2023/reverse/chaussette-xs/chaussette-xs
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
14171339039947875846
123
Breakpoint 1, 0x000055555555612c in ?? ()
(gdb) x/i $rip
=> 0x55555555612c: call 0x555555557000
(gdb) x/110i 0x555555557000
0x555555557000: movabs rax,0x676e656c6c616863
0x55555555700a: push r15
0x55555555700c: push r14
0x55555555700e: push r13
0x555555557010: push r12
0x555555557012: push rbp
0x555555557013: push rbx
0x555555557014: mov rbx,rdi
0x555555557017: lea rdi,[rip+0xfea] # 0x555555558008
0x55555555701e: sub rsp,0x68
0x555555557022: movdqa xmm0,XMMWORD PTR [rip+0xfe6] # 0x555555558010
0x55555555702a: mov QWORD PTR [rsp+0x8],rsi
0x55555555702f: movups XMMWORD PTR [rsp+0x30],xmm0
0x555555557034: movdqa xmm0,XMMWORD PTR [rip+0xfe4] # 0x555555558020
0x55555555703c: mov QWORD PTR [rsp],rdx
0x555555557040: mov QWORD PTR [rsp+0x50],rax
0x555555557045: mov DWORD PTR [rsp+0x58],0x72662e65
0x55555555704d: mov BYTE PTR [rsp+0x5c],0x0
0x555555557052: movups XMMWORD PTR [rsp+0x40],xmm0
0x555555557057: call 0x5555555550a0 <getprotobyname@plt>
0x55555555705c: test rax,rax
0x55555555705f: je 0x5555555571bf
0x555555557065: mov edx,DWORD PTR [rax+0x10]
0x555555557068: mov esi,0x1
0x55555555706d: mov edi,0x2
0x555555557072: call 0x5555555550f0 <socket@plt>
0x555555557077: mov DWORD PTR [rip+0x313b],eax # 0x55555555a1b8
0x55555555707d: cmp eax,0xffffffff
0x555555557080: je 0x5555555571bf
0x555555557086: lea rdi,[rsp+0x30]
0x55555555708b: call 0x555555555090 <gethostbyname@plt>
0x555555557090: test rax,rax
0x555555557093: je 0x5555555571bf
0x555555557099: mov rax,QWORD PTR [rax+0x18]
0x55555555709d: mov rax,QWORD PTR [rax]
0x5555555570a0: mov edi,DWORD PTR [rax]
0x5555555570a2: call 0x555555555050 <inet_ntoa@plt>
0x5555555570a7: mov rdi,rax
0x5555555570aa: call 0x555555555080 <inet_addr@plt>
0x5555555570af: cmp eax,0xffffffff
0x5555555570b2: je 0x5555555571bf
0x5555555570b8: mov edi,DWORD PTR [rip+0x30fa] # 0x55555555a1b8
0x5555555570be: lea rsi,[rsp+0x20]
0x5555555570c3: mov DWORD PTR [rsp+0x24],eax
0x5555555570c7: mov edx,0x10
0x5555555570cc: mov DWORD PTR [rsp+0x20],0xcd080002
0x5555555570d4: call 0x5555555550e0 <connect@plt>
0x5555555570d9: cmp eax,0xffffffff
0x5555555570dc: je 0x5555555571bf
0x5555555570e2: lea r12,[rsp+0x18]
0x5555555570e7: lea r13,[rsp+0x10]
0x5555555570ec: lea r14,[rsp+0x8]
0x5555555570f1: nop DWORD PTR [rax+0x0]
0x5555555570f8: mov rax,QWORD PTR [rsp+0x8]
0x5555555570fd: mov edi,DWORD PTR [rip+0x30b5] # 0x55555555a1b8
0x555555557103: mov edx,0x8
0x555555557108: mov rsi,r12
0x55555555710b: mov QWORD PTR [rsp+0x18],rax
0x555555557110: call 0x555555555040 <write@plt>
0x555555557115: mov rax,QWORD PTR [rsp]
0x555555557119: mov edx,0x8
0x55555555711e: mov rsi,r12
0x555555557121: mov edi,DWORD PTR [rip+0x3091] # 0x55555555a1b8
0x555555557127: mov QWORD PTR [rsp+0x18],rax
0x55555555712c: call 0x555555555040 <write@plt>
0x555555557131: mov edi,DWORD PTR [rip+0x3081] # 0x55555555a1b8
0x555555557137: mov edx,0x8
0x55555555713c: mov rsi,r13
0x55555555713f: call 0x555555555070 <read@plt>
0x555555557144: mov rbp,QWORD PTR [rsp+0x10]
0x555555557149: cmp rbp,0xffffffffffffffff
0x55555555714d: je 0x5555555571ce
0x55555555714f: mov r15,rbx
0x555555557152: test rbp,rbp
0x555555557155: jne 0x55555555716e
0x555555557157: jmp 0x555555557189
0x555555557159: nop DWORD PTR [rax+0x0]
0x555555557160: add r15,rax
0x555555557163: mov rax,r15
0x555555557166: sub rax,rbx
0x555555557169: cmp rax,rbp
0x55555555716c: jae 0x555555557189
0x55555555716e: mov edi,DWORD PTR [rip+0x3044] # 0x55555555a1b8
0x555555557174: xor ecx,ecx
0x555555557176: mov edx,0x1000
0x55555555717b: mov rsi,r15
0x55555555717e: call 0x555555555030 <recv@plt>
0x555555557183: cmp rax,0xffffffffffffffff
0x555555557187: jne 0x555555557160
0x555555557189: mov rsi,r14
0x55555555718c: lea rdi,[rip+0xe71] # 0x555555558004
0x555555557193: xor eax,eax
0x555555557195: call 0x5555555550d0 <__isoc99_scanf@plt>
0x55555555719a: mov rsi,rsp
0x55555555719d: lea rdi,[rip+0xe60] # 0x555555558004
0x5555555571a4: xor eax,eax
0x5555555571a6: call 0x5555555550d0 <__isoc99_scanf@plt>
0x5555555571ab: mov rsi,QWORD PTR [rsp]
0x5555555571af: mov rdi,QWORD PTR [rsp+0x8]
0x5555555571b4: call rbx
0x5555555571b6: test rax,rax
0x5555555571b9: je 0x5555555570f8
0x5555555571bf: add rsp,0x68
0x5555555571c3: pop rbx
0x5555555571c4: pop rbp
0x5555555571c5: pop r12
0x5555555571c7: pop r13
0x5555555571c9: pop r14
0x5555555571cb: pop r15
0x5555555571cd: ret
cette fonction peut paraître effrayante aux premiers abords, mais elle est en réalité plutôt simple. On peut d’abord voir les appels aux fonctions de la libc tels que socket
, connect
, recv
etc.
Elle va établir une connexion tcp avec un serveur distant, lui envoyer nos 2 valeurs entrées précédemment et récuperer des données que le serveur lui envoie. Les octets que la fonction récupère vont être placés à l’addresse de notre précédente fonction. Ensuite le programme va demander à nouveau 2 entrées à l’utilisateur et appeler notre nouvelle fonction avec les valeurs données par l’utlisateur et recommencer.
On pourrait représenter le fonctionnement de cette fonction par le pseudo code suivant :
void strange_function(char* shellcode, ulong value1, ulong value2)
{
conn = connexion(); // établit une connexion tcp avec le serveur
while (1)
{
size = read(conn, 8); // lit 8 octets qui seront la taille du shellcode
*shellcode = recv(conn) // reçois le shellcode et l'écris à
// l'addresse de l'ancien shellcode
// récupère les 2 entrées utilisateur
scanf("%lu", &value1);
scanf("%lu", &value2);
// verifie que les entrées résolve le shellcode.
// pareil que la première partie du challenge.
if ((*(void(*)())shellcode)(value1, value2) != 0)
{
// signifie que notre entrée est fausse
// le programme se stoppe. (et donc par conséquent pas de flag)
return;
}
}
}
On va donc récuperer l’IP et le port afin de créer nous même la connexion tcp avec le serveur distant, lui envoyer nos valeurs et récuperer les shellcodes.
Pour récuperer l’ip et le port on pourrait placer un point d’arret au niveau de l’appel à la fonction getprotobyname
et au niveau de la fonction socket
. Mais étant flemmard (ou malin à vous de me dire) j’ai préféré regarder les connexions ouvertes sur ma machine par le processus en question.
Pour ce faire j’ai utilisé l’outil ss
. J’execute donc mon binaire, je lui donne les valeurs trouvé précedemment afin qu’il établisse la connexion et ensuite avec la commande ss -nap | grep chaussette-xs
je récupère l’ip et le port.
Le serveur distant se trouve donc en 51.254.115.216:2253
.
Résoudre les shellcode avec miasm
Après quelques tests on s’aperçoit qu’a chaque connexion le code renvoyé par le serveur change. On va donc implémenter un algorithme qui resout les fonctions donnée par le serveur distant.
Heureusement nous avons déjà fait une grande partie du travail, en effet nous avons implémenté durant la première partie une fonction qui utilise miasm afin de résoudre les shellcodes. Nous avons donc a configurer la connexion avec le serveur et ensuite résoudre les instructions que le serveur nous enverra.
Voici mon code :
from pwn import *
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB
from miasm.analysis.binary import Container
from miasm.expression.expression import *
from miasm.ir.symbexec import SymbolicExecutionEngine
from miasm.ir.translators.z3_ir import TranslatorZ3
from miasm.arch.x86.lifter_model_call import LifterModelCall_x86_64
from miasm.analysis.simplifier import *
from miasm.expression.simplifications import expr_simp, ExpressionSimplifier
from z3 import *
import re
def solve_shellcode(shellcode):
# on créé une nouvelle location pour notre shellcode
loc_db = LocationDB()
# on load notre shellcode
container = Container.from_string(shellcode, loc_db)
machine = Machine('x86_64')
ira = machine.lifter(loc_db)
dis_engine = machine.dis_engine(container.bin_stream, loc_db=loc_db)
start_addres = 0
# https://github.com/cea-sec/miasm/blob/master/doc/ir/lift.ipynb
asm_cfg = dis_engine.dis_multiblock(start_addres)
ira_cfg = ira.new_ircfg_from_asmcfg(asm_cfg)
"""
# on peut utiliser ce bout de code pour afficher les instructions assembleurs.
for block in asm_cfg.blocks:
print(block)
"""
init_state = {}
# on definit ici RDI qui va contenir la valeur en entrée.
init_state[ExprId("RDI", 64)] = ExprId('input', 64)
# rax qui est censé être égal à 0 à la fin du script.
init_state[ExprId("RAX", 64)] = ExprId('result', 64)
# on lance l'execution symbolique.
sb = SymbolicExecutionEngine(LifterModelCall_x86_64(loc_db) , state=init_state)
sb.run_at(ira_cfg, addr=start_addres)
# on va transformer l'expression symbolique en contraintes pour z3
trans = TranslatorZ3(loc_db=loc_db)
# solveur basique z3
s = Solver()
# on utilise miasm pour simplifier les expressions symboliques (on aurait pu faire sans pour ce chall)
expr_simp_cond = ExpressionSimplifier()
expr_simp_cond.enable_passes(ExpressionSimplifier.PASS_COND)
# récuperer l'expression symbolique (+ la simplifiée.)
expr_rax = sb.eval_expr(expr_simp(expr_simp_cond(ExprId('RAX', 64))))
# on solve avec z3
s.add(trans.from_expr(expr_rax) == trans.from_expr(ExprInt(0, 64)))
if s.check() == sat:
# print("found")
model = s.model()
return int(re.findall("[0-9]+", str(model))[0])
else:
print("[-] fail")
print(shellcode)
return 0
# j'ai placé mon breakpoint au final, je trouvai un hostname plus joli
host = "challenges.france-cybersecurity-challenge.fr"
port = 2253
data_1 = 14171339039947875846
data_2 = 0xdeadbeef # useless
# on etablit la connection tcp.
p = remote(host, port)
compteur = 0
while 1:
# on envoie notre solution ainsi que celle inutile pour ce challenge.
p.send(data_1.to_bytes(8, "little"))
p.send(data_2.to_bytes(8, "little"))
size = int.from_bytes(p.recv(8), "little")
shellcode = b""
print(f"[~] shellcode of {size} bytes")
shellcode = p.recv(size)
flag = re.findall(b"FCSC{.*}", shellcode)
if flag:
print("flag is : ", end="")
print(flag[0].decode())
exit()
print(f"[~] solving shellcode {compteur}...")
result = solve_shellcode(shellcode)
if result:
data_1 = result
print(f"[+] solution found : {data_1}")
compteur += 1
else:
exit(1)
# on ferme la connection
p.close()
Ce code va établir une connexion avec le serveur et tant qu’il ne trouve pas le flag dans les fonctions qu’il reçoit il va les résoudre et renvoyer la solution. Je ne pense pas qu’il est nécessaire que j’explique mon script en détail, il est très similaire au précédent.
On execute donc le script et au bout de quelques secondes le flag apparait.
╭─user@arch-vmware ~/shared/fcsc2023/reverse/chaussette-xs
╰─➤ python3 solving_using_miasm.py
[+] Opening connection to challenges.france-cybersecurity-challenge.fr on port 2253: Done
[~] shellcode of 414 bytes
[~] solving shellcode 0...
[+] solution found : 16584048833465228239
[~] shellcode of 444 bytes
[~] solving shellcode 1...
[+] solution found : 5460636808995531285
[~] shellcode of 842 bytes
[~] solving shellcode 2...
[+] solution found : 3445377768356975671
[~] shellcode of 810 bytes
[~] solving shellcode 3...
[+] solution found : 6412023629681526002
[~] shellcode of 929 bytes
[~] solving shellcode 4...
[+] solution found : 1172201077721270149
[~] shellcode of 18446744073709551615 bytes
flag is : FCSC{2a86d6edc5d08afc03d2c9ef3e2ff83cd63e520ffbd716d96479df8147e6da5e}
[*] Closed connection to challenges.france-cybersecurity-challenge.fr port 2253
Pour conclure
J’ai trouvé ce challenge très intéressant, j’ai malheureusement été bloqué par son grand frère durant plusieurs jours sans le réussir. Il m’a néanmoins appris beaucoup de choses sur l’automatisation de l’analyse binaire. En effet j’ai toujours cru que les outils tels que angr, miasm et tout les solveurs de ce genre était pour les personnes qui ne voulaient pas réfléchir. C’était donc la première fois que j’utilisais miasm (et angr) et j’ai trouvé ça très intéressant. Cela m’a permis d’aborder un nouvel aspect du reverse engineering qui m’a l’air très complexe mais également très intéressant.