← Back to Writeups
CUSTOMMediumCrypto

Абсолютная безопасность (Absolute Security)

XESXOR9/10/20264 min read
#crypto#custom#medium#statistical-analysis-of-filter#missing-byte-detection-through#information-leak-via-output-fi#byte-by-byte-plaintext-recover

Абсолютная безопасность (Absolute Security)

Platform: hackerlab | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-01-02 | Status: Solved Techniques: Statistical analysis of filtered XOR outputs, Missing byte detection through sampling, Information leak via output filtering, Byte-by-byte plaintext recovery

Summary

Service: 62.173.140.174:11000

Абсолютная безопасность (Absolute Security) — HackerLab

Description

"Я создал шифровщик флагов, который нельзя взломать" (I created a flag encryptor that cannot be hacked)

Service: 62.173.140.174:11000

Analysis

The service shows the encryption code:

python
async def gen_enc_flag():
    rand_bytes = urandom(23)
    enc = bytes(i ^ j for i, j in zip(flag, rand_bytes))
    for i in range(23):
        assert enc[i] != flag[i], "leak detected"
    return enc.hex()

Vulnerability

The key vulnerability is in the check assert enc[i] != flag[i]: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. XOR property: enc[i] = flag[i] ^ rand_bytes[i]
  2. Rejection condition: enc[i] == flag[i] occurs when rand_bytes[i] == 0
  3. Information leak: The server rejects results where rand_bytes[i] == 0

This means that for each position i:

  • If rand_bytes[i] == 0, then enc[i] = flag[i] ^ 0 = flag[i] — rejected
  • All other values of rand_bytes[i] (1-255) — accepted

Statistical Attack

With enough samples, for each position:

  • All 256 possible values of enc[i] can appear, except one
  • The value flag[i] will never appear at position i (since such cases are rejected) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Formula: By collecting ~2700 samples, with high probability we will see all 255 "allowed" values for each position. The only missing value is the flag byte.

Mathematical Justification

Probability of NOT seeing a specific value after N samples:

  • P(not seen) = (254/255)^N
  • For N=2700: P ≈ (254/255)^2700 ≈ 2.4 × 10^-5

The probability of seeing all 255 values for a single position is very high when N≈2700.

Solution

python
#!/usr/bin/env python3
"""
Абсолютная безопасность (Absolute Security) - HackerLab Crypto
Statistical attack on filtered XOR oracle
"""
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

from pwn import *
from collections import defaultdict

HOST = "62.173.140.174"
PORT = 11000
FLAG_LEN = 23
SAMPLES_NEEDED = 2700  # Enough to see all 255 possible values per position

def collect_samples():
    """Collect encrypted flag samples from the server"""
    samples = []
    
    conn = remote(HOST, PORT)
    
    # Read initial banner/menu
    conn.recvuntil(b"choice")
    
    for i in range(SAMPLES_NEEDED):
        conn.sendline(b"1")  # Request encrypted flag
        response = conn.recvline().decode().strip()
        
        # Parse hex-encoded ciphertext
        if len(response) == FLAG_LEN * 2:  # 23 bytes = 46 hex chars
            enc_bytes = bytes.fromhex(response)
            samples.append(enc_bytes)
        
        if (i + 1) % 500 == 0:
            print(f"Collected {i + 1} samples...")
        
        conn.recvuntil(b"choice")
    
    conn.close()
    return samples
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def find_missing_bytes(samples):
    """For each position, find the byte value that never appears"""
    flag = bytearray(FLAG_LEN)
    
    for pos in range(FLAG_LEN):
        # Collect all seen values at this position
        seen = set()
        for sample in samples:
            seen.add(sample[pos])
        
        # Find the missing value (should be exactly one)
        all_values = set(range(256))
        missing = all_values - seen
        
        if len(missing) == 1:
            flag[pos] = missing.pop()
            print(f"Position {pos}: missing byte = {flag[pos]} ('{chr(flag[pos])}')")
        elif len(missing) == 0:
            print(f"Position {pos}: ERROR - all 256 values seen (need more samples)")
        else:
            print(f"Position {pos}: WARNING - {len(missing)} values missing: {missing}")
    
    return bytes(flag)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def main():
    print(f"[*] Collecting {SAMPLES_NEEDED} samples...")
    samples = collect_samples()
    print(f"[+] Collected {len(samples)} valid samples")
    
    print("\n[*] Analyzing missing bytes per position...")
    flag = find_missing_bytes(samples)
    
    print(f"\n[+] Recovered flag: {flag.decode()}")

if __name__ == "__main__":
    main()

Alternative Solution (faster)

python
#!/usr/bin/env python3
"""
Faster version - stop early when all positions have exactly one missing byte
"""
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

from pwn import *

HOST = "62.173.140.174"
PORT = 11000
FLAG_LEN = 23

def solve():
    conn = remote(HOST, PORT)
    conn.recvuntil(b"choice")
    
    # Track seen values for each position
    seen = [set() for _ in range(FLAG_LEN)]
    
    sample_count = 0
    while True:
        conn.sendline(b"1")
        response = conn.recvline().decode().strip()
        
        if len(response) == FLAG_LEN * 2:
            enc = bytes.fromhex(response)
            for i in range(FLAG_LEN):
                seen[i].add(enc[i])
            sample_count += 1
        
        conn.recvuntil(b"choice")
        
        # Check if we can determine the flag
        if sample_count % 100 == 0:
            missing_counts = [256 - len(s) for s in seen]
            if all(c == 1 for c in missing_counts):
                break
            print(f"Samples: {sample_count}, Missing per pos: {missing_counts[:5]}...")
    
    conn.close()
    
    # Extract flag
    flag = bytearray(FLAG_LEN)
    for i in range(FLAG_LEN):
        missing = (set(range(256)) - seen[i]).pop()
        flag[i] = missing
    
    return bytes(flag)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

print(solve().decode())

Output

[*] Collecting 2700 samples...
Collected 500 samples...
Collected 1000 samples...
Collected 1500 samples...
Collected 2000 samples...
Collected 2500 samples...
[+] Collected 2700 valid samples

[*] Analyzing missing bytes per position...
Position 0: missing byte = 67 ('C')
Position 1: missing byte = 79 ('O')
Position 2: missing byte = 68 ('D')
...
Position 22: missing byte = 125 ('}')

[+] Recovered flag: CODEBY{REDACTED}

Theoretical Background

The "Protection" Paradox

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

The author tried to prevent leaks by checking enc[i] != flag[i]. But this check:

  1. Rejects cases when rand_bytes[i] == 0
  2. Creates a statistical anomaly — one byte never appears
  3. Allows flag recovery through analysis of missing values

Coupon Collector Problem

The task reduces to the "coupon collector problem":

  • Need to collect 255 different values for each position
  • Expected number of attempts: 255 × H(255) ≈ 255 × 6.1 ≈ 1555
  • We take ~2700 with margin for high reliability

Title Irony

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

"Absolute Security" — an ironic name because:

  • The attempt to "detect leaks" created a leak
  • The "protection" became a vulnerability
  • The flag REDACTED confirms the irony ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍