-
-
[翻译]使用Rust进行系统编程 - take2
-
发表于: 2023-3-21 15:48 6348
-
我有个朋友曾经说过:知不足者好学,耻下问者自满;这便是问题所在,我对自己所做的工作既感到自满又觉得不足。
自从完成上一篇Rust文章后,我就经常被问到关于Rust编程的问题,问题本身不难,但是为了回答这些问题,我每次都不得不重新阅读我的文章——我对我上一篇文章(中文版)并不满意,代码质量和结构都不是很好;所以,我决定尽可能的重写它,并且也趁此机会检验一下两年后我是否需要再次重写 :p
代码结构
在开始之前,我想解释一下我对我文章中的代码片段的处理方法。大部分人在搜索文章时,希望文章中提供的代码能直接在自己本地机器上运行;遗憾的是,在我的文章中,这种想法行不通,这是因为我提供的是代码片段而非一个包含所有函数、导入和结构的程序。
我的方法有点儿不同,代码片段只是为了说明我在某一节中所解释的概念,这些概念不是一个完整的程序;然而,我依旧会将完整的代码放在文章末尾的链接里,这样你就可以自己尝试运行代码了。
运行程序
我们依旧从我两年前写的那段差强人意的代码开始——运行另一个程序
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 | fn main() { / / breakpoints to set let breakpoints: [u64; 1 ] = [ 0x8048451 ]; match unsafe{fork()} { Ok(ForkResult::Child) = > { run_child(); } Ok(ForkResult::Parent {child}) = > { run_parent(child, &breakpoints); } Err(err) = > { panic!( "[main] fork() failed: {}" , err); } }; } / / Code that runs only for child fn run_child() { / / Allows process to be traced ptrace::traceme().unwrap(); / / Disable ASLR for this process personality(linux_personality::ADDR_NO_RANDOMIZE).unwrap(); / / Execute binary replacing the currently running code Command::new( "/home/carstein/sample" ). exec (); exit( 0 ); } |
这段代码有几个问题:首先,它又长又复杂,而且虽然它使用命令模块,但是它并没有利用它提供的所有可能性。最重要的是,我们手动调用fork()函数,这看起来就像是带有借用检查器的C一样。总之我们可以做的更好,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | use std::os::unix::process::CommandExt; use std::process::{Command, Stdio}; fn main() { let child = unsafe {Command::new( "/home/carstein/sample" ) .stdout(Stdio::null()) .stderr(Stdio::null()) .pre_exec(|| { personality(linux_personality::ADDR_NO_RANDOMIZE) .expect( "[!] cannot set personality" ); Ok(()) }) .spawn() .expect( "[!] Failed to run process" ) }; println!( "Started process with PID: {:?}" , child. id ()); } |
在我看来,更新后的版本更优秀——我们正在充分利用Command的功能。pre_exec()函数和spawn()函数需要特别注意,第一个函数接受闭包并在运行主代码之前执行所有封闭的指令;第二个函数负责将目标二进制文件作为子进程运行。由于缺乏对pre_exec()的了解,才会导致第一段代码看起来如此奇怪。
还有一件事儿需要解释一下,特别是如果你打算进行更多的系统编程;你可能想知道为什么我们要导入std::os::unix::process::CommandExt;乍一看我们好像没有在使用它;原因很简单——std::os::unix::process::CommandExt只包含支持的环境中的基本功能,如果你想使用某些操作系统特有的功能,比如用给定的uid运行程序,或者像是我们的例子中,在程序的主要功能开始之前执行一些系统调用,则需要加载第三方扩展库。
检查状态
假设你想看看程序运行后发生了什么,你当然可以统统过调用child.wait()和检查退出码来检查,但这种方法并没有告诉我们更多关于程序终止和各种异常方式,所以我想尝试使用waitpid()代替
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | / / this code goes after starting the process match waitpid(Pid::from_raw(child. id () as i32), None ) { Ok(WaitStatus::Exited(pid, status)) = > { println!( "Program {} exited normally with code {}" ,pid, status); } Ok(WaitStatus::Signaled(pid, signal, core)) = > { println!( "Program {} was terminated by signal {} (core dumped: {})" , pid, signal, core); } Ok(status) = > println!( "Status: {:?}" , status), Err(err) = > { println!( "We've encountered some kind of error: {:?}" , err); } } |
在这里,除了正常的程序终止方式外,我们还检查终止的原因是否是某种信号,例如:SIGSEGV或SIGABRT。如果想要获取有关可能情况的完整列表,我强烈建议你应该查看文档。
不知你是否注意到这段代码和先前版本的不同之处,匹配指令不需要重新组装一个嵌套树,更扁平的代码可以大大提高阅读性。
追踪
这段代码上次的作用是为我的fuzzer做一个仪表模块,这次依旧是如此;我们可以在pre_exec块增加一条指令来启用进程跟踪
1 2 3 4 5 6 7 8 9 | / / Partial snippet of code that normally should be chained together / / with the Command::new .pre_exec(|| { ptrace::traceme() .expect( "[!] cannot trace process" ); personality(linux_personality::ADDR_NO_RANDOMIZE) .expect( "[!] cannot set personality" ); Ok(()) }) |
设置断点
如果你尝试运行这段代码,那么waitpid()会报告一个SIGTAP,我们需要明确的处理这个问题——主要是因为接下来要做不少工作
1 2 3 4 5 6 7 8 9 10 11 | / / This is part of the waitpid() match instruction Ok(WaitStatus::Stopped(pid, signal)) = > { println!( "Program {} received {} event" , pid, signal); handle_sigstop(pid); } / / This is a separate function that should be defined outside of main fn handle_sigstop(pid: Pid) { let regs = ptrace::getregs(pid).unwrap(); println!( "Hit breakpoint at 0x{:x}" , regs.rip); } |
现在我们已经成功附加到一个正在运行的进程了,接下来需要设置一些断点;设置断点所用的函数并不难
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | fn set_breakpoint(pid: Pid, addr: u64) - > u64 { / / Read 8 bytes from the process memory let value = ptrace::read(pid, (addr) as * mut c_void).unwrap() as u64; / / Insert breakpoint by write new values let bp = (value & (u64:: MAX ^ 0xFF )) | 0xCC ; unsafe { ptrace::write(pid, addr as * mut c_void, bp as * mut c_void).unwrap(); } / / Return original bytecode value } |
正如我之前解释的那样——为了在给定地址设置断点,我们需要用ptrace::read从内存中读取8个字节,将第一个字节(小端)设置为0xCC,并使用ptrace::write将其写回进程内存。原始字节码被返回给调用者,这样我们就可以在以后处理给定的断点时恢复它
这种操作是十分危险的,一旦操作失误,Rust是不会报错的
如果你对软件断点的工作原理感兴趣,可以在这篇文章(中文版)中了解到更多信息
我们需要设计一个清除函数,在命中断点后清除它
1 2 3 4 5 6 | fn remove_breakpoint(pid: Pid, addr: u64, orig_value: u64) { unsafe { / / Restore original bytecode ptrace::write(pid, addr as * mut c_void, orig_value as * mut c_void).unwrap(); } } |
重写代码
关于处理断点的代码我已经在之前文章(中文版)里提到过了,不过这里为了方便其他人理解,我还是决定将这段代码放上来
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | fn handle_sigstop(pid: Pid, saved_values: &HashMap<u64, u64>) { let mut regs = ptrace::getregs(pid).unwrap(); println!( "Hit breakpoint at 0x{:x}" , regs.rip - 1 ); match saved_values.get(&(regs.rip - 1 )) { Some(orig) = > { restore_breakpoint(pid, regs.rip - 1 , * orig); / / rewind rip regs.rip - = 1 ; ptrace::setregs(pid, regs).expect( "Error rewinding RIP" ); } None = > print !( "Nothing saved here" ), } ptrace::cont(pid, None ).expect( "Restoring breakpoint failed" ); } |
结语
我希望这篇文章能够弥补前一篇文章的糟糕质量。当涉及到Rust(同样也适用于其他编程语言)时,你需要从别人的代码中学习一些代码经验(我从Brandon Falk那里学到了很多)。我打算多写一些关于系统编程的文章,但是写的越多我越烦躁,每次在使用第三方库时,我就感觉我在写一个带有借用检查器的C;尽管如此,我还是会分享一些关于如何围绕这些低级概念编写安全包装的技巧
正如开头承诺的那样——你可以在我写的rfuss2中看到上述代码的大部分功能
译者言
因本人翻译水平有限,如有错误之处,请斧正
原文链接