Etape 1 : reconnaissance du terrain.
Le programme proposé est un binaire GO.
On peut en apprendre un peu plus en utilisant un outil qui s’appelle redress (j’ai utilisé la version 1.2.77).
$ redress-1.2.77 source la-routine
Package main: /home/user/challenges/fcsc_orga/fcsc_orga_26/checkme/src
File: main.go
Check Lines: 10 to 33 (23)
d Lines: 33 to 39 (6)
i Lines: 39 to 45 (6)
n Lines: 45 to 51 (6)
o Lines: 51 to 57 (6)
main Lines: 57 to 76 (19)
maingowrap1 Lines: 64 to 64 (0)
maingowrap2 Lines: 65 to 65 (0)
maingowrap3 Lines: 66 to 66 (0)
maingowrap4 Lines: 67 to 67 (0)
On voit donc que le programme n’est pas super long ce qui est cohérent avec un programme dans la catégorie intro.
Etape 2 : Analyse.
GO se compile en un binaire statique. En général strippé. Le fonctionnement de GO se traduit également par des appels en assembleur compliqué.
Néanmoins ici le binaire n’est pas strippé :
$ file la-routine
la-routine: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=cd7c9e8bb6c8f45b599663c9a36056baa5333618, with debug_info, not stripped
Je vais utiliser GHIDRA 12.1.2 pour analyser le programme. Retrospectivement le support de GO semble s’être amélioré (peut-être avoir un binaire non strippé aide-t-il aussi).
Avec les informations de REDRESS, je cherche directement les éléments dont on a les noms.
Dans main.main(), on voit qu’on alloue 4 channels de noms
d, i, n, o. Le challenge porte-t-il le nom « la routine »
parce qu’au final ce sera un truc assez classique ou parce que
l’on retrouve le goût des concepteurs de challenge FCSC pour
mettre du dinosaure un peu partout ?
Plus loin dans la fonction, on voit :
bVar1 = main_Check(flag,(chan_uint8)d,(chan_uint8)i,(chan_uint8)n,(chan_uint8)o,(chan_uint8)res);
if (bVar1) {
...
format_00.str = "Bravo, vous pouvez valider avec %s\n";
...
fmt::fmt_Fprintf(w,format_00,a_00);
}
else {
...
format_01.str = "Essayez encore\n";
...
fmt::fmt_Fprintf(w,format_00,a_00);
}
Comme le nom le laissait penser on va vérifier quelque chose via
main.Check() qui décidera si l’on a gagné ou pas.
Si l’on survole le code de main.Check() on voit à la fin les quelques
lignes qui décident du succès ou pas :
iVar7 = internal/bytealg::internal_bytealg_Compare
(main_expected.array,main_expected.len,main_expected.cap,puVar3,uVar5,uVar4);
return iVar7 == 0;
Sans être programmeur GO, on comprend qu’on compare quelque chose
calculé à partir de l’input par rapport à main.expected (qui
ressemble à une structure avec des champs array, len, cap).
GHIDRA nous renvoie sur main_expected :
main.expected.len XREF[2,2]: Entry Point(*),
main.expected.cap main.Check:004a696f(R),
main.expected main.Check:004a6976(R),
main.Check:004a697d(R)
00583690 a0 e6 57 []uint8
00 00 00
00 00 30
00583690 a0 e6 57 00 00 uint8 * main..gobytes.1 array XREF[2]: Entry Point(*),
00 00 00 main.Check:004a696f(R)
00583698 30 00 00 00 00 int 30h len XREF[1]: main.Check:004a6976(R)
00 00 00
005836a0 30 00 00 00 00 int 30h cap XREF[1]: main.Check:004a697d(R)
00 00 00
Donc array renvoie sur l’adresse 0x57e6a0 dans le binaire sur
main..gobytes.1 où l’on devrait trouver 48 bytes (0x30) intéressants.
Effectivement on voit :
main..gobytes.1 XREF[2]: main.Check:004a696f(*),
00583690(*)
0057e6a0 47 42 55 undefine
41 7c 46
71 4a 62
0057e6a0 47 undefined147h [0] XREF[2]: main.Check:004a696f(*),
00583690(*)
0057e6a1 42 undefined142h [1]
0057e6a2 55 undefined155h [2]
0057e6a3 41 undefined141h [3]
0057e6a4 7c undefined17Ch [4]
0057e6a5 46 undefined146h [5]
0057e6a6 71 undefined171h [6]
0057e6a7 4a undefined14Ah [7]
0057e6a8 62 undefined162h [8]
0057e6a9 6d undefined16Dh [9]
0057e6aa 49 undefined149h [10]
0057e6ab 5d undefined15Dh [11]
0057e6ac 49 undefined149h [12]
0057e6ad 33 undefined133h [13]
0057e6ae 75 undefined175h [14]
0057e6af 5d undefined15Dh [15]
0057e6b0 43 undefined143h [16]
0057e6b1 74 undefined174h [17]
0057e6b2 33 undefined133h [18]
0057e6b3 6a undefined16Ah [19]
0057e6b4 55 undefined155h [20]
0057e6b5 30 undefined130h [21]
0057e6b6 50 undefined150h [22]
0057e6b7 5d undefined15Dh [23]
0057e6b8 44 undefined144h [24]
0057e6b9 2f undefined12Fh [25]
0057e6ba 70 undefined170h [26]
0057e6bb 41 undefined141h [27]
0057e6bc 76 undefined176h [28]
0057e6bd 51 undefined151h [29]
0057e6be 54 undefined154h [30]
0057e6bf 31 undefined131h [31]
0057e6c0 6f undefined16Fh [32]
0057e6c1 62 undefined162h [33]
0057e6c2 7b undefined17Bh [34]
0057e6c3 5d undefined15Dh [35]
0057e6c4 51 undefined151h [36]
0057e6c5 33 undefined133h [37]
0057e6c6 56 undefined156h [38]
0057e6c7 52 undefined152h [39]
0057e6c8 66 undefined166h [40]
0057e6c9 71 undefined171h [41]
0057e6ca 50 undefined150h [42]
0057e6cb 71 undefined171h [43]
0057e6cc 22 undefined122h [44]
0057e6cd 20 undefined120h [45]
0057e6ce 23 undefined123h [46]
0057e6cf 7b undefined17Bh [47]
On a donc cette string hexadécimale :
474255417c46714a626d495d4933755d4374336a5530505d442f7041765154316f627b5d51335652667150712220237b
qui donne la chaine ASCII :
GBUA|FqJbmI]I3u]Ct3jU0P]D/pAvQT1ob{]Q3VRfqPq" #{
Si l’on revient sur « dino », on voit pour chacun :
/* DWARF original prototype: void main.d(chan_uint8 c, chan_uint8 res)
Golang function info: {@address 0057dc98 "Flags: []"}
Golang source: /home/user/challenges/fcsc_orga/fcsc_orga_26/checkme/src/main.go:33
Golang stacktrace signature: func main.d(8, 8) ??? */
void main::main_d(chan_uint8 c,chan_uint8 res)
{
bool bVar1;
chan_uint8 c_spill;
chan_uint8 res_spill;
char local_a;
char local_9;
while (&stack0x00000000 <= CURRENT_G.stackguard0) {
runtime::runtime_morestack_noctxt();
}
while (bVar1 = runtime::runtime_chanrecv2((runtime_hchan *)c,&local_a), local_9 = local_a, bVar1)
{
local_a = '\0';
local_9 = local_9 + '\x01';
runtime::runtime_chansend1((runtime_hchan *)res,&local_9);
}
return;
}
On lit sur le channel d un char sur lequel on fait +1 et on l’écrit sur
le channel res.
De la même façon on voit :
- On lit sur le channel
iun char sur lequel on fait -1 et on l’écrit sur le channelres. - On lit sur le channel
nun char sur lequel on fait +2 et on l’écrit sur le channelres. - On lit sur le channel
oun char sur lequel on fait -2 et on l’écrit sur le channelres.
Donc on comprend que le programme attend une string et la convertit en
appliquant successivement +1, -1, +2, -2 sur chaque valeur ASCII du
caractère et le résultat doit être la string
GBUA|FqJbmI]I3u]Ct3jU0P]D/pAvQT1ob{]Q3VRfqPq" #{.
Je laisse au lecteur le soin de faire l’opération inverse « routinière » de conversion de codes ASCII.
On construit alors l’entrée gagnante :
FCSC{GoLanG_H4s_Bu1lT1N_C0nCuRR3ncy_P4TTerNs!!!}
Le flag se valide.
QED.
🐒