首页
社区
课程
招聘
[翻译]使用Rust进行系统编程 - part1
发表于: 2023-3-16 16:36 8668

[翻译]使用Rust进行系统编程 - part1

2023-3-16 16:36
8668

在2020年五月份,我开始了一个系列文章——如何编写一个简单的fuzz;随着开发的进行,我遇到了一个很严重的问题——我是否还要继续用Python作为开发fuzz工具的语言。
不得不说Python的确是一门简单的语言,学习和使用都毫无压力,但也有一些缺点;首先我使用的是一个第三方的ptrace库,这个库本身代码没问题,有问题的是运行速度和并发问题。这些问题可能会毁掉我目前所有的工作成果,于是我决定使用Rust继续并重写这个项目,然而这又引发了另一个问题——我不会Rust。
尽管如此,我还是决定从陪产假中抽身,学习一些项目,并同时学习Rust

Rust里的系统编程

我向来喜欢分享,尤其是分享当初自己在网上无论如何都无法找到的内容时。提到系统编程,目前网上并没有特别好的项目或文章来讲述如何将C语言中得到的灵感转化为更高效的Rust代码。因此,我决定在此分享如何在Rust中使用fork和ptrace。

如何使用fork(叉子)吃饭

在这个项目中,我会先写一个空的代码骨架,并以循序渐进的方式介绍某些概念。我们需要用到两个第三方库,nixlinux-personality;第一个是Rust与各种nix API绑定的集合,第二个将允许我们为子进程禁用ASLR。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);
        }
    };
}

生成一个子进程非常简单——你只需要调用fork()函数,然后匹配到三个分支条件的其中一个。值得注意的是,Rust认为fork是一个不安全的操作,但里面的代码不需要标记为不安全。
这实际上是Rust的一个十分优秀的特性,即匹配必须是详尽的——这样我们就不会忘记任何可能性,也不会引入一些奇怪的情况。

带来ptrace

下一个模块是只在子进程中运行的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 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
    Command::new("/home/carstein/sample").exec();
 
    exit(0);
}

这里只有三个指令:第一个指令告诉内核,子进程变得可追踪;我知道我应该在这里更好的处理错误,但你现在只需要知道的是unwrap()函数和这条指令的执行返回结果,如果报错了,我们只能panic(此处译者也不知道如何翻译更好)
第二条指令很明显——我们为子进程禁用ASLR
最后是一个处理进程生成和参数传递的便捷函数——Command::new()。请记住,虽然spawn()方法会分叉并创建一个新的进程,但exec()将运行命令以替换当前的子进程的代码。

如何为人父母

现在来看最大的那部分代码——父进程执行的那部分代码

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
// Code that runs only for parent
fn run_parent(pid: Pid, breakpoints: &[u64]) {
    let mut saved_values = HashMap::new();
 
    // Placing breakpoints
    wait().unwrap();
    for addr in breakpoints.iter() {
        let orig = set_breakpoint(pid, *addr);
        saved_values.insert(*addr, orig);
    }
    ptrace::cont(pid, None).expect("Failed continue process");
 
    loop {
        match wait() {
            Ok(status) => {
                match status {
                    WaitStatus::Stopped(pid_t, sig_num) => {
                        match sig_num {
                            Signal::SIGTRAP => {
                                handle_sigstop(pid_t, &saved_values);
                            }
 
                            Signal::SIGSEGV => {
                                let regs = ptrace::getregs(pid_t).unwrap();
                                println!("Segmentation fault at 0x{:x}", regs.rip);
                                break
                            }
                            _ => {
                                println!("Some other signal - {}", sig_num);
                                break
                            }
                        }
 
                    },
                    WaitStatus::Exited(pid, exit_status) => {
                        println!("Process with pid: {} exited with status {}",
                                            pid, exit_status);
                        break;
                    },
 
                    _ => {
                        println!("Received status: {:?}", status);
                        ptrace::cont(pid, None).expect("Failed to deliver signal");
                    }
                }
            }
 
            Err(err) => {
                println!("Some kind of error - {:?}",err);
 
            },
        }
    }
}

代码有三个主要部分:刚开始,我们使用wait()函数等待子进程通知我们他刚刚被加载并准备好被追踪,我们利用这个机会放置一些断点——其实只放了一个;关于如何放置断点,我们后面再说。
一旦设置了断点,我们就可以通过trace::cont()指示子进程继续执行直到下一个中断。
需要注意的是,还有其他方法可以达到同样的效果;两个最常见的方法是通过调用ptrace::step()进行单步操作,或者通过调用trace::syscall()直到下一个系统调用。
接下来我们会进入一个循环,等待子进程改变其状态,一旦发生变化,我们就通过匹配状态检查过渡的性质,并做出相应的反应——如果子进程被停止,我们就检查原因;如果进程退出,我们就打破循环,同时终止父进程。为了简单起见,我决定暂不实现所有可能的状态。

如何处理你的子进程

当子进程停止时,WatiStatus会好心的告诉我们是什么信号导致的,我们会使用内部匹配语句相应的处理每种情况。
我对Rust的模式匹配语句还不是很了解,但我感觉这段代码应该可以简化,减少嵌套。
下面的代码处理的是特殊情况,即我们真的碰到了我们设置的断点并希望正确的处理它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn handle_sigstop(pid: Pid, saved_values: &HashMap<u64, i64>) {
    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");
 
        }
        _ => print!("Nothing saved here"),
    }
 
    ptrace::cont(pid, None).expect("Restoring breakpoint failed");
 
}

如果终止发生在我们之前放置断点的地方,我们就用原始指令操作码将其删除,并将指令指针回退一个以继续执行。

无聊的断点

放置和恢复断点分别由两个独立的函数完成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn set_breakpoint(pid: Pid, addr: u64) -> i64 {
    // 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读取八个字节的指令操作码并将第一个替换为0xCC;然后,我们覆写修改后的字节码,并将原始字节码传递给调用函数进行保存。
如果你很好奇软件断点是如何工作的,请阅读我之前的博客文章
下面是删除断点的代码

1
2
3
4
5
6
fn restore_breakpoint(pid: Pid, addr: u64, orig_value: i64) {
    unsafe {
        // Restore original bytecode
        ptrace::write(pid, addr as *mut c_void, orig_value as *mut c_void).unwrap();
    }
}

在这两种情况下,唯一棘手的部分是ptrace::read和ptrace::write需要表示为c_void的原始指针,因此我们需要用强制转换来获取它;此处操作需要谨慎,因为当你想要搬起石头砸自己脚的时候,Rust不会保护你 :p

总结

第一次接触Rust还是挺有趣的;我在将一些C语言的概念翻译成Rust时遇到了一些小问题,缺乏文档也是困难之一,不过无论如何,我都非常喜欢Rust。
到目前为止,Rust相比于C可以返回更多有用的信息,所以你不必使用奇怪的宏来提取某些准确的状态;另外,在Rust中也可以使用匹配,这也可以帮你写出更简洁的代码。
代码在GitHub上可以找到

译者言

因本人翻译水平有限,如有错误之处,请斧正
原文链接


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

收藏
免费 3
支持
分享
最新回复 (3)
雪    币: 1825
活跃值: (5354)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
调用个“getmodulehandleA/W”你rust字符串都不能直接用. 调用十万二十万次含字符串参数的API, 光你转换字符串为A字符串或W字符串的浪费的时间和CPU算力, 人家早比你多循环几万次了
2023-3-19 01:11
1
雪    币: 14530
活跃值: (17548)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
3
PEDIY 调用个“getmodulehandleA/W”你rust字符串都不能直接用. 调用十万二十万次含字符串参数的API, 光你转换字符串为A字符串或W字符串的浪费的时间和CPU算力, 人家早比你多循环几万 ...
不知你是否注意到,这是翻译文章,如果有任何问题,请和原作者沟通
2023-3-19 16:16
0
游客
登录 | 注册 方可回帖
返回
//