基于Rust的Windows平台进程遍历
1900 发表于 中国 二进制安全 2715浏览 · 2024-02-26 13:21

Rust是这两年比较火的一门语言,作为新研发的系统级编程语言。用Rust写出来的程序不仅运行速度快,还通过所有权系统等对代码进行编译时检查,有效地提升了程序的安全性,降低了bug率。

通过Rust调用Windows API需要使用相应的包(crate)来实现,几年前有一个winapi 包可以做这件事。不过这个包已经好久没有更新了,微软官方最近则提供了windowswindows-rs 这两个crate供大家使用。

本文将展示通过windowswindows-rs这两个crate来调用Windows API并实现进程遍历,并打印每个进程的PID和进程名的功能。

windows和windows-rs的区别

这两个crate都可以用来调用Windows API,两者的区别可以看Choosing between the windows and windows-sys crates - Kenny Kerr 中的介绍,大概如下。对于本文来说,最主要的是第四点区别,也就是用windows这个crate写出来的代码更有Rust的风格,而用windows-rs写出来的代码看起来和C/C++没什么区别。

之所以会这样,是因为windows-rs这个crate没有对Windows API进行封装,而是直接从kernel32.dll中获取然后调用:

#[cfg(feature = "Win32_Foundation")]
::windows_targets::link!("kernel32.dll" "system" #[doc = "Required features: `\"Win32_Foundation\"`"] fn CreateToolhelp32Snapshot(dwflags : CREATE_TOOLHELP_SNAPSHOT_FLAGS, th32processid : u32) -> super::super::super::Foundation:: HANDLE);

但是windows这个crate则会对API进行简单的封装,让函数的返回值符合Rust的风格:

#[inline]
pub unsafe fn CreateToolhelp32Snapshot(dwflags: CREATE_TOOLHELP_SNAPSHOT_FLAGS, th32processid: u32) -> ::windows_core::Result<super::super::super::Foundation::HANDLE> {
    ::windows_targets::link!("kernel32.dll" "system" fn CreateToolhelp32Snapshot(dwflags : CREATE_TOOLHELP_SNAPSHOT_FLAGS, th32processid : u32) -> super::super::super::Foundation:: HANDLE);
    let result__ = CreateToolhelp32Snapshot(dwflags, th32processid);
    (!result__.is_invalid()).then(|| result__).ok_or_else(::windows_core::Error::from_win32)
}

所以从crate.io中查询道到函数定义上看,这两个函数除了返回值,其他都一样,并且都和原生API一样:

代码实现

首先,需要在Cargo.toml中引入这两个包,之后才能使用这里面的函数:

[dependencies.windows]
version = "*"
features = [
    "Win32_Foundation",
    "Win32_System_Diagnostics_ToolHelp",
]

[dependencies.windows-sys]
version = "*"
features = [
    "Win32_Foundation",
    "Win32_System_Diagnostics_ToolHelp",
]

其次,由于这些Windows API基本上都是用unsafe声明的,所以要调用这些API需要在Rust的unsafe块中调用。

如果使用windows-rs来实现进程遍历,写出来的代码基本和用C/C++写出来的代码没什么区别,都是获取进程快照,然后循环获取每个进程的PID和进程名:

fn test_in_windows_sys() {
    use windows_sys::Win32::Foundation::{INVALID_HANDLE_VALUE, GetLastError};
    use windows_sys::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32,TH32CS_SNAPPROCESS};

    unsafe {
        // 获取进程快照
        let handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
        if handle == INVALID_HANDLE_VALUE {
            println!("CreateToolhelp32Snapshot Error: {}", GetLastError());
            process::exit(1);
        }

        let mut pe32: PROCESSENTRY32 = zeroed();
        pe32.dwSize = size_of_val(&pe32) as u32;

        let mut b_ret = Process32First(handle, &mut pe32) != 0;

        while b_ret {
            // 将进程名转换为UTF-8
            let name = String::from_utf8(pe32.szExeFile[..].to_vec()).unwrap_or_else(|e| {
                println!("String::from_utf8 Error: {}", e);
                process::exit(1);
            });
            println!("PID={},name={}", pe32.th32ProcessID, name);

            b_ret = Process32Next(handle, &mut pe32) != 0;
        }
    }
}

如果是使用windows这个包来实现,方法也是一样,只不过要对返回值进行处理,写出来的代码就具有Rust风格:

fn test_in_windows() {
    use windows::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS};

    unsafe {
        let handle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0).unwrap_or_else(|e| {
            println!("CreateToolhelp32Snapshot Error: {}", e);
            process::exit(1);
        });

        let mut pe32: PROCESSENTRY32 = PROCESSENTRY32::default();
        pe32.dwSize = size_of_val(&pe32) as u32;

        Process32First(handle, &mut pe32).unwrap_or_else(|e| {
            println!("Process32First Error: {}", e);
            process::exit(1);
        });

        loop {
            // 将进程名转换为UTF-8
            let sz_exe_file = pe32.szExeFile.map(|x| x as u8).to_vec();
            let name = String::from_utf8(sz_exe_file).unwrap_or_else(|e| {
                println!("String::from_utf8 Error: {}", e);
                process::exit(1);
            });

            println!("PID={},name={}", pe32.th32ProcessID, name);

            if let Err(_) = Process32Next(handle, &mut pe32) {
                break;
            }
        }
    }
}

编码问题

上面两段代码,无论哪段代码运行以后,进程名的输出都是像下面这样出错:

这是因为,在Rust中字符是按UTF-8编码的,而Process32First和Process32Next这两个API都是按照ASCII编码来将进程名字写入到PROCESSENTRY32->szExeFile数组中,所以在获取进程名的时候,都需要通过String::from_utf8来转换:

let name = String::from_utf8(sz_exe_file).unwrap_or_else(|e| {
       println!("String::from_utf8 Error: {}", e);
       process::exit(1);
});

然而,Process32First和Process32Next在将进程名写入到szExeFile数组的时候并没有清空该数组。

以下是调试的结果:

首先在第一次运行Process32Next函数之前,Process32First函数会将第一个进程名写入到szExeFile数组中。该进程名共16个字节,所以szExeFile[0]到szExeFile[15]就保存了这个进程的名称:

当第二次准备运行Process32Next函数的时候,该函数已经把第二个进程的名称写入到szExeFile数组中,第二个进程名共6个字节,所以szExeFile[0]到szExeFile[5]就保存了第二个进程的名称。

但是,Process32Next函数在写入第二个进程名的时候没有清空szExeFile数组,而是直接将szExeFile[6]赋值为0,以此来截断字符。这就导致szExeFile[7]到szExeFile[15]中保存了上一个进程名的数据。

由此可以推测,Process32First和Process32Next这两个函数在写入进程名称的时候,逻辑大概如下所示,也就是直接将新的进程的进程名写入到数组中,然后在后面加个0截断,而没有去清空szExeFile数组。

void write_process_name(char szExeFile[], char newProName[]) {
    DWORD dwProNameLen = strlen(newProName);

    for (DWORD i = 0; i < dwProNameLen; i++) {
            szExeFile[i] = newProName[i];
    }
    szExeFile[dwProNameLen] = 0;
}

虽然这样写不合适,不过你如果用ASCII来编码szExeFile程序是可以正常输出的。但问题是,Rust是用UTF-8来编码的。这个时候,程序不会将szExeFile[6]中的0当成是字符串结束,而是继续向后编码,直到遇到连续的两个0,这就产生了上面说的输出进程名出现的错误。

要解决这个问题也不难,在这个程序中只要在调用Process32Next函数之前,自己手动将szExeFile数组清空即可:

for item in pe32.szExeFile.iter_mut() {
                *item = 0;
}

这个时候就会发现,程序的输出是正常的:

后记

这个字符编码问题怎么说呢,应该说是Process32First/Process32Next函数这两个原生API的问题。按道理不应该那样去写入进程名,不知道最开始为什么要这样去写代码,可能是为了快?

而且,如果Process32First/Process32Next函数有这个问题,其他的比如Module32First/Module32Next这些函数也肯定会有这个问题。但是坑的地方是,windows和windows-rs这两个包没有在crate-io中说明这个问题。而如果按照MSDN的定义,程序员是不会想到要去特别处理这一块代码。

所以可想而知,这样就很容易出现很多Bug。windows和windows-rs作为crate,按道理在封装的时候应该对这些代码进行处理,不然你也得在crate-io中进行说明。可是什么都没有,我已经把这个问题在github上向这两个crate的开发人员进行反馈了反馈:Unable to get the correct process name by using Process32First/Process32Next function · Issue #2879 · microsoft/windows-rs · GitHub

但是目前得到的回复是:

There doesn't appear to be any bug here. Your program owns the PROCESSENTRY32 structure and is responsible for ensuring it's ready before calling Process32Next.

I don't think we want to do this at the crate level for a number of reasons, such as:

  • The crate is automatically generated from metadata and there's no metadata to indicate which buffers should be zeroed before call
  • Zeroing out the struct member automatically could be wasteful if the user already zeroed it out
  • The caller may want to provide a szExeFile padded out with 0x20 (space) characters

I'll leave this open for others to chime in, just in case I'm missing something.

大概意思就是要程序员们自己注意,调这些函数的时候多想想,相应结构体有没有被你合理的赋值。

(不得不说这个锅甩的一言难尽,希望之后这两个包在迭代过程中会有所修改吧,不然真的是要带一堆BUG出来)

2 条评论
某人
表情
可输入 255