← Back to Writeups
CUSTOMMediumReversing

Daddy-Long-Arms

XESXOR9/10/20264 min read
#reversing#custom#medium#ld_preload_hook#rand_sequence_forcing#static_aarch64_analysis#xor_decryption#linux

Daddy-Long-Arms

Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-24 | Status: Solved Techniques: ld_preload_hook, rand_sequence_forcing, static_aarch64_analysis, xor_decryption

Summary

Task: AArch64 Tigress-obfuscated ELF that XOR-decrypts a flag with a user-supplied key, but hides a backdoor lucky branch gated behind six fixed rand() return values. Solution: run the binary natively on Apple-Silicon arm64 in Docker and LD_PRELOAD a shim that makes rand() emit the magic sequence, forcing the lucky branch to decrypt the flag with the embedded correct key.

Daddy-Long-Arms — spbctf

Description

qemu-aarch64 -L /usr/aarch64-linux-gnu ./daddy_long_arms.elf

An ARM AArch64 binary (SPbCTF rev-kids 2020, rev/new7) decrypts a flag using a key you pass on the command line. With a wrong key you get garbage. The title "Daddy-Long-Arms" puns on the ARM architecture. Goal: recover the flag without knowing the correct key.

Analysis

Recon

$ file daddy_long_arms.elf
ELF 64-bit LSB pie executable, ARM aarch64, dynamically linked,
interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0,
GLIBC_2.17, with debug_info, not stripped, GCC (Debian 8.3.0)

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

  • Not stripped; the source was processed by Tigress (the binary contains a tigress.c marker and the typical _1_obfuscated_*_$array / _$strings data tables of a Tigress stack-VM interpreter).
  • Imports: srand, rand, time, malloc, printf, puts, __libc_start_main.
  • Symbols of interest:
    • main @ 0x8f4
    • getflag @ 0xc70
    • obfuscated_getflag @ 0x97c
    • obfuscated_getkey @ 0xe44
    • data tables _1_obfuscated_getflag_$array/$strings, _1_obfuscated_getkey_$array/$strings.
  • Strings hint the logic: "Key must be specified as a parameter", "Flag decrypted with your key:", "luck=1" .. "luck=5", "You're lucky today!", "You don't need to guess the correct key, we're giving you the flag for free". ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Apple LLVM objdump disassembles elf64-littleaarch64 fine:

objdump -d daddy_long_arms.elf

Program logic

main:

  1. malloc(35) for the output buffer.
  2. Requires argc > 1, else prints "Key must be specified as a parameter".
  3. Loads argv[1] (the user key).
  4. Calls rand() & 0xff to get a random byte.
  5. Calls getflag(out_buf, argv[1], rand_byte).
  6. Prints "Flag decrypted with your key: %s".

getflag(out, key, randbyte):

  1. Allocates buf40 and buf38 (35 bytes each).

  2. Calls obfuscated_getflag(buf40) — fills buf40 with the encrypted flag bytes. Importantly this function begins with srand(time(0)) (a Tigress stack-VM interpreter that rebuilds bytes from its $array/$strings data).

  3. Calls rand() six times and compares the results against a fixed sequence: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    callvalue (dec)value (hex)
    1313370x7a69
    2120610x2f1d
    3134150x3467
    4233530x5b39
    5238170x5d09
    634210x0d5d
  4. If all six match → the "lucky" branch: prints luck=1..5, the two free-flag messages, calls obfuscated_getkey(buf38) to obtain the correct key, then computes out[i] = buf40[i] ^ buf38[i] (encrypted_flag XOR correct_key) = the real flag, for free.

  5. Otherwise (b.ne -> 0xdd8) it computes out[i] = buf40[i] ^ key[i] with the user-supplied key → garbage unless you already know the key. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The trick / backdoor

The author left a backdoor "lucky" branch that decrypts the flag with the embedded correct key, but gated it behind six specific rand() return values. Because obfuscated_getflag re-seeds with srand(time(0)), the rand() sequence is normally uncontrollable, so the lucky branch effectively never fires — but we can force it.

Solution

  1. The binary runs natively on Apple-Silicon (arm64) inside Docker with --platform linux/arm64 — no qemu-user needed. A gcc:12 arm64 image also lets us compile the LD_PRELOAD hook.

  2. Write an LD_PRELOAD shim that overrides srand() (reset the index) and rand() to emit the magic sequence in order. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    Ordering note: main's rand() runs before obfuscated_getflag's srand(). Our srand() hook resets the index back to 0, so the six checked rand() calls in getflag receive exactly the sequence — regardless of the one earlier rand() consumed by main.

c
// hook.c
#define _GNU_SOURCE
#include <stdlib.h>
static int idx = 0;
static int seq[] = {31337, 12061, 13415, 23353, 23817, 3421};
void srand(unsigned int s){ (void)s; idx = 0; }
int rand(void){
    if (idx < (int)(sizeof(seq)/sizeof(seq[0]))) return seq[idx++];
    return 0;
}

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

  1. Build and run (inside the arm64 container):
sh
gcc -shared -fPIC -o hook.so hook.c -ldl
chmod +x daddy_long_arms.elf
LD_PRELOAD=./hook.so ./daddy_long_arms.elf AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
  1. Output:
luck=1
luck=2
luck=3
luck=4
luck=5
You're lucky today!
You don't need to guess the correct key, we're giving you the flag for free
Flag decrypted with your key:
SPbCTF{REDACTED}

The lucky branch ignores our AAAA... key entirely and XORs the encrypted flag with the embedded correct key returned by obfuscated_getkey. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Flag wordplay

REDACTED is a pun: forcing the lucky branch effectively NOP'd the random gate — and the binary is ARM. The title "Daddy-Long-Arms" is itself an ARM pun. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍