diff --git a/README.md b/README.md new file mode 100644 index 0000000..b0a116a --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# ADBD Guard + +A daemon for aarch64 android to monitor on tcp stream of adbd and notify if suspicious actions are detected. + +NOTE: some part of libs may not available, so try dev on linux. + + +## Build + +### Install Cargo NDK +```bash +cargo install cargo-ndk +``` + +### Install NDK +``` +https://developer.android.com/ndk/downloads +``` + +### Build Script + +```bash +#!/bin/bash +ANDROID_NDK_HOME=/path/to/ndk_top_level cargo ndk --platform 30 -t aarch64-linux-android build --release +``` \ No newline at end of file diff --git a/adbdguard.json b/adbdguard.json new file mode 100644 index 0000000..bb430d6 --- /dev/null +++ b/adbdguard.json @@ -0,0 +1,51 @@ +{ + "debounce_ms": 3000, + "max_payload_peek": 256, + "broadcast_action": "com.adbguard.ALERT", + "rules": [ + { + "name": "exec_any", + "when": "service", + "contains": [ + "exec:" + ] + }, + { + "name": "danger_shell", + "when": "service", + "contains": [ + "shell:" + ], + "patterns": "(su|setenforce|mount|dd|pm\\s+grant|appops|iptables|chcon)\\b" + }, + { + "name":"abb_pm", + "when": "service", + "contains": [ + "abb_exec:pm " + ] + }, + { + "name": "sync_sensitive", + "when": "service", + "contains": [ + "sync:" + ], + "patterns": "/(system|vendor|data/system|data/adb)/" + }, + { + "name": "remount", + "when": "service", + "contains": [ + "remount" + ] + }, + { + "name": "tcpip", + "when":"service", + "contains": [ + "tcpip:" + ] + } + ] +} \ No newline at end of file diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 4749214..e1bb49a 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,2 +1,2 @@ -#[cfg(all(feature = "ptrace", target_arch = "aarch64"))] + pub mod ptrace; diff --git a/src/backend/ptrace.rs b/src/backend/ptrace.rs index b183ca2..d14f12b 100644 --- a/src/backend/ptrace.rs +++ b/src/backend/ptrace.rs @@ -11,19 +11,163 @@ use nix::{ unistd::Pid, }; use std::{ - collections::HashMap, - time::{Duration, Instant}, + collections::HashMap, hash::Hash, time::{Duration, Instant} }; -const PTRACE_GETREGSET_FALLBACK: i32 = 0x4204; -const NT_PRSTATUS_FALLBACK: i32 = 1; +const PTRACE_GETREGSET_FALLBACK: libc::c_int = 0x4204; +const PTRACE_SEIZE_FALLBACK: libc::c_int = 0x4206; +const PTRACE_INTERRUPT: libc::c_int = 0x4207; +const NT_PRSTATUS_FALLBACK: libc::c_int = 1; + -#[cfg(target_arch = "aarch64")] #[repr(C)] #[derive(Clone, Copy, Default)] -struct UserAtRegs { +struct UserPtRegs { regs: [u64; 31], sp: u64, pc: u64, pstate: u64, } + +fn get_regs(pid: Pid) -> Result { + let mut regs = UserPtRegs::default(); + unsafe { + let r = libc::ptrace( + PTRACE_GETREGSET_FALLBACK.try_into().unwrap(), + pid.as_raw(), + NT_PRSTATUS_FALLBACK, + &mut regs as *mut _, + ); + if r < 0 { + return Err(anyhow::anyhow!("GETREGSET failed")); + } + } + Ok(regs) +} + +// x8 holds syscall +fn syscall_no(regs: &UserPtRegs) -> u64 { + regs.regs[8] +} + +// x0 1st arg +fn arg0(regs: &UserPtRegs) -> u64 { + regs.regs[0] +} + +// x1 buf/count +fn arg1(regs: &UserPtRegs) -> u64 { + regs.regs[1] +} + +fn arg2(regs: &UserPtRegs) -> u64 { + regs.regs[2] +} + +const SYS_write: u64 = 64; +const SYS_sendto: u64 = 206; +const SYS_read: u64 = 63; +const SYS_recvfrom: u64 = 207; + +struct ConnState { + buf: BytesMut, + last_alert: Instant +} + +impl ConnState { + fn new() -> Self { + Self { + buf: BytesMut::with_capacity(4096), + last_alert: Instant::now() - Duration::from_secs(3600) + } + } +} + +unsafe fn ptrace_cmd(cmd: libc::c_int, pid: Pid, addr: usize) -> nix::Result<()> { + let mut regs = UserPtRegs::default(); + let r = unsafe { libc::ptrace(cmd.try_into().unwrap(), pid.as_raw(), addr as *mut libc::c_int, &mut regs as *mut _) }; + if r == -1 { + return Err(nix::errno::Errno::last()); + } + + Ok(()) +} + +pub fn run(pid: i32, rules: RuleSet, action: String, debounce: Duration, peek: usize) -> Result<()> { + let pid = Pid::from_raw(pid); + // cannot compiled + // ptrace::seize(pid, ptrace::Options::PTRACE_O_TRACESYSGOOD)?; + + unsafe { + ptrace_cmd(PTRACE_SEIZE_FALLBACK.try_into().unwrap(), pid, 0)?; + } + + // cannot compiled + // ptrace::interrupt(pid)?; + + unsafe { + ptrace_cmd(PTRACE_INTERRUPT.try_into().unwrap(), pid, 0)?; + } + + waitpid(pid, None); + + let mut conns: HashMap = HashMap::new(); + + loop { + ptrace::syscall(pid, None)?; + let st = waitpid(pid, Some(WaitPidFlag::__WALL))?; + let WSIG = |ws: &WaitStatus| matches!(ws, WaitStatus::PtraceSyscall(_)); + if !WSIG(&st){continue;} + + // syscall entry + let regs = get_regs(pid)?; + let scn = syscall_no(®s); + let is_out = scn == SYS_write || scn == SYS_sendto; + let is_in = scn == SYS_read || scn == SYS_recvfrom; + if !(is_in || is_out){ + continue; + } + + let fd = arg0(®s) as i32; + let buf_ptr = if is_out { + arg1(®s) + } else { + arg0(®s) + }; + let count = if is_out { + arg2(®s) + } else { + arg1(®s) + } as usize; + let take = count.min(peek); + + // syscall run to exit, return value + ptrace::syscall(pid, None)?; + let _ = waitpid(pid, Some(WaitPidFlag::__WALL))?; + + // copy user buffer + if take > 0 { + let mut data = vec![0u8; take]; + let local_iov = libc::iovec { iov_base: data.as_mut_ptr() as *mut _, iov_len: take}; + let remote_iov = libc::iovec { iov_base: buf_ptr as *mut _, iov_len: take}; + unsafe { + let n = libc::process_vm_readv(pid.as_raw(), &local_iov as *const _, 1, &remote_iov as *const _, 1, 0); + if n as isize <= 0 {continue;} + } + let s = conns.entry(fd).or_insert_with(ConnState::new); + s.buf.extend_from_slice(&data); + + while let Some(frame) = adb::try_parse(&mut s.buf){ + if frame.cmd == adb::CMD_OPEN { + let service = String::from_utf8_lossy(&frame.payload).to_string(); + if let Some(rule) = rules.match_service(&service) { + if s.last_alert.elapsed() >= debounce { + broadcaster::broadcast(&action, &service, rule); + s.last_alert = Instant::now(); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 3fb6ac7..c824823 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,72 @@ +use std::{path::PathBuf, sync::{Arc, Mutex}, time::Duration}; + + +use config::Config; +use notify::{RecommendedWatcher, Watcher}; +use rule::RuleSet; +use tracing::{info, error}; +use tracing_subscriber::FmtSubscriber; + mod adb; mod backend; mod broadcaster; mod config; mod rule; -fn main() { - println!("Hello, world!"); +fn pidof(name: &str) -> Option { + let out = std::process::Command::new("pidof").arg(name).output().ok()?; + let s = String::from_utf8_lossy(&out.stdout).trim().to_string(); + s.split_whitespace().next()?.parse::().ok() +} + +fn watch_config(path: PathBuf, cfg: Arc>) -> notify::Result { + let saved_path = path.clone(); + let mut w = notify::recommended_watcher(move |res: Result| { + if let Ok(event) = res { + if event.kind.is_modify() { + if let Ok(newc) = Config::load(path.clone().to_str().unwrap()){ + *cfg.lock().unwrap() = newc; + } + } + } + })?; + w.watch(&saved_path, notify::RecursiveMode::NonRecursive)?; + Ok(w) +} + +fn main() -> anyhow::Result<()> { + let sub = FmtSubscriber::builder().without_time().with_target(false).finish(); + tracing::subscriber::set_global_default(sub).ok(); + + let mut args = std::env::args().skip(1); + let cfg_path = args.next().filter(|s| s=="--config").and_then(|_| args.next()).unwrap_or_else(|| "/data/adb/modules/adbguard/adbguard.json".into()); + let cfg0 = Config::load(&cfg_path)?; + let cfg = Arc::new(Mutex::new(cfg0)); + let _w = watch_config((&cfg_path).into(), cfg.clone()).ok(); + + loop { + let pid = loop { + if let Some(p) = pidof("adbd"){ + break p; + } + std::thread::sleep(Duration::from_secs(1)); + }; + + info!("attaching to adbd {}", pid); + + let snapshot = { + cfg.lock().unwrap().clone() + }; + let rules = RuleSet::from_cfg(&snapshot); + let action = snapshot.broadcast_action.clone(); + + #[cfg(feature = "ptrace")] + { + if let Err(e) = backend::ptrace::run(pid, rules, action, snapshot.debounce(), snapshot.max_payload_peek){ + error!("backend exited, {e:?}"); + } + } + std::thread::sleep(Duration::from_secs(1)); + + } }