implemented, no test

This commit is contained in:
Pakin 2025-08-25 13:21:06 +07:00
parent 59b801dfe3
commit f2f9f6a2d1
5 changed files with 292 additions and 9 deletions

25
README.md Normal file
View file

@ -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
```

51
adbdguard.json Normal file
View file

@ -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:"
]
}
]
}

View file

@ -1,2 +1,2 @@
#[cfg(all(feature = "ptrace", target_arch = "aarch64"))]
pub mod ptrace;

View file

@ -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<UserPtRegs> {
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<i32, ConnState> = 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(&regs);
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(&regs) as i32;
let buf_ptr = if is_out {
arg1(&regs)
} else {
arg0(&regs)
};
let count = if is_out {
arg2(&regs)
} else {
arg1(&regs)
} 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();
}
}
}
}
}
}
}

View file

@ -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<i32> {
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::<i32>().ok()
}
fn watch_config(path: PathBuf, cfg: Arc<Mutex<Config>>) -> notify::Result<RecommendedWatcher> {
let saved_path = path.clone();
let mut w = notify::recommended_watcher(move |res: Result<notify::Event, _>| {
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));
}
}