diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e14c44b..e9602c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,28 +1,21 @@ -# This is a basic workflow to help you get started with Actions - name: CI -# Controls when the workflow will run on: - # Triggers the workflow on push or pull request events but only for the "main" branch push: branches: ["main"] tags: - - "*" + - "v*" pull_request: branches: ["main"] - # Allows you to run this workflow manually from the Actions tab workflow_dispatch: permissions: contents: write -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: build: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 @@ -35,27 +28,21 @@ jobs: - name: build run: cargo ndk --target aarch64-linux-android build --release - # - name: Setup tmate session - # uses: mxschmitt/action-tmate@v3 - - name: move files run: | mv target/aarch64-linux-android/release/linjector-cli ./linjector-cli - xz -9 -k ./linjector-cli - name: save hashes in env run: | echo '```' > hashes.txt echo "SHA256 hashes:" >> hashes.txt sha256sum linjector-cli >> hashes.txt - sha256sum linjector-cli.xz >> hashes.txt echo '```' >> hashes.txt - name: release uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') + if: startsWith(github.ref, 'refs/tags/v') with: body_path: hashes.txt files: | ./linjector-cli - ./linjector-cli.xz diff --git a/Cargo.toml b/Cargo.toml index 475bff0..93ca9b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,15 @@ path = "src/lib.rs" name = "linjector-cli" path = "bin/cli.rs" +[profile.release] +opt-level = "z" +debug = false +lto = true +debug-assertions = false +codegen-units = 1 +panic = "abort" +strip = true + [dependencies] android_logger = "0.13.3" backtrace = "0.3.69" @@ -25,3 +34,4 @@ nix = { version = "0.27.1", features = ["uio"] } pretty-hex = "0.4.0" proc-maps = "0.3.2" simple_logger = "4.3.3" +glob = "0.3.1" \ No newline at end of file diff --git a/README.md b/README.md index 492c248..eff9ce1 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,21 @@ To get an idea of how it works, you can read the [blog post](https://erfur.githu ``` Inject code into a running process using /proc/mem -Usage: linjector-cli [OPTIONS] --pid --file +Usage: linjector-cli [OPTIONS] --file Options: -p, --pid pid of the target process + -a, --app-package-name + target application's package name, (re)start the application and do injection + -f, --file path of the library/shellcode to inject -i, --injection-type type of injection - + [default: raw-dlopen] Possible values: diff --git a/bin/cli.rs b/bin/cli.rs index ed6236d..3763586 100644 --- a/bin/cli.rs +++ b/bin/cli.rs @@ -9,7 +9,11 @@ use simple_logger::SimpleLogger; struct Args { /// pid of the target process #[arg(short, long)] - pid: i32, + pid: Option, + + /// target application's package name, (re)start the application and do injection + #[arg(short, long)] + app_package_name: Option, /// path of the library/shellcode to inject #[arg(short, long)] @@ -57,21 +61,34 @@ fn main() { } else { android_logger::init_once(Config::default().with_max_level(LevelFilter::Info)); } + } else if args.debug { + SimpleLogger::new() + .with_level(LevelFilter::Debug) + .init() + .unwrap(); } else { - if args.debug { - SimpleLogger::new() - .with_level(LevelFilter::Debug) - .init() - .unwrap(); - } else { - SimpleLogger::new() - .with_level(LevelFilter::Info) - .init() - .unwrap(); - } + SimpleLogger::new() + .with_level(LevelFilter::Info) + .init() + .unwrap(); } - let mut injector = match linjector_rs::Injector::new(args.pid) { + let mut target_pid = args.pid.unwrap_or(0); + if target_pid <= 0 { + let Some(name) = args.app_package_name else { + error!("No pid or app_package_name is specified"); + return; + }; + let Ok(app_pid) = linjector_rs::Injector::restart_app_and_get_pid(&name) else { + error!("Can't restart package: {}, or cannot found pid", name); + return; + }; + target_pid = app_pid as i32; + } + + info!("target process pid: {}", target_pid); + + let mut injector = match linjector_rs::Injector::new(target_pid) { Ok(injector) => injector, Err(e) => { error!("Error creating injector: {:?}", e); diff --git a/src/lib.rs b/src/lib.rs index aff131b..cc05ae9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ pub enum InjectionError { FileError, CommandError, ShellcodeError, + PidNotFound, } pub struct Injector { @@ -167,6 +168,15 @@ impl Injector { Ok(self) } + pub fn restart_app_and_get_pid(package_name: &str) -> Result { + let pid = utils::restart_app_and_get_pid(package_name); + if pid > 0 { + Ok(pid) + } else { + Err(InjectionError::PidNotFound) + } + } + pub fn inject(&mut self) -> Result<(), InjectionError> { let file_path = self.prepare_file()?; let proc = remote_proc::RemoteProc::new(self.pid)?; @@ -177,29 +187,22 @@ impl Injector { } info!("build second stage shellcode"); - let second_stage: Vec; - match self.injection_type { - InjectionType::RawDlopen => { - second_stage = shellcode::raw_dlopen_shellcode( - *self.sym_cache.get("dlopen").unwrap(), - file_path, - *self.sym_cache.get("malloc").unwrap(), - ) - .unwrap(); - } - InjectionType::MemFdDlopen => { - second_stage = shellcode::memfd_dlopen_shellcode( - *self.sym_cache.get("dlopen").unwrap(), - *self.sym_cache.get("malloc").unwrap(), - &std::fs::read(file_path.as_str()).unwrap(), - *self.sym_cache.get("sprintf").unwrap(), - ) - .unwrap(); - } - InjectionType::RawShellcode => { - second_stage = shellcode::raw_shellcode().unwrap(); - } - } + let second_stage = match self.injection_type { + InjectionType::RawDlopen => shellcode::raw_dlopen_shellcode( + *self.sym_cache.get("dlopen").unwrap(), + file_path, + *self.sym_cache.get("malloc").unwrap(), + ) + .unwrap(), + InjectionType::MemFdDlopen => shellcode::memfd_dlopen_shellcode( + *self.sym_cache.get("dlopen").unwrap(), + *self.sym_cache.get("malloc").unwrap(), + &std::fs::read(file_path.as_str()).unwrap(), + *self.sym_cache.get("sprintf").unwrap(), + ) + .unwrap(), + InjectionType::RawShellcode => shellcode::raw_shellcode().unwrap(), + }; info!("build first stage shellcode"); let first_stage = @@ -223,6 +226,7 @@ impl Injector { info!("wait for shellcode to trigger"); let mut new_map: u64; loop { + std::thread::sleep(std::time::Duration::from_millis(10)); let data = proc.mem.read(self.target_var_sym_addr, 0x8).unwrap(); // u64 from val new_map = u64::from_le_bytes(data[0..8].try_into().unwrap()); diff --git a/src/remote_module.rs b/src/remote_module.rs index 3ad6c31..698e976 100644 --- a/src/remote_module.rs +++ b/src/remote_module.rs @@ -25,7 +25,7 @@ impl RemoteModule { .iter() .find(|sym| symbol_name == elf.strtab.get_at(sym.st_name).unwrap()); - if !result.is_none() { + if result.is_some() { let offset = result.unwrap().st_value as usize; return Ok(offset + self.vm_addr); } diff --git a/src/utils.rs b/src/utils.rs index a798a60..e941fcd 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,14 @@ use hxdmp::hexdump; use std::io::{ErrorKind, Read}; +use std::process::Output; +use std::str::from_utf8; use crate::InjectionError; +use glob::glob; +use std::thread; +use std::time::Duration; + const HEXDUMP_BUFFER_SIZE: usize = 0x200; const TMP_DIR_PATH: &str = "/data/local/tmp"; @@ -162,3 +168,103 @@ pub fn fix_file_permissions(file_path: &str) -> Result<(), InjectionError> { } } } + +pub fn execute_command(program: &str, args: &Vec<&str>) -> Result { + match std::process::Command::new(program).args(args).output() { + Ok(output) => { + if !output.status.success() { + error!( + "Error running cmd {} {:?} err: {}", + program, + args, + String::from_utf8_lossy(&output.stderr) + ); + Err(InjectionError::CommandError) + } else { + info!("Running cmd successfully: {} {:?}", program, args); + Ok(output) + } + } + Err(e) => { + error!("Error running cmd {} {:?} err: {}", program, args, e); + Err(InjectionError::CommandError) + } + } +} + +pub fn get_pid_by_package(pkg_name: &str) -> std::io::Result { + for entry in glob("/proc/*/cmdline").unwrap() { + match entry { + Ok(path) => { + let mut file = std::fs::File::open(&path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let tmp_name = contents.trim_end_matches('\0'); + if tmp_name == pkg_name { + let path_str = path.to_str().unwrap(); + let pid_str = path_str.split("/").nth(2).unwrap(); + let pid = pid_str.parse::().unwrap(); + return Ok(pid); + } + } + Err(err) => println!("{:?}", err), + } + } + Ok(0) +} + +pub fn get_pid_by_package_with_polling(pkg_name: &str) -> u32 { + let mut _pid: u32 = 0; + + let count = 100; + for _i in 0..count { + _pid = get_pid_by_package(pkg_name).unwrap(); + if _pid > 0 { + break; + } + thread::sleep(Duration::from_micros(500)); + } + + _pid +} + +pub fn restart_app_and_get_pid(pkg_name: &str) -> u32 { + let _ = execute_command("am", &vec!["force-stop", pkg_name]); + // check if this command can start the application + let _ = execute_command( + "monkey", + &vec![ + "-p", + pkg_name, + "-c", + "android.intent.category.LAUNCHER", + "1", + ], + ); + + let mut _pid = get_pid_by_package_with_polling(pkg_name); + + if _pid == 0 { + // try another way to start the application + let get_main_activity_result = execute_command( + "cmd", + &vec![ + "package", + "resolve-activity", + "--brief", + pkg_name, + "|", + "tail", + "-n", + "1", + ], + ) + .unwrap(); + let result_str = from_utf8(&get_main_activity_result.stdout).unwrap(); + let last_line = result_str.lines().last().unwrap(); + let _ = execute_command("am", &vec!["start", last_line]); + _pid = get_pid_by_package_with_polling(pkg_name); + } + + _pid +}