← Back to Writeups
CUSTOMEasyForensics

Человеческий фактор (Human Factor)

XESXOR9/2/20266 min read
#forensics#custom#easy#aes_xts_decryption#dictionary_attack#encrypted_container_identifica#manual_truecrypt_decrypt#pbkdf2_key_derivation#tcp_stream_reconstruction#truecrypt_password_cracking#linux

Человеческий фактор (Human Factor)

Platform: hackerlab | Category: Forensics | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-01-05 | Status: Solved Techniques: aes_xts_decryption, dictionary_attack, encrypted_container_identification, manual_truecrypt_decrypt, pbkdf2_key_derivation, tcp_stream_reconstruction, truecrypt_password_cracking

Summary

Task: forensics PCAP where an intern uploads a 1 MiB encrypted file over TCP; reconstruct it and decrypt. Solution: carve the TrueCrypt volume from TCP stream 4, crack its weak dictionary password (kittykitty) with hashcat mode 6221 (SHA512+AES-XTS-512), then manually decrypt the AES-XTS volume to recover a FAT12 image holding FLAG.TXT.

Человеческий фактор (Human Factor) — HackerLab

Description

Конфиденциальные данные компании были получены хакерами. Узнайте, как это произошло.

English summary: A single network capture is provided. A plaintext chat in the capture shows a senior employee telling an intern to "encrypt and send your file — just use a normal password." The intern then uploads a file. The goal is to reconstruct that uploaded file, defeat its weak password, and recover the flag inside.

Crucial author hints (platform chat, author Андрей/Exited3n): "true crypt" and "approach it from the very end" (the TrueCrypt backup header lives at the end of the volume; the human factor is the weak dictionary password). ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Analysis

Step 1: PCAP examination

task.zip contains only traffic.pcapng — a loopback (127.0.0.1) capture with 5 TCP streams.

  • Streams 0–3: plaintext ASCII chat on ports 3434/3535 (no length framing):
    • "Hey, did you drop the server yesterday? :)"
    • "Yes, I don't want to think about it."
    • "Okay, intern, I get you. Encrypt and send me your file. Just use a normal password!"
    • "OK"
  • Stream 4 (49883 -> 3535): client uploads exactly 1,048,576 bytes (1 MiB); the server returns 0 application bytes. This is the encrypted file. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Reconstruct the client→server payload of stream 4:

bash
tshark -r traffic.pcapng -q -z follow,tcp,raw,4
# or extract only client->server bytes:
tshark -r traffic.pcapng \
  -Y "tcp.stream==4 && tcp.srcport==49883 && tcp.len>0" \
  -T fields -e tcp.payload | tr -d '\n' | xxd -r -p > upload.bin

upload.bin: 1,048,576 bytes, md5 6a65732a3cd19da359cc020d1bc852b1.

Step 2: Identifying the container

- Entropy ~7.997 across every window (full entropy)
- NO file header, NO `Salted__` prefix, NO recognizable magic at offset 0
- Length is a multiple of 16 and exactly a round 1 MiB
- All 16-byte blocks unique (no ECB/CTR keystream reuse)

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Headerless + full entropy + round size = an encrypted disk container, not a raw cipher over plaintext. TrueCrypt/VeraCrypt have no plaintext magic because the header itself is encrypted. The author hint "true crypt" confirms it: this is a TrueCrypt volume.

This identification is the whole challenge. Once you stop trying to "guess the AES mode" and treat it as a TrueCrypt container, the rest is mechanical.

Solution

Step 3: Crack the password with hashcat

hashcat reads the raw TrueCrypt container file directly for modes 62xx — do NOT feed it truecrypt2hashcat.py output, the $truecrypt$ string format will not load. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Iterate over the TrueCrypt legacy sub-modes:

ModeKDF + cipher
6211RIPEMD160 + AES XTS512 (default)
6212/6213RIPEMD160 cascades
6221SHA512 + AES XTS512
6222/6223SHA512 cascades
6231/6232/6233Whirlpool
6241/6242/6243RIPEMD160 boot-mode
bash
# 6211 exhausted with no hit; 6221 cracked it:
hashcat -m 6221 upload.bin rockyou.txt
# Recovered password: kittykitty

GPU note: on a rented RTX 4090 (vast.ai) mode 6211 runs ~1.4 MH/s vs ~25 kH/s on CPU (~56×). GPU rental is justified here precisely because it is a known slow PBKDF2 KDF + a dictionary password + a correctness oracle (TrueCrypt header validation gives a hard yes/no). ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 4: Decrypt the volume manually

The vast.ai GPU container had no device-mapper (dm_mod), so cryptsetup open --type tcrypt failed. Solution: decrypt in pure Python (pycryptodome). (Alternatively use veracrypt/tcplay on a host with dm support.)

TrueCrypt SHA512 + AES-XTS-512 layout:

  • salt = first 64 bytes of the volume.
  • Header key = PBKDF2-HMAC-SHA512(password, salt, iterations=1000, dklen=64) → key1 (data, 32B) | key2 (tweak, 32B).
  • Decrypt encrypted header [64:512] with AES-XTS (sector 0); verify magic == b'TRUE'.
  • Master keys at decrypted-header offset 192..256 (mk1 [192:224], mk2 [224:256]).
  • Volume data starts at offset 131072; decrypt with AES-XTS using master keys, sector number = data_offset / 512. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
python
#!/usr/bin/env python3
# TrueCrypt volume decryptor: SHA512 + AES-XTS-512 (hashcat mode 6221)
import sys, hashlib, struct, re
from Crypto.Cipher import AES

PASSWORD = b"kittykitty"
data = open("upload.bin", "rb").read()
salt       = data[:64]      # first 64 bytes = salt
header_enc = data[64:512]   # encrypted header (448 bytes)

dk = hashlib.pbkdf2_hmac("sha512", PASSWORD, salt, 1000, 64)
key1, key2 = dk[:32], dk[32:64]

def xts_decrypt(ct, k1, k2, sector_start=0, sector_size=512):
    out = bytearray()
    enc_tweak = AES.new(k2, AES.MODE_ECB)
    dec = AES.new(k1, AES.MODE_ECB)
    for i in range(0, len(ct), sector_size):
        block = ct[i:i+sector_size]
        sector = sector_start + i // sector_size
        t = int.from_bytes(enc_tweak.encrypt(struct.pack("<Q", sector) + b"\x00"*8), "little")
        for j in range(0, len(block), 16):
            tb = t.to_bytes(16, "little")
            xblk = bytes(a ^ b for a, b in zip(block[j:j+16], tb))
            pblk = dec.decrypt(xblk)
            out += bytes(a ^ b for a, b in zip(pblk, tb))
            carry = (t >> 127) & 1
            t = ((t << 1) & ((1 << 128) - 1)) ^ (0x87 if carry else 0)
    return bytes(out)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Header is XTS-encrypted as one chunk at sector 0
hdr = xts_decrypt(header_enc, key1, key2, sector_start=0, sector_size=len(header_enc))
print("Header magic:", hdr[:4], "(should be b'TRUE')")
assert hdr[:4] == b"TRUE", hdr[:8].hex()

mk1, mk2 = hdr[192:224], hdr[224:256]   # volume master keys

DATA_OFFSET = 131072
pt = xts_decrypt(data[DATA_OFFSET:], mk1, mk2,
                 sector_start=DATA_OFFSET // 512, sector_size=512)
open("decrypted.bin", "wb").write(pt)

for m in re.finditer(rb"CODEBY\{[^}]*\}", pt):
    print("FLAG:", m.group().decode())

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

decrypted.bin is a FAT12 filesystem (\xeb\x3c\x90MSDOS5.0 ... FAT12) containing FLAG.TXT:

bash
file decrypted.bin            # DOS/MBR boot sector, FAT12
strings decrypted.bin | grep CODEBY
# CODEBY{REDACTED}

Tools

  • tshark / wireshark — TCP stream reconstruction (follow,tcp,raw,4).
  • hashcat — modes 62xx (TrueCrypt) / 137xx (VeraCrypt); reads the raw volume directly. Iterate RIPEMD160 / SHA512 / Whirlpool × XTS 512/1024/1536 × boot variants.
  • python3 + pycryptodome — manual PBKDF2-HMAC-SHA512 + AES-XTS-512 decryption when device-mapper is unavailable. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Lessons

  • Headerless data + entropy ~8.0 + round size ⇒ encrypted container. Stop guessing AES modes; identify the container format.
  • TrueCrypt/VeraCrypt have no plaintext magic — the header is encrypted. Use context and hints.
  • hashcat reads the raw volume for modes 62xx/137xx; the $truecrypt$ text format does not load. Try every sub-mode (default 6211 RIPEMD160 is common, but 6221 SHA512 here).
  • GPU rental is justified only when you have a known slow KDF + dictionary password + a success oracle (TrueCrypt header validation provides this).
  • When cryptsetup/dm_mod is unavailable (containers, restricted hosts), implement the TrueCrypt PBKDF2 + AES-XTS decryption manually in Python. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Dead ends (do not repeat)

  • The string @2FtP=G at offset 796387 in upload.bin is statistical noise (in 1 MiB of random data ~950 coincidental 7-char printable runs are expected), NOT the password.
  • XOR (single-byte and repeating-key, lengths 1–256 via IoC/Hamming/autocorrelation) ruled out conclusively.
  • AES/ChaCha/Salsa/RC4/3DES with passwords derived directly (md5/sha256/PBKDF2/scrypt) over full rockyou = no hit — because it is a TrueCrypt container, not a raw cipher over the plaintext.
  • Covert channels (timing, packet sizes, IP ID, TTL, pcapng comments) all clean; bimodal packet timing was just natural TCP write-loop batching. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍