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-interlacedThat 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.pybest coarse run: 2000 at 0x44bb80
best refined run: 3328 at 0x44bb7c
hint run: 3324 at 0x44bbe0Once 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.pyFound 4110 vertices
X range: -0.8916 to 0.9961
Y range: -0.0237 to 0.9961
Saved rendered image to rendered_flag_repro.pngAt that point, the rendered image visibly contains the flag, which can be read directly as texsaw{switch96959d49370}.