← Back to Writeups
CUSTOMEasyReversing

Тихий страж (Silent Guardian)

XESXOR9/2/20264 min read
#reversing#custom#easy#xor_decryption#static_string_extraction#pipe_fd_redirection#rodata_extraction

Тихий страж (Silent Guardian)

Platform: bug-makers | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-05-27 | Status: Solved Techniques: xor_decryption, static_string_extraction, pipe_fd_redirection, rodata_extraction

Summary

Task: stripped ELF binary that creates a pipe, redirects to fd 69, reads a password, and XOR-decrypts the flag. Solution: extract encrypted data and XOR key from .rodata, perform repeating-key XOR decryption statically.

Silent Guardian — bug-makers CTF

Description

В глубинах системы, затерянный среди множества других, дремлет одинокий процесс. Он не говорит и не слушает обычные пути. Его единственная цель — хранить тайну и ждать того, кто знает волшебное слово. Шепни ему на ухо правильную команду, и он откроет тебе свои секреты.

English summary: A lonely process hides deep in the system. It doesn't speak or listen through ordinary paths. Its only purpose is to guard a secret and wait for someone who knows the magic word. Whisper the right command in its ear, and it will reveal its secrets. Given: a single ELF binary silent_guardian. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Initial Recon

$ file silent_guardian
ELF 64-bit LSB PIE executable, x86-64, dynamically linked (musl libc), stripped

The binary is stripped and linked against musl libc (not glibc). Running strings reveals key artifacts:

  • /flag — the password string
  • YnJ1aA== — a base64-encoded string (decodes to "bruh"), but used as a raw XOR key
  • Function names in imports: pipe, dup2, read, strcmp, putchar, malloc, close, exit

Disassembly (Ghidra/radare2)

The binary's logic follows five stages: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. Pipe creation: Calls pipe() to create a pipe, yielding two file descriptors (read end pipefd[0] and write end pipefd[1]).

  2. FD redirection: Calls dup2(pipefd[0], 69) — redirects the pipe's read end to file descriptor 69 (0x45). This is the "unusual path" referenced in the description. The binary doesn't read from stdin (fd 0) at all.

  3. Close original: Closes the original pipefd[0] since fd 69 now points to the same pipe.

  4. Password check loop: Reads up to 99 bytes from fd 69 via read(69, buffer, 0x63), strips trailing newline, then compares with the string "/flag" using strcmp(). If the input doesn't match, it loops back. If it matches, proceeds to flag output. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  5. Flag decryption: Allocates memory, XOR-decrypts 24 bytes of encrypted data from .rodata using the key "YnJ1aA==" (8 bytes, repeating), outputs each character via putchar(), frees memory, and exits.

Key Insight: Base64 String as Raw Key

The string "YnJ1aA==" is valid base64 that decodes to "bruh", but the binary uses the raw ASCII bytes 59 6e 4a 31 61 41 3d 3d as the XOR key — it never calls any base64 decode function. This is a deliberate misdirection.

Extracted Data

From the binary's .rodata section: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

ItemFile OffsetHexASCII
XOR key0x201859 6e 4a 31 61 41 3d 3dYnJ1aA==
Encrypted flag0x20301b 1b 2d 72 35 07 46 7b 1d 31 7c 08 3e 08 6e 62 0d 26 0f 6e 2a 04 64 40(binary)

Solution

The flag can be recovered entirely through static analysis — no need to run the binary or interact with fd 69.

Extract the encrypted bytes and XOR key from .rodata, then perform repeating-key XOR:

python
#!/usr/bin/env python3
"""
Silent Guardian - Static XOR Decryption
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The binary XOR-encrypts the flag with the raw string "YnJ1aA==" (not base64-decoded).
Extract both from .rodata and decrypt.
"""

# Encrypted flag data from binary offset 0x2030 (24 bytes)
encrypted_flag = bytes.fromhex('1b1b2d723507467b1d317c083e086e620d260f6e2a046440')

# XOR key from binary offset 0x2018 - raw base64 string, NOT decoded
key = b"YnJ1aA=="

# Repeating-key XOR decryption
decrypted = bytes([encrypted_flag[i] ^ key[i % len(key)] for i in range(len(encrypted_flag))])

print(decrypted.decode())  # BugCTF{REDACTED}

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

Verification

B = 0x1b ^ 0x59 = 0x42 ('B')
u = 0x1b ^ 0x6e = 0x75 ('u')
g = 0x2d ^ 0x4a = 0x67 ('g')
C = 0x72 ^ 0x31 = 0x43 ('C')
T = 0x35 ^ 0x61 = 0x54 ('T')
F = 0x07 ^ 0x41 = 0x46 ('F')
{ = 0x46 ^ 0x3d = 0x7b ('{')
F = 0x7b ^ 0x3d = 0x46 ('F')
...

The flag is self-referential: REDACTED tells you that file descriptor 69 was the key to understanding the binary's communication mechanism. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍