use asc_engine::{CacheConfig, CallQuery, CallRoots, Direction, Engine, names};
use asc_fixture::{ClassSpec, CodeSpec, DexBuilder, MethodSpec, make_apk};
use std::io::Write;
const CALLER: &str = "La/Caller;";
const SECURE: &str = "Lsys/Secure;";
const LOG: &str = "Lsys/Log;";
const BUILD: &str = "Lsys/Build;";
const RES: &str = "Lsys/Res;";
const STR: &str = "Ljava/lang/String;";
fn u16le(v: u32) -> [u8; 2] {
u16::try_from(v).unwrap().to_le_bytes()
}
fn op21c(op: u8, a: u8, idx: u32) -> Vec<u8> {
let mut v = vec![op, a];
v.extend_from_slice(&u16le(idx));
v
}
fn invoke(op: u8, m: u32, regs: &[u8]) -> Vec<u8> {
let r = |i: usize| regs.get(i).copied().unwrap_or(0);
let count = u8::try_from(regs.len()).unwrap();
let mut v = vec![op, (count << 4) | r(4)];
v.extend_from_slice(&u16le(m));
v.extend_from_slice(&[(r(1) << 4) | r(0), (r(3) << 4) | r(2)]);
v
}
const STATIC: u8 = 0x71;
const VIRTUAL: u8 = 0x6e;
const RETURN_VOID: [u8; 2] = [0x0e, 0x00];
fn method(idx: u32, registers: u16, ins: u16, parts: &[Vec<u8>]) -> MethodSpec {
let insns = [parts.concat(), RETURN_VOID.to_vec()].concat();
MethodSpec { idx, access: 0x1 | 0x8, code: Some(CodeSpec { registers, ins, outs: 4, insns, ..Default::default() }) }
}
fn dex() -> Vec<u8> {
let mut d = DexBuilder::new();
d.string("android_id").string("other").string("k").type_("Ljava/lang/Object;");
d.field(BUILD, "SERIAL", STR);
d.method(SECURE, "getString", STR, &[RES, STR])
.method(SECURE, "putLong", "V", &[RES, "J", STR])
.method(SECURE, "query", "V", &[STR])
.method(LOG, "d", "V", &[STR])
.method(LOG, "i", "V", &["I"])
.method(LOG, "c", "V", &["Ljava/lang/Class;"]);
let callers =
[("direct", vec![]), ("param", vec![STR]), ("branchy", vec!["I"]), ("unknown", vec![]), ("wide", vec![]), ("misc", vec![])];
for (name, params) in &callers {
d.method(CALLER, name, "V", params);
}
let mut ids = d.resolve();
let get = ids.method(SECURE, "getString", STR, &[RES, STR]);
let put = ids.method(SECURE, "putLong", "V", &[RES, "J", STR]);
let query = ids.method(SECURE, "query", "V", &[STR]);
let (log_d, log_i, log_c) =
(ids.method(LOG, "d", "V", &[STR]), ids.method(LOG, "i", "V", &["I"]), ids.method(LOG, "c", "V", &["Ljava/lang/Class;"]));
let s = |v: &str| ids.string(v);
let caller = |name: &str, params: &[&str]| ids.method(CALLER, name, "V", params);
let methods = vec![
method(
caller("direct", &[]),
2,
0,
&[
vec![0x12, 0x01],
op21c(0x1a, 0, s("android_id")),
invoke(STATIC, get, &[1, 0]),
vec![0x0c, 0x00],
invoke(STATIC, log_d, &[0]),
],
),
method(caller("param", &[STR]), 3, 1, &[vec![0x12, 0x01], invoke(STATIC, get, &[1, 2])]),
method(
caller("branchy", &["I"]),
3,
1,
&[
vec![0x12, 0x01],
op21c(0x1a, 0, s("other")),
vec![0x38, 0x02, 0x04, 0x00], op21c(0x1a, 0, s("android_id")),
invoke(STATIC, get, &[1, 0]),
],
),
method(caller("unknown", &[]), 2, 0, &[vec![0x12, 0x01], vec![0x46, 0x00, 0x00, 0x01], invoke(STATIC, get, &[1, 0])]),
method(
caller("wide", &[]),
4,
0,
&[vec![0x12, 0x00], vec![0x16, 0x01, 42, 0], op21c(0x1a, 3, s("k")), invoke(STATIC, put, &[0, 1, 2, 3])],
),
method(
caller("misc", &[]),
2,
0,
&[
op21c(0x22, 0, u32::from(ids.type_(SECURE))),
op21c(0x62, 1, ids.field(BUILD, "SERIAL", STR)),
invoke(VIRTUAL, query, &[0, 1]),
vec![0x13, 0x00, 0xe8, 0x03], invoke(STATIC, log_i, &[0]),
op21c(0x1c, 0, u32::from(ids.type_(SECURE))),
invoke(STATIC, log_c, &[0]),
],
),
];
ids.add_class(ClassSpec {
descriptor: CALLER.into(),
access: 0x1,
superclass: Some("Ljava/lang/Object;".into()),
direct_methods: methods,
..Default::default()
});
ids.finish()
}
fn temp_apk() -> tempfile::NamedTempFile {
let mut f = tempfile::Builder::new().suffix(".apk").tempfile().unwrap();
f.write_all(&make_apk(&[("classes.dex", &dex())], true)).unwrap();
f.flush().unwrap();
f
}
fn rows(engine: &Engine, rule: &str) -> Vec<String> {
let q = names::parse_rule(rule).unwrap();
let mut out = Vec::new();
engine
.find_refs(&q, |b| {
assert!(b.error.is_none(), "{:?}", b.error);
out.extend(b.results.iter().map(|r| {
let name = r.method.rsplit("->").next().unwrap().to_string();
let matched: Vec<String> =
r.matched.iter().map(|m| m.as_str().split_once(' ').map_or("", |(_, n)| n).to_string()).collect();
format!("{name} {}", matched.join(","))
}))
})
.unwrap();
out.sort();
out
}
#[test]
fn argument_values_keep_display_and_mark_sites() {
let apk = temp_apk();
let cached = CacheConfig::default().memory_bytes(64 << 20);
for engine in [Engine::new(apk.path()).with_threads(2), Engine::new(apk.path()).with_threads(2).with_cache(cached)] {
let get = "method:sys.Secure->getString";
assert_eq!(
rows(&engine, &format!("{get}[arg1=*]")),
["branchy [arg1=\"android_id\"|\"other\"]", "direct [arg1=\"android_id\"]", "param [arg1=param0]", "unknown [arg1=?]",]
);
assert_eq!(
rows(&engine, &format!("{get}[arg1=\"android_id\"]")),
["branchy [arg1=\"android_id\"|\"other\"]", "direct [arg1=\"android_id\"]", "unknown ?[arg1=?]"]
);
assert_eq!(
rows(&engine, &format!("{get}[arg1==\"android_id\"]")),
["branchy [arg1=\"android_id\"|\"other\"]", "direct [arg1=\"android_id\"]"]
);
assert_eq!(rows(&engine, &format!("{get}[arg1==~^and.*_id$]")), rows(&engine, &format!("{get}[arg1==\"android_id\"]")));
assert_eq!(rows(&engine, &format!("{get}[arg1==param]")), ["param [arg1=param0]"]);
assert_eq!(rows(&engine, &format!("{get}[arg1==param0, arg0==null]")), ["param [arg1=param0, arg0=0]"]);
assert!(rows(&engine, &format!("{get}[arg1==param2]")).is_empty());
assert_eq!(rows(&engine, &format!("{get}[ret==used]")), ["direct [ret=used]"]);
assert_eq!(
rows(&engine, &format!("{get}[ret=unused][arg1=\"other\"]")),
["branchy [ret=unused, arg1=\"android_id\"|\"other\"]", "unknown ?[ret=unused, arg1=?]"]
);
assert_eq!(rows(&engine, "method:sys.Log->d[arg0==ret:sys.Secure->getString]"), ["direct [arg0=ret:Lsys/Secure;->getString]"]);
assert_eq!(rows(&engine, "method:sys.Secure->query[arg0==field:sys.Build->SERIAL]"), ["misc [arg0=field:Lsys/Build;->SERIAL]"]);
assert_eq!(rows(&engine, "method:sys.Secure->query[this==new:sys.Secure]"), ["misc [this=new:Lsys/Secure;]"]);
assert_eq!(rows(&engine, "method:sys.Log->c[arg0==class:sys.Secure]"), ["misc [arg0=class:Lsys/Secure;]"]);
assert_eq!(rows(&engine, "method:sys.Log->i[arg0==0x3e8]"), ["misc [arg0=1000]"]);
assert!(rows(&engine, "method:sys.Log->i[arg0==true]").is_empty());
assert_eq!(rows(&engine, "method:sys.Secure->putLong[arg1==42, arg2==\"k\"]"), ["wide [arg1=42, arg2=\"k\"]"]);
assert!(rows(&engine, &format!("{get}[this=*]")).is_empty());
assert!(rows(&engine, &format!("{get}[arg7=*]")).is_empty());
}
}
#[test]
fn constraints_compose_with_rules_nesting_and_call_graphs() {
let apk = temp_apk();
let engine = Engine::new(apk.path()).with_threads(2);
assert_eq!(
rows(&engine, "method:sys.Secure->getString[arg1==\"android_id\"] & !method:sys.Log->d"),
["branchy [arg1=\"android_id\"|\"other\"]"]
);
let q = names::parse_rule("method:{method:sys.Secure->getString[arg1==param]}").unwrap();
assert!(matches!(q, asc_findrefs::Query::Virtual { target, .. } if matches!(*target, asc_findrefs::Query::Nested(_))));
let rule = names::parse_rule("method:sys.Secure->getString[arg1==\"android_id\"]").unwrap();
let t = engine.call_graph(&CallQuery::new(CallRoots::Matching(rule), Direction::Callers).depth(1)).unwrap();
let mut roots: Vec<String> = t.lines().into_iter().filter(|l| l.starts_with("0 ")).collect();
roots.sort();
assert_eq!(roots, ["0 La/Caller;->branchy(I)V", "0 La/Caller;->direct()V"]);
let rules = [names::parse_rule("method:sys.Log->i[arg0==1000]").unwrap(), names::parse_rule("method:sys.Log->i[arg0==1]").unwrap()];
let mut per_rule = [0; 2];
engine
.find_refs_rules(&rules, asc_engine::FindOptions::default(), &std::sync::atomic::AtomicBool::new(false), &mut |i, b| {
per_rule[i] += b.results.len()
})
.unwrap();
assert_eq!(per_rule, [1, 0]);
}
#[test]
fn result_cache_keys_on_the_constraints() {
let apk = temp_apk();
let dir = tempfile::tempdir().unwrap();
let cached = || {
let disk = asc_engine::asc_cache::DiskCache::open(dir.path().join("cache")).with_capacity(1 << 30).with_min_dex_bytes(0);
Engine::new(apk.path()).with_threads(2).with_cache(CacheConfig::default().memory_bytes(64 << 20).disk(Some(disk)))
};
let (hit, miss) = ("method:sys.Log->i[arg0==1000]", "method:sys.Log->i[arg0==1]");
let engine = cached();
assert_eq!(rows(&engine, hit).len(), 1);
assert!(rows(&engine, miss).is_empty());
engine.flush_cache_writes();
let engine = cached();
for (rule, want) in [(hit, 1), (miss, 0)] {
let mut sources = Vec::new();
let mut n = 0;
engine
.find_refs(&names::parse_rule(rule).unwrap(), |b| {
sources.push(b.source);
n += b.results.len();
})
.unwrap();
assert_eq!(n, want, "{rule}");
assert!(sources.iter().all(|s| *s == "result"), "{rule}: {sources:?}");
}
}