REVERSE

MouseTrap

Description

APT27 targeted us and something was leaked can you help and identify what was leaked ?

Goal

Decrypt the PCAP traffic to retrieve the flag.

Challenge

Static Analysis (IDA)

Load MouseTrap.exe into IDA.

Main function

Anti-debugging

  • The calls CreateDesktopA("LuckyMouse") and SwitchDesktop move the process to a private desktop, which blanks your screen and hides any UI on a black/empty desktop.

  • The call NtSetInformationThread(..., 17), an anti-debug tactic that blocks first-chance exceptions and debugging events from reaching WinDbg/IDA.

To debug and analyze, I NOP’d these anti-debug sections.

Patched version

Dynamic Analysis (Procmon)

Setup

  • Configure Procmon to monitor MouseTrap.exe.

Findings

  • Run the malware

The malware tries to open C:\creds.txt. If it doesn’t exist, it exits.

  • Create C:\creds.txt and run again

  • Run the malware again

The malware reads the file via ReadFileEx, encrypts the content, and attempts to connect to 127.0.0.1:13337 to send the encrypted data (the same traffic seen in the PCAP).

Dynamic Analysis (IDA/WinDbg)

Catching the crypto

  • Initial setup

Set a breakpoint on kernel32!ReadFileEx, continue, and configure the debugger not to break on exceptions.

  • After resuming execution, the ReadFileEx breakpoint triggered.

Based on the stack arguments, lpBuffer is 0x005853B0 in .data (dword_5853B0); 0x005853B0 holds the content.

  • Set a hardware read breakpoint on dword_5853B0 and continue.

  • A read triggers inside a routine at sub_401000 function (Encryption)

Identifying the algorithm

  • Search the constants used by the routine

Searching these leads to SPC (a tweakable Lai-Massey block cipher using a SipHash-like core). The implementation matches the public reference (SPC over 64-bit words, 128-bit key, 128-bit block, 56-bit tweak folded into a 64-bit word).

Decrypt the PCAP Traffic

  • I reproduced the SPC round function in Python via GPT 5 and used the key/tweak lifted from .data section.

  • Export the TCP stream from the PCAP as hex and feed it to the script to obtain the plaintext.

Script

# solution.py
from binascii import unhexlify, hexlify

MASK64 = (1 << 64) - 1

def rotl(x, b): return ((x << b) & MASK64) | (x >> (64 - b))

def sipround(v0, v1, v2, v3):
    v0 = (v0 + v1) & MASK64
    v1 = v0 ^ rotl(v1, 13)
    v0 = rotl(v0, 32)
    v2 = (v2 + v3) & MASK64
    v3 = v2 ^ rotl(v3, 16)
    v0 = (v0 + v3) & MASK64
    v3 = v0 ^ rotl(v3, 21)
    v2 = (v2 + v1) & MASK64
    v1 = v2 ^ rotl(v1, 17)
    v2 = rotl(v2, 32)
    return v0, v1, v2, v3

def sha4(round_i, k0, k1, tw, X):
    v0 = 0x50726f736563636f ^ k0
    v1 = 0x43686f636f6c6174 ^ k1
    v2 = 0x01f32d1f4361f48e ^ k0
    v3 = ((round_i & 0xFF) << 56) ^ tw ^ k1
    v3 ^= X
    v0, v1, v2, v3 = sipround(v0, v1, v2, v3)
    v0 ^= X
    v2 ^= 16
    v0, v1, v2, v3 = sipround(v0, v1, v2, v3)
    v0, v1, v2, v3 = sipround(v0, v1, v2, v3)
    return (v0 ^ v1 ^ v2 ^ v3) & MASK64

def sigma(L):
    X = L & 0xffffffff
    return ((X << 32) | (((L >> 32) ^ X) & 0xffffffff)) & MASK64

def unsigma(L):
    X = (L >> 32) & 0xffffffff  # upper 32
    Y = L & 0xffffffff          # lower 32
    return (((Y ^ X) << 32) | X) & MASK64

def le64(b): return int.from_bytes(b, 'little')
def be64(x): return x.to_bytes(8, 'little')

# Keys/tweak lifted from .data segment
KEY  = bytes.fromhex("3d1790908e448599b0829e584cf2cd56")
TWEAK = bytes.fromhex("aa12de3344b4c6f8")

def decrypt_block(ct16, key=KEY, tweak=TWEAK):
    k0 = le64(key[:8]); k1 = le64(key[8:])
    tw = le64(tweak)
    L = le64(ct16[:8]); R = le64(ct16[8:])
    for i in (3, 2, 1, 0):
        L = unsigma(L)
        X = sha4(i, k0, k1, tw, L ^ R)
        L ^= X; R ^= X
    return be64(L) + be64(R)

def decrypt(hex_str, key=KEY, tweak=TWEAK):
    h = ''.join(hex_str.split()).lower()
    if len(h) % 32 != 0:
        raise ValueError("Ciphertext hex length must be a multiple of 32 (16 bytes per block).")
    data = unhexlify(h)
    out = bytearray()
    for off in range(0, len(data), 16):
        out += decrypt_block(data[off:off+16], key, tweak)
    return bytes(out)

def pretty_ascii(b):
    return ''.join(chr(x) if 32 <= x < 127 else '.' for x in b)

if __name__ == "__main__":
    ct_hex = "2f012a1e13a9a54b103424d8873cf1685e61ccc93f9dcae98e7b8f3ed0f862a66c72df99dcf517982af398a726d8d22f881635d66b8d0a5c67582f397a88f8b3131a5bab38430b60a068eaeb3638712f9376b89eb05adb31e8a8472f5060aab8ab880ed106e9d743cc34f7a593b25283bfcc23d8f03ddf79fa319ed355c85e9779ae9ee61878f0832c9b5825a664c8f922303edc220256435fdff0b650d7ea6c88f2961aa53da9f69bb7849f582c79794e5d6aa9bf2f3210013d5bb7c8a2b819dfa8a1ee0f1cf71172995895e5778796624178017ea89df5f949f7bf7a800a7a740d6070117f1742b41eb8cb5bc7f02b42a4476da7657deacf13147e1b879d40d20d0fd34c6d055aca1368b3803384c74d8108b1c58f14d77dc10b3e6d9a095aa9e13ec09f009a7c97e2cd9d883bab15de027e8ffe45552941f2f9e33e16b0f0afe3abbc865ff7c629ca794e4e1c159c53a0174a57f4f94b979e8551796a1418548916de67c13cf83400427edfcce65f41582ea54541f1dacf2a65b90607e123c2718f610e0ae2527ea40b3ad82a6692896d36b65a1e465bb7abcbaf7f44017f8d3535d4c1beeb3ed5be6a061fa3e6d9e656e910f7ed9a3080d7f044786c9a86e54ed1d87c03dde9c8b70703e6376aa65bcc02690a67cbaf1b0cbb3b944383bd87f6ab3f9aa2c29a3a2f260f844ee064ec3ed0e3c84c63979507e9ab3e7e94af9088d622aea6ca505048e859bef1138e5fa9115d1aab7249345aa0e356c781da361f8599a6ae63103f0c5f4754b2e8ce9b43c90e069aef908353b2db47722906bc097009d2fe651f1c9034d7782bd75a4a44050e46d7bcf93038ae1c1da11b5f07bbf8e1f56c26250b1b3636b5f884a5f0176bad7f1a1c74dad8c99e06f691686ce428e71b6cf62143c7e5b6aeb19fe68912bd8183287fec70205eba34fda3a873cdf753ac8389817610a93a04e5478de9a7246d157ac33033d12bb622f266ec6e332b816c87a4ff31adc191a5e7e548eed8ea93ddb5c77cf2ab8c1ab398bfd2ec86b3164fd546e41dbe3ec1054efb632c77fea51e6bd14c10185c8d963d84d9805faabc76bb5a1adbf6be795f4fc57d9bf3527cb0fa90afd1c1f83863161f33ea452c78c298ef1920bc54f7b24c4de64a3aa2312c338dc49efd3bb37029db5680049c85cbc803cc4ef1a013c435e529333f29faffe7fee727c86c08279e6d31005951c55dc571509ac997e24965caea41a3ebd6e99fa3280c0f499774abe2e85abb329a01f55692c12d6acc0bc81d870e584c58d5f3187b967bad9ceb02ab07a130f2864e7663926a1e8885ee76294eaf346e5c8cb0335bc58b1e6f47476448e081e25e09407e4ca6a7f00876843a1494e1c93778d23b7832d3a7d66fa69fb2589ba1f3af974d1dd651b5f7b718f924dc661ead149e9fbd6f11dced48e934f0"
    pt = decrypt(ct_hex)
    print("PT hex  :", hexlify(pt).decode())
    print("PT ascii:", pretty_ascii(pt))

Last updated

Was this helpful?