package com.googlecode.d2j.ai; import com.googlecode.d2j.Field; import com.googlecode.d2j.Method; import com.googlecode.d2j.node.*; import com.googlecode.d2j.node.insn.ConstStmtNode; import com.googlecode.d2j.node.insn.DexStmtNode; import com.googlecode.d2j.node.insn.FieldStmtNode; import com.googlecode.d2j.node.insn.MethodStmtNode; import com.googlecode.d2j.node.insn.TypeStmtNode; import com.googlecode.d2j.reader.BaseDexFileReader; import com.googlecode.d2j.reader.MultiDexFileReader; import com.googlecode.dex2jar.tools.BaseCmd; import java.nio.file.*; import java.util.*; /** * Reverse cross-reference: find every site that references a target symbol. * *

Method-trace answers "what does method X call?"; xref answers the inverse — * "where is symbol S used, and from which enclosing method?". A reference is any * invoke target, field access, type usage (new-instance / check-cast / …), or * string constant whose textual form contains the query. * *

The query is either a free-text substring (--to) or a named * preset of sensitive-API indicators (--preset crypto|reflection|dynload|net), * which expands to a list of substrings that are OR-matched. Presets turn * "see the structure" into "find the sink" — the first move in most malware triage. */ public class DexXrefCmd extends BaseCmd { @Opt(opt = "t", longOpt = "to", description = "Match references whose symbol contains this substring") String to; @Opt(opt = "p", longOpt = "preset", description = "Sensitive-API preset: crypto | reflection | dynload | net") String preset; @Opt(opt = "k", longOpt = "kinds", description = "Comma-separated reference kinds to include: invoke,field,type,string (default: all)") String kinds; @Opt(opt = "o", longOpt = "output", description = "Output file path (default: stdout)") String output; private static final Map PRESETS = new LinkedHashMap<>(); static { PRESETS.put("crypto", new String[]{ "Ljavax/crypto/", "Ljava/security/", "Cipher", "MessageDigest", "SecretKeySpec", "IvParameterSpec", "Mac", "KeyGenerator", "KeyPairGenerator", "Signature", "PBEKeySpec", }); PRESETS.put("reflection", new String[]{ "Ljava/lang/reflect/", "Ljava/lang/Class;->forName", "->getMethod", "->getDeclaredMethod", "->getDeclaredField", "->getField", "Ljava/lang/reflect/Method;->invoke", "->setAccessible", }); PRESETS.put("dynload", new String[]{ "Ldalvik/system/DexClassLoader", "Ldalvik/system/PathClassLoader", "Ldalvik/system/BaseDexClassLoader", "Ldalvik/system/InMemoryDexClassLoader", "Ljava/lang/System;->load", "Ljava/lang/Runtime;->load", "->loadLibrary", "Ljava/lang/ClassLoader", }); PRESETS.put("net", new String[]{ "Ljava/net/", "Ljavax/net/", "Lokhttp3/", "Lcom/squareup/okhttp", "Lorg/apache/http", "HttpURLConnection", "->openConnection", "Landroid/webkit/WebView", "Ljava/net/Socket", "http://", "https://", }); } @Override protected void doCommandLine() throws Exception { if (remainingArgs == null || remainingArgs.length == 0) { throw new HelpException("No input file specified"); } String[] needles; String queryLabel; if (preset != null && !preset.isEmpty()) { String[] p = PRESETS.get(preset.toLowerCase(Locale.ROOT)); if (p == null) { System.err.println("Unknown preset: " + preset + " (expected one of " + PRESETS.keySet() + ")"); return; } needles = p; queryLabel = "preset:" + preset.toLowerCase(Locale.ROOT); } else if (to != null && !to.isEmpty()) { needles = new String[]{to}; queryLabel = to; } else { throw new HelpException("Specify either --to or --preset "); } Set wantKinds = parseKinds(kinds); Path input = Paths.get(remainingArgs[0]); if (!Files.exists(input)) { System.err.println("File not found: " + input); return; } byte[] data = Files.readAllBytes(input); BaseDexFileReader reader = MultiDexFileReader.open(data); DexFileNode fileNode = new DexFileNode(); reader.accept(fileNode, 0); // need code List matches = new ArrayList<>(); if (fileNode.clzs != null) { for (DexClassNode cn : fileNode.clzs) { if (cn.methods == null) { continue; } for (DexMethodNode mn : cn.methods) { if (mn.codeNode == null || mn.codeNode.stmts == null) { continue; } String inMethod = sig(mn.method); for (DexStmtNode stmt : mn.codeNode.stmts) { String kind = null; String symbol = null; if (stmt instanceof MethodStmtNode) { Method m = ((MethodStmtNode) stmt).method; if (m != null) { kind = "invoke"; symbol = m.getOwner() + "->" + m.getName(); } } else if (stmt instanceof FieldStmtNode) { Field f = ((FieldStmtNode) stmt).field; if (f != null) { kind = "field"; symbol = f.getOwner() + "->" + f.getName() + ":" + f.getType(); } } else if (stmt instanceof TypeStmtNode) { kind = "type"; symbol = ((TypeStmtNode) stmt).type; } else if (stmt instanceof ConstStmtNode) { Object v = ((ConstStmtNode) stmt).value; if (v instanceof String) { kind = "string"; symbol = (String) v; } } if (kind == null || symbol == null) { continue; } if (!wantKinds.contains(kind)) { continue; } if (!matchesAny(symbol, needles)) { continue; } Json.Obj match = Json.obj() .put("kind", kind) .put("symbol", symbol) .put("in_method", inMethod) .put("op", stmt.op == null ? "?" : stmt.op.toString()); matches.add(match); } } } } Json.Arr arr = Json.arr(); for (Json.Obj m : matches) { arr.addRaw(m.build()); } String result = Json.obj() .put("query", queryLabel) .put("match_count", matches.size()) .putRaw("matches", arr.build()) .build(); emit(result); } private static Set parseKinds(String kinds) { Set all = new LinkedHashSet<>(Arrays.asList("invoke", "field", "type", "string")); if (kinds == null || kinds.isEmpty()) { return all; } Set want = new LinkedHashSet<>(); for (String k : kinds.split(",")) { String t = k.trim().toLowerCase(Locale.ROOT); if (all.contains(t)) { want.add(t); } } return want.isEmpty() ? all : want; } private static boolean matchesAny(String haystack, String[] needles) { for (String n : needles) { if (haystack.contains(n)) { return true; } } return false; } private void emit(String result) throws Exception { if (output != null && !output.isEmpty()) { Files.write(Paths.get(output), result.getBytes("UTF-8")); System.out.println("Written to " + output); } else { System.out.println(result); } } private static String sig(Method m) { if (m == null) { return "?"; } StringBuilder sb = new StringBuilder(); sb.append(m.getOwner()).append("->").append(m.getName()).append("("); String[] ps = m.getParameterTypes(); if (ps != null) { for (String p : ps) { sb.append(p); } } sb.append(")").append(m.getReturnType() == null ? "" : m.getReturnType()); return sb.toString(); } }