The Sthack is a well-regarded cybersecurity event in France, and the CTF is composed of difficult challenges intended for experienced cybersecurity professionals. This year we attended with a group of students, and managed to solve some challenges to finish 12th at this competition.

I chose to share with you this writeup of a reverse engineering challenge I solved during this event.

Code Lyoko

Auteur : ghozt

Reverse, medium

Jeremie took control of the XANA code for a short period of time and successfully placed a backdoor. He asks you to get a hand on it as fast as possible !

nc 51.20.123.75 1337

This executable is given. Let’s try and run it :

$ ./8f612a248da7de80738a208bed1d4d19a27621adf12a2f0adcf55a05af9f06a0
              C0d3_Ly0k0                
                                        
                  &&@&                  
                 %@&@@@                 
                 /@%%@(                 
                 /@  @(                 
              *&@@@@@@@@%,              
          @@&             .@@&          
       &@,       ./../.       %@/       
     /@,    .@@*        *@@     #@.     
    %@    ,@/              (@    .@,    
   ,@    /@     *@@&&@@      @,   (@    
   .*   %@#    #@(@@@@.@     %@@   /    
    (@&  ,@@    .@&#@@*@@     @@,  @@.   
     @@    @&                &@   .@&    
      @#    *@%            %@.    @@     
      /%@. .   *@@@&..&@@@,   . #@*/     
         /@@                 ,@@.        
            .@@@@*.&&&#.(@@@@            
           ,@ *   @@@@@%   ( @           
          .@ @.    &  %    (# @          
         .@.@,    .@  @.    ##/@         
        .@.@,      @  @      %@#@        
       ,*@,       @  @       %@&        
                  @%@@                  
                  @%@@                  
                  ,%&,                  
######@@@@@@@ X.A.N.A is watching you @@@~####']-[}
XANA Management Console 
> a

It takes a character as input and stops, let’s try and analyse the source code with Ghidra.

Static analysis

void main(void)
{
  int user_input;
  
  setvbuf_init();
  signal(14,handle_sigalrm);
  signal(8,handle_sigfpe);
  alarm(30);
  menu_art();
  menu_msg();
  print_msg("XANA Management Console \n",30);
  do {
     print_msg(&INPUT_SYMBOL,30);
     user_input = getchar();
     handle_input((int)(char)user_input);
  } while( true );
}

The main function starts by declaring which functions will handle signal 14 and 8. According to signal’s man code 14 and 8 are respectively handling SIGALRM and SIGFPE. The first one doesn’t interest us much since the handle_sigalrm only prints a message before restarting the program (unless we can use the system function later on) :

void handle_sigalrm(int signal)
{
  if (signal == 14) {
     print_msg("Too late... Back to the past !\n",0x1e);
     system("/home/ctf/chall");
  }
  return;
}

The second one is more interesting since it’s printing out the content of the BCKDR variable (probably useful for the challenge) :

void handle_sigfpe(int signal)
{
  char *bckdr;
  size_t length;
  int index;
  
  if (signal == 8) {
     bckdr = getenv("BCKDR");
     length = strlen(bckdr);
     for (index = 0; (ulong)(long)index < length; index = index + 1) {
       printf("%d ",(ulong)(byte)(bckdr[index] ^ 127));
     }
     putchar(10);
     exit(1);
  }
  return;
}

Since SIGFPE is signals an arithmetic error, we’ll have to find a way to make the program do a bad arithmetic operation.

The main function then prints out a menu, gets an input of 1 character, and sends it to the handle_input function :

void handle_input(char user_input)
{
  if (user_input == 's') {
     calculate_mob_army();
     exit(42);
  }
  if (user_input < 't') {
     if (user_input == 'h') {
       print_help_msg();
       return;
     }
     if (user_input == 'q') {
       print_msg("Exiting session ....",60);
       exit(1337);
     }
  }
  exit(1337);
}

Nothing very interesting if the user inputs h or q, but if he inputs s then we arrive at this function :

void calculate_mob_army(void)
{
  long in_FS_OFFSET;
  int user_input;
  undefined4 local_14;
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  print_msg("Sending mobs...\n",10);
  puts("How much mobs: \n");
  puts("> \n");
  __isoc99_scanf(&DECIMAL_INPUT,&user_input);
  local_14 = 1000;
  ARMY_STRENGTH = 1000 / user_input;
  printf("Total army strength : %d \n",(ulong)ARMY_STRENGTH,1000 % (long)user_input & 0xffffffff);
  print_msg("Reconfiguring mob army...\n ",60);
  puts("Done ! Attack launched !");
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
     __stack_chk_fail();
  }
  return;
}

This function is doing an arithmetic operation using the user input, and saves the result in the ARMY_STRENGTH global variable. We can easily get a SIGFPE using this line of code :

  ARMY_STRENGTH = 1000 / user_input;

Finally, this environment variable BCKDR seems interesting, but it isn’t used afterwards in the rest of the code we found. By looking for references to this variable in the rest of the code, we can find this function :

void pas_envie_de_t_analyser(void)
{
  ulong __len;
  size_t sVar1;
  undefined *__src;
  code *pcVar2;
  undefined *puVar3;
  long in_FS_OFFSET;
  undefined auStack_108 [8];
  ulong local_100;
  char *local_f8;
  ulong local_f0;
  undefined8 local_e8;
  undefined *local_e0;
  code *local_d8;
  code *local_d0;
  undefined8 local_c8;
  undefined8 local_c0;
  undefined8 local_b8;
  undefined8 local_b0;
  undefined8 local_a8;
  undefined8 local_a0;
  undefined8 local_98;
  undefined8 local_90;
  undefined8 local_88;
  undefined8 local_80;
  undefined8 local_78;
  undefined8 local_70;
  undefined8 local_68;
  undefined8 local_60;
  undefined8 local_58;
  undefined8 local_50;
  undefined local_48;
  long local_40;
  
  local_40 = *(long *)(in_FS_OFFSET + 0x28);
  local_c8 = 0xe809a6fd21ac5409;
  local_c0 = 0x6963df4161741a59;
  local_b8 = 0x469e424604e6174;
  local_b0 = 0x650f54f9216c6541;
  local_a8 = 0x216c65416ecd696c;
  local_a0 = 0xb34521a4ec099e45;
  local_98 = 0x5534b14c7ae66373;
  local_90 = 0x9624a6be29b29624;
  local_88 = 0x29971c6c9cc229bd;
  local_80 = 0x54099e8b9693a286;
  local_78 = 0x46d32d179e45219a;
  local_70 = 0x361c1a434a2f0816;
  local_68 = 0x604ef82c52063a15;
  local_60 = 0x69696a41617455d4;
  local_58 = 0x3618520a5a3e5c31;
  local_50 = 0x651250135c1a0d77;
  local_48 = 10;
  local_f8 = getenv("BCKDR");
  local_f0 = 0x81;
  local_e8 = 0x80;
  for (puVar3 = auStack_108; puVar3 != auStack_108; puVar3 = puVar3 + -0x1000) {
     *(undefined8 *)(puVar3 + -8) = *(undefined8 *)(puVar3 + -8);
  }
  *(undefined8 *)(puVar3 + -8) = *(undefined8 *)(puVar3 + -8);
  __len = local_f0;
  local_e0 = puVar3 + -0x90;
  for (local_100 = 0; local_100 < local_f0; local_100 = local_100 + 1) {
     puVar3[local_100 - 0x90] = *(byte *)((long)&local_c8 + local_100) ^ local_f8[local_100 % 6];
  }
  *(undefined8 *)(puVar3 + -0x98) = 0x10168f;
  pcVar2 = (code *)mmap((void *)0x0,__len,7,0x22,-1,0);
  __src = local_e0;
  sVar1 = local_f0;
  local_d8 = pcVar2;
  if (pcVar2 == (code *)0xffffffffffffffff) {
     *(undefined8 *)(puVar3 + -0x98) = 0x1016af;
     perror("mmap");
  }
  else {
     *(undefined8 *)(puVar3 + -0x98) = 0x1016d1;
     memcpy(pcVar2,__src,sVar1);
     pcVar2 = local_d8;
     local_d0 = local_d8;
     *(undefined8 *)(puVar3 + -0x98) = 0x1016ed;
     (*pcVar2)();
     pcVar2 = local_d8;
     sVar1 = local_f0;
     *(undefined8 *)(puVar3 + -0x98) = 0x101706;
     munmap(pcVar2,sVar1);
  }
  if (local_40 == *(long *)(in_FS_OFFSET + 0x28)) {
     return;
  }
  __stack_chk_fail();
}

By looking for references to this function we can find :

void __libc_csu_fini(void)
{
  if (ARMY_STRENGTH < 1) {
     pas_envie_de_t_analyser();
  }
  return;
}

So we can run pas_envie_de_t_analyser when the program is shutting down, by setting a negative value to ARMY_STRENGTH in calculate_mob_army. Now let’s try and analyse dynamically pas_envie_de_t_analyser.

Analyse dynamique

The function pas_envie_de_t_analyser is using the environment variable BCKDR :

local_f8 = getenv("BCKDR");

Let’s start by extracting BCKDR :

$ nc 51.20.123.75 1337
              C0d3_Ly0k0                
                                        
                  &&@&                  
                 %@&@@@                 
                 /@%%@(                 
                 /@  @(                 
              *&@@@@@@@@%,              
          @@&             .@@&          
       &@,       ./../.       %@/       
     /@,    .@@*        *@@     #@.     
    %@    ,@/              (@    .@,    
   ,@    /@     *@@&&@@      @,   (@    
   .*   %@#    #@(@@@@.@     %@@   /    
    (@&  ,@@    .@&#@@*@@     @@,  @@.   
     @@    @&                &@   .@&    
      @#    *@%            %@.    @@     
      /%@. .   *@@@&..&@@@,   . #@*/     
         /@@                 ,@@.        
            .@@@@*.&&&#.(@@@@            
           ,@ *   @@@@@%   ( @           
          .@ @.    &  %    (# @          
         .@.@,    .@  @.    ##/@         
        .@.@,      @  @      %@#@        
       ,*@,       @  @       %@&        
                  @%@@                  
                  @%@@                  
                  ,%&,                  
######@@@@@@@ X.A.N.A is watching you @@@~####']-[}
XANA Management Console 
> s
Sending mobs...
How much mobs: 

> 

0
62 26 19 22 11 30

As we can see in the source code of handle_sigfpe, the BCKDR variable is xored :

     bckdr = getenv("BCKDR");
     length = strlen(bckdr);
     for (index = 0; (ulong)(long)index < length; index = index + 1) {
       printf("%d ",(ulong)(byte)(bckdr[index] ^ 127));
     }

Let’s decode this variable using Python :

>>> var = "62 26 19 22 11 30"
>>> for i in var.split(" "):
...     print(chr(int(i)^127), end="")
... 
Aelita

We can now set the environment variable BCKDR to run pas_envie_de_t_analyser :

gef➤  file 8f612a248da7de80738a208bed1d4d19a27621adf12a2f0adcf55a05af9f06a0 
Reading symbols from 8f612a248da7de80738a208bed1d4d19a27621adf12a2f0adcf55a05af9f06a0...
(No debugging symbols found in 8f612a248da7de80738a208bed1d4d19a27621adf12a2f0adcf55a05af9f06a0)
gef➤  set env BCKDR=Aelita
gef➤  r
Starting program: /home/coucou/Documents/Sthack_2024/8f612a248da7de80738a208bed1d4d19a27621adf12a2f0adcf55a05af9f06a0 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
              C0d3_Ly0k0                
                                        
                  &&@&                  
                 %@&@@@                 
                 /@%%@(                 
                 /@  @(                 
              *&@@@@@@@@%,              
          @@&             .@@&          
       &@,       ./../.       %@/       
     /@,    .@@*        *@@     #@.     
    %@    ,@/              (@    .@,    
   ,@    /@     *@@&&@@      @,   (@    
   .*   %@#    #@(@@@@.@     %@@   /    
    (@&  ,@@    .@&#@@*@@     @@,  @@.   
     @@    @&                &@   .@&    
      @#    *@%            %@.    @@     
      /%@. .   *@@@&..&@@@,   . #@*/     
         /@@                 ,@@.        
            .@@@@*.&&&#.(@@@@            
           ,@ *   @@@@@%   ( @           
          .@ @.    &  %    (# @          
         .@.@,    .@  @.    ##/@         
        .@.@,      @  @      %@#@        
       ,*@,       @  @       %@&        
                  @%@@                  
                  @%@@                  
                  ,%&,                  
######@@@@@@@ X.A.N.A is watching you @@@~####']-[}
XANA Management Console 
> s
Sending mobs...
How much mobs: 

> 

-1
Total army strength : -1000 
Reconfiguring mob army...
 Done ! Attack launched !
test
[Inferior 1 (process 9883) exited normally]

The program asks the user for another input. By exploring the code, we understand that this input is asked inside a shellcode executed at line 83 of the pas_envie_de_t_analyser function :

(*pcVar2)();

That we can now find inside GDB :

──── code:x86:64 ────
   0x5555555556d8                  mov    QWORD PTR [rbp-0xc8], rax
   0x5555555556df                  mov    rdx, QWORD PTR [rbp-0xc8]
   0x5555555556e6                  mov    eax, 0x0
●→ 0x5555555556eb                  call   rdx
   0x5555555556ed                  mov    rdx, QWORD PTR [rbp-0xe8]
   0x5555555556f4                  mov    rax, QWORD PTR [rbp-0xd0]
   0x5555555556fb                  mov    rsi, rdx
   0x5555555556fe                  mov    rdi, rax
   0x555555555701                  call   0x555555555240 <munmap@plt>
──── arguments (guessed) ────
*0x7ffff7fc2000 (
   $rdi = 0x00007ffff7fc2000 → 0x8d48c78948c03148,
   $rsi = 0x00007fffffffd6d0 → 0x8d48c78948c03148,
   $rdx = 0x00007ffff7fc2000 → 0x8d48c78948c03148
)
gef➤  x/34i 0x7ffff7fc2000
   0x7ffff7fc2000:	xor    rax,rax
   0x7ffff7fc2003:	mov    rdi,rax
   0x7ffff7fc2006:	lea    rsi,[rip+0x73]        # 0x7ffff7fc2080
   0x7ffff7fc200d:	mov    edx,0xf
   0x7ffff7fc2012:	syscall
   0x7ffff7fc2014:	lea    rbx,[rip+0x65]        # 0x7ffff7fc2080
   0x7ffff7fc201b:	lea    rsi,[rip+0x4e]        # 0x7ffff7fc2070
   0x7ffff7fc2022:	mov    ecx,0xf
   0x7ffff7fc2027:	xor    rdi,rdi
   0x7ffff7fc202a:	mov    rax,rcx
   0x7ffff7fc202d:	xor    rdx,rdx
   0x7ffff7fc2030:	xor    al,BYTE PTR [rsi]
   0x7ffff7fc2032:	mov    dl,BYTE PTR [rbx]
   0x7ffff7fc2034:	cmp    al,dl
   0x7ffff7fc2036:	jne    0x7ffff7fc2068
   0x7ffff7fc2038:	inc    rsi
   0x7ffff7fc203b:	inc    rbx
   0x7ffff7fc203e:	dec    rcx
   0x7ffff7fc2041:	cmp    rcx,0x0
   0x7ffff7fc2045:	jne    0x7ffff7fc202a
   0x7ffff7fc2047:	mov    rdi,0xffffffffffffffff
   0x7ffff7fc204e:	xor    rsi,rsi
   0x7ffff7fc2051:	xor    rdi,rdi
   0x7ffff7fc2054:	push   rsi
   0x7ffff7fc2055:	movabs rdi,0x68732f2f6e69622f
   0x7ffff7fc205f:	push   rdi
   0x7ffff7fc2060:	push   rsp
   0x7ffff7fc2061:	pop    rdi
   0x7ffff7fc2062:	push   0x3b
   0x7ffff7fc2064:	pop    rax
   0x7ffff7fc2065:	cdq
   0x7ffff7fc2066:	syscall
   0x7ffff7fc2068:	mov    eax,0x3c
   0x7ffff7fc206d:	syscall

By dynamically exploring this shellcode, we find out that the first syscall is asking for the input, which is then compared to something else in these instructions :

   0x7ffff7fc2030:	xor    al,BYTE PTR [rsi]
   0x7ffff7fc2032:	mov    dl,BYTE PTR [rbx]
   0x7ffff7fc2034:	cmp    al,dl

If the bytes being compared are different, we go to another syscall that will shut down the program :

   0x7ffff7fc2036:	jne    0x7ffff7fc2068
   0x7ffff7fc2068:	mov    eax,0x3c
   0x7ffff7fc206d:	syscall

Otherwise, the algorithm repeats itself with the next bytes. To extract the string to which our input is being compared, we can simply modify this instruction 0x7ffff7fc2036: jne 0x7ffff7fc2068 into a je, and then get the string bytes by bytes when it’s in memory : 0x7ffff7fc2034: cmp al,dl. This step could be scripted, but the password isn’t very long :

──── code:x86:64 ────
 → 0x7ffff7fc2000                  xor    rax, rax
   0x7ffff7fc2003                  mov    rdi, rax
   0x7ffff7fc2006                  lea    rsi, [rip+0x73]        # 0x7ffff7fc2080
   0x7ffff7fc200d                  mov    edx, 0xf
   0x7ffff7fc2012                  syscall 
   0x7ffff7fc2014                  lea    rbx, [rip+0x65]        # 0x7ffff7fc2080
──── threads ────
[#0] Id 1, Name: "8f612a248da7de8", stopped 0x7ffff7fc2000 in ?? (), reason: SINGLE STEP
gef➤  set {char}0x7ffff7fc2036=0x74
gef➤  br *0x7ffff7fc2034
Breakpoint 10 at 0x7ffff7fc2034
gef➤  c
Continuing.
AAAA
──── code:x86:64 ────
   0x7ffff7fc202d                  xor    rdx, rdx
   0x7ffff7fc2030                  xor    al, BYTE PTR [rsi]
   0x7ffff7fc2032                  mov    dl, BYTE PTR [rbx]
●→ 0x7ffff7fc2034                  cmp    al, dl
   0x7ffff7fc2036                  je     0x7ffff7fc2068
   0x7ffff7fc2038                  inc    rsi
   0x7ffff7fc203b                  inc    rbx
   0x7ffff7fc203e                  dec    rcx
   0x7ffff7fc2041                  cmp    rcx, 0x0
──── threads ────
[#0] Id 1, Name: "8f612a248da7de8", stopped 0x7ffff7fc2034 in ?? (), reason: BREAKPOINT
gef➤  p $rax
$10 = 0x4a
gef➤  c
Continuing.
──── code:x86:64 ────
   0x7ffff7fc202d                  xor    rdx, rdx
   0x7ffff7fc2030                  xor    al, BYTE PTR [rsi]
   0x7ffff7fc2032                  mov    dl, BYTE PTR [rbx]
●→ 0x7ffff7fc2034                  cmp    al, dl
   0x7ffff7fc2036                  je     0x7ffff7fc2068
   0x7ffff7fc2038                  inc    rsi
   0x7ffff7fc203b                  inc    rbx
   0x7ffff7fc203e                  dec    rcx
   0x7ffff7fc2041                  cmp    rcx, 0x0
──── threads ────
[#0] Id 1, Name: "8f612a248da7de8", stopped 0x7ffff7fc2034 in ?? (), reason: BREAKPOINT
gef➤  p $rax
$11 = 0x33

By repeating this procedure, we get :

>>> print(solu)
[74, 51, 114, 51, 109, 49, 101, 95, 49, 110, 115, 49, 100, 51, 82]
>>> for i in range(len(solu)):
...     solu[i] = chr(int(solu[i]))
... 
>>> print("".join(solu))
J3r3m1e_1ns1d3R

Exploit

Let’s try the password :

$ nc 51.20.123.75 1337
              C0d3_Ly0k0                
                                        
                  &&@&                  
                 %@&@@@                 
                 /@%%@(                 
                 /@  @(                 
              *&@@@@@@@@%,              
          @@&             .@@&          
       &@,       ./../.       %@/       
     /@,    .@@*        *@@     #@.     
    %@    ,@/              (@    .@,    
   ,@    /@     *@@&&@@      @,   (@    
   .*   %@#    #@(@@@@.@     %@@   /    
    (@&  ,@@    .@&#@@*@@     @@,  @@.   
     @@    @&                &@   .@&    
      @#    *@%            %@.    @@     
      /%@. .   *@@@&..&@@@,   . #@*/     
         /@@                 ,@@.        
            .@@@@*.&&&#.(@@@@            
           ,@ *   @@@@@%   ( @           
          .@ @.    &  %    (# @          
         .@.@,    .@  @.    ##/@         
        .@.@,      @  @      %@#@        
       ,*@,       @  @       %@&        
                  @%@@                  
                  @%@@                  
                  ,%&,                  
######@@@@@@@ X.A.N.A is watching you @@@~####']-[}
XANA Management Console 
> s
Sending mobs...
How much mobs: 

> 

-1
Total army strength : -1000 
Reconfiguring mob army...
 Done ! Attack launched !
J3r3m1e_1ns1d3R
whoami
ctf
ls
chall
flag.txt
cat flag.txt
STHACK{X@n4_PwN3d_F0r3v3r}