KPwhy

722 Words · 3 Minutes, 16 Seconds

Challenge Name: rev_kpwhy

Category: Reversing

Difficulty: Easy-Medium

Author: rvsmvs

Challenge Description

Here at BrunnerCorp, we value performance almost as much as we value employee satisfaction. That’s why we’ve rolled out KPIman, our new tracker that assigns every employee a productivity score. Suspiciously, exactly one employee ID ever hit a perfect 100%. Find our model employee and keep your job.

We are handed kpiman, a small dynamically-linked C binary that reads an “employee ID” and grades it. Exactly one badge scores 100% and prints a promotion code — that code is the flag. We need to recover the one ID the checker accepts.

Approach

Step 1 — Triage

$ file kpiman
ELF 64-bit LSB executable, x86-64, dynamically linked, ... not stripped

Not stripped, built from kpimeter.c. nm/strings reveal the whole cast:

calculateSynergy   measureVelocity   assessAlignment          # the three checks
kpi_alpha  kpi_beta  kpi_gamma  synergy_table                 # constant tables
"Productivity: 100%%. Finally, someone who gets it."
"Promotion code: %s"
"Productivity: 0%%. Wrong badge length."

The binary needs GLIBC_2.34 and won’t run on the local box, so this is a pure static solve.

Step 2 — Read main

main reads a line with fgets, strips the newline via strcspn, then requires strlen(buf) == 0x2c (44) — anything else prints “Wrong badge length.” It then calls the three checkers and sums their return values:

score = calculateSynergy(buf) + measureVelocity(buf) + assessAlignment(buf);
if (score == 3) { printf("Productivity: 100%%...\n"); printf("Promotion code: %s\n", buf); }

Each checker returns 1 only if every byte in its slice matches, else 0. So a perfect score means all three constraint sets are satisfied simultaneously, and the badge that satisfies them is printed back verbatim as the promotion code. The 44 bytes are split into three contiguous ranges: [0..14], [15..29], [30..43].

Step 3 — Invert each checker

calculateSynergy — bytes 0..14 (XOR with a per-index constant):

eax = i<<3 - i + 0x2a      ; = 7*i + 42
cl  = buf[i] ^ (al)        ; masked to a byte
cmp cl, kpi_alpha[i]

buf[i] = kpi_alpha[i] ^ ((7*i + 42) & 0xff), directly solvable.

measureVelocity — bytes 15..29 (running-sum chain against an int32 array):

ecx = buf[i] + buf[i-1]
cmp ecx, kpi_beta[i-15]    ; kpi_beta is a DWORD array (lea rax*4)

buf[i] = kpi_beta[i-15] - buf[i-1]. buf[14] is already fixed by stage 1, so the chain unrolls forward deterministically.

assessAlignment — bytes 30..43 (substitution table):

dl = synergy_table[buf[i]]
cmp dl, kpi_gamma[i-30]

⇒ we need synergy_table[buf[i]] == kpi_gamma[i-30], i.e. invert the 256-entry synergy_table. Picking the printable pre-image for each target byte resolves every position uniquely.

Step 4 — Pull the tables and solve

All four tables live in .rodata (vaddr 0x402000 → file offset 0x2000): kpi_alpha @ 0x402020 (15 bytes), kpi_beta @ 0x402040 (15×int32), kpi_gamma @ 0x402080 (14 bytes), synergy_table @ 0x4020a0 (256 bytes).

import struct
data = open("kpiman","rb").read()
off = lambda v: 0x2000 + (v - 0x402000)
alpha  = data[off(0x402020):off(0x402020)+15]
beta   = list(struct.unpack("<15i", data[off(0x402040):off(0x402040)+60]))
gamma  = data[off(0x402080):off(0x402080)+14]
table  = data[off(0x4020a0):off(0x4020a0)+256]

buf = [0]*44
for i in range(15):                              # calculateSynergy
    buf[i] = alpha[i] ^ ((7*i + 42) & 0xff)
for i in range(15, 30):                          # measureVelocity (chained)
    buf[i] = beta[i-15] - buf[i-1]
inv = {}
for idx, val in enumerate(table):                # assessAlignment (inverse LUT)
    inv.setdefault(val, []).append(idx)
for i in range(30, 44):
    cands = inv[gamma[i-30]]
    buf[i] = next(c for c in cands if 0x20 <= c < 0x7f)

print(bytes(buf).decode())

Re-implementing the three checks against the recovered badge confirms synergy=1, velocity=1, alignment=1 (sum 3) — the exact 100% branch.

Flag

brunner{y0ur_kp1s_ar3_n0t_l00king_gr8_buddy}

Reflections and Learnings


reversingstatic-analysisdecompilationxor

Reversing