HTB: Pusheen Loves Graphs Challenge
Pusheen Loves Graphs - HackTheBox Challenge Writeup
| Field | Value |
|---|---|
| Challenge Name | Pusheen Loves Graphs |
| Category | Misc |
| Difficulty | Easy |
| Author | d3vn0mi |
Challenge Description
Pusheen has an unusual obsession with graphs and reverse engineering tools—specifically IDA Pro. The challenge hints that “cats are weirdly controlling about their reverse engineering tools” and that “Pusheen just won’t use anything except IDA,” suggesting the solution requires static analysis rather than dynamic execution.
The binary provided is intentionally designed to fail under normal execution (SIGILL signals), forcing solvers to analyze it statically using tools like IDA or objdump.
Solution Overview
The binary is a REpsych output—a tool by Chris Domas that generates control flow graphs from assembly. The key insight is that the flag is not hidden in traditional executable code, but rather encoded in the structure of the CFG itself: specifically, in the byte sizes of individual basic blocks.
The Architecture
- 6,767 labeled basic blocks (
e_A_Bnaming convention) form a 67×101 grid - Block edges form boring, linear chains (101 disjoint chains of uniform length)
- The real signal: block byte-sizes encode a bitmap
- Most blocks: 16 bytes (background/noise)
- Signal blocks: 17–113 bytes (varying sizes)
Extraction Method
Extract block sizes, map them to a 67×101 grid, and render as a heatmap where intensity represents byte size:
# Pseudo-code for extractionimport re
# Parse nm/objdump output to get block addresses and sizesblock_pattern = r'([0-9a-f]+)\s+<e_(\d+)_(\d+)>'
# Extract sizes from disassemblyblocks = {}for match in matches: addr, a, b = match.groups() block_id = (int(a), int(b)) blocks[block_id] = size_in_bytes
# Create 67×101 gridgrid = [[blocks.get((a, b), 0) for b in range(101)] for a in range(67)]Visualization and Decoding
Render the grid as an image heatmap, then apply transformations to reveal readable text:
from PIL import Imageimport numpy as np
# Convert block sizes to pixel intensitiesgrid_array = np.array(grid, dtype=np.uint8)
# Normalize sizes to 0-255 rangeintensity = ((grid_array - 16) / (113 - 16) * 255).astype(np.uint8)
# Create heatmapimg = Image.fromarray(intensity, mode='L')
# Apply transformations (rotations and flips) to find readable orientationimg_rot = img.rotate(90)img_flipped = img_rot.transpose(Image.FLIP_TOP_BOTTOM)img_180 = img_flipped.rotate(180)
# The correct transformation reveals: "fun_ / w17h_ / CFGz"# Capital 'U' confirmed by glyph height matching other capitals in "CFGz"Key Steps
Step 1: Reconnaissance and Initial Analysis
# Check file type and basic infofile Pusheenstrings -n 8 Pusheen | grep -i flag
# Look for exported symbols and hintsnm Pusheen | grep -c " e_" # Count basic blocksnm Pusheen | head -50 # Preview block namesobjdump -T Pusheen # Check dynamic symbolsFinding: 6,767 blocks named e_A_B, indicating grid structure.
Step 2: Extract Block Metadata
# Disassemble and extract block boundaries and sizesobjdump -d Pusheen > disassembly.txt
# Parse block structurenm Pusheen | grep " t e_" | sort > blocks.txtStep 3: Map Block Sizes to Grid
import reimport subprocess
# Get nm outputoutput = subprocess.check_output(['nm', 'Pusheen'], text=True)
# Parse block addresses and coordinatesblocks = {}for line in output.split('\n'): match = re.search(r'([0-9a-f]+)\s+t e_(\d+)_(\d+)', line) if match: addr, a, b = match.groups() blocks[(int(a), int(b))] = int(addr, 16)
# Create sorted block list to compute sizessorted_blocks = sorted(blocks.items(), key=lambda x: x[1])
# Calculate block sizes (distance between consecutive block starts)block_sizes = {}for i in range(len(sorted_blocks) - 1): (a, b), addr = sorted_blocks[i] next_addr = sorted_blocks[i + 1][1] block_sizes[(a, b)] = next_addr - addrStep 4: Render Heatmap
from PIL import Image, ImageDrawimport numpy as np
# Build 67×101 gridHEIGHT, WIDTH = 67, 101grid = np.zeros((HEIGHT, WIDTH), dtype=np.uint16)
for (a, b), size in block_sizes.items(): if 0 <= a < HEIGHT and 0 <= b < WIDTH: grid[a, b] = size
# Normalize to 0-255 for visualizationmin_size = np.min(grid[grid > 0]) # 16max_size = np.max(grid) # ~113normalized = ((grid - min_size) / (max_size - min_size) * 255).astype(np.uint8)
# Create and save heatmapimg = Image.fromarray(normalized, mode='L')img.save('size_heatmap.png')
# Try various rotations and flipsfor rot in [90, 180, 270]: img_rot = img.rotate(rot, expand=False) img_rot.save(f'size_heatmap_rot{rot}.png')
# Flip transformationsimg_flip = img.transpose(Image.FLIP_TOP_BOTTOM)img_flip.save('size_heatmap_flipped.png')Step 5: Decode the Message
After applying rotations and flips to the heatmap:
Original grid rendering → rotated/flipped↓Visual pattern emerges: "fun_" / "w17h_" / "CFGz"↓Capital 'U' in "fun_" confirmed by pixel height matching other capitals↓Flag: HTB{REDACTED}Tools Used
| Tool | Purpose |
|---|---|
file | Binary identification |
strings | Extract readable strings |
nm | List symbol table (block names/addresses) |
objdump | Disassembly and section analysis |
Python 3 (PIL, NumPy) | Heatmap rendering and image manipulation |
grep, sed, awk | Text processing and parsing |
Key Learnings
-
REpsych & Graph-Based Obfuscation: Chris Domas’s REpsych tool generates valid executables with massive, redundant CFGs. The challenge exploits this to hide data in the structure rather than executable code.
-
Static-Only Analysis: The SIGILL signals prevent traditional dynamic analysis (running the binary). The hint “won’t use anything except IDA” reinforces that IDA’s graph visualization is the intended approach.
-
Metadata as Signal: Unlike typical steganography, the data here isn’t in bytes or instructions—it’s in block sizes forming a 2D grid. Rendering block sizes as pixel intensities creates a visual bitmap.
-
Image Transformations: The grid requires rotation and/or flipping to become human-readable. This is a common technique in CTF challenges to add an extra layer of obfuscation.
-
Grid Encoding: The 67×101 grid structure is hardcoded in the symbol names; discovering this pattern (via
nmoutput) is the crucial first insight.
Flag
HTB{REDACTED}