Exfilter

This was the easiest rev challenge in brics+ CTF, hosted by ITMO in Russia with the support of several (as of the time of writing) US sanctioned Russian companies.
The challenge includes two files, exfilter.ko
, a kernel module, and exfilter_traff.pcapng
, a network capture.
Opening exfilter_traff.pcapng
in wireshark, we see this:

There is a lot of UDP traffic outbound to 8.8.8.8:1337. Each packet has 256 bytes of data which consists of random hex characters. Dumping all the hex characters gives us random garbage, grepping for the flag gives us no results 😦
Let's look at exfilter.ko
instead:
We find that there are four functions:
packet_injector_ioctl
modify_packet_out
mod_init
mod_exit
Of the four, modify_packet_out
seems the most interesting. mod_init
and mod_exit
just loads and kills the kernel module so they aren't very interesting. packet_injector_ioctl
just loads a string from user space to kernel space.
Using IDA, I found that modify_packet_out
has two main sections:
Boring stuff:
__int64 __fastcall modify_packet_out(void *priv, sk_buff *skb, const nf_hook_state *state)
{
__int64 actual_length_2; // r12
unsigned __int8 *ip_header_1; // r15
sk_buff *pakhet; // rbx
size_t actual_length; // rax
__int64 ip_head_offset; // rax
bool probably_false; // zf
unsigned __int8 *ip_header; // rax
size_t actual_length_1; // rax
unsigned __int8 *udp_header; // rax
int udp_length; // r14d
unsigned __int8 udp_data; // r13
unsigned __int8 *v15; // rdx
unsigned __int8 v16; // al
unsigned int v17; // eax
int v18; // ecx
bool v19; // cc
unsigned int v20; // eax
int v21; // ecx
int v22; // ecx
_BOOL4 v23; // ett
bool v24; // cf
int v25; // ecx
_BOOL4 v26; // ett
int v27; // ecx
_BOOL4 v28; // ett
unsigned __int8 *v29; // [rsp-38h] [rbp-38h]
Part I:
_fentry__();
pakhet = skb;
actual_length = strnlen(user_string, 100uLL);
if ( actual_length <= 100 )
{
if ( actual_length != 100 )
{
LODWORD(actual_length_2) = actual_length;
ip_head_offset = skb->network_header;
probably_false = &skb->head[ip_head_offset] == 0;
ip_header = &skb->head[ip_head_offset];
ip_header_1 = ip_header;
if ( probably_false || ip_header[9] != 17 )// check malformed pkt or not udp
return 1LL; // fail
actual_length_1 = strnlen(user_string, 100uLL);
if ( actual_length_1 > 100 )
goto fail;
if ( actual_length_1 != 100 )
{
if ( !actual_length_1 )
return 1LL; // fail
udp_header = &ip_header_1[4 * (*ip_header_1 & 0xF)];
udp_length = (unsigned __int16)__ROL2__(*((_WORD *)udp_header + 2), 8) - 8;
if ( udp_length <= 0 )
return 1LL;
LODWORD(pakhet) = actual_length_2 - 1;
actual_length_2 = 0LL;
pakhet = (sk_buff *)(int)pakhet;
while ( 1 )
{
udp_data = udp_header[actual_length_2 + 8];
if ( (unsigned __int64)(int)pakhet > 99 )// will not run
{
v29 = udp_header;
_ubsan_handle_out_of_bounds(&off_900, (int)pakhet);
udp_header = v29;
}
if ( udp_data == user_string[(int)pakhet] )
break;
if ( udp_length <= (int)++actual_length_2 )
return 1LL;
}
if ( (unsigned __int64)(int)pakhet <= 99 )// always true
goto LABEL_16; // error
goto LABEL_23;
}
}
fortify_panic("__fortify_strlen");
}
fail:
fortify_panic("strnlen");
This part loads a string from userspace. It then uses the built in skb datatype in the linux kernel to load the packet to be processed. Most of the code is just trying to find the UDP data section. It has to parse the IP and UDP headers before it can get to the location of the UDP data. It then iterates through the UDP data byte by byte until it finds a match between the UDP data and the last character of the user provided string.
Part II:
LABEL_23:
_ubsan_handle_out_of_bounds(&off_8E0, pakhet);
LABEL_16:
user_string[(_QWORD)pakhet] = 0;
v15 = ip_header_1;
*((_WORD *)ip_header_1 + 5) = 0;
v16 = *ip_header_1;
ip_header_1[1] = actual_length_2 + 1;
v17 = v16 & 0xF;
v18 = *(_DWORD *)ip_header_1;
v19 = v17 <= 4;
v20 = v17 - 4;
if ( !v19 )
{
v24 = __CFADD__(*((_DWORD *)ip_header_1 + 1), v18);
v21 = *((_DWORD *)ip_header_1 + 1) + v18;
v23 = v24;
v24 = __CFADD__(v24, v21);
v22 = v23 + v21;
v24 |= __CFADD__(*((_DWORD *)ip_header_1 + 2), v22);
v22 += *((_DWORD *)ip_header_1 + 2);
v26 = v24;
v24 = __CFADD__(v24, v22);
v25 = v26 + v22;
v24 |= __CFADD__(*((_DWORD *)ip_header_1 + 3), v25);
v18 = *((_DWORD *)ip_header_1 + 3) + v25;
do
{
v28 = v24;
v24 = __CFADD__(v24, v18);
v27 = v28 + v18;
v24 |= __CFADD__(*((_DWORD *)v15 + 4), v27);
v18 = *((_DWORD *)v15 + 4) + v27;
v15 += 4;
--v20;
}
while ( v20 );
LOWORD(v18) = ~(__CFADD__(v24 + (_WORD)v18, ((unsigned int)v24 + v18) >> 16)
+ v24
+ (_WORD)v18
+ (((unsigned int)v24 + v18) >> 16));
}
*((_WORD *)ip_header_1 + 5) = v18;
return 1LL;
}
I got lazy on this part and just plugged it into chatGPT. It told me that all this code did was fix the IP checksum.
All this time, we see the code reading the packet data, but none of it was changed.
My teammates Dan and Nastia pointed out one line from the second part of the function that I missed:
ip_header_1[1] = actual_length_2 + 1;
This sets the congestion notification part of the IP header to the position in the UDP data that matches the last character of the user provided string.
Knowing this, I wrote a quick and dirty python script that processes the packet data I extracted from Wireshark. I extracted the second byte of the IP header and uses that value to extract another byte from the UDP data.
#! /usr/bin/python3
hex_vals = "abcdef1234567890"
offset = 43
packets = []
with open("fuckyou.txt") as file:
packet = []
for line in file:
parts = line.split()
if parts:
parts = parts[1:]
for hexval in parts:
if all(char in hex_vals for char in hexval):
packet.append(hexval)
else:
packets.append(packet)
packet = []
#print(packets)
packets.append(packet)
hecks = ""
actualstr = ""
for packet in packets:
index = packet[17]
if offset + int(index,16) >= len(packet):
pass
else:
target = packet[offset + int(index,16)]
hecks += str(target)
if chr(int(target,16)) != 'S':
actualstr += chr(int(target,16))
#print(target)
#print(chr(int(target,16)))
print(hecks)
print(actualstr[::-1])
We obtain:
SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS_SSokayS_flSÿg_SisS: bSriScs+S{Sc544SSSdd9dSSSe4f7SSScSS011cSSS9SS04SSSSS795692SSSSdbSSS31e}SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS_SHaSSn_ZAMASSY_SSLASVSAS_KSPSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS_SPoSzSdnySaSkSSoSSSvS_SkSogda_ySaderkSSaSS????SSSSSSSSSSSSSøññ÷:S:9S(SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSW(SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSZSShmSSSysSShSSSSeSSSSSSnSkSo_VSaleSriSSy_ASSSlSbertSSoviSchSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS
That's a lot of S's!
_okay_flÿg_is: brics+{c544dd9de4f7c011c904795692db31e}_Han_ZAMAY_LAVA_KP_Pozdnyakov_kogda_yaderka????øññ÷::9(W(Zhmyshenko_Valeriy_Albertovich
Nice! We got the flag!
I don't know what Han ZAMAY LAVA KP means, but Pozdnyakov kogda yaderka???? translates to "Pozdnyakov when nuclear????"
Pozdnya is a name and could either be referring the leader of the Russian Olympic Committee or a Russian ultranationalist blogger (They have the same name).
Zhmyshenko Valeriy Albertovich is the name of an asset flip game on Steam. You can check it out here:

The main theme song for the game can be found here: