← Back to Writeups
CUSTOMMediumCrypto

Хмак! Будь здоров!

XESXOR9/10/20265 min read
#crypto#custom#medium#collision_crafting#hash_compression_attack#linux

Хмак! Будь здоров!

Platform: duckerz | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-01-09 | Status: Solved Techniques: collision_crafting, hash_compression_attack

Summary

Task: HMAC verification service with custom insecure_compare function that compresses 32-byte digest to 4 bytes via block_xor. Solution: Use oracle to get block_xor of target message, craft fake MAC with same compressed value by placing target bytes with zeros.

Хмак! Будь здоров! — duckerz CTF

Description

"Ты никогда не сможешь взломать этот код! Тут все по новейшим технологиям."

Server: tasks.duckerz.ru:30017
Files: apchi.py

Source Code

python
#!/usr/bin/env python3
import os, hmac, hashlib, functools, operator
from flask import Flask, request, abort, jsonify

SECRET = os.getenv("SECRET_KEY", "dev_secret").encode()
FLAG   = os.getenv("FLAG", "DUCKERZ{not_fake_flag}")
FLAG_CMD = b"give_me_flag_plz"

app = Flask(__name__)

def block_xor(digest: bytes) -> bytes:
    out = bytearray()
    for i in range(0, 32, 8):
        out.append(functools.reduce(operator.xor, digest[i:i+8])) 
    return bytes(out)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def insecure_compare(candidate: bytes, real: bytes) -> bool:
    return block_xor(candidate) == block_xor(real)

@app.get("/")
def index():
    return "<!--Empty-->"

@app.post("/oracle")
def oracle():
    data = request.get_json(force=True)
    msg  = data.get("msg", "").encode()
    digest = hmac.new(SECRET, msg, hashlib.sha256).digest()
    return jsonify(block_xor=block_xor(digest).hex())

@app.post("/verify")
def verify():
    try:
        d      = request.get_json(force=True)
        msg    = d["msg"].encode()
        mac_in = bytes.fromhex(d["mac"])
        if len(mac_in) != 32:
            abort(400, "digest must be 32 bytes")
    except Exception:
        abort(400, "bad json")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    real = hmac.new(SECRET, msg, hashlib.sha256).digest()
    if insecure_compare(mac_in, real):
        if msg == FLAG_CMD:
            return FLAG
        return "OK"
    abort(403, "INVALID")

Analysis

Service Architecture

The service provides two endpoints:

  1. /oracle - accepts a message, computes HMAC-SHA256 and returns block_xor of it
  2. /verify - verifies MAC for a message and returns the flag if msg == "give_me_flag_plz"

Vulnerability: Weak Comparison Function

The key vulnerability is in the insecure_compare function: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
def block_xor(digest: bytes) -> bytes:
    out = bytearray()
    for i in range(0, 32, 8):
        out.append(functools.reduce(operator.xor, digest[i:i+8])) 
    return bytes(out)

def insecure_compare(candidate: bytes, real: bytes) -> bool:
    return block_xor(candidate) == block_xor(real)

Problem: Instead of comparing full 32-byte HMACs, the function compresses them to 4 bytes!

How block_xor Works

32-byte HMAC:
[b0 b1 b2 b3 b4 b5 b6 b7] [b8 b9 ... b15] [b16 ... b23] [b24 ... b31]
        ↓                       ↓               ↓              ↓
   XOR of all 8 bytes       XOR of all 8    XOR of all 8   XOR of all 8
        ↓                       ↓               ↓              ↓
       [x0]                   [x1]            [x2]           [x3]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Result: 4 bytes instead of 32!

Collision Space

  • Original HMAC: 2^256 possible values
  • After block_xor: 2^32 possible values
  • Collisions per block_xor: 2^224 (an astronomical number!)

This means that for any 4-byte block_xor there exists a huge number of 32-byte values that produce the same result.

Solution

Attack Strategy

  1. Get block_xor from the real HMAC for "give_me_flag_plz" via /oracle
  2. Construct a fake 32-byte MAC with the same block_xor
  3. Send to /verify and get the flag ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Constructing the Fake MAC

For each 8-byte block, the XOR of all bytes must equal the target byte.

Simplest approach: put the target byte first, remaining 7 bytes are zeros:

Target block_xor: cf b9 e6 2b

Block 0: cf ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 = cf ✓
Block 1: b9 ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 = b9 ✓
Block 2: e6 ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 = e6 ✓
Block 3: 2b ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 ^ 00 = 2b ✓

Fake MAC: cf00000000000000 b900000000000000 e600000000000000 2b00000000000000

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

Exploit

python
#!/usr/bin/env python3
"""
Exploit for "Хмак! Будь здоров!" - duckerz CTF

Vulnerability: insecure_compare uses block_xor which compresses
32-byte HMAC to 4 bytes, creating massive collision space.
"""
import requests

URL = "http://tasks.duckerz.ru:30017"
FLAG_CMD = "give_me_flag_plz"

def main():
    # Step 1: Get block_xor for flag command via oracle
    print("[*] Querying oracle for block_xor...")
    resp = requests.post(f"{URL}/oracle", json={"msg": FLAG_CMD})
    target_xor = bytes.fromhex(resp.json()["block_xor"])
    print(f"[+] Target block_xor: {target_xor.hex()}")
    
    # Step 2: Craft fake MAC with same block_xor
    # Each 8-byte block XORs to corresponding target byte
    fake_mac = bytearray(32)
    for i, target_byte in enumerate(target_xor):
        fake_mac[i * 8] = target_byte
    
    print(f"[+] Crafted fake MAC: {fake_mac.hex()}")
    
    # Step 3: Verify and get flag
    print("[*] Sending to /verify...")
    resp = requests.post(f"{URL}/verify", json={
        "msg": FLAG_CMD,
        "mac": fake_mac.hex()
    })
    
    if resp.status_code == 200:
        print(f"[+] Flag: {resp.text}")
    else:
        print(f"[-] Failed: {resp.status_code} - {resp.text}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

if __name__ == "__main__":
    main()

Execution

bash
$ python3 solve.py
[*] Querying oracle for block_xor...
[+] Target block_xor: cfb9e62b
[+] Crafted fake MAC: cf00000000000000b900000000000000e6000000000000002b00000000000000
[*] Sending to /verify...
[+] Flag: DUCKERZ{REDACTED}

Lessons

  1. Never use custom comparison functions for cryptography - use hmac.compare_digest() or similar
  2. Compressing digests destroys security - 32 bytes → 4 bytes = loss of 224 bits of entropy
  3. The function name was a hint - insecure_compare directly indicates the problem
  4. Oracles are dangerous - even partial information about HMAC can be used for an attack ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Alternative Approaches

The fake MAC could be constructed in other ways:

python
# Option 1: All bytes the same
# If target = 0xcf, then 0xcf ^ 0xcf ^ 0xcf ^ 0xcf ^ 0xcf ^ 0xcf ^ 0xcf ^ 0xcf = 0xcf (for odd count)
# Doesn't work for 8 bytes (even number)

# Option 2: Distribute bits
# target = 0xcf = 11001111
# Can split into: 0x80 ^ 0x40 ^ 0x08 ^ 0x04 ^ 0x02 ^ 0x01 ^ 0x00 ^ 0x00 = 0xcf

# Option 3: Random 7 bytes + computed 8th
import os
for i in range(4):
    block = bytearray(os.urandom(7))
    block.append(target_xor[i] ^ functools.reduce(operator.xor, block))

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

But the simplest approach (target byte + zeros) works perfectly. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍