Post

PwnMe CTF 2025 Quals Write Up

PwnMe CTF 2025 Quals Write Up

PwnMe CTF 2025 Quals WU

This is the write up for 4/5 Reverse Engineering challenge (Including the Insange challenge :3)

Back to the past

Challenge Description

Using the provided binary and the encrypted file, find a way to retrieve the flag contained in “flag.enc”. Note that the binary would have been run in May 2024. Note: The flag is in the format PWNME{…}

Author : Fayred

Flag format: PWNME{.........................}

Firstly I used the file on the challenge ELF files and see that this file is statically linked, and not stripped. C1_img1

Then I use IDA to analyze the given executables:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v3; // cl
  int v5; // edx
  char v6; // cl
  int v7; // edx
  char v8; // cl
  int v9; // eax
  char v10; // cl
  int v11; // [rsp+1Ch] [rbp-124h]
  unsigned int SEED; // [rsp+20h] [rbp-120h]
  __int64 FILE; // [rsp+28h] [rbp-118h]
  char v14[264]; // [rsp+30h] [rbp-110h] BYREF
  unsigned __int64 v15; // [rsp+138h] [rbp-8h]

  v15 = __readfsqword(0x28u);
  if ( argc > 1 )
  {
    SEED = time(0LL);
    printf((unsigned int)"time : %ld\n", SEED, v5, v6);
    srand(SEED);
    FILE = fopen64(argv[1], "rb+");
    if ( FILE )
    {
      while ( 1 )
      {
        v11 = getc(FILE);
        if ( v11 == -1 )
          break;
        fseek(FILE, -1LL, 1LL);
        v9 = rand();
        fputc(v11 ^ (unsigned int)(v9 % 127), FILE);
      }
      fclose(FILE);
      strcpy(v14, argv[1]);
      strcat(v14, ".enc");
      if ( (unsigned int)rename(argv[1], v14) )
      {
        printf((unsigned int)"Can't rename %s filename to %s.enc", (unsigned int)argv[1], (unsigned int)argv[1], v10);
        return 1;
      }
      else
      {
        return 0;
      }
    }
    else
    {
      printf((unsigned int)"Can't open file %s\n", (unsigned int)argv[1], v7, v8);
      return 1;
    }
  }
  else
  {
    printf((unsigned int)"Usage: %s <filename>\n", (unsigned int)*argv, (_DWORD)envp, v3);
    return 1;
  }
}

You can easily see that this the flow of this program is that it read the flag from the file and then perform encryption by xoring it with a pseudorandom value modulo 127. Firstly, I thought that we only need to recover the seed and then decrypt it. So I used exiftool to investigate the give flag.enc file: exif1

And then based on the challenge description and the exiftool result, I recovered the seed, which is 1715198477. Then to be certain I bruted it like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  from ctypes import CDLL
  from ctypes.util import find_library

  start = 1715198477
  libc = CDLL(find_library('c'))


  with open('flag.enc', 'rb') as f:
      encrypted_data = f.read()
  print(encrypted_data)

  for i in range(start - 100000, start + 100000, 1):
      SEED = i
      libc.srand(SEED)
      decrypted = [byte ^ (libc.rand() % 127) for byte in encrypted_data]
      if decrypted[0] == 80 and decrypted[1] == 87 and decrypted[2] == 78:
          print(bytes(decrypted))

But surprisingly it doesn’t work, this is the only result the decryption script printed:

1
b'PWN3BfExw\r\x11# rKQ3~; \x00\x022-R\t"\x1b,\x1aA9\x1b6jF\x00v\x01'

Then I remembered that the ELF file was statically linked and the function may be tampered to trick the challenger. Then I start to analyze the the srand then the rand function, which turns out to be custom func:

1
2
3
4
void __fastcall srand(int SEED)
{
  seed = (unsigned int)(SEED - 1);
}
1
2
3
4
5
unsigned __int64 rand()
{
  seed = 0x5851F42D4C957F2DLL * seed + 1;
  return (unsigned __int64)seed >> 33;
}

Realizing the above trick, I rewrite the custom rand() function in my solve script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from ctypes import CDLL
from ctypes.util import find_library

start = 1715198477
libc = CDLL(find_library('c'))


with open('flag.enc', 'rb') as f:
    encrypted_data = f.read()
print(encrypted_data)

for i in range(start - 1000, start + 1000, 1):
    SEED = i
    def rand_custom():
        global SEED
        SEED = (0x5851F42D4C957F2D * SEED + 1) & 0xFFFFFFFFFFFFFFFF
        return SEED >> 33
    decrypted = [byte ^ (rand_custom() % 127) for byte in encrypted_data]
    if decrypted[0] == 80 and decrypted[1] == 87 and decrypted[2] == 78:
        print(bytes(decrypted))

Which gives us the flag: b'PWNME{4baf3723f62a15f22e86d57130bc40c3}'

C4-License

Challenge Description

Using the license of ‘Noa’ and the provided binary, develop a keygen to create a valid license for the 100 requested users.

Author : Fayred

Flag format: PWNME{.........................}

Connect : nc --ssl [Host] 443

I’m too tired to write this :peepoleave: so I’m gonna cite my friends, Eenosse’ WU: HERE

My sol is available on my github repo here

Mimirev

Challenge Description

A new and obscure programming language, MimiLang, has surfaced. It runs on a peculiar interpreter, but something about it feels… off. Dive into its inner workings and figure out what’s really going on. Maybe you’ll uncover something unexpected.

Author : Lxt3h

Flag format: PWNME{.........................}

Firstly, you were given a binary file which is actually a compiler. I put it on IDA to analyse it. Looking at the function names, this binary looks like it is a Golang binary (bad but still better than Rust :3)

func_names

On IDA, in the main_main function, you would see something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
  v3 = flag__ptr_FlagSet_String(qword_5A5640, (int)"file", 4, 0LL, 0LL, (int)"Path to a .mimi file", 20, v0, v1);
  v3[1] = 0LL;
  if ( dword_5C5970 )
  {
    v3 = (__int64 *)runtime_gcWriteBarrier1((__int64)v3, (__int64)"file", v4, 0LL, 0LL, v5, v6, v7);
    *v8 = v9;
  }
  v178 = v3;
  *v3 = 0LL;
  flag__ptr_FlagSet_Var(
    qword_5A5640,
    (unsigned int)&off_50CAE0,
    (_DWORD)v3,
    (unsigned int)"f",
    1,
    (unsigned int)"Alias of -file",
    14,
    v7,
    (_DWORD)v8);
  v13 = (_BYTE *)flag__ptr_FlagSet_Bool(
                   qword_5A5640,
                   (unsigned int)"disassemble",
                   11,
                   0,
                   (unsigned int)"Disassemble instead of executing",
                   32,
                   v10,
                   v11,
                   v12);
  v177 = v13;
  *v13 = 0;
  flag__ptr_FlagSet_Var(
    qword_5A5640,
    (unsigned int)&off_50CB08,
    (_DWORD)v13,
    (unsigned int)"d",
    1,
    (unsigned int)"Alias of -disassemble",
    21,
    v14,
    v15);
  v19 = (_BYTE *)flag__ptr_FlagSet_Bool(
                   qword_5A5640,
                   (unsigned int)"debug",
                   5,
                   0,
                   (unsigned int)"Enable debug mode",
                   17,
                   v16,
                   v17,
                   v18);
  v176 = v19;

These are just only flag settings when you run executables. Further reading and understanding the file, I see some thing interesting. Firstly the function github_com_Lexterl33t_mimicompiler_vm_NewVM:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v179 = github_com_Lexterl33t_mimicompiler_vm_NewVM(
             v89,
             v57,
             v90,
             (unsigned int)"mTfYS2+3UoKAO+gueELVdxNc6QDBwKW1t8uN5Dx/HIGvWb7kMtmLoyt6SB0EIw39",
             64,
             (unsigned int)"11466b4b07a438fdba619b86088353976073d790344cbf4dae99512028808ecf",
             64,
             v93,
             v94,
             v136,
             v147,
             v155,
             v161,
             v162,
             v163);

In this function the program performs decoding the the base64 string:

1
2
3
4
5
6
7
8
9
10
v34 = encoding_base64__ptr_Encoding_DecodeString(
        qword_5A57B8,
        b64_string,
        hex_string,
        (int)b64_string,
        hex_string,
        a6,
        a7,
        a8,
        a9);

And then saved it to the return obj:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  if ( some_buffer )
  {
    obj = runtime_gcWriteBarrier4();
    v27 = a1;
    *v31 = a1;
    v28 = v35;
    v31[1] = v35;
    v29 = b64decode;
    v31[2] = b64decode;
    v30 = a6;
    v31[3] = a6;
  }
  else
  {
    v27 = a1;
    v28 = v35;
    v29 = b64decode;
    v30 = a6;
  }
  *(_QWORD *)(obj + 24) = v27;
  *(_QWORD *)(obj + 48) = v28;
  *(_QWORD *)(obj + 64) = b64_string;
  *(_QWORD *)(obj + 72) = v32;
  *(_QWORD *)(obj + 56) = v29;
  *(_QWORD *)(obj + 88) = a7;
  *(_QWORD *)(obj + 80) = v30;
  return obj;

After that I start to analyze the github_com_Lexterl33t_mimicompiler_vm__ptr_VM_Run function. There are a lot of instructions like github_com_Lexterl33t_mimicompiler_vm__ptr_VM_Push, and github_com_Lexterl33t_mimicompiler_vm__ptr_VM_Sstore or arithmetic ones like github_com_Lexterl33t_mimicompiler_vm__ptr_VM_Add, … After a while of analyzing, I found one suspicious function which is github_com_Lexterl33t_mimicompiler_vm__ptr_VM_VerifyProof, in which you’ll see a code snippet like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
    else if ( v85 + v84 == 314159 )
    {
      v22 = v85 * v84;
      v23 = (__int64)(v85 * v85 + v84 * v84 * v84 - v85 * v84) % 1048573;
      if ( v23 == 273262 )
      {
        v88[0] = &unk_4C6240;
        v88[1] = &off_50C000;
        v24 = qword_5A5668;
        fmt_Fprintln(
          (unsigned int)&off_50C720,
          qword_5A5668,
          (unsigned int)v88,
          1,
          1,
          (unsigned int)&off_50C000,
          v85,
          v22,
          v21,
          v67,
          v72,
          v75);
        v89 = v9;
        v90 = v9;
        v30 = runtime_convT64(v85, v24, v25, 1, 1, v26, v27, v28, v29);
        *(_QWORD *)&v89 = "\b";
        *((_QWORD *)&v89 + 1) = v30;
        v36 = runtime_convT64(v84, v24, v31, 1, 1, v32, v33, v34, v35);
        *(_QWORD *)&v90 = "\b";
        *((_QWORD *)&v90 + 1) = v36;
        LODWORD(v24) = fmt_Sprintf(
                         (unsigned int)"%d:%d",
                         5,
                         (unsigned int)&v89,
                         2,
                         2,
                         v37,
                         v38,
                         v39,
                         v40,
                         v68,
                         v73,
                         v76,
                         v80);
        v45 = runtime_stringtoslicebyte((unsigned int)&v83, v24, 5, 2, 2, v41, v42, v43, v44, v69, v74, v77);
        crypto_sha256_Sum256(v45, v24, v46, 2, 2, v47, v48, v49, v50, v70, v78, v81);
        key[0] = v71;
        key[1] = v79;
        v51 = (signed __int64)obj[8];
        v55 = github_com_Lexterl33t_mimicompiler_vm_decryptFlag(
                (__int64)obj[7],                // obj[7] is the b64decoded string
                (signed __int64)v51,
                (__int64)obj[9],
                (char *)key,
                16LL,
                32,
                v52,
                v53,
                v54);

You’ll see that it requires two variables (let’s call it x and y) that satisfy the follow system of equations:

\[\begin{cases} x + y = 314159 \\ x^2 + y^3 - xy \equiv 273262 \pmod{1048573} \end{cases}\]

And then format it to SHA256 hash it to become the key for the github_com_Lexterl33t_mimicompiler_vm_decryptFlag function. And finally in that function you’ll see that it decrypts the base64 decoded string and the print it as the flag.

1
cipher = crypto_aes_NewCipher(a4, key, a6, a4, key, a6, a7, a8, a9);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  else {
    v37 = cipher;
    v16 = runtime_makeslice((unsigned int)&buffer, a2, a2, a4, key, v12, v13, v14, v15);
    v39 = v16;
    j = a2;
    v21 = key;
    v22 = a3;
    v23 = ciphertext;
    v24 = v37;
    for ( i = 0LL; j > i; i += 16LL )
    {
      v28 = i + 16;
      if ( j < (unsigned __int64)(i + 16) )
        runtime_panicSliceAcap(v16, i, i + 16, v23, v22, v24);
      if ( i > v28 )
        runtime_panicSliceB(i, i, i + 16, (int)v23, v22, v24, v17, v28, v19, v34, v35);
      (*(void (__golang **)(__int64, __int64, __int64, signed __int64, char *, __int64))(v24 + 32))(
        v21,
        v16 + (i & ((i - j) >> 63)),
        16LL,
        j - i,
        &v23[i & ((i - v22) >> 63)],
        16LL);
      v17 = a3;
      LODWORD(v18) = (_DWORD)ciphertext;
      LODWORD(v19) = v37;
      v16 = v39;
      j = a2;
      v21 = key;
      v22 = a3;
      v23 = ciphertext;
      v24 = v37;
    }
  }

I guess this is an AES128 in ECB mode cipher then I start to write script to solve it by writing the script to solve the system of equations and perform decryption on the base64 decoded string:

1
2
3
4
5
6
for i in range(314160):
    a = i
    b = 314159 - i
    
    if (a * a + b * b * b - b * a) % 1048573 == 273262:
        print(a, b)

which produces output:

1
2
123456 190703
206712 107447

And I format it and decrypt and got the flag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from Crypto.Cipher import AES
from base64 import b64decode
from hashlib import sha256


key1 = '123456:190703'.encode()
key2 = '206712:107447'.encode()

def decrypt_aes_ecb(ciphertext, key):
    cipher = AES.new(key, AES.MODE_ECB)
    try:
        plaintext = cipher.decrypt(ciphertext)
        return plaintext
    except ValueError as e:
        print(f"Decryption error: {e}")
        return None

ciphertext = b64decode('mTfYS2+3UoKAO+gueELVdxNc6QDBwKW1t8uN5Dx/HIGvWb7kMtmLoyt6SB0EIw39')
key = bytes.fromhex(sha256(key1).hexdigest())[:16]
decrypted = decrypt_aes_ecb(ciphertext, key)
if decrypted:
    print("Decrypted message:", decrypted)
    try:
        print("As string:", decrypted.decode('utf-8'))
    except UnicodeDecodeError:
        print("Could not decode as UTF-8")
1
2
Decrypted message: b'PWNME{R3v3rS1ng_Compil0_C4n_B3_good}\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c\x0c'
As string: PWNME{R3v3rS1ng_Compil0_C4n_B3_good}

Super secure network

The target is hiding hisself in a small hotel of the town. One of our agents managed to capture the communications from the parking lot… Unfortunately, the target seems to use software on his phone to protect himself, reading the data exchanged with the server is impossible ! You have the captured trace and the recovered software. It’s up to you.

Author : Prince2lu

Flag format: PWNME{.........................}

Given the pcap file and the binary, I start by using IDA to analyze the binary, seems like it is in the form of Linux kernal. In the init_module function, I recognize this is a TLS protocol using Diffie-Hellman encryption.

I fixed the struct type of the dh by using this struct:

1
2
3
4
5
6
7
8
9
struct dh
{
  void *key;
  void *p;
  void *g;
  unsigned int key_size;
  unsigned int p_size;
  unsigned int g_size;
};

You can define custom struct in IDA Pro 9.0 by doing this:

Open View -> Open subviews -> Local Types.

Then you can right-clicked the screen and choose Add Type

After doing that your code would look cleaner like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
__int64 __fastcall init_module()
{
  dh dh_obj; // [rsp+0h] [rbp-58h] BYREF
  __int64 v2; // [rsp+28h] [rbp-30h] BYREF
  __int64 v3; // [rsp+30h] [rbp-28h] BYREF
  int v4; // [rsp+3Ch] [rbp-1Ch]
  __int64 v5; // [rsp+40h] [rbp-18h]
  __int64 client_public_key; // [rsp+48h] [rbp-10h]
  unsigned int len; // [rsp+54h] [rbp-4h]

  dh_obj.p_size = 8;
  *(&dh_obj.g_size + 1) = 0;
  len = 0;
  client_public_key = 0LL;
  big_prime = get_big_prime(60);
  get_random_bytes(&client_private_key, 8LL);
  v2 = 2LL;
  dh_obj.g_size = 1;
  dh_obj.p = &big_prime;
  dh_obj.g = &v2;
  v3 = pow_mod(2uLL, client_private_key, big_prime);
  dh_obj.key = &v3;
  dh_obj.key_size = 8;
  len = crypto_dh_key_len(&dh_obj);
  if ( len )
  {
    v5 = (int)len;
    v4 = 0x1080020;
    client_public_key = _kmalloc((int)len, 0x1080020LL);
    if ( client_public_key )
    {
      if ( !(unsigned int)crypto_dh_encode_key(client_public_key, len, &dh_obj) && (int)func3(0xC0A8013C, 0xD05) >= 0 )
      {
        qword_B5C0 = (__int64)begin_conv;
        byte_B5D8 = 2;
        dword_B5DC = 0;
        dword_B5E0 = 0x80000000;
        qword_B600 = (__int64)sub_104E;
        byte_B618 = 2;
        dword_B61C = 4;
        dword_B620 = 0x80000000;
        if ( !(unsigned int)nf_register_net_hook(&init_net, &qword_B5C0)
          && !(unsigned int)nf_register_net_hook(&init_net, &qword_B600) )
        {
          send_msg(socket_obj, client_public_key, len);
        }
      }
    }
  }
  return 0LL;
}

The beginning of the pcap is the key exchange operation between the client and the server. key_exchange The first package of the traffic of the pcap is the result of the the crypto_dh_encode_key function. After refer to this link, I recovered the client_public_key, p , and g. Then, I ask my Crypto teammate Malosdaf to compute the private key of the client for me, which is 1330218279148611220. Further investigating the traffic, I see that the second package is the server sending its public key.

If you want to find out more about Diffie-Hellman as a part of TLS protocol, you can try this link

Back to the challenge executables, in the sub_E0 function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
__int64 __fastcall sub_E0A(__int64 a1, __int64 a2)
{
  int v2; // eax
  __int64 v3; // rax
  int v5; // [rsp+1Ch] [rbp-4Ch] BYREF
  unsigned __int64 *server_public_key; // [rsp+20h] [rbp-48h]
  __int64 v7; // [rsp+28h] [rbp-40h]
  int v8; // [rsp+34h] [rbp-34h]
  __int64 v9; // [rsp+38h] [rbp-30h]
  __int64 v10; // [rsp+40h] [rbp-28h]
  unsigned int v11; // [rsp+4Ch] [rbp-1Ch]
  __int64 v12; // [rsp+50h] [rbp-18h]
  __int64 v13; // [rsp+58h] [rbp-10h]

  v12 = 0LL;
  v11 = 0;
  v10 = 0LL;
  v9 = 0LL;
  v5 = 0;
  v13 = sub_215(a2);
  if ( v13 )
  {
    v8 = *(_DWORD *)(a2 + 120);
    v7 = sub_19C(a2);
    server_public_key = (unsigned __int64 *)(4 * (*(_BYTE *)(v13 + 12) >> 4) + v13);
    v12 = sub_FC(a2);
    v11 = v12 - (_DWORD)server_public_key;
    if ( (_DWORD)v12 != (_DWORD)server_public_key )
    {
      v2 = *(_DWORD *)((char *)server_public_key + v11 - 4);
      if ( v2 == 0x86E35DE5 )
      {
        set_up_cipher(server_public_key);
      }
      else if ( v2 == 0x89E35DE5 )
      {
        if ( lll1ll1111l111ll )
        {
          v11 -= 4;
          encrypt_data((__int64)server_public_key, v11 - 16, (BYTE *)server_public_key + v11 - 16);
          v11 -= 16;
          v10 = sub_D4(v11 + 96, 0x1080020u);
          if ( v10 )
          {
            sub_123(v10, 96);
            v3 = skb_put(v10, v11);
            csum_partial_copy_from_user(server_public_key, v3, v11, 0LL, &v5);
            *(_WORD *)(v10 + 184) = 8;
            *(_WORD *)(v10 + 186) = 0;
            *(_WORD *)(v10 + 190) = 0;
            sub_1C5(v10);
            *(_QWORD *)(v10 + 16) = *(_QWORD *)(a2 + 16);
            v9 = sub_1FC(v10);
            if ( v9 )
              netif_rx(v10);
          }
        }
      }
    }
  }
  return 1LL;
}
}

Here I see in the the set_up_cipher, it set up an AES ECB cipher with the SHA256-hashed share secrets as the key.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void __fastcall set_up_cipher(unsigned __int64 *a1)
{
  __int64 share_secret; // [rsp+8h] [rbp-30h] BYREF
  _QWORD aes_key[4]; // [rsp+10h] [rbp-28h] BYREF
  unsigned __int64 server_public_key; // [rsp+30h] [rbp-8h]

  memset(aes_key, 0, sizeof(aes_key));
  share_secret = 0LL;
  server_public_key = *a1;
  share_secret = pow_mod(server_public_key, client_private_key, big_prime);
  if ( !(unsigned int)sha256_helper((__int64)&share_secret, 8u, (__int64)aes_key) )
  {
    cipher = crypto_alloc_cipher((__int64)"aes", 4, 128);
    set_key(cipher, (__int64)aes_key, 0x20u);
    memset(aes_key, 0, sizeof(aes_key));
    lll1ll1111l111ll = 1;
  }
}

Then in the sub_104E, I see that it encrypts the data that would be sent to the server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
__int64 __fastcall sub_104E(__int64 a1, __int64 a2)
{
  char *dest; // [rsp+28h] [rbp-28h]
  void *src; // [rsp+30h] [rbp-20h]
  unsigned int n; // [rsp+3Ch] [rbp-14h]
  __int64 v6; // [rsp+48h] [rbp-8h]

  if ( !a2 )
    return 1LL;
  if ( !sub_1FC(a2) )
    return 1LL;
  v6 = sub_215(a2);
  if ( !v6 )
    return 1LL;
  if ( __ROL2__(*(_WORD *)(v6 + 2), 8) == 0xD05 )
    return 1LL;
  if ( !lll1ll1111l111ll )
    return 1LL;
  n = *(_DWORD *)(a2 + 120);
  src = (void *)sub_19C(a2);
  dest = (char *)_kmalloc(n + 20, 0x1080020LL);
  if ( !dest )
    return 1LL;
  memcpy(dest, src, n);
  get_random_bytes(&dest[n], 16LL);
  encrypt_data((__int64)dest, n, (BYTE *)&dest[n]);
  *(_DWORD *)&dest[n + 16] = 0x89E35BE5;
  if ( !(unsigned int)send_msg(socket_obj, (__int64)dest, n + 20) )
    return 1LL;
  kfree(dest);
  kfree_skb(a2);
  return 2LL;
}

From the this code I can comprehend the structure of the package. The last for bytes is the kinda the end signal, the client data ends with e5 5b e3 89 and the server data ends with e5 5d e3 89. And the 16 bytes after that is the random values that is used in the encryption. Further reading the encrypt_data and after a while of research on AES, I recognize the each package data is encrypted by using the AES-CTR:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
unsigned __int64 __fastcall encrypt_data(__int64 dest, unsigned __int64 n, BYTE *key)
{
  __int64 v4; // rdx
  unsigned __int64 result; // rax
  __int64 iv; // [rsp+18h] [rbp-30h] BYREF
  __int64 v7; // [rsp+20h] [rbp-28h]
  _QWORD v8[2]; // [rsp+28h] [rbp-20h] BYREF
  unsigned __int64 j; // [rsp+38h] [rbp-10h]
  unsigned __int64 i; // [rsp+40h] [rbp-8h]

  v8[0] = 0LL;
  v8[1] = 0LL;
  iv = 0LL;
  v7 = 0LL;
  j = 0LL;
  v4 = *((_QWORD *)key + 1);
  iv = *(_QWORD *)key;
  v7 = v4;
  for ( i = 0LL; ; i += 16LL )
  {
    result = i;
    if ( i >= n )
      break;
    sub_2FB(cipher, (__int64)v8, (__int64)&iv);
    for ( j = 0LL; j <= 0xF && n > i + j; ++j )
      *(_BYTE *)(j + i + dest) ^= *((_BYTE *)v8 + j);
    for ( j = 15LL; !++*((_BYTE *)&iv + j); --j )
      ;
  }
  return result;
}

This is a very good visualization of the AES_CTR: AES_CTR

After gathering all the pieces of the puzzle, I dumped the data of the traffic(excluding the first two packages), and write the solve script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Cipher import AES
import hashlib

packets = []


def decrypt(packet):
    client_private_key = 1330218279148611220
    prime = int.from_bytes(bytes.fromhex('6b152f4845212e15'), 'little')
    server_public_key = int.from_bytes(bytes.fromhex('65027c9877d09804'), 'little')
    share_secret = pow(server_public_key, client_private_key, prime).to_bytes(8, 'little')
    aes_key = hashlib.sha256(share_secret).digest()

    random_key = list(packet[len(packet) - 20: len(packet) - 4])
    packet = packet[0:len(packet) - 20]
    cipher = AES.new(aes_key, AES.MODE_CTR, iv = bytes(random_key))

    plaintext = cipher.decrypt(packet)
    return plaintext

packets = [packet.strip() for packet in open('packets.txt', 'r').readlines()]

for packet in packets:
    packet = bytes.fromhex(packet)
    print(decrypt(packet))

And get the flag PWNME{Crypt0_&_B4ndwidth_m4k3s_m3_f33l_UN83474813!!!} The challenge given files and the traffic dump file are available on my github repo here

Thank you for reading my WU.

This post is licensed under CC BY 4.0 by the author.

Trending Tags