Kitten and fl@g
Kitten and fl@g
Platform: hackerlab | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-06 | Status: Solved Techniques: upx_header_repair, false_flag_identification, patched_libc_function_analysis, xor_chain_decoding, overlapping_memory_write_reconstruction, environment_variable_flag_verification
Summary
Task: UPX-packed statically linked ELF binary (Tamagotchi game) with corrupted UPX header and a false flag in the game-win path. Solution: repaired UPX magic bytes, identified give_flag as a trap, discovered that printf was patched to call hidden __vfxsprintf function containing XOR-based flag construction verified via environment variables.
Kitten and fl@g — Hackerlab
Description
Это AAA-котик? Tags: C, патчинг функций
English summary: we are given a ZIP archive containing kitten.elf — a 64-bit x86 ELF binary, statically linked and UPX-packed. The binary implements a Tamagotchi-style kitten simulator. The task is to find the real flag hidden inside the binary. The author's tags explicitly hint at "C" and "function patching" as the key concepts.
Analysis
Initial Recon
The file kitten.elf is identified as an ELF 64-bit LSB executable, x86-64, statically linked. Running strings reveals UPX markers, confirming it's UPX-packed. However, standard upx -d fails because the UPX magic bytes at offset 0xec have been tampered with — they read Vupx! instead of the expected UPX!. This is a deliberate anti-analysis technique.
UPX Header Corruption
At offset 0xec in the binary, the bytes are lowercase upx preceded by V instead of the standard uppercase UPX! magic. This prevents the UPX decompressor from recognizing the file format.
The False Flag Trap
After unpacking, the binary reveals a Tamagotchi game where you feed, water, and put a kitten to sleep. When all three parameters (satiety, water, sleep) reach 100, the function give_flag() is called. This function:
- Constructs a template string
C0DEBY{F#t_$#t_a#t_aN!_r*v!rs*} - Substitutes values from game state into the template
- Encodes with 24 sequential XOR/ADD/SUB operations
- Compares with hardcoded target bytes
- If matched, decodes and prints a flag:
C0DEBY{F@t_C@t_aN2_rEv2rsE}
This is a FALSE FLAG! The decoded result uses C0DEBY (with zero) instead of CODEBY and doesn't match the flag format. The author explicitly designed this as a honeypot to waste time.
The Real Solution: Patched printf
The key insight comes from the author's hint about "function patching" (патчинг функций). Since the binary is statically linked, all standard library functions are embedded directly in the binary. The author replaced the standard printf implementation with a custom one containing the real flag logic.
The call chain is:
printf(at 0x410c50) → calls__stack_chk_fail_global(at 0x402bac)__stack_chk_fail_global→ calls__vfxsprintf(at 0x402bc1)
The function __stack_chk_fail_global is suspicious — instead of handling stack canary failures (its normal purpose), it redirects to __vfxsprintf, which contains the real flag construction and verification logic.
Solution
Step 1: Repair UPX Header
Fix the corrupted magic bytes at offset 0xec to allow standard UPX unpacking:
python read-only • copy allowed#!/usr/bin/env python3 # Fix corrupted UPX magic bytes data = bytearray(open('kitten.elf', 'rb').read()) data[0xec] = ord('U') # was 'u' (lowercase) data[0xed] = ord('P') # was 'p' (lowercase) data[0xee] = ord('X') # was 'x' (lowercase) open('kitten_fixed.elf', 'wb').write(data)
Then unpack:
bash read-only • copy allowedupx -d kitten_fixed.elf -o kitten_unpacked.elf
Step 2: Identify the False Flag
Analyze the give_flag() function and recognize it as a trap. The decoded flag C0DEBY{F@t_C@t_aN2_rEv2rsE} uses C0DEBY (zero instead of O) and doesn't validate against the submission system.
Step 3: Reverse the Hidden __vfxsprintf Function
The __vfxsprintf function performs the following operations:
1. Decode environment variable name "g00d_oR_bad":
Raw bytes 07 50 50 04 3f 0f 32 3f 02 01 04 60 XORed with 0x60 produce g00d_oR_bad.
2. Decode environment variable name "cDb_rEv2rE":
Raw bytes 20 07 21 1c 31 06 35 71 71 31 06 43 XORed with 0x43 (11 iterations) produce cDb_rEv2rE.
3. Build the flag buffer using overlapping movabs instructions:
python read-only • copy allowedimport struct buf = bytearray(28) # Four movabs instructions write 8 bytes each, with overlap at offset 12 struct.pack_into('<Q', buf, 0, 0x742B091215383013) struct.pack_into('<Q', buf, 8, 0x613C0F334924531C) struct.pack_into('<Q', buf, 12, 0x25165A15613C0F33) # overwrites bytes 12-19 struct.pack_into('<Q', buf, 20, 0x502D1E206162331E)
4. Apply individual XOR corrections on specific positions:
python read-only • copy allowedbuf[9] ^= 0x43 buf[11] ^= 0x50 buf[17] ^= 0x55 buf[24] ^= 0x1f buf[1] ^= 0x2f buf[2] ^= 0x2c
5. Check environment variable "g00d_oR_bad":
If getenv("g00d_oR_bad") returns NULL, the function exits without printing anything.
6. XOR the entire 29-byte flag buffer with 0x50:
python read-only • copy allowedfor i in range(29): buf[i] ^= 0x50
7. Conditional path based on env value:
If the first byte of g00d_oR_bad equals '1' (0x31), then buf[8] ^= 0x18 (correct path). Otherwise buf[1] ^= 0x7f (wrong path, corrupts the flag).
8. Verify against second environment variable:
Calls getenv("cDb_rEv2rE") and compares its value with the decoded flag buffer using strncmp with length 27.
9. If match: Decodes format string %s\0 (from bytes 0x06, 0x50, 0x23 XORed with 0x23), prints the flag via printf("%s", env_value), and calls exit(0).
Step 4: Compute the Flag
After all transformations, the flag buffer decodes to:
CODEBY{REDACTED}
Step 5: Verify
Run the unpacked binary with the required environment variables:
bash read-only • copy allowedexport g00d_oR_bad=1 export cDb_rEv2rE='CODEBY{REDACTED}' ./kitten_unpacked.elf
The binary prints the flag and exits.
Full Solve Script
python read-only • copy allowed#!/usr/bin/env python3 """ Solve script for Hackerlab - Kitten and fl@g Reconstructs the flag from __vfxsprintf function logic """ import struct # Step 1: Build flag buffer from overlapping movabs instructions buf = bytearray(28) struct.pack_into('<Q', buf, 0, 0x742B091215383013) struct.pack_into('<Q', buf, 8, 0x613C0F334924531C) struct.pack_into('<Q', buf, 12, 0x25165A15613C0F33) # overlaps bytes 12-19 struct.pack_into('<Q', buf, 20, 0x502D1E206162331E) # Step 2: Individual XOR corrections buf[9] ^= 0x43 buf[11] ^= 0x50 buf[17] ^= 0x55 buf[24] ^= 0x1f buf[1] ^= 0x2f buf[2] ^= 0x2c # Step 3: XOR entire buffer with 0x50 for i in range(len(buf)): buf[i] ^= 0x50 # Step 4: Conditional XOR (correct path: g00d_oR_bad == "1") buf[8] ^= 0x18 flag = buf.decode('ascii', errors='replace').rstrip('\x00') print(f"Flag: {flag}") # Output: CODEBY{REDACTED}
Lessons Learned
- In CTF reverse challenges, the obvious path (winning the game → getting the flag) can be a deliberate trap. Always verify the flag format before investing more time.
- Always check if standard library functions have been patched in statically linked binaries —
printf,puts,__stack_chk_failare common targets. - The task description "Это AAA-котик?" and tags "C, патчинг функций" were direct hints pointing to function patching as the core technique.
- The flag name itself is a meta-reference:
REDACTED= "Static Lie Function" — the statically linkedprintffunction was replaced with a "lie" (false implementation) that secretly contained the real flag logic. - When a flag doesn't work, look for hidden code paths, especially in functions that shouldn't contain complex logic (like
__stack_chk_fail).