Exfilter

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:

Save 51% on Zhmyshenko Valery Albertovich on Steam
You play for an old bivas Valera, and you need to collect baby bons, beers and run away from guys from the club boys

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