770 words
4 minutes
TexSAW 2026 - drawing - Reverse Engineering Writeup

Category: Reverse Engineering Flag: texsaw{switch96959d49370}

Challenge Description#

drawing be like

Analysis#

The provided file was a Nintendo Switch homebrew NRO rather than a normal desktop executable, which immediately suggested that the interesting data might be embedded assets rather than a plaintext flag. A quick type check showed that drawing.nro was just reported as generic data in this environment, while the already-rendered reference image was a tiny 512x512 PNG.

file "/home/rei/Downloads/drawing.nro" && file "/home/rei/Downloads/rendered_flag.png"
/home/rei/Downloads/drawing.nro: data
/home/rei/Downloads/rendered_flag.png: PNG image data, 512 x 512, 8-bit/color RGB, non-interlaced

That matched the challenge theme: the binary was probably meant to draw something rather than print it. The solve path was to recover the embedded geometry data and render it ourselves. The key idea was that a region of the file could be interpreted as a stream of little-endian float32 values laid out as vertex records (x, y, z, r, g, b), so each record is 24 bytes. To make sure the suspected region was not a coincidence, I ran a small plausibility scan over the file. It measured how long a run of 24-byte records kept decoding into sensible coordinates and colors. The hinted offset 0x44bbe0 landed inside a long valid run, confirming that this was a real vertex blob.

import struct

with open('drawing.nro', 'rb') as f:
    data = f.read()

def runlen(off: int, limit: int = 6000) -> int:
    n = 0
    for i in range(limit):
        p = off + i * 24
        if p + 24 > len(data):
            break
        x, y, z, r, g, b = struct.unpack_from('<6f', data, p)
        if not (-1.2 < x < 1.2 and -1.2 < y < 1.2 and -5 < z < 5 and -0.2 < r < 1.2 and -0.2 < g < 1.2 and -0.2 < b < 1.2):
            break
        n += 1
    return n

step = 0x40
best = (0, -1)
for off in range(0, len(data) - 24 * 100, step):
    rl = runlen(off, limit=2000)
    if rl > best[0]:
        best = (rl, off)

print('best coarse run:', best[0], 'at', hex(best[1]))

coarse_off = best[1]
best2 = best
for off in range(max(0, coarse_off - step), min(len(data) - 24 * 100, coarse_off + step), 4):
    rl = runlen(off, limit=6000)
    if rl > best2[0]:
        best2 = (rl, off)

print('best refined run:', best2[0], 'at', hex(best2[1]))

hint = 0x44bbe0
print('hint run:', runlen(hint, limit=6000), 'at', hex(hint))
python sanity_check.py
best coarse run: 2000 at 0x44bb80
best refined run: 3328 at 0x44bb7c
hint run: 3324 at 0x44bbe0

Once that structure was clear, the rest was just rasterizing it. Starting at offset 0x44BBE0, I parsed up to 5000 vertex records, kept entries whose x and y coordinates fell in the expected normalized device coordinate range, and mapped the resulting coordinate bounds onto a 512x512 canvas. The vertex stream behaves like a triangle list, and the visible shapes appear when the data is consumed in groups of six vertices, treating the first four vertices as a filled quad. That reproduces the text the original program intended to draw.

Solution#

The following script recreates the flag image from the embedded vertex data:

import struct

from PIL import Image, ImageDraw


def main() -> None:
    # Extract and render embedded vertex data from the NRO.
    # Vertex struct: little-endian 6x float32 => (x,y,z,r,g,b) => 24 bytes.
    in_path = 'drawing.nro'
    offset = 0x44BBE0
    stride = 24
    max_vertices = 5000

    with open(in_path, 'rb') as f:
        data = f.read()

    vertices: list[tuple[float, float]] = []

    for i in range(max_vertices):
        pos = offset + i * stride
        if pos + stride > len(data):
            break

        x, y, z, r, g, b = struct.unpack_from('<6f', data, pos)

        if -1.0 < x < 1.0 and -1.0 < y < 1.0:
            vertices.append((x, y))

    print(f'Found {len(vertices)} vertices')

    if not vertices:
        raise SystemExit('No vertices extracted; wrong offset/format')

    xs = [v[0] for v in vertices]
    ys = [v[1] for v in vertices]
    min_x, max_x = min(xs), max(xs)
    min_y, max_y = min(ys), max(ys)

    print(f'X range: {min_x:.4f} to {max_x:.4f}')
    print(f'Y range: {min_y:.4f} to {max_y:.4f}')

    img_size = 512
    img = Image.new('RGB', (img_size, img_size), (255, 255, 255))
    draw = ImageDraw.Draw(img)

    def map_x(x: float) -> int:
        return int((x - min_x) / (max_x - min_x) * (img_size - 20) + 10)

    def map_y(y: float) -> int:
        return int((max_y - y) / (max_y - min_y) * (img_size - 20) + 10)

    for i in range(0, len(vertices), 6):
        if i + 5 < len(vertices):
            quad = vertices[i:i + 6]
            points = [(map_x(v[0]), map_y(v[1])) for v in quad[:4]]
            draw.polygon(points, fill=(0, 0, 0), outline=(0, 0, 0))

    out_path = 'rendered_flag_repro.png'
    img.save(out_path)
    print('Saved rendered image to', out_path)


if __name__ == '__main__':
    main()

Run it with Python:

python drawing_repro.py
Found 4110 vertices
X range: -0.8916 to 0.9961
Y range: -0.0237 to 0.9961
Saved rendered image to rendered_flag_repro.png

At that point, the rendered image visibly contains the flag, which can be read directly as texsaw{switch96959d49370}.

TexSAW 2026 - drawing - Reverse Engineering Writeup
https://blog.rei.my.id/posts/137/texsaw-2026-drawing-reverse-engineering-writeup/
Author
Reidho Satria
Published at
2026-03-30
License
CC BY-NC-SA 4.0