← Back to Writeups
CUSTOMEasyReversing

CPUID

XESXOR9/2/20264 min read
#reversing#custom#easy#byte_permutation_reversing#cpuid_vendor_string_extraction#disassembly#qemu_cpuid_vendor_override#static_analysis#unicorn_emulation

CPUID

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: byte_permutation_reversing, cpuid_vendor_string_extraction, disassembly, qemu_cpuid_vendor_override, static_analysis, unicorn_emulation

Summary

Walkthrough of the CUSTOM Reversing CPUID. Task: ELF64 PIE reverse challenge that reads the CPU vendor via CPUID leaf 0 twice (with a 'swap the processor' prompt in between) and permutes the vendor-string bytes into a 31-char flag. Solution: r

Description

Generating flag... Half flag done, please insert other processor and press ENTER... OK, flag generated. If it looks like meaningful phrase, it's what you need:

English summary: cpuid.elf is an ELF64 PIE (not stripped, compiled from cpuid.c, C + inline ASM). It prints Generating flag..., sleeps, reads the CPU vendor string via the CPUID instruction (leaf 0), and builds the first half of the flag. It then prints Half flag done, please insert other processor and press ENTER... and blocks on scanf("%c"), waiting for the user to "swap" the CPU. After the keypress it reads CPUID again, builds the second half, and prints the assembled 31-char string. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The joke: you cannot physically hot-swap a CPU mid-process, so the "flag" is the deterministic combined output of two different vendor CPUs — the intended pair is AuthenticAMD (first read) then GenuineIntel (second read).

Analysis

CPUID leaf 0 vendor string layout

mov eax, 0; cpuid returns the 12-byte vendor string spread across three 32-bit registers as little-endian dwords:

  • EBX = v[0:4], EDX = v[4:8], ECX = v[8:12]

So for the two relevant vendors:

VendorEBXEDXECX
GenuineIntelGenuineIntel
AuthenticAMDAuthenticAMD
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The byte permutation

Disassembling main with objdump -d -M intel (relevant range ~0x7ce–0x88c) shows the flag is assembled by a fixed byte permutation. Each source byte is identified by a register-label plus a byte index (0..3), where B=EBX, D=EDX, C=ECX. The assembly performs this with stosb/stosw/shr/shl/bswap on eax reloaded from ebx/ecx/edx.

First half — output[0..13], sourced from CPU #1:

C2 D1 D0 C3 D0 D2 D3 B0 B1 D2 D3 C0 D2 D3

Second half — output[14..30], sourced from CPU #2: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

b0 b2 b3 c3 d0 b0 d1 d0 c3 b3 d0 b3 b0 b1 c0 c1 b3

(31 bytes total.)

Verification (three independent methods)

  1. Manual byte-label simulation in Python (below).
  2. Unicorn full emulation of main with a CPUID hook that injects chosen EBX/ECX/EDX per call.
  3. Running the real binary under Docker linux/amd64: on Apple Silicon, Rosetta reports GenuineIntel to the x86 binary, which printed exactly enilieIGeeIneIGnuliGniluiuGentu — matching the GenuineIntel, GenuineIntel model. Vendor control was demonstrated with qemu-x86_64 -cpu qemu64,vendor=GenuineIntel ./cpuid.elf and vendor=AuthenticAMD (QEMU TCG default vendor is AuthenticAMD). ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Since a real CPU cannot be swapped during one process run, the fix is to make the two CPUID reads return different vendors: AMD for the first call, Intel for the second. Reversing the permutation and feeding AuthenticAMD then GenuineIntel reproduces the flag directly.

Solve script

python
#!/usr/bin/env python3
# Reproduce the CPUID flag from the reverse-engineered permutation.
first  = ['C2','D1','D0','C3','D0','D2','D3','B0','B1','D2','D3','C0','D2','D3']
second = ['b0','b2','b3','c3','d0','b0','d1','d0','c3','b3','d0','b3','b0','b1','c0','c1','b3']
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# register label -> offset into the 12-byte vendor string; index digit is 0..3
idx = lambda l: {'B': 0, 'D': 4, 'C': 8}[l[0].upper()] + int(l[1])

# v1 = vendor read by CPU #1 (first half), v2 = vendor read by CPU #2 (second half)
build = lambda v1, v2: ''.join(v1[idx(l)] for l in first) + ''.join(v2[idx(l)] for l in second)

print(build("AuthenticAMD", "GenuineIntel"))  # REDACTED

All four Intel/AMD combinations (reference)

CPU #1CPU #2Output
GenuineIntelGenuineIntelenilieIGeeIneIGnuliGniluiuGentu
GenuineIntelAuthenticAMDenilieIGeeIneIAthDeAneDhehAucAh
AuthenticAMDGenuineIntelREDACTED ← flag
AuthenticAMDAuthenticAMDMneDetiAutictiAthDeAneDhehAucAh
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Real-binary alternative

Instead of simulating the permutation, you can run the real binary and force the two vendor reads:

  • Inject vendors per-call with a CPUID hook (Unicorn emulation, or a ptrace-based single-step that rewrites EBX/ECX/EDX after each cpuid — return AMD dwords on the 1st call, Intel dwords on the 2nd).
  • For a single-vendor sanity check, qemu-x86_64 -cpu qemu64,vendor=GenuineIntel ./cpuid.elf (or vendor=AuthenticAMD) overrides the vendor the whole run; combine with a per-call hook to get the mixed AMD→Intel pair. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering byte_permutation_reversing, cpuid_vendor_string_extraction, disassembly, qemu_cpuid_vendor_override, static_analysis, unicorn_emulation was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-02 • CUSTOM • CPUID • Difficulty: Easy