PwnCollege - RE - A Basic cIMG

A Basic cIMG (Python)

ANSI escape sequences are a standard for in-band signaling to control cursor location, color, font styling, and other options on video text terminals and terminal emulators. Certain sequences of bytes, most starting with an ASCII escape character and a bracket character, are embedded into text. The terminal interprets these sequences as commands, rather than text to display verbatim.

Wikipedia

控制光标位置、颜色、字体样式的带内信号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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:6], "little") == 2, "ERROR: Invalid version!"

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

height = int.from_bytes(header[8:10], "little")
assert height == 20, "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!"

ansii_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(ansii_escape(pixel) for pixel in pixels[row_start : row_start + width])
+ ansii_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 != 1020:
return

asu_maroon = (0x8C, 0x1D, 0x40) # #8C1D40
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)

脚本优先从命令行参数 (sys.argv) 读取 .cimg 文件;如果不传参,就从标准输入 sys.stdin.buffer 读管道流。

  • 它读取了 51 * 20 * 4 = 4080 字节的 payload 数据。
  • 每 4 个字节被打包成一个 Pixel 结构( R, G, B 值和 ASCII 码)。
  • 确保 ASCII 值都在可打印字符范围内 (0x200x7E)。
  • 利用 ansii_escape,把这些像素转换成终端带色字符,拼接成 framebuffer 字符串并 print

获取 Flag 的条件

  • 条件一: nonspace_count != 1020 则退出。由于画面一共 51 * 20 = 1020 个像素,这意味着你的图片里不能包含任何一个空格 (0x20),必须全部被非空格的可打印字符填满。
  • 条件二: 所有像素的颜色必须完全是 ASU Maroon (亚利桑那州立大学紫红色),即 #8C1D40(红:0x8C, 绿:0x1D, 蓝:0x40)。

ansii_escape (ansi? mispelled?) 用来生成 24-bit 真彩色 (Truecolor) 的终端输出字符串。

  • \x1b[:ESC 控制符(Escape character,ASCII 码是 27 或 0x1B),标志着转义序列的开始。
  • 38;2;:设置前景(文字)颜色,并且使用 24-bit 的 RGB 模式。
  • {pixel.r:03};{pixel.g:03};{pixel.b:03}m:把解析出的像素 R、G、B 值填进去(不足三位补零),m 代表颜色/格式设置结束。
  • {chr(pixel.ascii)}:打印到屏幕上的字符。
  • \x1b[0m:重置符。打印完这个字符后,立刻清除颜色设置,防止颜色溢出污染 CLI 界面。

try try?

1
printf "\x1b[31m\n"

not work in fish shell btw

Color codes

Most terminals support 8 and 16 colors, as well as 256 (8-bit) colors. These colors are set by the user, but have commonly defined meanings.

8-16 Colors

Color Name Foreground Color Code Background Color Code
Black 30 40
Red 31 41
Green 32 42
Yellow 33 43
Blue 34 44
Magenta 35 45
Cyan 36 46
White 37 47
Default 39 49

Most terminals, apart from the basic set of 8 colors, also support the “bright” or “bold” colors. These have their own set of codes, mirroring the normal colors, but with an additional ;1 in their codes:

1
2
3
4
# Set style to bold, red foreground.
\x1b[1;31mHello
# Set style to dimmed white foreground with red background.
\x1b[2;37;41mWorld

Terminals that support the aixterm specification provides bright versions of the ISO colors, without the need to use the bold modifier:

Color Name Foreground Color Code Background Color Code
Bright Black 90 100
Bright Red 91 101
Bright Green 92 102
Bright Yellow 93 103
Bright Blue 94 104
Bright Magenta 95 105
Bright Cyan 96 106
Bright White 97 107

256 Colors

The following escape codes tells the terminal to use the given color ID:

ESC Code Sequence Description
ESC[38;5;{ID}m Set foreground color.
ESC[48;5;{ID}m Set background color.

Where {ID} should be replaced with the color index from 0 to 255 of the following color table:

256 Color table
  • 0-7: standard colors (as in ESC [ 30–37 m)
  • 8–15: high intensity colors (as in ESC [ 90–97 m)
  • 16-231: 6 × 6 × 6 cube (216 colors): 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) > Some emulators interpret these steps as linear increments (256 / 24) on all three channels while others may explicitly define these values.
  • 232-255: grayscale from dark to light in 24 steps.

RGB Colors

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

These escape sequences are usually not well documented.

ESC Code Sequence Description
ESC[38;2;{r};{g};{b}m Set foreground color as RGB.
ESC[48;2;{r};{g};{b}m Set background color as RGB.

Note that ;38 and ;48 corresponds to the 16 color sequence and is interpreted by the terminal to set the foreground and background color respectively. Where as ;2 and ;5 sets the color format.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *

header = b"cIMG"
version = 2
width = 51
height = 20
total_pixels = width * height

file_header = struct.pack("<4sHHH", header,version, width, height)

ascii_char = ord('A')

r, g, b = 0x8C, 0x1D, 0x40
single_pixel = struct.pack("<BBBB", r, g, b, ascii_char)

pixel_data = single_pixel * total_pixels

payload = file_header + pixel_data

file = open("payload.cimg", "wb")
file.write(payload)
file.close()

p = process(["/challenge/cimg", "payload.cimg"], stdin=process.PTY, stdout=process.PTY)
print(p.recvall())

pwn.college{**********************************************}

A Basic cIMG (C)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// ...
struct cimg_header
{
char magic_number[4];
uint64_t version;
uint16_t width;
uint16_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("");
}

}
// ...
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 != 46)
{
puts("ERROR: Incorrect width!");
exit(-1);
}

if (cimg.header.height != 23)
{
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 != 1058) won = 0;

if (won) win();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from pwn import *
from pwn import process

header = b"cIMG"
version = 2
width = 46
height = 23
total_pixels = width * height

file_header = struct.pack("<4sQHH", header,version, width, height)

ascii_char = ord('A')

r, g, b = 0x8C, 0x1D, 0x40
single_pixel = struct.pack("<BBBB", r, g, b, ascii_char)

pixel_data = single_pixel * total_pixels

payload = file_header + pixel_data

file = open("payload.cimg", "wb")
file.write(payload)
file.close()

p = process(["/challenge/cimg", "payload.cimg"], stdin=process.PTY, stdout=process.PTY)
print(p.recvall())

pwn.crllege{**********************************************}

A Basic cIMG (x86)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
      │   0x004012f5      4889ee         mov rsi, rbp
│ 0x004012f8 ba0b000000 mov edx, 0xb ; 11
│ 0x004012fd e839030000 call sym.read_exact ;[3]
│ 0x00401302 807c240d63 cmp byte [rsp + 0xd], 0x63 ; 'c'
┌──< 0x00401307 7515 jne 0x40131e
││ 0x00401309 807c240e49 cmp byte [rsp + 0xe], 0x49 ; 'I'
┌───< 0x0040130e 750e jne 0x40131e
│││ 0x00401310 807c240f4d cmp byte [rsp + 0xf], 0x4d ; 'M'
┌────< 0x00401315 7507 jne 0x40131e
││││ 0x00401317 807c241047 cmp byte [rsp + 0x10], 0x47 ; 'G'
┌─────< 0x0040131c 7414 je 0x401332
│└└└──> 0x0040131e 488d3d0e0e.. lea rdi, str.ERROR:_Invalid_magic_number_ ; 0x402133 ; "ERROR: Invalid magic number!"
┌─┌┌┌──> 0x00401325 e816feffff call sym.imp.puts ;[4]
┌─────└─> 0x0040132a 83cfff or edi, 0xffffffff ; -1
╎╎│╎╎╎ 0x0040132d e8cefeffff call sym.imp.exit ;[5]
╎╎└─────> 0x00401332 66837c241102 cmp word [rsp + 0x11], 2
╎╎ ╎╎╎ 0x00401338 488d3d110e.. lea rdi, str.ERROR:_Unsupported_version_ ; 0x402150 ; "ERROR: Unsupported version!"
╎└──────< 0x0040133f 75e4 jne 0x401325
╎ ╎╎╎ 0x00401341 837c24131a cmp dword [rsp + 0x13], 0x1a
╎ ╎╎╎ 0x00401346 488d3d1f0e.. lea rdi, str.ERROR:_Incorrect_width_ ; 0x40216c ; "ERROR: Incorrect width!"
╎ └────< 0x0040134d 75d6 jne 0x401325
╎ ╎╎ 0x0040134f 807c241717 cmp byte [rsp + 0x17], 0x17
╎ ╎╎ 0x00401354 488d3d290e.. lea rdi, str.ERROR:_Incorrect_height_ ; 0x402184 ; "ERROR: Incorrect height!"
╎ └───< 0x0040135b 75c8 jne 0x401325
╎ ╎ 0x0040135d bf58090000 mov edi, 0x958 ; 2392
╎ ╎ 0x00401362 e859feffff call sym.imp.malloc ;[6]
╎ ╎ 0x00401367 488d3d2f0e.. lea rdi, str.ERROR:_Failed_to_allocate_memory_for_the_image_data_ ; 0x40219d ; "ERROR: Failed to all
╎ ╎ 0x0040136e 4889c3 mov rbx, rax
╎ ╎ 0x00401371 4885c0 test rax, rax
╎ └──< 0x00401374 74af je 0x401325
╎ 0x00401376 ba58090000 mov edx, 0x958 ; 2392
╎ 0x0040137b 4889c6 mov rsi, rax
╎ 0x0040137e 4183c8ff or r8d, 0xffffffff ; -1
╎ 0x00401382 31ff xor edi, edi
╎ 0x00401384 488d0d470e.. lea rcx, str.ERROR:_Failed_to_read_data_ ; 0x4021d2 ; "ERROR: Failed to read data!"
╎ 0x0040138b e8ab020000 call sym.read_exact ;[3]
╎ 0x00401390 0fb6542417 movzx edx, byte [rsp + 0x17]
╎ 0x00401395 0faf542413 imul edx, dword [rsp + 0x13]
╎ 0x0040139a 31c0 xor eax, eax
╎ ┌─> 0x0040139c 39c2 cmp edx, eax
╎ ┌──< 0x0040139e 7630 jbe 0x4013d0
╎ │╎ 0x004013a0 0fb64c8303 movzx ecx, byte [rbx + rax*4 + 3]
╎ │╎ 0x004013a5 48ffc0 inc rax
╎ │╎ 0x004013a8 8d71e0 lea esi, [rcx - 0x20]
╎ │╎ 0x004013ab 4080fe5e cmp sil, 0x5e ; '^' ; 94
╎ │└─< 0x004013af 76eb jbe 0x40139c
╎ │ 0x004013b1 488b3d882c.. mov rdi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
╎ │ ; [0x404040:8]=0
╎ │ 0x004013b8 488d152f0e.. lea rdx, str.ERROR:_Invalid_character_0x_x_in_the_image_data__n ; str.ERROR:_Invalid_character_0x_x_
╎ │ ; 0x4021ee ; "ERROR: Invalid character 0x%x in the image data!\n"
╎ │ 0x004013bf be01000000 mov esi, 1
╎ │ 0x004013c4 31c0 xor eax, eax
╎ │ 0x004013c6 e845feffff call sym.imp.__fprintf_chk ;[7]
└───────< 0x004013cb e95affffff jmp 0x40132a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from pwn import *

header = b"cIMG"
version = 2
width = 26
height = 23
total_pixels = width * height

file_header = struct.pack("<4sHIB", header,version, width, height)

ascii_char = ord('A')

r, g, b = 0x8C, 0x1D, 0x40
single_pixel = struct.pack("<BBBB", r, g, b, ascii_char)

pixel_data = single_pixel * total_pixels

payload = file_header + pixel_data

file = open("payload.cimg", "wb")
file.write(payload)
file.close()

p = process(["/challenge/cimg", "payload.cimg"], stdin=process.PTY, stdout=process.PTY)
print(p.recvall())

pwn.college{**********************************************}