興味本位で、不審なプロセスを簡単に捉えられないか実験してみたく、土台となるコードを書いてみました。
仕様
起動時
- 起動しているプロセスの一覧を表示
実行中
- OS上で起動されたプロセスの表示
- 終了したプロセスの表示
これを元に何ができるか
例えば、過去に起動したことのない新しい実行ファイルが起動されたらリアルタイムでslack通知するとか、セキュリティ関連のツールが作れそうです。
あと、これは興味本位でやってみたいのですが、セキュリティが穴だらけの適当なwebサーバを作って、スクリプトを仕込ませて動作を観察するとか。
(バックドアができたり意図せずクラッカーの共犯になってしまわぬようアウトバウンドを全て遮断しておくとか対応は必要で、すぐ落とせるサーバに限る)
不審なプロセスが開始された時点でそのプロセスを停止させ、静的な解析もできそう。
どこにどんな情報を送ってるのか知ることができたら通報できるのかしら。
実行中の様子
PID=42 PPID=2 EXE='kworker/R-crypt' START=2025-09-14T11:02:25.139868850+00:00
PID=35 PPID=2 EXE='khungtaskd' START=2025-09-14T11:02:25.139718193+00:00
PID=396 PPID=2 EXE='kworker/R-xfsal' START=2025-09-14T11:02:25.140526150+00:00
PID=545 PPID=2 EXE='kworker/R-nfit' START=2025-09-14T11:02:25.141787322+00:00
PID=790 PPID=1 EXE='/usr/lib/systemd/systemd' START=2025-09-14T11:02:25.143341824+00:00
PID=33 PPID=2 EXE='kworker/R-inet_' START=2025-09-14T11:02:25.139670586+00:00
PID=12 PPID=2 EXE='kworker/u8:1-events_unbound' START=2025-09-14T11:02:25.139241222+00:00
PID=58 PPID=2 EXE='kworker/R-acpi_' START=2025-09-14T11:02:25.140148806+00:00
PID=403 PPID=2 EXE='kworker/R-xfs-l' START=2025-09-14T11:02:25.140700078+00:00
PID=792 PPID=790 EXE='/usr/lib/systemd/systemd' START=2025-09-14T11:02:25.143364356+00:00
PID=9 PPID=2 EXE='kworker/0:0H-kblockd' START=2025-09-14T11:02:25.139192245+00:00
PID=20 PPID=2 EXE='rcu_exp_gp_kthr' START=2025-09-14T11:02:25.139456458+00:00
PID=12779 PPID=12778 EXE='/usr/bin/bash' START=2025-09-14T11:02:25.143589606+00:00
PID=46 PPID=2 EXE='kworker/R-tpm_d' START=2025-09-14T11:02:25.139962341+00:00
PID=16029 PPID=2 EXE='kworker/0:1H-kblockd' START=2025-09-14T11:02:25.143617433+00:00
PID=11 PPID=2 EXE='kworker/R-mm_pe' START=2025-09-14T11:02:25.139216035+00:00
[EXIT ] pid=18175 exe='kworker/0:0-mm_percpu_wq'
いい感じです。
- PID(プロセスID)
- PPID(親プロセスID)
- EXE(実行ファイルパス)
- START(起動時刻)
そして別端末などからコマンドを打ってみます。
$ /bin/pwd
すると
[START] pid=18319 ppid=16803 exe='/usr/bin/pwd'
[EXIT ] pid=18319 exe='/usr/bin/pwd'
pwdコマンドの起動と終了をフックすることができました。
ちなみに /bin/pwd
ではなく pwd
だけだと、bashの組み込みコマンドであるため検知できません。
コード
Cargo.toml
[package]
name = "procwatch"
version = "0.1.0"
edition = "2021"
[dependencies]
chrono = "0.4"
libc = "0.2.175"
nix = { version = "0.27", features = ["net"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
main.rs
use chrono::Local;
use std::{collections::HashMap, fs, io, mem, os::unix::io::AsRawFd};
use nix::sys::socket::{NetlinkAddr};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
struct ProcInfo {
start_time: String,
pid: i32,
ppid: i32,
exe: String,
alive: bool,
}
fn get_comm(pid: i32) -> Option<String> {
let path = format!("/proc/{}/comm", pid);
fs::read_to_string(path).ok().map(|s| s.trim().to_string())
}
fn get_exe_path(pid: i32) -> String {
let path = format!("/proc/{}/exe", pid);
fs::read_link(path)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| String::from("unknown"))
}
fn get_ppid(pid: i32) -> i32 {
let status_path = format!("/proc/{}/status", pid);
if let Ok(content) = fs::read_to_string(status_path) {
for line in content.lines() {
if let Some(rest) = line.strip_prefix("PPid:") {
return rest.trim().parse().unwrap_or(0);
}
}
}
0
}
fn snapshot_processes() -> HashMap<i32, ProcInfo> {
let mut map = HashMap::new();
if let Ok(entries) = fs::read_dir("/proc") {
for e in entries.flatten() {
if let Ok(name) = e.file_name().into_string() {
if let Ok(pid) = name.parse::<i32>() {
let exe = get_exe_path(pid);
let exe = if exe == "unknown" {
get_comm(pid).unwrap_or("unknown".to_string())
} else {
exe
};
let ppid = get_ppid(pid);
map.insert(
pid,
ProcInfo {
start_time: Local::now().to_rfc3339(),
pid,
ppid,
exe,
alive: true,
},
);
}
}
}
}
map
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct CnMsg {
id_idx: u32,
id_val: u32,
seq: u32,
ack: u32,
len: u16,
flags: u16,
}
const CN_IDX_PROC: u32 = 0x1;
const CN_VAL_PROC: u32 = 0x1;
const PROC_CN_MCAST_LISTEN: u32 = 1;
const PROC_EVENT_EXEC: u32 = 0x00000002;
const PROC_EVENT_EXIT: u32 = 0x80000000;
#[repr(C)]
struct ProcEventHeader {
what: u32,
cpu: u32,
timestamp_ns: u64,
}
#[repr(C)]
struct ExecEvent {
process_pid: u32,
process_tgid: u32,
}
#[repr(C)]
struct ExitEvent {
process_pid: u32,
process_tgid: u32,
exit_code: u32,
exit_signal: u32,
}
fn main() -> io::Result<()> {
let mut processes: HashMap<i32, ProcInfo> = snapshot_processes();
for (pid, info) in &processes {
println!(
"PID={} PPID={} EXE='{}' START={}",
pid, info.ppid, info.exe, info.start_time
);
}
let fd = unsafe { libc::socket(libc::AF_NETLINK, libc::SOCK_DGRAM, libc::NETLINK_CONNECTOR) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
let addr = NetlinkAddr::new(0, 1);
nix::sys::socket::bind(fd, &addr)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
#[repr(C)]
struct NlMsg {
nlh: libc::nlmsghdr,
cn: CnMsg,
mcop: u32,
}
let mut msg = NlMsg {
nlh: libc::nlmsghdr {
nlmsg_len: (mem::size_of::<NlMsg>()) as u32,
nlmsg_type: libc::NLMSG_DONE as u16,
nlmsg_flags: 0,
nlmsg_seq: 0,
nlmsg_pid: unsafe { libc::getpid() as u32 },
},
cn: CnMsg {
id_idx: CN_IDX_PROC,
id_val: CN_VAL_PROC,
seq: 0,
ack: 0,
len: mem::size_of::<u32>() as u16,
flags: 0,
},
mcop: PROC_CN_MCAST_LISTEN,
};
let ret = unsafe {
libc::send(
fd,
&msg as *const _ as *const libc::c_void,
mem::size_of::<NlMsg>(),
0,
)
};
if ret < 0 {
return Err(io::Error::last_os_error());
}
let mut buf = vec![0u8; 8192];
loop {
let size = unsafe {
libc::recv(
fd,
buf.as_mut_ptr() as *mut libc::c_void,
buf.len(),
0,
)
};
if size <= 0 {
continue;
}
// skip headers
let header_off = mem::size_of::<libc::nlmsghdr>();
let cn_off = header_off + mem::size_of::<CnMsg>();
if (size as usize) < cn_off + mem::size_of::<u32>() {
continue;
}
let ev = unsafe { &*(buf[cn_off..].as_ptr() as *const ProcEventHeader) };
match ev.what {
PROC_EVENT_EXEC => {
let exec_ev = unsafe {
&*(buf[cn_off + mem::size_of::<ProcEventHeader>()..].as_ptr() as *const ExecEvent)
};
let pid = exec_ev.process_pid as i32;
let exe = get_exe_path(pid);
let exe = if exe == "unknown" {
get_comm(pid).unwrap_or("unknown".to_string())
} else {
exe
};
let ppid = get_ppid(pid);
let info = ProcInfo {
start_time: Local::now().to_rfc3339(),
pid,
ppid,
exe: exe.clone(),
alive: true,
};
processes.insert(pid, info.clone());
println!("[START] pid={} ppid={} exe='{}'", pid, ppid, exe);
}
PROC_EVENT_EXIT => {
let exit_ev = unsafe {
&*(buf[cn_off + mem::size_of::<ProcEventHeader>()..].as_ptr() as *const ExitEvent)
};
let pid = exit_ev.process_pid as i32;
if let Some(info) = processes.get_mut(&pid) {
info.alive = false;
println!("[EXIT ] pid={} exe='{}'", pid, info.exe);
} else {
// プロセス開始をフックできなかったパターン
// 多分だけど、起動時 /proc から既存のプロセスを読み取った後にフックするまでの間で開始されたもの?
println!("[EXIT?] pid={} (unknown)", pid);
}
// alive=false
}
_ => {}
}
}
}