这篇文章主要介绍了Rust在Linux下通过修改argv0和/proc/self/comm的方式修改进程名以及原理.


Begin

最近在看tsh的相关代码,想着用rust重写一遍tsh,看到github上的rust以及go改写的tsh功能和体积都不如意,于是自己重新造轮子,过一遍完整的理论与项目实践。

此文章示例代码都放在github上:FakeProcessNameDemo

Principle

在Linux中进程名通常由修改两个地方就能伪造进程名。分别是argv[0],comm.

  1. argv[0]
    作用:存储程序启动时的完整命令行
    对应文件位置: /proc/[pid]/cmdline
    修改方式: 直接修改内存即可伪造进程名
    长度: 可以很长(通常几百字节)
    位置: 存储在进程的栈内存中
1
2
3
4
5
6
7
8
ps aux | grep nginx
root 1234 nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 这就是 argv[0] 加上参数

# 方式2: ps -ef
ps -ef | grep nginx
root 1234 /usr/sbin/nginx -c /etc/nginx/nginx.conf
  1. comm (进程任务名)
    长度限制: 最多16个字符(算上’\0’)
    位置: 存储在内核的 task_struct 结构体中
    修改方式: 使用 prctl(PR_SET_NAME) 系统调用
    对应文件: /proc/[PID]/comm
1
2
cat /proc/1234/comm
nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 方式1: ps -eo comm
ps -eo pid,comm | grep 1234
1234 nginx
# ^^^^^
# 这就是 comm

# 方式2: top
top -p 1234
PID USER COMMAND
1234 root nginx ← 这里显示的也是 comm

# 方式3: htop
htop -p 1234
PID COMMAND
1234 nginx

Coding

根据tinyshell的源码,我们可以看到C语言版本是这么处理argv[0]修改进程名的

1
2
memset((void *)argv[0], '\0', strlen(argv[0]));
strcpy(argv[0], FAKE_PROC_NAME);

我们构造一个测试C语言程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv) {
printf("=== C语言版本 argv[0] ===\n\n");

printf("原始 argv[0]: %s\n", argv[0]);
printf("原始长度: %lu 字节\n\n", strlen(argv[0]));
char *fake_name="fake";
memset(argv[0],'\0',strlen(argv[0]));
strcpy(argv[0],fake_name);
printf("\n");
printf("进程将运行15秒,请检查:\n");
printf(" ps aux | grep %d\n", getpid());
printf(" cat /proc/%d/cmdline | tr '\\0' ' '\n", getpid());

sleep(15);
return 0;
}
// gcc -o test fake_name.c

结果如下:

测试OK!

使用rust实现进程名修改–修改Argv[0]

众所周知,Rust是一门内存安全的语言,不使用unsafe修改argv[0]的时候,你会发现程序内读取argv[0]是修改过的进程名,使用命令ps -ef之类的发现进程名还是原来的进程名。

1
2
3
4
5
6
fn main() {
let mut args=std::env::args().collect::<Vec<_>>();
args[0]=String::from("fake-name");
println!("args[0]={}",args[0]);
std::thread::sleep(std::time::Duration::from_secs(60));
}

实际上ps -ef还是显示的demo进程名

原因是rust返回的args是一个拷贝,并不是引用,修改拷贝不影响原始数据。详情可以了解没有途径修改argv[0]
我们使用unsafe来改写上述C语言代码来实现进程名字伪造.整体逻辑如下:

  定位当前程序栈地址->在栈中搜索argv[0]->计算可用空间->修改argv[0]
  1. 通过/proc/self/maps定位到当前程序的栈地址 (因为Rust不像C那样直接暴露char **argv指针。我们必须自己找到它在内存中的位置)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// 获取进程栈的地址范围
///
/// 通过读取 /proc/self/maps 查找 [stack] 区域
/// 返回 (stack_start, stack_end)
fn get_stack_range()->Result<(usize,usize),String>{
let maps=std::fs::read_to_string("/proc/self/maps").map_err(|e|format!("不能读取maps:{}",e))?;
let (mut stack_start,mut stack_end)=(0usize,0usize);
//查找stack行
for line in maps.lines(){
if line.contains("[stack]"){
let parts:Vec<&str>=line.split_whitespace().collect();
if let Some(range)=parts.first(){
let addrs:Vec<&str>=range.split("-").collect();
if addrs.len()==2{
stack_start=usize::from_str_radix(addrs[0], 16).map_err(|e|format!("解析起始地址失败:{}",e))?;
stack_end=usize::from_str_radix(addrs[1], 16).map_err(|e|format!("解析结束地址失败:{}",e))?;
return Ok((stack_start,stack_end));
}
}
}
}
Err("未找到[stack]区域".to_string())
}

简单的单元测试以及结果

  1. 在栈中搜索argv[0]字符串以及地址
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
unsafe fn find_argv0_address_in_stack(_stack_start:usize,stack_end:usize)->Result<usize,String>{  
let argv0=std::env::args().next()
.ok_or("没有argv[0]".to_string())?;
let argv0_bytes=argv0.as_bytes();
let search_start = stack_end.saturating_sub(1024 * 1024);
let search_end = stack_end - argv0_bytes.len();
println!(
"搜索范围: 0x{:x} - 0x{:x} ({} 字节)",
search_start,
search_end,
search_end - search_start
);
for addr in (search_start..search_end).step_by(1){
let ptr=addr as *const u8;
let mut matches = true;
for i in 0..argv0_bytes.len(){
unsafe {
if *ptr.add(i)!=argv0_bytes[i]{
matches=false;
break;
}
}
}
unsafe {
if matches && *ptr.add(argv0_bytes.len())==0{
return Ok(addr);
}
}
}
Err("不能在栈中找到argv[0]的地址".to_string())
}

单元测试结果

1
2
3
4
5
running 1 test
搜索范围: 0x7ffdb065b000 - 0x7ffdb075afba (1048506 字节)
找到 argv[0] 地址: 0x7ffdb0759dc8
argv[0] 内容: /home/asd/Desktop/project/demo/target/debug/deps/demo-dc6b4fb4fd053556
test tests::test_find_argv0_address ... ok
  1. 计算可用空间
    实际上经过测试,不计算空间直接篡改argv[0]也行,但是在实战中遇到一些奇葩系统可能会出现一些奇葩问题,于是想了想还是加上,把cmdline和environ都清空,由于在栈中cmdline和environ的地址都是连续的,这样保证了伪装进程名的完整性。
1
2
3
4
5
6
7
8
9
10
11
12
13
unsafe fn calculate_argv_space(argv_start:usize)->Result<usize,String>{
//解析cmdline和environ的长度,获取可用长度
let cmdline = std::fs::read("/proc/self/cmdline")
.map_err(|e| format!("Failed to read cmdline: {}", e))?;
let environ_data = std::fs::read("/proc/self/environ")
.map_err(|e| format!("Failed to read environ: {}", e))?;

let argv_end = argv_start + cmdline.len();

let environ_end = argv_end + environ_data.len();

Ok(environ_end)
}

单元测试以及结果

1
2
3
4
5
6
running 1 test
搜索范围: 0x7ffcb3ce4000 - 0x7ffcb3de3fba (1048506 字节)
argv[0] 起始地址: 0x7ffcb3de2dc6
environ 结束地址: 0x7ffcb3de3fb1
总可用空间: 4587 bytes (4 KB)
test tests::test_calculate_argv_space ... ok
  1. 修改argv[0]
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
unsafe fn modify_argv0_name(process_name:&str)->Result<(),String>{
let (stack_start,stack_end)=get_stack_range()?;
let argv0_start = unsafe {
find_argv0_address_in_stack(stack_start, stack_end)?
};
let environ_end = unsafe {
calculate_argv_space(argv0_start)?
};
let total_available = environ_end - argv0_start;
let process_name_bytes = process_name.as_bytes();

// 检查名称长度是否超过可用空间
if process_name_bytes.len() + 1 > total_available {
return Err(format!(
"进程名太长: {} bytes,可用空间: {} bytes",
process_name_bytes.len() + 1,
total_available
));
}

println!("修改前 argv[0]: {}", std::env::args().next().unwrap());
println!("将要修改为: {}", process_name);
println!("可用空间: {} bytes", total_available);

unsafe {
std::ptr::write_bytes(argv0_start as *mut u8, 0, total_available);

std::ptr::copy_nonoverlapping(
process_name_bytes.as_ptr(),
argv0_start as *mut u8,
process_name_bytes.len()
);
}

println!("修改成功!");

Ok(())
}

单元测试以及结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[test]
fn test_modify_argv0_name(){

// 修改进程名
let fake_name = "fakefakefakefakefakefakefakefake";
let result = unsafe { modify_argv0_name(fake_name) };

assert!(result.is_ok(), "修改进程名失败: {:?}", result.err());

// 读取修改后的 cmdline
let after = std::fs::read("/proc/self/cmdline").unwrap();
let after_str = String::from_utf8_lossy(&after);
println!("/proc/self/cmdline: {:?}\n", after_str.replace('\0', " "));

}
1
2
3
4
5
6
7
running 1 test
搜索范围: 0x7fff68427000 - 0x7fff68526fba (1048506 字节)
修改前 argv[0]: /home/asd/Desktop/project/demo/target/debug/deps/demo-dc6b4fb4fd053556
将要修改为: fakefakefakefakefakefakefakefake
可用空间: 4584 bytes
修改成功!
/proc/self/cmdline: "fakefakefakefakefakefakefakefake
  1. 最终测试
1
2
3
4
5
6
7
8
fn main() {
unsafe {
modify_argv0_name("fakenamefakenamefakenamefakenamefakenamefakenamefakenamefakename").unwrap();
}
println!(" cat /proc/{}/cmdline | tr '\\0' ' '", std::process::id());
println!(" ps aux | grep {}", std::process::id());
std::thread::sleep(std::time::Duration::from_secs(60));
}

这样就完美搞定了。

使用rust实现进程名修改–修改comm

根据先前提到的,篡改了argv[0]后,在/proc/self/comm中还是能看到原始的进程名。

那么我们使用prctl函数直接篡改就好了。这个函数的限制就是篡改的进程名最大长度不超过16个字符(算上’\0’)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  //调用prctl函数修改comm
unsafe fn mofidy_comm_name(process_name:&str)->Result<(), String>{
//截断长进程名到15个字符
let truncated_name= if process_name.len()>15{
&process_name[..15]
}else{
process_name
};
//转换为C字符串
let c_process_name=CString::new(truncated_name)
.map_err(|e|format!("转换错误:{}",e)).unwrap();

//调用prctl
unsafe{
if libc::prctl(libc::PR_SET_NAME,c_process_name.as_ptr() as usize,0,0,0)<0{
return Err("prctl函数调用错误".to_string());
}
}
Ok(())
}
1
2
3
4
5
6
7
8
9
10
fn main() {
unsafe {
modify_argv0_name("fakenamefakenamefakenamefakenamefakenamefakenamefakenamefakename").unwrap();
mofidy_comm_name("fakenamefakenamefakenamefakenamefakenamefakenamefakenamefakename").unwrap();
}
println!(" cat /proc/{}/cmdline | tr '\\0' ' '", std::process::id());
println!(" cat /proc/{}/comm", std::process::id());
println!(" ps aux | grep {}", std::process::id());
std::thread::sleep(std::time::Duration::from_secs(60));
}

效果

Ending

至此就总结了常规Linux下Rust修改进程名的两种方式。已经满足了常规Linux下的需求了实际上正式环境会导致程序崩溃,因为读了内存。可以使用C包装器跟tinyshell一样修改argv[0]的方式才是最安全的 如果是freebsd以及sunos之类的,那么就需要把ptrcl函数得更换一下了。

⬆︎TOP