PwnMe CTF 2025

Miscellaneous

Decode Runner

Category

Miscellaneous

Tags

Flag

PWNME{d4m_y0U_4r3_F4st!_4nd_y0u_kn0w_y0ur_c1ph3rs!}

Welcome to Decode Runner ! You will receive 100 encoded words. Your goal is to decode them and send back the decoded word. You have 3 seconds to respond to each word. Good luck!

Author : Offpath

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

Connect : nc --ssl [host] 443

by Offpath


So we are given a service that sends us encoded words and we have to decode them and send back the decoded word. We have 3 seconds to respond to each word. After connecting to the service, we are given a hint and the encoded word. The hint is used to determine which decoding function to use.

We can use dcode.fr cipher indetifier to identify the cipher used in the encoded word. After many conversations with ChatGPT, GitHub Copilot, Deepseek, and some code searching to write the decoding functions for each hint, I came up with the following script:

def trithemius_decrypt(ciphertext, initial_shift=3):
    decrypted_text = ""
    for i, char in enumerate(ciphertext):
        shift = initial_shift + i  # Incremental shift
        decrypted_char = chr(((ord(char) - ord('a') - shift) % 26) + ord('a'))
        decrypted_text += decrypted_char
    return decrypted_text

def shankar_speech_defect(text):
    table = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
    table2 = "DFGHJKLMNUOPQRSTIVWXYZBACE"
    return "".join([table2[table.index(c)] if c in table else c for c in text])

def nato_phonetic_alphabet(text):
    table = {
        'Alfa': 'A', 'Bravo': 'B', 'Charlie': 'C', 'Delta': 'D', 'Echo': 'E', 'Foxtrot': 'F', 'Golf': 'G', 'Hotel': 'H',
        'India': 'I', 'Juliett': 'J', 'Kilo': 'K', 'Lima': 'L', 'Mike': 'M', 'November': 'N', 'Oscar': 'O', 'Papa': 'P',
        'Quebec': 'Q', 'Romeo': 'R', 'Sierra': 'S', 'Tango': 'T', 'Uniform': 'U', 'Victor': 'V', 'Whiskey': 'W', 'X-ray': 'X',
        'Yankee': 'Y', 'Zulu': 'Z', 'Zero': '0', 'One': '1', 'Two': '2', 'Three': '3', 'Four': '4', 'Five': '5', 'Six': '6',
        'Seven': '7', 'Eight': '8', 'Nine': '9', 'Dash': '-', 'Stop': '.'
    }
    return "".join([table[c] for c in text.split()])

def chuck_norris_code(text):
    # Splitting the encoded text into parts
    parts = text.split()
    
    binary_string = ""
    
    # Iterate through the encoded chunks in pairs
    for i in range(0, len(parts), 2):
        prefix = parts[i]  # Either "00" (for 0s) or "0" (for 1s)
        sequence = parts[i+1]  # The sequence of zeroes indicating length

        if prefix == "00":  # Represents a sequence of 0s
            binary_string += "0" * len(sequence)
        elif prefix == "0":  # Represents a sequence of 1s
            binary_string += "1" * len(sequence)
    
    # Convert binary string to ASCII text
    decoded_text = "".join(chr(int(binary_string[i:i+7], 2)) for i in range(0, len(binary_string), 7))
    
    return decoded_text

WABUN = {
    'A': '--.--',   # a
    'I': '.-',      # i
    'U': '..-',     # u
    'E': '-.---',   # e
    'O': '.-...',   # o
    
    'KA': '.-.-',    # ka
    'KI': '-.-..',   # ki
    'KU': '...-',    # ku
    'KE': '-.--',    # ke
    'KO': '----',    # ko
    
    'SA': '-.-.-',   # sa
    'SHI': '--.-..',  # shi
    'SU': '---.-',   # su
    'SE': '.---.',   # se
    'SO': '---.',    # so
    
    'TA': '-.',      # ta
    'CHI': '..-.',    # chi
    'TSU': '.--.',    # tsu
    'TE': '.-.--',   # te
    'TO': '..-..',   # to
    
    'NA': '.-.',     # na
    'NI': '-.--.',   # ni
    'NU': '....',    # nu
    'NE': '--.-',    # ne
    'NO': '..--',    # no
    
    'HA': '-...',    # ha
    'HI': '--..-',   # hi
    'FU': '--..',    # fu
    'HE': '.',       # he
    'HO': '-..',     # ho
    
    'MA': '-..-',    # ma
    'MI': '..-.-',   # mi
    'MU': '-',       # mu
    'ME': '-...-',   # me
    'MO': '-..-.',   # mo
    
    'YA': '.--',     # ya
    'YU': '-..--',   # yu
    'YO': '--',      # yo
    
    'RA': '...',     # ra
    'RI': '--.',     # ri
    'RU': '-.--.',   # ru
    'RE': '---',     # re
    'RO': '.-.-.',   # ro
    
    'WA': '-.-',     # wa
    'WO': '.---',    # wo
    'N': '.-.-.',    # n
    
    ',': '.-..-',   # Japanese comma
    # 'PERIOD': '.-.-.-',  # Japanese period
    # 'LPAREN': '-.--.',   # Left parenthesis 
    # 'RPAREN': '.-..-.',  # Right parenthesis
    # 'QUOTE_OPEN': '.-.-.',   # Opening quote
    # 'QUOTE_CLOSE': '.-.-..',  # Closing quote
    
    '1': '.----',
    '2': '..---',
    '3': '...--',
    '4': '....-',
    '5': '.....',
    '6': '-....',
    '7': '--...',
    '8': '---..',
    '9': '----.',
    '0': '-----',
    
    'SPACE': '/',        # Word separator
}

# Create reverse mapping for decoding
REVERSE_WABUN = {v: k for k, v in WABUN.items()}

def wabun_code(text):
    # decode the wabun code
    return "".join([REVERSE_WABUN.get(c, c) for c in text.split()])

# Define the letter and figure shift codes for Baudot (5-bit code)
LETTERS_SHIFT = 0b11111  # 31 - Shift to letters set
FIGURES_SHIFT = 0b11011  # 27 - Shift to figures set

# Original Baudot code (Murray code/ITA1)
# Maps 5-bit codes to characters in the letters set
BAUDOT_LETTERS = {
    0b00000: ' ',    # NULL/BLANK
    0b00001: 'E',
    0b00010: 'LF',   # Line Feed
    0b00011: 'A',
    0b00100: ' ',    # SPACE
    0b00101: 'S',
    0b00110: 'I',
    0b00111: 'U',
    0b01000: 'CR',   # Carriage Return
    0b01001: 'D',
    0b01010: 'R',
    0b01011: 'J',
    0b01100: 'N',
    0b01101: 'F',
    0b01110: 'C',
    0b01111: 'K',
    0b10000: 'T',
    0b10001: 'Z',
    0b10010: 'L',
    0b10011: 'W',
    0b10100: 'H',
    0b10101: 'Y',
    0b10110: 'P',
    0b10111: 'Q',
    0b11000: 'O',
    0b11001: 'B',
    0b11010: 'G',
    0b11011: 'FIGS', # Figures shift
    0b11100: 'M',
    0b11101: 'X',
    0b11110: 'V',
    0b11111: 'LTRS', # Letters shift
}

# Maps 5-bit codes to characters in the figures set
BAUDOT_FIGURES = {
    0b00000: ' ',    # NULL/BLANK
    0b00001: '3',
    0b00010: 'LF',   # Line Feed
    0b00011: '-',
    0b00100: ' ',    # SPACE
    0b00101: "'",    # Bell signal in some implementations
    0b00110: '8',
    0b00111: '7',
    0b01000: 'CR',   # Carriage Return
    0b01001: '$',    # WRU (Who are you?) in some implementations
    0b01010: '4',
    0b01011: "'",    # Bell
    0b01100: ',',
    0b01101: '!',
    0b01110: ':',
    0b01111: '(',
    0b10000: '5',
    0b10001: '+',
    0b10010: ')',
    0b10011: '2',
    0b10100: '#',    # Pound sign in some implementations
    0b10101: '6',
    0b10110: '0',
    0b10111: '1',
    0b11000: '9',
    0b11001: '?',
    0b11010: '&',
    0b11011: 'FIGS', # Figures shift
    0b11100: '.',
    0b11101: '/',
    0b11110: ';',
    0b11111: 'LTRS', # Letters shift
}

# Create reverse mappings for encoding
REVERSE_BAUDOT_LETTERS = {v: k for k, v in BAUDOT_LETTERS.items() if v not in ['LTRS', 'FIGS', 'CR', 'LF']}
REVERSE_BAUDOT_FIGURES = {v: k for k, v in BAUDOT_FIGURES.items() if v not in ['LTRS', 'FIGS', 'CR', 'LF']}

# Special control character mappings
CONTROL_CHARS = {
    '\n': 0b00010,  # LF
    '\r': 0b01000,  # CR
}

def baudot_code(baudot_codes):
    # decode the baudot code
    result = []
    current_shift = None
    
    for code in baudot_codes:
        # Handle shift codes
        if code == LETTERS_SHIFT:
            current_shift = LETTERS_SHIFT
            continue
        elif code == FIGURES_SHIFT:
            current_shift = FIGURES_SHIFT
            continue
            
        # If we haven't determined shift yet, default to letters
        if current_shift is None:
            current_shift = LETTERS_SHIFT
        
        # Decode based on current shift
        if current_shift == LETTERS_SHIFT:
            char = BAUDOT_LETTERS.get(code, '?')
        else:  # current_shift == FIGURES_SHIFT
            char = BAUDOT_FIGURES.get(code, '?')
        
        # Skip shift characters in output
        if char not in ['LTRS', 'FIGS']:
            if char == 'CR':
                result.append('\r')
            elif char == 'LF':
                result.append('\n')
            else:
                result.append(char)
    
    return ''.join(result)

def baudot_code_from_binary_string(binary_string):
    """Decode a binary string representation of Baudot code to text"""
    # Remove any whitespace and split into 5-bit chunks
    binary_string = binary_string.replace(' ', '')
    codes = []
    
    for i in range(0, len(binary_string), 5):
        if i + 5 <= len(binary_string):
            code = int(binary_string[i:i+5], 2)
            codes.append(code)
    
    return baudot_code(codes)

def leet_speak(text):
    table = {
        '4': 'a', '8': 'b', 'C': 'C', 'D': 'D', '3': 'e', 'F': 'F', '6': 'g', 'H': 'H',
        '1': 'i', 'J': 'J', 'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', '0': 'o', 'P': 'P',
        'Q': 'Q', 'R': 'R', '5': 's', '7': 't', 'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X',
        'Y': 'Y', '2': 'z', '|<': 'k'
    }
    return "".join([table.get(c, c) for c in text])

def latin_gibberish(text):
    words = text.split()
    decrypted_words = []

    # Common Latin suffixes that may be appended
    suffixes = ["US", "UM", "A", "IS", "OS", "ES", "AE", "E", "I", "IT"]

    for word in words:
        # Remove the suffix if present
        for suffix in suffixes:
            if word.endswith(suffix.lower()):
                word = word[:-len(suffix)]
                break  # Stop after removing the first matching suffix

        # Reverse the remaining characters
        decrypted_word = word[::-1]
        decrypted_words.append(decrypted_word)

    return " ".join(decrypted_words)

guitar_chords = {
    "x24442": "B",
    "x02220": "A",
    "xx0232": "D",
    "320003": "G",
    "022100": "E",
    "133211": "F",
    "224442": "B",
    "032010": "C",
    "x35553": "C",
    "x57775": "D",
    "xx0212": "D",
    "x13331": "F",
    "355433": "G",
    "577655": "A",
    "799877": "B",
    "x32010": "C",
}

def guitar_chords_notation(text):
    return "".join([guitar_chords.get(c, c) for c in text.split()])



table = {
    '1': '..',
    '2': './',
    '3': '/-',
    '4': '//',
    '5': '-.',
    '6': '--',
    '7': '/.',
    '8': '-/',
    '9': '.-'
}

morse_table = {
    '.-': 'A', '-...': 'B', '-.-.': 'C', '-..': 'D', '.': 'E', '..-.': 'F', '--.': 'G', '....': 'H',
    '..': 'I', '.---': 'J', '-.-': 'K', '.-..': 'L', '--': 'M', '-.': 'N', '---': 'O', '.--.': 'P',
    '--.-': 'Q', '.-.': 'R', '...': 'S', '-': 'T', '..-': 'U', '...-': 'V', '.--': 'W', '-..-': 'X',
    '-.--': 'Y', '--..': 'Z', '.----': '1', '..---': '2', '...--': '3', '....-': '4', '.....': '5',
    '-....': '6', '--...': '7', '---..': '8', '----.': '9', '-----': '0'
}

def morbit_decrypt(ciphertext , keyword):
    morse = ""
    for c in ciphertext:
        print(c)
        if c in table:
            morse += table[c]
        
    # split by /
    morse = morse.split('/')
    plaintext = ""

    for m in morse:
        if m in morse_table:
            plaintext += morse_table[m]
        else:
            plaintext += ' '
    
    return plaintext


from pwn import *

context.log_level = 'debug'

r = remote('decoderunner-6bc786c74ac9f4f0.deploy.phreaks.fr', 443, ssl=True)

r.recvuntil(b'and send back the decoded word. You have 3 seconds to respond to each word. Good luck!\n')
r.recvline()
r.recvline()
r.recvline()
while True:
    is_hint_or_cipher = r.recvline().strip().decode()
    is_hint_or_cipher = is_hint_or_cipher.split(': ')
    
    if is_hint_or_cipher[0] == 'cipher':
        r.sendline(nato_phonetic_alphabet(is_hint_or_cipher[1]).lower())
        continue

    hint = is_hint_or_cipher[1]
    r.recvuntil(b'cipher: ')
    cipher = r.recvline().strip().decode()

    known_cipher = {
        '4rC': 'arc',
        'D4N53r': 'danser',
        'xx0232 022100 320003 x02220 320003 022100': 'degage',
        '8r0U3773': 'brouette',
        'x24442 022100 x02220 xx0232 022100 xx0232': 'beaded',
        '00001 10010 00001 10110 10100 00011 01100 10000': 'elephant',
        '10111 00111 00011 01010 10000 10001': 'quartz',
        '11010 00111 00110 10000 00011 01010 00001': 'guitare',
        'x24442 x02220 xx0232 320003 022100 xx0232': 'badged',
        '00110 01100 10000 00001 01010 01100 00001 10000': 'internet',
        '10011 00011 10010 01010 00111 00101': 'walrus',
        '-.-.. -.- ..--': 'kiwano',
        '01011 11000 00111 00001 10000': 'gamte',
        '|<4N60Ur0U': 'kangourou',
        '5293212292': 'danser',
        '932932971': 'ananas',
        '5781972922': 'navire',
        '557121732': 'chien'
    }

    if cipher in known_cipher:
        r.sendline(known_cipher[cipher])
        continue

    if hint == '1337 ...':
        r.sendline(leet_speak(cipher).lower())
    elif hint == 'He can\'t imagine finding himself in CTF 150 years later...':
        r.sendline(baudot_code_from_binary_string(cipher).lower())
    elif hint == 'It looks like Morse code, but ...':
        r.sendline(wabun_code(cipher).lower())
    elif hint == 'Did you realy see slumdog millionaire ?':
        r.sendline(shankar_speech_defect(cipher).lower())
    elif hint == 'Born in 1462 in Germany...':
        r.sendline(trithemius_decrypt(cipher))
    elif hint == 'He can snap his toes, and has already counted to infinity twice ...':
        r.sendline(chuck_norris_code(cipher))
    elif hint == 'what is this charabia ???':
        r.sendline(latin_gibberish(cipher))
    elif hint == 'Hendrix would have had it...':
        r.sendline(guitar_chords_notation(cipher).lower())
    elif hint == 'A code based on pairs of dots and dashes. Think of a mix of Morse code and numbers... (AZERTYUIO)':
        r.sendline(morbit_decrypt(cipher, 'AZERTYUIO').lower())

I want to use dcode.fr API but it is not available for public they said? :(.

Mafia at the end 1

Category

Miscellaneous

Tags

Blockchain

Flag

PWNME{1ls_0nt_t0vt_Sh4ke_dz8a4q6}

You’re an agent, your unit recently intercepted a mob discussion about an event that’s going to take place on August 8, 2024. You already know the location, though. A password for the event was mentioned. Your job is to find it and return it so that an agent can go to the scene and collect evidence.

Note : The contract is deployed on sepolia network

Authors : wepfen, teazer

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

by wepfen and teazer


Another PCAP challenge. This time we have a PCAP file with a lot of traffic. We found the IRC traffic.

alt text

Following the TCP stream and we found chat messages.

alt text

In the chat, we found the contract address 0xCAB0b02864288a9cFbbd3d004570FEdE2faad8F8, because the contract is deployed on the sepolia network, we can find the contract on the etherscan.

alt text

Mafia at the end 2

Category

Miscellaneous

Tags

Blockchain

Flag

PWNME{th3_H0us3_41way5_w1n_bu7_sh0uld_be_4fr41d_0f_7h3_ul7im4te_g4m8l3r!}

You’re in the final step before catching them lacking. Prove yourself by winning at the casino and access the VIP room !

But remember, the house always win.

Note : To connect to the casino with Metamask, we recommend you to use another browser and a “trash” MetaMask wallet to avoid some weird behavior.

Author : Wefpen, Tzer

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

Connect : nc mafia2.phreaks.fr 10020

by Wefpen and Tzer


Given Setup.sol and Casino.sol, we can see that the casino contract uses a PRNG to determine the outcome of the game. The PRNG is seeded with the prevrandao value from the previous block. We can use this information to predict the outcome of the game.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

contract CasinoPWNME {

  bool public isWinner;

	uint256 public multiplier = 14130161972673258133;
	uint256 public increment = 11367173177704995300;
	uint256 public modulus = 4701930664760306055;
  uint private state;

  constructor (){
    state = block.prevrandao % modulus;
  }

  function checkWin() public view returns (bool) {
    return isWinner;
  }

  function playCasino(uint number) public payable  {

    require(msg.value >= 0.1 ether, "My brother in christ, it's pay to lose not free to play !");
    PRNG();
    if (number == state){
      isWinner = true;
    } else {
      isWinner = false;
    }
  }
  
  function PRNG() private{
    state = (multiplier * state + increment) % modulus;
  }

}

So we can see the state is at storage slot 0x4. We can get the value of the state by calling web3.eth.getStorageAt(casinoAddress, "0x4"). We can then calculate the next state by using the PRNG formula.

bool public isWinner;

uint256 public multiplier = 14130161972673258133;
uint256 public increment = 11367173177704995300;
uint256 public modulus = 4701930664760306055;
uint private state;

Here is the code to predict the outcome of the game:

const stateStorage = await web3.eth.getStorageAt(casinoAddress, "0x4");
console.log("State:", BigInt(stateStorage).toString());

const multiplier = BigInt("14130161972673258133");
const increment = BigInt("11367173177704995300");
const modulus = BigInt("4701930664760306055");

let state = BigInt(stateStorage) % modulus;
state = (multiplier * state + increment) % modulus;

Note: The code above is write to the casino website source code. and modify playCasino arguments from 0 to state value. (The challenge start instance error, so we can’t connect to the casino)

Reverse Engineering

Back to the past

Category

Reverse Engineering

Tags

Flag

PWNME{4baf3723f62a15f22e86d57130bc40c3}

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{.........................}

by Fayred


The binary uses the current time as a seed for the random number generator. The random number generator is used to generate a key to xor the content of the file flag.enc. The key can be recovered by using the same seed as the binary.

int __fastcall main(int argc, const char **argv, const char **envp)
{
  char v3; // cl
  int v5; // edx
  char v6; // cl
  const char *v7; // rsi
  int v8; // edx
  char v9; // cl
  int v10; // eax
  char v11; // cl
  int v13; // [rsp+1Ch] [rbp-124h]
  unsigned int v14; // [rsp+20h] [rbp-120h]
  __int64 v15; // [rsp+28h] [rbp-118h]
  char v16[264]; // [rsp+30h] [rbp-110h] BYREF
  unsigned __int64 v17; // [rsp+138h] [rbp-8h]

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

By examining the binary, we can see that it encrypts a file using the following process:

  1. It retrieves the current time (time(0LL)) and uses it as a seed for the random number generator (srand(time)).
  2. It opens the specified file and reads its contents byte by byte.
  3. Each byte is XOR-ed with a random number generated using rand() % 127.
  4. The modified file is saved with the .enc extension.

The binary uses a custom implementation of rand():

__int64 __fastcall srand(int a1)
{
  __int64 result; // rax

  result = (unsigned int)(a1 - 1);
  seed = result;
  return result;
}

unsigned __int64 rand()
{
  seed = 0x5851F42D4C957F2DLL * seed + 1;
  return (unsigned __int64)seed >> 33;
}

This is a linear congruential generator (LCG). Since the only input to this function is the seed (derived from time).

Since the encryption key is derived from time(0LL), we can determine the exact seed by checking the file’s modification time (os.path.getmtime). This allows us to reproduce the same random sequence and decrypt the file.

We can implement the decryption process as follows:

import os

filename = 'flag.enc'

def srand(seed):
    global current_seed
    current_seed = seed - 1

def rand():
    global current_seed
    current_seed = (0x5851F42D4C957F2D * current_seed + 1) & 0xFFFFFFFFFFFFFFFF
    return current_seed >> 33

mod_time = int(os.path.getmtime(filename))
srand(mod_time)

f = open(filename, 'rb')
content = f.read()

flag = bytearray()
for byte in content:
    flag.append(byte ^ (rand() % 127))

print(f'Flag: {"".join(map(chr, flag))}')

C4 License

Category

Reverse Engineering

Tags

Flag

PWNME{8d0f21d2a2989b739673732d8155022b}

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

by Fayred


They provide a binary and a license for user Noa. The binary is a QT application that reads a license key from the user and validates it. The license key is a base64 encoded JSON object with the user and the serial key. The serial key is encrypted using the RC4 algorithm with a random key generated from the CRC32 of the user. The encrypted data is then hashed using SHA1 and compared with the expected hash.

We start by analyzing the given license for Noa, which is:

eyJ1c2VyIjogIk5vYSIsICJzZXJpYWwiOiAiZTNiZmJkZjE2MzE0ZWJlZDdiZDJjNjA4YWU1MzA2OTI3MjRjYzNhNSJ9

Decoding it from base64 gives:

{
  "user": "Noa",
  "serial": "e3bfbdf16314ebed7bd2c608ae530692724cc3a5"
}

Decompiling the binary, we see that this is QT application. The binary reads the user input to validate the license.

alt text

We can see that the binary calls had a class RC4 and a function checker that validates the license. The checker function is called when the user clicks the button on_checkKey_clicked.

alt text

In the on_checkKey_clicked function, the input from the user is decoded from base64 and passed to the checker function.

alt text

The checker function is responsible for validating the license. The decompiled code of the checker function is as follows:

__int64 __fastcall checker(__int64 *a1, const QByteArray *a2)
{
  __int64 v3; // rdx
  __int64 v4; // rsi
  unsigned int v5; // eax
  int v6; // ebx
  volatile signed __int32 *v7; // rdi
  signed __int32 v8; // et0
  volatile signed __int32 **v9; // r13
  unsigned int v10; // edx
  _BYTE *v11; // rax
  unsigned __int64 v12; // rsi
  __int64 v13; // rax
  volatile signed __int32 *v14; // rdi
  __int64 v15; // r12
  signed __int32 v16; // et0
  volatile signed __int32 *v17; // rdi
  signed __int32 v18; // et0
  volatile signed __int32 *v19; // rdi
  signed __int32 v20; // et0
  volatile signed __int32 *v22; // [rsp+8h] [rbp-160h] BYREF
  volatile signed __int32 *v23; // [rsp+10h] [rbp-158h] BYREF
  volatile signed __int32 *v24; // [rsp+18h] [rbp-150h] BYREF
  unsigned __int8 v25[276]; // [rsp+20h] [rbp-148h] BYREF
  unsigned __int8 v26[4]; // [rsp+134h] [rbp-34h] BYREF
  unsigned __int64 v27; // [rsp+138h] [rbp-30h]

  v3 = *((unsigned int *)a1 + 2);
  v4 = *a1;
  v27 = __readfsqword(0x28u);
  v5 = crc32(0LL, v4, v3);
  srand(v5);
  v6 = rand();
  *(_DWORD *)v26 = _byteswap_ulong(rand() % 0xFFFF * (v6 % 0xFFFF));
  RC4::RC4((RC4 *)v25, v26);
  QByteArray::fromHex((QByteArray *)&v24, a2);
  RC4::decrypt((QByteArray *)&v22, (RC4 *)v25, (__int64 *)&v24);
  v7 = v24;
  if ( !*v24 || *v24 != -1 && (v8 = _InterlockedSub(v24, 1u), v7 = v24, !v8) )
    QArrayData::deallocate(v7, 1LL, 8LL);
  v9 = &v23;
  QCryptographicHash::hash(&v23, &v22, 2LL);
  QByteArray::toHex((QByteArray *)&v24);
  v10 = *((_DWORD *)v24 + 1);
  if ( v10 )
  {
    v11 = (char *)v24 + *((_QWORD *)v24 + 2);
    v12 = 0LL;
    while ( *v11 )
    {
      v12 = (unsigned int)(v12 + 1);
      ++v11;
      if ( v10 == (_DWORD)v12 )
      {
        v12 = v10;
        break;
      }
    }
  }
  else
  {
    v12 = 0LL;
  }
  v13 = QString::fromAscii_helper((QString *)((char *)v24 + *((_QWORD *)v24 + 2)), (const char *)v12, v10);
  v14 = v24;
  v15 = v13;
  if ( !*v24 || *v24 != -1 && (v16 = _InterlockedSub(v24, 1u), v14 = v24, !v16) )
    QArrayData::deallocate(v14, 1LL, 8LL);
  v17 = v23;
  if ( !*v23 || *v23 != -1 && (v18 = _InterlockedSub(v23, 1u), v17 = v23, !v18) )
    QArrayData::deallocate(v17, 1LL, 8LL);
  LOBYTE(v9) = (unsigned int)QString::compare_helper(
                               v15 + *(_QWORD *)(v15 + 16),
                               *(unsigned int *)(v15 + 4),
                               "b039d6daea04c40874f80459bff40142bd25b995",
                               0xFFFFFFFFLL,
                               1LL) == 0;
  if ( !*(_DWORD *)v15 || *(_DWORD *)v15 != -1 && !_InterlockedSub((volatile signed __int32 *)v15, 1u) )
    QArrayData::deallocate(v15, 2LL, 8LL);
  v19 = v22;
  if ( !*v22 || *v22 != -1 && (v20 = _InterlockedSub(v22, 1u), v19 = v22, !v20) )
    QArrayData::deallocate(v19, 1LL, 8LL);
  return (unsigned int)v9;
}

Where arguments are:

  • a1 is the user
  • a2 is the serial key

The function checker does the following:

  1. Calculate the CRC32 of the user
  2. Seed the random number generator with the CRC32 value
  3. Generate a random key
  4. Initialize the RC4 object with the random key
  5. Decrypt the serial key
  6. Calculate the SHA1 hash of the decrypted serial key
  7. Compare the hash with the expected hash

So to generate a valid license for a user, we need to:

  1. Calculate the CRC32 of the user
  2. Seed the random number generator with the CRC32 value
  3. Generate a random key
  4. Initialize the RC4 object with the random key
  5. Encrypt the data
  6. Send the base64 encoded JSON object with the user and the encrypted data

We the create a Python script to automate the process. It performs the necessary transformations and sends valid licenses to the challenge server for 100 different users.

from pwn import *
import ctypes
import binascii
import hashlib
import base64
import json

libc = ctypes.CDLL('libc.so.6')
libc.srand(libc.time(0))

class RC4:
    def __init__(self, key: bytes):
        # Initialize the array with values 0-255
        self.state = [i for i in range(256)]
        # Reset position tracking
        self.x = 0
        self.y = 0
        
        # Key scheduling algorithm
        j = 0
        for i in range(256):
            # In C++ code, a2[v3 & 3] is accessing key with modulo 4
            # Assuming key is a bytes object or bytearray
            j = (j + self.state[i] + key[i % len(key)]) & 0xFF
            # Swap values
            self.state[i], self.state[j] = self.state[j], self.state[i]
            
    def get_bit(self):
        # Increment x and wrap around to stay within 0-255
        self.x = (self.x + 1) % 256
        
        # Get value at state[x]
        x_val = self.state[self.x]
        
        # Update y using value at state[x]
        self.y = (self.y + x_val) % 256
        
        # Swap values at state[x] and state[y]
        self.state[self.x], self.state[self.y] = self.state[self.y], self.state[self.x]
        
        # Return value at state[(state[x] + state[y]) % 256]
        return self.state[(self.state[self.x] + self.state[self.y]) % 256]
        
    def decrypt(self, encrypted_data: bytes):
        result = bytearray()
        
        for byte in encrypted_data:
            # XOR each byte with the next byte from the RC4 keystream
            decrypted_byte = byte ^ self.get_bit()
            result.append(decrypted_byte)
            
        return bytes(result)
    
    def encrypt(self, data: bytes):
        return self.decrypt(data)

def decrypt(key, data):
    crc32_value = binascii.crc32(key)
    libc.srand(crc32_value)

    v6 = libc.rand()
    random_key = libc.rand() % 0xFFFF * (v6 % 0xFFFF)
    random_key = random_key.to_bytes(4, 'big')

    rc4 = RC4(random_key)
    return rc4.decrypt(binascii.unhexlify(data))

def encrypt(key, data):
    crc32_value = binascii.crc32(bytearray(key))
    libc.srand(crc32_value)

    v6 = libc.rand()
    random_key = libc.rand() % 0xFFFF * (v6 % 0xFFFF)
    random_key = random_key.to_bytes(4, 'big')

    rc4 = RC4(random_key)

    encrypted_data = rc4.encrypt(data)
    return encrypted_data.hex()

def checker(key, data):
    sha1_hash = hashlib.sha1(decrypt(key, data)).hexdigest()

    expected_hash = "b039d6daea04c40874f80459bff40142bd25b995"
    return sha1_hash == expected_hash

data = decrypt(b'Noa',b'e3bfbdf16314ebed7bd2c608ae530692724cc3a5')
print(f'Decrypted data: {data}')
print(f'Encrypted data: {encrypt(b"Noa", data)}')
print(f'Checker: {checker(b"Noa", b"e3bfbdf16314ebed7bd2c608ae530692724cc3a5")}')

context.log_level = 'debug'

r = remote('c4license-3a7a8c81137937c1.deploy.phreaks.fr', 443, ssl=True)

current = 0
while True:
    r.recvuntil(b'Your license for ')
    user = r.recv()
    user = user.split(b' ')[0]

    json_data = {
        'user': user.decode(),
        'serial': encrypt(user, data)
    }
    base64_json = base64.b64encode(json.dumps(json_data).encode()).decode()
    r.sendline(base64_json)

    current += 1
    if current == 100:
        break

r.interactive()

Flattened Vyper

Category

Reverse Engineering

Tags

Flag

PWNME{SuCh_4_M3t4R3veRS3r}

I achieve to obtain this smart contract, but I can’t understand what it does. Can you help me?

The only information that I have are the followings:

  • The flag is cut in three parts and each part is emitted once.
  • The first part is emitted in raw bytes.
  • The second part is emitted in base58 encoding.
  • The last part has to be xor-ed with the second part.

Author : Fabrisme

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

by Fabrisme


So we have EVM bytecode that we need to reverse engineer. The contract is written in Vyper which is a higher-level language that compiles to EVM bytecode. The contract is flattened, so we need to decompile it to the original source code.

0x6003361161001843405d6106b4576106ac43405d6106b4565b5f3560e01c346106b043405d6106b4576301002b1f81186106aa43405d6106b457608436106106b043405d6106b45760243560040160408135116106b043405d6106b4578035602082018181606037508060405250507f1b47819435df544ae4e6a35d3c2d0eb2900cab1460ec254d464c1d82d70db60a7fe4b87e6bca20abb51b195ca36f2af2f5438b8a072af4f6f657b9a8304e8d36910133186106b043405d6106b4577f400000000000000000000000000000000000000000000000000000000000000060a0527f100000000000000000000000000000000000000000000000000000000000000060c0527f6d1cd107e7ef14bc558622a86cb621d9f18c50764e98df43777f3b33164b87dd7f42e0da5ae4babe43a564859fc944bb6033a02fb2741ff60444793a962b5ccf627fa2b11742daceaab03c583a6aa15e32d664450c3eb36da7897f6ce121490078597f8d4d1c1fd99b004fccba9d5d04aca86fa66973fa89ea8ece4c6ae08474173fa6181803517f781d48306de91b1cbdc32a7036761292d1c1cf57e84fe74689f7583d7e24c64f7f781d48306de91b1cbdc32a7036761292d1c1cf57e84fe74689f7583d7e24c6ef18517f0512bd13110722311710cf5327ac435a7a97c643656412a9b8a1abcd1a6916c77f459142deccea264542a00403ce80c4b0a4042bb3d4341aad06905269ed6f0b097f4083ffcddded047455b0cb50e92c87eade93edf0b1500804be31f9a4f7061dc2180335181861069f43405d6106b4577f805d92843fa8a2a28a4c797f73b9b4d5a4d8fd4f515d4e8cdfc2c303969081497f4c45d48945056a0c8203311b215240ed84ad80e70629f350834b13c2c0f510d47f0768ff01e42043ef7236fc833ca34516c059f32dce8faaa7c3085cb73a85b9df18037fadaf670881744dc7aa596a2d1eb68dbc21fef929481f4bc6ec75331d53acbd2e7f61ec705ea65e9bbf5b5a26cc5fa4f55ad1074377dce1ffbc52d65a8816af56a47f9c1a44f72090b2084eff4360bf11986150f7b5b16b3d4c0a999ed8953cfd668a010360e05260207f5a589468edfec83b6b027d7dc5bb900ed2921f1620aae0da1aa294cc3ef1dd717fa5a76b97120137c494fd82823a446ff12d6de0e9df551f25e55d6b33c10e236f01a17f101aa2511b4501cd6f4bedff54bee2f014409dfdf0c34e4a64b6f4f4e72d12a17f6aaf1061dc80372e146e902c25be0914f9997523a728f840c73791d7d1c5554e7fed7a429f015e2d1de96ca3ecadf167436726c87d156b2102064cdc2db8a65eba7f8a0afd773820c30d4395e19e96c0e921f3358da90bb44eb99631bbcc1afc52d27f2933690f7726bee93b2fb03b006b8c4f0cd1d2602dd08d3884e88cb07c9985557ff498467872da19bd835f3a0d95241a369f38625609e70acb1761497dddfe741101010101516060201861069f43405d6106b4577f7d32a137160083987c5b53f4a7153ac04f8d6c93569d179ad4df3963572af76b7f9650daf8bc6e9af151f6c7125f922329197841d23a7933641b5c2b5a836f208d7f0d973dfa900ec9b8498e449d6a71d2eed81d94c326c4acc325db9c6420acc6bd01037fc86afc4c1f60387b16cce4e4b7c47baf30b9fc18389a2563b2afbe90f43b938b7f9de848aa2e1f7fadbc76fdeb1e6d86e23f62c033260728029884a9e1533cf0907f4535f7c4dddcbd2e353778b864f7e5aadeb48786211942f0066b5715f41e13447fcd1941de2602e740c239ec7184cd0608b8cebb856e7243bca25b386695fc5d4f7f11fea6990cda43a993f3c8cb366da2c23305d78b65ad90ea8578bc704a1b53617f81385805084428d13c1994377979238f1c9f574353e344bf9674b56f2380894f031818010160e0527fb861afb70639f08b7f0a674d3ce0216ce6746772b2c753574d99d19c2507759b7f479e5048f9c60f7480f598b2c31fde93198b988d4d38aca8b2662e63daf88a85017f5a589468edfec83b6b027d7dc5bb900ed2921f1620aae0da1aa294cc3ef1dd717fa5a76b97120137c494fd82823a446ff12d6de0e9df551f25e55d6b33c10e236f01a1610a286002604435181861069f43405d6106b4577f6751ff9969e5beee7bb9fd6731aba2e2a213bd96a1f54b0a24d11452a915ab967fa3f4105dff5270ad5b285974bd7f411880965825ce709ecdb52d7d484df31bfd7f493267cb0f8113a9bd75b52869d3344723cd0d18ac091431f0c8c0557d4d69c37f8a7d2dab1437a704175dec5cd4ac755fa35b553d62798afc45e5bd1d30be723e180360e052602060e0a1600160e052602060e06106a843405d6106b4565b5f60e052602060e05bf35b505b5f5ffd5b5f80fd5b43405c8081181856

We can use ethervm.io/decompile to decompile the bytecode to the original source code. And ethervm.io to view the opcode of decompiled code.

So the decompiled code have three LOG1 instructions that emit the flag in three parts. The first part is emitted in raw bytes, the second part is emitted in base58 encoding, and the last part has to be xor-ed with the second part.

alt text

First Part

The first part is emitted in raw bytes.

02A9    7F    PUSH32 0xadaf670881744dc7aa596a2d1eb68dbc21fef929481f4bc6ec75331d53acbd2e
02CA    7F    PUSH32 0x61ec705ea65e9bbf5b5a26cc5fa4f55ad1074377dce1ffbc52d65a8816af56a4
02EB    7F    PUSH32 0x9c1a44f72090b2084eff4360bf11986150f7b5b16b3d4c0a999ed8953cfd668a
030C    01    ADD
030D    03    SUB
030E    60    PUSH1 0xe0
0310    52    MSTORE
0311    60    PUSH1 0x20
0313    7F    PUSH32 0x5a589468edfec83b6b027d7dc5bb900ed2921f1620aae0da1aa294cc3ef1dd71
0334    7F    PUSH32 0xa5a76b97120137c494fd82823a446ff12d6de0e9df551f25e55d6b33c10e236f
0355    01    ADD
0356    A1    LOG1

Here is the more readable version of the first part.

A = ADD(0x9c1a44f72090b2084eff4360bf11986150f7b5b16b3d4c0a999ed8953cfd668a, 0x61ec705ea65e9bbf5b5a26cc5fa4f55ad1074377dce1ffbc52d65a8816af56a4)
A = SUB(A, 0xadaf670881744dc7aa596a2d1eb68dbc21fef929481f4bc6ec75331d53acbd2e)

// Offset and store A at B
B = 0xe0
STORE A at B

// Size
C = 0x20

// Topic
D = ADD(0xa5a76b97120137c494fd82823a446ff12d6de0e9df551f25e55d6b33c10e236f, 0x5a589468edfec83b6b027d7dc5bb900ed2921f1620aae0da1aa294cc3ef1dd71)

LOG1(D, C, B)

Second Part

The second part is emitted in base58 encoding.

0495    7F    PUSH32 0xc86afc4c1f60387b16cce4e4b7c47baf30b9fc18389a2563b2afbe90f43b938b
04B6    7F    PUSH32 0x9de848aa2e1f7fadbc76fdeb1e6d86e23f62c033260728029884a9e1533cf090
04D7    7F    PUSH32 0x4535f7c4dddcbd2e353778b864f7e5aadeb48786211942f0066b5715f41e1344
04F8    7F    PUSH32 0xcd1941de2602e740c239ec7184cd0608b8cebb856e7243bca25b386695fc5d4f
0519    7F    PUSH32 0x11fea6990cda43a993f3c8cb366da2c23305d78b65ad90ea8578bc704a1b5361
053A    7F    PUSH32 0x81385805084428d13c1994377979238f1c9f574353e344bf9674b56f2380894f
055B    03    SUB
055C    18    XOR
055D    18    XOR
055E    01    ADD
055F    01    ADD
0560    60    PUSH1 0xe0
0562    52    MSTORE
0563    7F    PUSH32 0xb861afb70639f08b7f0a674d3ce0216ce6746772b2c753574d99d19c2507759b
0584    7F    PUSH32 0x479e5048f9c60f7480f598b2c31fde93198b988d4d38aca8b2662e63daf88a85
05A5    01    ADD
05A6    7F    PUSH32 0x5a589468edfec83b6b027d7dc5bb900ed2921f1620aae0da1aa294cc3ef1dd71
05C7    7F    PUSH32 0xa5a76b97120137c494fd82823a446ff12d6de0e9df551f25e55d6b33c10e236f
05E8    01    ADD
05E9    A1    LOG1

Third Part

The third part has to be xor-ed with the second part.

05FE    7F    PUSH32 0x6751ff9969e5beee7bb9fd6731aba2e2a213bd96a1f54b0a24d11452a915ab96
061F    7F    PUSH32 0xa3f4105dff5270ad5b285974bd7f411880965825ce709ecdb52d7d484df31bfd
0640    7F    PUSH32 0x493267cb0f8113a9bd75b52869d3344723cd0d18ac091431f0c8c0557d4d69c3
0661    7F    PUSH32 0x8a7d2dab1437a704175dec5cd4ac755fa35b553d62798afc45e5bd1d30be723e
0682    18    XOR
0683    03    SUB
0684    60    PUSH1 0xe0
0686    52    MSTORE
0687    60    PUSH1 0x20
0689    60    PUSH1 0xe0
068B    A1    LOG1

Python Script

Here is the python script to get the flag.

import base58

flag = ''

a = 0x9c1a44f72090b2084eff4360bf11986150f7b5b16b3d4c0a999ed8953cfd668a + 0x61ec705ea65e9bbf5b5a26cc5fa4f55ad1074377dce1ffbc52d65a8816af56a4
a -= 0xadaf670881744dc7aa596a2d1eb68dbc21fef929481f4bc6ec75331d53acbd2e
a %= (2**256) # Stay in 256-bit
print(hex(a))
data = bytes.fromhex(hex(a)[2:])
data = data.replace(b'\x00', b'')
flag += data.decode()

b = 0x81385805084428d13c1994377979238f1c9f574353e344bf9674b56f2380894f - 0x11fea6990cda43a993f3c8cb366da2c23305d78b65ad90ea8578bc704a1b5361
b ^= 0xcd1941de2602e740c239ec7184cd0608b8cebb856e7243bca25b386695fc5d4f
b ^= 0x4535f7c4dddcbd2e353778b864f7e5aadeb48786211942f0066b5715f41e1344
b += 0x9de848aa2e1f7fadbc76fdeb1e6d86e23f62c033260728029884a9e1533cf090
b += 0xc86afc4c1f60387b16cce4e4b7c47baf30b9fc18389a2563b2afbe90f43b938b
b %= (2**256) # Stay in 256-bit
print(hex(b))
data = bytes.fromhex(hex(b)[2:])
data = data.replace(b'\x00', b'')
flag += base58.b58decode(data).decode()

c = 0x8a7d2dab1437a704175dec5cd4ac755fa35b553d62798afc45e5bd1d30be723e ^ 0x493267cb0f8113a9bd75b52869d3344723cd0d18ac091431f0c8c0557d4d69c3
c -= 0xa3f4105dff5270ad5b285974bd7f411880965825ce709ecdb52d7d484df31bfd
c %= (2**256) # Stay in 256-bit
print(hex(c))
c ^= b
c %= (2**256) # Stay in 256-bit
print(hex(c))
data = bytes.fromhex(hex(c)[2:])
data = data.replace(b'\x00', b'')
flag += data.decode()

print(f'Flag: {flag}')

Mimirev

Category

Reverse Engineering

Tags

Flag

PWNME{R3v3rS1ng_Compil0_C4n_B3_good}

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{.........................}

by Lxt3h


Given a file mimicompiler which is a binary file. We can open it in Ghidra and see the decompiled code.

alt text

So we can see that the binary is a compiler and interpreter for the language. The binary has some functions like github_com_Lexterl33t_mimicompiler_compiler__ptr_Compiler_Compile and github_com_Lexterl33t_mimicompiler_vm__ptr_VM_Run. We found interesting statement parser function github_com_Lexterl33t_mimicompiler_compiler__ptr_Parser_VerifyProofStatement which is used to verify the proof.

v44[0] = "verifyProof";
v44[1] = 11LL;
v44[2] = v42;
v44[3] = 28LL;
v44[4] = v41;

The statement verifyProof is used to verify the proof. So in the VM namespace we can also see how the verifyProof statement is used. The function github_com_Lexterl33t_mimicompiler_vm__ptr_VM_VerifyProof is used to verify the proof.

alt text alt text

So it will pop the top two values from the stack and compare them. The first value is stored in the v84 (y) variable and the second value is stored in the v85 (x) variable.

if ( v85 + v84 == 314159 )
{
    v22 = v85 * v84;
    v23 = (v85 * v85 + v84 * v84 * v84 - v85 * v84) % 1048573;
    if ( v23 == 273262 ) {
        // Do some operation and call decryptFlag
    }
}

So we need to find the value of x and y such that x + y = 314159 and (x * x + y * y * y - x * y) % 1048573 = 273262. Because this is compiler and interpreter, we can write the solver in the language itself (because we know the syntax of the language from the decompiled code and its support to solve the problem).

init vv = 0;
init vvv = 1;
init vvvv = 0;

while vvv < 314159 {
    if vvvv == 273262 {
        verifyProof(vvv, vv);
        rea vvv = 314159;
        break;
    } else {
        rea vvv = vvv + 1;
        rea vv = 314159 - vvv;
        rea vvvv = (vvv * vvv + vv * vv * vv - vvv * vv) % 1048573;
    }
}

Why there rea vvv = 314159;? Because the break statement is not working as expected. So we need to assign the value of vvv to 314159 to break the loop.

Compile and running the code with following command:

./mimicompiler -f solver.mimi

Super secure network

Category

Reverse Engineering

Tags

Flag

PWNME{Crypt0_&_B4ndwidth_m4k3s_m3_f33l_UN83474813!!!}

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{.........................}

by Prince2lu


The provided software contains an initialization function, init_module, which appears to set up a secure communication mechanism using Diffie-Hellman key exchange and AES encryption. Below is the relevant portion of the code:

__int64 __fastcall init_module(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6)
{
  _QWORD v7[3]; // [rsp+0h] [rbp-58h] BYREF
  __int64 v8; // [rsp+18h] [rbp-40h]
  __int64 v9; // [rsp+20h] [rbp-38h]
  __int64 v10; // [rsp+28h] [rbp-30h] BYREF
  __int64 v11; // [rsp+30h] [rbp-28h] BYREF
  int v12; // [rsp+3Ch] [rbp-1Ch]
  __int64 v13; // [rsp+40h] [rbp-18h]
  __int64 v14; // [rsp+48h] [rbp-10h]
  unsigned int v15; // [rsp+54h] [rbp-4h]

  v15 = 0;
  v14 = 0LL;
  l1111l11l11l111l = l111lll11l1ll11l(60LL, a2, a3, a4, a5, a6, 0LL, 0LL, 0LL, 0x800000000LL, 0LL, 0LL);
  get_random_bytes(&lll1ll1l111l111l, 8LL);
  v10 = 2LL;
  LODWORD(v9) = 1;
  v7[1] = &l1111l11l11l111l;
  v7[2] = &v10;
  v11 = lll1l11l1l1l1111(2LL, lll1ll1l111l111l, l1111l11l11l111l);// pow
  v7[0] = &v11;
  LODWORD(v8) = 8;
  v15 = crypto_dh_key_len(v7);
  if ( v15 )
  {
    v13 = (int)v15;
    v12 = 17301536;
    v14 = _kmalloc((int)v15, 17301536LL);
    if ( v14 )
    {
      if ( !(unsigned int)crypto_dh_encode_key(v14, v15, v7) && (int)lll1llll111ll1l1(3232235836LL, 3333LL) >= 0 )
      {
        qword_B5C0 = (__int64)sub_E0A;
        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) )
        {
          llll1l1ll1lll1l1(l1ll111lll11lll1, v14, v15);
        }
      }
    }
  }
  return 0LL;
}

The function init_module initializes the module and sets up the network hooks. The Diffie-Hellman key exchange is performed using the crypto_dh_encode_key function. nf_register_net_hook registers the network hooks for the functions sub_E0A and sub_104E where sub_E0A is recv and sub_104E is send.

The function sub_E0A checks for specific magic numbers to determine the next steps:

if ( v2 == 0x86E35DE5 )
      {
        l11lll1l1l1ll1l1(v6);
      }
      else if ( v2 == 0x89E35DE5 )
      {
        if ( lll1ll1111l111ll )
        {
          v11 -= 4;
          ll1lll11ll1ll11l(v6, v11 - 16, v6 + v11 - 16LL);
          v11 -= 16;
          v10 = sub_D4(v11 + 96, 17301536LL);
          if ( v10 )
          {
            sub_123(v10, 96LL);
            v3 = skb_put(v10, v11);
            csum_partial_copy_from_user(v6, 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);
          }
        }
      }

If the magic number is 0x86E35DE5, the function l11lll1l1l1ll1l1 is called. If the magic number is 0x89E35DE5, encrypted data is processed using ll1lll11ll1ll11l.

When 0x86E35DE5 is received, l11lll1l1l1ll1l1 is invoked:

void *__fastcall l11lll1l1l1ll1l1(__int64 *a1)
{
  __int64 v1; // rcx
  __int64 v2; // r8
  __int64 v3; // r9
  void *result; // rax
  __int64 v5; // [rsp+8h] [rbp-30h] BYREF
  _QWORD s[4]; // [rsp+10h] [rbp-28h] BYREF
  __int64 v7; // [rsp+30h] [rbp-8h]

  memset(s, 0, sizeof(s));
  v5 = 0LL;
  v7 = *a1;
  v5 = lll1l11l1l1l1111(v7, lll1ll1l111l111l, l1111l11l11l111l);// sha256
  result = (void *)((__int64 (__fastcall *)(__int64 *, __int64, _QWORD *, __int64, __int64, __int64, __int64 *))l1l1l1l11111111l)(
                     &v5,
                     8LL,
                     s,
                     v1,
                     v2,
                     v3,
                     a1);
  if ( !(_DWORD)result )
  {
    l1l1llll1l1ll11l = sub_23F("aes", 4LL, 128LL);
    sub_2AE(l1l1llll1l1ll11l, s, 32LL);
    result = memset(s, 0, sizeof(s));
    lll1ll1111l111ll = 1;
  }
  return result;
}

This function calculates the SHA-256 hash of the exchanged key and initializes the AES encryption context and l1l1llll1l1ll11l becomes the AES encryption object.

When 0x89E35DE5 is received, ll1lll11ll1ll11l is called:

__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) == 3333 )
    return 1LL;
  if ( !lll1ll1111l111ll )
    return 1LL;
  n = *(_DWORD *)(a2 + 120);
  src = (void *)sub_19C(a2);
  dest = (char *)_kmalloc(n + 20, 17301536LL);
  if ( !dest )
    return 1LL;
  memcpy(dest, src, n);
  get_random_bytes(&dest[n], 16LL);
  ll1lll11ll1ll11l(dest, n, &dest[n]);
  *(_DWORD *)&dest[n + 16] = -1981588507;
  if ( !(unsigned int)llll1l1ll1lll1l1(l1ll111lll11lll1, dest, n + 20) )
    return 1LL;
  kfree(dest);
  kfree_skb(a2);
  return 2LL;
}

ll1lll11ll1ll11l is the function that encrypts the data using AES-CTR. And l1l1llll1l1ll11l variable is the aes object. from l11lll1l1l1ll1l1 called after server sends the public key with 0x86E35DE5 magic number.

The Communication

Because we have pcpapng file, we can extract the data from it and reconstruct the communication between the client and the server.

First, client send dh key, public key, and modulus to the server. Where we can extract the base, public key, and modulus from the pcapng file. Second, server sends the public key to the client. Third, client sends the encrypted data to the server and server decrypts it or vice versa.

The encrypted packets are encrypted using AES-CTR. Packet structure is as follows:

FieldSizeDescription
Encrypted DataVariableEncrypted data using AES-CTR
Nonce16 bytesRandom nonce used for AES-CTR
Magic Number4 bytesMagic number to determine the next steps

Solution

So here is the python code to reconstruct the communication between the client and the server:

import binascii
import os
import struct
import hashlib
from Crypto.Cipher import AES
from Crypto.Util import Counter
from Crypto import Random

def derive_dh_key(server_pubkey, client_privkey, prime):
    key = pow(server_pubkey, client_privkey, prime)
    key_bytes = key.to_bytes(8, byteorder='little', signed=False)
    print("Derived DH Key:", key)
    sha256_hash = hashlib.sha256(key_bytes).digest()
    print("SHA256 Hash:", sha256_hash.hex())
    return sha256_hash

# https://www.alpertron.com.ar/DILOG.HTM
aes_context = None
aes_key = None
base = 0
client_private_key = 1330218279148611220
server_private_key = 1375379614977946020
modulus = 0

def reconstruct_sub_104E(aes_context, data):
    dest = bytearray(len(data) + 20)
    nonce = Random.get_random_bytes(16)
    print("Nonce (Random):", nonce.hex())
    counter = Counter.new(128, initial_value=int.from_bytes(nonce, byteorder='big'))
    cipher = AES.new(aes_key, AES.MODE_CTR, counter=counter)
    dest = cipher.encrypt(data)
    dest += nonce
    dest += struct.pack("<I", 0x89E35BE5)
    return dest

def reconstruct_l11lll1l1l1ll1l1(data):
    global base, client_private_key, modulus, aes_context, aes_key
    
    print("Received Data:", hex(data))
    print("Private Key:", hex(client_private_key))
    print("Base:", hex(base))
    print("Modulus:", hex(modulus))

    v2 = pow(data, client_private_key, modulus)
    print("Shared Key:", v2)

    v3 = hashlib.sha256(v2.to_bytes(8, byteorder='little')).digest()
    print("Derived Key:", v3.hex())

    aes_key = v3
    aes_context = AES.new(v3, AES.MODE_ECB)
    return None

def reconstruct_sub_E0A(aes_context, data):
    global aes_key

    magic_number = struct.unpack("<I", data[-4:])[0]

    print("Magic Number:", hex(magic_number))
    if magic_number == 0x86E35DE5:
        reconstruct_l11lll1l1l1ll1l1(int.from_bytes(data[:-4], byteorder='little'))
        return None
    # elif magic_number != 0x89E35BE5: # 0x86E35DE5
    #     print("Invalid magic number")
    #     return None
    
    nonce = data[-20:-4]
    print("Nonce (Extracted):", nonce.hex())

    dest = data[:-20]

    counter = Counter.new(128, initial_value=int.from_bytes(nonce, byteorder='big'))
    cipher = AES.new(aes_key, AES.MODE_CTR, counter=counter)
    dest = cipher.decrypt(dest)
    return dest

DH_KPP_SECRET_MIN_SIZE = 4 + 3 * 4
CRYPTO_KPP_SECRET_TYPE_DH = 1

class DHParams:
	def __init__(self):
		self.key_size = 0
		self.p_size = 0
		self.g_size = 0
		self.key = None
		self.p = None
		self.g = None

class KPPSecret:
    def __init__(self, type, len):
        self.type = type
        self.len = len

def dh_unpack_data(src, size):
    data = src[:size]
    return data, src[size:]

def dh_data_size(params):
	return params.key_size + params.p_size + params.g_size

def crypto_dh_key_len(params):
	return DH_KPP_SECRET_MIN_SIZE + dh_data_size(params)

def crypto_dh_decode_key(buf, params):
    if not buf or len(buf) < DH_KPP_SECRET_MIN_SIZE:
        return -1

    ptr = bytes.fromhex(buf.decode())

    secret_data, ptr = dh_unpack_data(ptr, struct.calcsize('HH'))
    secret = KPPSecret(*struct.unpack('HH', secret_data))

    if secret.type != CRYPTO_KPP_SECRET_TYPE_DH:
        return -1

    key_size_data, ptr = dh_unpack_data(ptr, struct.calcsize('i'))
    params.key_size = struct.unpack('i', key_size_data)[0]

    p_size_data, ptr = dh_unpack_data(ptr, struct.calcsize('i'))
    params.p_size = struct.unpack('i', p_size_data)[0]

    g_size_data, ptr = dh_unpack_data(ptr, struct.calcsize('i'))
    params.g_size = struct.unpack('i', g_size_data)[0]

    if secret.len != crypto_dh_key_len(params):
        return -1

    if params.key_size > params.p_size or params.g_size > params.p_size:
        return -1

    params.key = ptr[:params.key_size]
    params.p = ptr[params.key_size:params.key_size + params.p_size]
    params.g = ptr[params.key_size + params.p_size:params.key_size + params.p_size + params.g_size]

    if all(b == 0 for b in params.p):
        return -1

    return 0

# Client's send base, public key, and modulus
crypto_dh_encode_key = b'010021000800000008000000010000005c4f31a50c2d980c6b152f4845212e1502'
params = DHParams()
crypto_dh_decode_key(crypto_dh_encode_key, params)

base = int.from_bytes(params.g, byteorder='little')
public_key = int.from_bytes(params.key, byteorder='little')
modulus = int.from_bytes(params.p, byteorder='little')

computed_pubkey = pow(base, client_private_key, modulus)
if computed_pubkey == public_key:
    print("Diffie-Hellman Key Agreement: Success")
else:
    print("Diffie-Hellman Key Agreement Failed")
    exit(1)

# Server's public key
reconstruct_sub_E0A(None, binascii.unhexlify('65027c9877d09804e55de386'))

plaintext = b"Hello, AES-CTR encryption!"
ciphertext = reconstruct_sub_104E(aes_context, plaintext)
print("Encrypted Data (Hex):", ciphertext.hex())

encrypted_data = [
    '010021000800000008000000010000005c4f31a50c2d980c6b152f4845212e1502',
    '65027c9877d09804e55de386',
    ciphertext.hex(),
    'a9962d2730df6c3c2d16f4f7d3022967f4f7e0947e5e03c2c2c026acc8a656f5708a8aa99d05134172f8df5b71ad32f7806743c9529fb6ad7e41c8f97e168034893020a6bb15636ded20bd90e55be389',
    '9e02b2aaf3fe8219de13745bce224b38130db68bf13ee3a2cfc035ab67d5420c5872d2952aa3e4ce43e368c6cd02564341ff59b05a2580bb8b7568d10944c5185739023e86901b98957cc54ae55be389',
    'd529357938cf40ae503b1eea1859efefea69add0d4e0d1a5c35fe217402fd3b81194bd8cf947cc29cb6e0790ef92b4c35741ca9392753c504bc33cf2b37dd856ea02d194e55de389',
    'a87e9bc7f7a3dd312c0146935ffd3e95b8f77643b70e8d7576002cc99bc64d202a3dbf268a7b1358cd163e7e3e9a134fe5e776e3b9695ef25726b78a8c35d2fae438b046e55de389',
    '0dc6f6ba652c3717abd212715e08623d19dc44b856a5c8037a056a060ae3cb6e4dac2b5754bbed2bb7781da6d58d5a4a8e3c320a379cafb5e55be389',
    '30a42a38d76a23bb443e477eb163fe14d52c390e40a262f4b2f0bbcafbd0fd45522d4ae3d05ca27f7942916b87c53cd10d78ed670cf5ae64e55be389',
    '7e70f360d880d8dcc05874c639d46d19c273729f554abc2f0c63a4c4e829c839abd846ee5aab313e453f98103f4682b277372133e57260f9c96076a1a33edbac155ad7882173f8f264aeb8318c06b1e8909ea89e341be7d9cc7abbc957579582a446915d5f4c8f8261b7b6039f1fedd96b4e44a8ba14e1e143f8db7e567bda25fd3c0d6fbee55be389',
    '6877c550aa2cebfc5f899f1847fb5ae6045f8d562277d574bee80647031fb055c7e71efbf9517ccbb0c2446b854dfed44ae867ecbc33dde85258c6f7887180e2d7be7ec4cb76739f39f0e3abb56a1b253594a96f086424c639d4af536ddd57301fcd2a9d35ac43e516d22a042f5c98bd464c0c761cf73514837545c98ff8cffc037af2eec2e55be389',
    '87cf38d005dff7f4059b87f002e425b64a399593ed98470fff9c3828298ba0a9919773d20acf7c86231f37439e8230930393af9bb6806d570f3a9b15afa125ae2021a1f9e55de389',
    '74501ea8ca0ef57a16c74dd16b1481f2ef51fabacfc93220da39d0488b27fb6d293c8a40828e875ae12f52fa65c34b63c6f100bdaf189580e55be389',
    'f1e9b87e501092585285c05b2fe9b3dbffe0961f08d1ce22b8e71fd8b46634e4d811fd78f4006c846495c820691e153595bb1363ab979e5b0dc2fd797ce3e65fe7e68febd2d352725e5f2dc4d1e75a46f88cf0c05dfdfb945fe2be5ea47ccaa68efcad3fa151d2856f37a38a6afbfc922f40895c41288d8839e4220a3de9daf8bd1d0dee4de55be389',
    '95bac19e9a3410287b34474501b83c5fb8ddf90d795160cc958fd739088acb84b318b3d580a3c3527406e0018e67ac3d40aadd6e952e2569e55de389',
    '469a8133423b95ac561f8e162498a4c3652c720fe26c46f14fc709072fb687c759cd623657fc631740b7d1e87a42d5cfee0e369971294d42e55be389',
    'c032ca90eabbceaebf7f5222c19a29980f9a9f483a79b5dc80d571b3f25f6d3c5a8fb7fe2e4dc83d2cfe54032683f5466bd856cf8d040290e55de389',
    'ab5c22714b7d71c53d404f345a3ee217d0fce3fd0f8d625c00b7b505b97449b139390b58d8ef448f43e9a839f84e0a1a35bd44df34340d8ae55be389',
    '4ecc53cd97a0a3ca6961e720c73f49c51a159f584febc30c8ce5be71cdbeacbce4bc61eb5e7ff5b80aaf33f359869d9d6650569b04f030e25b0d27c5225dbf30ae2e80e2cab6ee099f63d1f6b5ae8579fd05e9e2f315989614efe7ff05e237f219568f94c15361e0b2928ea76501ee65343653880c0c07948f954be3d3499ecccb9b34090dc1025c3fbd78b5828b9b3442928a323d3d684f053ca9793cc94bd6d7ab1bbf62a4441c70ecf4854e3d30ee442c673945e2c5f7e2a89129af3f3ad51bdc409209987071ddae949edfb84cf27f85d267fc4eb644c4379dbfe92599e3985149536b3b8511367ca341514812ddd704e55de389',
    'a714fe5ff6c6fb25b6e0c7c4ab1abe6c1524055ad03b862848df5019d52e7e44f90b31197b993f0f2bfa95d24b743bb55bb13a6a6d8034cae55be389',
    '47ece28b01ce83b3adb01403c647dcb5cf37db57c2d01704560ea42523464350f393aa6ab089621da42e602a895b80197d0283c4d20cf82f9b586497cdfdf27873a3abe17f830c84b5765837298868d372033dfb8942acdf1417d88d282e684b9beafdeb64bf7b3cb1e862b273f9f3143687d21169b59a5142ac9b4b7fadf2d8eb9ef6dbc2896cee584b64aee325b30341d25be3df4981dcd1a8dc8bfd5fe705d7b6b5f858253fe0d3c3299ae8d30392aab7f5443b18a9814904883f79bc98aaf8b38f75d12d24b9005458a088b8e209fb34a60d4bc7f4dcbd07321c5546f234565022e96bd56d0f0384ebf11f58f3501db2bbb1712f01480e0d016dcf8d0f7810db5f73cf0eabadce65fe40975cb2dab081f277cb88616b1addcf3c79e63b18a64df7e5cbdc5121a3726908ca6212376fc87a0108517bf3ad63e63537da41cb71197c30694e94309f436c8fc2d1bce9fbd1d4546575201a30f40b44b6b2d6d6216a95e02c2f7355f1635c5f40c0eff9b35696650885d2a2f1e189c4b746bd2ffb119f1a10ec868697e6fa05e3017dd0838cf283e8c8f25c5197b45d03aca6aaa5f43bbc22a0b558f50078edbd8de65efb9b73090dc9c981063ac4b3a3a06e8e59fc34cf5c609cb14d9737962efd9fce75f7e020fa5b3a7e1eafc504873b07617acb5ff545a99d5a5d2d637c44c81256a56179859249afeadb6d03090a67b58dccc780078f11c74729579f7a73b1e72ed1482c24bdc286a59a7a12f3855fd3114cfb1240670ae9315cfaae06a913746e6123af0a106d27a1f1fe6a76aa600f39fa673329535e975a2acf16f35893d6400ec42f06c9c716586606184f0f50a75a6bc03c06b147b76d42cc4bd0caaccb51ad4515c8e64236c283fb7b9cd5e7a2dcfd446a9b39dd8e0ae6081a863f01cc64beafd475db4cd3195aefb007d36cd5701757c2aedec4e161e21e58fab52330a0b370f7eb1e5f2f5ed86c68c77cc60bf3981657636df15cfc9d80633253b6130509a4c67e0071803a11e6511ff978da24afe5edc6e76ed3febd315470f2e98ec312a504e436ada941e89f611942eaeca8b3e33362b71f0346001dc3bc36495d78748d546aca1dbfa4257014abc01a1232a5e13e1661b004e5acd48732e445d71c09559faa2fc987fe637ccabbd1a81149bcede48b257dbdc218fc7bd5822b0792705e6ad628f795fba8b6541127c221717c86df4d1020c0aa5ce31a721f6c86d653a75f2492be6ff4a214c2857b1e2b9fe91a27a20109c179fa89811861aaa8b47eebe3e72e068fef5db2c61be3291b5e0efbd39ddc1248ace8e0d3ae99d76e3fcf0ccb47f5662ca949cf3de176fd7703a1bbd51393af4ef43760c402641201adaf9e5ffdbff8c8d86b6b3fd94475740218cf0c44f5a4fe75f658dfa428fc5b099105cb32183c90f239900d030946fa15836cc6c56762ffe924a34cfdefad56813f9288d7e99111cffbc76ffa1f7216d9944df94656c19e01bdc12e0f318d6aa3728664ef13c663ceee7c7cdb0af0060cd9630a8ca6ee57776b714efe2ec1ac1e90db18691f5dabbf0e9cd2e98d6efd7ce278374d161b37ba23da371eab299e1c13664237d0d07a5309ff656019579ea745553fef5bc8c9982d5f6e9d61363c7e9f10e27ec85b9ec8b8dc980aaff632a0a9684eb8b407bdb8a2a047bd253f50de6bb3260496076681aee55de389',
    'ae79ecfe16d1db7abbd095a05b6e0fafdf90f11874bbd15bfa3dcec70e5cfe95bfd1ef18cce34291b728474ada51f46c29704645b2c19772e55be389',
    '3ab84ffa9c90ab300f2abbae8e2c35de002cd8536dee90e4e51a8a73879bf1ebdb7cbf8f3c49f0413c9af6c1ea823d0852159bf81b22727ae55be389',
    '433ee94cf71e14e11e804dd4273fc8b060004d1f3225ac1b16149a5af54506a1a0807d02034e4bea1c0127438bceb2542ff3272d1879432b54ae3f46a1f7d54e7e735633c416e6d59318386106d2e048eb57029b993348b65ab5bd9ae8b74034046811e3df4e414dffe06d42372d242fd9b67f3289d625b4b55011db8a2762c5c1550f71515c9bbbd252bc5153e8e9238f25037ac6f3cb6e7a00d728a53f996e1b41066e8194d1ceda9c09e2452ccc846c82740718fcd6e4b98404300b1a018f2d6b142af5b0eab8e9b5253d254bbffaf4b2134f980f86bae7e31120dd328c219dd8baf15ce18603ae8c66c6984e44b8fcfda9a386d0fc8c949a7f66bf52dc2d35c3759a25792bd007b61a66effb1dfbeba01eb950fca3d01e359ec88395b44cfa9f3044a2a0ee885bda074ec1d40f1c7bc75e4aaff449c8c0d5a0d6c4dd40fee33a1d3f2022d3c7aef60b9267c2c74860b77cd4e24e9e757764a56fadb9659760aa58768e97f8d2a79ba34b3787e4ae9a398edc0cf26fea2b268861a07b99c1fcea95bfb84641833bf0ef07b47a7efd864f0fcb142e7ab7903efc25587389b805887641f66811080a85a62896ae4da7ca3f38715812e0e95276eb87fc94f99b4261227c8ec8f6cb0b7557a801d4a6e80f85f011e476f775481e0e3dbaa52119b0d6079d9d9344f22ea1a347168cf181b72aeaf10f2c55ab101423c9bfc6392cf8d05ad512f7db5bab16c33ace1976c262ac9cbdb4c5d20923a1bd7126b0bfa6b131ae243cb682a9c22c2a2601440810a1350ae6f7620e47ead954af3b75a37a9844146ac99fd0432d3b661d925e3c670f51f7b305f39677fe2e6279582d44b774788d1f89ac4dfce89af54918ee4ec1b65d572f18f04b3cfea02023805e4222fac9e7515a94eb9a20d7428ee4e93ba9376c690b78d84b71aa501376245c202f93c97a223b03cba94a4c6f64c11682561609adee00bdac101ce53fad42cf6908e8781a252386762d6f7f844d85a25fad7e07689e687bd61ad232a4c895bcf8a4f2ea7a9ab78210ae1c92662f92cf11b1656a757411848198faee8134ea6a439184de5d679b2840798032d67492cb7e14c575154ab23532882b57622b98831078b89efc9b8ef7287bb8705789f3399a5927bb4e63cf83ffaae7f4aa632e7189bddfb8a74ad04c9acfa4800e2503fe468cd1e742e109b85751168cbbe31b15523126f183626c216fb7831946e7814cb6ab0b39e48972cae3323704268af1eb434e260294cf7768fa0eb49244631a940aa9657b3a75ab6927bb6dc35eb0282ac9b28c4ac8f60576f8a2bc3decce4cae4315187726fa44b2fa46af888a07909fc9785308729f3588df48ec4aae0fda24a5787686897cde2c8f22d6339c8a721e30b7cf154f0f47e96f123213b4f3bc0dfe04d9632584d1e4bb5698078eed602bce5a4590e397fca1a22ffbdfc04a2889bf5ae61419f0cae4a8148b9c041cb7fb4b90b2ca5fc20e100a065258ba3f50dcc96a7644a2fc2631c0ee6dd9af55924b166205e618e39b5b481ea9ed327130de4c405d2ea2c7c9ec250296559839fc057012e6087b1e15f902d20b3d84c4660d744e99069292f5ac601e4035457dca02e3482a3f6ffbd4e295a890dbbe4d60d6f7268e3c0fe3ff9d740d35916602a17e8d464afe5f2466daaec058ec3b0f16110cceaa497dbdc44cf85130e55de389',
    '6dbc5f593321fdda9e60488ae01ec3c016e57d14d1341c38f019f7e978a2d88d5d048413bbd3b7b9963ecf2ebd36b6ec95825ec79e9c549b83182602d176c6bd4aee305be55be389',
    'd306bb2ddd23a54385e37f6977fd618cf9bf84c07e0c6c5b95ecb048251528dc1f5cf0d0054d1529106c97cf7b941fdf8234a7aaa988026ce9f761dd2b14c17ff0a298f59190ee4495b070b5a57715cdfd28e44855a839f04e00e3bfb9a2e5674d05c9ad32545f929f9232233f89e0dd1b76ea962a19c06fc808e784a980fb5f5d11c729dfe55de389',
    'fac86ac5117d45b79d54772bdc3be3a4a085b8126f34fa26f9c0a9fde263fb78bd866abd74015588510b5391d670519b1813c2a99d1a8f3ce55be389',
    'fc6b312ca781f99b07689be346cb137c0f6995c5241afbc1e8953edb27101953e5098da8ea85acd1049fc2a44da54d1bac84cd59a3b5bf15e55de389',
    'd324a93477f7ca75fc4e1228be1533d1e259d064e1cd25cc6745d40fa9a236e78012298fb5e5e8309ef10fc518ddc237ab5b5acb3d1a47d0e55be389',
    'f359679fa5ab2e3b4555dbf31d2100820809659901da4b59a07b5881457cb83197192a48c0b404f2a0edf07c30e395d5d89d37081b4cb5aee55de389',
    'a7ae9b48d41337370574d2f43b3fcd0758c6881f8827b259cf4e8aacb525cb838f82a4a8c61bb846da48e332f5289843212548b9a9806a26e55de389',
    'bff04b2f9e14ecf1f4b965ecc11e6fcd2e88556e0299c5866a9db761854b475dbe5c7d1e65de0fde38c479b7a9f2ab788f0b60dbe82c757ce55be389',
    '87c98375dedb36597f3790e2b1459d19887e51c44e720dfbdbb4055ce94823b0decd16e2ca91a940edfe380db4610c2a49b57ddb0924f45ce55de389',
    'd21ba305beec78de830677ccbdb976ceafc5dced0ae3e6465cac995516134a57c53c38bcbeb3359b3cca0be83c7b028563695ce9ee0d27e8e55de389',
    'd29e1e2002732a168f813a12017e8d243bb8b0187cd3ab384fdbb2a0f302a87a7b4bb321f5f7f0f780ebdc38ee9ea0a7657fcab6c34847b9e55be389',
    '7fc806db0dbf3687e542cbadb4dcfce22081de4bb5c4ec88a5f8e8641aa3e59efa57d4bec7f6ebf5a6ab8a12182a9baa493258cf3130c4bad281fff854ef5bd96130ef74e55de389',
    'be2ac8d131ba29199858b34834437c78c10ba0c464b9e6429d9e464c84663961ac298317c5ed43a431e3efdd48e9d13f046b45266ffaea4fe55be389',
    'a45639793f6f469dab76011f6f98314288d32d6b81753273eba6407268255815c700e436a2e94659067e5c29a2855dd36ce4b5ba609698bae55de389',
    '6a5150a543184a7110fb861d9d40cea11cf369283144ba7b8111ad0ee3171fd40a1e792840b114c5eaf58a2ccab6a3604d68380682557150e55de389',
    'b2b571a66d4b2105e553f376f6c1fd056bd1c3bc85d89dbb7f217063b13eb3479e5b9fb688955d9becb298740cfdd6761f740bbc4050624ce55be389',
    '55b677aa5ae672baf4ed57a5271c00b3bd2355bbee129630a616190a0d3c2574c7974c7eb1d70eb957c386a8e93b1cf5460ba6b83649f61de55de389',
    '729d1e26a5161c66fdf49138cc48d1d85f266651cb8d1eef0f5f9ebd1ebf16bf6aca5b196b4bce006043d940f4bc96237c77a332f8492617e55de389',
    '0f88a5f6d3ed13aa736c713d2b55e6e3350e97812e927ff68992e80474b258bce100f77e6bcea980601ab5cbdd266b0c054f0979de197830e55de389',
    '7f8f9e73925b9307d510adcaec8e15f778e39b7a946990880f2b5465a70f67e4cc2aa94e6ff11fd2a721863d5ecf5957e1b2a776ee180ccbe55de389',
    '3be49582026fa8a31e26122f93972e45bd0ae8dd6a0ef50af0850dff24d788b8556e32b990ae1f3f2badd2bad44c8456daa0c9dac594a6905ee3b5630796aa041156a89bafd9cf9e1050260ee55be389',
    '20a3f972b0e8e65fe4f82ab48942adb819fd7549c0a73bc7a54bbe640503b61fb269c73577247fd0b4cca77a4b95fc6a6d637deddded54388bebaa62148c5ed7adad0ed2d91c7fc0b3c9b939e55be389',
    '5bd507e6de008778439d7bc6b3d66a922b7bc86e5dde93400ed4a860deef71ff62ead788b7ba4a7743e11820cc29f057c21cef22f9e2397aadf440493cda5eca4d9a52b7e55de389',
    'a4e5dfe479df5345763142d97dc31e90670d5a0bc12ee94a45da3becc72ad11085481105c33a9612683cfd279e49aa74629b526a945e038fe55be389',
    '1f8ff7a27fd621e7f6c9712e81e59698cf024783ecc6f1f302dbeb06d719e0bfbc4d26e2880c9d60748c05aa407fc0b4579c4529bbaf909fe86213437fce84474538201ee55de389',
    '7c5ec79330f0dd15347b4e73421d61cad5ad435031333d90d9abdd2c40a18d70a59b9e9ed52adcbb2c5fc530575da45f264d806855d10d38e0bff6b43c95f088cb4eca08f7485457fbf53046ee9d337ec2c1cde59b69661c993d22b34c5cf408a5956f697e27d185d54259a83cbc66bc19bb58fcee30b76d810676e510627c0fc6c6e6b5db176c0bb253430440e55be389',
    '4dd28cc81ee865c00fd7b6238dc8e900c29b1e98264d1bea3e429e839ac9711340227b409e20d87c3115d3e4fa70c4066e7939e8f19c07bf57e4fc0faa04d3f4a067b54a57dbd3902fd6e2fa9ba11c37b12620f7d926798d0801deef0537cb928cf039dc2fa2ed1963edf7ee69332bcb32397594e8c173054bce2cd7d3f6876e13b6329a354c7ba5be3e09e66ee55be389',
    '9ca43ef9e258adc9206258bf6cdc15d8552f134e562cc335c81b02ede11f93413aa97bd4002a9bfe8b6667fe77746b812ec4eedd6d74de12f2f4911e85d99af9b977eb7ee55de389',
    '4bece9e86bd821513632d64e803f886f7868cec1b9d66ddf0775f57f83a6602e52de60b93b06fca5c7e19bf46b4a1a58aa7bd9cc853319f6e55be389',
    'bf93516f44535f93f8554ce0ff586d3ea0a798479e6f561ec8d5c44db37a3ce76d8d2fc7ab96ee2ebc9ea47f65b58ce816bc2ebc11d354c34f0fdda912d4ac45b71dd1ff36581307c37696b2f1bb9112d38bdfb4eb777c1b08bab9a18b0f3a089416946aa31dfa60035dd00f07f298812de22e3926cad6318e89debf822d9b8775ea0f27bebf731819e629d4cce55be389',
    '9b3f4e8dd569bb9be528b412d5f7a988d88ce940978e192dff11f28e4e3302eea6cebffef9e62a8297c1faa1264ea31df708106d1c2d7025e55de389',
    '1389f3449b5cbdf51ccfbb53a43cd3cf1bfe82fecba5b362861c238bb4720f2dc93394c63d80b1552836151b162f93bf7a1154879acc9a45e55be389',
    'b508fc1db3bef01cb3d60f0551c873a384b860e5d789761a5cc9d4785ba95c0832116c0a7b1488ab9984077864c5a18b1fab42cbbf513e4bbf0dfca596e7a7f2d37034efb09db5a4e7c2ddf743dc0ecca6df9904aa872d33d0cd423b93d14def085f98a09e0bb87d871edceff7eb82d01da44422e5c415943da05073eba72ebe055744070713bcdedc199d790d4cb6989da036e905eda643adbdd54e4db8b161f4f483e616b479baab3faf69abc7b03bbbbdd5b77ad68b8c46742b6abbb7466e895bd80e7ad0549a6e237bde78f4e1b14f17be879dd4452189391956052fda3e1d0be9928f657ae7aed518df690ff4add6b0e55de389',
    '048a3a2bb3e77d8c465b9e3dd49bb64b2b326df0777f3208f86d30296d91350847fae4427a549092a0a20805fbbda78099b0000cb348fbf0e55be389',
    'c52eeab7009ce2fe5ab0d29bac610794f1f25ed947da88a908a1e1520f55d0f8b3f5988540b20a2302a0c49fd5036de8fa9c0ee5401c47f7513952acc4a5e3141df150e770b016fa9d6e13ed35ecc28224f80c95184355f906ca9a867fcfbbafeda93819ec99ec6c8986f2803df52e9b655ea445a4f775bbc0c7dbda9433bfb480543a6e7b485758c4444c6dde93148738b4fd72dbf29e0b65c97e2e4df36ac1633e00319d7288f8cc9985271f28a0c6eaecb5142d2d3e73ca974f84ad80bdb521b56cb7d969356a009ed14009ad0d71de023a7c290dbd50679759119f38f0a25b35dadf8e9bd9fd1a1b210f00817766ebe19b195d31f64ddde874eeff26c0b586cf8a13558477e9eb9285792a21e2c81ba0d866c9ae18874e4cfe25ace18346d78fa7a1b7dc6be55de389',
    'a4135fb38289797f876a6e852e79c54364bc1a5f3585d45b13fb64f46e9e00f98e34256443202a223d38aa3d703c4166e72b547f7f63884be55be389',
    '73e586a33b33580621f29b8ee2d9f7a76b16978023c9b3c59fa2cff07055c025518e506d1f9f2dcb849750486f6e27070e7a2844987cb9f9e55be389',
    '43bad150964c4e84d9728618d2a9d432d91196bc1b293a1a5f943dace6058ab4fc99c530bf6d6e3dc5412e06656053bbee33cad71fb3fc60abd846b49aefcf61b249b04d7d5b5036b11512880b7efa1431bcb703070ab6bbeaa7f1e8bac33244b0df6c160748914721b70c7899bd2ce0cc3dc1c7c417abf6e71ad92846237fb09e616cbc311bf2a79e0514159a6762bd6f2df177067408798fa1048a5f3cdd8c0cec3215dcea50bfc6d98728fcb2ae67f981080a8b051ee393cce29574014282877280337aacd83e22d5ea9eed7eb1bd0830ad1693837ee94ba884f57734b1490643382006579b2c8bec103c104d846fb799568aea0e06106ec063e6f782a625d922a72ed707e3350ad48e31d73d6dcb509231ab0c3d0c5a2260e2e4fb52b70e419213f4815f22e55de389',
    '912c10d70e4e6be6e9fe05e60738bb5b0c126d209a78fcfd46d8257d8079d1a7b60fbb6c1b8a69603d6059661abc87641b5a614e48f1367952aa4567e368d74bfcbfa843e55be389',
]

print('\n\n')
for data in encrypted_data:
    try: 
        decrypted_text = reconstruct_sub_E0A(aes_context, binascii.unhexlify(data))
        if decrypted_text == None:
             continue
        
        print("Decrypted Message:", decrypted_text.decode('utf-8', errors='ignore'))
    except Exception as e:
        print(e)