Home > Writeups > DEADROP Rev 1 - agent_verify

DEADROP Rev 1 - agent_verify

A Linux ELF binary that XOR-encodes the correct passphrase in .rodata. The flag is the passphrase itself, ltrace -s 200 hands it to you directly via strcmp interception.

agent_verify

Overview

A field agent authentication binary, written in 2022 by someone who no longer works here. Run it, poke at it, ask it what it's looking for. It's not subtle.

Step 1: Triage with strings

strings agent_verify

The interesting output:

Usage: %s <passphrase>
ACCESS GRANTED. Welcome, agent.
Proceed to Sub-Basement C for briefing.
ACCESS DENIED. Authorisation failure logged.
NEGATIVE. Identity not confirmed.
CLEARANCE REJECTED. Stand by for escort.
ciphertext
denied

strings won't show the ciphertext itself, it's XOR'd bytes, mostly non-ASCII, so they fall below the printable threshold. The symbol names ciphertext and denied are visible in the stripped symbol table though, which tells you exactly what to look for in the disassembly.

Option A: ltrace (fastest solve)

ltrace -s 200 ./agent_verify anything 2>&1 | grep strcmp
# strcmp("anything", "DEADROP{xor_is_not_encryption_it_is_a_cry_for_help}") = ...

ltrace intercepts library calls. strcmp is called with the decoded expected string as its second argument, handed to you directly. The -s 200 flag is essential; without it ltrace truncates strings to 32 characters and you only see "DEADROP{xor_is_not_encryption_it"...

Note: the binary never prints the flag. The flag is the correct passphrase itself. Patching the binary to skip the comparison just makes it say ACCESS GRANTED to anything, it does not reveal what the passphrase was. You need to recover the plaintext via ltrace or static analysis.

Option B: Ghidra / static analysis

Load the binary and navigate to main. The relevant decompiled logic:

// Count length of ciphertext (pointer walk from ciphertext[1])
do {
    lVar4 = lVar5;
    lVar5 = lVar4 + 1;
} while ((&ciphertext)[lVar4] != '\0');

// Allocate and XOR-decode
__s2 = malloc(lVar4 + 2);
lVar2 = 0;
do {
    __s2[lVar2] = (&ciphertext)[lVar2] ^ 0xab;
    bVar6 = lVar4 != lVar2;
    lVar2 = lVar2 + 1;
} while (bVar6);
__s2[lVar5] = '\0';

// Compare against argv[1]
iVar1 = strcmp((char *)argv[1], __s2);
if (iVar1 == 0) {
    puts("ACCESS GRANTED. Welcome, agent.");
} else {
    // Denial message chosen by first char of input mod 3
    puts(*(char **)(denied + (argv[1][0] % 3) * 8));
}

Key values: XOR key = 0xab, ciphertext is the ciphertext symbol in .rodata. Dump and decode:

ct = bytes([
    0xef,0xee,0xea,0xef,0xf9,0xe4,0xfb,0xd0,
    0xd3,0xc4,0xd9,0xf4,0xc2,0xd8,0xf4,0xc5,
    0xc4,0xdf,0xf4,0xce,0xc5,0xc8,0xd9,0xd2,
    0xdb,0xdf,0xc2,0xc4,0xc5,0xf4,0xc2,0xdf,
    0xf4,0xc2,0xd8,0xf4,0xca,0xf4,0xc8,0xd9,
    0xd2,0xf4,0xcd,0xc4,0xd9,0xf4,0xc3,0xce,
    0xc7,0xdb,0xd6
])
print(bytes(b ^ 0xab for b in ct).decode())

Or extract directly from the binary at file offset 0x2100:

python3 -c "
data = open('agent_verify','rb').read()
ct   = data[0x2100:data.index(b'\x00', 0x2100)]
print(bytes(b ^ 0xab for b in ct).decode())
"

Flag: DEADROP{xor_is_not_encryption_it_is_a_cry_for_help}

Key Takeaway

XOR with a static single-byte key is not encryption. The key is a visible immediate in the decompiled loop, and the ciphertext is a named symbol in .rodata. ltrace recovers the plaintext without even needing to understand the encoding. The denial message selector (input[0] % 3) is a nice distraction but irrelevant to the solve.

< Back to All Writeups