Solution de ThierryBesancon pour La routine

intro reverse linux x86/x64

7 juillet 2026

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 i un char sur lequel on fait -1 et on l’écrit sur le channel res.
  • On lit sur le channel n un char sur lequel on fait +2 et on l’écrit sur le channel res.
  • On lit sur le channel o un char sur lequel on fait -2 et on l’écrit sur le channel res.

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.

🐒