Skip to main content

Reverse Engineering

File Formats: Magic Numbers (Python)

Source code

/challenge/cimg
#!/opt/pwn.college/python 

import os
import sys
from collections import namedtuple

Pixel = namedtuple("Pixel", ["ascii"])


def main():
if len(sys.argv) >= 2:
path = sys.argv[1]
assert path.endswith(".cimg"), "ERROR: file has incorrect extension"
file = open(path, "rb")
else:
file = sys.stdin.buffer

header = file.read1(4)
assert len(header) == 4, "ERROR: Failed to read header!"

assert header[:4] == b"CMge", "ERROR: Invalid magic number!"

with open("/flag", "r") as f:
flag = f.read()
print(flag)


if __name__ == "__main__":
try:
main()
except AssertionError as e:
print(e, file=sys.stderr)
sys.exit(-1)

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (4 bytes total):
    • Magic number (4 bytes): Must be CMge
hacker@reverse-engineering~file-formats-magic-numbers-python:/$ echo "CMge" > ~/solution.cimg
hacker@reverse-engineering~file-formats-magic-numbers-python:/$ /challenge/cimg ~/solution.cimg 
pwn.college{gnCrQDFokTc18WCgwd5eHW6GcYc.QX1ATN2EDL4ITM0EzW}

 

File Formats: Magic Numbers (C)

Source code

/challenge/cimg.c
#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <sys/prctl.h>
#include <sys/personality.h>
#include <arpa/inet.h>

void win()
{
char flag[256];
int flag_fd;
int flag_length;

flag_fd = open("/flag", 0);
if (flag_fd < 0)
{
printf("\n ERROR: Failed to open the flag -- %s!\n", strerror(errno));
if (geteuid() != 0)
{
printf(" Your effective user id is not 0!\n");
printf(" You must directly run the suid binary in order to have the correct permissions!\n");
}
exit(-1);
}
flag_length = read(flag_fd, flag, sizeof(flag));
if (flag_length <= 0)
{
printf("\n ERROR: Failed to read the flag -- %s!\n", strerror(errno));
exit(-1);
}
write(1, flag, flag_length);
printf("\n\n");
}

void read_exact(int fd, void *dst, int size, char *msg, int exitcode)
{
int n = read(fd, dst, size);
if (n != size)
{
fprintf(stderr, msg);
fprintf(stderr, "\n");
exit(exitcode);
}
}

struct cimg_header
{
char magic_number[4];
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
typedef pixel_bw_t pixel_t;

struct cimg
{
struct cimg_header header;
};

void __attribute__ ((constructor)) disable_buffering()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
}

int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'c' || cimg.header.magic_number[1] != 'n' || cimg.header.magic_number[2] != '~' || cimg.header.magic_number[3] != 'R')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (won) win();

}

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (4 bytes total):
    • Magic number (4 bytes): Must be cn~R
hacker@reverse-engineering~file-formats-magic-numbers-c:/$ echo "cn~R" > ~/solution.cimg
hacker@reverse-engineering~file-formats-magic-numbers-c:/$ /challenge/cimg ~/solution.cimg 
pwn.college{IxtdOuGoBMdBfHrqJNAjzZ96L1h.QX2ATN2EDL4ITM0EzW}

 

File Formats: Magic Numbers (x86)

hacker@reverse-engineering~file-formats-magic-numbers-x86:/$ file /challenge/cimg 
/challenge/cimg: setuid ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=47edd63950d3f7b9b5c95bf4c93080ff12b75711, for GNU/Linux 3.2.0, not stripped

This time the code is a binary executable in little endian format.

Let's decompile it using Binary Ninja Cloud.

After some variable renaming and type editing, we are left with the following:

Binary Analysis

image

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (4 bytes total):
    • Magic number (4 bytes): Must be 0x474D215B, which is ASCII GM![ in little-endian
hacker@reverse-engineering~file-formats-magic-numbers-x86:/$ echo "(~m6" > ~/solution.cimg
hacker@reverse-engineering~file-formats-magic-numbers-x86:/$ /challenge/cimg ~/solution.cimg 
pwn.college{U45kfQ4KNJIp6KwDH0lQRHdpFeL.QXwAzMwEDL4ITM0EzW}

 

Reading Endianness (Python)

Source code

/challenge/cimg
#!/opt/pwn.college/python

import os
import sys
from collections import namedtuple

Pixel = namedtuple("Pixel", ["ascii"])


def main():
if len(sys.argv) >= 2:
path = sys.argv[1]
assert path.endswith(".cimg"), "ERROR: file has incorrect extension"
file = open(path, "rb")
else:
file = sys.stdin.buffer

header = file.read1(4)
assert len(header) == 4, "ERROR: Failed to read header!"

assert int.from_bytes(header[:4], "little") == 0x474D215B, "ERROR: Invalid magic number!"

with open("/flag", "r") as f:
flag = f.read()
print(flag)


if __name__ == "__main__":
try:
main()
except AssertionError as e:
print(e, file=sys.stderr)
sys.exit(-1)

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (4 bytes total):
    • Magic number (4 bytes): Must be 0x474D215B, which is GM![ in big-endian or [!MG in little-endian ASCII
>>> big_endian = bytearray.fromhex("474D215B").decode()
>>> print(f"Big endian ASCII: {big_endian}")
Big endian ASCII: GM![

>>> little_endian = bytearray.fromhex("474D215B")[::-1].decode()
>>> print(f"Little endian ASCII: {little_endian}")
Little endian ASCII: [!MG

Endianness

Big endian

  0x1337   0x1338   0x1339   0x1340   
┌────────┬────────┬────────┬────────┐
│ 47 │ 4D │ 21 │ 5B │
│ ( G ) │ ( M ) │ ( ! ) │ ( [ ) │
└────────┴────────┴────────┴────────┘

The LSB is stored in the high memory address (0x1340) while the MSB is stored in the low memory address (0x1337).

This is the format in which humans write numbers. Network traffic is also sent in big endian format.

Little endian

  0x1337   0x1338   0x1339   0x1340   
┌────────┬────────┬────────┬────────┐
│ 5B │ 21 │ 4D │ 47 │
│ ( [ ) │ ( ! ) │ ( M ) │ ( G ) │
└────────┴────────┴────────┴────────┘

The LSB is stored in the low memory address (0x1337) while the MSB is stored in the high memory address (0x1340).

This is the format in which machines store data. This is the relevant format for our level.

Therefore, we have to set the first 4 bytes of the solution to [!MG.

hacker@reverse-engineering~reading-endianness-python:/$ echo "[!MG" > ~/solution.cimg
bash: !MG: event not found

This is happening because Bash uses ! for history expansion. So Bash tries to expand !MG as a previous command, but can't find one. We can easily get around this by using single quotes (').

hacker@reverse-engineering~reading-endianness-python:/$ echo '[!MG' > ~/solution.cimg
hacker@reverse-engineering~reading-endianness-python:/$ /challenge/cimg ~/solution.cimg
pwn.college{UeceXp6n13KASFhim5T8GOhpq63.QX3ATN2EDL4ITM0EzW}

 

Reading Endianness (C)

Source code

/challenge/cimg.c
#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <sys/prctl.h>
#include <sys/personality.h>
#include <arpa/inet.h>

void win()
{
char flag[256];
int flag_fd;
int flag_length;

flag_fd = open("/flag", 0);
if (flag_fd < 0)
{
printf("\n ERROR: Failed to open the flag -- %s!\n", strerror(errno));
if (geteuid() != 0)
{
printf(" Your effective user id is not 0!\n");
printf(" You must directly run the suid binary in order to have the correct permissions!\n");
}
exit(-1);
}
flag_length = read(flag_fd, flag, sizeof(flag));
if (flag_length <= 0)
{
printf("\n ERROR: Failed to read the flag -- %s!\n", strerror(errno));
exit(-1);
}
write(1, flag, flag_length);
printf("\n\n");
}

void read_exact(int fd, void *dst, int size, char *msg, int exitcode)
{
int n = read(fd, dst, size);
if (n != size)
{
fprintf(stderr, msg);
fprintf(stderr, "\n");
exit(exitcode);
}
}

struct cimg_header
{
unsigned int magic_number;
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
typedef pixel_bw_t pixel_t;

struct cimg
{
struct cimg_header header;
};

void __attribute__ ((constructor)) disable_buffering()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
}

int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number != 1733109083)
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (won) win();

}
  • File Extension: Must end with .cimg
  • Header (4 bytes total):
    • Magic number (4 bytes): Must be 1733109083, which is [%Mg in little-endian ASCII
>>> print('{0:x}'.format(1733109083))
674d255b
>>> little_endian = bytearray.fromhex("674d255b")[::-1].decode()
>>> print(f"Little endian ASCII: {little_endian}")
Little endian ASCII: [%Mg

The same concept of endianness applies here.

hacker@reverse-engineering~reading-endianness-c:/$ echo '[%Mg' > ~/solution.cimg
hacker@reverse-engineering~reading-endianness-c:/$ /challenge/cimg ~/solution.cimg
pwn.college{Iz_N1i6LBqszqfN70WeEVNJzFd9.QX4ATN2EDL4ITM0EzW}

 

Reading Endianness (x86)

Binary Analysis

image

  • File Extension: Must end with .cimg
  • Header (4 bytes total):
    • Magic number (4 bytes): Must be 0x72254f3c, which is <0%r in little-endian ASCII
>>> little_endian = bytearray.fromhex("72254f3c")[::-1].decode()
>>> print(f"Little endian ASCII: {little_endian}")
Little endian ASCII: <O%r
hacker@reverse-engineering~reading-endianness-c:/$ echo '<0%r' > ~/solution.cimg
hacker@reverse-engineering~reading-endianness-x86:/$ /challenge/cimg ~/solution.cimg
pwn.college{Et6nh45-ta1HCaJmdwJf5eDGBdd.QXxAzMwEDL4ITM0EzW}

 

Version Information (Python)

Source code

/challenge/cimg
#!/opt/pwn.college/python

import os
import sys
from collections import namedtuple

Pixel = namedtuple("Pixel", ["ascii"])


def main():
if len(sys.argv) >= 2:
path = sys.argv[1]
assert path.endswith(".cimg"), "ERROR: file has incorrect extension"
file = open(path, "rb")
else:
file = sys.stdin.buffer

header = file.read1(8)
assert len(header) == 8, "ERROR: Failed to read header!"

assert header[:4] == b"<0%R", "ERROR: Invalid magic number!"

assert int.from_bytes(header[4:8], "little") == 11, "ERROR: Invalid version!"

with open("/flag", "r") as f:
flag = f.read()
print(flag)


if __name__ == "__main__":
try:
main()
except AssertionError as e:
print(e, file=sys.stderr)
sys.exit(-1)

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (8 bytes total):
    • Magic number (4 bytes): Must be b"<0%R"
    • Version (4 bytes): Must be 11 in little-endian

Exploit

~/script.py
import struct

# Build the header (8 bytes total)
magic = b"<0%R" # 4 bytes
version = struct.pack("<I", 11) # 4 bytes

header = magic + version

# Full file content
cimg_data = header

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~version-information-python:/$ python ~/script.py
Wrote 8 bytes: b'<0%R\x0b\x00\x00\x00' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~version-information-python:/$ /challenge/cimg ~/solution.cimg 
pwn.college{QE3tgVGh7hvrbDbs175V291MQid.QX5ATN2EDL4ITM0EzW}

 

Version Information (C)

Source code

/challenge/cimg.c
#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <sys/prctl.h>
#include <sys/personality.h>
#include <arpa/inet.h>

void win()
{
char flag[256];
int flag_fd;
int flag_length;

flag_fd = open("/flag", 0);
if (flag_fd < 0)
{
printf("\n ERROR: Failed to open the flag -- %s!\n", strerror(errno));
if (geteuid() != 0)
{
printf(" Your effective user id is not 0!\n");
printf(" You must directly run the suid binary in order to have the correct permissions!\n");
}
exit(-1);
}
flag_length = read(flag_fd, flag, sizeof(flag));
if (flag_length <= 0)
{
printf("\n ERROR: Failed to read the flag -- %s!\n", strerror(errno));
exit(-1);
}
write(1, flag, flag_length);
printf("\n\n");
}

void read_exact(int fd, void *dst, int size, char *msg, int exitcode)
{
int n = read(fd, dst, size);
if (n != size)
{
fprintf(stderr, msg);
fprintf(stderr, "\n");
exit(exitcode);
}
}

struct cimg_header
{
char magic_number[4];
uint16_t version;
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
typedef pixel_bw_t pixel_t;

struct cimg
{
struct cimg_header header;
};

void __attribute__ ((constructor)) disable_buffering()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
}

int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'c' || cimg.header.magic_number[1] != 'm' || cimg.header.magic_number[2] != '6' || cimg.header.magic_number[3] != 'e')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (cimg.header.version != 135)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

if (won) win();

}

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (8 bytes total):
    • Magic number (4 bytes): Must be b"cm6e"
    • Version (4 bytes): Must be 135 in little-endian

Exploit

~/script.py
import struct

# Build the header (8 bytes total)
magic = b"cm6e" # 4 bytes
version = struct.pack("<I", 135) # 4 bytes

header = magic + version

# Full file content
cimg_data = header

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~version-information-c:/$ python ~/script.py
Wrote 8 bytes: b'cm6e\x87\x00\x00\x00' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~version-information-c:/$ /challenge/cimg ~/solution.cimg
pwn.college{MX7npfEYKHEaMMoN-13n0RYXQiX.QXwETN2EDL4ITM0EzW}

 

Version Information (x86)

Binary Analysis

image

  • File Extension: Must end with .cimg
  • Header (8 bytes total):
    • Magic number (4 bytes): Must be 0x5b6e6e52
    • Version (4 bytes): Must be 0xaa in little-endian

Exploit

~/script.py
import struct

# Build the header (8 bytes total)
magic = bytes.fromhex("5b6e6e52") # 4 bytes
version = struct.pack("<I", 0xaa) # 4 bytes

header = magic + version

# Full file content
cimg_data = header

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~version-information-x86:/$ python ~/script.py
Wrote 8 bytes: b'[nnR\xaa\x00\x00\x00' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~version-information-x86:/$ /challenge/cimg ~/solution.cimg
pwn.college{MS8bIZFkAQQ3-xwFV98pplsoCa7.QXyAzMwEDL4ITM0EzW}

 

Metadata and Data (Python)

Source code

/challenge/cimg
#!/opt/pwn.college/python

import os
import sys
from collections import namedtuple

Pixel = namedtuple("Pixel", ["ascii"])


def main():
if len(sys.argv) >= 2:
path = sys.argv[1]
assert path.endswith(".cimg"), "ERROR: file has incorrect extension"
file = open(path, "rb")
else:
file = sys.stdin.buffer

header = file.read1(20)
assert len(header) == 20, "ERROR: Failed to read header!"

assert header[:4] == b"CmgE", "ERROR: Invalid magic number!"

assert int.from_bytes(header[4:12], "little") == 1, "ERROR: Invalid version!"

width = int.from_bytes(header[12:16], "little")
assert width == 59, "ERROR: Incorrect width!"

height = int.from_bytes(header[16:20], "little")
assert height == 21, "ERROR: Incorrect height!"

data = file.read1(width * height)
assert len(data) == width * height, "ERROR: Failed to read data!"

pixels = [Pixel(character) for character in data]

with open("/flag", "r") as f:
flag = f.read()
print(flag)


if __name__ == "__main__":
try:
main()
except AssertionError as e:
print(e, file=sys.stderr)
sys.exit(-1)

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (20 bytes total):
    • Magic number (4 bytes): Must be b"CmgE"
    • Version (8 bytes): Must be 1 in little-endian
    • Width (4 bytes): Must be 59 in little-endian
    • Height (4 bytes): Must be 21 in little-endian
  • Pixel Data:
    • The number of non-space ASCII characters must be 59 * 21 = 1239

Exploit

~/script.py
import struct

# Build the header (20 bytes total)
magic = b"CmgE" # 4 bytes
version = struct.pack("<Q", 1) # 8 bytes
width = struct.pack("<L", 59) # 4 bytes
height = struct.pack("<L", 21) # 4 bytes

header = magic + version + width + height

# Build the pixel data (59 * 21 = 1239 bytes)
pixel_data = b"." * (59 * 21)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~metadata-and-data-python:/$ python ~/script.py
Wrote 1259 bytes: b'CmgE\x01\x00\x00\x00\x00\x00\x00\x00;\x00\x00\x00\x15\x00\x00\x00.......................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~metadata-and-data-python:/$ /challenge/cimg ~/solution.cimg 
pwn.college{gmcsTJSAE9Fvci5d7be0NM7T0Af.QXxETN2EDL4ITM0EzW}

 

Metadata and Data (C)

Source code

/challenge/cimg.c
#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <sys/prctl.h>
#include <sys/personality.h>
#include <arpa/inet.h>

void win()
{
char flag[256];
int flag_fd;
int flag_length;

flag_fd = open("/flag", 0);
if (flag_fd < 0)
{
printf("\n ERROR: Failed to open the flag -- %s!\n", strerror(errno));
if (geteuid() != 0)
{
printf(" Your effective user id is not 0!\n");
printf(" You must directly run the suid binary in order to have the correct permissions!\n");
}
exit(-1);
}
flag_length = read(flag_fd, flag, sizeof(flag));
if (flag_length <= 0)
{
printf("\n ERROR: Failed to read the flag -- %s!\n", strerror(errno));
exit(-1);
}
write(1, flag, flag_length);
printf("\n\n");
}

void read_exact(int fd, void *dst, int size, char *msg, int exitcode)
{
int n = read(fd, dst, size);
if (n != size)
{
fprintf(stderr, msg);
fprintf(stderr, "\n");
exit(exitcode);
}
}

struct cimg_header
{
char magic_number[4];
uint16_t version;
uint16_t width;
uint16_t height;
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
typedef pixel_bw_t pixel_t;

struct cimg
{
struct cimg_header header;
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))

void __attribute__ ((constructor)) disable_buffering()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
}

int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'C' || cimg.header.magic_number[1] != 'N' || cimg.header.magic_number[2] != 'm' || cimg.header.magic_number[3] != 'G')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (cimg.header.version != 1)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

if (cimg.header.width != 66)
{
puts("ERROR: Incorrect width!");
exit(-1);
}

if (cimg.header.height != 17)
{
puts("ERROR: Incorrect height!");
exit(-1);
}

unsigned long data_size = cimg.header.width * cimg.header.height * sizeof(pixel_t);
pixel_t *data = malloc(data_size);
if (data == NULL)
{
puts("ERROR: Failed to allocate memory for the image data!");
exit(-1);
}
read_exact(0, data, data_size, "ERROR: Failed to read data!", -1);

if (won) win();

}

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (10 bytes total):
    • Magic number (4 bytes): Must be b"CNmG"
    • Version (2 bytes): Must be 1 in little-endian
    • Width (2 bytes): Must be 66 in little-endian
    • Height (2 bytes): Must be 17 in little-endian
  • Pixel Data:
    • The number of non-space characters must be 66 * 17 = 1122

Exploit

~/script.py
import struct

# Build the header (10 bytes total)
magic = b"CNmG" # 4 bytes
version = struct.pack("<H", 1) # 2 bytes
width = struct.pack("<H", 66) # 2 bytes
height = struct.pack("<H", 17) # 2 bytes

header = magic + version + width + height

# Build the pixel data (66 * 17 = 1122 bytes)
pixel_data = b"." * (66 * 17)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~metadata-and-data-c:/$ python ~/script.py
Wrote 1132 bytes: b'CNmG\x01\x00B\x00\x11\x00..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~metadata-and-data-c:/$ /challenge/cimg ~/solution.cimg 
pwn.college{UiHnq7dEOB75oBiYdd31IiDPdHG.QXyETN2EDL4ITM0EzW}

 

Metadata and Data (x86)

Binary Analysis

image

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (14 bytes total):
    • Magic number (4 bytes): Must be 0x284e6e72
    • Version (2 bytes): Must be 1 in little-endian
    • Width (4 bytes): Must be 0x40 (64) in little-endian
    • Height (4 bytes): Must be 0xc (12) in little-endian
  • Pixel Data:
    • The number of non-space ASCII characters must be 66 * 17 = 1122

Exploit

~/script.py
import struct

# Build the header (14 bytes total)
magic = bytes.fromhex("284e6e72") # 4 bytes
version = struct.pack("<H", 1) # 2 bytes
width = struct.pack("<I", 0x40) # 4 bytes
height = struct.pack("<I", 0xc) # 4 bytes

header = magic + version + width + height

# Build the pixel data (64 * 12 = 768 bytes)
pixel_data = b"." * (64 * 12)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~metadata-and-data-x86:/$ python ~/script.py
Wrote 782 bytes: b'(Nnr\x01\x00@\x00\x00\x00\x0c\x00\x00\x00................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~metadata-and-data-x86:/$ /challenge/cimg ~/solution.cimg 
pwn.college{UB4Rk1u_RCBjfYamRdf9nAU0tlF.QXzAzMwEDL4ITM0EzW}

 

Input Restrictions (Python)

Source code

/challenge/cimg
#!/opt/pwn.college/python

import os
import sys
from collections import namedtuple

Pixel = namedtuple("Pixel", ["ascii"])


def main():
if len(sys.argv) >= 2:
path = sys.argv[1]
assert path.endswith(".cimg"), "ERROR: file has incorrect extension"
file = open(path, "rb")
else:
file = sys.stdin.buffer

header = file.read1(16)
assert len(header) == 16, "ERROR: Failed to read header!"

assert header[:4] == b"cIMG", "ERROR: Invalid magic number!"

assert int.from_bytes(header[4:12], "little") == 1, "ERROR: Invalid version!"

width = int.from_bytes(header[12:14], "little")
assert width == 66, "ERROR: Incorrect width!"

height = int.from_bytes(header[14:16], "little")
assert height == 17, "ERROR: Incorrect height!"

data = file.read1(width * height)
assert len(data) == width * height, "ERROR: Failed to read data!"

pixels = [Pixel(character) for character in data]

invalid_character = next((pixel.ascii for pixel in pixels if not (0x20 <= pixel.ascii <= 0x7E)), None)
assert invalid_character is None, f"ERROR: Invalid character {invalid_character:#04x} in data!"

with open("/flag", "r") as f:
flag = f.read()
print(flag)


if __name__ == "__main__":
try:
main()
except AssertionError as e:
print(e, file=sys.stderr)
sys.exit(-1)

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (16 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (8 bytes): Must be 1 in little-endian
    • Width (2 bytes): Must be 66 in little-endian
    • Height (2 bytes): Must be 17 in little-endian
  • Pixel Data:
    • The number of non-space ASCII characters must be 66 * 17 = 1122
    • Non-space ASCII must be in between 0x20 and 0x7E

Exploit

~/script.py
import struct

# Build the header (16 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<Q", 1) # 8 bytes
width = struct.pack("<H", 66) # 2 bytes
height = struct.pack("<H", 17) # 2 bytes

header = magic + version + width + height

# Build the pixel data (66 * 17 = 1122 bytes)
pixel_data = b"." * (66 * 17)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~input-restrictions-python:/$ python ~/script.py 
Wrote 1138 bytes: b'cIMG\x01\x00\x00\x00\x00\x00\x00\x00B\x00\x11\x00..................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~input-restrictions-python:/$ /challenge/cimg ~/solution.cimg 
pwn.college{89KE9mKkbzytvUe2ab0YCyPzt55.QXzETN2EDL4ITM0EzW}

 

Input Restrictions (C)

Source code

/challenge/cimg.c
#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <sys/prctl.h>
#include <sys/personality.h>
#include <arpa/inet.h>

void win()
{
char flag[256];
int flag_fd;
int flag_length;

flag_fd = open("/flag", 0);
if (flag_fd < 0)
{
printf("\n ERROR: Failed to open the flag -- %s!\n", strerror(errno));
if (geteuid() != 0)
{
printf(" Your effective user id is not 0!\n");
printf(" You must directly run the suid binary in order to have the correct permissions!\n");
}
exit(-1);
}
flag_length = read(flag_fd, flag, sizeof(flag));
if (flag_length <= 0)
{
printf("\n ERROR: Failed to read the flag -- %s!\n", strerror(errno));
exit(-1);
}
write(1, flag, flag_length);
printf("\n\n");
}

void read_exact(int fd, void *dst, int size, char *msg, int exitcode)
{
int n = read(fd, dst, size);
if (n != size)
{
fprintf(stderr, msg);
fprintf(stderr, "\n");
exit(exitcode);
}
}

struct cimg_header
{
char magic_number[4];
uint32_t version;
uint8_t width;
uint8_t height;
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
typedef pixel_bw_t pixel_t;

struct cimg
{
struct cimg_header header;
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))

void __attribute__ ((constructor)) disable_buffering()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
}

int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'c' || cimg.header.magic_number[1] != 'I' || cimg.header.magic_number[2] != 'M' || cimg.header.magic_number[3] != 'G')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (cimg.header.version != 1)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

if (cimg.header.width != 80)
{
puts("ERROR: Incorrect width!");
exit(-1);
}

if (cimg.header.height != 13)
{
puts("ERROR: Incorrect height!");
exit(-1);
}

unsigned long data_size = cimg.header.width * cimg.header.height * sizeof(pixel_t);
pixel_t *data = malloc(data_size);
if (data == NULL)
{
puts("ERROR: Failed to allocate memory for the image data!");
exit(-1);
}
read_exact(0, data, data_size, "ERROR: Failed to read data!", -1);

for (int i = 0; i < cimg.header.width * cimg.header.height; i++)
{
if (data[i].ascii < 0x20 || data[i].ascii > 0x7e)
{
fprintf(stderr, "ERROR: Invalid character 0x%x in the image data!\n", data[i].ascii);
exit(-1);
}
}

if (won) win();

}

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (10 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (4 bytes): Must be 1 in little-endian
    • Width (1 bytes): Must be 80 in little-endian
    • Height (1 bytes): Must be 13 in little-endian
  • Pixel Data:
    • The number of non-space ASCII characters must be 80 * 13 = 1040
    • Non-space ASCII must be in between 0x20 and 0x7e

Exploit

~/script.py
import struct

# Build the header (10 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<I", 1) # 4 bytes
width = struct.pack("<B", 80) # 1 bytes
height = struct.pack("<B", 13) # 1 bytes

header = magic + version + width + height

# Build the pixel data (80 * 13 = 1040 bytes)
pixel_data = b"." * (80 * 13)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~input-restrictions-c:/$ python ~/script.py
Wrote 1050 bytes: b'cIMG\x01\x00\x00\x00P\r................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~input-restrictions-c:/$ /challenge/cimg ~/solution.cimg 
pwn.college{MncM_uybJBUtPMNqnf4uUZTvN38.QX0ETN2EDL4ITM0EzW}

 

Input Restrictions (x86)

Binary Analysis

image

image

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (10 bytes total):
    • Magic number (4 bytes): Must be 0x63494d47"
    • Version (4 bytes): Must be 1 in little-endian
    • Width (1 bytes): Must be 0x3b (59) in little-endian
    • Height (1 bytes): Must be 0x15 (21) in little-endian
  • Pixel Data:
    • The number of non-space ASCII bytes must be 80 * 13 = 1040
    • Non-space ASCII must be in between 0x20 and 0x7e

Exploit

~/script.py
import struct

# Build the header (8 bytes total)
magic = bytes.fromhex("63494d47") # 4 bytes
version = struct.pack("<H", 1) # 2 bytes
width = struct.pack("<B", 0x3b) # 1 bytes
height = struct.pack("<B", 0x15) # 1 bytes

header = magic + version + width + height

# Build the pixel data (59 * 21 = 1239 bytes)
pixel_data = b"." * (59 * 21)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~input-restrictions-x86:/$ python ~/script.py 
Wrote 1247 bytes: b'cIMG\x01\x00;\x15.......................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~input-restrictions-x86:/$ /challenge/cimg ~/solution.cimg 
pwn.college{Qr3ER4NieY66DXLbO5a4RvjVuTi.QX0AzMwEDL4ITM0EzW}

 

Behold the cIMG! (Python)

Source code

/challenge/cimg
#!/opt/pwn.college/python

import os
import sys
from collections import namedtuple

Pixel = namedtuple("Pixel", ["ascii"])


def main():
if len(sys.argv) >= 2:
path = sys.argv[1]
assert path.endswith(".cimg"), "ERROR: file has incorrect extension"
file = open(path, "rb")
else:
file = sys.stdin.buffer

header = file.read1(14)
assert len(header) == 14, "ERROR: Failed to read header!"

assert header[:4] == b"cIMG", "ERROR: Invalid magic number!"

assert int.from_bytes(header[4:8], "little") == 1, "ERROR: Invalid version!"

width = int.from_bytes(header[8:12], "little")

height = int.from_bytes(header[12:14], "little")

data = file.read1(width * height)
assert len(data) == width * height, "ERROR: Failed to read data!"

pixels = [Pixel(character) for character in data]

invalid_character = next((pixel.ascii for pixel in pixels if not (0x20 <= pixel.ascii <= 0x7E)), None)
assert invalid_character is None, f"ERROR: Invalid character {invalid_character:#04x} in data!"

framebuffer = "".join(
bytes(pixel.ascii for pixel in pixels[row_start : row_start + width]).decode() + "\n"
for row_start in range(0, len(pixels), width)
)
print(framebuffer)

nonspace_count = sum(1 for pixel in pixels if chr(pixel.ascii) != " ")
if nonspace_count != 275:
return

with open("/flag", "r") as f:
flag = f.read()
print(flag)


if __name__ == "__main__":
try:
main()
except AssertionError as e:
print(e, file=sys.stderr)
sys.exit(-1)

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (14 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (4 bytes): Must be 1 in little-endian
    • Width (4 bytes): Must be in little-endian
    • Height (2 bytes): Must be in little-endian
  • Pixel Data:
    • The number of non-space ASCII characters must be 275
    • Non-space ASCII must be in between 0x20 and 0x7E

Based on the number of pixels (275) we want, we can reverse engineer some values for the height (25) and weight (11).

Exploit

~/script.py
import struct

# Build the header (8 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<L", 1) # 4 bytes
width = struct.pack("<L", 25) # 4 bytes
height = struct.pack("<H", 11) # 2 bytes

header = magic + version + width + height

# Build the pixel data (25 * 11 = 275 bytes)
pixel_data = b"." * (25 * 11)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~behold-the-cimg-python:/$ python ~/script.py 
Wrote 289 bytes: b'cIMG\x01\x00\x00\x00\x19\x00\x00\x00\x0b\x00...................................................................................................................................................................................................................................................................................' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~behold-the-cimg-python:/$ /challenge/cimg ~/solution.cimg 
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................

pwn.college{Q-1xYEjUKjuKBEfHIV5c_J4d_fu.QX1ETN2EDL4ITM0EzW}

 

Behold the cIMG! (C)

Source code

/challenge/cimg.c
#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <sys/prctl.h>
#include <sys/personality.h>
#include <arpa/inet.h>

void win()
{
char flag[256];
int flag_fd;
int flag_length;

flag_fd = open("/flag", 0);
if (flag_fd < 0)
{
printf("\n ERROR: Failed to open the flag -- %s!\n", strerror(errno));
if (geteuid() != 0)
{
printf(" Your effective user id is not 0!\n");
printf(" You must directly run the suid binary in order to have the correct permissions!\n");
}
exit(-1);
}
flag_length = read(flag_fd, flag, sizeof(flag));
if (flag_length <= 0)
{
printf("\n ERROR: Failed to read the flag -- %s!\n", strerror(errno));
exit(-1);
}
write(1, flag, flag_length);
printf("\n\n");
}

void read_exact(int fd, void *dst, int size, char *msg, int exitcode)
{
int n = read(fd, dst, size);
if (n != size)
{
fprintf(stderr, msg);
fprintf(stderr, "\n");
exit(exitcode);
}
}

struct cimg_header
{
char magic_number[4];
uint8_t version;
uint64_t width;
uint16_t height;
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
typedef pixel_bw_t pixel_t;

struct cimg
{
struct cimg_header header;
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))

void display(struct cimg *cimg, pixel_t *data)
{
int idx = 0;
for (int y = 0; y < cimg->header.height; y++)
{
for (int x = 0; x < cimg->header.width; x++)
{
idx = (0+y)*((cimg)->header.width) + ((0+x)%((cimg)->header.width));
putchar(data[y * cimg->header.width + x].ascii);

}
puts("");
}

}

void __attribute__ ((constructor)) disable_buffering()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
}

int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'c' || cimg.header.magic_number[1] != 'I' || cimg.header.magic_number[2] != 'M' || cimg.header.magic_number[3] != 'G')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (cimg.header.version != 1)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

unsigned long data_size = cimg.header.width * cimg.header.height * sizeof(pixel_t);
pixel_t *data = malloc(data_size);
if (data == NULL)
{
puts("ERROR: Failed to allocate memory for the image data!");
exit(-1);
}
read_exact(0, data, data_size, "ERROR: Failed to read data!", -1);

for (int i = 0; i < cimg.header.width * cimg.header.height; i++)
{
if (data[i].ascii < 0x20 || data[i].ascii > 0x7e)
{
fprintf(stderr, "ERROR: Invalid character 0x%x in the image data!\n", data[i].ascii);
exit(-1);
}
}

display(&cimg, data);

int num_nonspace = 0;
for (int i = 0; i < cimg.header.width * cimg.header.height; i++)
{
if (data[i].ascii != ' ') num_nonspace++;
}
if (num_nonspace != 275) won = 0;

if (won) win();

}

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (15 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (1 bytes): Must be 1 in little-endian
    • Width (8 bytes): Must be in little-endian
    • Height (2 bytes): Must be in little-endian
  • Pixel Data:
    • The number of non-space ASCII characters must be 275
    • Non-space ASCII must be in between 0x20 and 0x7E

Based on the number of pixels (275) we want, we can reverse engineer some values for the height (25) and weight (11).

Exploit

~/script.py
import struct
from pwn import *

# Build the header (15 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<B", 1) # 1 bytes
width = struct.pack("<Q", 25) # 8 bytes
height = struct.pack("<H", 11) # 2 bytes

header = magic + version + width + height

# Build the pixel data (25 * 11 = 275 bytes)
pixel_data = b"." * (25 * 11)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~behold-the-cimg-c:/$ python ~/script.py 
Wrote 290 bytes: b'cIMG\x01\x19\x00\x00\x00\x00\x00\x00\x00\x0b\x00...................................................................................................................................................................................................................................................................................' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~behold-the-cimg-c:/$ /challenge/cimg ~/solution.cimg 
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
pwn.college{Y9UIwcU8PAlWDhlav3ieIczJPrB.QX2ETN2EDL4ITM0EzW}

 

Behold the cIMG! (x86)

Binary Analysis

image

image

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (14 bytes total):
    • Magic number (4 bytes): Must be 0x63494d47"
    • Version (4 bytes): Must be 1 in little-endian
    • Width (4 bytes): Must be in little-endian
    • Height (2 bytes): Must be in little-endian
  • Pixel Data:
    • The number of non-space ASCII characters must be 275
    • Non-space ASCII must be in between 0x20 and 0x7E

Exploit

~/script.py
import struct
from pwn import *

# Build the header (14 bytes total)
magic = bytes.fromhex("63494d47") # 4 bytes
version = struct.pack("<L", 1) # 4 bytes
width = struct.pack("<L", 25) # 4 bytes
height = struct.pack("<H", 11) # 2 bytes

header = magic + version + width + height

# Build the pixel data (25 * 11 = 275 bytes)
pixel_data = b"." * (25 * 11)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~behold-the-cimg-x86:/$ python ~/script.py 
Wrote 289 bytes: b'cIMG\x01\x00\x00\x00\x19\x00\x00\x00\x0b\x00...................................................................................................................................................................................................................................................................................' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~behold-the-cimg-x86:/$ /challenge/cimg ~/solution.cimg 
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
.........................
pwn.college{8tyvsR_873aVBQOOVrrdXDT1h4x.QX1AzMwEDL4ITM0EzW}

 

A Basic cIMG (Python)

Source code

/chalenge/cimg
#!/opt/pwn.college/python

import os
import sys
from collections import namedtuple

Pixel = namedtuple("Pixel", ["r", "g", "b", "ascii"])


def main():
if len(sys.argv) >= 2:
path = sys.argv[1]
assert path.endswith(".cimg"), "ERROR: file has incorrect extension"
file = open(path, "rb")
else:
file = sys.stdin.buffer

header = file.read1(10)
assert len(header) == 10, "ERROR: Failed to read header!"

assert header[:4] == b"cIMG", "ERROR: Invalid magic number!"

assert int.from_bytes(header[4:5], "little") == 2, "ERROR: Invalid version!"

width = int.from_bytes(header[5:6], "little")
assert width == 39, "ERROR: Incorrect width!"

height = int.from_bytes(header[6:10], "little")
assert height == 21, "ERROR: Incorrect height!"

data = file.read1(width * height * 4)
assert len(data) == width * height * 4, "ERROR: Failed to read data!"

pixels = [Pixel(*data[i : i + 4]) for i in range(0, len(data), 4)]

invalid_character = next((pixel.ascii for pixel in pixels if not (0x20 <= pixel.ascii <= 0x7E)), None)
assert invalid_character is None, f"ERROR: Invalid character {invalid_character:#04x} in data!"

ansi_escape = lambda pixel: f"\x1b[38;2;{pixel.r:03};{pixel.g:03};{pixel.b:03}m{chr(pixel.ascii)}\x1b[0m"
framebuffer = "".join(
"".join(ansi_escape(pixel) for pixel in pixels[row_start : row_start + width])
+ ansi_escape(Pixel(0, 0, 0, ord("\n")))
for row_start in range(0, len(pixels), width)
)
print(framebuffer)

nonspace_count = sum(1 for pixel in pixels if chr(pixel.ascii) != " ")
if nonspace_count != 819:
return

asu_maroon = (0x8C, 0x1D, 0x40)
if any((pixel.r, pixel.g, pixel.b) != asu_maroon for pixel in pixels):
return

with open("/flag", "r") as f:
flag = f.read()
print(flag)


if __name__ == "__main__":
try:
main()
except AssertionError as e:
print(e, file=sys.stderr)
sys.exit(-1)

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (10 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (1 bytes): Must be 2 in little-endian
    • Width (1 bytes): Must be 39 in little-endian
    • Height (4 bytes): Must be 21 in little-endian
  • Pixel Data:
    • The number of non-space ASCII pixels must be 39 * 21 = 819, i.e. the number of bytes must be 819 * 4 = 3276
    • Non-space ASCII must be between 0x20 and 0x7E
    • Must have the non-space ASCII character in ASU maroon (0x8C, 0x1D, 0x40) color when 4 consecutive bytes are chunked

This time the challenge treats 4 bytes as one pixel, and the bytes hold the following values:

     R        G        B        ASCII to be printed
┌────────┬────────┬────────┬────────┐
│ 140 │ 29 │ 64 │ . │
└────────┴────────┴────────┴────────┘

Exploit

~/script.py
import struct
from pwn import *

# Build the header (10 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<B", 2) # 1 bytes
width = struct.pack("<B", 39) # 1 bytes
height = struct.pack("<L", 21) # 4 bytes

header = magic + version + width + height

# Build the pixel data (39 * 21 * 4 = 3276 bytes)
pixel = b"\x8C\x1D\x40."
pixel_data = pixel * (39 * 21)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~a-basic-cimg-python:/$ python ~/script.py 
Wrote 3286 bytes: b"cIMG\x02'\x15\x00\x00\x00\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@." to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~a-basic-cimg-python:/$ /challenge/cimg ~/solution.cimg 
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................
.......................................

pwn.college{Q6Dv1cf7dB1UwYRL2Mzcu_E967L.QX3ETN2EDL4ITM0EzW}

image

 

A Basic cIMG (C)

Source code

/challenge/cimg.c
#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <sys/prctl.h>
#include <sys/personality.h>
#include <arpa/inet.h>

void win()
{
char flag[256];
int flag_fd;
int flag_length;

flag_fd = open("/flag", 0);
if (flag_fd < 0)
{
printf("\n ERROR: Failed to open the flag -- %s!\n", strerror(errno));
if (geteuid() != 0)
{
printf(" Your effective user id is not 0!\n");
printf(" You must directly run the suid binary in order to have the correct permissions!\n");
}
exit(-1);
}
flag_length = read(flag_fd, flag, sizeof(flag));
if (flag_length <= 0)
{
printf("\n ERROR: Failed to read the flag -- %s!\n", strerror(errno));
exit(-1);
}
write(1, flag, flag_length);
printf("\n\n");
}

void read_exact(int fd, void *dst, int size, char *msg, int exitcode)
{
int n = read(fd, dst, size);
if (n != size)
{
fprintf(stderr, msg);
fprintf(stderr, "\n");
exit(exitcode);
}
}

struct cimg_header
{
char magic_number[4];
uint16_t version;
uint8_t width;
uint64_t height;
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
#define COLOR_PIXEL_FMT "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m"
typedef struct
{
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t ascii;
} pixel_color_t;
typedef pixel_color_t pixel_t;

struct cimg
{
struct cimg_header header;
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))

void display(struct cimg *cimg, pixel_t *data)
{
int idx = 0;
for (int y = 0; y < cimg->header.height; y++)
{
for (int x = 0; x < cimg->header.width; x++)
{
idx = (0+y)*((cimg)->header.width) + ((0+x)%((cimg)->header.width));
printf("\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m", data[y * cimg->header.width + x].r, data[y * cimg->header.width + x].g, data[y * cimg->header.width + x].b, data[y * cimg->header.width + x].ascii);

}
puts("");
}

}

void __attribute__ ((constructor)) disable_buffering()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
}

int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'c' || cimg.header.magic_number[1] != 'I' || cimg.header.magic_number[2] != 'M' || cimg.header.magic_number[3] != 'G')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (cimg.header.version != 2)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

if (cimg.header.width != 51)
{
puts("ERROR: Incorrect width!");
exit(-1);
}

if (cimg.header.height != 24)
{
puts("ERROR: Incorrect height!");
exit(-1);
}

unsigned long data_size = cimg.header.width * cimg.header.height * sizeof(pixel_t);
pixel_t *data = malloc(data_size);
if (data == NULL)
{
puts("ERROR: Failed to allocate memory for the image data!");
exit(-1);
}
read_exact(0, data, data_size, "ERROR: Failed to read data!", -1);

for (int i = 0; i < cimg.header.width * cimg.header.height; i++)
{
if (data[i].ascii < 0x20 || data[i].ascii > 0x7e)
{
fprintf(stderr, "ERROR: Invalid character 0x%x in the image data!\n", data[i].ascii);
exit(-1);
}
}

display(&cimg, data);

for (int i = 0; i < cimg.header.width * cimg.header.height; i++)
{
if (data[i].r != 0x8c || data[i].g != 0x1d || data[i].b != 0x40) won = 0;
}

int num_nonspace = 0;
for (int i = 0; i < cimg.header.width * cimg.header.height; i++)
{
if (data[i].ascii != ' ') num_nonspace++;
}
if (num_nonspace != 1224) won = 0;

if (won) win();

}

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (15 bytes total):
    • Magic number (4 bytes): Must be 0x63494d47"
    • Version (2 bytes): Must be 2 in little-endian
    • Width (1 bytes): Must be 51 in little-endian
    • Height (8 bytes): Must be 24 in little-endian
  • Pixel Data:
    • The number of non-space ASCII pixels must be 51 * 24 = 1224, i.e. the number of bytes must be 1224 * 4 = 4896
    • Non-space ASCII must be between 0x20 and 0x7E
    • Must have the non-space ASCII character in ASU maroon (0x8c, 0x1d, 0x40) color when 4 consecutive bytes are chunked

Exploit

~/script.py
import struct
from pwn import *

# Build the header (15 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<H", 2) # 2 bytes
width = struct.pack("<B", 51) # 1 bytes
height = struct.pack("<Q", 24) # 8 bytes

header = magic + version + width + height

# Build the pixel data (51 * 24 * 4 = 4896 bytes)
pixel = b"\x8C\x1D\x40."
pixel_data = pixel * (51 * 24)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~a-basic-cimg-c:/$ python ~/script.py 
Wrote 4907 bytes: b'cIMG\x023\x00\x18\x00\x00\x00\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~a-basic-cimg-c:/$ /challenge/cimg ~/solution.cimg
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
...................................................
pwn.college{4IY_xVT2BdscpfYAibCo97rS48E.QX4ETN2EDL4ITM0EzW}

image

 

A Basic cIMG (x86)

Binary Analysis

image

image

image

image

image

image

image

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (24 bytes total):
    • Magic number (4 bytes): Must be 0x63494d47"
    • Version (4 bytes): Must be 2 in little-endian
    • Width (8 bytes): Must be 75 in little-endian
    • Height (8 bytes): Must be 22 in little-endian
  • Pixel Data:
    • The number of non-space ASCII pixels must be 75 * 22 = 1650, i.e. the number of bytes must be 1650 * 4 = 6600
    • Non-space ASCII must be between 0x20 and 0x7E
    • Must have the non-space ASCII character in ASU maroon (0x8c, 0x1d, 0x40) color when 4 consecutive bytes are chunked

Exploit

~/script.py
import struct
from pwn import *

# Build the header (24 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<I", 2) # 4 bytes
width = struct.pack("<Q", 75) # 8 bytes
height = struct.pack("<Q", 22) # 8 bytes

header = magic + version + width + height

# Build the pixel data (75 * 22 * 4 = 6600 bytes)
pixel = b"\x8c\x1d\x40."
pixel_data = pixel * (75 * 22)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to file: '{filename}'")
hacker@reverse-engineering~a-basic-cimg-x86:/$ python ~/script.py 
Wrote 6624 bytes: b'cIMG\x02\x00\x00\x00K\x00\x00\x00\x00\x00\x00\x00\x16\x00\x00\x00\x00\x00\x00\x00\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.\x8c\x1d@.' to file: '/home/hacker/solution.cimg'
hacker@reverse-engineering~a-basic-cimg-x86:/$ /challenge/cimg ~/solution.cimg 
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
...........................................................................
pwn.college{UtFfALQAM2u-0NsYhnUJCQXPUZc.QX2AzMwEDL4ITM0EzW}

image

 

Internal State Mini (C)

Source code

/challenge/cimg.c
#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <sys/prctl.h>
#include <sys/personality.h>
#include <arpa/inet.h>

void win()
{
char flag[256];
int flag_fd;
int flag_length;

flag_fd = open("/flag", 0);
if (flag_fd < 0)
{
printf("\n ERROR: Failed to open the flag -- %s!\n", strerror(errno));
if (geteuid() != 0)
{
printf(" Your effective user id is not 0!\n");
printf(" You must directly run the suid binary in order to have the correct permissions!\n");
}
exit(-1);
}
flag_length = read(flag_fd, flag, sizeof(flag));
if (flag_length <= 0)
{
printf("\n ERROR: Failed to read the flag -- %s!\n", strerror(errno));
exit(-1);
}
write(1, flag, flag_length);
printf("\n\n");
}

void read_exact(int fd, void *dst, int size, char *msg, int exitcode)
{
int n = read(fd, dst, size);
if (n != size)
{
fprintf(stderr, msg);
fprintf(stderr, "\n");
exit(exitcode);
}
}

char desired_output[] = "\x1b[38;2;200;040;131mc\x1b[0m\x1b[38;2;001;019;165mI\x1b[0m\x1b[38;2;160;134;059mM\x1b[0m\x1b[38;2;195;046;079mG\x1b[0m\x00";

struct cimg_header
{
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
#define COLOR_PIXEL_FMT "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m"
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;

struct cimg
{
struct cimg_header header;
unsigned num_pixels;
term_pixel_t *framebuffer;
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))
#define CIMG_FRAMEBUFFER_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_FRAMEBUFFER_SIZE(cimg) (CIMG_FRAMEBUFFER_PIXELS(cimg) * sizeof(term_pixel_t))

void display(struct cimg *cimg, pixel_t *data)
{
int idx = 0;
for (int y = 0; y < cimg->header.height; y++)
{
for (int x = 0; x < cimg->header.width; x++)
{
idx = (0+y)*((cimg)->header.width) + ((0+x)%((cimg)->header.width));
char emit_tmp[24+1];
snprintf(emit_tmp, sizeof(emit_tmp), "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m", data[y * cimg->header.width + x].r, data[y * cimg->header.width + x].g, data[y * cimg->header.width + x].b, data[y * cimg->header.width + x].ascii);
memcpy((cimg)->framebuffer[idx%(cimg)->num_pixels].data, emit_tmp, 24);

}
}

for (int i = 0; i < cimg->header.height; i++)
{
write(1, cimg->framebuffer+i*cimg->header.width, sizeof(term_pixel_t)*cimg->header.width);
write(1, "\x1b[38;2;000;000;000m\n\x1b[0m", 24);
}
}

struct cimg *initialize_framebuffer(struct cimg *cimg)
{
cimg->num_pixels = CIMG_FRAMEBUFFER_PIXELS(cimg);
cimg->framebuffer = malloc(CIMG_FRAMEBUFFER_SIZE(cimg)+1);
if (cimg->framebuffer == NULL)
{
puts("ERROR: Failed to allocate memory for the framebuffer!");
exit(-1);
}
for (int idx = 0; idx < cimg->num_pixels; idx += 1)
{
char emit_tmp[24+1];
snprintf(emit_tmp, sizeof(emit_tmp), "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m", 255, 255, 255, ' ');
memcpy(cimg->framebuffer[idx].data, emit_tmp, 24);

}

return cimg;
}

void __attribute__ ((constructor)) disable_buffering()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
}

int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
cimg.framebuffer = NULL;
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'c' || cimg.header.magic_number[1] != 'I' || cimg.header.magic_number[2] != 'M' || cimg.header.magic_number[3] != 'G')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (cimg.header.version != 2)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

initialize_framebuffer(&cimg);

unsigned long data_size = cimg.header.width * cimg.header.height * sizeof(pixel_t);
pixel_t *data = malloc(data_size);
if (data == NULL)
{
puts("ERROR: Failed to allocate memory for the image data!");
exit(-1);
}
read_exact(0, data, data_size, "ERROR: Failed to read data!", -1);

for (int i = 0; i < cimg.header.width * cimg.header.height; i++)
{
if (data[i].ascii < 0x20 || data[i].ascii > 0x7e)
{
fprintf(stderr, "ERROR: Invalid character 0x%x in the image data!\n", data[i].ascii);
exit(-1);
}
}

display(&cimg, data);

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++)
{
if (cimg.framebuffer[i].str.c != ((term_pixel_t*)&desired_output)[i].str.c)
{
won = 0;
}
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();

}

The challenge performs the following checks:

  • File Extension: Must end with .cimg
  • Header (8 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (2 bytes): Must be 2 in little-endian
    • Dimensions (2 bytes total): Must be 4 bytes
      • Width (1 bytes): Must be either 4 (if height = 1), 2 (if height = 2) or 1 (if height = 4) in little-endian
      • Height (1 bytes): Must be either 1 (if width = 4), 2 (if width = 2) or 4 (if width = 1) in little-endian
  • Pixel Data:
    • The number of non-space ASCII pixels must be 4 * 1 = 4, i.e. the number of bytes must be 4 * 4 = 16
    • When pixel data is loaded into the ANSI escape code: "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m" one by one and appended together, it should match the following: "\x1b[38;2;200;040;131mc\x1b[0m\x1b[38;2;001;019;165mI\x1b[0m\x1b[38;2;160;134;059mM\x1b[0m\x1b[38;2;195;046;079mG\x1b[0m\x00";

The template that the challenge enforces isn't just a random template, it is the the ANSI SGR.

ANSI 24-bit escape code

Modern terminals supports Truecolor (24-bit RGB), which allows you to set foreground and background colors using RGB.

Escape Code SequenceDescription
ESC[38;2;{r};{g};{b}mSet foreground color as RGB.
\x1b            --> ESC character
[ --> CSI (Control Sequence Initiator)
38;2;R;G;B --> ANSI SGR parameter meaning “Set 24-bit foreground color”
m --> End of SGR
. --> Character printed in RGB colours
\x1b[0m --> Reset terminal formatting

If we write the first pixel as b"xc8(\x83c", the challenge fills in the pixel RGB bytes using the %03d placeholder and fills the ASCII character byte with the %c placeholder.

## Template:
\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m

## First pixel (c)
%03d -> 200 -> "200"
%03d -> 40 -> "040"
%03d -> 131 -> "131"
%c -> 'c'

## Final ANSI pixel
\x1b[38;2;200;040;131mc\x1b[0m

Exploit

~/script.py
import struct

# Build the header (8 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<H", 2) # 2 bytes
width = struct.pack("<B", 4) # 1 bytes
height = struct.pack("<B", 1) # 1 bytes

header = magic + version + width + height

# Build the pixel data (51 * 24 * 4 = 4896 bytes)
pixels = [
(200, 40, 131, ord('c')),
(1, 19, 165, ord('I')),
(160, 134, 59, ord('M')),
(195, 46, 79, ord('G')),
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to: {filename}")
hacker@reverse-engineering~internal-state-mini-c:/$ python ~/script.py 
Wrote 24 bytes: b'cIMG\x02\x00\x04\x01\xc8(\x83c\x01\x13\xa5I\xa0\x86;M\xc3.OG' to: /home/hacker/solution.cimg
hacker@reverse-engineering~internal-state-mini-c:/$ /challenge/cimg ~/solution.cimg 
cIMG
pwn.college{sxObMoehMoum3fSzW12W3zqJsBu.QX5ETN2EDL4ITM0EzW}
image

 

Internal State Mini (x86)

After decompiling the program within IDA, and some variable renaming and type altering, we get the following pseudo-C code:

image

Binary Analysis

/challenge/cimg :: main() :: Pseudocode
int __fastcall main(int argc, const char **argv, const char **envp)
{
const char *file_arg; // rbp
int file; // eax
const char *error_msg; // rdi
unsigned int v6; // ebx
unsigned __int8 *v7; // rax
unsigned __int8 *v8; // rbp
__int64 i_1; // rax
__int64 data_i_ascii; // rcx
char *desired_ansi_sequence; // r12
unsigned int v12; // r13d
__int64 framebuffer_2; // r14
_BOOL8 won; // rbx
__int64 i; // rbp
char str_c; // al
__int128 cimg_header; // [rsp+0h] [rbp-58h] BYREF
__int64 framebuffer; // [rsp+10h] [rbp-48h]
unsigned __int64 v20; // [rsp+18h] [rbp-40h]

v20 = __readfsqword(0x28u);
cimg_header = 0LL;
framebuffer = 0LL;
if ( argc > 1 )
{
file_arg = argv[1];
if ( strcmp(&file_arg[strlen(file_arg) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto EXIT;
}
file = open(file_arg, 0);
dup2(file, 0);
}
read_exact(0LL, &cimg_header, 8LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( (_DWORD)cimg_header != 'GMIc' )
{
error_msg = "ERROR: Invalid magic number!";
PRINT_ERROR_AND_EXIT:
puts(error_msg);
goto EXIT;
}
error_msg = "ERROR: Unsupported version!";
if ( WORD2(cimg_header) != 2 )
goto PRINT_ERROR_AND_EXIT;
initialize_framebuffer(&cimg_header);
v6 = 4 * BYTE7(cimg_header) * BYTE6(cimg_header);
v7 = (unsigned __int8 *)malloc(4LL * BYTE7(cimg_header) * BYTE6(cimg_header));
error_msg = "ERROR: Failed to allocate memory for the image data!";
v8 = v7;
if ( !v7 )
goto PRINT_ERROR_AND_EXIT;
read_exact(0LL, v7, v6, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
i_1 = 0LL;
while ( BYTE7(cimg_header) * BYTE6(cimg_header) > (int)i_1 )
{
data_i_ascii = v8[4 * i_1++ + 3]; // data[i].ascii
if ( (unsigned __int8)(data_i_ascii - 32) > 0x5Eu )// if (data[i].ascii < 0x20 || data[i].ascii > 0x7e)
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", data_i_ascii);
EXIT:
exit(-1);
}
}
desired_ansi_sequence = desired_output;
display(&cimg_header, v8);
v12 = DWORD2(cimg_header);
framebuffer_2 = framebuffer;
won = DWORD2(cimg_header) == 4;
for ( i = 0LL; (_DWORD)i != 4 && v12 > (unsigned int)i; ++i )
{
str_c = *(_BYTE *)(framebuffer_2 + 24 * i + 19);
if ( str_c != desired_ansi_sequence[19] )
LODWORD(won) = 0;
if ( str_c != 32 && str_c != 10 )
{
if ( memcmp((const void *)(framebuffer_2 + 24 * i), desired_ansi_sequence, 0x18uLL) )
LODWORD(won) = 0;
}
desired_ansi_sequence += 24;
}
if ( won )
win();
return 0;
}

However, we can make the disassembly look much closer to the actual C code, if we just add the structs.

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
char __pad[4]; // ABI padding (x86-64)
union term_pixel_t *framebuffer;
};

After adding the above given struct, changing the types and names of certain variables, and adding some helpful comments, the decompiled code now looks much better.

image
/challenge/cimg :: main() :: Pseudocode
int __fastcall main(int argc, const char **argv, const char **envp)
{
const char *file_arg; // rbp
int file; // eax
const char *error_msg; // rdi
unsigned int data_size; // ebx
unsigned __int8 *data; // rax
unsigned __int8 *data_1; // rbp
__int64 i_1; // rax
char character; // cl
char *desired_output; // r12
unsigned int num_pixels; // r13d
union term_pixel_t *framebuffer; // r14
_BOOL8 won; // rbx
__int64 i; // rbp
char ascii_char; // al
struct cimg cimg; // [rsp+0h] [rbp-58h] BYREF
unsigned __int64 v19; // [rsp+18h] [rbp-40h]

v19 = __readfsqword(0x28u);
memset(&cimg, 0, sizeof(cimg));
if ( argc > 1 )
{
file_arg = argv[1];
if ( strcmp(&file_arg[strlen(file_arg) - 5], ".cimg") )// Check if the file extension is correct (.cimg)
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto EXIT;
}
file = open(file_arg, 0);
dup2(file, 0);
}
read_exact(0LL, &cimg, 8LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( *(_DWORD *)cimg.header.magic_number != 'GMIc' )// Check if the magic number is (cIMG) in big-endian
{
error_msg = "ERROR: Invalid magic number!";
OUTPUT_ERROR_MSG_AND_EXIT:
puts(error_msg);
goto EXIT;
}
error_msg = "ERROR: Unsupported version!";
if ( cimg.header.version != 2 ) // Check if the cimg_header.version is correct (2)
goto OUTPUT_ERROR_MSG_AND_EXIT;
initialize_framebuffer(&cimg);
data_size = 4 * cimg.header.height * cimg.header.width;
data = (unsigned __int8 *)malloc(4LL * cimg.header.height * cimg.header.width);
error_msg = "ERROR: Failed to allocate memory for the image data!";
data_1 = data;
if ( !data )
goto OUTPUT_ERROR_MSG_AND_EXIT;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
i_1 = 0LL;
while ( cimg.header.height * cimg.header.width > (int)i_1 )
{
*(_QWORD *)&character = data_1[4 * i_1++ + 3];
// Check if the character falls in the range 0x20 - 0x7e
if ( (unsigned __int8)(character - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&character);
EXIT:
exit(-1);
}
}
desired_output = ::desired_output;
display(&cimg, data_1);
num_pixels = cimg.num_pixels;
framebuffer = cimg.framebuffer;
won = cimg.num_pixels == 4;
for ( i = 0LL; (_DWORD)i != 4 && num_pixels > (unsigned int)i; ++i )
{
ascii_char = framebuffer[i].data[19];
if ( ascii_char != desired_output[19] )
LODWORD(won) = 0;
// Check if the ASCII character is a space or new-line character
if ( ascii_char != ' ' && ascii_char != '\n' )
{
if ( memcmp(&framebuffer[i], desired_output, 0x18uLL) )
LODWORD(won) = 0;
}
desired_output += 24;
}
if ( won )
win();
return 0;
}

The desired_ansi_sequence is the same as last time.

image

Expected ANSI sequence:

"\x1b[38;2;200;040;131mc\x1b[0m\x1b[38;2;001;019;165mI\x1b[0m\x1b[38;2;160;134;059mM\x1b[0m\x1b[38;2;195;046;079mG\x1b[0m\x00"

The reason that \x1b is represented as . is that the ASCII ESC character is non-printable, and IDA replaces all non-printable characters with (.).

The code checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: main() :: Pseudocode
# ---- snip ----

if ( (unsigned __int8)(character - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&character);
EXIT:
exit(-1);
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

It then uses the 4-bytes in the pixels to fill in the ANSI sequence \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m based on the struct we defined earlier:

/challenge/cimg :: main() :: Pseudocode
# ---- snip ----

data_size = 4 * cimg.header.height * cimg.header.width;
data = (unsigned __int8 *)malloc(4LL * cimg.header.height * cimg.header.width);
error_msg = "ERROR: Failed to allocate memory for the image data!";

# ---- snip ----

display(&cimg, data_1);

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

Afterwards, it checks if the 20th byte in the crafted ANSI sequence is a space or newline character. If it is not, and everything else is matching the desired ANSI sequence, it calls win() which prints the flag.

To put it all together, we can see that the challenge performs the exact same checks as the Internal State Mini (C) version:

  • File Extension: Must end with .cimg
  • Header (8 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (2 bytes): Must be 2 in little-endian
    • Dimensions (2 bytes total): Must be 4 bytes
      • Width (1 bytes): Must be either 4 (if height = 1), 2 (if height = 2) or 1 (if height = 4) in little-endian
      • Height (1 bytes): Must be either 1 (if width = 4), 2 (if width = 2) or 4 (if width = 1) in little-endian
  • Pixel Data:
    • The number of non-space ASCII pixels must be 4 * 1 = 4, i.e. the number of bytes must be 4 * 4 = 16
    • When pixel data is loaded into the ANSI escape code: \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m one by one and appended together, it should match the following: \x1b[38;2;200;040;131mc\x1b[0m\x1b[38;2;001;019;165mI\x1b[0m\x1b[38;2;160;134;059mM\x1b[0m\x1b[38;2;195;046;079mG\x1b[0m\x00

Exploit

~/script.py
import struct

# Build the header (8 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<H", 2) # 2 bytes
width = struct.pack("<B", 4) # 1 bytes
height = struct.pack("<B", 1) # 1 bytes

header = magic + version + width + height

# Build the pixel data (51 * 24 * 4 = 4896 bytes)
pixels = [
(200, 40, 131, ord('c')),
(1, 19, 165, ord('I')),
(160, 134, 59, ord('M')),
(195, 46, 79, ord('G')),
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to: {filename}")
hacker@reverse-engineering~internal-state-mini-x86:~$ python ~/script.py 
Wrote 24 bytes: b'cIMG\x02\x00\x04\x01\xc8(\x83c\x01\x13\xa5I\xa0\x86;M\xc3.OG' to: /home/hacker/solution.cimg
hacker@reverse-engineering~internal-state-mini-x86:~$ /challenge/cimg ~/solution.cimg 
cIMG
pwn.college{gNl9haWUsUcGB0Nwci7BWxzvy8e.QXwITN2EDL4ITM0EzW}
image

 

Internal State (C)

Source code

/challenge/cimg.c
#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <sys/prctl.h>
#include <sys/personality.h>
#include <arpa/inet.h>

void win()
{
char flag[256];
int flag_fd;
int flag_length;

flag_fd = open("/flag", 0);
if (flag_fd < 0)
{
printf("\n ERROR: Failed to open the flag -- %s!\n", strerror(errno));
if (geteuid() != 0)
{
printf(" Your effective user id is not 0!\n");
printf(" You must directly run the suid binary in order to have the correct permissions!\n");
}
exit(-1);
}
flag_length = read(flag_fd, flag, sizeof(flag));
if (flag_length <= 0)
{
printf("\n ERROR: Failed to read the flag -- %s!\n", strerror(errno));
exit(-1);
}
write(1, flag, flag_length);
printf("\n\n");
}

void read_exact(int fd, void *dst, int size, char *msg, int exitcode)
{
int n = read(fd, dst, size);
if (n != size)
{
fprintf(stderr, msg);
fprintf(stderr, "\n");
exit(exitcode);
}
}

char desired_output[] = "\x1b[38;2;255;255;255m.\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m.\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;228;010;217m \x1b[0m\x1b[38;2;228;010;217m \x1b[0m\x1b[38;2;228;010;217m_\x1b[0m\x1b[38;2;228;010;217m_\x1b[0m\x1b[38;2;228;010;217m_\x1b[0m\x1b[38;2;228;010;217m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;228;010;217m \x1b[0m\x1b[38;2;228;010;217m/\x1b[0m\x1b[38;2;228;010;217m \x1b[0m\x1b[38;2;228;010;217m_\x1b[0m\x1b[38;2;228;010;217m_\x1b[0m\x1b[38;2;228;010;217m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;137;221;241m \x1b[0m\x1b[38;2;137;221;241m_\x1b[0m\x1b[38;2;137;221;241m_\x1b[0m\x1b[38;2;137;221;241m_\x1b[0m\x1b[38;2;137;221;241m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;228;010;217m|\x1b[0m\x1b[38;2;228;010;217m \x1b[0m\x1b[38;2;228;010;217m(\x1b[0m\x1b[38;2;228;010;217m_\x1b[0m\x1b[38;2;228;010;217m_\x1b[0m\x1b[38;2;228;010;217m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;137;221;241m|\x1b[0m\x1b[38;2;137;221;241m_\x1b[0m\x1b[38;2;137;221;241m \x1b[0m\x1b[38;2;137;221;241m_\x1b[0m\x1b[38;2;137;221;241m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;228;010;217m \x1b[0m\x1b[38;2;228;010;217m\\\x1b[0m\x1b[38;2;228;010;217m_\x1b[0m\x1b[38;2;228;010;217m_\x1b[0m\x1b[38;2;228;010;217m_\x1b[0m\x1b[38;2;228;010;217m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;137;221;241m \x1b[0m\x1b[38;2;137;221;241m|\x1b[0m\x1b[38;2;137;221;241m \x1b[0m\x1b[38;2;137;221;241m|\x1b[0m\x1b[38;2;137;221;241m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;137;221;241m \x1b[0m\x1b[38;2;137;221;241m|\x1b[0m\x1b[38;2;137;221;241m \x1b[0m\x1b[38;2;137;221;241m|\x1b[0m\x1b[38;2;137;221;241m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;137;221;241m|\x1b[0m\x1b[38;2;137;221;241m_\x1b[0m\x1b[38;2;137;221;241m_\x1b[0m\x1b[38;2;137;221;241m_\x1b[0m\x1b[38;2;137;221;241m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m_\x1b[0m\x1b[38;2;250;025;157m_\x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m_\x1b[0m\x1b[38;2;250;025;157m_\x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m\\\x1b[0m\x1b[38;2;250;025;157m/\x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;250;025;157m\\\x1b[0m\x1b[38;2;250;025;157m/\x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;131;228;012m/\x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;131;228;012m|\x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;131;228;012m|\x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;250;025;157m_\x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m \x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;250;025;157m_\x1b[0m\x1b[38;2;250;025;157m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;131;228;012m|\x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;131;228;012m|\x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m|\x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;131;228;012m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;131;228;012m \x1b[0m\x1b[38;2;131;228;012m\\\x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m_\x1b[0m\x1b[38;2;131;228;012m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m'\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m'\x1b[0m\x00";

struct cimg_header
{
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
#define COLOR_PIXEL_FMT "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m"
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;

struct cimg
{
struct cimg_header header;
unsigned num_pixels;
term_pixel_t *framebuffer;
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))
#define CIMG_FRAMEBUFFER_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_FRAMEBUFFER_SIZE(cimg) (CIMG_FRAMEBUFFER_PIXELS(cimg) * sizeof(term_pixel_t))

void display(struct cimg *cimg, pixel_t *data)
{
int idx = 0;
for (int y = 0; y < cimg->header.height; y++)
{
for (int x = 0; x < cimg->header.width; x++)
{
idx = (0+y)*((cimg)->header.width) + ((0+x)%((cimg)->header.width));
char emit_tmp[24+1];
snprintf(emit_tmp, sizeof(emit_tmp), "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m", data[y * cimg->header.width + x].r, data[y * cimg->header.width + x].g, data[y * cimg->header.width + x].b, data[y * cimg->header.width + x].ascii);
memcpy((cimg)->framebuffer[idx%(cimg)->num_pixels].data, emit_tmp, 24);

}
}

for (int i = 0; i < cimg->header.height; i++)
{
write(1, cimg->framebuffer+i*cimg->header.width, sizeof(term_pixel_t)*cimg->header.width);
write(1, "\x1b[38;2;000;000;000m\n\x1b[0m", 24);
}
}

struct cimg *initialize_framebuffer(struct cimg *cimg)
{
cimg->num_pixels = CIMG_FRAMEBUFFER_PIXELS(cimg);
cimg->framebuffer = malloc(CIMG_FRAMEBUFFER_SIZE(cimg)+1);
if (cimg->framebuffer == NULL)
{
puts("ERROR: Failed to allocate memory for the framebuffer!");
exit(-1);
}
for (int idx = 0; idx < cimg->num_pixels; idx += 1)
{
char emit_tmp[24+1];
snprintf(emit_tmp, sizeof(emit_tmp), "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m", 255, 255, 255, ' ');
memcpy(cimg->framebuffer[idx].data, emit_tmp, 24);

}

return cimg;
}

void __attribute__ ((constructor)) disable_buffering()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
}

int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
cimg.framebuffer = NULL;
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'c' || cimg.header.magic_number[1] != 'I' || cimg.header.magic_number[2] != 'M' || cimg.header.magic_number[3] != 'G')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (cimg.header.version != 2)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

initialize_framebuffer(&cimg);

unsigned long data_size = cimg.header.width * cimg.header.height * sizeof(pixel_t);
pixel_t *data = malloc(data_size);
if (data == NULL)
{
puts("ERROR: Failed to allocate memory for the image data!");
exit(-1);
}
read_exact(0, data, data_size, "ERROR: Failed to read data!", -1);

for (int i = 0; i < cimg.header.width * cimg.header.height; i++)
{
if (data[i].ascii < 0x20 || data[i].ascii > 0x7e)
{
fprintf(stderr, "ERROR: Invalid character 0x%x in the image data!\n", data[i].ascii);
exit(-1);
}
}

display(&cimg, data);

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++)
{
if (cimg.framebuffer[i].str.c != ((term_pixel_t*)&desired_output)[i].str.c)
{
won = 0;
}
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();

}

In this challenge, the desired ANSI sequence is to big for us to manually craft the pixels. We will have to dynamically craft our ASCII payload based on the expected ANSI sequence.

  • File Extension: Must end with .cimg
  • Header (8 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (2 bytes): Must be 2 in little-endian
    • Dimensions (2 bytes total): Must be 53 x (num_pixels / 53) bytes
      • Width (1 bytes): Must be 53 (discovered by trial and error) in little-endian
      • Height (1 bytes): Must be num_pixels / width in little-endian
  • Pixel Data:
    • The number of non-space ASCII pixels must be num_pixels, i.e. the number of bytes must be 4 * num_pixels
    • When pixel data is loaded into the ANSI escape code: "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m" one by one and appended together, it should match the given ANSI sequence.

Exploit

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")

# This regex looks for the RGB numbers and the character that follows the 'm'
# (\d+) matches the digits for R, G, and B
# m(.) matches the 'm' followed by the single character we want
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"

# Find all matches in the sequence
matches = re.findall(pattern, desired_ansi_sequence)

# Convert the strings to the format you want: (int, int, int, ord(char))
pixels = [
(int(r), int(g), int(b), ord(char))
for r, g, b, char in matches
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

width_value = 53
height_value = len(pixels) // width_value

# Build the header (8 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<H", 2) # 2 bytes
width = struct.pack("<B", width_value) # 1 bytes
height = struct.pack("<B", height_value) # 1 bytes

header = magic + version + width + height

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to: {filename}")
hacker@reverse-engineering~internal-state-c:/$ python ~/script.py 
Wrote 3612 bytes:

# ---- snip ----

to: /home/hacker/solution.cimg
hacker@reverse-engineering~internal-state-c:/$ /challenge/cimg ~/solution.cimg 
.---------------------------------------------------.
| |
| |
| ___ |
| / __| ___ |
| | (__ |_ _| |
| \___| | | |
| | | |
| |___| __ __ |
| | \/ | ____ |
| | |\/| | / ___| |
| | | | | | | _ |
| |_| |_| | |_| | |
| \____| |
| |
| |
'---------------------------------------------------'
pwn.college{MeWc9ChLvjW8FhGUVQm-MFmVW7z.QXxITN2EDL4ITM0EzW}
image

 

Internal State (x86)

Binary Analysis

Again, let's add create some necessary structs, and change the types of some variables.

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
union term_pixel_t *framebuffer;
};

Notice that we did not include the __pad[4] field this time. That is because IDA sorts the padding for us, and ensures that the *framebuffer is naturally aligned on an 8-byte boundary according to the x86-64 ABI, without requiring any explicit padding in the source structure.

/challenge/cimg :: main() :: Pseudocode
int __fastcall main(int argc, const char **argv, const char **envp)
{
const char *file_arg; // rbp
int file; // eax
const char *error_msg; // rdi
unsigned int data_size; // ebx
unsigned __int8 *data; // rax
unsigned __int8 *data_1; // rbp
__int64 i_1; // rax
__int64 character; // rcx
char *desired_output; // r12
unsigned int num_pixels; // r14d
union term_pixel_t *framebuffer; // r13
_BOOL8 won; // rbx
unsigned int i; // ebp
char ascii_char; // al
struct cimg cimg; // [rsp+0h] [rbp-58h] BYREF
unsigned __int64 v19; // [rsp+18h] [rbp-40h]

v19 = __readfsqword(0x28u);
memset(&cimg, 0, sizeof(cimg));
if ( argc > 1 )
{
file_arg = argv[1];
if ( strcmp(&file_arg[strlen(file_arg) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto EXIT;
}
file = open(file_arg, 0);
dup2(file, 0);
}
read_exact(0LL, &cimg, 8LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( *(_DWORD *)cimg.header.magic_number != 'GMIc' )
{
error_msg = "ERROR: Invalid magic number!";
OUTPUT_ERROR_MSG_AND_EXIT:
puts(error_msg);
goto EXIT;
}
error_msg = "ERROR: Unsupported version!";
if ( cimg.header.version != 2 )
goto OUTPUT_ERROR_MSG_AND_EXIT;
initialize_framebuffer(&cimg);
data_size = 4 * cimg.header.height * cimg.header.width;
data = (unsigned __int8 *)malloc(4LL * cimg.header.height * cimg.header.width);
error_msg = "ERROR: Failed to allocate memory for the image data!";
data_1 = data;
if ( !data )
goto OUTPUT_ERROR_MSG_AND_EXIT;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
i_1 = 0LL;
while ( cimg.header.height * cimg.header.width > (int)i_1 )
{
character = data_1[4 * i_1++ + 3];
if ( (unsigned __int8)(character - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", character);
EXIT:
exit(-1);
}
}
desired_output = ::desired_output;
display(&cimg, data_1);
num_pixels = cimg.num_pixels;
framebuffer = cimg.framebuffer;
won = cimg.num_pixels == 1365;
for ( i = 0; i != 1365 && num_pixels > i; ++i )
{
ascii_char = framebuffer->data[19];
if ( ascii_char != desired_output[19] )
LODWORD(won) = 0;
if ( ascii_char != ' ' && ascii_char != '\n' )
{
if ( memcmp(framebuffer, desired_output, 24uLL) )
LODWORD(won) = 0;
}
++framebuffer;
desired_output += 24;
}
if ( won )
win();
return 0;
}

The code checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: main() :: Pseudocode
# ---- snip ----

if ( (unsigned __int8)(character - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&character);
EXIT:
exit(-1);
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

It then uses the 4-bytes in the pixels to fill in the ANSI sequence \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m based on the struct we defined earlier:

/challenge/cimg :: main() :: Pseudocode
# ---- snip ----

data_size = 4 * cimg.header.height * cimg.header.width;
data = (unsigned __int8 *)malloc(4LL * cimg.header.height * cimg.header.width);
error_msg = "ERROR: Failed to allocate memory for the image data!";

# ---- snip ----

display(&cimg, data_1);

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

Afterwards, it checks if the 20th byte in the crafted ANSI sequence is a space or newline character. If it is not, and everything else is matching the desired ANSI sequence, it calls win() which prints the flag.

The required ANSI sequence:

image

Putting everything together, we can see that the challenge performs the exact same checks as the Internal State Mini (C) version:

  • File Extension: Must end with .cimg
  • Header (8 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (2 bytes): Must be 2 in little-endian
    • Dimensions (2 bytes total): Must be 4 bytes
      • Width (1 bytes): Must be either 4 (if height = 1), 2 (if height = 2) or 1 (if height = 4) in little-endian
      • Height (1 bytes): Must be either 1 (if width = 4), 2 (if width = 2) or 4 (if width = 1) in little-endian
  • Pixel Data:
    • The number of non-space ASCII pixels must be 4 * 1 = 4, i.e. the number of bytes must be 4 * 4 = 16
    • When pixel data is loaded into the ANSI escape code: \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m one by one and appended together, it should match the followingthe much larger desired sequence.

Exploit

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")

# This regex looks for the RGB numbers and the character that follows the 'm'
# (\d+) matches the digits for R, G, and B
# m(.) matches the 'm' followed by the single character we want
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"

# Find all matches in the sequence
matches = re.findall(pattern, desired_ansi_sequence)

# Convert the strings to the format you want: (int, int, int, ord(char))
pixels = [
(int(r), int(g), int(b), ord(char))
for r, g, b, char in matches
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

width_value = 65
height_value = len(pixels) // width_value

# Build the header (8 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<H", 2) # 2 bytes
width = struct.pack("<B", width_value) # 1 bytes
height = struct.pack("<B", height_value) # 1 bytes

header = magic + version + width + height

# Full file content
cimg_data = header + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to: {filename}")
hacker@reverse-engineering~internal-state-x86:/$ python ~/script.py 
Wrote 5468 bytes:

# ---- snip ----

to: /home/hacker/solution.cimg
hacker@reverse-engineering~internal-state-x86:/$ /challenge/cimg ~/solution.cimg 
.---------------------------------------------------------------.
| |
| |
| |
| |
| ____ |
| ___ ___ / ___| |
| / __| |_ _| __ __ | | _ |
| | (__ | | | \/ | | |_| | |
| \___| | | | |\/| | \____| |
| |___| | | | | |
| |_| |_| |
| |
| |
| |
| |
| |
| |
| |
| |
'---------------------------------------------------------------'
pwn.college{gmLJQK0xAMY1I1Cfskv-zutRCEa.QX3AzMwEDL4ITM0EzW}
image

 

File Formats: Directives (C)

Source code

/challenge/cimg.c
#define _GNU_SOURCE 1

#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <assert.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/signal.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>
#include <sys/prctl.h>
#include <sys/personality.h>
#include <arpa/inet.h>

void win()
{
char flag[256];
int flag_fd;
int flag_length;

flag_fd = open("/flag", 0);
if (flag_fd < 0)
{
printf("\n ERROR: Failed to open the flag -- %s!\n", strerror(errno));
if (geteuid() != 0)
{
printf(" Your effective user id is not 0!\n");
printf(" You must directly run the suid binary in order to have the correct permissions!\n");
}
exit(-1);
}
flag_length = read(flag_fd, flag, sizeof(flag));
if (flag_length <= 0)
{
printf("\n ERROR: Failed to read the flag -- %s!\n", strerror(errno));
exit(-1);
}
write(1, flag, flag_length);
printf("\n\n");
}

void read_exact(int fd, void *dst, int size, char *msg, int exitcode)
{
int n = read(fd, dst, size);
if (n != size)
{
fprintf(stderr, msg);
fprintf(stderr, "\n");
exit(exitcode);
}
}

char desired_output[] = "\x1b[38;2;255;255;255m.\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m.\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;125;194;085m \x1b[0m\x1b[38;2;125;194;085m_\x1b[0m\x1b[38;2;125;194;085m_\x1b[0m\x1b[38;2;125;194;085m_\x1b[0m\x1b[38;2;125;194;085m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;054;062;047m \x1b[0m\x1b[38;2;054;062;047m \x1b[0m\x1b[38;2;054;062;047m_\x1b[0m\x1b[38;2;054;062;047m_\x1b[0m\x1b[38;2;054;062;047m_\x1b[0m\x1b[38;2;054;062;047m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;125;194;085m|\x1b[0m\x1b[38;2;125;194;085m_\x1b[0m\x1b[38;2;125;194;085m \x1b[0m\x1b[38;2;125;194;085m_\x1b[0m\x1b[38;2;125;194;085m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;054;062;047m \x1b[0m\x1b[38;2;054;062;047m/\x1b[0m\x1b[38;2;054;062;047m \x1b[0m\x1b[38;2;054;062;047m_\x1b[0m\x1b[38;2;054;062;047m_\x1b[0m\x1b[38;2;054;062;047m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;125;194;085m \x1b[0m\x1b[38;2;125;194;085m|\x1b[0m\x1b[38;2;125;194;085m \x1b[0m\x1b[38;2;125;194;085m|\x1b[0m\x1b[38;2;125;194;085m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;054;062;047m|\x1b[0m\x1b[38;2;054;062;047m \x1b[0m\x1b[38;2;054;062;047m(\x1b[0m\x1b[38;2;054;062;047m_\x1b[0m\x1b[38;2;054;062;047m_\x1b[0m\x1b[38;2;054;062;047m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;125;194;085m \x1b[0m\x1b[38;2;125;194;085m|\x1b[0m\x1b[38;2;125;194;085m \x1b[0m\x1b[38;2;125;194;085m|\x1b[0m\x1b[38;2;125;194;085m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m_\x1b[0m\x1b[38;2;135;246;022m_\x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m_\x1b[0m\x1b[38;2;135;246;022m_\x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;183;227;088m/\x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;054;062;047m \x1b[0m\x1b[38;2;054;062;047m\\\x1b[0m\x1b[38;2;054;062;047m_\x1b[0m\x1b[38;2;054;062;047m_\x1b[0m\x1b[38;2;054;062;047m_\x1b[0m\x1b[38;2;054;062;047m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;125;194;085m|\x1b[0m\x1b[38;2;125;194;085m_\x1b[0m\x1b[38;2;125;194;085m_\x1b[0m\x1b[38;2;125;194;085m_\x1b[0m\x1b[38;2;125;194;085m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m\\\x1b[0m\x1b[38;2;135;246;022m/\x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;183;227;088m|\x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;183;227;088m|\x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;135;246;022m\\\x1b[0m\x1b[38;2;135;246;022m/\x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;183;227;088m|\x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;183;227;088m|\x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m|\x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;183;227;088m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;183;227;088m \x1b[0m\x1b[38;2;183;227;088m\\\x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m_\x1b[0m\x1b[38;2;183;227;088m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;135;246;022m_\x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m \x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;135;246;022m_\x1b[0m\x1b[38;2;135;246;022m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;000;000;000m \x1b[0m\x1b[38;2;255;255;255m|\x1b[0m\x1b[38;2;255;255;255m'\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m-\x1b[0m\x1b[38;2;255;255;255m'\x1b[0m\x00";

struct cimg_header
{
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
} __attribute__((packed));

typedef struct
{
uint8_t ascii;
} pixel_bw_t;
#define COLOR_PIXEL_FMT "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m"
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;

struct cimg
{
struct cimg_header header;
unsigned num_pixels;
term_pixel_t *framebuffer;
};

#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))
#define CIMG_FRAMEBUFFER_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_FRAMEBUFFER_SIZE(cimg) (CIMG_FRAMEBUFFER_PIXELS(cimg) * sizeof(term_pixel_t))

void handle_17571(struct cimg *cimg)
{
unsigned long data_size = cimg->header.width * cimg->header.height * sizeof(pixel_t);
pixel_t *data = malloc(data_size);
if (data == NULL)
{
puts("ERROR: Failed to allocate memory for the image data!");
exit(-1);
}
read_exact(0, data, data_size, "ERROR: Failed to read data!", -1);

for (int i = 0; i < cimg->header.width * cimg->header.height; i++)
{
if (data[i].ascii < 0x20 || data[i].ascii > 0x7e)
{
fprintf(stderr, "ERROR: Invalid character 0x%x in the image data!\n", data[i].ascii);
exit(-1);
}
}

int idx = 0;
for (int y = 0; y < cimg->header.height; y++)
{
for (int x = 0; x < cimg->header.width; x++)
{
idx = (0+y)*((cimg)->header.width) + ((0+x)%((cimg)->header.width));
char emit_tmp[24+1];
snprintf(emit_tmp, sizeof(emit_tmp), "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m", data[y * cimg->header.width + x].r, data[y * cimg->header.width + x].g, data[y * cimg->header.width + x].b, data[y * cimg->header.width + x].ascii);
memcpy((cimg)->framebuffer[idx%(cimg)->num_pixels].data, emit_tmp, 24);

}
}

}

void display(struct cimg *cimg, pixel_t *data)
{
for (int i = 0; i < cimg->header.height; i++)
{
write(1, cimg->framebuffer+i*cimg->header.width, sizeof(term_pixel_t)*cimg->header.width);
write(1, "\x1b[38;2;000;000;000m\n\x1b[0m", 24);
}
}

struct cimg *initialize_framebuffer(struct cimg *cimg)
{
cimg->num_pixels = CIMG_FRAMEBUFFER_PIXELS(cimg);
cimg->framebuffer = malloc(CIMG_FRAMEBUFFER_SIZE(cimg)+1);
if (cimg->framebuffer == NULL)
{
puts("ERROR: Failed to allocate memory for the framebuffer!");
exit(-1);
}
for (int idx = 0; idx < cimg->num_pixels; idx += 1)
{
char emit_tmp[24+1];
snprintf(emit_tmp, sizeof(emit_tmp), "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m", 255, 255, 255, ' ');
memcpy(cimg->framebuffer[idx].data, emit_tmp, 24);

}

return cimg;
}

void __attribute__ ((constructor)) disable_buffering()
{
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 1);
}

int main(int argc, char **argv, char **envp)
{

struct cimg cimg = { 0 };
cimg.framebuffer = NULL;
int won = 1;

if (argc > 1)
{
if (strcmp(argv[1]+strlen(argv[1])-5, ".cimg"))
{
printf("ERROR: Invalid file extension!");
exit(-1);
}
dup2(open(argv[1], O_RDONLY), 0);
}

read_exact(0, &cimg.header, sizeof(cimg.header), "ERROR: Failed to read header!", -1);

if (cimg.header.magic_number[0] != 'c' || cimg.header.magic_number[1] != 'I' || cimg.header.magic_number[2] != 'M' || cimg.header.magic_number[3] != 'G')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (cimg.header.version != 3)
{
puts("ERROR: Unsupported version!");
exit(-1);
}

initialize_framebuffer(&cimg);

while (cimg.header.remaining_directives--)
{
uint16_t directive_code;
read_exact(0, &directive_code, sizeof(directive_code), "ERROR: Failed to read &directive_code!", -1);

switch (directive_code)
{
case 17571:
handle_17571(&cimg);
break;
default:
fprintf(stderr, "ERROR: invalid directive_code %ux\n", directive_code);
exit(-1);
}
}
display(&cimg, NULL);

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++)
{
if (cimg.framebuffer[i].str.c != ((term_pixel_t*)&desired_output)[i].str.c)
{
won = 0;
}
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();

}
  • File Extension: Must end with .cimg
  • Header (12 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (2 bytes): Must be 2 in little-endian
    • Dimensions (2 bytes total): Must be 53 x (num_pixels / 53) bytes
      • Width (1 bytes): Must be 53 (discovered by trial and error) in little-endian
      • Height (1 bytes): Must be num_pixels / width in little-endian
    • Remaining Directives (4 bytes): Must be 1 in little-endian (This tells the while loop to process one directive).
  • Directive Code (2 bytes):
    • Immediately following the header, we must provide the 2-byte code 17571 (little-endian) to trigger the handle_17571 function
  • Pixel Data:
    • The number of non-space ASCII pixels must be num_pixels, i.e. the number of bytes must be 4 * num_pixels
    • When pixel data is loaded into the ANSI escape code: "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m" one by one and appended together, it should match the given ANSI sequence.

Exploit

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")

# This regex looks for the RGB numbers and the character that follows the 'm'
# (\d+) matches the digits for R, G, and B
# m(.) matches the 'm' followed by the single character we want
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"

# Find all matches in the sequence
matches = re.findall(pattern, desired_ansi_sequence)

# Convert the strings to the format you want: (int, int, int, ord(char))
pixels = [
(int(r), int(g), int(b), ord(char))
for r, g, b, char in matches
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

width_value = 55
height_value = len(pixels) // width_value

# Build the header (12 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<H", 3) # 2 bytes
width = struct.pack("<B", width_value) # 1 bytes
height = struct.pack("<B", height_value) # 1 bytes
remaining_directives = struct.pack("<I", 1) # 4 bytes

header = magic + version + width + height + remaining_directives

# Add directive code
directive_code = struct.pack("<H", 17571) # 2 bytes

# Full file content
cimg_data = header + directive_code + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to: {filename}")
hacker@reverse-engineering~file-formats-directives-c:/$ python ~/script.py 
Wrote 3314 bytes:

# ---- snip ----

to: /home/hacker/solution.cimg
hacker@reverse-engineering~file-formats-directives-c:/$ /challenge/cimg ~/solution.cimg 
.-----------------------------------------------------.
| |
| |
| ___ |
| ___ |_ _| |
| / __| | | ____ |
| | (__ | | __ __ / ___| |
| \___| |___| | \/ | | | _ |
| | |\/| | | |_| | |
| | | | | \____| |
| |_| |_| |
| |
| |
| |
'-----------------------------------------------------'
pwn.college{YtYqzGPTd8ZcDWzwyHLOGwSsY0S.QXyITN2EDL4ITM0EzW}
image

 

File Formats: Directives (x86)

Binary Analysis

This time, the cimg_header struct has a new field called remaining_directives.

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
union term_pixel_t *framebuffer;
};

After using the structs, renaming some variables, and adding comments, we get the following:

/challenge/cimg :: main() :: Pseudocode
int __fastcall main(int argc, const char **argv, const char **envp)
{
const char *file_arg; // rbp
int file; // eax
const char *error_msg; // rdi
char *desired_output; // r12
unsigned int num_pixels; // r14d
union term_pixel_t *framebuffer; // r13
_BOOL8 won; // rbx
unsigned int i; // ebp
char ascii_char; // al
unsigned __int16 directive_code; // [rsp+0h] [rbp-5Ah] BYREF
struct cimg cimg; // [rsp+2h] [rbp-58h] BYREF
unsigned __int64 v16; // [rsp+1Ah] [rbp-40h]

v16 = __readfsqword(40u);
memset(&cimg, 0, sizeof(cimg));
if ( argc > 1 )
{
file_arg = argv[1];
if ( strcmp(&file_arg[strlen(file_arg) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto EXIT;
}
file = open(file_arg, 0);
dup2(file, 0);
}
read_exact(0LL, &cimg, 12LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( *(_DWORD *)cimg.header.magic_number != 'GMIc' )
{
error_msg = "ERROR: Invalid magic number!";
OUTPUT_ERROR_MSG_AND_EXIT:
puts(error_msg);
goto EXIT;
}
error_msg = "ERROR: Unsupported version!";
if ( cimg.header.version != 3 )
goto OUTPUT_ERROR_MSG_AND_EXIT;
initialize_framebuffer(&cimg);
while ( cimg.header.remaining_directives-- )
{
read_exact(0LL, &directive_code, 2LL, "ERROR: Failed to read &directive_code!", 0xFFFFFFFFLL);
if ( directive_code != 45381 )
{
__fprintf_chk(stderr, 1LL, "ERROR: invalid directive_code %ux\n", directive_code);
EXIT:
exit(-1);
}
handle_45381(&cimg);
}
desired_output = ::desired_output;
display(&cimg, 0LL);
num_pixels = cimg.num_pixels;
framebuffer = cimg.framebuffer;
won = cimg.num_pixels == 800;
for ( i = 0; i < num_pixels && i != 800; ++i )
{
ascii_char = framebuffer->data[19];
if ( ascii_char != desired_output[19] )
LODWORD(won) = 0;
// Check if ASCII character is a newline or space
if ( ascii_char != ' ' && ascii_char != '\n' )
{
if ( memcmp(framebuffer, desired_output, 24uLL) )
LODWORD(won) = 0;
}
++framebuffer;
desired_output += 24;
}
if ( won )
win();
return 0;
}
  • File Extension: Must end with .cimg
  • Header (12 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (2 bytes): Must be 2 in little-endian
    • Dimensions (2 bytes total): Must be 53 x (num_pixels / 53) bytes
      • Width (1 bytes): Must be 53 (discovered by trial and error) in little-endian
      • Height (1 bytes): Must be num_pixels / width in little-endian
    • Remaining Directives (4 bytes): Must be 1 in little-endian (This tells the while loop to process one directive).
  • Directive Code (2 bytes):
    • Immediately following the header, we must provide the 2-byte code 45381 (little-endian) to trigger the handle_45381 function
  • Pixel Data:
    • The number of non-space ASCII pixels must be num_pixels, i.e. the number of bytes must be 4 * num_pixels
    • When pixel data is loaded into the ANSI escape code: "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m" one by one and appended together, it should match the given ANSI sequence.

This time, the program additionally checks if the directive_code is equal to 45381, and calls the handle_45381 function. Let's see what that functions does.

/challenge/cimg :: handle_45381() :: Pseudocode
unsigned __int64 __fastcall handle_45381(struct cimg *cimg)
{
int width; // ebp
int height; // edx
size_t data_size; // rbp
pixel_t *data; // rax
pixel_t *data_1; // r12
__int64 i; // rax
__int64 ascii; // rcx
int y; // r13d
int x; // ebp
int width_1; // r15d
pixel_t *pixels; // rax
__int64 idx; // kr00_8
term_pixel_t emit_tmp; // [rsp+1Fh] [rbp-59h] BYREF
unsigned __int64 v15; // [rsp+38h] [rbp-40h]

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
i = 0LL;
while ( cimg->header.height * cimg->header.width > (int)i )
{
ascii = data_1[i++].ascii;
if ( (unsigned __int8)(ascii - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", ascii);
EXIT:
exit(-1);
}
}
for ( y = 0; cimg->header.height > y; ++y )
{
for ( x = 0; ; ++x )
{
width_1 = cimg->header.width;
if ( width_1 <= x )
break;
pixels = &data_1[y * width_1 + x];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
idx = x;
cimg->framebuffer[((unsigned int)(idx % width_1) + y * width_1) % cimg->num_pixels] = (union term_pixel_t)emit_tmp;
}
}
return __readfsqword(40u) ^ v15;
}

In the handle_45381(), the program allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_45381() :: Pseudocode
# ---- snip ----

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_45381() :: Pseudocode
# ---- snip ----

data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
i = 0LL;
while ( cimg->header.height * cimg->header.width > (int)i )
{
ascii = data_1[i++].ascii;
if ( (unsigned __int8)(ascii - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", ascii);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

It then uses the 4-bytes in the pixels to fill in the ANSI sequence \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m based on the struct we defined earlier:

/challenge/cimg :: handle_45381() :: Pseudocode
# ---- snip ----

for ( y = 0; cimg->header.height > y; ++y )
{
for ( x = 0; ; ++x )
{
width_1 = cimg->header.width;
if ( width_1 <= x )
break;
pixels = &data_1[y * width_1 + x];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
idx = x;
cimg->framebuffer[((unsigned int)(idx % width_1) + y * width_1) % cimg->num_pixels] = (union term_pixel_t)emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

Afterwards, main() checks if the 20th byte in the crafted ANSI sequence is a space or newline character. If it is not, and everything else is matching the desired ANSI sequence, it calls win() which prints the flag.

/challenge/cimg :: main() :: Pseudocode
# ---- snip ----

desired_output = ::desired_output;
display(&cimg, 0LL);
num_pixels = cimg.num_pixels;
framebuffer = cimg.framebuffer;
won = cimg.num_pixels == 800;
for ( i = 0; i < num_pixels && i != 800; ++i )
{
ascii_char = framebuffer->data[19];
if ( ascii_char != desired_output[19] )
LODWORD(won) = 0;
// Check if ASCII character is a newline or space
if ( ascii_char != ' ' && ascii_char != '\n' )
{
if ( memcmp(framebuffer, desired_output, 24uLL) )
LODWORD(won) = 0;
}
++framebuffer;
desired_output += 24;
}
if ( won )
win();

# ---- snip ----

Exploit

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")

# This regex looks for the RGB numbers and the character that follows the 'm'
# (\d+) matches the digits for R, G, and B
# m(.) matches the 'm' followed by the single character we want
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"

# Find all matches in the sequence
matches = re.findall(pattern, desired_ansi_sequence)

# Convert the strings to the format you want: (int, int, int, ord(char))
pixels = [
(int(r), int(g), int(b), ord(char))
for r, g, b, char in matches
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

width_value = 55
height_value = len(pixels) // width_value

# Build the header (12 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<H", 3) # 2 bytes
width = struct.pack("<B", width_value) # 1 bytes
height = struct.pack("<B", height_value) # 1 bytes
remaining_directives = struct.pack("<I", 1) # 4 bytes

header = magic + version + width + height + remaining_directives

# Add directive code
directive_code = struct.pack("<H", 45381) # 2 bytes

# Full file content
cimg_data = header + directive_code + pixel_data

# Write to disk
filename = "/home/hacker/solution.cimg"
with open(filename, "wb") as f:
f.write(cimg_data)

print(f"Wrote {len(cimg_data)} bytes: {cimg_data} to: {filename}")
hacker@reverse-engineering~file-formats-directives-x86:/$ python ~/script.py
Wrote 3214 bytes:

# ---- snip ----

to: /home/hacker/solution.cimg
hacker@reverse-engineering~file-formats-directives-x86:/$ /challenge/cimg ~/solution.cimg 
.--------------------------------------.
| |
| |
| |
| |
| |
| |
| |
| |
| |
| ___ __ __ |
| / __| ___ | \/ | ____ |
| | (__ |_ _| | |\/| | / ___| |
| \___| | | | | | | | | _ |
| | | |_| |_| | |_| | |
| |___| \____| |
| |
| |
| |
'--------------------------------------'
pwn.college{syk86MEMK8yI4ABF2f5AcH3BOKX.QX4AzMwEDL4ITM0EzW}
image

 

The Patch Directive

Binary Analysis

Lets create the necessary structs, and use them in the pseudocode.

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
union term_pixel_t *framebuffer;
};
/challenge/cimg :: main() :: Pseudocode
int __fastcall main(int argc, const char **argv, const char **envp)
{
const char *file_arg; // rbp
int file; // eax
const char *error_msg; // rdi
char *desired_output; // r12
unsigned int num_pixels; // r14d
union term_pixel_t *framebuffer; // r13
_BOOL8 won; // rbx
unsigned int i; // ebp
char ascii_char; // al
unsigned __int16 directive_code; // [rsp+0h] [rbp-5Ah] BYREF
struct cimg cimg; // [rsp+2h] [rbp-58h] BYREF
unsigned __int64 v16; // [rsp+1Ah] [rbp-40h]

v16 = __readfsqword(40u);
memset(&cimg, 0, sizeof(cimg));
if ( argc > 1 )
{
file_arg = argv[1];
if ( strcmp(&file_arg[strlen(file_arg) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto EXIT;
}
file = open(file_arg, 0);
dup2(file, 0);
}
read_exact(0LL, &cimg, 12LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( *(_DWORD *)cimg.header.magic_number != 'GMIc' )
{
error_msg = "ERROR: Invalid magic number!";
OUTPUT_ERROR_MSG_AND_EXIT:
puts(error_msg);
goto EXIT;
}
error_msg = "ERROR: Unsupported version!";
if ( cimg.header.version != 3 )
goto OUTPUT_ERROR_MSG_AND_EXIT;
initialize_framebuffer(&cimg);
while ( cimg.header.remaining_directives-- )
{
read_exact(0LL, &directive_code, 2LL, "ERROR: Failed to read &directive_code!", 0xFFFFFFFFLL);
if ( directive_code == 52965 )
{
handle_52965(&cimg);
}
else
{
if ( directive_code != 55369 )
{
__fprintf_chk(stderr, 1LL, "ERROR: invalid directive_code %ux\n", directive_code);
EXIT:
exit(-1);
}
handle_55369(&cimg);
}
}
desired_output = ::desired_output;
display(&cimg, 0LL);
num_pixels = cimg.num_pixels;
framebuffer = cimg.framebuffer;
won = cimg.num_pixels == 901;
for ( i = 0; num_pixels > i && i != 901; ++i )
{
ascii_char = framebuffer->data[19];
if ( ascii_char != desired_output[19] )
LOBYTE(won) = 0;
if ( ascii_char != ' ' && ascii_char != '\n' )
{
if ( memcmp(framebuffer, desired_output, 24uLL) )
LOBYTE(won) = 0;
}
++framebuffer;
desired_output += 24;
}
if ( (unsigned __int64)total_data <= 1340 && won )
win();
return 0;
}

Let's find the required width:

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")
print(desired_ansi_sequence)
hacker@reverse-engineering~the-patch-directive:/$ python ~/script.py 
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
.---------------------------------------------------.| || || ___ || / __| ___ || | (__ |_ _| || \___| | | || | | || |___| __ __ || | \/ | ____ || | |\/| | / ___| || | | | | | | _ || |_| |_| | |_| | || \____| || || |'---------------------------------------------------'
In [1]: print(len(".---------------------------------------------------."))
53

The main() function basically reads in the user input, sends it to the relevant handler funtions based on the directive code that it observes, and finally checks if the 20th byte in the crafted ANSI sequence is a space or newline character.

If it is not, and the total number of user provided bytes is less than 1340 it calls win() which prints the flag.

Let's look at both the handler functions.

/challenge/cimg :: handle_55369() :: Pseudocode
unsigned __int64 __fastcall handle_55369(struct cimg *cimg)
{
int width; // ebp
int height; // edx
size_t data_size; // rbp
pixel_t *data; // rax
pixel_t *data_1; // r12
__int64 i_1; // rax
uint8_t ascii_char; // cl
int i; // r13d
int j; // ebp
int width_1; // r15d
pixel_t *pixels; // rax
__int64 j_1; // kr00_8
term_pixel_t emit_tmp; // [rsp+1Fh] [rbp-59h] BYREF
unsigned __int64 v15; // [rsp+38h] [rbp-40h]

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
i_1 = 0LL;
while ( cimg->header.height * cimg->header.width > (int)i_1 )
{
*(_QWORD *)&ascii_char = data_1[i_1++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}
for ( i = 0; cimg->header.height > i; ++i )
{
for ( j = 0; ; ++j )
{
width_1 = cimg->header.width;
if ( width_1 <= j )
break;
pixels = &data_1[i * width_1 + j];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
j_1 = j;
cimg->framebuffer[((unsigned int)(j_1 % width_1) + i * width_1) % cimg->num_pixels] = (union term_pixel_t)emit_tmp;
}
}
return __readfsqword(40u) ^ v15;
}

The handle_55369() the allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_55369() :: Pseudocode
# ---- snip ----

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (unsigned __int8 *)malloc(data_size);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_55369() :: Pseudocode
# ---- snip ----

data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
i_1 = 0LL;
while ( cimg->header.height * cimg->header.width > (int)i_1 )
{
*(_QWORD *)&ascii_char = data_1[i_1++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

It then uses the 4-bytes in the pixels to fill in the ANSI sequence \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m based on the struct we defined earlier:

/challenge/cimg :: handle_55369() :: Pseudocode
# ---- snip ----

for ( i = 0; cimg->header.height > i; ++i )
{
for ( j = 0; ; ++j )
{
width_1 = cimg->header.width;
if ( width_1 <= j )
break;
pixels = (pixel_t *)&data_1[4 * i * width_1 + 4 * j];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
j_1 = j;
cimg->framebuffer[((unsigned int)(j_1 % width_1) + i * width_1) % cimg->num_pixels] = (union term_pixel_t)emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

Let's look at the other handle function: handle_52965().

/challenge/cimg :: handle_52965() :: Pseudocode
unsigned __int64 __fastcall handle_52965(struct cimg *cimg)
{
unsigned int data_size; // ebx
pixel_t *data; // rax
pixel_t *pixels; // rbp
__int64 v4; // rax
__int64 ascii_char; // rcx
int y_offset; // r13d
int x_offset; // r14d
int x_coord; // eax
int pixel_idx; // ecx
unsigned int ansi_idx; // ebx
unsigned __int8 width; // [rsp+Bh] [rbp-5Dh] BYREF
unsigned __int8 height; // [rsp+Ch] [rbp-5Ch] BYREF
unsigned __int8 base_x; // [rsp+Dh] [rbp-5Bh] BYREF
unsigned __int8 base_y; // [rsp+Eh] [rbp-5Ah] BYREF
term_pixel_t emit_tmp; // [rsp+Fh] [rbp-59h] BYREF
unsigned __int64 v17; // [rsp+28h] [rbp-40h]

v17 = __readfsqword(40u);
read_exact(0LL, &base_x, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &base_y, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);
data_size = 4 * height * width;
data = (pixel_t *)malloc(4LL * height * width);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
pixels = data;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( height * width > (int)v4 )
{
ascii_char = pixels[v4++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", ascii_char);
EXIT:
exit(-1);
}
}
for ( y_offset = 0; height > y_offset; ++y_offset )
{
x_offset = 0;
while ( width > x_offset )
{
x_coord = x_offset + base_x;
pixel_idx = x_offset + y_offset * width;
++x_offset;
ansi_idx = x_coord % cimg->header.width + cimg->header.width * (y_offset + base_y);
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[pixel_idx].r,
pixels[pixel_idx].g,
pixels[pixel_idx].b,
pixels[pixel_idx].ascii);
cimg->framebuffer[ansi_idx % cimg->num_pixels] = emit_tmp;
}
}
return __readfsqword(40u) ^ v17;
}

The handle_52965(), the program handles our input slightly differently, as it renders the characters at the the coordinates that we provide.

It first reads in the base_x, base_y, width and height values for the chunk of the cIMG to be rendered.

/challenge/cimg :: handle_52965() :: Pseudocode
# ---- snip ----

read_exact(0LL, &base_x, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &base_y, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

# ---- snip ----

Then it allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_52965() :: Pseudocode
# ---- snip ----

data_size = 4 * height * width;
data = (pixel_t *)malloc(4LL * height * width);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_52965() :: Pseudocode
# ---- snip ----

pixels = data;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( height * width > (int)v4 )
{
ascii_char = pixels[v4++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

Then, the required indexes (pixel_idx and ansi_idx) are calculated based on the base_x, base_y, width and height values that we provided. Based on these indexes, the code knows which characters to use in the chunk of the cIMG.

/challenge/cimg :: handle_52965() :: Pseudocode
# ---- snip ----

for ( y_offset = 0; height > y_offset; ++y_offset )
{
x_offset = 0;
while ( width > x_offset )
{
x_coord = x_offset + base_x;
pixel_idx = x_offset + y_offset * width;
++x_offset;
ansi_idx = x_coord % cimg->header.width + cimg->header.width * (y_offset + base_y);
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[pixel_idx].r,
pixels[pixel_idx].g,
pixels[pixel_idx].b,
pixels[pixel_idx].ascii);
cimg->framebuffer[ansi_idx % cimg->num_pixels] = emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

The main difference in the handle_52965() function compared to the handle_55369() function is that the handle_52965() allows us to choose the exact chunk in the cIMG we want to render.

This allows us to save space, as we no longer have to render the empty characters, and can render only the borders and the letters which are in the cIMG.

However, we will have to keep a few things in mind:

  • We will have to call the handle_52965() function multiple times to render the disconnected parts of the cIMG.
  • Each patch directive costs 6 bytes: 2 for directive_code + 4 for parameters (base_x, base_y, width, height).

To put everything together:

  • File Extension: Must end with .cimg
  • Header (12 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (2 bytes): Must be 3 in little-endian
    • Dimensions (2 bytes total): Must be 53 x (num_pixels / 53) bytes
      • Width (1 bytes): Must be 53 in little-endian
      • Height (1 bytes): Must be num_pixels / width in little-endian
    • Remaining Directives (4 bytes): Value TBD (This tells the while loop to process one directive).
  • Directive Code (2 bytes):
    • Immediately following the header, we must provide the 2-byte code 55369 and / or 52965 (little-endian) to trigger the handle_55369 and / or handle_52965 function
  • Pixel Data:
    • The total number of bytes must be less that or equal to 1340
    • The framebuffer contains exactly 1824 entries total. All entries must have the correct character at position 19; non-space/newline entries must also have fully matching 24-byte ANSI sequences.

Exploit

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")

# This regex looks for the RGB numbers and the character that follows the 'm'
# (\d+) matches the digits for R, G, and B
# m(.) matches the 'm' followed by the single character we want
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"

# Find all matches in the sequence
matches = re.findall(pattern, desired_ansi_sequence)

# Convert the strings to the format you want: (int, int, int, ord(char))
pixels = [
(int(r), int(g), int(b), ord(char))
for r, g, b, char in matches
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

width_value = 53
height_value = len(pixels) // width_value

directives_payload = b""
remaining_directives = 0

# Grouping logic to stay under 1340 bytes
for y in range(height_value):
x = 0
while x < width_value:
idx = y * width_value + x
_, _, _, char = pixels[idx]

if char == ord(' '):
x += 1
continue

run_pixels = []
start_x = x
while x < width_value:
curr_idx = y * width_value + x
curr_r, curr_g, curr_b, curr_char = pixels[curr_idx]
if curr_char == ord(' '):
break
run_pixels.append(struct.pack("BBBB", curr_r, curr_g, curr_b, curr_char))
x += 1

remaining_directives += 1
directives_payload += struct.pack("<H", 52965)
directives_payload += struct.pack("<B", start_x)
directives_payload += struct.pack("<B", y)
directives_payload += struct.pack("<B", len(run_pixels))
directives_payload += struct.pack("<B", 1)
directives_payload += b"".join(run_pixels)

# Build the header (12 bytes total)
magic = b"cIMG" # 4 bytes
version = struct.pack("<H", 3) # 2 bytes
width_byte = struct.pack("<B", width_value) # 1 bytes
height_byte = struct.pack("<B", height_value) # 1 bytes
dir_count = struct.pack("<I", remaining_directives) # 4 bytes

header = magic + version + width_byte + height_byte + dir_count

# Full file content
cimg_data = header + directives_payload

# Write to disk
with open("/home/hacker/solution.cimg", "wb") as f:
f.write(cimg_data)

print(f"Total Bytes: {len(cimg_data)}")
print(f"Directives used: {remaining_directives}")
hacker@reverse-engineering~the-patch-directive:~$ python ~/script.py 
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Total Bytes: 1292
Directives used: 70
~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")

# This regex looks for the RGB numbers and the character that follows the 'm'
# (\d+) matches the digits for R, G, and B
# m(.) matches the 'm' followed by the single character we want
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"

# Find all matches in the sequence
matches = re.findall(pattern, desired_ansi_sequence)

# Convert the strings to the format you want: (int, int, int, ord(char))
pixels = [
(int(r), int(g), int(b), ord(char))
for r, g, b, char in matches
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

width_value = 53
height_value = len(pixels) // width_value

directives_payload = b""
remaining_directives = 0

# --- 1. THE FOUR BORDER DIRECTIVES ---

def add_directive(x, y, w, h, pixel_list):
global directives_payload, remaining_directives
remaining_directives += 1
directives_payload += struct.pack("<HBBBB", 52965, x, y, w, h)
for p in pixel_list:
directives_payload += struct.pack("BBBB", p[0], p[1], p[2], p[3])

# Top Border (Row 0)
top_pixels = [pixels[i] for i in range(width_value)]
add_directive(0, 0, width_value, 1, top_pixels)

# Bottom Border (Row 16)
bottom_pixels = [pixels[i] for i in range(16 * width_value, 17 * width_value)]
add_directive(0, 16, width_value, 1, bottom_pixels)

# Left Border (Rows 1 to 15, Column 0)
left_pixels = [pixels[y * width_value] for y in range(1, 16)]
add_directive(0, 1, 1, 15, left_pixels)

# Right Border (Rows 1 to 15, Column 52)
right_pixels = [pixels[y * width_value + 52] for y in range(1, 16)]
add_directive(52, 1, 1, 15, right_pixels)

# --- 2. LOGO CONTENT (Interior Only) ---
# We skip x=0, x=52, y=0, and y=16 to avoid redrawing borders
for y in range(1, 16):
x = 1
while x < width_value - 1:
idx = y * width_value + x
_, _, _, char = pixels[idx]

if char == ord(' '):
x += 1
continue

run_pixels = []
start_x = x
while x < width_value - 1:
curr_idx = y * width_value + x
p = pixels[curr_idx]
if p[3] == ord(' '):
break
run_pixels.append(struct.pack("BBBB", p[0], p[1], p[2], p[3]))
x += 1

remaining_directives += 1
directives_payload += struct.pack("<HBBBB", 52965, start_x, y, len(run_pixels), 1)
directives_payload += b"".join(run_pixels)

# --- 3. HEADER AND OUTPUT ---
header = struct.pack("<IHBBI", 0x474d4963, 3, width_value, height_value, remaining_directives)
cimg_data = header + directives_payload

with open("/home/hacker/solution.cimg", "wb") as f:
f.write(cimg_data)

print(f"Total Bytes: {len(cimg_data)}")
print(f"Directives used: {remaining_directives}")
hacker@reverse-engineering~the-patch-directive:~$ python ~/script.py 
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Total Bytes: 1124
Directives used: 42
~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")

# This regex looks for the RGB numbers and the character that follows the 'm'
# (\d+) matches the digits for R, G, and B
# m(.) matches the 'm' followed by the single character we want
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"

# Find all matches in the sequence
matches = re.findall(pattern, desired_ansi_sequence)

# Convert the strings to the format you want: (int, int, int, ord(char))
pixels = [
(int(r), int(g), int(b), ord(char))
for r, g, b, char in matches
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

width_value = 53
height_value = len(pixels) // width_value

directives_payload = b""
remaining_directives = 0

def add_box(x, y, w, h):
global directives_payload, remaining_directives
remaining_directives += 1
directives_payload += struct.pack("<HBBBB", 52965, x, y, w, h)
for row in range(y, y + h):
for col in range(x, x + w):
p = pixels[row * width_value + col]
directives_payload += struct.pack("BBBB", p[0], p[1], p[2], p[3])

# --- BORDERS (4 Directives) ---
add_box(0, 0, width_value, 1) # Top
add_box(0, 16, width_value, 1) # Bottom
add_box(0, 1, 1, 15) # Left
add_box(52, 1, 1, 15) # Right

# --- CHARACTERS (4 Directives) ---
# Coordinates approximate based on the ASCII art provided
add_box(6, 3, 6, 4) # "C"
add_box(19, 4, 5, 5) # "I"
add_box(29, 8, 8, 5) # "M"
add_box(39, 9, 7, 5) # "G"

# --- HEADER ---
header = struct.pack("<IHBBI", b"cIMG", 3, width_value, height_value, remaining_directives)

# Full file content
cimg_data = header + directives_payload

# Write to disk
with open("/home/hacker/solution.cimg", "wb") as f:
f.write(cimg_data)

print(f"Total Bytes: {len(cimg_data)}")
print(f"Directives used: {remaining_directives}")
hacker@reverse-engineering~the-patch-directive:/$ python ~/script.py 
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Total Bytes: 1100
Directives used: 8
hacker@reverse-engineering~the-patch-directive:/$ /challenge/cimg ~/solution.cimg
hacker@reverse-engineering~the-patch-directive:~$ /challenge/cimg ~/solution.cimg 
.---------------------------------------------------.
| |
| |
| ___ |
| / __| ___ |
| | (__ |_ _| |
| \___| | | |
| | | |
| |___| __ __ |
| | \/ | ____ |
| | |\/| | / ___| |
| | | | | | | _ |
| |_| |_| | |_| | |
| \____| |
| |
| |
'---------------------------------------------------'
pwn.college{UMeXSUaFZYlR24CqDvgWdMZihWp.QX5AzMwEDL4ITM0EzW}
image

 

Optimizing for Space

Binary Analysis

We must first create the necessary structs.

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
union term_pixel_t *framebuffer;
};
/challenge/cimg :: main() :: Pseudocode
int __fastcall main(int argc, const char **argv, const char **envp)
{
const char *file_arg; // rbp
int file; // eax
const char *error_msg; // rdi
char *desired_output; // r12
unsigned int num_pixels; // r14d
union term_pixel_t *framebuffer; // r13
_BOOL8 won; // rbx
unsigned int i; // ebp
char ascii_char; // al
unsigned __int16 directive_code; // [rsp+0h] [rbp-5Ah] BYREF
struct cimg cimg; // [rsp+2h] [rbp-58h] BYREF
unsigned __int64 v16; // [rsp+1Ah] [rbp-40h]

v16 = __readfsqword(40u);
memset(&cimg, 0, sizeof(cimg));
if ( argc > 1 )
{
file_arg = argv[1];
if ( strcmp(&file_arg[strlen(file_arg) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto EXIT;
}
file = open(file_arg, 0);
dup2(file, 0);
}
read_exact(0LL, &cimg, 12LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( *(_DWORD *)cimg.header.magic_number != 'GMIc' )
{
error_msg = "ERROR: Invalid magic number!";
OUTPUT_ERROR_MSG_AND_EXIT:
puts(error_msg);
goto EXIT;
}
error_msg = "ERROR: Unsupported version!";
if ( cimg.header.version != 3 )
goto OUTPUT_ERROR_MSG_AND_EXIT;
initialize_framebuffer(&cimg);
while ( cimg.header.remaining_directives-- )
{
read_exact(0LL, &directive_code, 2LL, "ERROR: Failed to read &directive_code!", 0xFFFFFFFFLL);
if ( directive_code == 52965 )
{
handle_52965(&cimg);
}
else
{
if ( directive_code != 55369 )
{
__fprintf_chk(stderr, 1LL, "ERROR: invalid directive_code %ux\n", directive_code);
EXIT:
exit(-1);
}
handle_55369(&cimg);
}
}
desired_output = ::desired_output;
display(&cimg, 0LL);
num_pixels = cimg.num_pixels;
framebuffer = cimg.framebuffer;
won = cimg.num_pixels == 1824;
for ( i = 0; num_pixels > i && i != 1824; ++i )
{
ascii_char = framebuffer->data[19];
if ( ascii_char != desired_output[19] )
LOBYTE(won) = 0;
if ( ascii_char != ' ' && ascii_char != '\n' )
{
if ( memcmp(framebuffer, desired_output, 0x18uLL) )
LOBYTE(won) = 0;
}
++framebuffer;
desired_output += 24;
}
if ( (unsigned __int64)total_data <= 1337 && won )
win();
return 0;
}

The main() function basically reads in the user input, sends it to the relevant handler funtions based on the directive code that it observes, and finally checks if the 20th byte in the crafted ANSI sequence is a space or newline character.

It checks if the 20th byte in the crafted ANSI sequence is a space or newline character. If it is not, and the total number of user provided bytes is less than 1337 it calls win() which prints the flag.

Let's find the required width:

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")
print(desired_ansi_sequence)
hacker@reverse-engineering~optimizing-for-space:~$ python ~/script.py
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
.--------------------------------------------------------------------------.| || || || || || || || || ___ __ __ ____ || ___ |_ _| | \/ | / ___| || / __| | | | |\/| | | | _ || | (__ | | | | | | | |_| | || \___| |___| |_| |_| \____| || || || || || || || || || |'--------------------------------------------------------------------------'
In [1]: print(len(".--------------------------------------------------------------------------."))
76

Let's look at the handle_55369() function.

/challenge/cimg :: handle_55369() :: Pseudocode
unsigned __int64 __fastcall handle_55369(struct cimg *cimg)
{
int width; // ebp
int height; // edx
size_t data_size; // rbp
pixel_t *data; // rax
pixel_t *data_1; // r12
__int64 i_1; // rax
uint8_t ascii_char; // cl
int i; // r13d
int j; // ebp
int width_1; // r15d
pixel_t *pixels; // rax
__int64 j_1; // kr00_8
term_pixel_t emit_tmp; // [rsp+1Fh] [rbp-59h] BYREF
unsigned __int64 v15; // [rsp+38h] [rbp-40h]

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
i_1 = 0LL;
while ( cimg->header.height * cimg->header.width > (int)i_1 )
{
*(_QWORD *)&ascii_char = data_1[i_1++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}
for ( i = 0; cimg->header.height > i; ++i )
{
for ( j = 0; ; ++j )
{
width_1 = cimg->header.width;
if ( width_1 <= j )
break;
pixels = &data_1[i * width_1 + j];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
j_1 = j;
cimg->framebuffer[((unsigned int)(j_1 % width_1) + i * width_1) % cimg->num_pixels] = (union term_pixel_t)emit_tmp;
}
}
return __readfsqword(40u) ^ v15;
}

The handle_55369() the allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_55369() :: Pseudocode
# ---- snip ----

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_55369() :: Pseudocode
# ---- snip ----

data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
i_1 = 0LL;
while ( cimg->header.height * cimg->header.width > (int)i_1 )
{
*(_QWORD *)&ascii_char = data_1[i_1++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

It then uses the 4-bytes in the pixels to fill in the ANSI sequence \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m based on the struct we defined earlier:

/challenge/cimg :: handle_55369() :: Pseudocode
# ---- snip ----

for ( i = 0; cimg->header.height > i; ++i )
{
for ( j = 0; ; ++j )
{
width_1 = cimg->header.width;
if ( width_1 <= j )
break;
pixels = &data_1[i * width_1 + j];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
j_1 = j;
cimg->framebuffer[((unsigned int)(j_1 % width_1) + i * width_1) % cimg->num_pixels] = (union term_pixel_t)emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

Let's look at the other handle function: handle_52965().

/challenge/cimg :: handle_52965() :: Pseudocode
unsigned __int64 __fastcall handle_52965(struct cimg *cimg)
{
unsigned int data_size; // ebx
pixel_t *data; // rax
pixel_t *pixels; // rbp
__int64 v4; // rax
__int64 ascii_char; // rcx
int y_offset; // r13d
int x_offset; // r14d
int x_coord; // eax
int pixel_idx; // ecx
unsigned int ansi_idx; // ebx
unsigned __int8 width; // [rsp+Bh] [rbp-5Dh] BYREF
unsigned __int8 height; // [rsp+Ch] [rbp-5Ch] BYREF
unsigned __int8 base_x; // [rsp+Dh] [rbp-5Bh] BYREF
unsigned __int8 base_y; // [rsp+Eh] [rbp-5Ah] BYREF
term_pixel_t emit_tmp; // [rsp+Fh] [rbp-59h] BYREF
unsigned __int64 v17; // [rsp+28h] [rbp-40h]

v17 = __readfsqword(40u);
read_exact(0LL, &base_x, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &base_y, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);
data_size = 4 * height * width;
data = (pixel_t *)malloc(4LL * height * width);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
pixels = data;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( height * width > (int)v4 )
{
ascii_char = pixels[v4++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", ascii_char);
EXIT:
exit(-1);
}
}
for ( y_offset = 0; height > y_offset; ++y_offset )
{
x_offset = 0;
while ( width > x_offset )
{
x_coord = x_offset + base_x;
pixel_idx = x_offset + y_offset * width;
++x_offset;
ansi_idx = x_coord % cimg->header.width + cimg->header.width * (y_offset + base_y);
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[pixel_idx].r,
pixels[pixel_idx].g,
pixels[pixel_idx].b,
pixels[pixel_idx].ascii);
cimg->framebuffer[ansi_idx % cimg->num_pixels] = emit_tmp;
}
}
return __readfsqword(0x28u) ^ v17;
}

The handle_52965(), the program handles our input slightly differently, as it renders the characters at the the coordinates that we provide.

It first reads in the base_x, base_y, width and height values for the chunk of the cIMG to be rendered.

/challenge/cimg :: handle_52965() :: Pseudocode
# ---- snip ----

read_exact(0LL, &base_x, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &base_y, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

# ---- snip ----

Then it allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_52965() :: Pseudocode
# ---- snip ----

data_size = 4 * height * width;
data = (pixel_t *)malloc(4LL * height * width);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_52965() :: Pseudocode
# ---- snip ----

pixels = data;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( height * width > (int)v4 )
{
ascii_char = pixels[v4++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

Then, the required indexes (pixel_idx and ansi_idx) are calculated based on the base_x, base_y, width and height values that we provided. Based on these indexes, the code knows which characters to use in the chunk of the cIMG.

/challenge/cimg :: handle_52965() :: Pseudocode
# ---- snip ----

for ( y_offset = 0; height > y_offset; ++y_offset )
{
x_offset = 0;
while ( width > x_offset )
{
x_coord = x_offset + base_x;
pixel_idx = x_offset + y_offset * width;
++x_offset;
ansi_idx = x_coord % cimg->header.width + cimg->header.width * (y_offset + base_y);
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[pixel_idx].r,
pixels[pixel_idx].g,
pixels[pixel_idx].b,
pixels[pixel_idx].ascii);
cimg->framebuffer[ansi_idx % cimg->num_pixels] = emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

The main difference in the handle_52965() function compared to the handle_55369() function is that the handle_52965() allows us to choose the exact chunk in the cIMG we want to render.

This allows us to save space, as we no longer have to render the empty characters, and can render only the borders and the letters which are in the cIMG.

However, we will have to keep a few things in mind:

  • We will have to call the handle_52965() function multiple times to render the disconnected parts of the cIMG.
  • Each patch directive costs 6 bytes: 2 for directive_code + 4 for parameters (base_x, base_y, width, height).

To put everything together:

  • File Extension: Must end with .cimg
  • Header (12 bytes total):
    • Magic number (4 bytes): Must be b"cIMG"
    • Version (2 bytes): Must be 3 in little-endian
    • Dimensions (2 bytes total): Must be 76 x (num_pixels / 76) bytes
      • Width (1 bytes): Must be 76 in little-endian
      • Height (1 bytes): Must be num_pixels / width in little-endian
    • Remaining Directives (4 bytes): Value TBD (This tells the while loop to process one directive).
  • Directive Code (2 bytes):
    • Immediately following the header, we must provide the 2-byte code 55369 and / or 52965 (little-endian) to trigger the handle_55369 and / or handle_52965 function
  • Pixel Data:
    • The total number of bytes must be less that or equal to 1337
    • The framebuffer contains exactly 1824 entries total. All entries must have the correct character at position 19; non-space/newline entries must also have fully matching 24-byte ANSI sequences.

All in all, the only difference is the constraint on number of bytes. We can only provide 1337 bytes.

Exploit

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")

# This regex looks for the RGB numbers and the character that follows the 'm'
# (\d+) matches the digits for R, G, and B
# m(.) matches the 'm' followed by the single character we want
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"

# Find all matches in the sequence
matches = re.findall(pattern, desired_ansi_sequence)

# Convert the strings to the format you want: (int, int, int, ord(char))
pixels = [
(int(r), int(g), int(b), ord(char))
for r, g, b, char in matches
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

width_value = 76
height_value = len(pixels) // width_value

directives_payload = b""
remaining_directives = 0

# --- 1. THE FOUR BORDER DIRECTIVES ---

def add_directive(x, y, w, h, pixel_list):
global directives_payload, remaining_directives
remaining_directives += 1
directives_payload += struct.pack("<HBBBB", 52965, x, y, w, h)
for p in pixel_list:
directives_payload += struct.pack("BBBB", p[0], p[1], p[2], p[3])

# Top Border (Row 0) - 76 pixels
top_pixels = pixels[0 : width_value]
add_directive(0, 0, width_value, 1, top_pixels)

# Bottom Border (Row 23) - 76 pixels
bottom_pixels = pixels[23 * width_value : 24 * width_value]
add_directive(0, 23, width_value, 1, bottom_pixels)

# Left Border (Rows 1 to 22) - 22 pixels
# Note: w=1, h=22. List must have exactly 22 items.
left_pixels = [pixels[y * width_value] for y in range(1, 23)]
add_directive(0, 1, 1, 22, left_pixels)

# Right Border (Rows 1 to 22, Column 75) - 22 pixels
# Note: x=75, w=1, h=22. List must have exactly 22 items.
right_pixels = [pixels[y * width_value + 75] for y in range(1, 23)]
add_directive(75, 1, 1, 22, right_pixels)

# --- 2. LOGO CONTENT (Interior Only) ---
# We skip x=0, x=52, y=0, and y=16 to avoid redrawing borders
for y in range(1, 23):
x = 1
while x < width_value - 1:
idx = y * width_value + x
_, _, _, char = pixels[idx]

if char == ord(' '):
x += 1
continue

run_pixels = []
start_x = x
while x < width_value - 1:
curr_idx = y * width_value + x
p = pixels[curr_idx]
if p[3] == ord(' '):
break
run_pixels.append(struct.pack("BBBB", p[0], p[1], p[2], p[3]))
x += 1

remaining_directives += 1
directives_payload += struct.pack("<HBBBB", 52965, start_x, y, len(run_pixels), 1)
directives_payload += b"".join(run_pixels)

# --- 3. HEADER AND OUTPUT ---
header = struct.pack("<IHBBI", 0x474d4963, 3, width_value, height_value, remaining_directives)
cimg_data = header + directives_payload

with open("/home/hacker/solution.cimg", "wb") as f:
f.write(cimg_data)

print(f"Total Bytes: {len(cimg_data)} (Limit: 1337)")
print(f"Directives used: {remaining_directives}")
hacker@reverse-engineering~optimizing-for-space:~$ python ~/script.py 
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Total Bytes: 1364 (Limit: 1337)
Directives used: 42
~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")

# This regex looks for the RGB numbers and the character that follows the 'm'
# (\d+) matches the digits for R, G, and B
# m(.) matches the 'm' followed by the single character we want
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"

# Find all matches in the sequence
matches = re.findall(pattern, desired_ansi_sequence)

# Convert the strings to the format you want: (int, int, int, ord(char))
pixels = [
(int(r), int(g), int(b), ord(char))
for r, g, b, char in matches
]

pixel_data = b"".join(struct.pack("BBBB", r, g, b, a) for r, g, b, a in pixels)

width_value = 76
height_value = len(pixels) // width_value

directives_payload = b""
remaining_directives = 0

def add_box(x, y, w, h):
global directives_payload, remaining_directives
remaining_directives += 1
directives_payload += struct.pack("<HBBBB", 52965, x, y, w, h)
for row in range(y, y + h):
for col in range(x, x + w):
p = pixels[row * width_value + col]
directives_payload += struct.pack("BBBB", p[0], p[1], p[2], p[3])

# --- BORDERS (4 Directives) ---
add_box(0, 0, width_value, 1) # Top
add_box(0, (height_value-1), width_value, 1) # Bottom

side_border_height = height_value - 2
add_box(0, 1, 1, side_border_height) # Left
add_box((width_value-1), 1, 1, side_border_height) # Right

# --- CHARACTERS (4 Directives) ---
# Coordinates approximate based on the ASCII art provided
add_box(31, 9, 20, 1) # "___ __ __ ____"
add_box(25, 10, 27, 1) # "___ |_ _| | \/ | / ___|"
add_box(24, 11, 27, 1) # "/ __| | | | |\/| | | | _ "
add_box(23, 12, 29, 1) # "| (__ | | | | | | | |_| |"
add_box(24, 13, 28, 1) # "\___| |___| |_| |_| \____|"

# --- HEADER ---
header = struct.pack("<IHBBI", 0x474d4963, 3, width_value, height_value, remaining_directives)

# Full file content
cimg_data = header + directives_payload

# Write to disk
with open("/home/hacker/solution.cimg", "wb") as f:
f.write(cimg_data)

print(f"Total Bytes: {len(cimg_data)} (Limit: 1337)")
print(f"Directives used: {remaining_directives}")
hacker@reverse-engineering~optimizing-for-space:/$ python ~/script.py 
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Total Bytes: 1374
Directives used: 9

Dynamically decide whether header overhead / blank spaces would take less bytes

~/script.py
from pwn import *
import struct
import re

# 1. Setup and Pixel Extraction
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")

# Extract RGB and Character data
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"
matches = re.findall(pattern, desired_ansi_sequence)
pixels = [(int(r), int(g), int(b), ord(char)) for r, g, b, char in matches]

# Global dimensions for the 1824-pixel challenge
width_value = 76
height_value = 24 # 1824 total pixels / 76 width
directives_payload = b""
remaining_directives = 0

# 2. Directive Builder
def add_directive(x, y, w, h, pixel_list):
global directives_payload, remaining_directives
# Verification to prevent the "Invalid character 0x1" desync error
if len(pixel_list) != (w * h):
return

remaining_directives += 1
# Directive Header (6 bytes)
directives_payload += struct.pack("<HBBBB", 52965, x, y, w, h)
# Pixel Payload (4 bytes per pixel)
for p in pixel_list:
directives_payload += struct.pack("BBBB", p[0], p[1], p[2], p[3])

# 3. Optimized Border Directives
# Top and Bottom (Full row)
add_directive(0, 0, width_value, 1, pixels[0 : width_value])
add_directive(0, 23, width_value, 1, pixels[23 * width_value : 24 * width_value])

# Left and Right Sides (Height = 22 to avoid corner overlap)
side_height = height_value - 2
left_pixels = [pixels[y * width_value] for y in range(1, 23)]
add_directive(0, 1, 1, side_height, left_pixels)

right_pixels = [pixels[y * width_value + 75] for y in range(1, 23)]
add_directive(75, 1, 1, side_height, right_pixels)

# 4. Optimized Logo Content (Horizontal Strips with Bridging)
for y in range(1, 23):
x = 1
while x < 75:
idx = y * width_value + x
if pixels[idx][3] == ord(' '):
x += 1
continue

start_x = x
run_data = []
while x < 75:
p = pixels[y * width_value + x]

if p[3] == ord(' '):
# BRIDGE LOGIC: If a single space is followed by more ink,
# include it to save 6 bytes of header overhead.
if x + 1 < 75 and pixels[y * width_value + (x+1)][3] != ord(' '):
run_data.append(p)
x += 1
continue
else:
break # Gap is too large, end directive

run_data.append(p)
x += 1

# Add the horizontal segment
add_directive(start_x, y, len(run_data), 1, run_data)

# 5. Global Header and File Output
# Magic: 0x474d4963 ('cIMG'), Version: 3, Width: 76, Height: 24, Count: remaining_directives
header = struct.pack("<IHBBI", 0x474D4963, 3, width_value, height_value, remaining_directives)
cimg_data = header + directives_payload

with open("/home/hacker/solution.cimg", "wb") as f:
f.write(cimg_data)

print(f"Total Bytes: {len(cimg_data)} (Limit: 1337)")
print(f"Directives used: {remaining_directives}")
hacker@reverse-engineering~optimizing-for-space:~$ python ~/script.py 
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Total Bytes: 1328 (Limit: 1337)
Directives used: 24
hacker@reverse-engineering~optimizing-for-space:~$ /challenge/cimg ~/solution.cimg 
.--------------------------------------------------------------------------.
| |
| |
| |
| |
| |
| |
| |
| |
| ___ __ __ ____ |
| ___ |_ _| | \/ | / ___| |
| / __| | | | |\/| | | | _ |
| | (__ | | | | | | | |_| | |
| \___| |___| |_| |_| \____| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
'--------------------------------------------------------------------------'
pwn.college{AIEMVclq8dEJTwR93Pr44xYlNxZ.QXwEzMwEDL4ITM0EzW}
image

 

Tweaking Images

hacker@reverse-engineering~tweaking-images:/$ ls /challenge/
DESCRIPTION.md cimg cimg.c generate_flag_image

This time, there is a generate_flag_image script, which generates a flag cIMG for us.

hacker@reverse-engineering~tweaking-images:/$ /challenge/generate_flag_image 
hacker@reverse-engineering~tweaking-images:/$ ls /challenge/
DESCRIPTION.md cimg cimg.c flag.cimg generate_flag_image

Since, we already have the /challenge/flag.cimg file, let's pass it to the /challenge/cimg program.

hacker@reverse-engineering~tweaking-images:/$ /challenge/cimg /challenge/flag.cimg 
ERROR: invalid directive_code 2x

So, the directive code in the /challenge/flag.cimg file is 2 but that is not what is expected.

Binary Analysis

We can even verify this. For some reason the free version if IDA does not support the file format, so we will have to use Binary Ninja.

image

We can see that it sets the directive code to 2.

struct.pack(\"<HBBBBBBBB\", 2, p[0], p[1], 1, 1, 0x8c, 0x1d, 0x40, p[2]) 

Let's check what the /challenge/cimg program expects.

int __fastcall main(int argc, const char **argv, const char **envp)
{
const char *v3; // rbp
int v4; // eax
const char *v5; // rdi
unsigned __int16 v8; // [rsp+0h] [rbp-3Ah] BYREF
__int128 v9; // [rsp+2h] [rbp-38h] BYREF
__int64 v10; // [rsp+12h] [rbp-28h]
unsigned __int64 v11; // [rsp+1Ah] [rbp-20h]

v11 = __readfsqword(0x28u);
v9 = 0LL;
v10 = 0LL;
if ( argc > 1 )
{
v3 = argv[1];
if ( strcmp(&v3[strlen(v3) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto LABEL_8;
}
v4 = open(v3, 0);
dup2(v4, 0);
}
read_exact(0LL, &v9, 12LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( (_DWORD)v9 != 1196247395 )
{
v5 = "ERROR: Invalid magic number!";
LABEL_7:
puts(v5);
goto LABEL_8;
}
v5 = "ERROR: Unsupported version!";
if ( WORD2(v9) != 3 )
goto LABEL_7;
initialize_framebuffer(&v9);
while ( DWORD2(v9)-- )
{
read_exact(0LL, &v8, 2LL, "ERROR: Failed to read &directive_code!", 0xFFFFFFFFLL);
if ( v8 == 13725 )
{
handle_13725(&v9);
}
else
{
if ( v8 != 0x8B48 )
{
__fprintf_chk(stderr, 1LL, "ERROR: invalid directive_code %ux\n", v8);
LABEL_8:
exit(-1);
}
handle_35656(&v9);
}
}
display(&v9, 0LL);
return 0;
}

The expected directive is 13725, which is b"\x9d\x35".

Let's craft an exploit that fixes /challenge/flag.cimg.

Exploit

~/script.py
#!/usr/bin/env python3

# Define the paths
input_file = "/challenge/flag.cimg"
output_file = "/home/hacker/fixed_flag.cimg"

with open(input_file, "rb") as f:
# Read the entire file into a bytearray so we can modify it
data = bytearray(f.read())

# --- Step 1: Fix the Canvas Height ---
# The header is 12 bytes. Offset 0x07 is the Height.
# The original generator set this to 1. We change it to 100 (0x64).
data[0x07] = 0x40

# --- Step 2: Fix the Directive Codes ---
# The binary expects code 13725 (0x359D), which is \x9d\x35 in little-endian.
# The generator wrote code 2, which is \x02\x00 in little-endian.
# We replace all instances of the wrong code with the right one.
# We skip the first 12 bytes to avoid accidentally touching the header.
header = data[:12]
body = data[12:].replace(b"\x02\x00", b"\x9d\x35")

# Combine and save
with open(output_file, "wb") as o:
o.write(header + body)

print(f"Patched file saved to {output_file}")
hacker@reverse-engineering~tweaking-images:/$ python ~/script.py 
Patched file saved to /home/hacker/fixed_flag.cimg
hacker@reverse-engineering~tweaking-images:/$ /challenge/cimg ~/fixed_flag.cimg 

""# ""#
mmmm m m m mm mmm mmm # # mmm mmmm mmm
#" "# "m m m" #" # #" " #" "# # # #" # #" "# #" #
# # #m#m# # # # # # # # #"""" # # #""""
##m#" # # # # # "#mm" "#m#" "mm "mm "#mm" "#m"# "#mm"
# m #
" ""

m"" m m m m # mmmmm mmmm mmmm mmm
# # # "m m" mmm# # "#m m mmm # # m" "m m mm #
mm" # # # # #" "# #mmmm""m m m" #" "# "mmmm" # # #" " #
# # # "mm" # # # "m #m#m# # # # "# # # # #
# "mmmm" ## "#m## # " # # "#m#" "#mmm" #mm#" # mm#mm
"" #


mmmmm m m mmmmmm # m m mmmm mmmm mmmm m m mmmm
# "# "m m" #" mmmm #mmm # # m" "m " "# " "# ## ## m" "m
#mmm#" "#" m" #" "# #" "# #mmmm# # m # mmm" mmm" # ## # # #
# # m" # # # # # # # # "# "# # "" # # #
# # m" "#m## ##m#" # # #mm# "mmm#" "mmm#" # # #mm#
#
"

mmmmm mm m m m m mmmm m m mmmmm mmmmmm
# m mm ## # # # # m mm m" "m # # # #
# #" # # # ## #mmmm# #" # # # ## """"mm #mmmmm
# # # #mm# m""m # # # # # # m""m # #
mm#mm # # # # m" "m # # # # # #mm#" m" "m "mmm#" #mmmmm
#


m m mmmmmm mmmm m mm mmmmm mmmmmmm m m mmmm
mmmmm ## ##m m # # "m # m"# # # ## ## m" "m
m" # ## #"m m m" #mmmmm # # # #" # # # # ## # # m #
m" # "" # #m#m# # # # # #mmm#m # # # "" # # #
#mmmm # # # # #mmmmm #mmm" #mmmmm # mm#mm # # # #mm#



mmmmmm m m ""m
# mmmmm # # # #
#mmmmm m" " #"# # "mm
# m" ## ##" #
#mmmmm #mmmm # # #
""
pwn.college{UVdRwo8Qr1PY7qbH033MOInAXHn.QX5EzMwEDL4ITM0EzW}

 

Storage and Retrieval

Binary Analysis

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
union term_pixel_t *framebuffer;
};

After creating the struct, we can modify the main() function's pseudocode to look more like C code.

/challenge/cimg :: main() :: Pseudocode
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rcx
struct cimg *p_cimg; // rdi
bool v6; // of
int v7; // r8d
const char *file_arg; // r12
int file; // eax
const char *error_msg; // rdi
char *desired_output; // r12
unsigned int num_pixels; // r14d
union term_pixel_t *framebuffer; // r13
_BOOL8 won; // rbp
unsigned int i; // ebx
char ascii_char; // al
unsigned __int16 directive_code; // [rsp+Eh] [rbp-105Ah] BYREF
struct cimg cimg; // [rsp+10h] [rbp-1058h] BYREF
unsigned __int64 v21; // [rsp+1028h] [rbp-40h]

v3 = 1030LL;
v21 = __readfsqword(40u);
p_cimg = &cimg;
v6 = __OFSUB__(argc, 1);
v7 = argc - 1;
// `memset(&cimg, 0, sizeof(cimg));`
while ( v3 )
{
*(_DWORD *)p_cimg->header.magic_number = 0;
p_cimg = (struct cimg *)((char *)p_cimg + 4);
--v3;
}
// `if ( argc > 1 )`
if ( !((v7 < 0) ^ v6 | (v7 == 0)) )
{
file_arg = argv[1];
if ( strcmp(&file_arg[strlen(file_arg) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto EXIT;
}
file = open(file_arg, 0);
dup2(file, 0);
}
read_exact(0LL, &cimg, 12LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( *(_DWORD *)cimg.header.magic_number != 'GMIc' )
{
error_msg = "ERROR: Invalid magic number!";
OUTPUT_ERROR_MSG_AND_EXIT:
puts(error_msg);
goto EXIT;
}
error_msg = "ERROR: Unsupported version!";
if ( cimg.header.version != 3 )
goto OUTPUT_ERROR_MSG_AND_EXIT;
initialize_framebuffer(&cimg);
while ( cimg.header.remaining_directives-- )
{
read_exact(0LL, &directive_code, 2LL, "ERROR: Failed to read &directive_code!", 0xFFFFFFFFLL);
if ( directive_code == 3 )
{
handle_3(&cimg);
}
else if ( directive_code > 3u )
{
if ( directive_code != 4 )
{
OUTPUT_DIRECTIVE_CODE_ERROR_MSG_AND_EXIT:
__fprintf_chk(stderr, 1LL, "ERROR: invalid directive_code %ux\n", directive_code);
EXIT:
exit(-1);
}
handle_4(&cimg);
}
else if ( directive_code == 1 )
{
handle_1(&cimg);
}
else
{
if ( directive_code != 2 )
goto OUTPUT_DIRECTIVE_CODE_ERROR_MSG_AND_EXIT;
handle_2(&cimg);
}
}
desired_output = ::desired_output;
display(&cimg, 0LL);
num_pixels = cimg.num_pixels;
framebuffer = cimg.framebuffer;
won = cimg.num_pixels == 1824;
for ( i = 0; num_pixels > i && i != 1824; ++i )
{
ascii_char = framebuffer->data[19];
if ( ascii_char != desired_output[19] )
LOBYTE(won) = 0;
if ( ascii_char != ' ' && ascii_char != '\n' )
{
if ( memcmp(framebuffer, desired_output, 24uLL) )
LOBYTE(won) = 0;
}
++framebuffer;
desired_output += 24;
}
if ( (unsigned __int64)total_data <= 400 && won )
win();
return 0;
}

The main() function basically reads in the user input, sends it to the relevant handler funtions based on the directive code that it observes, and finally checks if the 20th byte in the crafted ANSI sequence is a space or newline character.

It checks if the 20th byte in the crafted ANSI sequence is a space or newline character. If it is not, and the total number of user provided bytes is less than 400 it calls win() which prints the flag.

Let's find the required width:

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")
print(desired_ansi_sequence)
hacker@reverse-engineering~storage-and-retrieval:~$ python ~/script.py
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
.--------------------------------------------------------------------------.| || || || || || || || || ___ __ __ ____ || ___ |_ _| | \/ | / ___| || / __| | | | |\/| | | | _ || | (__ | | | | | | | |_| | || \___| |___| |_| |_| \____| || || || || || || || || || |'--------------------------------------------------------------------------'
In [1]: print(len(".--------------------------------------------------------------------------."))
76

We can now look at the four handlers one by one, starting with handle_1

/challenge/cimg :: handle_1() :: Pseudocode
unsigned __int64 __fastcall handle_1(struct cimg *cimg)
{
int width; // ebp
int height; // edx
size_t data_size; // rbp
pixel_t *data; // rax
pixel_t *data_1; // r12
__int64 v6; // rax
uint8_t ascii_char; // cl
int i; // r13d
int j; // ebp
int width_1; // r15d
pixel_t *pixels; // rax
__int64 j_1; // kr00_8
term_pixel_t emit_tmp; // [rsp+1Fh] [rbp-59h] BYREF
unsigned __int64 v15; // [rsp+38h] [rbp-40h]

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v6 = 0LL;
while ( cimg->header.height * cimg->header.width > (int)v6 )
{
*(_QWORD *)&ascii_char = data_1[v6++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}
for ( i = 0; cimg->header.height > i; ++i )
{
for ( j = 0; ; ++j )
{
width_1 = cimg->header.width;
if ( width_1 <= j )
break;
pixels = &data_1[i * width_1 + j];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
j_1 = j;
cimg->framebuffer[((unsigned int)(j_1 % width_1) + i * width_1) % cimg->num_pixels] = emit_tmp;
}
}
return __readfsqword(40u) ^ v15;
}

The handle_1() the allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_1() :: Pseudocode
# ---- snip ----

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_1() :: Pseudocode
# ---- snip ----

data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v6 = 0LL;
while ( cimg->header.height * cimg->header.width > (int)v6 )
{
*(_QWORD *)&ascii_char = data_1[v6++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

It then uses the 4-bytes in the pixels to fill in the ANSI sequence \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m based on the struct we defined earlier:

/challenge/cimg :: handle_1() :: Pseudocode
# ---- snip ----

for ( i = 0; cimg->header.height > i; ++i )
{
for ( j = 0; ; ++j )
{
width_1 = cimg->header.width;
if ( width_1 <= j )
break;
pixels = &data_1[i * width_1 + j];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
j_1 = j;
cimg->framebuffer[((unsigned int)(j_1 % width_1) + i * width_1) % cimg->num_pixels] = (union term_pixel_t)emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

Let's look at the handle_2() function next.

/challenge/cimg :: handle_2() :: Pseudocode
unsigned __int64 __fastcall handle_2(struct cimg *cimg)
{
unsigned int data_size; // ebx
pixel_t *data; // rax
pixel_t *pixels; // rbp
__int64 v4; // rax
uint8_t ascii_char; // cl
int y_offset; // r13d
int x_offset; // r14d
int x_coord; // eax
int pixel_idx; // ecx
unsigned int ansi_idx; // ebx
unsigned __int8 width; // [rsp+Bh] [rbp-5Dh] BYREF
unsigned __int8 height; // [rsp+Ch] [rbp-5Ch] BYREF
unsigned __int8 base_x; // [rsp+Dh] [rbp-5Bh] BYREF
unsigned __int8 base_y; // [rsp+Eh] [rbp-5Ah] BYREF
term_pixel_t emit_tmp; // [rsp+Fh] [rbp-59h] BYREF
unsigned __int64 v17; // [rsp+28h] [rbp-40h]

v17 = __readfsqword(40u);
read_exact(0LL, &base_x, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &base_y, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);
data_size = 4 * height * width;
data = (pixel_t *)malloc(4LL * height * width);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
pixels = data;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( height * width > (int)v4 )
{
*(_QWORD *)&ascii_char = pixels[v4++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}
for ( y_offset = 0; height > y_offset; ++y_offset )
{
x_offset = 0;
while ( width > x_offset )
{
x_coord = x_offset + base_x;
pixel_idx = x_offset + y_offset * width;
++x_offset;
ansi_idx = x_coord % cimg->header.width + cimg->header.width * (y_offset + base_y);
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[pixel_idx].r,
pixels[pixel_idx].g,
pixels[pixel_idx].b,
pixels[pixel_idx].ascii);
cimg->framebuffer[ansi_idx % cimg->num_pixels] = emit_tmp;
}
}
return __readfsqword(40u) ^ v17;
}

The handle_2(), the program handles our input slightly differently, as it renders the characters at the the coordinates that we provide.

It first reads in the base_x, base_y, width and height values for the chunk of the cIMG to be rendered.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

read_exact(0LL, &base_x, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &base_y, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

# ---- snip ----

Then it allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

data_size = 4 * height * width;
data = (pixel_t *)malloc(4LL * height * width);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

pixels = data;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( height * width > (int)v4 )
{
*(_QWORD *)&ascii_char = pixels[v4++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

Then, the required indexes (pixel_idx and ansi_idx) are calculated based on the base_x, base_y, width and height values that we provided. Based on these indexes, the code knows which characters to use in the chunk of the cIMG.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

for ( y_offset = 0; height > y_offset; ++y_offset )
{
x_offset = 0;
while ( width > x_offset )
{
x_coord = x_offset + base_x;
pixel_idx = x_offset + y_offset * width;
++x_offset;
ansi_idx = x_coord % cimg->header.width + cimg->header.width * (y_offset + base_y);
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[pixel_idx].r,
pixels[pixel_idx].g,
pixels[pixel_idx].b,
pixels[pixel_idx].ascii);
cimg->framebuffer[ansi_idx % cimg->num_pixels] = emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

The handle_2() allows us to choose the exact chunk in the cIMG we want to render and thus save space, as we no longer have to render the empty characters, and can render only the borders and the letters which are in the cIMG.

Next, let's take a look at the handle_3() function.

/challenge/cimg :: handle_3() :: Pseudocode
unsigned __int64 __fastcall handle_3(__int64 a1)
{
__int64 v2; // rax
void *v3; // rdi
int v4; // r12d
unsigned __int8 *v5; // rax
unsigned __int8 *v6; // rbx
__int64 v7; // rax
__int64 v8; // rcx
unsigned __int8 v10; // [rsp+5h] [rbp-23h] BYREF
unsigned __int8 v11; // [rsp+6h] [rbp-22h] BYREF
unsigned __int8 v12; // [rsp+7h] [rbp-21h] BYREF
unsigned __int64 v13; // [rsp+8h] [rbp-20h]

v13 = __readfsqword(0x28u);
read_exact(0LL, &v10, 1LL, "ERROR: Failed to read &sprite_id!", 0xFFFFFFFFLL);
read_exact(0LL, &v11, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &v12, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);
v2 = a1 + 16LL * v10;
*(_BYTE *)(v2 + 25) = v11;
v3 = *(void **)(v2 + 32);
*(_BYTE *)(v2 + 24) = v12;
if ( v3 )
free(v3);
v4 = v12 * v11;
v5 = (unsigned __int8 *)malloc(v4);
v6 = v5;
if ( !v5 )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto LABEL_9;
}
read_exact(0LL, v5, (unsigned int)v4, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v7 = 0LL;
while ( v12 * v11 > (int)v7 )
{
v8 = v6[v7++];
if ( (unsigned __int8)(v8 - 32) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", v8);
LABEL_9:
exit(-1);
}
}
*(_QWORD *)(16LL * v10 + a1 + 32) = v6;
return __readfsqword(0x28u) ^ v13;
}

In order to make this function look better, we have to modify the structs, so that we end up with the following structs:

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

typedef struct sprite_store {
uint8_t height;
uint8_t width;
char __pad[6];
uint8_t *data;
} sprite_store_t;

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
union term_pixel_t *framebuffer;
sprite_store_t sprites[16];
};

After updating the structs, changing some types, renaming a few things and adding some comments, we get the following decompilation of the handle_3() function.

/challenge/cimg :: handle_3() :: Pseudocode
unsigned __int64 __fastcall handle_3(struct cimg *cimg)
{
sprite_store_t *sprites; // rax
sprite_store_t *old_sprite_data; // rdi
int data_size; // r12d
unsigned __int8 *sprite_data; // rax
unsigned __int8 *sprite_data_1; // rbx
int64_t i; // rax
uint8_t ascii_char; // cl
unsigned __int8 sprite_id; // [rsp+5h] [rbp-23h] BYREF
unsigned __int8 width; // [rsp+6h] [rbp-22h] BYREF
unsigned __int8 height; // [rsp+7h] [rbp-21h] BYREF
unsigned __int64 v13; // [rsp+8h] [rbp-20h]

v13 = __readfsqword(40u);

// Read sprite metadata
read_exact(0LL, &sprite_id, 1LL, "ERROR: Failed to read &sprite_id!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

// `sprites = &cimg->sprites[sprite_id];`
sprites = (sprite_store_t *)((char *)cimg + 16 * sprite_id);

// Update sprite dimensions
BYTE1(sprites[1].data) = width; // `cimg->sprites[sprite_id].width = width;`
old_sprite_data = *(sprite_store_t **)&sprites[2].height;// `old_sprite_data = cimg->sprites[sprite_id].data`
LOBYTE(sprites[1].data) = height; // `cimg->sprites[sprite_id].height = height;`

// Free old sprite if it exists
// ```
// if (cimg->sprites[sprite_id].data)
// free(cimg->sprites[sprite_id].data);
// ```
if ( old_sprite_data )
free(old_sprite_data);

// Allocate memory for sprite character data (width * height bytes)
data_size = height * width;
sprite_data = (unsigned __int8 *)malloc(data_size);
sprite_data_1 = sprite_data;
if ( !sprite_data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
read_exact(0LL, sprite_data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);

// Validate all characters are printable ASCII (0x20-0x7E)
i = 0LL;
while ( height * width > (int)i )
{
*(_QWORD *)&ascii_char = sprite_data_1[i++];
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

// Store sprite data pointer
cimg->sprites[sprite_id].data = sprite_data_1;
return __readfsqword(40u) ^ v13;
}

The third handler reads the first 3 bytes of the input data as sprite_id, width, height values.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

read_exact(0LL, &sprite_id, 1LL, "ERROR: Failed to read &sprite_id!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

# ---- snip ----

Then it sets the sprite_store_t.width and sprite_store_t.height values to the ones we provided.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

// `sprites = &cimg->sprites[sprite_id];`
sprites = (sprite_store_t *)((char *)cimg + 16 * sprite_id);

// Update sprite dimensions
BYTE1(sprites[1].data) = width; // `cimg->sprites[sprite_id].width = width;`
old_sprite_data = *(sprite_store_t **)&sprites[2].height;// `old_sprite_data = cimg->sprites[sprite_id].data`
LOBYTE(sprites[1].data) = height; // `cimg->sprites[sprite_id].height = height;`

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

typedef struct sprite_store {
uint8_t height;
uint8_t width;
char __pad[6];
uint8_t *data;
} sprite_store_t;

# ---- snip ----

Next it allocates a location of memory data which can fit width * height number of bytes.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

// Allocate memory for sprite character data (width * height bytes)
data_size = height * width;
sprite_data = (unsigned __int8 *)malloc(data_size);

# ---- snip ----

The user provided input is read into the data memory allocation. Then, it checks if each the character byte, lies between 0x20 and 0x7e.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

sprite_data_1 = sprite_data;
if ( !sprite_data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
read_exact(0LL, sprite_data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);

// Validate all characters are printable ASCII (0x20-0x7E)
i = 0LL;
while ( height * width > (int)i )
{
*(_QWORD *)&ascii_char = sprite_data_1[i++];
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----

Finally, we can figure out what handle_4() is doing.

/challenge/cimg :: handle_4() :: Pseudocode
// positive sp value has been detected, the output may be wrong!
unsigned __int64 __fastcall handle_4(struct cimg *cimg)
{
pixel_t **p_pixels; // rdi
__int64 v3; // rcx
__int64 v4; // rdx
char v5; // r10
char v6; // r11
char v7; // bp
char *sprites; // rdx
int v9; // r12d
int v10; // r8d
int v11; // edi
__int64 v12; // rax
__int64 v13; // r9
int v14; // r14d
int v15; // r15d
int i; // r13d
int v17; // ebp
int width; // ecx
int v19; // r12d
int v20; // eax
__int64 v21; // rdx
_BYTE v23[6]; // [rsp-2Fh] [rbp-4005Fh] BYREF
pixel_t *pixels; // [rsp-29h] [rbp-40059h] BYREF
char v25; // [rsp+0h] [rbp-40030h] BYREF
__int64 v26; // [rsp+1000h] [rbp-3F030h] BYREF
term_pixel_t emit_tmp; // [rsp+3FFD7h] [rbp-59h] BYREF
unsigned __int64 v28; // [rsp+3FFF0h] [rbp-40h]

while ( &v25 != (char *)(&v26 - 0x8000) )
;
v28 = __readfsqword(0x28u);
read_exact(0LL, v23, 6LL, "ERROR: Failed to read &sprite_render_record!", 0xFFFFFFFFLL);
p_pixels = &pixels;
v3 = 0x10000LL;
v4 = v23[0];
v5 = v23[1];
while ( v3 )
{
*(_DWORD *)p_pixels = 0;
p_pixels = (pixel_t **)((char *)p_pixels + 4);
--v3;
}
v6 = v23[2];
v7 = v23[3];
sprites = &cimg->header.magic_number[16 * v4];
v9 = (unsigned __int8)sprites[24];
while ( v9 > (int)v3 )
{
v10 = (unsigned __int8)sprites[25];
v11 = 0;
v12 = (unsigned int)(v3 * v10);
while ( v10 > v11 )
{
v13 = *((_QWORD *)sprites + 4);
*((_BYTE *)&pixels + 4 * v12) = v5;
*((_BYTE *)&pixels + 4 * v12 + 1) = v6;
*((_BYTE *)&pixels + 4 * v12 + 2) = v7;
if ( !v13 )
{
fputs("ERROR: attempted to render uninitialized sprite!\n", stderr);
exit(-1);
}
++v11;
*((_BYTE *)&pixels + 4 * v12 + 3) = *(_BYTE *)(v13 + v12);
++v12;
}
LODWORD(v3) = v3 + 1;
}
v14 = v23[5];
v15 = v23[4];
for ( i = 0; cimg->sprites[v23[0]].height > i; ++i )
{
v17 = 0;
while ( 1 )
{
width = cimg->sprites[v23[0]].width;
if ( width <= v17 )
break;
v19 = cimg->header.width;
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
*((unsigned __int8 *)&pixels + 4 * v17 + 4 * i * width),
*((unsigned __int8 *)&pixels + 4 * v17 + 4 * i * width + 1),
*((unsigned __int8 *)&pixels + 4 * v17 + 4 * i * width + 2),
*((unsigned __int8 *)&pixels + 4 * v17 + 4 * i * width + 3));
v20 = v17 + v15;
++v17;
v21 = (unsigned int)(v20 % v19);
LODWORD(v21) = (unsigned int)(v21 + v14 * v19) % cimg->num_pixels;
cimg->framebuffer[v21] = emit_tmp;
}
++v14;
}
return __readfsqword(0x28u) ^ v28;
}

We need to add another struct for the rendered sprites.

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

typedef struct sprite_store {
uint8_t height;
uint8_t width;
char __pad[6];
uint8_t *data;
} sprite_store_t;

typedef struct sprite_render {
uint8_t sprite_id;
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t x_offset;
uint8_t y_offset;
} sprite_render_t;

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
union term_pixel_t *framebuffer;
sprite_store_t sprites[16];
};
/challenge/cimg :: handle_4() :: Pseudocode
// positive sp value has been detected, the output may be wrong!
unsigned __int64 __fastcall handle_4(struct cimg *cimg)
{
pixel_t *p_pixels; // rdi
__int64 init_count; // rcx
uint8_t sprite_id; // dl
uint8_t r; // r10
uint8_t g; // r11
uint8_t b; // bp
sprite_store_t *sprites; // rdx
int sprite_height; // r12d
int sprite_width; // r8d
int col; // edi
__int64 pixel_idx; // rax
uint8_t *sprite_char; // r9
int y_offset; // r14d
int x_offset; // r15d
int i; // r13d
int x; // ebp
int width; // ecx
int framebuffer_width; // r12d
int framebuffer_x; // eax
unsigned int framebuffer_idx; // edx
sprite_render_t sprite_render_record; // [rsp-2Fh] [rbp-4005Fh] BYREF
pixel_t pixels[65536]; // [rsp-29h] [rbp-40059h] BYREF
term_pixel_t emit_tmp; // [rsp+3FFD7h] [rbp-59h] BYREF
unsigned __int64 v26; // [rsp+3FFF0h] [rbp-40h]

// Stack canary check
while ( &pixels[10].g != &pixels[-64502].g )
;
v26 = __readfsqword(0x28u);

// Read render parameters (6 bytes total)
read_exact(0LL, &sprite_render_record, 6LL, "ERROR: Failed to read &sprite_render_record!", 0xFFFFFFFFLL);

// Initialize temporary pixel buffer to zero
p_pixels = pixels;
init_count = 0x10000LL;
*(_QWORD *)&sprite_id = sprite_render_record.sprite_id;
r = sprite_render_record.r;
while ( init_count )
{
*p_pixels++ = 0;
--init_count;
}
g = sprite_render_record.g;
b = sprite_render_record.b;

// Get sprite pointer (IDA's weird arithmetic - actually &cimg->sprites[sprite_id])
sprites = (sprite_store_t *)((char *)cimg + 16 * *(_QWORD *)&sprite_id);

// PHASE 1: Build temporary RGBC buffer
// Combine sprite character data with RGB color values
sprite_height = LOBYTE(sprites[1].data); // `cimg->sprites[sprite_id].height`
while ( sprite_height > (int)init_count )
{
sprite_width = BYTE1(sprites[1].data); // `cimg->sprites[sprite_id].width`
col = 0;
pixel_idx = (unsigned int)(init_count * sprite_width);
while ( sprite_width > col )
{
sprite_char = *(uint8_t **)&sprites[2].height;// `cimg->sprites[sprite_id].data`

// Set RGB color values
pixels[pixel_idx].r = r;
pixels[pixel_idx].g = g;
pixels[pixel_idx].b = b;

// Verify sprite data exists
if ( !sprite_char )
{
fputs("ERROR: attempted to render uninitialized sprite!\n", stderr);
exit(-1);
}
++col;
// Set character from sprite data
pixels[pixel_idx].ascii = sprite_char[pixel_idx];
++pixel_idx;
}
LODWORD(init_count) = init_count + 1;
}

// PHASE 2: Render the colored sprite to framebuffer
y_offset = sprite_render_record.y_offset;
x_offset = sprite_render_record.x_offset;

// For each row in the sprite
for ( i = 0; cimg->sprites[sprite_render_record.sprite_id].height > i; ++i )
{
x = 0;

// For each column in the sprite
while ( 1 )
{
width = cimg->sprites[sprite_render_record.sprite_id].width;
if ( width <= x )
break;
framebuffer_width = cimg->header.width;

// Create ANSI escape sequence: `\x1b[38;2;R;G;Bm + char + \x1b[0m`
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[x + i * width].r,
pixels[x + i * width].g,
pixels[x + i * width].b,
pixels[x + i * width].ascii);

// Calculate framebuffer position: (x_offset + x, y_offset + i)
framebuffer_x = x + x_offset;
++x;

// Convert 2D coordinates to 1D framebuffer index
*(_QWORD *)&framebuffer_idx = (unsigned int)(framebuffer_x % framebuffer_width);
framebuffer_idx = (framebuffer_idx + y_offset * framebuffer_width) % cimg->num_pixels;

// Write the colored character to framebuffer
cimg->framebuffer[*(_QWORD *)&framebuffer_idx] = emit_tmp;
}
++y_offset;
}
return __readfsqword(0x28u) ^ v26;
}

The function first reads the sprite_render_record from the user input.

/challenge/cimg :: handle_4() :: Pseudocode
# ---- snip ----

// Read render parameters (6 bytes total)
read_exact(0LL, &sprite_render_record, 6LL, "ERROR: Failed to read &sprite_render_record!", 0xFFFFFFFFLL);

# ---- snip ----

It zeroes out the pixel buffer, by basically setting the first 0x10000 bytes to 0.

/challenge/cimg :: handle_4() :: Pseudocode
# ---- snip ----

// Initialize temporary pixel buffer to zero
p_pixels = pixels;
init_count = 0x10000LL;
*(_QWORD *)&sprite_id = sprite_render_record.sprite_id;
r = sprite_render_record.r;
while ( init_count )
{
*p_pixels++ = 0;
--init_count;
}
g = sprite_render_record.g;
b = sprite_render_record.b;

# ---- snip ----

Next, it create fills in the RGB values along with the sprite character in pixel_t.

/challenge/cimg :: handle_4() :: Pseudocode
# ---- snip ----

// PHASE 1: Build temporary RGBC buffer
// Combine sprite character data with RGB color values
sprite_height = LOBYTE(sprites[1].data); // `cimg->sprites[sprite_id].height`
while ( sprite_height > (int)init_count )
{
sprite_width = BYTE1(sprites[1].data); // `cimg->sprites[sprite_id].width`
col = 0;
pixel_idx = (unsigned int)(init_count * sprite_width);
while ( sprite_width > col )
{
sprite_char = *(uint8_t **)&sprites[2].height;// `cimg->sprites[sprite_id].data`

// Set RGB color values
pixels[pixel_idx].r = r;
pixels[pixel_idx].g = g;
pixels[pixel_idx].b = b;

// Verify sprite data exists
if ( !sprite_char )
{
fputs("ERROR: attempted to render uninitialized sprite!\n", stderr);
exit(-1);
}
++col;
// Set character from sprite data
pixels[pixel_idx].ascii = sprite_char[pixel_idx];
++pixel_idx;
}
LODWORD(init_count) = init_count + 1;
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

Finally the same ANSI template filling logic is implemented in the program.

/challenge/cimg :: handle_4() :: Pseudocode
# ---- snip ----

// For each row in the sprite
for ( i = 0; cimg->sprites[sprite_render_record.sprite_id].height > i; ++i )
{
x = 0;

// For each column in the sprite
while ( 1 )
{
width = cimg->sprites[sprite_render_record.sprite_id].width;
if ( width <= x )
break;
framebuffer_width = cimg->header.width;

// Create ANSI escape sequence: `\x1b[38;2;R;G;Bm + char + \x1b[0m`
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[x + i * width].r,
pixels[x + i * width].g,
pixels[x + i * width].b,
pixels[x + i * width].ascii);

// Calculate framebuffer position: (x_offset + x, y_offset + i)
framebuffer_x = x + x_offset;
++x;

// Convert 2D coordinates to 1D framebuffer index
*(_QWORD *)&framebuffer_idx = (unsigned int)(framebuffer_x % framebuffer_width);
framebuffer_idx = (framebuffer_idx + y_offset * framebuffer_width) % cimg->num_pixels;

// Write the colored character to framebuffer
cimg->framebuffer[*(_QWORD *)&framebuffer_idx] = emit_tmp;
}
++y_offset;
}
return __readfsqword(0x28u) ^ v26;

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

# ---- snip ----

This is what the solution file would look like:

cIMG header
└── magic "cIMG" (4 bytes)
└── version (2 bytes)
└── width (1 byte)
└── height (1 byte)
└── n_directives (4 bytes) = 2

Then repeated n_directives times:

Directive #1
└── uint16 code = 3

if code == 3 (store sprite):
└── sprite_id (1)
└── w (1)
└── h (1)
└── sprite_data (w * h bytes)

Directive #2
└── uint16 code = 4

if code == 4 (render sprite):
└── sprite_id (1)
└── r (1)
└── g (1)
└── b (1)
└── x (1)
└── y (1)

Exploit

~/script.py
from pwn import *
import struct
import re

# ============================================================
# STEP 1: Extract the target image from the binary
# ============================================================
binary = ELF('/challenge/cimg')

# Read the desired output from the binary
raw_data = binary.read(binary.sym.desired_output, 45000)
ansi_string = raw_data.split(b'\0')[0].decode()

# Parse ANSI color codes: \x1b[38;2;R;G;Bm + character
pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"
pixels = re.findall(pattern, ansi_string)

# Image dimensions
WIDTH = 76
HEIGHT = 24
assert len(pixels) == WIDTH * HEIGHT

# Helper: Get pixel at (x, y)
def get_pixel(x, y):
r, g, b, char = pixels[y * WIDTH + x]
return int(r), int(g), int(b), char

# ============================================================
# STEP 2: Build directives (sprites + render commands)
# ============================================================
directives = []
next_sprite_id = 0

# Helper: Create a sprite (directive 3)
def create_sprite(sprite_id, width, height, characters):
directive = struct.pack("<H", 3) # Directive code 3
params = struct.pack("<BBB", sprite_id, width, height)
data = characters.encode()
directives.append(directive + params + data)

# Helper: Render a sprite (directive 4)
def render_sprite(sprite_id, r, g, b, x, y):
directive = struct.pack("<H", 4) # Directive code 4
params = struct.pack("<BBBBBB", sprite_id, r, g, b, x, y)
directives.append(directive + params)

# ============================================================
# STEP 3: Create sprites for borders
# ============================================================
WHITE = get_pixel(0, 0)[:3] # White color from corner

# Top and bottom borders (37 dashes each)
create_sprite(next_sprite_id, 37, 1, "-" * 37)
render_sprite(next_sprite_id, *WHITE, 1, 0) # Top left section
render_sprite(next_sprite_id, *WHITE, 38, 0) # Top right section
render_sprite(next_sprite_id, *WHITE, 1, 23) # Bottom left section
render_sprite(next_sprite_id, *WHITE, 38, 23) # Bottom right section
next_sprite_id += 1

# Left and right borders (22 pipes each)
create_sprite(next_sprite_id, 1, 22, "|" * 22)
render_sprite(next_sprite_id, *WHITE, 0, 1) # Left border
render_sprite(next_sprite_id, *WHITE, 75, 1) # Right border
next_sprite_id += 1

# Corners: top-left and top-right (dot)
create_sprite(next_sprite_id, 1, 1, ".")
render_sprite(next_sprite_id, *WHITE, 0, 0) # Top-left corner
render_sprite(next_sprite_id, *WHITE, 75, 0) # Top-right corner
next_sprite_id += 1

# Corners: bottom-left and bottom-right (apostrophe)
create_sprite(next_sprite_id, 1, 1, "'")
render_sprite(next_sprite_id, *WHITE, 0, 23) # Bottom-left corner
render_sprite(next_sprite_id, *WHITE, 75, 23) # Bottom-right corner
next_sprite_id += 1

# ============================================================
# STEP 4: Create sprites for the logo (grouped by color)
# ============================================================
# Logo region
logo_x, logo_y = 22, 9
logo_width, logo_height = 35, 5

# Group all non-space characters by their color
pixels_by_color = {}
for dy in range(logo_height):
for dx in range(logo_width):
r, g, b, char = get_pixel(logo_x + dx, logo_y + dy)
if char != " ": # Skip empty spaces
color = (r, g, b)
if color not in pixels_by_color:
pixels_by_color[color] = []
pixels_by_color[color].append((dx, dy, char))

# Create one sprite per color
for color, points in pixels_by_color.items():
r, g, b = color

# Find bounding box for this color
x_coords = [p[0] for p in points]
y_coords = [p[1] for p in points]
min_x, max_x = min(x_coords), max(x_coords)
min_y, max_y = min(y_coords), max(y_coords)

sprite_width = max_x - min_x + 1
sprite_height = max_y - min_y + 1

# Build character grid (fill with spaces)
grid = [" "] * (sprite_width * sprite_height)
for x, y, char in points:
grid_index = (y - min_y) * sprite_width + (x - min_x)
grid[grid_index] = char

# Create and render sprite
create_sprite(next_sprite_id, sprite_width, sprite_height, "".join(grid))
render_sprite(next_sprite_id, r, g, b, logo_x + min_x, logo_y + min_y)
next_sprite_id += 1

# ============================================================
# STEP 5: Build the final .cimg file
# ============================================================
header = struct.pack(
"<4s H B B I",
b"cIMG", # Magic number
3, # Version
WIDTH, # Width
HEIGHT, # Height
len(directives) # Number of directives
)

cimg_data = header + b"".join(directives)

with open("/home/hacker/solution.cimg", "wb") as f:
f.write(cimg_data)

print(f"Total Bytes: {len(cimg_data)} (Limit: 400)")
print(f"Directives used: {len(directives)}")
hacker@reverse-engineering~storage-and-retrieval:~$ python ~/script.py
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Total Bytes: 349 (Limit: 400)
Directives used: 22
hacker@reverse-engineering~storage-and-retrieval:~$ /challenge/cimg ~/solution.cimg 
.--------------------------------------------------------------------------.
| |
| |
| |
| |
| |
| |
| |
| |
| ___ __ __ ____ |
| ___ |_ _| | \/ | / ___| |
| / __| | | | |\/| | | | _ |
| | (__ | | | | | | | |_| | |
| \___| |___| |_| |_| \____| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
'--------------------------------------------------------------------------'
pwn.college{gP6BoUhTPNZk0ED6EhlqT9CLBMs.QXxEzMwEDL4ITM0EzW}
image

 

Extracting Knowledge

How well do you grasp the cIMG format? This is a chance to show yourself just how much you've learned! This level's /challenge/cimg has no way to give you the flag, but we'll give you a cimg file containing it!

hacker@reverse-engineering~extracting-knowledge:~$ ls /challenge/
DESCRIPTION.md cimg cimg.c generate_flag_image

Let's run the /challenge/generate_flag_image script.

hacker@reverse-engineering~extracting-knowledge:~$ /challenge/generate_flag_image 
hacker@reverse-engineering~extracting-knowledge:~$ ls /challenge/
DESCRIPTION.md cimg cimg.c flag.cimg generate_flag_image
hacker@reverse-engineering~extracting-knowledge:~$ /challenge/cimg /challenge/flag.cimg 

""m
#
"mm
#
#
""

So that script basically writes the flag ASCII in the /challenge/flag.cimg file. For some reason though, passing the flag file to /challenge/cimg only renders the last character of the flag.

Let's check if this is because /challenge/generate_flag_image writes only the last character to the flag file or not.

Binary Analysis

/challenge/generate_flag_image
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import subprocess
import struct
import os

sprites = {}
directives = []

for c in open("/flag", "rb").read().strip():
if c not in sprites:
sprites[c] = len(directives)

sprite = subprocess.check_output(
["/usr/bin/figlet", "-fascii9"],
input=bytes([c])
).split(b"\n")[:-1]

directives.append(
struct.pack(
"<HBBB",
3,
sprites[c],
len(sprite[0]),
len(sprite),
) + b"".join(sprite)
)

directives.append(
struct.pack(
"<HBBBBBB",
4,
sprites[c],
0xFF,
0xFF,
0xFF,
0,
0,
)
)

img = (
b"cIMG"
+ struct.pack("<HBBI", 3, 16, 16, len(directives))
+ b"".join(directives)
)

with open("/challenge/flag.cimg", "wb") as o:
o.write(img)

So this script basically takes in the flag, checks character by character if the character exists in a sprites dictionary, if not, it adds it with its key being the length of directives upto that point.

It then calls figlet -f ascii9 to generate an ASCII art of that character, separates it into different lines, and then appends it all into the following directive:

/challenge/generate_flag_image
# ---- snip ----

directives.append(
struct.pack(
"<HBBB",
3,
sprites[c],
len(sprite[0]),
len(sprite),
) + b"".join(sprite)
)

# ---- snip ----

Looks like the storage handle is to be called in the /challenge/cimg program.

The outside the conditional loop, it appends another directive:

/challenge/generate_flag_image
# ---- snip ----

directives.append(
struct.pack(
"<HBBBBBB",
4,
sprites[c],
0xFF,
0xFF,
0xFF,
0,
0,
)
)

# ---- snip ----

This looks like data which is passed to the render sprite handle. Interestingly, it provides 0 in both the X and Y coordinates. That is why when the sprites are rendered, each character is overwritten by it's trailing character, which is why we only see the last character of the flag in the render.

Finally, it appends these two sets of data with the relevant headers, and stores the flag in the /challenge/flag.cimg file.

/challenge/generate_flag_image
# ---- snip ----

img = (
b"cIMG"
+ struct.pack("<HBBI", 3, 16, 16, len(directives))
+ b"".join(directives)
)

with open("/challenge/flag.cimg", "wb") as o:
o.write(img)

# ---- snip ----

Now let us look at the /challenge/cimg binary.

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

typedef struct sprite_store {
uint8_t height;
uint8_t width;
char __pad[6];
uint8_t *data;
} sprite_store_t;

typedef struct sprite_render {
uint8_t sprite_id;
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t x_offset;
uint8_t y_offset;
} sprite_render_t;

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
union term_pixel_t *framebuffer;
sprite_store_t sprites[16];
};

After creating the struct, we can modify the main() function's pseudocode to look more like C code.

/challenge/cimg :: main() :: Pseudocode
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rcx
struct cimg *p_cimg; // rdi
bool v6; // of
int v7; // r8d
const char *file_arg; // r12
int file; // eax
const char *error_msg; // rdi
char *desired_output; // r12
unsigned int num_pixels; // r14d
union term_pixel_t *framebuffer; // r13
_BOOL8 won; // rbp
unsigned int i; // ebx
char ascii_char; // al
unsigned __int16 directive_code; // [rsp+Eh] [rbp-105Ah] BYREF
struct cimg cimg; // [rsp+10h] [rbp-1058h] BYREF
unsigned __int64 v21; // [rsp+1028h] [rbp-40h]

v3 = 1030LL;
v21 = __readfsqword(40u);
p_cimg = &cimg;
v6 = __OFSUB__(argc, 1);
v7 = argc - 1;
// `memset(&cimg, 0, sizeof(cimg));`
while ( v3 )
{
*(_DWORD *)p_cimg->header.magic_number = 0;
p_cimg = (struct cimg *)((char *)p_cimg + 4);
--v3;
}
// `if ( argc > 1 )`
if ( !((v7 < 0) ^ v6 | (v7 == 0)) )
{
file_arg = argv[1];
if ( strcmp(&file_arg[strlen(file_arg) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto EXIT;
}
file = open(file_arg, 0);
dup2(file, 0);
}
read_exact(0LL, &cimg, 12LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( *(_DWORD *)cimg.header.magic_number != 'GMIc' )
{
error_msg = "ERROR: Invalid magic number!";
OUTPUT_ERROR_MSG_AND_EXIT:
puts(error_msg);
goto EXIT;
}
error_msg = "ERROR: Unsupported version!";
if ( cimg.header.version != 3 )
goto OUTPUT_ERROR_MSG_AND_EXIT;
initialize_framebuffer(&cimg);
while ( cimg.header.remaining_directives-- )
{
read_exact(0LL, &directive_code, 2LL, "ERROR: Failed to read &directive_code!", 0xFFFFFFFFLL);
if ( directive_code == 3 )
{
handle_3(&cimg);
}
else if ( directive_code > 3u )
{
if ( directive_code != 4 )
{
OUTPUT_DIRECTIVE_CODE_ERROR_MSG_AND_EXIT:
__fprintf_chk(stderr, 1LL, "ERROR: invalid directive_code %ux\n", directive_code);
EXIT:
exit(-1);
}
handle_4(&cimg);
}
else if ( directive_code == 1 )
{
handle_1(&cimg);
}
else
{
if ( directive_code != 2 )
goto OUTPUT_DIRECTIVE_CODE_ERROR_MSG_AND_EXIT;
handle_2(&cimg);
}
}
desired_output = ::desired_output;
display(&cimg, 0LL);
num_pixels = cimg.num_pixels;
framebuffer = cimg.framebuffer;
won = cimg.num_pixels == 1824;
for ( i = 0; num_pixels > i && i != 1824; ++i )
{
ascii_char = framebuffer->data[19];
if ( ascii_char != desired_output[19] )
LOBYTE(won) = 0;
if ( ascii_char != ' ' && ascii_char != '\n' )
{
if ( memcmp(framebuffer, desired_output, 24uLL) )
LOBYTE(won) = 0;
}
++framebuffer;
desired_output += 24;
}
if ( (unsigned __int64)total_data <= 400 && won )
win();
return 0;
}

The main() function basically reads in the user input, sends it to the relevant handler funtions based on the directive code that it observes, and finally checks if the 20th byte in the crafted ANSI sequence is a space or newline character.

It checks if the 20th byte in the crafted ANSI sequence is a space or newline character. If it is not, and the total number of user provided bytes is less than 400 it calls win() which prints the flag.

Let's find the required width:

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")
print(desired_ansi_sequence)
hacker@reverse-engineering~storage-and-retrieval:~$ python ~/script.py
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
.--------------------------------------------------------------------------.| || || || || || || || || ___ __ __ ____ || ___ |_ _| | \/ | / ___| || / __| | | | |\/| | | | _ || | (__ | | | | | | | |_| | || \___| |___| |_| |_| \____| || || || || || || || || || |'--------------------------------------------------------------------------'
In [1]: print(len(".--------------------------------------------------------------------------."))
76

We can now look at the four handlers one by one, starting with handle_1

/challenge/cimg :: handle_1() :: Pseudocode
unsigned __int64 __fastcall handle_1(struct cimg *cimg)
{
int width; // ebp
int height; // edx
size_t data_size; // rbp
pixel_t *data; // rax
pixel_t *data_1; // r12
__int64 v6; // rax
uint8_t ascii_char; // cl
int i; // r13d
int j; // ebp
int width_1; // r15d
pixel_t *pixels; // rax
__int64 j_1; // kr00_8
term_pixel_t emit_tmp; // [rsp+1Fh] [rbp-59h] BYREF
unsigned __int64 v15; // [rsp+38h] [rbp-40h]

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v6 = 0LL;
while ( cimg->header.height * cimg->header.width > (int)v6 )
{
*(_QWORD *)&ascii_char = data_1[v6++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}
for ( i = 0; cimg->header.height > i; ++i )
{
for ( j = 0; ; ++j )
{
width_1 = cimg->header.width;
if ( width_1 <= j )
break;
pixels = &data_1[i * width_1 + j];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
j_1 = j;
cimg->framebuffer[((unsigned int)(j_1 % width_1) + i * width_1) % cimg->num_pixels] = emit_tmp;
}
}
return __readfsqword(40u) ^ v15;
}

The handle_1() the allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_1() :: Pseudocode
# ---- snip ----

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_1() :: Pseudocode
# ---- snip ----

data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v6 = 0LL;
while ( cimg->header.height * cimg->header.width > (int)v6 )
{
*(_QWORD *)&ascii_char = data_1[v6++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

It then uses the 4-bytes in the pixels to fill in the ANSI sequence \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m based on the struct we defined earlier:

/challenge/cimg :: handle_1() :: Pseudocode
# ---- snip ----

for ( i = 0; cimg->header.height > i; ++i )
{
for ( j = 0; ; ++j )
{
width_1 = cimg->header.width;
if ( width_1 <= j )
break;
pixels = &data_1[i * width_1 + j];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
j_1 = j;
cimg->framebuffer[((unsigned int)(j_1 % width_1) + i * width_1) % cimg->num_pixels] = (union term_pixel_t)emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

Let's look at the handle_2() function next.

/challenge/cimg :: handle_2() :: Pseudocode
unsigned __int64 __fastcall handle_2(struct cimg *cimg)
{
unsigned int data_size; // ebx
pixel_t *data; // rax
pixel_t *pixels; // rbp
__int64 v4; // rax
uint8_t ascii_char; // cl
int y_offset; // r13d
int x_offset; // r14d
int x_coord; // eax
int pixel_idx; // ecx
unsigned int ansi_idx; // ebx
unsigned __int8 width; // [rsp+Bh] [rbp-5Dh] BYREF
unsigned __int8 height; // [rsp+Ch] [rbp-5Ch] BYREF
unsigned __int8 base_x; // [rsp+Dh] [rbp-5Bh] BYREF
unsigned __int8 base_y; // [rsp+Eh] [rbp-5Ah] BYREF
term_pixel_t emit_tmp; // [rsp+Fh] [rbp-59h] BYREF
unsigned __int64 v17; // [rsp+28h] [rbp-40h]

v17 = __readfsqword(40u);
read_exact(0LL, &base_x, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &base_y, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);
data_size = 4 * height * width;
data = (pixel_t *)malloc(4LL * height * width);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
pixels = data;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( height * width > (int)v4 )
{
*(_QWORD *)&ascii_char = pixels[v4++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}
for ( y_offset = 0; height > y_offset; ++y_offset )
{
x_offset = 0;
while ( width > x_offset )
{
x_coord = x_offset + base_x;
pixel_idx = x_offset + y_offset * width;
++x_offset;
ansi_idx = x_coord % cimg->header.width + cimg->header.width * (y_offset + base_y);
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[pixel_idx].r,
pixels[pixel_idx].g,
pixels[pixel_idx].b,
pixels[pixel_idx].ascii);
cimg->framebuffer[ansi_idx % cimg->num_pixels] = emit_tmp;
}
}
return __readfsqword(40u) ^ v17;
}

The handle_2(), the program handles our input slightly differently, as it renders the characters at the the coordinates that we provide.

It first reads in the base_x, base_y, width and height values for the chunk of the cIMG to be rendered.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

read_exact(0LL, &base_x, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &base_y, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

# ---- snip ----

Then it allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

data_size = 4 * height * width;
data = (pixel_t *)malloc(4LL * height * width);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

pixels = data;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( height * width > (int)v4 )
{
*(_QWORD *)&ascii_char = pixels[v4++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

Then, the required indexes (pixel_idx and ansi_idx) are calculated based on the base_x, base_y, width and height values that we provided. Based on these indexes, the code knows which characters to use in the chunk of the cIMG.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

for ( y_offset = 0; height > y_offset; ++y_offset )
{
x_offset = 0;
while ( width > x_offset )
{
x_coord = x_offset + base_x;
pixel_idx = x_offset + y_offset * width;
++x_offset;
ansi_idx = x_coord % cimg->header.width + cimg->header.width * (y_offset + base_y);
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[pixel_idx].r,
pixels[pixel_idx].g,
pixels[pixel_idx].b,
pixels[pixel_idx].ascii);
cimg->framebuffer[ansi_idx % cimg->num_pixels] = emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

The handle_2() allows us to choose the exact chunk in the cIMG we want to render and thus save space, as we no longer have to render the empty characters, and can render only the borders and the letters which are in the cIMG.

Next, let's take a look at the handle_3() function.

/challenge/cimg :: handle_3() :: Pseudocode
unsigned __int64 __fastcall handle_3(struct cimg *cimg)
{
sprite_store_t *sprites; // rax
sprite_store_t *old_sprite_data; // rdi
int data_size; // r12d
unsigned __int8 *sprite_data; // rax
unsigned __int8 *sprite_data_1; // rbx
int64_t i; // rax
uint8_t ascii_char; // cl
unsigned __int8 sprite_id; // [rsp+5h] [rbp-23h] BYREF
unsigned __int8 width; // [rsp+6h] [rbp-22h] BYREF
unsigned __int8 height; // [rsp+7h] [rbp-21h] BYREF
unsigned __int64 v13; // [rsp+8h] [rbp-20h]

v13 = __readfsqword(40u);

// Read sprite metadata
read_exact(0LL, &sprite_id, 1LL, "ERROR: Failed to read &sprite_id!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

// `sprites = &cimg->sprites[sprite_id];`
sprites = (sprite_store_t *)((char *)cimg + 16 * sprite_id);

// Update sprite dimensions
BYTE1(sprites[1].data) = width; // `cimg->sprites[sprite_id].width = width;`
old_sprite_data = *(sprite_store_t **)&sprites[2].height;// `old_sprite_data = cimg->sprites[sprite_id].data`
LOBYTE(sprites[1].data) = height; // `cimg->sprites[sprite_id].height = height;`

// Free old sprite if it exists
// ```
// if (cimg->sprites[sprite_id].data)
// free(cimg->sprites[sprite_id].data);
// ```
if ( old_sprite_data )
free(old_sprite_data);

// Allocate memory for sprite character data (width * height bytes)
data_size = height * width;
sprite_data = (unsigned __int8 *)malloc(data_size);
sprite_data_1 = sprite_data;
if ( !sprite_data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
read_exact(0LL, sprite_data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);

// Validate all characters are printable ASCII (0x20-0x7E)
i = 0LL;
while ( height * width > (int)i )
{
*(_QWORD *)&ascii_char = sprite_data_1[i++];
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

// Store sprite data pointer
cimg->sprites[sprite_id].data = sprite_data_1;
return __readfsqword(40u) ^ v13;
}

The third handler reads the first 3 bytes of the input data as sprite_id, width, height values.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

read_exact(0LL, &sprite_id, 1LL, "ERROR: Failed to read &sprite_id!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

# ---- snip ----

Then it sets the sprite_store_t.width and sprite_store_t.height values to the ones we provided.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

// `sprites = &cimg->sprites[sprite_id];`
sprites = (sprite_store_t *)((char *)cimg + 16 * sprite_id);

// Update sprite dimensions
BYTE1(sprites[1].data) = width; // `cimg->sprites[sprite_id].width = width;`
old_sprite_data = *(sprite_store_t **)&sprites[2].height;// `old_sprite_data = cimg->sprites[sprite_id].data`
LOBYTE(sprites[1].data) = height; // `cimg->sprites[sprite_id].height = height;`

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

typedef struct sprite_store {
uint8_t height;
uint8_t width;
char __pad[6];
uint8_t *data;
} sprite_store_t;

# ---- snip ----

Next it allocates a location of memory data which can fit width * height number of bytes.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

// Allocate memory for sprite character data (width * height bytes)
data_size = height * width;
sprite_data = (unsigned __int8 *)malloc(data_size);

# ---- snip ----

The user provided input is read into the data memory allocation. Then, it checks if each the character byte, lies between 0x20 and 0x7e.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

sprite_data_1 = sprite_data;
if ( !sprite_data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
read_exact(0LL, sprite_data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);

// Validate all characters are printable ASCII (0x20-0x7E)
i = 0LL;
while ( height * width > (int)i )
{
*(_QWORD *)&ascii_char = sprite_data_1[i++];
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----

Finally, we can figure out what handle_4() is doing.

/challenge/cimg :: handle_4() :: Pseudocode
// positive sp value has been detected, the output may be wrong!
unsigned __int64 __fastcall handle_4(struct cimg *cimg)
{
pixel_t *p_pixels; // rdi
__int64 init_count; // rcx
uint8_t sprite_id; // dl
uint8_t r; // r10
uint8_t g; // r11
uint8_t b; // bp
sprite_store_t *sprites; // rdx
int sprite_height; // r12d
int sprite_width; // r8d
int col; // edi
__int64 pixel_idx; // rax
uint8_t *sprite_char; // r9
int y_offset; // r14d
int x_offset; // r15d
int i; // r13d
int x; // ebp
int width; // ecx
int framebuffer_width; // r12d
int framebuffer_x; // eax
unsigned int framebuffer_idx; // edx
sprite_render_t sprite_render_record; // [rsp-2Fh] [rbp-4005Fh] BYREF
pixel_t pixels[65536]; // [rsp-29h] [rbp-40059h] BYREF
term_pixel_t emit_tmp; // [rsp+3FFD7h] [rbp-59h] BYREF
unsigned __int64 v26; // [rsp+3FFF0h] [rbp-40h]

// Stack canary check
while ( &pixels[10].g != &pixels[-64502].g )
;
v26 = __readfsqword(0x28u);

// Read render parameters (6 bytes total)
read_exact(0LL, &sprite_render_record, 6LL, "ERROR: Failed to read &sprite_render_record!", 0xFFFFFFFFLL);

// Initialize temporary pixel buffer to zero
p_pixels = pixels;
init_count = 0x10000LL;
*(_QWORD *)&sprite_id = sprite_render_record.sprite_id;
r = sprite_render_record.r;
while ( init_count )
{
*p_pixels++ = 0;
--init_count;
}
g = sprite_render_record.g;
b = sprite_render_record.b;

// Get sprite pointer (IDA's weird arithmetic - actually &cimg->sprites[sprite_id])
sprites = (sprite_store_t *)((char *)cimg + 16 * *(_QWORD *)&sprite_id);

// PHASE 1: Build temporary RGBC buffer
// Combine sprite character data with RGB color values
sprite_height = LOBYTE(sprites[1].data); // `cimg->sprites[sprite_id].height`
while ( sprite_height > (int)init_count )
{
sprite_width = BYTE1(sprites[1].data); // `cimg->sprites[sprite_id].width`
col = 0;
pixel_idx = (unsigned int)(init_count * sprite_width);
while ( sprite_width > col )
{
sprite_char = *(uint8_t **)&sprites[2].height;// `cimg->sprites[sprite_id].data`

// Set RGB color values
pixels[pixel_idx].r = r;
pixels[pixel_idx].g = g;
pixels[pixel_idx].b = b;

// Verify sprite data exists
if ( !sprite_char )
{
fputs("ERROR: attempted to render uninitialized sprite!\n", stderr);
exit(-1);
}
++col;
// Set character from sprite data
pixels[pixel_idx].ascii = sprite_char[pixel_idx];
++pixel_idx;
}
LODWORD(init_count) = init_count + 1;
}

// PHASE 2: Render the colored sprite to framebuffer
y_offset = sprite_render_record.y_offset;
x_offset = sprite_render_record.x_offset;

// For each row in the sprite
for ( i = 0; cimg->sprites[sprite_render_record.sprite_id].height > i; ++i )
{
x = 0;

// For each column in the sprite
while ( 1 )
{
width = cimg->sprites[sprite_render_record.sprite_id].width;
if ( width <= x )
break;
framebuffer_width = cimg->header.width;

// Create ANSI escape sequence: `\x1b[38;2;R;G;Bm + char + \x1b[0m`
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[x + i * width].r,
pixels[x + i * width].g,
pixels[x + i * width].b,
pixels[x + i * width].ascii);

// Calculate framebuffer position: (x_offset + x, y_offset + i)
framebuffer_x = x + x_offset;
++x;

// Convert 2D coordinates to 1D framebuffer index
*(_QWORD *)&framebuffer_idx = (unsigned int)(framebuffer_x % framebuffer_width);
framebuffer_idx = (framebuffer_idx + y_offset * framebuffer_width) % cimg->num_pixels;

// Write the colored character to framebuffer
cimg->framebuffer[*(_QWORD *)&framebuffer_idx] = emit_tmp;
}
++y_offset;
}
return __readfsqword(0x28u) ^ v26;
}

The function first reads the sprite_render_record from the user input.

/challenge/cimg :: handle_4() :: Pseudocode
# ---- snip ----

// Read render parameters (6 bytes total)
read_exact(0LL, &sprite_render_record, 6LL, "ERROR: Failed to read &sprite_render_record!", 0xFFFFFFFFLL);

# ---- snip ----

It zeroes out the pixel buffer, by basically setting the first 0x10000 bytes to 0.

/challenge/cimg :: handle_4() :: Pseudocode
# ---- snip ----

// Initialize temporary pixel buffer to zero
p_pixels = pixels;
init_count = 0x10000LL;
*(_QWORD *)&sprite_id = sprite_render_record.sprite_id;
r = sprite_render_record.r;
while ( init_count )
{
*p_pixels++ = 0;
--init_count;
}
g = sprite_render_record.g;
b = sprite_render_record.b;

# ---- snip ----

Next, it create fills in the RGB values along with the sprite character in pixel_t.

/challenge/cimg :: handle_4() :: Pseudocode
# ---- snip ----

// PHASE 1: Build temporary RGBC buffer
// Combine sprite character data with RGB color values
sprite_height = LOBYTE(sprites[1].data); // `cimg->sprites[sprite_id].height`
while ( sprite_height > (int)init_count )
{
sprite_width = BYTE1(sprites[1].data); // `cimg->sprites[sprite_id].width`
col = 0;
pixel_idx = (unsigned int)(init_count * sprite_width);
while ( sprite_width > col )
{
sprite_char = *(uint8_t **)&sprites[2].height;// `cimg->sprites[sprite_id].data`

// Set RGB color values
pixels[pixel_idx].r = r;
pixels[pixel_idx].g = g;
pixels[pixel_idx].b = b;

// Verify sprite data exists
if ( !sprite_char )
{
fputs("ERROR: attempted to render uninitialized sprite!\n", stderr);
exit(-1);
}
++col;
// Set character from sprite data
pixels[pixel_idx].ascii = sprite_char[pixel_idx];
++pixel_idx;
}
LODWORD(init_count) = init_count + 1;
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

Finally the same ANSI template filling logic is implemented in the program.

/challenge/cimg :: handle_4() :: Pseudocode
# ---- snip ----

// For each row in the sprite
for ( i = 0; cimg->sprites[sprite_render_record.sprite_id].height > i; ++i )
{
x = 0;

// For each column in the sprite
while ( 1 )
{
width = cimg->sprites[sprite_render_record.sprite_id].width;
if ( width <= x )
break;
framebuffer_width = cimg->header.width;

// Create ANSI escape sequence: `\x1b[38;2;R;G;Bm + char + \x1b[0m`
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[x + i * width].r,
pixels[x + i * width].g,
pixels[x + i * width].b,
pixels[x + i * width].ascii);

// Calculate framebuffer position: (x_offset + x, y_offset + i)
framebuffer_x = x + x_offset;
++x;

// Convert 2D coordinates to 1D framebuffer index
*(_QWORD *)&framebuffer_idx = (unsigned int)(framebuffer_x % framebuffer_width);
framebuffer_idx = (framebuffer_idx + y_offset * framebuffer_width) % cimg->num_pixels;

// Write the colored character to framebuffer
cimg->framebuffer[*(_QWORD *)&framebuffer_idx] = emit_tmp;
}
++y_offset;
}
return __readfsqword(0x28u) ^ v26;

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

# ---- snip ----

All in all, the handles in this level are teh exact same as the ones in Storage and Retrieval.

Exploit

In order to get the flag, we have to ignore the metadata and headers, and only read in the bytes of the flag figlet. Once we have those, we can reverse the operation to get the flag.

~/script.py
import struct
import subprocess

CIMG_PATH = "/challenge/flag.cimg"

# ------------------------------------------------------------
# Precompute figlet ascii9 for all printable ASCII
# ------------------------------------------------------------
figlet_map = {}

for c in range(32, 127):
art = subprocess.check_output(
["/usr/bin/figlet", "-fascii9"],
input=bytes([c])
).rstrip(b"\n")
figlet_map[art] = chr(c)

# ------------------------------------------------------------
# Read cimg file
# ------------------------------------------------------------
with open(CIMG_PATH, "rb") as f:
data = f.read()

off = 0

# Header
assert data[off:off+4] == b"cIMG"
off += 4
version, width, height, n_directives = struct.unpack_from("<HBBI", data, off)
off += 8
assert version == 3

sprites = {} # sprite_id -> ascii art
render_order = [] # sprite_ids in order

# ------------------------------------------------------------
# Parse directives
# ------------------------------------------------------------
while n_directives > 0:
code = struct.unpack_from("<H", data, off)[0]
off += 2

if code == 3: # handle_3
sprite_id, w, h = struct.unpack_from("<BBB", data, off)
off += 3

size = w * h
sprite_bytes = data[off:off + size]
off += size

rows = [
sprite_bytes[i*w:(i+1)*w]
for i in range(h)
]
art = b"\n".join(rows)
sprites[sprite_id] = art

elif code == 4: # handle_4
sprite_id = data[off]
off += 1
off += 5 # r, g, b, x, y
render_order.append(sprite_id)

else:
raise ValueError(f"Unknown directive code: {code}")

n_directives -= 1

# ------------------------------------------------------------
# Decode sprites → characters
# ------------------------------------------------------------
sprite_to_char = {}
for sid, art in sprites.items():
if art not in figlet_map:
raise ValueError(f"Unknown figlet art for sprite {sid}")
sprite_to_char[sid] = figlet_map[art]

# ------------------------------------------------------------
# Recover flag
# ------------------------------------------------------------
flag = "".join(sprite_to_char[sid] for sid in render_order)
print(flag)
hacker@reverse-engineering~extracting-knowledge:~$ python ~/script.py
pwn.college{s59MKbl6TiR1gXgYJHsskPU-q9b.QXyEzMwEDL4ITM0EzW}

 

Advanced Sprites

Binary Analysis

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

typedef struct sprite_store {
uint8_t height;
uint8_t width;
char __pad[6];
uint8_t *data;
} sprite_store_t;

typedef struct sprite_render {
uint8_t sprite_id;
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t x_offset;
uint8_t y_offset;
} sprite_render_t;

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
union term_pixel_t *framebuffer;
sprite_store_t sprites[16];
};
/challenge/cimg :: main() :: Pseudocode
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rcx
struct cimg *p_cimg; // rdi
bool v6; // of
int v7; // r8d
const char *v8; // r12
int v9; // eax
const char *error_msg; // rdi
char *desired_output; // r12
unsigned int num_pixels; // r14d
union term_pixel_t *framebuffer; // r13
_BOOL8 won; // rbp
unsigned int i; // ebx
char ascii_char; // al
unsigned __int16 directive_count; // [rsp+Eh] [rbp-105Ah] BYREF
struct cimg cimg; // [rsp+10h] [rbp-1058h] BYREF
unsigned __int64 v21; // [rsp+1028h] [rbp-40h]

v3 = 1030LL;
v21 = __readfsqword(0x28u);
p_cimg = &cimg;
v6 = __OFSUB__(argc, 1);
v7 = argc - 1;

// `memset(&cimg, 0, sizeof(cimg));`
while ( v3 )
{
*(_DWORD *)p_cimg->header.magic_number = 0;
p_cimg = (struct cimg *)((char *)p_cimg + 4);
--v3;
}

// `if ( argc > 1 )`
if ( !((v7 < 0) ^ v6 | (v7 == 0)) )
{
v8 = argv[1];
if ( strcmp(&v8[strlen(v8) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto EXIT;
}
v9 = open(v8, 0);
dup2(v9, 0);
}
read_exact(0LL, &cimg, 12LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( *(_DWORD *)cimg.header.magic_number != 'GMIc' )
{
error_msg = "ERROR: Invalid magic number!";
OUTPUT_ERROR_MSG_AND_EXIT:
puts(error_msg);
goto EXIT;
}
error_msg = "ERROR: Unsupported version!";
if ( cimg.header.version != 4 )
goto OUTPUT_ERROR_MSG_AND_EXIT;
initialize_framebuffer(&cimg);
while ( cimg.header.remaining_directives-- )
{
read_exact(0LL, &directive_count, 2LL, "ERROR: Failed to read &directive_code!", 0xFFFFFFFFLL);
if ( directive_count == 3 )
{
handle_3(&cimg);
}
else if ( directive_count > 3u )
{
if ( directive_count != 4 )
{
EXIT_1:
__fprintf_chk(stderr, 1LL, "ERROR: invalid directive_code %ux\n", directive_count);
EXIT:
exit(-1);
}
handle_4(&cimg);
}
else if ( directive_count == 1 )
{
handle_1(&cimg);
}
else
{
if ( directive_count != 2 )
goto EXIT_1;
handle_2(&cimg);
}
}
desired_output = ::desired_output;
display(&cimg, 0LL);
num_pixels = cimg.num_pixels;
framebuffer = cimg.framebuffer;
won = cimg.num_pixels == 1824;
for ( i = 0; num_pixels > i && i != 1824; ++i )
{
ascii_char = framebuffer->data[19];
if ( ascii_char != desired_output[19] )
LOBYTE(won) = 0;
if ( ascii_char != ' ' && ascii_char != '\n' )
{
if ( memcmp(framebuffer, desired_output, 0x18uLL) )
LOBYTE(won) = 0;
}
++framebuffer;
desired_output += 24;
}
if ( (unsigned __int64)total_data <= 285 && won )
win();
return 0;
}

The main() function basically reads in the user input, sends it to the relevant handler funtions based on the directive code that it observes, and finally checks if the 20th byte in the crafted ANSI sequence is a space or newline character.

It checks if the 20th byte in the crafted ANSI sequence is a space or newline character. If it is not, and the total number of user provided bytes is less than 285 it calls win() which prints the flag.

Let's find the required width:

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")
print(desired_ansi_sequence)
hacker@reverse-engineering~advanced-sprites:~/cse240/25-proj-mud/01$ python ~/script.py
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
.--------------------------------------------------------------------------.| || || || || || || || || ___ __ __ ____ || ___ |_ _| | \/ | / ___| || / __| | | | |\/| | | | _ || | (__ | | | | | | | |_| | || \___| |___| |_| |_| \____| || || || || || || || || || |'--------------------------------------------------------------------------'
In [1]: print(len(".--------------------------------------------------------------------------."))
76

Lets quickly go through all the handles as well.

/challenge/cimg :: handle_1() :: Pseudocode
unsigned __int64 __fastcall handle_1(struct cimg *cimg)
{
int width; // ebp
int height; // edx
size_t data_size; // rbp
pixel_t *data; // rax
pixel_t *data_1; // r12
__int64 v6; // rax
uint8_t ascii_char; // cl
int i; // r13d
int j; // ebp
int width_1; // r15d
pixel_t *pixels; // rax
__int64 j_1; // kr00_8
term_pixel_t emit_tmp; // [rsp+1Fh] [rbp-59h] BYREF
unsigned __int64 v15; // [rsp+38h] [rbp-40h]

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(0x28u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v6 = 0LL;
while ( cimg->header.height * cimg->header.width > (int)v6 )
{
*(_QWORD *)&ascii_char = data_1[v6++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}
for ( i = 0; cimg->header.height > i; ++i )
{
for ( j = 0; ; ++j )
{
width_1 = cimg->header.width;
if ( width_1 <= j )
break;
pixels = &data_1[i * width_1 + j];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
j_1 = j;
cimg->framebuffer[((unsigned int)(j_1 % width_1) + i * width_1) % cimg->num_pixels] = emit_tmp;
}
}
return __readfsqword(40u) ^ v15;
}

Let's find the required width:

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")
print(desired_ansi_sequence)
hacker@reverse-engineering~storage-and-retrieval:~$ python ~/script.py
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
.--------------------------------------------------------------------------.| || || || || || || || || ___ __ __ ____ || ___ |_ _| | \/ | / ___| || / __| | | | |\/| | | | _ || | (__ | | | | | | | |_| | || \___| |___| |_| |_| \____| || || || || || || || || || |'--------------------------------------------------------------------------'
In [1]: print(len(".--------------------------------------------------------------------------."))
76

The handle_1() the allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_1() :: Pseudocode
# ---- snip ----

width = cimg->header.width;
height = cimg->header.height;
v15 = __readfsqword(40u);
data_size = 4LL * height * width;
data = (pixel_t *)malloc(data_size);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_1() :: Pseudocode
# ---- snip ----

data_1 = data;
read_exact(0LL, data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v6 = 0LL;
while ( cimg->header.height * cimg->header.width > (int)v6 )
{
*(_QWORD *)&ascii_char = data_1[v6++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

It then uses the 4-bytes in the pixels to fill in the ANSI sequence \x1b[38;2;%03d;%03d;%03dm%c\x1b[0m based on the struct we defined earlier:

/challenge/cimg :: handle_1() :: Pseudocode
# ---- snip ----

for ( i = 0; cimg->header.height > i; ++i )
{
for ( j = 0; ; ++j )
{
width_1 = cimg->header.width;
if ( width_1 <= j )
break;
pixels = &data_1[i * width_1 + j];
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels->r,
pixels->g,
pixels->b,
pixels->ascii);
j_1 = j;
cimg->framebuffer[((unsigned int)(j_1 % width_1) + i * width_1) % cimg->num_pixels] = (union term_pixel_t)emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

Let's look at the handle_2() function next.

/challenge/cimg :: handle_2() :: Pseudocode
unsigned __int64 __fastcall handle_2(struct cimg *cimg)
{
unsigned int data_size; // ebx
pixel_t *data; // rax
pixel_t *pixels; // rbp
__int64 v4; // rax
uint8_t ascii_char; // cl
int y_offset; // r13d
int x_offset; // r14d
int x_coord; // eax
int pixel_idx; // ecx
unsigned int ansi_idx; // ebx
unsigned __int8 width; // [rsp+Bh] [rbp-5Dh] BYREF
unsigned __int8 height; // [rsp+Ch] [rbp-5Ch] BYREF
unsigned __int8 base_x; // [rsp+Dh] [rbp-5Bh] BYREF
unsigned __int8 base_y; // [rsp+Eh] [rbp-5Ah] BYREF
term_pixel_t emit_tmp; // [rsp+Fh] [rbp-59h] BYREF
unsigned __int64 v17; // [rsp+28h] [rbp-40h]

v17 = __readfsqword(0x28u);
read_exact(0LL, &base_x, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &base_y, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);
data_size = 4 * height * width;
data = (pixel_t *)malloc(4LL * height * width);
if ( !data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
pixels = data;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( height * width > (int)v4 )
{
*(_QWORD *)&ascii_char = pixels[v4++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}
for ( y_offset = 0; height > y_offset; ++y_offset )
{
x_offset = 0;
while ( width > x_offset )
{
x_coord = x_offset + base_x;
pixel_idx = x_offset + y_offset * width;
++x_offset;
ansi_idx = x_coord % cimg->header.width + cimg->header.width * (y_offset + base_y);
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[pixel_idx].r,
pixels[pixel_idx].g,
pixels[pixel_idx].b,
pixels[pixel_idx].ascii);
cimg->framebuffer[ansi_idx % cimg->num_pixels] = emit_tmp;
}
}
return __readfsqword(40u) ^ v17;
}

The handle_2(), the program handles our input slightly differently, as it renders the characters at the the coordinates that we provide.

It first reads in the base_x, base_y, width and height values for the chunk of the cIMG to be rendered.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

read_exact(0LL, &base_x, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &base_y, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

# ---- snip ----

Then it allocates a location of memory data which can fit 4 * width * height number of bytes.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

data_size = 4 * height * width;
data = (pixel_t *)malloc(4LL * height * width);

# ---- snip ----

The user's input is read into the data memory allocation. Then, it checks if each fourth byte in the pixel, i.e. the character byte (r,g,b,c), lies between 0x20 and 0x7e.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

pixels = data;
read_exact(0LL, data, data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( height * width > (int)v4 )
{
*(_QWORD *)&ascii_char = pixels[v4++].ascii;
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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

# ---- snip ----

Then, the required indexes (pixel_idx and ansi_idx) are calculated based on the base_x, base_y, width and height values that we provided. Based on these indexes, the code knows which characters to use in the chunk of the cIMG.

/challenge/cimg :: handle_2() :: Pseudocode
# ---- snip ----

for ( y_offset = 0; height > y_offset; ++y_offset )
{
x_offset = 0;
while ( width > x_offset )
{
x_coord = x_offset + base_x;
pixel_idx = x_offset + y_offset * width;
++x_offset;
ansi_idx = x_coord % cimg->header.width + cimg->header.width * (y_offset + base_y);
__snprintf_chk(
&emit_tmp,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
pixels[pixel_idx].r,
pixels[pixel_idx].g,
pixels[pixel_idx].b,
pixels[pixel_idx].ascii);
cimg->framebuffer[ansi_idx % cimg->num_pixels] = emit_tmp;
}
}

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

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
};

# ---- snip ----

The handle_2() allows us to choose the exact chunk in the cIMG we want to render and thus save space, as we no longer have to render the empty characters, and can render only the borders and the letters which are in the cIMG.

Next, let's take a look at the handle_3() function.

/challenge/cimg :: handle_3() :: Pseudocode
unsigned __int64 __fastcall handle_3(struct cimg *cimg)
{
sprite_store_t *sprites; // rax
sprite_store_t *old_sprite_data; // rdi
int data_size; // r12d
uint8_t *sprite_data; // rax MAPDST
__int64 i; // rax
uint8_t ascii_char; // cl
unsigned __int8 sprite_id; // [rsp+5h] [rbp-23h] BYREF
unsigned __int8 width; // [rsp+6h] [rbp-22h] BYREF
unsigned __int8 height; // [rsp+7h] [rbp-21h] BYREF
unsigned __int64 v13; // [rsp+8h] [rbp-20h]

v13 = __readfsqword(0x28u);

// Read sprite metadata
read_exact(0LL, &sprite_id, 1LL, "ERROR: Failed to read &sprite_id!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

// `sprites = &cimg->sprites[sprite_id];`
sprites = (sprite_store_t *)((char *)cimg + 16 * sprite_id);

// Update sprite dimensions
BYTE1(sprites[1].data) = width; // `cimg->sprites[sprite_id].width = width;`
old_sprite_data = *(sprite_store_t **)&sprites[2].height;// `old_sprite_data = cimg->sprites[sprite_id].data`
LOBYTE(sprites[1].data) = height; // `cimg->sprites[sprite_id].height = height;`

// Free old sprite if it exists
// ```
// if (cimg->sprites[sprite_id].data)
// free(cimg->sprites[sprite_id].data);
// ```
if ( old_sprite_data )
free(old_sprite_data);

// Allocate memory for sprite character data (width * height bytes)
data_size = height * width;
sprite_data = (uint8_t *)malloc(data_size);
if ( !sprite_data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
read_exact(0LL, sprite_data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);

// Validate all characters are printable ASCII (0x20-0x7E)
i = 0LL;
while ( height * width > (int)i )
{
*(_QWORD *)&ascii_char = sprite_data[i++];
if ( (unsigned __int8)(ascii_char - 32) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

// Store sprite data pointer
cimg->sprites[sprite_id].data = sprite_data;
return __readfsqword(40u) ^ v13;
}

The third handler reads the first 3 bytes of the input data as sprite_id, width, height values.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

read_exact(0LL, &sprite_id, 1LL, "ERROR: Failed to read &sprite_id!", 0xFFFFFFFFLL);
read_exact(0LL, &width, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &height, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);

# ---- snip ----

Then it sets the sprite_store_t.width and sprite_store_t.height values to the ones we provided.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

// `sprites = &cimg->sprites[sprite_id];`
sprites = (sprite_store_t *)((char *)cimg + 16 * sprite_id);

// Update sprite dimensions
BYTE1(sprites[1].data) = width; // `cimg->sprites[sprite_id].width = width;`
old_sprite_data = *(sprite_store_t **)&sprites[2].height;// `old_sprite_data = cimg->sprites[sprite_id].data`
LOBYTE(sprites[1].data) = height; // `cimg->sprites[sprite_id].height = height;`

# ---- snip ----
/challenge/cimg :: Local Types
# ---- snip ----

typedef struct sprite_store {
uint8_t height;
uint8_t width;
char __pad[6];
uint8_t *data;
} sprite_store_t;

# ---- snip ----

Next it allocates a location of memory data which can fit width * height number of bytes.

# ---- snip ----

// Allocate memory for sprite character data (width * height bytes)
data_size = height * width;
sprite_data = (unsigned __int8 *)malloc(data_size);

# ---- snip ----

The user provided input is read into the data memory allocation. Then, it checks if each the character byte, lies between 0x20 and 0x7e.

/challenge/cimg :: handle_3() :: Pseudocode
# ---- snip ----

if ( !sprite_data )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto EXIT;
}
read_exact(0LL, sprite_data, (unsigned int)data_size, "ERROR: Failed to read data!", 0xFFFFFFFFLL);

// Validate all characters are printable ASCII (0x20-0x7E)
i = 0LL;
while ( height * width > (int)i )
{
*(_QWORD *)&ascii_char = sprite_data[i++];
if ( (unsigned __int8)(ascii_char - 0x20) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", *(_QWORD *)&ascii_char);
EXIT:
exit(-1);
}
}

# ---- snip ----

Finally, we can figure out what handle_4() is doing.

/challenge/cimg :: handle_4() :: Pseudocode
// positive sp value has been detected, the output may be wrong!
unsigned __int64 __fastcall handle_4(struct cimg *cimg)
{
pixel_t p_pixels; // edi
__int64 init_count; // rcx
__int64 height; // rdx
uint8_t width; // r10
char v6; // r11
char v7; // bp
char *v8; // rdx
int v9; // r12d
int v10; // r8d
int v11; // edi
__int64 v12; // rax
__int64 v13; // r9
int i; // r15d
int j; // r10d
int v16; // r11d
char *v17; // rdx
int v18; // r12d
int v19; // ebp
int k; // r13d
int v21; // eax
__int64 v22; // rax
__int64 v23; // rdx
int v24; // r14d
__int64 v25; // rdx
union term_pixel_t *v26; // rdx
int v28; // [rsp-40h] [rbp-40070h]
int v29; // [rsp-3Ch] [rbp-4006Ch]
sprite_store_t sprite_render_record; // [rsp-32h] [rbp-40062h] BYREF
char v31; // [rsp+0h] [rbp-40030h] BYREF
__int64 v32; // [rsp+1000h] [rbp-3F030h] BYREF
__int128 v33; // [rsp+3FFD7h] [rbp-59h] BYREF
__int64 v34; // [rsp+3FFE7h] [rbp-49h]
unsigned __int64 v35; // [rsp+3FFF0h] [rbp-40h]

while ( &v31 != (char *)(&v32 - 0x8000) )
;
v35 = __readfsqword(40u);
read_exact(0LL, &sprite_render_record, 9LL, "ERROR: Failed to read &sprite_render_record!", 0xFFFFFFFFLL);
*(_QWORD *)&p_pixels.r = (char *)&sprite_render_record.data + 1;
init_count = 0x10000LL;
height = sprite_render_record.height;
width = sprite_render_record.width;
while ( init_count )
{
**(_DWORD **)&p_pixels.r = 0;
*(_QWORD *)&p_pixels.r += 4LL;
--init_count;
}
v6 = sprite_render_record.__pad[0];
v7 = sprite_render_record.__pad[1];
v8 = &cimg->header.magic_number[16 * height];
v9 = (unsigned __int8)v8[24];
while ( v9 > (int)init_count )
{
v10 = (unsigned __int8)v8[25];
v11 = 0;
v12 = (unsigned int)(init_count * v10);
while ( v10 > v11 )
{
v13 = *((_QWORD *)v8 + 4);
sprite_render_record.__pad[4 * v12 + 7] = width;
sprite_render_record.__pad[4 * v12 + 8] = v6;
sprite_render_record.__pad[4 * v12 + 9] = v7;
if ( !v13 )
{
fputs("ERROR: attempted to render uninitialized sprite!\n", stderr);
exit(-1);
}
++v11;
sprite_render_record.__pad[4 * v12 + 10] = *(_BYTE *)(v13 + v12);
++v12;
}
LODWORD(init_count) = init_count + 1;
}
for ( i = 0; (unsigned __int8)sprite_render_record.__pad[5] > i; ++i )
{
for ( j = 0; (unsigned __int8)sprite_render_record.__pad[4] > j; ++j )
{
v16 = 0;
v17 = &cimg->header.magic_number[16 * sprite_render_record.height];
v18 = (unsigned __int8)(sprite_render_record.__pad[2] + j * v17[25]);
v19 = (unsigned __int8)(sprite_render_record.__pad[3] + i * v17[24]);
while ( cimg->sprites[sprite_render_record.height].height > v16 )
{
for ( k = 0; ; ++k )
{
v21 = cimg->sprites[sprite_render_record.height].width;
if ( v21 <= k )
break;
v22 = k + v16 * v21;
v23 = (unsigned __int8)sprite_render_record.__pad[4 * v22 + 10];
if ( (_BYTE)v23 != LOBYTE(sprite_render_record.data) )
{
v29 = v16;
v24 = cimg->header.width;
v28 = j;
__snprintf_chk(
&v33,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
(unsigned __int8)sprite_render_record.__pad[4 * v22 + 7],
(unsigned __int8)sprite_render_record.__pad[4 * v22 + 8],
(unsigned __int8)sprite_render_record.__pad[4 * v22 + 9],
v23);
v16 = v29;
j = v28;
v25 = (unsigned int)((k + v18) % v24);
LODWORD(v25) = (unsigned int)(v25 + v19 * v24) % cimg->num_pixels;
v26 = &cimg->framebuffer[v25];
*(_OWORD *)v26->data = v33;
*(_QWORD *)&v26->str.b[1] = v34;
}
}
++v16;
++v19;
}
}
}
return __readfsqword(0x28u) ^ v35;
}

We have to update the structs so that they work with the updated handle_4() function.

/challenge/cimg :: Local Types
struct cimg_header {
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
uint32_t remaining_directives;
};

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

struct term_str_st {
char color_set[7];
char r[3];
char s1;
char g[3];
char s2;
char b[3];
char m;
char c;
char color_reset[4];
};

union term_pixel_t {
char data[24];
struct term_str_st str;
};

typedef struct sprite_store {
uint8_t height;
uint8_t width;
char __pad[6];
uint8_t *data;
} sprite_store_t;

typedef struct sprite_render {
uint8_t sprite_id;
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t x_offset;
uint8_t y_offset;
uint8_t repeat_x;
uint8_t repeat_y;
uint8_t skip_char;
} sprite_render_t;

struct cimg {
struct cimg_header header;
unsigned int num_pixels;
union term_pixel_t *framebuffer;
sprite_store_t sprites[16];
};
/challenge/cimg :: handle_4() :: Pseudocode
unsigned __int64 __fastcall handle_4(struct cimg *cimg)
{
_DWORD *p_pixels;
__int64 init_count;
__int64 sprite_id;
uint8_t r;
uint8_t g;
uint8_t b;
char *sprites;
int sprite_height;
int sprite_width;
int col;
__int64 pixel_idx;
__int64 sprite_char;
int repeat_y_counter;
int repeat_x_counter;
int row;
char *sprites2;
int tile_x;
int tile_y;
int k;
int sprite_width2;
__int64 pixel_idx2;
__int64 ascii_char;
int framebuffer_width;
__int64 framebuffer_idx;
union term_pixel_t *framebuffer_ptr;
sprite_render_t sprite_render_record; // v30[9]
pixel_t pixels[65536]; // v31
term_pixel_t emit_tmp;
unsigned __int64 canary;

read_exact(0LL, &sprite_render_record, 9LL, "ERROR: Failed to read &sprite_render_record!", 0xFFFFFFFFLL);

// Zero out pixel buffer
p_pixels = pixels;
init_count = 0x10000LL;
sprite_id = sprite_render_record.sprite_id;
r = sprite_render_record.r;
while ( init_count )
{
*p_pixels++ = 0;
--init_count;
}
g = sprite_render_record.g;
b = sprite_render_record.b;

// Get sprite pointer
sprites = &cimg->header.magic_number[16 * sprite_id];
sprite_height = (unsigned __int8)sprites[24]; // cimg->sprites[sprite_id].height

// PHASE 1: Build RGBC buffer
while ( sprite_height > (int)init_count )
{
sprite_width = (unsigned __int8)sprites[25]; // cimg->sprites[sprite_id].width
col = 0;
pixel_idx = (unsigned int)(init_count * sprite_width);
while ( sprite_width > col )
{
sprite_char = *((_QWORD *)sprites + 4); // cimg->sprites[sprite_id].data
pixels[pixel_idx].r = r;
pixels[pixel_idx].g = g;
pixels[pixel_idx].b = b;
if ( !sprite_char )
{
fputs("ERROR: attempted to render uninitialized sprite!\n", stderr);
exit(-1);
}
++col;
pixels[pixel_idx].ascii = *(_BYTE *)(sprite_char + pixel_idx);
++pixel_idx;
}
LODWORD(init_count) = init_count + 1;
}

// PHASE 2: Tiled render to framebuffer
for ( repeat_y_counter = 0; sprite_render_record.repeat_y > repeat_y_counter; ++repeat_y_counter )
{
for ( repeat_x_counter = 0; sprite_render_record.repeat_x > repeat_x_counter; ++repeat_x_counter )
{
row = 0;
sprites2 = &cimg->header.magic_number[16 * sprite_render_record.sprite_id];
tile_x = (unsigned __int8)(sprite_render_record.x_offset + repeat_x_counter * sprites2[25]);
tile_y = (unsigned __int8)(sprite_render_record.y_offset + repeat_y_counter * sprites2[24]);
while ( cimg->sprites[sprite_render_record.sprite_id].height > row )
{
for ( k = 0; ; ++k )
{
sprite_width2 = cimg->sprites[sprite_render_record.sprite_id].width;
if ( sprite_width2 <= k )
break;
pixel_idx2 = k + row * sprite_width2;
ascii_char = (unsigned __int8)pixels[pixel_idx2].ascii;
if ( (_BYTE)ascii_char != sprite_render_record.skip_char )
{
framebuffer_width = cimg->header.width;
__snprintf_chk(
&emit_tmp, 25LL, 1LL, 25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
(unsigned __int8)pixels[pixel_idx2].r,
(unsigned __int8)pixels[pixel_idx2].g,
(unsigned __int8)pixels[pixel_idx2].b,
ascii_char);
framebuffer_idx = (unsigned int)((k + tile_x) % framebuffer_width);
framebuffer_idx = (unsigned int)(framebuffer_idx + tile_y * framebuffer_width) % cimg->num_pixels;
cimg->framebuffer[framebuffer_idx] = emit_tmp;
}
}
++row;
++tile_y;
}
}
}
return __readfsqword(0x28u) ^ canary;
}

Exploit

~/script.py
from pwn import *
import struct
import re

# Desired ANSI sequence
binary = context.binary = ELF('/challenge/cimg')
desired_ansi_sequence_bytes = binary.string(binary.sym.desired_output)
desired_ansi_sequence = desired_ansi_sequence_bytes.decode("utf-8")
print(desired_ansi_sequence)
hacker@reverse-engineering~advanced-sprites:~$ python ~/script.py
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
.--------------------------------------------------------------------------.| || || || || || || || || ___ __ __ ____ || ___ |_ _| | \/ | / ___| || / __| | | | |\/| | | | _ || | (__ | | | | | | | |_| | || \___| |___| |_| |_| \____| || || || || || || || || || |'--------------------------------------------------------------------------'
In [1]: print(len(".--------------------------------------------------------------------------."))
76

Width is 76.

gdb

set pagination off
set confirm off

b read_exact
commands
silent
printf "read_exact(len=%ld) total_data=%ld\n", $rdx, *(unsigned long *)&total_data
continue
end

run solution.cimg
hacker@reverse-engineering~advanced-sprites:~$ gdb -q /challenge/cimg -x trace_total_data.gdb
Reading symbols from /challenge/cimg...
(No debugging symbols found in /challenge/cimg)
Breakpoint 1 at 0x4016fb
read_exact(len=12) total_data=0
read_exact(len=2) total_data=12
read_exact(len=1) total_data=14
read_exact(len=1) total_data=15
read_exact(len=1) total_data=16
read_exact(len=1) total_data=17
read_exact(len=2) total_data=18
read_exact(len=1) total_data=20
read_exact(len=1) total_data=21
read_exact(len=1) total_data=22
read_exact(len=1) total_data=23
read_exact(len=2) total_data=24
read_exact(len=1) total_data=26
read_exact(len=1) total_data=27
read_exact(len=1) total_data=28
read_exact(len=1) total_data=29
read_exact(len=2) total_data=30
read_exact(len=1) total_data=32
read_exact(len=1) total_data=33
read_exact(len=1) total_data=34
read_exact(len=1) total_data=35
read_exact(len=2) total_data=36
read_exact(len=9) total_data=38
read_exact(len=2) total_data=47
read_exact(len=9) total_data=49
read_exact(len=2) total_data=58
read_exact(len=9) total_data=60
read_exact(len=2) total_data=69
read_exact(len=9) total_data=71
read_exact(len=2) total_data=80
read_exact(len=9) total_data=82
read_exact(len=2) total_data=91
read_exact(len=9) total_data=93
read_exact(len=2) total_data=102
read_exact(len=9) total_data=104
read_exact(len=2) total_data=113
read_exact(len=9) total_data=115
read_exact(len=2) total_data=124
read_exact(len=1) total_data=126
read_exact(len=1) total_data=127
read_exact(len=1) total_data=128
read_exact(len=25) total_data=129
read_exact(len=2) total_data=154
read_exact(len=9) total_data=156
read_exact(len=2) total_data=165
read_exact(len=1) total_data=167
read_exact(len=1) total_data=168
read_exact(len=1) total_data=169
read_exact(len=40) total_data=170
read_exact(len=2) total_data=210
read_exact(len=9) total_data=212
read_exact(len=2) total_data=221
read_exact(len=1) total_data=223
read_exact(len=1) total_data=224
read_exact(len=1) total_data=225
read_exact(len=35) total_data=226
read_exact(len=2) total_data=261
read_exact(len=9) total_data=263
read_exact(len=2) total_data=272
read_exact(len=1) total_data=274
read_exact(len=1) total_data=275
read_exact(len=1) total_data=276
read_exact(len=30) total_data=277
read_exact(len=2) total_data=307
read_exact(len=9) total_data=309
.--------------------------------------------------------------------------.
| |
| |
| |
| |
| |
| |
| |
| |
| ___ __ __ ____ |
| ___ |_ _| | \/ | / ___| |
| / __| | | | |\/| | | | _ |
| | (__ | | | | | | | |_| | |
| \___| |___| |_| |_| \____| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
'--------------------------------------------------------------------------'
[Inferior 1 (process 60494) exited normally]
(gdb)
~/script.py
from pwn import *
import struct
import re

binary = ELF("/challenge/cimg")
ansi = binary.string(binary.sym.desired_output).decode()

pattern = r"\x1b\[38;2;(\d+);(\d+);(\d+)m(.)"
pixels = re.findall(pattern, ansi)

W, H = 76, 24
assert len(pixels) == W * H

def px(x, y):
r, g, b, ch = pixels[y * W + x]
return int(r), int(g), int(b), ch

WHITE = px(0, 0)[:3]

directives = []

def add_sprite(sid, w, h, data):
directives.append(struct.pack("<HBBB", 3, sid, w, h) + data.encode())

def render(sid, r, g, b, x, y, rx, ry, skip):
directives.append(
struct.pack("<HBBBBBBBBB",
4, sid, r, g, b, x, y, rx, ry, ord(skip))
)

sid = 0

# ------------------------------------------------------------
# SPRITES
# ------------------------------------------------------------

add_sprite(sid, 1, 1, "-")
dash = sid; sid += 1

add_sprite(sid, 1, 1, "|")
pipe = sid; sid += 1

add_sprite(sid, 1, 1, ".")
dot = sid; sid += 1

add_sprite(sid, 1, 1, "'")
quote = sid; sid += 1

# ------------------------------------------------------------
# BORDERS
# ------------------------------------------------------------

# Horizontal border (top + bottom)
render(dash, *WHITE, 0, H - 1, W, 2, ' ')

# Vertical border (left + right)
render(pipe, *WHITE, W - 1, 0, 2, H, ' ')

# ------------------------------------------------------------
# CORNERS (2 renders only, torus-aware)
# ------------------------------------------------------------

# Top-left + top-right
render(dot, *WHITE, W - 1, 0, 2, 1, ' ')

# Bottom-left + bottom-right
render(quote, *WHITE, W - 1, H - 1, 2, 1, ' ')

# ------------------------------------------------------------
# LOGO
# ------------------------------------------------------------

lx, ly, lw, lh = 22, 9, 35, 5
by_color = {}

for dy in range(lh):
for dx in range(lw):
r, g, b, ch = px(lx + dx, ly + dy)
if ch != " ":
by_color.setdefault((r, g, b), []).append((dx, dy, ch))

for (r, g, b), pts in by_color.items():
xs = [p[0] for p in pts]
ys = [p[1] for p in pts]

minx, maxx = min(xs), max(xs)
miny, maxy = min(ys), max(ys)

w = maxx - minx + 1
h = maxy - miny + 1

grid = [" "] * (w * h)
for x, y, ch in pts:
grid[(y - miny) * w + (x - minx)] = ch

add_sprite(sid, w, h, "".join(grid))
render(sid, r, g, b, lx + minx, ly + miny, 1, 1, ' ')
sid += 1

# ------------------------------------------------------------
# HEADER
# ------------------------------------------------------------

header = struct.pack(
"<I H B B I",
0x474D4963,
4,
W,
H,
len(directives)
)

payload = header + b"".join(directives)
print("Final Payload Size:", len(payload))

with open("solution.cimg", "wb") as f:
f.write(payload)
hacker@reverse-engineering~advanced-sprites:~$ python ~/script.py
[*] '/challenge/cimg'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
FORTIFY: Enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
Final Payload Size: 268
hacker@reverse-engineering~advanced-sprites:~$ /challenge/cimg ~/solution.cimg
.--------------------------------------------------------------------------.
| |
| |
| |
| |
| |
| |
| |
| |
| ___ __ __ ____ |
| ___ |_ _| | \/ | / ___| |
| / __| | | | |\/| | | | _ |
| | (__ | | | | | | | |_| | |
| \___| |___| |_| |_| \____| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
'--------------------------------------------------------------------------'
pwn.college{4JNfuh3hnmWTXsBKQetE_NAtGxO.QXzEzMwEDL4ITM0EzW}

 

Accessing Resources

Binary Analysis

/challenge/cimg :: main()
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rcx
int *v5; // rdi
bool v6; // of
int v7; // r8d
const char *v8; // r12
int v9; // eax
const char *v10; // rdi
unsigned __int16 v13; // [rsp+Eh] [rbp-103Ah] BYREF
int v14; // [rsp+10h] [rbp-1038h] BYREF
__int16 v15; // [rsp+14h] [rbp-1034h]
int v16; // [rsp+18h] [rbp-1030h]
unsigned __int64 v17; // [rsp+1028h] [rbp-20h]

v3 = 1030LL;
v17 = __readfsqword(0x28u);
v5 = &v14;
v6 = __OFSUB__(argc, 1);
v7 = argc - 1;
while ( v3 )
{
*v5++ = 0;
--v3;
}
if ( !((v7 < 0) ^ v6 | (v7 == 0)) )
{
v8 = argv[1];
if ( strcmp(&v8[strlen(v8) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto LABEL_11;
}
v9 = open(v8, 0);
dup2(v9, 0);
}
read_exact(0LL, &v14, 12LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( v14 != 1196247395 )
{
v10 = "ERROR: Invalid magic number!";
LABEL_10:
puts(v10);
LABEL_11:
exit(-1);
}
v10 = "ERROR: Unsupported version!";
if ( v15 != 4 )
goto LABEL_10;
initialize_framebuffer(&v14);
while ( 2 )
{
if ( v16-- )
{
read_exact(0LL, &v13, 2LL, "ERROR: Failed to read &directive_code!", 0xFFFFFFFFLL);
switch ( v13 )
{
case 1u:
handle_1(&v14);
continue;
case 2u:
handle_2(&v14);
continue;
case 3u:
handle_3(&v14);
continue;
case 4u:
handle_4(&v14);
continue;
case 5u:
handle_5(&v14);
continue;
default:
__fprintf_chk(stderr, 1LL, "ERROR: invalid directive_code %ux\n", v13);
goto LABEL_11;
}
}
break;
}
display(&v14, 0LL);
return 0;
}
/challenge/cimg :: handle_1() :: Pseudocode
unsigned __int64 __fastcall handle_1(__int64 a1)
{
int v1; // ebp
int v2; // edx
size_t v3; // rbp
unsigned __int8 *v4; // rax
unsigned __int8 *v5; // r12
__int64 v6; // rax
__int64 v7; // rcx
int i; // r13d
int v9; // ebp
int v10; // r15d
unsigned __int8 *v11; // rax
__int64 v12; // kr00_8
__int64 v13; // rdx
__int128 v15; // [rsp+1Fh] [rbp-59h] BYREF
__int64 v16; // [rsp+2Fh] [rbp-49h]
unsigned __int64 v17; // [rsp+38h] [rbp-40h]

v1 = *(unsigned __int8 *)(a1 + 6);
v2 = *(unsigned __int8 *)(a1 + 7);
v17 = __readfsqword(0x28u);
v3 = 4LL * v2 * v1;
v4 = (unsigned __int8 *)malloc(v3);
if ( !v4 )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto LABEL_7;
}
v5 = v4;
read_exact(0LL, v4, (unsigned int)v3, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v6 = 0LL;
while ( *(unsigned __int8 *)(a1 + 7) * *(unsigned __int8 *)(a1 + 6) > (int)v6 )
{
v7 = v5[4 * v6++ + 3];
if ( (unsigned __int8)(v7 - 32) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", v7);
LABEL_7:
exit(-1);
}
}
for ( i = 0; *(unsigned __int8 *)(a1 + 7) > i; ++i )
{
v9 = 0;
while ( 1 )
{
v10 = *(unsigned __int8 *)(a1 + 6);
if ( v10 <= v9 )
break;
v11 = &v5[4 * i * v10 + 4 * v9];
__snprintf_chk(&v15, 25LL, 1LL, 25LL, "\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m", *v11, v11[1], v11[2], v11[3]);
v12 = v9++;
v13 = *(_QWORD *)(a1 + 16) + 24LL * (((unsigned int)(v12 % v10) + i * v10) % *(_DWORD *)(a1 + 12));
*(_OWORD *)v13 = v15;
*(_QWORD *)(v13 + 16) = v16;
}
}
return __readfsqword(0x28u) ^ v17;
}
/challenge/cimg :: handle_2() :: Pseudocode
unsigned __int64 __fastcall handle_2(__int64 a1)
{
unsigned int v1; // ebx
unsigned __int8 *v2; // rax
unsigned __int8 *v3; // rbp
__int64 v4; // rax
__int64 v5; // rcx
int i; // r13d
int v7; // r14d
int v8; // eax
int v9; // ecx
unsigned int v10; // ebx
__int64 v11; // rdx
unsigned __int8 v13; // [rsp+Bh] [rbp-5Dh] BYREF
unsigned __int8 v14; // [rsp+Ch] [rbp-5Ch] BYREF
unsigned __int8 v15; // [rsp+Dh] [rbp-5Bh] BYREF
unsigned __int8 v16; // [rsp+Eh] [rbp-5Ah] BYREF
__int128 v17; // [rsp+Fh] [rbp-59h] BYREF
__int64 v18; // [rsp+1Fh] [rbp-49h]
unsigned __int64 v19; // [rsp+28h] [rbp-40h]

v19 = __readfsqword(0x28u);
read_exact(0LL, &v15, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &v16, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &v13, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &v14, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);
v1 = 4 * v14 * v13;
v2 = (unsigned __int8 *)malloc(4LL * v14 * v13);
if ( !v2 )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto LABEL_7;
}
v3 = v2;
read_exact(0LL, v2, v1, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( v14 * v13 > (int)v4 )
{
v5 = v3[4 * v4++ + 3];
if ( (unsigned __int8)(v5 - 32) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", v5);
LABEL_7:
exit(-1);
}
}
for ( i = 0; v14 > i; ++i )
{
v7 = 0;
while ( v13 > v7 )
{
v8 = v7 + v15;
v9 = v7 + i * v13;
++v7;
v10 = v8 % *(unsigned __int8 *)(a1 + 6) + *(unsigned __int8 *)(a1 + 6) * (i + v16);
__snprintf_chk(
&v17,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
v3[4 * v9],
v3[4 * v9 + 1],
v3[4 * v9 + 2],
v3[4 * v9 + 3]);
v11 = *(_QWORD *)(a1 + 16) + 24LL * (v10 % *(_DWORD *)(a1 + 12));
*(_OWORD *)v11 = v17;
*(_QWORD *)(v11 + 16) = v18;
}
}
return __readfsqword(0x28u) ^ v19;
}
/challenge/cimg :: handle_3() :: Pseudocode
unsigned __int64 __fastcall handle_3(__int64 a1)
{
__int64 v2; // rax
void *v3; // rdi
int v4; // r12d
unsigned __int8 *v5; // rax
unsigned __int8 *v6; // rbx
__int64 v7; // rax
__int64 v8; // rcx
unsigned __int8 v10; // [rsp+5h] [rbp-23h] BYREF
unsigned __int8 v11; // [rsp+6h] [rbp-22h] BYREF
unsigned __int8 v12; // [rsp+7h] [rbp-21h] BYREF
unsigned __int64 v13; // [rsp+8h] [rbp-20h]

v13 = __readfsqword(0x28u);
read_exact(0LL, &v10, 1LL, "ERROR: Failed to read &sprite_id!", 0xFFFFFFFFLL);
read_exact(0LL, &v11, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &v12, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);
v2 = a1 + 16LL * v10;
*(_BYTE *)(v2 + 25) = v11;
v3 = *(void **)(v2 + 32);
*(_BYTE *)(v2 + 24) = v12;
if ( v3 )
free(v3);
v4 = v12 * v11;
v5 = (unsigned __int8 *)malloc(v4);
v6 = v5;
if ( !v5 )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto LABEL_9;
}
read_exact(0LL, v5, (unsigned int)v4, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v7 = 0LL;
while ( v12 * v11 > (int)v7 )
{
v8 = v6[v7++];
if ( (unsigned __int8)(v8 - 32) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", v8);
LABEL_9:
exit(-1);
}
}
*(_QWORD *)(16LL * v10 + a1 + 32) = v6;
return __readfsqword(0x28u) ^ v13;
}
/challenge/cimg :: handle_4() :: Pseudocode
// positive sp value has been detected, the output may be wrong!
unsigned __int64 __fastcall handle_4(__int64 a1)
{
_DWORD *v2; // rdi
__int64 v3; // rcx
__int64 v4; // rdx
char v5; // r10
char v6; // r11
char v7; // bp
__int64 v8; // rdx
int v9; // r12d
int v10; // r8d
int v11; // edi
__int64 v12; // rax
__int64 v13; // r9
int i; // r15d
int j; // r10d
int v16; // r11d
__int64 v17; // rdx
int v18; // r12d
int v19; // ebp
int k; // r13d
int v21; // eax
__int64 v22; // rax
__int64 v23; // rdx
int v24; // r14d
__int64 v25; // rdx
__int64 v26; // rdx
int v28; // [rsp-40h] [rbp-40070h]
int v29; // [rsp-3Ch] [rbp-4006Ch]
_BYTE v30[9]; // [rsp-32h] [rbp-40062h] BYREF
_BYTE v31[41]; // [rsp-29h] [rbp-40059h] BYREF
char v32; // [rsp+0h] [rbp-40030h] BYREF
__int64 v33; // [rsp+1000h] [rbp-3F030h] BYREF
__int128 v34; // [rsp+3FFD7h] [rbp-59h] BYREF
__int64 v35; // [rsp+3FFE7h] [rbp-49h]
unsigned __int64 v36; // [rsp+3FFF0h] [rbp-40h]

while ( &v32 != (char *)(&v33 - 0x8000) )
;
v36 = __readfsqword(0x28u);
read_exact(0LL, v30, 9LL, "ERROR: Failed to read &sprite_render_record!", 0xFFFFFFFFLL);
v2 = v31;
v3 = 0x10000LL;
v4 = v30[0];
v5 = v30[1];
while ( v3 )
{
*v2++ = 0;
--v3;
}
v6 = v30[2];
v7 = v30[3];
v8 = a1 + 16 * v4;
v9 = *(unsigned __int8 *)(v8 + 24);
while ( v9 > (int)v3 )
{
v10 = *(unsigned __int8 *)(v8 + 25);
v11 = 0;
v12 = (unsigned int)(v3 * v10);
while ( v10 > v11 )
{
v13 = *(_QWORD *)(v8 + 32);
v31[4 * v12] = v5;
v31[4 * v12 + 1] = v6;
v31[4 * v12 + 2] = v7;
if ( !v13 )
{
fputs("ERROR: attempted to render uninitialized sprite!\n", stderr);
exit(-1);
}
++v11;
v31[4 * v12 + 3] = *(_BYTE *)(v13 + v12);
++v12;
}
LODWORD(v3) = v3 + 1;
}
for ( i = 0; v30[7] > i; ++i )
{
for ( j = 0; v30[6] > j; ++j )
{
v16 = 0;
v17 = a1 + 16LL * v30[0];
v18 = (unsigned __int8)(v30[4] + j * *(_BYTE *)(v17 + 25));
v19 = (unsigned __int8)(v30[5] + i * *(_BYTE *)(v17 + 24));
while ( *(unsigned __int8 *)(16LL * v30[0] + a1 + 24) > v16 )
{
for ( k = 0; ; ++k )
{
v21 = *(unsigned __int8 *)(16LL * v30[0] + a1 + 25);
if ( v21 <= k )
break;
v22 = k + v16 * v21;
v23 = (unsigned __int8)v31[4 * v22 + 3];
if ( (_BYTE)v23 != v30[8] )
{
v29 = v16;
v24 = *(unsigned __int8 *)(a1 + 6);
v28 = j;
__snprintf_chk(
&v34,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
(unsigned __int8)v31[4 * v22],
(unsigned __int8)v31[4 * v22 + 1],
(unsigned __int8)v31[4 * v22 + 2],
v23);
v16 = v29;
j = v28;
v25 = (unsigned int)((k + v18) % v24);
LODWORD(v25) = (unsigned int)(v25 + v19 * v24) % *(_DWORD *)(a1 + 12);
v26 = *(_QWORD *)(a1 + 16) + 24 * v25;
*(_OWORD *)v26 = v34;
*(_QWORD *)(v26 + 16) = v35;
}
}
++v16;
++v19;
}
}
}
return __readfsqword(0x28u) ^ v36;
}
/challenge/cimg :: handle_5() :: Pseudocode
unsigned __int64 __fastcall handle_5(__int64 a1)
{
__int16 v2; // dx
int v3; // eax
unsigned int v4; // ebp
void *v5; // rdi
int v6; // r13d
unsigned __int8 *v7; // rax
unsigned __int8 *v8; // rbx
__int64 v9; // rax
__int64 v10; // rcx
char v12[259]; // [rsp+5h] [rbp-133h] BYREF
unsigned __int64 v13; // [rsp+108h] [rbp-30h]

v13 = __readfsqword(0x28u);
memset(v12, 0, sizeof(v12));
read_exact(0LL, v12, 258LL, "ERROR: Failed to read &sprite_load_record!", 0xFFFFFFFFLL);
LOBYTE(v2) = v12[2];
HIBYTE(v2) = v12[1];
*(_WORD *)(a1 + 16LL * (unsigned __int8)v12[0] + 24) = v2;
v3 = open(&v12[3], 0);
if ( v3 < 0 )
{
fputs("ERROR: failed to open sprite file\n", stderr);
goto LABEL_7;
}
v4 = v3;
v5 = *(void **)(16LL * (unsigned __int8)v12[0] + a1 + 32);
if ( v5 )
free(v5);
v6 = (unsigned __int8)v12[2] * (unsigned __int8)v12[1];
v7 = (unsigned __int8 *)malloc(v6);
v8 = v7;
if ( !v7 )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto LABEL_7;
}
read_exact(v4, v7, (unsigned int)v6, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v9 = 0LL;
while ( (unsigned __int8)v12[2] * (unsigned __int8)v12[1] > (int)v9 )
{
v10 = v8[v9++];
if ( (unsigned __int8)(v10 - 32) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", v10);
LABEL_7:
exit(-1);
}
}
*(_QWORD *)(16LL * (unsigned __int8)v12[0] + a1 + 32) = v8;
close(v4);
return __readfsqword(0x28u) ^ v13;
}

In this challenge, the handle_5() function reads the sprite data from a file, instead of reading it from STDIN. We can leverage this to get the flag from the /flag file.

Exploit

~/script.py
from pwn import *
import struct

MAGIC = 0x474D4963 # "cIMG"
VERSION = 4
FB_W, FB_H = 80, 80 # tall framebuffer so nothing clips

SPRITE_ID = 0
FLAG_PATH = b"/flag\x00"

# Read enough bytes to cover full flag
SPRITE_W = 1 # ONE column
SPRITE_H = 59 # read 48 bytes vertically

def directive_5(sprite_id, w, h, path):
# [sprite_id][height][width]
rec = bytes([sprite_id, h, w])
rec += path.ljust(255, b"\x00")
return struct.pack("<H", 5) + rec

def directive_4(sprite_id, x, y):
return struct.pack(
"<HBBBBBBBBB",
4,
sprite_id,
255, 255, 255,
x, y,
1, 1, # NO repetition
0
)

directives = [
directive_5(SPRITE_ID, SPRITE_W, SPRITE_H, FLAG_PATH),
directive_4(SPRITE_ID, 0, 0),
]

header = struct.pack(
"<I H B B I",
MAGIC,
VERSION,
FB_W,
FB_H,
len(directives)
)

payload = header + b"".join(directives)

with open("solution.cimg", "wb") as f:
f.write(payload)

print("[+] Run: /challenge/cimg solution.cimg")
hacker@reverse-engineering~accessing-resources:~$ /challenge/cimg ~/solution.cimg
pwn.college{cl4-LleAt4eMVhiMpLcaDyn4RBc.QX0EzMwEDL4ITM0EzW}

 

Unsafe Animations

Binary Analysis

/challenge/cimg :: main() :: Pseudocode
int __fastcall main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rcx
int *v5; // rdi
bool v6; // of
int v7; // r8d
const char *v8; // r12
int v9; // eax
const char *v10; // rdi
unsigned __int16 v13; // [rsp+Eh] [rbp-103Ah] BYREF
int v14; // [rsp+10h] [rbp-1038h] BYREF
__int16 v15; // [rsp+14h] [rbp-1034h]
int v16; // [rsp+18h] [rbp-1030h]
unsigned __int64 v17; // [rsp+1028h] [rbp-20h]

v3 = 1030LL;
v17 = __readfsqword(0x28u);
v5 = &v14;
v6 = __OFSUB__(argc, 1);
v7 = argc - 1;
while ( v3 )
{
*v5++ = 0;
--v3;
}
if ( !((v7 < 0) ^ v6 | (v7 == 0)) )
{
v8 = argv[1];
if ( strcmp(&v8[strlen(v8) - 5], ".cimg") )
{
__printf_chk(1LL, "ERROR: Invalid file extension!");
goto LABEL_11;
}
v9 = open(v8, 0);
dup2(v9, 0);
}
read_exact(0LL, &v14, 12LL, "ERROR: Failed to read header!", 0xFFFFFFFFLL);
if ( v14 != 1196247395 )
{
v10 = "ERROR: Invalid magic number!";
LABEL_10:
puts(v10);
LABEL_11:
exit(-1);
}
v10 = "ERROR: Unsupported version!";
if ( v15 != 4 )
goto LABEL_10;
initialize_framebuffer(&v14);
while ( 2 )
{
if ( v16-- )
{
read_exact(0LL, &v13, 2LL, "ERROR: Failed to read &directive_code!", 0xFFFFFFFFLL);
switch ( v13 )
{
case 1u:
handle_1(&v14);
continue;
case 2u:
handle_2(&v14);
continue;
case 3u:
handle_3(&v14);
continue;
case 4u:
handle_4(&v14);
continue;
case 5u:
handle_5(&v14);
continue;
case 6u:
handle_6(&v14);
continue;
case 7u:
handle_7(&v14);
continue;
default:
__fprintf_chk(stderr, 1LL, "ERROR: invalid directive_code %ux\n", v13);
goto LABEL_11;
}
}
break;
}
display(&v14, 0LL);
return 0;
}
/challenge/cimg :: handle_1() :: Pseudocode
unsigned __int64 __fastcall handle_1(__int64 a1)
{
int v1; // ebp
int v2; // edx
size_t v3; // rbp
unsigned __int8 *v4; // rax
unsigned __int8 *v5; // r12
__int64 v6; // rax
__int64 v7; // rcx
int i; // r13d
int v9; // ebp
int v10; // r15d
unsigned __int8 *v11; // rax
__int64 v12; // kr00_8
__int64 v13; // rdx
__int128 v15; // [rsp+1Fh] [rbp-59h] BYREF
__int64 v16; // [rsp+2Fh] [rbp-49h]
unsigned __int64 v17; // [rsp+38h] [rbp-40h]

v1 = *(unsigned __int8 *)(a1 + 6);
v2 = *(unsigned __int8 *)(a1 + 7);
v17 = __readfsqword(0x28u);
v3 = 4LL * v2 * v1;
v4 = (unsigned __int8 *)malloc(v3);
if ( !v4 )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto LABEL_7;
}
v5 = v4;
read_exact(0LL, v4, (unsigned int)v3, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v6 = 0LL;
while ( *(unsigned __int8 *)(a1 + 7) * *(unsigned __int8 *)(a1 + 6) > (int)v6 )
{
v7 = v5[4 * v6++ + 3];
if ( (unsigned __int8)(v7 - 32) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", v7);
LABEL_7:
exit(-1);
}
}
for ( i = 0; *(unsigned __int8 *)(a1 + 7) > i; ++i )
{
v9 = 0;
while ( 1 )
{
v10 = *(unsigned __int8 *)(a1 + 6);
if ( v10 <= v9 )
break;
v11 = &v5[4 * i * v10 + 4 * v9];
__snprintf_chk(&v15, 25LL, 1LL, 25LL, "\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m", *v11, v11[1], v11[2], v11[3]);
v12 = v9++;
v13 = *(_QWORD *)(a1 + 16) + 24LL * (((unsigned int)(v12 % v10) + i * v10) % *(_DWORD *)(a1 + 12));
*(_OWORD *)v13 = v15;
*(_QWORD *)(v13 + 16) = v16;
}
}
return __readfsqword(0x28u) ^ v17;
}
/challenge/cimg :: handle_2() :: Pseudocode
unsigned __int64 __fastcall handle_2(__int64 a1)
{
unsigned int v1; // ebx
unsigned __int8 *v2; // rax
unsigned __int8 *v3; // rbp
__int64 v4; // rax
__int64 v5; // rcx
int i; // r13d
int v7; // r14d
int v8; // eax
int v9; // ecx
unsigned int v10; // ebx
__int64 v11; // rdx
unsigned __int8 v13; // [rsp+Bh] [rbp-5Dh] BYREF
unsigned __int8 v14; // [rsp+Ch] [rbp-5Ch] BYREF
unsigned __int8 v15; // [rsp+Dh] [rbp-5Bh] BYREF
unsigned __int8 v16; // [rsp+Eh] [rbp-5Ah] BYREF
__int128 v17; // [rsp+Fh] [rbp-59h] BYREF
__int64 v18; // [rsp+1Fh] [rbp-49h]
unsigned __int64 v19; // [rsp+28h] [rbp-40h]

v19 = __readfsqword(0x28u);
read_exact(0LL, &v15, 1LL, "ERROR: Failed to read &base_x!", 0xFFFFFFFFLL);
read_exact(0LL, &v16, 1LL, "ERROR: Failed to read &base_y!", 0xFFFFFFFFLL);
read_exact(0LL, &v13, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &v14, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);
v1 = 4 * v14 * v13;
v2 = (unsigned __int8 *)malloc(4LL * v14 * v13);
if ( !v2 )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto LABEL_7;
}
v3 = v2;
read_exact(0LL, v2, v1, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v4 = 0LL;
while ( v14 * v13 > (int)v4 )
{
v5 = v3[4 * v4++ + 3];
if ( (unsigned __int8)(v5 - 32) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", v5);
LABEL_7:
exit(-1);
}
}
for ( i = 0; v14 > i; ++i )
{
v7 = 0;
while ( v13 > v7 )
{
v8 = v7 + v15;
v9 = v7 + i * v13;
++v7;
v10 = v8 % *(unsigned __int8 *)(a1 + 6) + *(unsigned __int8 *)(a1 + 6) * (i + v16);
__snprintf_chk(
&v17,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
v3[4 * v9],
v3[4 * v9 + 1],
v3[4 * v9 + 2],
v3[4 * v9 + 3]);
v11 = *(_QWORD *)(a1 + 16) + 24LL * (v10 % *(_DWORD *)(a1 + 12));
*(_OWORD *)v11 = v17;
*(_QWORD *)(v11 + 16) = v18;
}
}
return __readfsqword(0x28u) ^ v19;
}
/challenge/cimg :: handle_3() :: Pseudocode
unsigned __int64 __fastcall handle_3(__int64 a1)
{
__int64 v2; // rax
void *v3; // rdi
int v4; // r12d
unsigned __int8 *v5; // rax
unsigned __int8 *v6; // rbx
__int64 v7; // rax
__int64 v8; // rcx
unsigned __int8 v10; // [rsp+5h] [rbp-23h] BYREF
unsigned __int8 v11; // [rsp+6h] [rbp-22h] BYREF
unsigned __int8 v12; // [rsp+7h] [rbp-21h] BYREF
unsigned __int64 v13; // [rsp+8h] [rbp-20h]

v13 = __readfsqword(0x28u);
read_exact(0LL, &v10, 1LL, "ERROR: Failed to read &sprite_id!", 0xFFFFFFFFLL);
read_exact(0LL, &v11, 1LL, "ERROR: Failed to read &width!", 0xFFFFFFFFLL);
read_exact(0LL, &v12, 1LL, "ERROR: Failed to read &height!", 0xFFFFFFFFLL);
v2 = a1 + 16LL * v10;
*(_BYTE *)(v2 + 25) = v11;
v3 = *(void **)(v2 + 32);
*(_BYTE *)(v2 + 24) = v12;
if ( v3 )
free(v3);
v4 = v12 * v11;
v5 = (unsigned __int8 *)malloc(v4);
v6 = v5;
if ( !v5 )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto LABEL_9;
}
read_exact(0LL, v5, (unsigned int)v4, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v7 = 0LL;
while ( v12 * v11 > (int)v7 )
{
v8 = v6[v7++];
if ( (unsigned __int8)(v8 - 32) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", v8);
LABEL_9:
exit(-1);
}
}
*(_QWORD *)(16LL * v10 + a1 + 32) = v6;
return __readfsqword(0x28u) ^ v13;
}
/challenge/cimg :: handle_4() :: Pseudocode
// positive sp value has been detected, the output may be wrong!
unsigned __int64 __fastcall handle_4(__int64 a1)
{
_DWORD *v2; // rdi
__int64 v3; // rcx
__int64 v4; // rdx
char v5; // r10
char v6; // r11
char v7; // bp
__int64 v8; // rdx
int v9; // r12d
int v10; // r8d
int v11; // edi
__int64 v12; // rax
__int64 v13; // r9
int i; // r15d
int j; // r10d
int v16; // r11d
__int64 v17; // rdx
int v18; // r12d
int v19; // ebp
int k; // r13d
int v21; // eax
__int64 v22; // rax
__int64 v23; // rdx
int v24; // r14d
__int64 v25; // rdx
__int64 v26; // rdx
int v28; // [rsp-40h] [rbp-40070h]
int v29; // [rsp-3Ch] [rbp-4006Ch]
_BYTE v30[9]; // [rsp-32h] [rbp-40062h] BYREF
_BYTE v31[41]; // [rsp-29h] [rbp-40059h] BYREF
char v32; // [rsp+0h] [rbp-40030h] BYREF
__int64 v33; // [rsp+1000h] [rbp-3F030h] BYREF
__int128 v34; // [rsp+3FFD7h] [rbp-59h] BYREF
__int64 v35; // [rsp+3FFE7h] [rbp-49h]
unsigned __int64 v36; // [rsp+3FFF0h] [rbp-40h]

while ( &v32 != (char *)(&v33 - 0x8000) )
;
v36 = __readfsqword(0x28u);
read_exact(0LL, v30, 9LL, "ERROR: Failed to read &sprite_render_record!", 0xFFFFFFFFLL);
v2 = v31;
v3 = 0x10000LL;
v4 = v30[0];
v5 = v30[1];
while ( v3 )
{
*v2++ = 0;
--v3;
}
v6 = v30[2];
v7 = v30[3];
v8 = a1 + 16 * v4;
v9 = *(unsigned __int8 *)(v8 + 24);
while ( v9 > (int)v3 )
{
v10 = *(unsigned __int8 *)(v8 + 25);
v11 = 0;
v12 = (unsigned int)(v3 * v10);
while ( v10 > v11 )
{
v13 = *(_QWORD *)(v8 + 32);
v31[4 * v12] = v5;
v31[4 * v12 + 1] = v6;
v31[4 * v12 + 2] = v7;
if ( !v13 )
{
fputs("ERROR: attempted to render uninitialized sprite!\n", stderr);
exit(-1);
}
++v11;
v31[4 * v12 + 3] = *(_BYTE *)(v13 + v12);
++v12;
}
LODWORD(v3) = v3 + 1;
}
for ( i = 0; v30[7] > i; ++i )
{
for ( j = 0; v30[6] > j; ++j )
{
v16 = 0;
v17 = a1 + 16LL * v30[0];
v18 = (unsigned __int8)(v30[4] + j * *(_BYTE *)(v17 + 25));
v19 = (unsigned __int8)(v30[5] + i * *(_BYTE *)(v17 + 24));
while ( *(unsigned __int8 *)(16LL * v30[0] + a1 + 24) > v16 )
{
for ( k = 0; ; ++k )
{
v21 = *(unsigned __int8 *)(16LL * v30[0] + a1 + 25);
if ( v21 <= k )
break;
v22 = k + v16 * v21;
v23 = (unsigned __int8)v31[4 * v22 + 3];
if ( (_BYTE)v23 != v30[8] )
{
v29 = v16;
v24 = *(unsigned __int8 *)(a1 + 6);
v28 = j;
__snprintf_chk(
&v34,
25LL,
1LL,
25LL,
"\x1B[38;2;%03d;%03d;%03dm%c\x1B[0m",
(unsigned __int8)v31[4 * v22],
(unsigned __int8)v31[4 * v22 + 1],
(unsigned __int8)v31[4 * v22 + 2],
v23);
v16 = v29;
j = v28;
v25 = (unsigned int)((k + v18) % v24);
LODWORD(v25) = (unsigned int)(v25 + v19 * v24) % *(_DWORD *)(a1 + 12);
v26 = *(_QWORD *)(a1 + 16) + 24 * v25;
*(_OWORD *)v26 = v34;
*(_QWORD *)(v26 + 16) = v35;
}
}
++v16;
++v19;
}
}
}
return __readfsqword(0x28u) ^ v36;
}
/challenge/cimg :: handle_5() :: Pseudocode
unsigned __int64 __fastcall handle_5(__int64 a1)
{
__int16 v2; // dx
int v3; // eax
FILE *v4; // rsi
const char *v5; // rdi
unsigned int v6; // ebp
void *v7; // rdi
int v8; // r13d
const char *v9; // rax
const char *v10; // rbx
__int64 v11; // rax
__int64 v12; // rcx
char v14[259]; // [rsp+5h] [rbp-133h] BYREF
unsigned __int64 v15; // [rsp+108h] [rbp-30h]

v15 = __readfsqword(0x28u);
memset(v14, 0, sizeof(v14));
read_exact(0LL, v14, 258LL, "ERROR: Failed to read &sprite_load_record!", 0xFFFFFFFFLL);
LOBYTE(v2) = v14[2];
HIBYTE(v2) = v14[1];
*(_WORD *)(a1 + 16LL * (unsigned __int8)v14[0] + 24) = v2;
v3 = open(&v14[3], 0);
v4 = stderr;
v5 = "ERROR: failed to open sprite file\n";
if ( v3 < 0 )
goto LABEL_13;
v6 = v3;
v7 = *(void **)(16LL * (unsigned __int8)v14[0] + a1 + 32);
if ( v7 )
free(v7);
v8 = (unsigned __int8)v14[2] * (unsigned __int8)v14[1];
v9 = (const char *)malloc(v8);
v10 = v9;
if ( !v9 )
{
puts("ERROR: Failed to allocate memory for the image data!");
goto LABEL_6;
}
read_exact(v6, v9, (unsigned int)v8, "ERROR: Failed to read data!", 0xFFFFFFFFLL);
v11 = 0LL;
while ( (unsigned __int8)v14[2] * (unsigned __int8)v14[1] > (int)v11 )
{
v12 = (unsigned __int8)v10[v11++];
if ( (unsigned __int8)(v12 - 32) > 0x5Eu )
{
__fprintf_chk(stderr, 1LL, "ERROR: Invalid character 0x%x in the image data!\n", v12);
goto LABEL_6;
}
}
if ( !strncmp(v10, "pwn.college{", 0xCuLL) )
{
v4 = stderr;
v5 = "ERROR: shenanigans detected!!!!!";
LABEL_13:
fputs(v5, v4);
LABEL_6:
exit(-1);
}
*(_QWORD *)(16LL * (unsigned __int8)v14[0] + a1 + 32) = v10;
close(v6);
return __readfsqword(0x28u) ^ v15;
}
/challenge/cimg :: handle_6() :: Pseudocode
unsigned __int64 __fastcall handle_6(__int64 a1)
{
__uid_t v1; // eax
char v3; // [rsp+7h] [rbp-11h] BYREF
unsigned __int64 v4; // [rsp+8h] [rbp-10h]

v4 = __readfsqword(0x28u);
read_exact(0LL, &v3, 1LL, "ERROR: Failed to read &clear!", 0xFFFFFFFFLL);
v1 = geteuid();
setuid(v1);
system("clear");
display(a1, 0LL);
return __readfsqword(0x28u) ^ v4;
}
/challenge/cimg :: handle_7() :: Pseudocode
unsigned __int64 handle_7()
{
unsigned int v1; // [rsp+4h] [rbp-24h] BYREF
struct timespec requested_time; // [rsp+8h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-10h]

v3 = __readfsqword(0x28u);
read_exact(0LL, &v1, 4LL, "ERROR: Failed to read &milliseconds!", 0xFFFFFFFFLL);
requested_time.tv_sec = v1 / 0x3E8;
requested_time.tv_nsec = 1000000 * (v1 % 0x3E8);
nanosleep(&requested_time, 0LL);
return __readfsqword(0x28u) ^ v3;
}

handle_7() is straightforward, it reads 4 bytes as milliseconds and calls nanosleep(), implementing a simple delay. handle_6() is more interesting, it calls system("clear") after setting the UID to the effective UID of the binary. Since system() resolves clear via PATH, this is a classic PATH hijacking vulnerability.

handle_5() also gained a new check compared to the previous challenge:

/challenge/cimg :: habdle_5() :: Pseudocode
# ---- snip ----

if ( !strncmp(v10, "pwn.college{", 0xCuLL) )
{
v4 = stderr;
v5 = "ERROR: shenanigans detected!!!!!";
LABEL_13:
fputs(v5, v4);
LABEL_6:
exit(-1);
}

# ---- snip ----

This prevents directly loading /flag as a sprite.

Exploit

The two main obstacles are:

  1. handle_5() blocks loading /flag directly by checking if the sprite data starts with pwn.college{
  2. handle_6() gives us a PATH hijacking primitive via system("clear")

We can chain these together. First, we create a fake clear script that runs with the binary's elevated privileges and copies the flag, but XORs every byte with 1 before saving it, so it no longer starts with pwn.college{ and passes the shenanigans check. We then prepend our fake clear directory to PATH so handle_6() picks it up.

hacker@reverse-engineering~unsafe-animations:~$ mkdir -p /tmp/pwn
cat > /tmp/pwn/clear << 'EOF'
#!/bin/sh
python3 -c "
data = open('/flag','rb').read()
out = bytes(b ^ 1 for b in data)
open('/tmp/flag_copy','wb').write(out)
"
chmod 777 /tmp/flag_copy
EOF
chmod 777 /tmp/pwn/clear
export PATH=/tmp/pwn:$PATH

With the fake clear in place, we build a .cimg that fires directive 6 first (triggering the PATH hijack and creating /tmp/flag_copy), then directive 5 (loading the XORed flag as a sprite), and finally directive 4 (rendering it to the screen).

~/script.py
import struct

MAGIC = 0x474D4963
VERSION = 4
FB_W, FB_H = 80, 80

SPRITE_ID = 0
FLAG_COPY = b"/tmp/flag_copy\x00"

def directive_6():
return struct.pack("<H", 6) + b"\x00"

def directive_5(sprite_id, w, h, path):
rec = bytes([sprite_id, h, w])
rec += path.ljust(255, b"\x00")
return struct.pack("<H", 5) + rec

def directive_4(sprite_id, x, y):
return struct.pack("<HBBBBBBBBB", 4, sprite_id, 255, 255, 255, x, y, 1, 1, 0)

directives = [
directive_6(),
directive_5(SPRITE_ID, 1, 59, FLAG_COPY),
directive_4(SPRITE_ID, 0, 0),
]

header = struct.pack("<I H B B I", MAGIC, VERSION, FB_W, FB_H, len(directives))
payload = header + b"".join(directives)

with open("solution.cimg", "wb") as f:
f.write(payload)

print("[+] Run: /challenge/cimg solution.cimg")
hacker@reverse-engineering~unsafe-animations:~$ python ~/script.py
[+] Run: /challenge/cimg solution.cimg
hacker@reverse-engineering~unsafe-animations:~$ /challenge/cimg ~/solution.cimg 

# ---- snip ----

qvo/bnmmdfdzXrl2jT{y36pLNqjv[c1Ysv@kKCS/PY2D{LvDEM5HUL1D{V|
hacker@reverse-engineering~unsafe-animations:~$ python3 -c "
data = 'qvo/bnmmdfdzXrl2jT{y36pLNqjv[c1Ysv@kKCS/PY2D{LvDEM5HUL1D{V|'
print(bytes(ord(b) ^ 1 for b in data).decode())
"
pwn.college{Ysm3kUzx27qMOpkwZb0XrwAjJBR.QX3EzMwEDL4ITM0EzW}

 

Animations

Binary Analysis

The animation is purely directives 2, 6, and 7:

  • Directive 2: renders a chunk of pixels at specific coordinates (a frame of the animation)
  • Directive 6: clears the screen and displays the framebuffer
  • Directive 7: sleeps for some milliseconds

So the pattern is: render a frame with handle_2(), display it with handle_6(), wait with handle_7(), repeat. The reason it renders blank is likely that directive 6 in this challenge just prints \x1B[H\x1B[2J (clear screen) and calls display() — but the terminal might be clearing the output before we can see it, or the animation plays too fast.

Exploit

Let's patch the flag.cimg file to replace all sleep durations with 0.

python3 -c "
import struct

with open('/challenge/flag.cimg', 'rb') as f:
data = bytearray(f.read())

offset = 12
for _ in range(11319):
code = struct.unpack_from('<H', data, offset)[0]
offset += 2
if code == 7:
struct.pack_into('<I', data, offset, 0) # set sleep to 0ms
offset += 4
elif code == 6:
offset += 1
elif code == 2:
base_x, base_y, w, h = data[offset], data[offset+1], data[offset+2], data[offset+3]
offset += 4 + 4 * w * h

with open('/tmp/flag_patched.cimg', 'wb') as f:
f.write(data)

print('Done')
"
/challenge/cimg /tmp/flag_patched.cimg

# ---- snip ----
""# ""#
mmmm m m m mm mmm mmm # # mmm mmmm mmm
#" "# "m m m" #" # #" " #" "# # # #" # #" "# #" #
# # #m#m# # # # # # # # #"""" # # #""""
##m#" # # # # # "#mm" "#m#" "mm "mm "#mm" "#m"# "#mm"
# m #
" ""

m"" mm m mmmmm mmm mmmmmm mmmmm ""# m m
# m"# # # # m" " # m m # "# # # m"
mm" #" # # #mmmm" # mm #mmmmm # # #mmm#" # #m#
# #mmm#m # # # """ # # # # # # # # #m
# # #mmmmm #mmmm" "mmm" #mmmmm "mm"# # "mm # "m
""


mmmm m m mmmm mmm " mmm m m
#" "m ## ## mmmmm #" "m # mmm m" " # # mmm mmmm mmm
#m m# # ## # m" #m m# # # # mm ## " # #" "# #" "#
""" # # "" # m" """ # # # # # m""m m"""# # # # #
"mmm" # # #mmmm "mmm" "mmm" # "mmm" m" "m "mm"# "#m"# "#m#"
# m #
"" ""

m m mmmmm mmmmm ""# mmm mm mmmm m m mm mmmmmm
# # # "# # # # m" " m"# m" "m # # m"# #
## #mmm#" #mmmm" # # mm #" # # # ## #" # #mmmmm
m""m # # # # # # #mmm#m # # m""m #mmm#m #
m" "m # #mmmm" "mm "mmm" # # #mm#" m" "m # #mmmmm
#


m m mmmmmm mmmm m mm mmmmm mmmmmmm m m mmmm
mmmmm ## ##m m # # "m # m"# # # ## ## m" "m
m" # ## #"m m m" #mmmmm # # # #" # # # # ## # # m #
m" # "" # #m#m# # # # # #mmm#m # # # "" # # #
#mmmm # # # # #mmmmm #mmm" #mmmmm # mm#mm # # # #mm#



mmmmmm m m ""m
# mmmmm # # # #
#mmmmm m" " #"# # "mm
# m" ## ##" #
#mmmmm #mmmm # # #
""

 

Interoperability: Patching Data

Binary Analysis

hacker@reverse-engineering~interoperability-patching-data:~$ ls /challenge/
DESCRIPTION.md cimg cimg.c quest.py

Let's see what the /challenge/quest.py script does.

/challenge/quest.py
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import termios
import random
import struct
import string
import sys
import os


class CIMG_NORMAL:
MAGIC = b"cIMG"
RENDER_FRAME = struct.pack("<H", 1)
RENDER_PATCH = struct.pack("<H", 2)
CREATE_SPRITE = struct.pack("<H", 3)
RENDER_SPRITE = struct.pack("<H", 4)
LOAD_SPRITE = struct.pack("<H", 5)
FLUSH = struct.pack("<H", 6)
SLEEP = struct.pack("<H", 7)

class CIMG_1337(CIMG_NORMAL):
MAGIC = b"CNNR"

class GraphicsEngine:
def __init__(self, width, height, cimg_version=4, cimg_ops=CIMG_1337):
self.num_sprites = 0
self.ops = cimg_ops
self.width = width
self.height = height
self.output = os.fdopen(1, 'wb', buffering=0)

self.output.write(
self.ops.MAGIC +
struct.pack("<H", cimg_version) +
bytes([width, height]) +
b"\xff\xff\xff\xff"
)

def render_frame_monochrome(self, lines, r=0xff, g=0xc6, b=0x27):
self.output.write(
self.ops.RENDER_FRAME +
b"".join(bytes([r, g, b, c]) for c in b"".join(lines))
)

def render_patch_monochrome(self, lines, x, y, r=0xff, g=0xc6, b=0x27):
assert all(b >= 20 and b <= 0x7e for b in b"".join(lines))
self.output.write(
self.ops.RENDER_PATCH +
bytes([x, y, len(lines[0]), len(lines)]) +
b"".join(bytes([r, g, b, c]) for c in b"".join(lines))
)

def create_sprite(self, lines, num=None):
if num is None:
num = self.num_sprites
self.num_sprites += 1

self.output.write(
self.ops.CREATE_SPRITE +
bytes([num, len(lines[0]), len(lines)]) +
b"".join(lines)
)
return num

def render_sprite(self, num, x, y, tile_x=1, tile_y=1, r=0x8c, g=0x1d, b=0x40, t=" "):
self.output.write(
self.ops.RENDER_SPRITE + bytes([num, r, g, b, x, y, tile_x, tile_y, ord(t)])
)

def flush(self, clear=True):
self.output.write(self.ops.FLUSH + bytes([clear]))

def sleep(self, ms):
self.output.write(self.ops.SLEEP + struct.pack("<I", ms))

def blank(self):
self.render_patch_monochrome([ b" "*self.width ]*self.height, 1, 1)
self.flush()

def animate_text(self, text, x, y, r=None, g=None, b=None, interval=20):
for i,c in enumerate(text):
self.render_patch_monochrome(
[bytes([ord(c)])], x+i, y,
r=random.randrange(128, 256) if r is None else r,
g=random.randrange(128, 256) if g is None else g,
b=random.randrange(128, 256) if b is None else b
)
self.flush()
self.sleep(interval)

class InputEngine:
# adapted from https://github.com/magmax/python-readchar/blob/master/readchar/_posix_read.py
@staticmethod
def readchar():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
term = termios.tcgetattr(fd)
try:
term[3] &= ~(termios.ICANON | termios.ECHO | termios.IGNBRK | termios.BRKINT)
termios.tcsetattr(fd, termios.TCSAFLUSH, term)
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

def game():
w = 70
h = 20
x = random.randrange(w)
y = random.randrange(h)

victory = False

kb = InputEngine()
screen = GraphicsEngine(w, h, cimg_ops=CIMG_NORMAL if "NOFLAG" in sys.argv else CIMG_1337)
our_sprite = screen.create_sprite([ b"\\o/", b" ^ "])

screen.render_frame_monochrome([ b"#"*(w) ]*(h), r=255, g=255, b=255)
screen.render_patch_monochrome([ b" "*(w-2) ]*(h-2), 1, 1)
screen.flush()

screen.animate_text("WELCOME TO THE EPIC QUEST FOR THE FLAG", 4, 4)
screen.animate_text("INSTRUCTIONS:", 4, 10)
screen.animate_text("- w: UP", 4, 11)
screen.animate_text("- a: LEFT", 4, 12)
screen.animate_text("- s: DOWN", 4, 13)
screen.animate_text("- d: RIGHT", 4, 14)
screen.animate_text("- q: QUIT", 4, 15)
screen.animate_text("- l: LOOK", 4, 16)
screen.animate_text("YOUR GOAL: UNCOVER THE FLAG", 4, 17)
screen.animate_text("PRESS ANY KEY TO BEGIN", 8, 18)
if kb.readchar() in ("q", "\x03"):
return

screen.blank()

try:
if "NOFLAG" in sys.argv:
flag = b"TEST"
else:
flag = open("/flag", "rb").read().strip()
except FileNotFoundError:
flag = b"ERROR: /flag NOT FOUND"
except PermissionError:
flag = b"ERROR: /flag permission denied"

hidden_bytes = [ bytes([b]) for b in flag ][::-1]

hidden_x = random.randrange(w)
hidden_y = random.randrange(h)
revealed_bytes = [ ]

bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
while bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)

key = ""
while True:
# quit on q or ctrl-c
if key == "q" or key == "\x03":
break

# move
if key == "w": y = (y-1)%h
if key == "a": x = (x-1)%w
if key == "s": y = (y+1)%h
if key == "d": x = (x+1)%w

# check bomb
if bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
screen.blank()
screen.animate_text("~~~~~ BOOOOOOOOM ~~~~~~", x, y)
break

# uncover flag
if hidden_bytes and key == "l" and hidden_x in (x, x+1, x+2) and hidden_y in (y, y+1):
revealed_bytes.append([
hidden_x, hidden_y,
random.randrange(128, 256), random.randrange(128, 256), random.randrange(128, 256),
hidden_bytes.pop()
])
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
while bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
prev_hidden_x = hidden_x
prev_hidden_y = hidden_y
while hidden_x == prev_hidden_x and hidden_y == prev_hidden_y:
hidden_x = (bomb_x+random.randrange(w-1))%w
hidden_y = (bomb_y+random.randrange(h-1))%h

# render everyone
screen.blank()
correct_bytes = ''
for rx,ry,r,g,b,c in revealed_bytes:
screen.render_patch_monochrome([c], rx, ry, r=r, g=g, b=b)
correct_bytes += str(c.decode())
if hidden_bytes:
screen.render_patch_monochrome(
[b"?"], hidden_x, hidden_y,
r=random.randrange(256), g=random.randrange(256), b=random.randrange(256)
)
else:
try:
while True:
screen.animate_text("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", 10, hidden_y)
screen.animate_text("!!! CONGRATULATIONS, YOU DID IT !!!", 10, hidden_y + 1)
screen.animate_text(correct_bytes, 10, hidden_y + 2)
screen.animate_text("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", 10, hidden_y + 1)
except KeyboardInterrupt:
print(flag.decode(), file=sys.stderr) # Print decoded flag to stderr
break

screen.render_patch_monochrome([b"B"], bomb_x, bomb_y)
screen.render_sprite(our_sprite, x, y)
screen.flush()

key = kb.readchar()

if __name__ == "__main__":
game()

Looking at /challenge/quest.py, the game uses a custom graphics engine class CIMG_1337 which is identical to the standard CIMG_NORMAL except for one difference, the magic number:

/challenge/quest.py
# ---- snip ----

class CIMG_1337(CIMG_NORMAL):
MAGIC = b"CNNR"

# ---- snip ----

The standard /challenge/cimg binary rejects any input that doesn't start with cIMG, so piping quest.py output directly into it fails.

Exploit

We have to find the cIMG magic number in the binary and replace it with CNNR:

hacker@reverse-engineering~interoperability-patching-data:~$ python3 -c "
data = bytearray(open('/challenge/cimg', 'rb').read())
offset = data.index(b'cIMG')
data[offset:offset+4] = b'CNNR'
open('/home/hacker/patched-cimg', 'wb').write(data)
print('Patched!')
"
chmod +x ~/patched-cimg
Patched!

With the patched binary, we pipe quest.py output into it .

hacker@reverse-engineering~patching-code:~$ /challenge/quest.py | /home/hacker/patched-cimg
pwn.college{IzzoGSOEUVXhnoJT1rTtL_6hFd4.QXyIzMwEDL4ITM0EzW}

 

Patching Code

Binary Analysis

/challenge/quest.py
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import termios
import random
import struct
import string
import sys
import os


class CIMG_NORMAL:
MAGIC = b"cIMG"
RENDER_FRAME = struct.pack("<H", 1)
RENDER_PATCH = struct.pack("<H", 2)
CREATE_SPRITE = struct.pack("<H", 3)
RENDER_SPRITE = struct.pack("<H", 4)
LOAD_SPRITE = struct.pack("<H", 5)
FLUSH = struct.pack("<H", 6)
SLEEP = struct.pack("<H", 7)

class CIMG_1337:
MAGIC = b"CNNR"
RENDER_FRAME = struct.pack("<H", 7)
RENDER_PATCH = struct.pack("<H", 6)
CREATE_SPRITE = struct.pack("<H", 5)
RENDER_SPRITE = struct.pack("<H", 4)
LOAD_SPRITE = struct.pack("<H", 3)
FLUSH = struct.pack("<H", 2)
SLEEP = struct.pack("<H", 1)

class GraphicsEngine:
def __init__(self, width, height, cimg_version=4, cimg_ops=CIMG_1337):
self.num_sprites = 0
self.ops = cimg_ops
self.width = width
self.height = height
self.output = os.fdopen(1, 'wb', buffering=0)

self.output.write(
self.ops.MAGIC +
struct.pack("<H", cimg_version) +
bytes([width, height]) +
b"\xff\xff\xff\xff"
)

def render_frame_monochrome(self, lines, r=0xff, g=0xc6, b=0x27):
self.output.write(
self.ops.RENDER_FRAME +
b"".join(bytes([r, g, b, c]) for c in b"".join(lines))
)

def render_patch_monochrome(self, lines, x, y, r=0xff, g=0xc6, b=0x27):
assert all(b >= 20 and b <= 0x7e for b in b"".join(lines))
self.output.write(
self.ops.RENDER_PATCH +
bytes([x, y, len(lines[0]), len(lines)]) +
b"".join(bytes([r, g, b, c]) for c in b"".join(lines))
)

def create_sprite(self, lines, num=None):
if num is None:
num = self.num_sprites
self.num_sprites += 1

self.output.write(
self.ops.CREATE_SPRITE +
bytes([num, len(lines[0]), len(lines)]) +
b"".join(lines)
)
return num

def render_sprite(self, num, x, y, tile_x=1, tile_y=1, r=0x8c, g=0x1d, b=0x40, t=" "):
self.output.write(
self.ops.RENDER_SPRITE + bytes([num, r, g, b, x, y, tile_x, tile_y, ord(t)])
)

def flush(self, clear=True):
self.output.write(self.ops.FLUSH + bytes([clear]))

def sleep(self, ms):
self.output.write(self.ops.SLEEP + struct.pack("<I", ms))

def blank(self):
self.render_patch_monochrome([ b" "*self.width ]*self.height, 1, 1)
self.flush()

def animate_text(self, text, x, y, r=None, g=None, b=None, interval=20):
for i,c in enumerate(text):
self.render_patch_monochrome(
[bytes([ord(c)])], x+i, y,
r=random.randrange(128, 256) if r is None else r,
g=random.randrange(128, 256) if g is None else g,
b=random.randrange(128, 256) if b is None else b
)
self.flush()
self.sleep(interval)

class InputEngine:
# adapted from https://github.com/magmax/python-readchar/blob/master/readchar/_posix_read.py
@staticmethod
def readchar():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
term = termios.tcgetattr(fd)
try:
term[3] &= ~(termios.ICANON | termios.ECHO | termios.IGNBRK | termios.BRKINT)
termios.tcsetattr(fd, termios.TCSAFLUSH, term)
return sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

def game():
w = 70
h = 20
x = random.randrange(w)
y = random.randrange(h)

victory = False

kb = InputEngine()
screen = GraphicsEngine(w, h, cimg_ops=CIMG_NORMAL if "NOFLAG" in sys.argv else CIMG_1337)
our_sprite = screen.create_sprite([ b"\\o/", b" ^ "])

screen.render_frame_monochrome([ b"#"*(w) ]*(h), r=255, g=255, b=255)
screen.render_patch_monochrome([ b" "*(w-2) ]*(h-2), 1, 1)
screen.flush()

screen.animate_text("WELCOME TO THE EPIC QUEST FOR THE FLAG", 4, 4)
screen.animate_text("INSTRUCTIONS:", 4, 10)
screen.animate_text("- w: UP", 4, 11)
screen.animate_text("- a: LEFT", 4, 12)
screen.animate_text("- s: DOWN", 4, 13)
screen.animate_text("- d: RIGHT", 4, 14)
screen.animate_text("- q: QUIT", 4, 15)
screen.animate_text("- l: LOOK", 4, 16)
screen.animate_text("YOUR GOAL: UNCOVER THE FLAG", 4, 17)
screen.animate_text("PRESS ANY KEY TO BEGIN", 8, 18)
if kb.readchar() in ("q", "\x03"):
return

screen.blank()

try:
if "NOFLAG" in sys.argv:
flag = b"TEST"
else:
flag = open("/flag", "rb").read().strip()
except FileNotFoundError:
flag = b"ERROR: /flag NOT FOUND"
except PermissionError:
flag = b"ERROR: /flag permission denied"

hidden_bytes = [ bytes([b]) for b in flag ][::-1]

hidden_x = random.randrange(w)
hidden_y = random.randrange(h)
revealed_bytes = [ ]

bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
while bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)

key = ""
while True:
# quit on q or ctrl-c
if key == "q" or key == "\x03":
break

# move
if key == "w": y = (y-1)%h
if key == "a": x = (x-1)%w
if key == "s": y = (y+1)%h
if key == "d": x = (x+1)%w

# check bomb
if bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
screen.blank()
screen.animate_text("~~~~~ BOOOOOOOOM ~~~~~~", x, y)
break

# uncover flag
if hidden_bytes and key == "l" and hidden_x in (x, x+1, x+2) and hidden_y in (y, y+1):
revealed_bytes.append([
hidden_x, hidden_y,
random.randrange(128, 256), random.randrange(128, 256), random.randrange(128, 256),
hidden_bytes.pop()
])
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
while bomb_x in (x, x+1, x+2) and bomb_y in (y, y+1):
bomb_x = random.randrange(w)
bomb_y = random.randrange(h)
prev_hidden_x = hidden_x
prev_hidden_y = hidden_y
while hidden_x == prev_hidden_x and hidden_y == prev_hidden_y:
hidden_x = (bomb_x+random.randrange(w-1))%w
hidden_y = (bomb_y+random.randrange(h-1))%h

# render everyone
screen.blank()
correct_bytes = ''
for rx,ry,r,g,b,c in revealed_bytes:
screen.render_patch_monochrome([c], rx, ry, r=r, g=g, b=b)
correct_bytes += str(c.decode())
if hidden_bytes:
screen.render_patch_monochrome(
[b"?"], hidden_x, hidden_y,
r=random.randrange(256), g=random.randrange(256), b=random.randrange(256)
)
else:
try:
while True:
screen.animate_text("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", 10, hidden_y)
screen.animate_text("!!! CONGRATULATIONS, YOU DID IT !!!", 10, hidden_y + 1)
screen.animate_text(correct_bytes, 10, hidden_y + 2)
screen.animate_text("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", 10, hidden_y + 1)
except KeyboardInterrupt:
print(flag.decode(), file=sys.stderr) # Print decoded flag to stderr
break

screen.render_patch_monochrome([b"B"], bomb_x, bomb_y)
screen.render_sprite(our_sprite, x, y)
screen.flush()

key = kb.readchar()

if __name__ == "__main__":
game()

Looking at /challenge/quest.py, this time CIMG_1337 differs from CIMG_NORMAL in two ways: the magic number and the directive codes are completely reversed.

/challenge/quest.py
class CIMG_NORMAL:
MAGIC = b"cIMG"
RENDER_FRAME = struct.pack("<H", 1)
RENDER_PATCH = struct.pack("<H", 2)
CREATE_SPRITE = struct.pack("<H", 3)
RENDER_SPRITE = struct.pack("<H", 4)
LOAD_SPRITE = struct.pack("<H", 5)
FLUSH = struct.pack("<H", 6)
SLEEP = struct.pack("<H", 7)

class CIMG_1337:
MAGIC = b"CNNR"
RENDER_FRAME = struct.pack("<H", 7)
RENDER_PATCH = struct.pack("<H", 6)
CREATE_SPRITE = struct.pack("<H", 5)
RENDER_SPRITE = struct.pack("<H", 4)
LOAD_SPRITE = struct.pack("<H", 3)
FLUSH = struct.pack("<H", 2)
SLEEP = struct.pack("<H", 1)

So directive codes 1↔7, 2↔6, 3↔5 are swapped, while directive 4 stays the same.

Exploit

We need two patches:

  1. Magic number** — patch the string cIMG to CNNR at offset 0x32f9.
  2. Directive code comparisons** — in main(), the binary uses a series of cmp $X,%cx instructions to dispatch to the correct handler. We need to swap the constants to match what CIMG_1337 sends:
4013ea: cmp $0x1,%cx  →  cmp $0x7,%cx
4013fa: cmp $0x2,%cx → cmp $0x6,%cx
40140a: cmp $0x3,%cx → cmp $0x5,%cx
40141a: cmp $0x4,%cx → stays $0x4
40142a: cmp $0x5,%cx → cmp $0x3,%cx
40143d: cmp $0x6,%cx → cmp $0x2,%cx
401450: cmp $0x7,%cx → cmp $0x1,%cx

The following patch script finds all 66 83 f9 XX patterns (the encoding of cmp $X,%cx) and swaps the constants.

~/patch.py
import re

data = bytearray(open('/challenge/cimg', 'rb').read())

# Patch magic number
offset = data.index(b'cIMG')
data[offset:offset+4] = b'CNNR'

# Swap directive code comparisons
pattern = re.compile(b'\x66\x83\xf9(.)', re.DOTALL)
matches = [(m.start(), m.group(1)[0]) for m in pattern.finditer(data)]

swap = {1: 7, 2: 6, 3: 5, 5: 3, 6: 2, 7: 1}

for pos, val in matches:
if val in swap:
data[pos+3] = swap[val]

open('/home/hacker/patched-cimg', 'wb').write(data)

With the patched binary, we pipe quest.py output into it.

hacker@reverse-engineering~patching-code:~$ /challenge/quest.py | /home/hacker/patched-cimg

After finishing the game, we get the flag.

pwn.college{Upj8Wp5DVOltDRK_958N0rhSiUg.QXzIzMwEDL4ITM0EzW}

 

Patching Control Flow

Binary Analysis

Looking at /challenge/quest.py, CIMG_1337 is identical to the previous challenge: magic number CNNR and reversed directive codes:

class CIMG_1337:
MAGIC = b"CNNR"
RENDER_FRAME = struct.pack("<H", 7)
RENDER_PATCH = struct.pack("<H", 6)
CREATE_SPRITE = struct.pack("<H", 5)
RENDER_SPRITE = struct.pack("<H", 4)
LOAD_SPRITE = struct.pack("<H", 3)
FLUSH = struct.pack("<H", 2)
SLEEP = struct.pack("<H", 1)

However, looking at the disassembly of main(), the directive dispatch is implemented differently this time. Instead of a series of cmp $X,%cx instructions, the binary uses a jump table:

4013ef:	lea    -0x1(%rcx),%eax    # eax = directive - 1
4013f2: cmp $0x6,%ax # if > 6, invalid
4013f6: ja 401439 # jump to error
4013fe: movslq (%rbx,%rax,4),%rax # look up jump table
401405: notrack jmp *%rax # jump to handler

It subtracts 1 from the directive code and uses it as an index into a jump table at 0x403384. Inspecting the table:

index 0 (directive 1): -0x1f7c  →  handle_1
index 1 (directive 2): -0x1f75 → handle_2
index 2 (directive 3): -0x1f6e → handle_3
index 3 (directive 4): -0x1f67 → handle_4
index 4 (directive 5): -0x1f60 → handle_5
index 5 (directive 6): -0x1f59 → handle_6
index 6 (directive 7): -0x1f52 → handle_7

Exploit

We need two patches:

  1. Magic number: patch cIMG --> CNNR
  2. Jump table: reverse the entries so that directive 7 maps to handle_1(), directive 6 maps to handle_2(), etc., while directive 4 stays in place
~/patch.py
import struct

data = bytearray(open('/challenge/cimg', 'rb').read())

# Patch magic number
offset = data.index(b'cIMG')
data[offset:offset+4] = b'CNNR'

# Reverse the jump table entries at file offset 0x3384
jt_offset = 0x3384
entries = []
for i in range(7):
val = struct.unpack_from('<i', data, jt_offset + i*4)[0]
entries.append(val)

swapped = [
entries[6], # directive 1 -> handle_1
entries[5], # directive 2 -> handle_2
entries[4], # directive 3 -> handle_3
entries[3], # directive 4 stays
entries[2], # directive 5 -> handle_5
entries[1], # directive 6 -> handle_6
entries[0], # directive 7 -> handle_7
]

for i, val in enumerate(swapped):
struct.pack_into('<i', data, jt_offset + i*4, val)

open('/home/hacker/patched-cimg', 'wb').write(data)
print('Done!')

We can now pass the output of /challenge/quest.py to the ~/patched-cimg renderer, and play the game. Once the game is won, we get the flag.

hacker@reverse-engineering~patching-code:~$ /challenge/quest.py | /home/hacker/patched-cimg

After finishing the game, we get the flag.

pwn.college{Upj8Wp5DVwk.PJK_958N0rhSiUg.QXzIzMwEDL4IkjIHzW}