Tape ton MDP

Catégorie: Analyse forensique - Difficulté: Moyen

Description:

Solution:

Ici nous avons une capture réseau qui exfiltre des données vers l'extérieur. À la vue du titre du chall ainsi que sa description, il s'agit d'un keylogger, nous allons donc devoir reconstituer ce que la personne a tapé au clavier.

Pour ce faire, après analyse manuelle (et avec une IA 🙃), nous trouvons les données intéressantes se trouvent à chaque requête HTTP vers /upload :

Maintenant pour déchiffrer tout cela, nous pouvons utiliser le script suivant :

import base64
import struct
import re

# AZERTY mapping based on standard US keycodes
AZERTY_LOWER = {
    1: 'ESC', 2: '&', 3: 'é', 4: '"', 5: "'", 6: '(', 7: '-', 8: 'è', 9: '_', 10: 'ç', 11: 'à',
    12: ')', 13: '=', 14: 'BACKSPACE', 15: 'TAB',
    16: 'a', 17: 'z', 18: 'e', 19: 'r', 20: 't', 21: 'y', 22: 'u', 23: 'i', 24: 'o', 25: 'p',
    26: '^', 27: '$', 28: '\n',  # KEY_ENTER
    29: 'L_CTRL', 30: 'q', 31: 's', 32: 'd', 33: 'f', 34: 'g', 35: 'h', 36: 'j', 37: 'k', 38: 'l',
    39: 'm', 40: 'ù', 41: '²', # Grave accent key (typically left of '1' on US layout, maps to '²')
    42: 'L_SHIFT',
    43: '*', # US backslash key, often '*' on AZERTY. Can also be '<' (keycode 86 for that usually)
    44: 'w', 45: 'x', 46: 'c', 47: 'v', 48: 'b', 49: 'n', 50: ',',
    51: ';', 52: ':', 53: '!', 54: 'R_SHIFT', 55: '*', # Numpad *
    56: 'L_ALT', 57: ' ', 58: 'CAPS_LOCK',
    59: 'F1', 60: 'F2', 61: 'F3', 62: 'F4', 63: 'F5', 64: 'F6', 65: 'F7', 66: 'F8', 67: 'F9', 68: 'F10',
    69: 'NUMLOCK', 70: 'SCROLLLOCK',
    71: '7', 72: '8', 73: '9', # Numpad
    74: '-', # Numpad -
    75: '4', 76: '5', 77: '6', # Numpad
    78: '+', # Numpad +
    79: '1', 80: '2', 81: '3', # Numpad
    82: '0', # Numpad 0
    83: '.', # Numpad .
    86: '<', # Often the key for < > on AZERTY
    96: '\n', # Numpad Enter
    98: '/', # Numpad /
}

AZERTY_UPPER = {
    1: 'ESC', 2: '1', 3: '2', 4: '3', 5: '4', 6: '5', 7: '6', 8: '7', 9: '8', 10: '9', 11: '0',
    12: '°', 13: '+', 14: 'BACKSPACE', 15: 'TAB',
    16: 'A', 17: 'Z', 18: 'E', 19: 'R', 20: 'T', 21: 'Y', 22: 'U', 23: 'I', 24: 'O', 25: 'P',
    26: '¨', 27: '£', 28: '\n', # KEY_ENTER
    29: 'L_CTRL', 30: 'Q', 31: 'S', 32: 'D', 33: 'F', 34: 'G', 35: 'H', 36: 'J', 37: 'K', 38: 'L',
    39: 'M', 40: '%', 41: '', # Shift+² is often nothing or same as unshifted ²
    42: 'L_SHIFT',
    43: 'µ', # If unshifted is '*'
    44: 'W', 45: 'X', 46: 'C', 47: 'V', 48: 'B', 49: 'N', 50: '?',
    51: '.', 52: '/', 53: '§', 54: 'R_SHIFT', 55: '*', # Numpad *
    56: 'L_ALT', 57: ' ', 58: 'CAPS_LOCK',
    59: 'F1', 60: 'F2', 61: 'F3', 62: 'F4', 63: 'F5', 64: 'F6', 65: 'F7', 66: 'F8', 67: 'F9', 68: 'F10',
    69: 'NUMLOCK', 70: 'SCROLLLOCK',
    71: '7', 72: '8', 73: '9', # Numpad
    74: '-', # Numpad -
    75: '4', 76: '5', 77: '6', # Numpad
    78: '+', # Numpad +
    79: '1', 80: '2', 81: '3', # Numpad
    82: '0', # Numpad 0
    83: '.', # Numpad .
    86: '>', # Often the key for < > on AZERTY
    96: '\n', # Numpad Enter
    98: '/', # Numpad /
}

EV_KEY = 0x01
KEY_LEFTSHIFT = 42
KEY_RIGHTSHIFT = 54

def solve():
    collected_raw_tokens = []
    try:
        with open("http_data.txt", "r") as f:
            for line in f:
                line_stripped = line.strip()
                if line_stripped:
                    # Split by comma, but also handle cases where base64 strings might be concatenated
                    # by multipart boundaries if not perfectly clean before.
                    # The previous version's split was likely fine, this is just to be robust.
                    parts = line_stripped.split(',')
                    for part in parts:
                        # Remove potential multipart boundary remnants if any, though unlikely here
                        # as tshark extracts http.file_data which should be cleaner.
                        if '--------------------------' in part:
                            sub_parts = re.split(r"--------------------------[a-f0-9]+(?:--)?\\r\\n", part)
                            for sp in sub_parts:
                                if sp.strip(): collected_raw_tokens.append(sp.strip())
                        else:
                            if part.strip(): collected_raw_tokens.append(part.strip())

    except FileNotFoundError:
        print("Error: http_data.txt not found.")
        return

    if not collected_raw_tokens:
        print("No raw tokens extracted from http_data.txt.")
        return

    all_data_tokens = []
    for token_from_file in collected_raw_tokens:
        token = token_from_file.strip()
        if not token:
            continue

        processed_b64_part = ""
        # Handle the "bGxISEk=" prefix if present
        if token.startswith("bGxISEk=") and len(token) > 8:
            potential_part = token[8:]
            if len(potential_part) == 32:
                processed_b64_part = potential_part
        elif len(token) == 32:
            processed_b64_part = token
        
        if processed_b64_part:
            try:
                # Validate base64 characters and padding
                if not re.fullmatch(r'[A-Za-z0-9+/]*={0,2}', processed_b64_part):
                    continue
                base64.b64decode(processed_b64_part) # Test decode
                all_data_tokens.append(processed_b64_part)
            except Exception:
                pass # Invalid base64

    if not all_data_tokens:
        print("No valid 32-character Base64 data tokens found after filtering and validation.")
        return

    password_chars = []
    shift_pressed = False

    for b64_event_data in all_data_tokens:
        try:
            decoded_data = base64.b64decode(b64_event_data)
            if len(decoded_data) != 24: # sizeof(struct input_event) typically 24 bytes
                continue
            
            # struct input_event: timeval (long long, long long), type (ushort), code (ushort), value (uint)
            # Format: <QQHHI (little-endian, 2x unsigned long long, 2x unsigned short, unsigned int)
            _, _, type_val, code_val, val_val = struct.unpack("<QQHHI", decoded_data)

            if type_val == EV_KEY:
                if code_val == KEY_LEFTSHIFT or code_val == KEY_RIGHTSHIFT:
                    if val_val == 1: # Press
                        shift_pressed = True
                    elif val_val == 0: # Release
                        shift_pressed = False
                elif val_val == 1: # Key press (value=0 is release, value=2 is repeat)
                    char_to_add = None
                    current_map = AZERTY_UPPER if shift_pressed else AZERTY_LOWER
                    
                    char_to_add = current_map.get(code_val)
                    
                    # Fallback for keys that might not be in UPPER map but are in LOWER (e.g. space, enter)
                    if shift_pressed and char_to_add is None and code_val in AZERTY_LOWER:
                         #This case is tricky: if a key like SPACE is pressed with SHIFT, it's still SPACE.
                         #However, for letters, it's different. The current_map logic should handle most cases.
                         #Only if AZERTY_UPPER explicitly misses an entry that AZERTY_LOWER has, this might be useful.
                         pass # char_to_add = AZERTY_LOWER.get(code_val) - this might be wrong for letters.

                    if char_to_add:
                        if char_to_add == 'BACKSPACE':
                            if password_chars:
                                password_chars.pop()
                        # Filter out non-printable control key names unless they are part of the password (e.g. \n)
                        elif char_to_add not in ['L_SHIFT', 'R_SHIFT', 'L_CTRL', 'CAPS_LOCK', 'ESC', 'L_ALT', 'TAB',
                                                 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10',
                                                 'NUMLOCK', 'SCROLLLOCK']:
                            password_chars.append(char_to_add)
        except Exception as e:
            # print(f"Error processing token {b64_event_data}: {e}") # For debugging
            continue
            
    final_password = "".join(password_chars)
    if final_password:
        # The user expects the flag in 404CTF{...} format. 
        # We will print the raw output, and then manually check it.
        print(final_password)
    else:
        print("Could not reconstruct password from events with AZERTY mapping.")

if __name__ == "__main__":
    solve()

Ce script va donc prendre toutes les données exfiltrées et les déchiffrer étape par étape. Cela nous donne donc le résultat suivant :

Nous avons donc le flag affiché, il suffit simplement de lui refaire une petite beauté et tout sera bon :) S'il y a ' et = c'est du au fait que notre script ne prenne pas en compte la touche Alt-Gr afin de faire les accolades {}

🚩FLAG

404CTF{k3yl0gg3r_3xf1ltr4t10n}

Last updated

Was this helpful?