关于rp-bf
rp-bf是一款Windows下辅助进行ROP gadgets搜索的Rust库,该工具可以通过模拟Windows用户模式下的崩溃转储来爆破枚举ROP gadgets。
在很多系统安全测试场景中,研究人员成功劫持控制流后,通常需要将堆栈数据转移到他们所能够控制的内存区域中,以便执行ROP链。但是在劫持控制流时,找到合适的部分很大程度取决于研究人员对CPU上下文场景的控制。那么为了劫持具备任意值的控制流,我们需要找到合适的地址,但这个地址的寻找过程又非常的麻烦,需要涉及到各种地址值、代码指针、堆栈和汇编指令。
为了解决上述问题,rp-bf便应运而生,该工具能够有效地解决上述问题,并通过ROP gadgets搜索来辅助广大研究人员完成ROP链的执行。
依赖组件配置
在使用该工具之前,我们首先需要手动配置好bochscpu模拟器,它也是rp-bf的主要依赖组件。具体的配置方法如下:
1、点击【这里】下载对应操作系统平台的bochscpu组件版本;
2、使用下列命令克隆bochscpu项目代码:
git clone https://github.com/yrp604/bochscpu.git
3、将项目中的lib和bochs目录提取到解压后的bochscpu根目录;
4、使用下列命令验证bochscpu是否构建成功:
cargo build --release
工具下载
广大研究人员可以直接使用下列命令将rp-bf项目源码克隆至本地:
git clone https://github.com/0vercl0k/rp-bf.rs.git
然后切换到项目目录下,并使用cargo build命令完成代码构建:
cargo build --release
工具运行机制
rp-bf能够从根据一个进程快照来模拟目标代码,并能够迭代快照中找到的每一个内存区域,然后将其传递给用户模块。接下来,研究人员就可以用这个地址来执行其他的安全测试了:
pub trait Finder { fn pre(&mut self, emu: &mut Emu, candidate: u64) -> Result<()>; fn post(&mut self, emu: &Emu) -> Result<bool>; }
工具使用
获取一个快照
该工具的使用方法适用于所有的操作系统和体系架构,但我们的使用样例会以Windows/Intel为例。
我们可以直接使用Windows调试器生成快照,在Windbg中运行你的目标,然后在所需状态下的所需位置生成崩溃转储(.dump/ma)即可。
搜索算法
rp-bf能够遍历崩溃转储中找到的所有内存区域,然后在模拟器中重新创建相同的执行环境。接下来,它会调用用户的pre条件,并持续执行,直到模拟器退出。此时,工具会调用post条件来让用户决定目标区域是否合适,模拟器的状态(即内存和CPU上下文)将被不断恢复和刷新。
pub fn explore(opts: &Opts, finder: &mut dyn Finder, ui: &mut dyn ui::Ui) -> Result<Vec<Candidate>> { // ... for (mem_address, mem_block) in dump.mem_blocks() { // ... 'outer: for candidate in mem_block.range.start..mem_block.range.end { // ... // Invoke the `pre` callback to set-up state. trace!("Trying out {candidate:#x}"); finder.pre(&mut emu, candidate)?; // Run the emulation with the candidate. let (res, stats) = emu.run()?; how_many_total += 1; // We found a candidate if it lead to a crash & the `post` condition // returned `true`. let crashed = matches!(res, TestcaseResult::Crash); let found_candidate = crashed && finder.post(&emu)?; // ... emu.restore()?; // ... } } }
从用户模式Windows崩溃转储模拟代码
该工具所使用的模拟器都使用了bochscpu库的Bochs CPU模拟器。为了在Bochs中重新创建执行环境,rp-bf将构建页面表以重新创建用户模式转储中可用的相同虚拟环境。
编写一个Finder模块
Finder模块需要提供一个pre方法和一个post方法:
pub trait Finder { fn pre(&mut self, emu: &mut Emu, candidate: u64) -> anyhow::Result<()>; fn post(&mut self, emu: &Emu) -> anyhow::Result<bool>; }
pre方法接收一个指向模拟器的可变引用,以及候选内存区域,我们可以在运行时环境中的某个位置“注入”候选区域。它能够将寄存器设置为候选值,或者将其写入内存中的某个位置:
impl Finder for Pwn2OwnMiami2022_2 { fn pre(&mut self, emu: &mut Emu, candidate: u64) -> Result<()> { // Here, we continue where we left off after the gadget found in |miami1|, // where we went from constrained arbitrary call, to unconstrained arbitrary // call. At this point, we want to pivot the stack to our heap chunk. // // ``` // (1de8.1f6c): Access violation - code c0000005 (first/second chance not available) // For analysis of this file, run !analyze -v // mfc140u!_guard_dispatch_icall_nop: // 00007ffd`57427190 ffe0 jmp rax {deadbeef`baadc0de} // // 0:011> dqs @rcx // 00000000`1970bf00 00000001`400aed08 GenBroker64+0xaed08 // 00000000`1970bf08 bbbbbbbb`bbbbbbbb // 00000000`1970bf10 deadbeef`baadc0de <-- this is where @rax comes from // 00000000`1970bf18 61616161`61616161 // ``` self.rcx_before = emu.rcx(); // Fix-up @rax with the candidate address. emu.set_rax(candidate); // Fix-up the buffer, where the address of the candidate would be if we were // executing it after |miami1|. let size_of_u64 = std::mem::size_of::<u64>() as u64; let second_qword = size_of_u64 * 2; emu.virt_write(Gva::from(self.rcx_before + second_qword), &candidate)?; // Overwrite the buffer we control with the `MARKER_PAGE_ADDR`. Skip the first 3 // qwords, because the first and third ones are already used to hijack flow // and the second we skip it as it makes things easier. for qword_idx in 3..18 { let byte_idx = qword_idx * size_of_u64; emu.virt_write( Gva::from(self.rcx_before + byte_idx), &MARKER_PAGE_ADDR.u64(), )?; } Ok(()) } // ... }
模拟完成后便会调用post方法,我们可以在这里找到你想要的内容,即堆栈数据和可控制的@rip值等数据,这里还允许我们指定需要实现的特定需求:
impl Finder for Pwn2OwnMiami2022_2 { // ... fn post(&mut self, emu: &Emu) -> Result<bool> { // Let's check if we pivoted into our buffer AND that we also are able to // start a ROP chain. let wanted_landing_start = self.rcx_before + 0x18; let wanted_landing_end = self.rcx_before + 0x90; let pivoted = has_stack_pivoted_in_range(emu, wanted_landing_start..=wanted_landing_end); let mask = 0xffffffff_ffff0000; let rip = emu.rip(); let rip_has_marker = (rip & mask) == (MARKER_PAGE_ADDR.u64() & mask); let is_interesting = pivoted && rip_has_marker; Ok(is_interesting) } }
post方法返回值之后,模拟器会恢复内存和CPU寄存器,并继续寻找下一个候选区域。
工具运行演示
许可证协议
本项目的开发与发布遵循MIT开源许可证协议。
项目地址
rp-bf:【GitHub传送门】
参考资料
Competing in Pwn2Own ICS 2022 Miami: Exploiting a zero click remote memory corruption in ICONICS Genesis64
GitHub - yrp604/bochscpu