这篇文章主要介绍了Rust在Linux下通过修改argv0和/proc/self/comm的方式修改进程名以及原理.
Begin
最近在看tsh的相关代码,想着用rust重写一遍tsh,看到github上的rust以及go改写的tsh功能和体积都不如意,于是自己重新造轮子,过一遍完整的理论与项目实践。
此文章示例代码都放在github上:FakeProcessNameDemo
Principle
在Linux中进程名通常由修改两个地方就能伪造进程名。分别是argv[0],comm.
- 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
ps -ef | grep nginx root 1234 /usr/sbin/nginx -c /etc/nginx/nginx.conf
|
- 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
| ps -eo pid,comm | grep 1234 1234 nginx
top -p 1234 PID USER COMMAND 1234 root nginx ← 这里显示的也是 comm
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; }
|
结果如下:
测试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]
- 通过/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
|
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); 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()) }
|
简单的单元测试以及结果
- 在栈中搜索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
|
- 计算可用空间
实际上经过测试,不计算空间直接篡改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>{ 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
|
- 修改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());
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 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
| unsafe fn mofidy_comm_name(process_name:&str)->Result<(), String>{ let truncated_name= if process_name.len()>15{ &process_name[..15] }else{ process_name }; let c_process_name=CString::new(truncated_name) .map_err(|e|format!("转换错误:{}",e)).unwrap();
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函数得更换一下了。