Slowpoke
Slowpoke
Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: deterministic_binary_execution, ld_preload_function_stub, ptrace_antidebug_bypass, signed_char_modulo, static_emulation, xor_identity_collapse
Summary
Walkthrough of the CUSTOM Reversing Slowpoke. Task: x86-64 ELF that takes no input, deterministically builds and prints a flag while stalling with sleep() and guarding with ptrace(PTRACE_TRACEME). Solution: stub out sleep()/ptrace() with an LD_PR
Description
Slowpoke https://rev-kids20.forkbomb.ru/files/rev/re7/slowpoke.elf
A 64-bit Linux ELF binary that takes no input. It deterministically
generates a fixed string and prints it as flag_is_<payload>. The name
"Slowpoke" hints at the many sleep() calls that stall execution between every
step. Goal: recover the printed flag.
Analysis
file / checksec summary:
- ELF 64-bit LSB, x86-64, dynamically linked, not stripped, GCC (Ubuntu 5.4.0) / glibc.
- BuildID
9d14a1ab27788540945649dd0f2eae99f4b71cbc. - Imports of interest:
puts,strlen,strcat,ptrace,sleep,random. - Notable strings:
Go out, dirty debugger!,flag_is_. - The only function of interest is
mainat0x4006f6.
Full algorithm reverse-engineered from main
- Anti-debug.
ptrace(PTRACE_TRACEME, 0, 1, 0)(call at0x40080a). If the return value is non-zero (process already traced), it printsGo out, dirty debugger!and exits with code 1. Otherwise it continues. - A 32-byte buffer
buf(atrbp-0x120) is zeroed. - Init loop (
0x4008a2):buf[i] = ifori = 0..31. - XOR-with-index loop (
0x400917,ifrom 31 down to 0):buf[31-i] ^= i. Sincebuf[31-i]currently equals(31-i), and(31-i) ^ i == 31for everyiin0..31(because31 = 0b11111, so31-i == 31^iwith no borrow), every byte collapses to0x1f(31). A neat constant-folding trick that hides the buffer's real dependency. - XOR-with-key loop (
0x4009a9,ifrom 31 down to 0):buf[31-i] ^= key[i], wherekeyis 32 hardcoded bytes atrbp-0x160..rbp-0x141. Net effect:buf[j] = 31 ^ key[31-j]. - Lowercase-mapping loop (
0x400a48,i = 0..31): if the byte (read as a signed char) is not in('a'..'z'](i.e. not0x60 < c <= 0x7a), thenbuf[i] = (c % 26) + 'a'. Becausecharis signed on x86-64 GCC, the modulo can yield negative remainders, which is why some output characters fall below'a'(e.g.0x5b '[',0x5d ']') — an (almost certainly unintended) signed-char bug that the shipped binary nonetheless relies on. buf[31] = 0— null terminator (0x400b1c).- The 8-byte literal
flag_is_(stored viamovabs 0x5f73695f67616c66) is reversed in place (0x400b4a). strcat(buf, reversed("flag_is_"))(0x400c4e) appends the reversed prefix.- The whole concatenated buffer is reversed again in place (
0x400c62). Net result: the printed string ="flag_is_" + reverse(payload). puts(buf)prints the final string.
Why the noise doesn't matter
Every step is separated by sleep(random()%1000 + 500) (the random()%1000
uses the 0x20c49ba5e353f7cf divide-by-1000 magic constant). These are pure
delays. random() is never seeded, so glibc uses its default seed 1 and
the output is fully deterministic across runs. The sleeps and randomness are
decoys — they have zero effect on the flag.
Hardcoded key (index 0..31)
fb 4b ba 76 77 ed 9d e5 02 d1 9a b1 62 97 53 c0
82 c8 fd ec 12 b9 a4 c9 ce 0c ec 16 dd a5 7e b8
Solution
Because the output depends only on hardcoded data, the fastest path is to just run the binary with the delays and anti-debug stubbed out.
Approach A — LD_PRELOAD stub + run (fastest)
stub.c:
c read-only • copy allowed#include <unistd.h> unsigned int sleep(unsigned int s){ return 0; } /* skip all delays */ long ptrace(int a, ...){ return 0; } /* defeat PTRACE_TRACEME */
Run under Docker on linux/amd64 with ubuntu:16.04 (matches the GCC 5.4.0
build environment):
bash read-only • copy allowedcurl -o slowpoke.elf https://rev-kids20.forkbomb.ru/files/rev/re7/slowpoke.elf docker run --rm --platform linux/amd64 -v "$PWD":/w -w /w ubuntu:16.04 bash -c \ "gcc -shared -fPIC -o stub.so stub.c; chmod +x slowpoke.elf; LD_PRELOAD=/w/stub.so ./slowpoke.elf" # -> flag_is_REDACTED
The ptrace stub returns 0 so the anti-debug check passes; the sleep stub
makes the otherwise-minutes-long run finish instantly.
Approach B — full static emulation in Python
Reproduces the identical output without running the binary, capturing the
crucial signed-char % 26 semantics:
python read-only • copy allowed#!/usr/bin/env python3 import ctypes c8 = lambda x: ctypes.c_int8(x).value # interpret byte as signed char key = [0xfb,0x4b,0xba,0x76,0x77,0xed,0x9d,0xe5,0x02,0xd1,0x9a,0xb1,0x62,0x97,0x53,0xc0, 0x82,0xc8,0xfd,0xec,0x12,0xb9,0xa4,0xc9,0xce,0x0c,0xec,0x16,0xdd,0xa5,0x7e,0xb8] buf = [i for i in range(32)] for i in range(31, -1, -1): # (31-i) ^ i == 31 -> all bytes become 31 buf[31 - i] ^= i for i in range(31, -1, -1): # XOR with reversed key buf[31 - i] ^= key[i] for i in range(32): # signed-char % 26 + 'a' for non-lowercase v = c8(buf[i] & 0xff) if not (0x60 < v <= 0x7a): q = int(v / 26) # C truncates toward zero buf[i] = (c8(v - q*26) + 0x61) & 0xff ft = list(b"flag_is_")[::-1] # reverse prefix full = (buf[:31] + ft)[::-1] # append, then reverse whole print(bytes(full).decode()) # flag_is_REDACTED
The accepted flag on the platform is the payload only — the flag_is_
prefix is just a hint.
Key Takeaways / Lessons
- Mastering deterministic_binary_execution, ld_preload_function_stub, ptrace_antidebug_bypass, signed_char_modulo, static_emulation, xor_identity_collapse was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-06-25 • CUSTOM • Slowpoke • Difficulty: Easy