C:\CTF\K17\ETCHASKETCH.EXE _□X

ETCHASKETCH

rev

Challenge

A single stripped ELF binary, etchasketch. Running it draws something to the screen with a thick brush — too thick to actually read. The picture, not the program, is the flag.

Finding the drawing data

The binary itself only does two things: hold a hardcoded array of points, and draw lines/dabs between them with a fixed brush radius. Rather than dealing with the rendering at runtime, the points array can just be read straight out of the ELF's data section with pyelftools.

The array lives at vaddr 0x2020: 325 packed int16 x/y pairs, terminated by a sentinel value of -2 (which starts a new stroke / lifts the pen). The actual drawing routine is a standard line() (Bresenham) plus a circular dab() brush stamped along it — but the real binary uses a brush radius of 10px, which at this point/canvas scale turns the whole picture into an unreadable blob.

Reimplementing dab()/line() in Python with the brush radius forced down to 0 (a single pixel per step, i.e. just the bare line art) turns the blob back into a readable image.

Solution

import struct
from elftools.elf.elffile import ELFFile
from PIL import Image

def load_points(path):
    with open(path, 'rb') as f:
        elf = ELFFile(f)
        for seg in elf.iter_segments():
            if seg['p_vaddr'] <= 0x2020 < seg['p_vaddr'] + seg['p_filesz']:
                data = seg.data()
                off = 0x2020 - seg['p_vaddr']
                break
    points = []
    i = off
    while True:
        x, y = struct.unpack_from('<hh', data, i)
        i += 4
        if x == -2 and y == -2:
            break
        points.append((x, y))
    return points

def line(img, x0, y0, x1, y1):
    dx, dy = abs(x1 - x0), -abs(y1 - y0)
    sx = 1 if x0 < x1 else -1
    sy = 1 if y0 < y1 else -1
    err = dx + dy
    while True:
        if 0 <= x0 < img.width and 0 <= y0 < img.height:
            img.putpixel((x0, y0), (0, 0, 0))
        if x0 == x1 and y0 == y1:
            break
        e2 = 2 * err
        if e2 >= dy:
            err += dy; x0 += sx
        if e2 <= dx:
            err += dx; y0 += sy

def render(path, out):
    points = load_points(path)
    img = Image.new('RGB', (512, 512), (255, 255, 255))
    prev = None
    for p in points:
        if prev is not None:
            line(img, *prev, *p)
        prev = p
    img.save(out)

if __name__ == "__main__":
    import sys
    render(sys.argv[1], "flag.png")