LeHACK 2025 - Hidden
Solve for the easiest LeHACK 2025 challenge... and even a lil bit more
Intro
LeHACK is a hacking conference held every year in Paris. At the end of the event, a CTF named “Wargame” is organized and lasts all night long.
During the 2025 LeHACK Wargame, the goal of the RE challenge named “Hidden” was to understand a cypher function stored in a dynamically-linked library.
The Write-up
The binary is a 64-bits stripped ELF named hidden.bin
.
To begin the investigation, I launched the automatics analysis with radare2 and then disassembled the function identified as the main
. Some call
instructions retained my attention.
1
2
3
4
0x000013f4 ff5510 call qword [rbp + 0x10]
0x000013f7 4989c4 mov r12, rax
0x000013fa 4889c7 mov rdi, rax
0x000013fd ff5508 call qword [rbp + 8]
I simply set a breakpoint at one of these call
instructions, and when I jumped in the function, GDB printed the name and the path of a dynamically-linked libray used by hidden.bin
.
1
2
3
4
5
6
7
(gdb) b* 0x1000013fd
Breakpoint 1 at 0x1000013fd
(gdb) r
...
Breakpoint 1, 0x00000001000013fd in ?? ()
(gdb) si
0x00007ffff7fba160 in check () from /tmp/hidden.so
After copying hidden.so
in my current working directory, I started the investigation.
In the terminal
1
user_one@machine:hidden $ r2 -AA ./hidden.so
In radare2
1
2
3
4
5
6
7
8
9
10
11
12
13
[0x000010a0]> afl
0x00001030 1 6 sym.imp.strncpy
0x00001040 1 6 sym.imp.strlen
0x00001050 1 6 sym.imp.strchr
0x00001060 1 6 sym.imp.memcmp
0x00001070 1 6 sym.imp.fgets
0x00001080 1 6 sym.imp.fwrite
0x00001090 1 6 sym.imp.__cxa_finalize
0x000010a0 4 34 entry0
0x000013a0 3 106 sym.input
0x00001160 27 538 sym.check
0x00001150 5 56 entry.init0
0x00001110 5 50 entry.fini0
Obviously, the check
function is what I investigated first. To do it, I exported the CFG representation of this function in GraphViz DOT before generating a PNG file from it.
In radare2
1
2
[0x000010a0]> s sym.check
[0x00001160]> agfd > check_cfg.dot
In the terminal
1
user_one@machine:hidden $ dot check_cfg.dot -T png -o check_cfg.png
The swap
Firstly we can identify a switch case
implemented as an if
-cascade by the compiler.
Once reversed we can conclude that this switch case
swaps some ASCII characters following this “swap table”:
Character A | Character B |
---|---|
_ | / |
? | ! |
# | @ |
To be honest, when I reversed all of of this, I firstly believed that the swap was only operated on a copy of the user input, which made me lost a lot of time (HOURS).
The XOR loop
After, we can see that several registers are initialized with some arbitrary values and the datas are then passed through a loop.
At each round, a character of the user input (pointed by rcx
) is xored by the value in al
. Then rcx
is incremented, which means it now point to the next character of the user input. rsi
is then set to 1-&userInput+(&userInput+i)
where i
is the number of rounds. This can be simplified to rsi = 1+i
.
The basic block starting at the address 0x12a8
contains a tricky way to perfom a modulo with the length of the key.
I still don’t fully understand the whole trick. Furthermore, I’m not able to say if it’s the result of an obfuscation technique or any form of compiler optimization. If you are able to explain all the details, feel free to rise an issue.
To sum it up, this loop simply XOR all the datas by hiDd3n
, which is the key.
Preparation of the stack for comparison
The last basic block seems a bit complicated to analyse because of the use of the xmm0
register for example. But in fact, this register act as a normal register in our case. The whole block simply puts values on the stack:
rsp | 0xa11062e41462b1d462c51574b132600 ( =xmm0 ) |
rsp+0x10 | 0xc1c370d215f4177 ( =rax ) |
rsp+0x18 | 0x4b2f2627 |
rsp+0x1c | 0x12 |
You can check this by setting up a breakpoint at the first instruction of the last basic block and then analyse the stack as follows:
1
2
3
4
5
(gdb) x/29x $rsp
0x7fffffffeb50: 0x00 0x26 0x13 0x4b 0x57 0x51 0x2c 0x46
0x7fffffffeb58: 0x1d 0x2b 0x46 0x41 0x2e 0x06 0x11 0x0a
0x7fffffffeb60: 0x77 0x41 0x5f 0x21 0x0d 0x37 0x1c 0x0c
0x7fffffffeb68: 0x27 0x26 0x2f 0x4b 0x12
The same opeartion can be done with the x/29bx $rsi
command if you break at the memcmp
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(gdb) x/10i $rip
=> 0x7ffff7fba30d <check+429>: call 0x7ffff7fba060 <memcmp@plt>
0x7ffff7fba312 <check+434>: add rsp,0x120
0x7ffff7fba319 <check+441>: pop rbx
0x7ffff7fba31a <check+442>: pop rbp
0x7ffff7fba31b <check+443>: pop r12
0x7ffff7fba31d <check+445>: ret
0x7ffff7fba31e <check+446>: xchg ax,ax
0x7ffff7fba320 <check+448>: mov BYTE PTR [rax],0x2f
0x7ffff7fba323 <check+451>: add rax,0x1
0x7ffff7fba327 <check+455>: cmp rax,rdi
(gdb) x/29bx $rsi
0x7fffffffeb50: 0x00 0x26 0x13 0x4b 0x57 0x51 0x2c 0x46
0x7fffffffeb58: 0x1d 0x2b 0x46 0x41 0x2e 0x06 0x11 0x0a
0x7fffffffeb60: 0x77 0x41 0x5f 0x21 0x0d 0x37 0x1c 0x0c
0x7fffffffeb68: 0x27 0x26 0x2f 0x4b 0x12
The last part with
xmm0
and the complicated instructions was reversed for me by Ghidra. I only understood this part completely once at home.
After having extracted these values, you can the reverse the cypher.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>
int main(){
unsigned char ciphered[0x1d] = {
0x00,0x26,0x13,0x4b,0x57,0x51,0x2c,0x46,
0x1d,0x2b,0x46,0x41,0x2e,0x06,0x11,0x0a,
0x77,0x41,0x5f,0x21,0x0d,0x37,0x1c,0x0c,
0x27,0x26,0x2f,0x4b,0x12};
unsigned char key[7] = "hiDd3n";
for(int i=0; i<0x1d; i++){
ciphered[i] = ciphered[i]^key[i%6];
switch (ciphered[i]) {
case '_':
ciphered[i] = '/';
break;
case '/':
ciphered[i] = '_';
break;
case '+':
ciphered[i] = '-';
break;
case '-':
ciphered[i] = '+';
break;
case '?':
ciphered[i] = '!';
break;
case '!':
ciphered[i] = '?';
break;
case '#':
ciphered[i] = '@';
break;
case '@':
ciphered[i] = '#';
break;
}
}
printf("%s\n",ciphered);
return 0;
}
Once this code compiled and executed you obtain hOW_d!D_YOu_FoUnD_7HIS_bOOk_?
, the complete flag is leHACK{hOW_d!D_YOu_FoUnD_7HIS_bOOk_?}
.
But how the hell this libray appeared in my system ??
With @remsflems, we were surprised that a library had appeared in our system and asked ourselves how it could happen. My theory was that one of the functions called before main
had been modified.
First let’s try to find the first hidden.so
appearance during the execution.
Command
1
2
strace -o strace.out ./hidden.bin
cat strace.out | grep hidden.so
Output
1
2
3
...
unlink("/tmp/hidden.so")
...
Then I looked to the syscalls
executed around:
Command
1
cat strace.out | grep -B 3 -A 3 ./hidden.so
Output
1
2
3
4
5
6
7
8
9
10
...
mprotect(0x681ad7526000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=16384*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x681ad74b3000, 226179) = 0
unlink("/tmp/hidden.so") = -1 ENOENT (Aucun fichier ou dossier de ce nom)
openat(AT_FDCWD, "/tmp/hidden.so", O_RDWR|O_CREAT, 0777) = 3
write(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\240\20\0\0\0\0\0\0"..., 14296) = 14296
close(3) = 0
readlink("/proc/self/exe", "/home/user_one/ctf/lehack/wargam"..., 4095) = 55
...
My job now will be to find the functions that are susceptible to make these syscalls
in one of the functions called before main
. In radare2 we can see entry.init0
and entry.init1
.
1
2
3
4
5
[0x00001570]> afl
...
0x00001560 5 56 entry.init0
0x000011b0 9 464 entry.init1
...
As you can see the entry.init0
function is really short and, believe me, there is nothing interesting in it. Unlikely, entry.init1
seems REAAAALLY interesting once disassembled.
I do not put the entire disassembly here because it would be too long.
We can for example see calls to the open
, write
, close
and readlink
functions. Something else that is really interesting is that before a large amount of these calls, a custom function (labelled fcn.00001570
by radare2) is called.
Example
1
2
3
4
call fcn.00001570
mov rdi, rbp
mov rsi, rax
call sym.imp.strcmp
Here you can see the whole disassembly of this function:
1
2
3
4
5
6
7
8
9
10
11
12
13
0x00001570 4989f8 mov r8, rdi
0x00001573 85f6 test esi, esi
┌─< 0x00001575 7e1c jle 0x1593
│ 0x00001577 8d4eff lea ecx, [rsi - 1]
│ 0x0000157a 4889f8 mov rax, rdi
│ 0x0000157d 488d4c0f01 lea rcx, [rdi + rcx + 1]
| 0x00001582 660f1f440000 nop word [rax + rax]
│┌> 0x00001588 3010 xor byte [rax], dl
│╎ 0x0000158a 4883c001 add rax, 1
│╎ 0x0000158e 4839c8 cmp rax, rcx
│└< 0x00001591 75f5 jne 0x1588
└─> 0x00001593 4c89c0 mov rax, r8
0x00001596 c3 ret
Once reversed, I conclude that it is a simple xor_by_x
function whose prototype in C could be:
1
char* xor_by_x(char* data, int dataSize, int x);
Let’s check the theory by extracting the bytes xored before the write
function call that seems to write the hidden.so
file in the system.
1
2
3
4
5
6
7
8
9
10
...
lea rdi, str.oUV
mov edx, 0x10
mov esi, 0x37d8
call fcn.00001570
mov edi, ebp
mov edx, 0x37d8
mov rsi, rax
call sym.imp.write
...
We can see that it takes 0x37d8
values stored at a place named str.oUV
by radare2, and then XOR them by 0x10
. Now, I’ve to find the offset where values pointed by str.oUV
are in hidden.bin
:
In radare2
1
2
3
[0x000011b0]> px 10 @ str.oUV
- offset - C0C1 C2C3 C4C5 C6C7 C8C9 CACB CCCD CECF 0123456789ABCDEF
0x000043c0 6f55 5c56 1211 1110 1010 oU\V......
In the terminal
1
2
user_one@machine:hidden $ xxd ./hidden.bin | grep 6f55
000033c0: 6f55 5c56 1211 1110 1010 1010 1010 1010 oU\V............
Lets now write a little bit of code in order to extracts and XOR everything.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>
#include <stdlib.h>
#define LIBOFFSET 0x33c0
#define LIBSIZE 0x37d8
int main(){
FILE *inFile = fopen("./hidden.bin", "rb");
FILE *outFile = fopen("./extracted_lib.so", "wb");
fseek(inFile, LIBOFFSET, SEEK_SET);
unsigned char *libBytes = malloc(LIBSIZE);
fread(libBytes, 1, LIBSIZE, inFile);
for(int i=0; i<LIBSIZE; i++){
libBytes[i] = libBytes[i] ^ 0x10;
}
fwrite(libBytes, 1, LIBSIZE, outFile);
free(libBytes);
fclose(inFile);
fclose(outFile);
return 0;
}
Once the code compiled and executed, we can see that what we extracted is the lib given by the hidden.bin
during the challenge.
1
2
3
4
user_one@machine:hidden $ sha512sum hidden.so
9fd6ba37925801f4abd45dcc2396c534e094ec7a6d3da33b13bc58c967a7ae94e02caf781ad82f6c5d2ccd039840670362c2e369cf2b10d9bc9a8cda84e8393e hidden.so
user_one@machine:hidden $ sha512sum extracted_lib.so
9fd6ba37925801f4abd45dcc2396c534e094ec7a6d3da33b13bc58c967a7ae94e02caf781ad82f6c5d2ccd039840670362c2e369cf2b10d9bc9a8cda84e8393e extracted_lib.so
Solved by W0nda