Logo Peter

PwnMeCTF 2025

March 3, 2025
8 min read
Table of Contents

Super secure network

Of all the reverse challenges, I like this challenge the most not because it’s the hardest but it made me feel really interesting when doing it. About the challenge, we were given two files capture.pcapng and secure_network.ko.

Open IDA start analysis the kernel module file. This is init_module function

__int64 init_module()
{
  __int64 v1[5]; // [rsp+0h] [rbp-58h] BYREF
  __int64 n2; // [rsp+28h] [rbp-30h] BYREF
  __int64 v3; // [rsp+30h] [rbp-28h] BYREF
  int n17301536; // [rsp+3Ch] [rbp-1Ch]
  __int64 v5; // [rsp+40h] [rbp-18h]
  __int64 v6; // [rsp+48h] [rbp-10h]
  unsigned int v7; // [rsp+54h] [rbp-4h]
 
  v7 = 0;
  v6 = 0LL;
  l1111l11l11l111l = l111lll11l1ll11l(60LL);
  get_random_bytes(&lll1ll1l111l111l, 8LL);
  n2 = 2LL;
  v1[4] = 1LL;
  v1[1] = (__int64)&l1111l11l11l111l;
  v1[2] = (__int64)&n2;
  v3 = lll1l11l1l1l1111(2LL, lll1ll1l111l111l, l1111l11l11l111l);
  v1[0] = (__int64)&v3;
  v1[3] = 0x800000008LL;
  v7 = crypto_dh_key_len(v1);
  if ( v7 )
  {
    v5 = (int)v7;
    n17301536 = 17301536;
    v6 = _kmalloc((int)v7, 17301536LL);
    if ( v6 )
    {
      if ( !(unsigned int)crypto_dh_encode_key(v6, v7, v1) && (int)lll1llll111ll1l1(3232235836LL, 3333LL) >= 0 )
      {
        psub_E0A = (__int64)sub_E0A;
        n2 = 2;
        dword_B5DC = 0;
        dword_B5E0 = 0x80000000;
        psub_104E = (__int64)sub_104E;
        n2_0 = 2;
        n4 = 4;
        dword_B620 = 0x80000000;
        if ( !(unsigned int)nf_register_net_hook(&init_net, &psub_E0A)
          && !(unsigned int)nf_register_net_hook(&init_net, &psub_104E) )
        {
          llll1l1ll1lll1l1(l1ll111lll11lll1, v6, v7);
        }
      }
    }
  }
  return 0LL;
}

A bunch of nonsense function which contains l1, maybe the author tried to obfuscate it ? LOL. Besides that, a lot of linux kernel APIs are called, which is the most fun part when you need to dig down source code and read more about those APIs.

Open linux source code, and read about two this crypto_dh_key_len and crypto_dh_encode_key APIs.

https://github.com/torvalds/linux/blob/03d38806a902b36bf364cae8de6f1183c0a35a67/include/crypto/dh.h#32

As you can see, IDA Pseudocode doesn’t recognise the struct dh

struct dh {
	const void *key;
	const void *p;
	const void *g;
	unsigned int key_size;
	unsigned int p_size;
	unsigned int g_size;
};

We can add that struct by hand but more convenient way (it helps us to define another struct in one time) is using Linux Kernel TIL. Back to challenge after adding the struct and reverse more we got this beauty.

int init_module(void)
{
  dh_lmao params_; // [rsp+0h] [rbp-58h] BYREF
  __int64 n2; // [rsp+28h] [rbp-30h] BYREF
  __int64 v3; // [rsp+30h] [rbp-28h] BYREF
  gfp_t flags; // [rsp+3Ch] [rbp-1Ch]
  size_t size; // [rsp+40h] [rbp-18h]
  char *buf; // [rsp+48h] [rbp-10h]
  unsigned int len; // [rsp+54h] [rbp-4h]
 
  params_.p_size = 8;
  *(&params_.g_size + 1) = 0;
  len = 0;
  buf = 0LL;
  sixty_prime_value = cal_mul_of_x_prime(60);
  get_random_bytes(&random_8bytes, 8uLL);
  n2 = 2LL;
  params_.g_size = 1;
  params_.p = &sixty_prime_value;
  params_.g = &n2;
  v3 = binary_exponentiation(2uLL, random_8bytes, sixty_prime_value);
  params_.key = &v3;
  params_.key_size = 8;
  len = crypto_dh_key_len((const dh *)&params_);
  if ( len )
  {
    size = (int)len;
    flags = 0x1080020;
    buf = (char *)_kmalloc((int)len, 0x1080020u);
    if ( buf )
    {
      if ( !crypto_dh_encode_key(buf, len, (const dh *)&params_) && (int)create_tcp_connect(0xC0A8013C, 3333) >= 0 )// 192.168.1.60
      {
        psub_E0A = (__int64)recv_;
        ::n2 = 2;
        dword_B5DC = 0;
        dword_B5E0 = 0x80000000;
        psub_104E = (__int64)send_;
        n2_0 = 2;
        n4 = 4;
        dword_B620 = 0x80000000;
        if ( !(unsigned int)nf_register_net_hook(&init_net, &psub_E0A)
          && !(unsigned int)nf_register_net_hook(&init_net, &psub_104E) )
        {
          send_msg_kernel(sock, buf, len);
        }
      }
    }
  }
  return 0;
}

The kernel module will hook 2 function send_ and recv which will handle the TCP communicate with 192.168.1.60:3333. Before that let see the DH part. It will get 8 bytes random than get mul of x prime number as p

unsigned __int64 __fastcall cal_mul_of_x_prime(char n60)
{
  unsigned int v2; // [rsp+Ch] [rbp-Ch]
  unsigned __int64 n2; // [rsp+10h] [rbp-8h]
  unsigned __int64 n2a; // [rsp+10h] [rbp-8h]
 
  do
  {
    n2 = 1LL;
    while ( !(n2 >> n60) )
    {
      v2 = call_random_get_prime();
      if ( !check_mul_valid(n2, v2) )
        n2 *= v2;
    }
    n2a = n2 + 1;
  }
  while ( !(unsigned int)check_prime_number(n2a) );
  return n2a;
}

Key will be result of binary_exponentiation(2, random_8bytes, sixty_prime_value);. Back to the pcapng file and first packet that send to 192.168.1.60:3333

010021000800000008000000010000005c4f31a50c2d980c6b152f4845212e1502

This is struct dh we can rewrite this as.

#include <stdio.h>
#include "defs.h"
 
struct dh {
	const void *key;
	const void *p;
	const void *g;
	unsigned int key_size;
	unsigned int p_size;
	unsigned int g_size;
};
 
int main() {
    struct dh params_;
    params_.key = 0xc982d0ca5314f5c; // binary_exponentiation(2uLL, random_8bytes, sixty_prime_value);
    params_.p = 0x152e2145482f156b; // sixty_prime_value
    params_.g = 2;
    params_.key_size = 8;
    params_.p_size = 8;
    params_.g_size = 1;
    // random_8bytes = 0x1275e27a22626694
}

We need to caculate random_8bytes for after use which is 0x1275e27a22626694.

__int64 __fastcall recv_(__int64 rdi0, sk_buff *skb)
{
  int v2; // eax
  int flags; // edx
  int node; // ecx
  void *v5; // rax
  int v7; // [rsp+1Ch] [rbp-4Ch] BYREF
  sk_buff *dest; // [rsp+20h] [rbp-48h]
  unsigned __int8 *v9; // [rsp+28h] [rbp-40h]
  int v10; // [rsp+34h] [rbp-34h]
  __int64 v11; // [rsp+38h] [rbp-30h]
  sk_buff *skba; // [rsp+40h] [rbp-28h]
  unsigned int len; // [rsp+4Ch] [rbp-1Ch]
  unsigned __int8 *dest_1; // [rsp+50h] [rbp-18h]
  unsigned __int8 *v15; // [rsp+58h] [rbp-10h]
 
  dest_1 = 0LL;
  len = 0;
  skba = 0LL;
  v11 = 0LL;
  v7 = 0;
  v15 = HIWORD__(skb);
  if ( v15 )
  {
    v10 = *(_DWORD *)&skb->mac_len;
    v9 = LOWORD_(skb);
    dest = (sk_buff *)&v15[4 * (v15[12] >> 4)];
    dest_1 = LODWORD_(skb);
    len = (_DWORD)dest_1 - (_DWORD)dest;
    if ( (_DWORD)dest_1 != (_DWORD)dest )
    {
      v2 = *(_DWORD *)((char *)dest + len - 4);
      if ( v2 == 0x86E35DE5 )
      {
        aes_enc_init((unsigned __int64 *)dest);
      }
      else if ( v2 == 0x89E35DE5 )
      {
        if ( check_val_if_aes_alloc )
        {
          len -= 4;
          aes_maybe((char *)dest, len - 16, (char *)dest + len - 16);
          len -= 16;
          skba = _alloc_skb_(len + 96, 0x1080020u, flags, node);
          if ( skba )
          {
            sub_123(skba, 96);
            v5 = skb_put(skba, len);
            csum_partial_copy_from_user(dest, v5, len, 0LL, &v7);
            LOWORD(skba->tail) = 8;
            HIWORD(skba->tail) = 0;
            HIWORD(skba->end) = 0;
            sub_1C5(skba);
            skba->dev_scratch = skb->dev_scratch;
            v11 = sub_1FC(skba);
            if ( v11 )
              netif_rx(skba);
          }
        }
      }
    }
  }
  return 1LL;
}

This function check the last 4 bytes packet data if it is 0x86E35DE5 (init AES CTR), 0x89E35DE5 (take next 16 bytes as nonce, decrypt the data).

void *__fastcall aes_enc_init(unsigned __int64 *dest)
{
  void *result; // rax
  __int64 v2; // [rsp+8h] [rbp-30h] BYREF
  __int64 s[4]; // [rsp+10h] [rbp-28h] BYREF
  unsigned __int64 n2; // [rsp+30h] [rbp-8h]
 
  memset(s, 0, sizeof(s));
  v2 = 0LL;
  n2 = *dest;
  v2 = binary_exponentiation(n2, random_8bytes, sixty_prime_value);
  result = (void *)alloc_sha256((__int64)&v2, 8u, (__int64)s);
  if ( !(_DWORD)result )
  {
    aes_alloc = alloc_aes((__int64)"aes", 4, 128);
    sub_2AE(aes_alloc, (__int64)s, 32u);
    result = memset(s, 0, sizeof(s));
    check_val_if_aes_alloc = 1;
  }
  return result;
}

The key for AES CTR will be caculate sha256(binary_exponentiation(n2, random_8bytes, sixty_prime_value)) : bf98795c03cdbce091b8986cc599628ea6ba2d91b8e50443c4bb144477ce982b.

unsigned __int64 __fastcall aes_maybe(char *dest, unsigned __int64 i, char *a3)
{
  char v4[16]; // rdx
  unsigned __int64 j_1; // rax
  char sht[16]; // [rsp+18h] [rbp-30h] BYREF
  char wtf[16]; // [rsp+28h] [rbp-20h] BYREF
  unsigned __int64 k; // [rsp+38h] [rbp-10h]
  unsigned __int64 j; // [rsp+40h] [rbp-8h]
 
  memset(wtf, 0, sizeof(wtf));
  memset(sht, 0, sizeof(sht));
  k = 0LL;
  *(_QWORD *)v4 = *((_QWORD *)a3 + 1);
  *(_QWORD *)sht = *(_QWORD *)a3;
  *(_QWORD *)&sht[8] = *(_QWORD *)v4;
  for ( j = 0LL; ; j += 16LL )
  {
    j_1 = j;
    if ( j >= i )
      break;
    sub_2FB(aes_alloc, (__int64)wtf, (__int64)sht);
    for ( k = 0LL; k <= 15 && i > j + k; ++k )
      dest[k + j] ^= wtf[k];
    for ( k = 15LL; !++sht[k]; --k )
      ;
  }
  return j_1;
}

At first when did this challenge I suffer a lot with recognise the AES MOD because CTR is rarely use 16 bytes nonce and the crypto_alloc_base didn’t reveal anything too. Afterall, when trying to decrypt it on cyberchef with mode AES CTR and 16 bytes nonces. I finally got it.

After asking my teammate, he gave a way to decrypt it in python. Final script to decrypt all the traffic data.

import pyshark
from pwn import *
from Crypto.Cipher import AES
from Crypto.Util import Counter
 
cap = pyshark.FileCapture('capture.pcapng', display_filter='data.len > 0')
key = bytes.fromhex("bf98795c03cdbce091b8986cc599628ea6ba2d91b8e50443c4bb144477ce982b")
 
for packet in cap:
    data_packet = bytes.fromhex(packet.data.data)
    if u32(data_packet[-4:]) == 0x89E35DE5 or u32(data_packet[-4:]) == 0x89E35BE5:
        cipher = data_packet
        nonce = cipher[-20:-4]
        ct = cipher[:-20]
        cipher = AES.new(key, AES.MODE_CTR, counter=Counter.new(128, initial_value=int.from_bytes(nonce, 'big')))
        decrypted = cipher.decrypt(ct)
        print(decrypted)
        print("----")

Flag: PWNME{Crypt0_&_B4ndwidth_m4k3s_m3_f33l_UN83474813!!!}