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}