Post

PwnCollege: File Formats

File Format Directives

1. General Information

PropertyValue
Binarycimg
Architecturex86-64 ELF
GoalGenerate an ANSI framebuffer that matches desired_output using the only available handler
Flag obtainedpwn.college{ x-x.x}

2. Extracted Source Code

The server provides cimg.c (main source code) but does NOT provide cimg-handlers.c. However, the main source code reveals the complete format structure.

Data Structures

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
struct cimg_header
{
    char magic_number[4];       // "cIMG"
    uint16_t version;           // 3
    uint8_t width;
    uint8_t height;
    uint32_t remaining_directives;
} __attribute__((packed));

typedef struct
{
    uint8_t r;
    uint8_t g;
    uint8_t b;
    uint8_t ascii;
} pixel_color_t;

typedef pixel_color_t pixel_t;

typedef struct
{
    union
    {
        char data[24];
        struct term_str_st
        {
            char color_set[7];   // \x1b[38;2;
            char r[3];          // 255
            char s1;            // ;
            char g[3];          // 255
            char s2;            // ;
            char b[3];          // 255
            char m;            // m
            char c;             // X
            char color_reset[4];     // \x1b[0m
        } str;
    };
} term_pixel_t;

3. .cimg File Format

Header (12 bytes)

OffsetSizeFieldDescription
0x004MagiccIMG
0x042VersionMust be 3
0x061WidthFramebuffer width
0x071HeightFramebuffer height
0x084CountNumber of directives/commands

Commands

In this level, there is only ONE handler:

Opcode (LE)DecimalHandlerDescription
0x60A424740handle_24740Loads a full RGBA image matching the framebuffer size

handle_24740 Payload

  • Reads exactly width * height * 4 bytes of RGBA data
  • Each pixel is R G B A where:
    • R, G, B are the color components (0-255)
    • A is the ASCII character to print (must be printable: 0x200x7E)
  • Formats each pixel into the ANSI string: \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m
  • Places each cell in framebuffer[row * width + col]

4. Key Functions

initialize_framebuffer(cimg)

  • Computes num_pixels = width * height
  • Allocates num_pixels * 24 + 1 bytes
  • Initializes ALL cells with white color (255,255,255) and space character (0x20)

display(cimg)

  • Iterates over each row
  • Writes width * 24 bytes of the row to stdout
  • Writes \x1b[38;2;000;000;000m\n\x1b[0m (24-byte newline with reset)

main() — Victory Logic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// No total_data restriction in this level!

if (cimg.num_pixels != sizeof(desired_output)/sizeof(term_pixel_t))
    won = 0;

for (int i = 0; i < cimg.num_pixels && i < sizeof(desired_output)/sizeof(term_pixel_t); i++)
{
    // Character MUST match
    if (cimg.framebuffer[i].str.c != ((term_pixel_t*)&desired_output)[i].str.c)
        won = 0;

    // For NON-space/NON-newline cells, all 24 bytes MUST match exactly
    if (
        cimg.framebuffer[i].str.c != ' ' &&
        cimg.framebuffer[i].str.c != '\n' &&
        memcmp(cimg.framebuffer[i].data, ((term_pixel_t*)&desired_output)[i].data, sizeof(term_pixel_t))
    )
        won = 0;
}

if (won) win();

5. The desired_output

Extracted from the binary at address 0x404020:

  • Size: 22320 bytes = 930 cells × 24 bytes
  • Dimensions: The framebuffer must have a total of 930 cells
  • The width × height dimensions can be any factorization of 930, but using 62 × 15 renders the ASCII art correctly

ASCII Art

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.------------------------------------------------------------.
|                                                            |
|                                          __  __            |
|                ___                      |  \/  |    ____   |
|               / __|                     | |\/| |   / ___|  |
|              | (__            ___       | |  | |  | |  _   |
|               \___|          |_ _|      |_|  |_|  | |_| |  |
|                               | |                  \____|  |
|                               | |                          |
|                              |___|                         |
|                                                            |
|                                                            |
|                                                            |
|                                                            |
'------------------------------------------------------------'

6. Solution Process

Step 1: Extract desired_output from the binary

1
2
3
4
5
with open('cimg_current', 'rb') as f:
    data = f.read()

idx = data.find(b'\x1b[38;2;255;255;255m.\x1b[0m\x1b[38;2;255;255;255m-')
desired = data[idx:idx+22320]

Step 2: Extract RGB+Character from each cell

For each 24-byte cell:

  • R = int(cell[7:10]) (bytes 7,8,9 are ASCII digits for red)
  • G = int(cell[11:14]) (bytes 11,12,13 are ASCII digits for green)
  • B = int(cell[15:18]) (bytes 15,16,17 are ASCII digits for blue)
  • C = cell[19] (byte 19 is the character)

Step 3: Build the .cimg file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
output = b'cIMG'                    # Magic
output += struct.pack('<H', 3)      # Version
output += struct.pack('B', 62)      # Width
output += struct.pack('B', 15)      # Height
output += struct.pack('<I', 1)      # 1 directive
output += struct.pack('<H', 24740)  # Opcode handle_24740

# 930 pixels × 4 bytes = 3720 bytes
for i in range(930):
    cell = desired[i*24:(i+1)*24]
    r = int(cell[7:10])
    g = int(cell[11:14])
    b = int(cell[15:18])
    a = cell[19]
    output += bytes([r, g, b, a])

Step 4: Result

  • Total file size: 3734 bytes
  • Header: 12 bytes
  • Opcode: 2 bytes
  • RGBA data: 3720 bytes (930 pixels × 4)
This post is licensed under CC BY 4.0 by the author.