← Back to Writeups
CUSTOMMediumSteganography

Агар-агар (Agar-agar)

XESXOR9/2/20265 min read
#steganography#custom#medium#upstream_source_diff#coordinate_grid_plotting#css_comment_stego#glyph_reading#linux

Агар-агар (Agar-agar)

Platform: hackerlab | Category: Steganography | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-10 | Status: Solved Techniques: upstream_source_diff, coordinate_grid_plotting, css_comment_stego, glyph_reading

Summary

Task: a deployed fork of huytd/agar.io-clone hides the flag as stego in a served static asset, teased by a hint about writing dots a special way since school. Solution: diff all served assets against the upstream repo via SHA-256 to isolate a modified css/main.css whose added comment stores ~86 (x,y) coordinate pairs; plotting them in order on a grid spells the phrase wrapped in CODEBY{}.

Агар-агар (Agar-agar) — hackerlab

Description

Лучший Агар-агар! А ещё я по-особенному записываю точки со школы...

(Best Agar-agar! And I record dots/points in a special way since school...)

The challenge deploys a fork of the open-source browser game huytd/agar.io-clone (Express + Socket.IO), titled "Hacker's agar-Agar". Two instances were provided — 62.173.140.174:13002 and 192.169.2.100:13002 (same instance behind both). The server is fragile and crashes after a few Socket.IO / spectator connections. Flag format is CODEBY{...}. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Client hint → flag wrapper. The client bundle js/app.js registers a -mass chat command. Toggling it a second time prints a system line:

Something to decode and add between\tCODEBY{}

So the flag wrapper is CODEBY{} and some decoded text goes inside it.

False lead — in-game food dots. The obvious reading of "точки" (dots/points) is to plot live in-game food coordinates. This is WRONG: food is randomly generated server-side by map.balanceMass, so its positions carry no message. Organizer hints confirmed the intended path:

  • the flag is "in the sources, not in JavaScript",
  • "a stego file was inserted into the distribution",
  • "there are spaces in the flag". ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Correct approach — diff distribution assets against the upstream repo. Because the app is a known open-source project, any inserted stego must live in a file that differs from the original huytd/agar.io-clone. The socket path crashes the server, so all static assets were pulled with single gentle HTTP GETs, then compared by SHA-256 against a fresh clone of the upstream repo:

bash
# gentle asset pull (avoid crash-prone socket path)
for f in index.html css/main.css js/app.js \
         img/feed.png img/split.png audio/split.mp3 audio/spawn.mp3; do
    curl -s "http://192.169.2.100:13002/$f" -o "server1/${f//\//__}"
done
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

git clone https://github.com/huytd/agar.io-clone upstream

# compare each served asset to its upstream counterpart
sha256sum server1/img__feed.png    upstream/client/img/feed.png
sha256sum server1/img__split.png   upstream/client/img/split.png
sha256sum server1/audio__split.mp3 upstream/client/audio/split.mp3
sha256sum server1/audio__spawn.mp3 upstream/client/audio/spawn.mp3

Result: all PNG and MP3 assets are byte-identical to upstream (SHA-256 match) — no media steganography. The only meaningful diff is in css/main.css, which contains an ADDED CSS comment holding a long list of integer coordinate pairs and ending with the sentence We went over this in high school!: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

css
*:focus {
 outline: 1px solid transparent;
 border-style: none; /* '5  5 5  0 1  0 1  2 2  ...  0 63  1 63  2 63  3.
   We went over this in high school!'
 */
}

The hint "по-особенному записываю точки со школы" ("record dots a special way since school") plus "We went over this in high school" both point at school-style coordinate-grid plotting: the integers are ordinary (x, y) pairs.

Solution

  1. Extract the quoted comment from css/main.css and pull every integer.
  2. Interpret them as (x, y) pairs and connect them in listed order.
  3. Render with matplotlib on an equal-aspect grid — the polyline draws letters. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍
python
#!/usr/bin/env python3
import re
from pathlib import Path
import matplotlib.pyplot as plt

css = Path("server1/css__main.css").read_text()
match = re.search(r"/\*\s*'([^']+)'\s*\*/", css)   # grab the quoted comment
numbers = [int(n) for n in re.findall(r"-?\d+", match.group(1))]

# ordinary school-graph coordinates stored as (x, y)
points = list(zip(numbers[0::2], numbers[1::2]))
x, y = zip(*points)

fig, ax = plt.subplots(figsize=(16, 3))
ax.plot(x, y, "o-", color="black", linewidth=1.8, markersize=3)
ax.set_aspect("equal")
ax.grid(True)
fig.tight_layout()
fig.savefig("css_points.png", dpi=200)
print(f"wrote css_points.png with {len(points)} points")

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

The exact coordinate list (from the CSS comment):

5 5  5 0  1 0  1 2  2 3  3 3  4 3  5 3  5 0  10 0  8 1  8 3  10 4  12 3  12 1
10 0  10 2  10 0  15 0  15 5  15 4  14 4  16 4  15 4  15 -1  17 -2  15 -1  15 0
20 0  19 1  20 0  21 1  20 2  19 3  20 4  21 3  20 4  19 3  21 1  20 0  30 0
30 4  31 5  30 4  29 4  31 4  30 4  30 0  35 0  33 1  33 3  35 4  37 3  37 1
35 0  40 0  40 4  41 5  40 4  40 0  50 0  48 2  50 0  52 2  50 0  50 -1  50 -2
49 -3  50 -2  50 0  57 0  55 1  55 3  57 4  59 3  59 1  57 0  57 2  57 0  65 0
65 4  65 0  64 0  63 1  63 2  63 3.

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

Reading the plot. Connecting the 86 points renders three space-separated words. Careful glyph inspection matters: the first and last round glyphs each contain an intentional internal vertical stroke from the baseline to their center, marking them as the digit 0, while the middle word for uses a plain letter o. The rendered phrase is:

REDACTED

(An initial naive reading dots for you — and an over-corrected d0ts f0r y0u — were both wrong; the digit/letter distinction is per-glyph.) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Wrapping the decoded phrase per the -mass hint (add between CODEBY{}), preserving the spaces: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍