HTB: Pusheen Loves Graphs Challenge

Pusheen Loves Graphs - HackTheBox Challenge Writeup

FieldValue
Challenge NamePusheen Loves Graphs
CategoryMisc
DifficultyEasy
Authord3vn0mi

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

  1. 6,767 labeled basic blocks (e_A_B naming convention) form a 67×101 grid
  2. Block edges form boring, linear chains (101 disjoint chains of uniform length)
  3. 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 extraction
import re
# Parse nm/objdump output to get block addresses and sizes
block_pattern = r'([0-9a-f]+)\s+<e_(\d+)_(\d+)>'
# Extract sizes from disassembly
blocks = {}
for match in matches:
addr, a, b = match.groups()
block_id = (int(a), int(b))
blocks[block_id] = size_in_bytes
# Create 67×101 grid
grid = [[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 Image
import numpy as np
# Convert block sizes to pixel intensities
grid_array = np.array(grid, dtype=np.uint8)
# Normalize sizes to 0-255 range
intensity = ((grid_array - 16) / (113 - 16) * 255).astype(np.uint8)
# Create heatmap
img = Image.fromarray(intensity, mode='L')
# Apply transformations (rotations and flips) to find readable orientation
img_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

Terminal window
# Check file type and basic info
file Pusheen
strings -n 8 Pusheen | grep -i flag
# Look for exported symbols and hints
nm Pusheen | grep -c " e_" # Count basic blocks
nm Pusheen | head -50 # Preview block names
objdump -T Pusheen # Check dynamic symbols

Finding: 6,767 blocks named e_A_B, indicating grid structure.

Step 2: Extract Block Metadata

Terminal window
# Disassemble and extract block boundaries and sizes
objdump -d Pusheen > disassembly.txt
# Parse block structure
nm Pusheen | grep " t e_" | sort > blocks.txt

Step 3: Map Block Sizes to Grid

import re
import subprocess
# Get nm output
output = subprocess.check_output(['nm', 'Pusheen'], text=True)
# Parse block addresses and coordinates
blocks = {}
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 sizes
sorted_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 - addr

Step 4: Render Heatmap

from PIL import Image, ImageDraw
import numpy as np
# Build 67×101 grid
HEIGHT, WIDTH = 67, 101
grid = 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 visualization
min_size = np.min(grid[grid > 0]) # 16
max_size = np.max(grid) # ~113
normalized = ((grid - min_size) / (max_size - min_size) * 255).astype(np.uint8)
# Create and save heatmap
img = Image.fromarray(normalized, mode='L')
img.save('size_heatmap.png')
# Try various rotations and flips
for rot in [90, 180, 270]:
img_rot = img.rotate(rot, expand=False)
img_rot.save(f'size_heatmap_rot{rot}.png')
# Flip transformations
img_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

ToolPurpose
fileBinary identification
stringsExtract readable strings
nmList symbol table (block names/addresses)
objdumpDisassembly and section analysis
Python 3 (PIL, NumPy)Heatmap rendering and image manipulation
grep, sed, awkText processing and parsing

Key Learnings

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Grid Encoding: The 67×101 grid structure is hardcoded in the symbol names; discovering this pattern (via nm output) is the crucial first insight.

Flag

HTB{REDACTED}