package com.googlecode.d2j.ai; import com.googlecode.d2j.Method; import com.googlecode.d2j.node.*; import com.googlecode.d2j.node.insn.DexStmtNode; import com.googlecode.d2j.node.insn.MethodStmtNode; 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.*; /** * Build a method call graph from a DEX/APK file. * *

For each method, lists the methods it invokes. Optionally restrict to * callers/callees matching a target signature to answer "who calls X?". */ public class DexMethodTraceCmd extends BaseCmd { @Opt(opt = "c", longOpt = "class", description = "Only trace methods in classes matching this pattern (substring)") String classFilter; @Opt(opt = "t", longOpt = "target", description = "Find all callers of methods matching this pattern (substring on owner.name)") String target; @Opt(opt = "o", longOpt = "output", description = "Output file path (default: stdout)") String output; @Override protected void doCommandLine() throws Exception { if (remainingArgs == null || remainingArgs.length == 0) { throw new HelpException("No input file specified"); } 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 // caller signature -> set of callee signatures List edges = new ArrayList<>(); int edgeCount = 0; if (fileNode.clzs != null) { for (DexClassNode cn : fileNode.clzs) { if (classFilter != null && !classFilter.isEmpty() && !cn.className.contains(classFilter)) { continue; } if (cn.methods == null) continue; for (DexMethodNode mn : cn.methods) { if (mn.codeNode == null || mn.codeNode.stmts == null) continue; String caller = sig(mn.method); for (DexStmtNode stmt : mn.codeNode.stmts) { if (stmt instanceof MethodStmtNode) { Method callee = ((MethodStmtNode) stmt).method; if (callee == null) continue; String calleeSig = sig(callee); if (target != null && !target.isEmpty()) { String needle = callee.getOwner() + "." + callee.getName(); if (!needle.contains(target)) continue; } edges.add(new String[]{caller, calleeSig}); edgeCount++; } } } } } StringBuilder sb = new StringBuilder(); sb.append("{\n"); sb.append(" \"edge_count\": ").append(edgeCount).append(",\n"); if (target != null && !target.isEmpty()) { sb.append(" \"target\": ").append(jsonStr(target)).append(",\n"); } sb.append(" \"edges\": ["); for (int i = 0; i < edges.size(); i++) { if (i > 0) sb.append(","); String[] e = edges.get(i); sb.append("\n {\"caller\": ").append(jsonStr(e[0])) .append(", \"callee\": ").append(jsonStr(e[1])).append("}"); } if (!edges.isEmpty()) sb.append("\n "); sb.append("]\n}"); emit(sb.toString()); } 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(); } private static String jsonStr(String s) { return Json.str(s); } }