├── .gitignore ├── README.md ├── configs └── android.jar ├── pom.xml └── src └── main └── java └── com └── hard └── piscan ├── IntentAnalysis.java ├── Main.java └── PendingIntentChecker.java /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/ 3 | *.iml 4 | .gradle/ 5 | 6 | *DS_Store 7 | gradle/* 8 | gradlew 9 | gradlew.bat 10 | 11 | target/ 12 | classes/ 13 | META-INF/ 14 | 15 | local.properties 16 | 17 | sootOutput/ 18 | out/ 19 | apks/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PendingIntentScan 2 | 3 | ## Usage 4 | ``` 5 | java -jar piscan.jar -f xxx.apk -a xxx/android.jar 6 | ``` 7 | 8 | ## Publications 9 | 10 | [Re-route Your Intent for Privilege Escalation: An Universal Way to Exploit Android PendingIntents in High-profile and System Apps](https://www.blackhat.com/eu-21/briefings/schedule/index.html#re-route-your-intent-for-privilege-escalation-an-universal-way-to-exploit-android-pendingintents-in-high-profile-and-system-apps-24340) 11 | -------------------------------------------------------------------------------- /configs/android.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/h0rd7/PendingIntentScan/77ab17c76b288df8fcc472c2af1933cb7dd2e92b/configs/android.jar -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | pi-scan 8 | PendingIntentScan 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | commons-cli 19 | commons-cli 20 | 1.4 21 | 22 | 23 | org.soot-oss 24 | soot 25 | 4.2.1 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/java/com/hard/piscan/IntentAnalysis.java: -------------------------------------------------------------------------------- 1 | package com.hard.piscan; 2 | 3 | import soot.Body; 4 | import soot.Local; 5 | import soot.SootMethod; 6 | import soot.Unit; 7 | import soot.jimple.*; 8 | import soot.toolkits.graph.BriefUnitGraph; 9 | import soot.toolkits.scalar.ForwardFlowAnalysis; 10 | import soot.toolkits.scalar.Pair; 11 | 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | public class IntentAnalysis extends ForwardFlowAnalysis> { 18 | 19 | private final List initSigs = new ArrayList() { 20 | { 21 | add("()>"); 22 | add("(java.lang.String)>"); 23 | add("(java.lang.String,android.net.Uri)>"); 24 | } 25 | }; 26 | 27 | private final List intentWithPkgSigs = new ArrayList() { 28 | { 29 | add("(android.content.Context,java.lang.Class)>"); 30 | add(""); 31 | add(""); 32 | add(""); 33 | add(""); 34 | add(""); 35 | } 36 | }; 37 | 38 | private final Body body; 39 | private final Map returnStmtMap = new HashMap<>(); 40 | 41 | public IntentAnalysis(Body body) { 42 | super(new BriefUnitGraph(body)); 43 | this.body = body; 44 | doAnalysis(); 45 | } 46 | 47 | public Object getRetAtStmt(Local local, Stmt stmt) { 48 | return getFlowBefore(stmt).get(local); 49 | } 50 | 51 | public String isRetIntentSafe() { 52 | for (Map.Entry entry : returnStmtMap.entrySet()) { 53 | Object ret = getRetAtStmt(entry.getKey(), entry.getValue()); 54 | if (ret instanceof Pair) { 55 | String type = ((Pair) ret).getO1(); 56 | if ("false".equals(type)) { 57 | return type; 58 | } else if (!"true".equals(type)){ 59 | return "unknown"; 60 | } 61 | } 62 | } 63 | return "true"; 64 | } 65 | 66 | @Override 67 | protected void flowThrough(Map in, Unit d, Map out) { 68 | copy(in, out); 69 | if (d instanceof AssignStmt) { 70 | AssignStmt stmt = (AssignStmt) d; 71 | if (!stmt.containsInvokeExpr()) return; 72 | InvokeExpr expr = stmt.getInvokeExpr(); 73 | SootMethod method = stmt.getInvokeExpr().getMethod(); 74 | if (intentWithPkgSigs.contains(method.getSignature())) { 75 | if (expr instanceof VirtualInvokeExpr) { 76 | out.put((Local) ((VirtualInvokeExpr) expr).getBase(), new Pair<>("true", null)); 77 | } else if (expr instanceof SpecialInvokeExpr) { 78 | out.put((Local) ((SpecialInvokeExpr) expr).getBase(), new Pair<>("true", null)); 79 | } 80 | } else if (initSigs.contains(method.getSignature())) { 81 | if (expr instanceof VirtualInvokeExpr) { 82 | out.put((Local) ((VirtualInvokeExpr) expr).getBase(), new Pair<>("false", null)); 83 | } else if (expr instanceof SpecialInvokeExpr) { 84 | out.put((Local) ((SpecialInvokeExpr) expr).getBase(), new Pair<>("false", null)); 85 | } 86 | } else if ("android.content.Intent".equals(method.getReturnType().toString())) { 87 | out.put((Local) stmt.getLeftOp(), new Pair<>("return", method)); 88 | } 89 | } else if (d instanceof InvokeStmt) { 90 | InvokeStmt stmt = (InvokeStmt) d; 91 | InvokeExpr expr = stmt.getInvokeExpr(); 92 | String sig = stmt.getInvokeExpr().getMethod().getSignature(); 93 | if (intentWithPkgSigs.contains(sig)) { 94 | if (expr instanceof VirtualInvokeExpr) { 95 | out.put((Local) ((VirtualInvokeExpr) expr).getBase(), new Pair<>("true", null)); 96 | } else if (expr instanceof SpecialInvokeExpr) { 97 | out.put((Local) ((SpecialInvokeExpr) expr).getBase(), new Pair<>("true", null)); 98 | } 99 | } else if (initSigs.contains(sig)) { 100 | if (expr instanceof VirtualInvokeExpr) { 101 | out.put((Local) ((VirtualInvokeExpr) expr).getBase(), new Pair<>("false", null)); 102 | } else if (expr instanceof SpecialInvokeExpr) { 103 | out.put((Local) ((SpecialInvokeExpr) expr).getBase(), new Pair<>("false", null)); 104 | } 105 | } 106 | } else if (d instanceof ReturnStmt) { 107 | ReturnStmt stmt = (ReturnStmt) d; 108 | if (stmt.getOp() instanceof Local) 109 | returnStmtMap.put((Local) stmt.getOp(), stmt); 110 | } 111 | } 112 | 113 | @Override 114 | protected Map entryInitialFlow() { 115 | Map ret = new HashMap<>(); 116 | List paras = this.body.getParameterLocals(); 117 | int len = paras.size(); 118 | for (int i = 0; i < len; i++) { 119 | ret.put(paras.get(i), new Pair<>("param", i)); 120 | } 121 | return ret; 122 | } 123 | 124 | @Override 125 | protected Map newInitialFlow() { 126 | return new HashMap<>(); 127 | } 128 | 129 | @Override 130 | protected void merge(Map in1, Map in2, Map out) { 131 | out.clear(); 132 | out.putAll(in1); 133 | out.putAll(in2); 134 | } 135 | 136 | @Override 137 | protected void copy(Map source, Map dest) { 138 | if (source == dest) { 139 | return; 140 | } 141 | dest.clear(); 142 | dest.putAll(source); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/com/hard/piscan/Main.java: -------------------------------------------------------------------------------- 1 | package com.hard.piscan; 2 | 3 | import org.apache.commons.cli.*; 4 | 5 | public class Main { 6 | 7 | public static void main(String[] args) { 8 | if (args.length == 0) { 9 | args = new String[]{ 10 | "-f", "apks/video.apk", 11 | "-a", "configs/android.jar", 12 | }; 13 | } 14 | Options options = new Options(); 15 | options.addOption("f", "file", true, "Apk file path to be analysed."); 16 | options.addOption("a", "android", true, "Android jar path."); 17 | CommandLineParser commandLineParser = new DefaultParser(); 18 | try { 19 | CommandLine commandLine = commandLineParser.parse(options, args); 20 | if (commandLine.hasOption("f") && commandLine.hasOption("a")) { 21 | String apkPath = commandLine.getOptionValue("f"); 22 | String androidJar = commandLine.getOptionValue("a"); 23 | new PendingIntentChecker(apkPath, androidJar).doCheck(); 24 | } 25 | } catch (ParseException e) { 26 | e.printStackTrace(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/hard/piscan/PendingIntentChecker.java: -------------------------------------------------------------------------------- 1 | package com.hard.piscan; 2 | 3 | import soot.Main; 4 | import soot.*; 5 | import soot.jimple.IntConstant; 6 | import soot.jimple.InvokeExpr; 7 | import soot.jimple.NullConstant; 8 | import soot.jimple.Stmt; 9 | import soot.options.Options; 10 | import soot.toolkits.scalar.Pair; 11 | 12 | import java.util.*; 13 | 14 | public class PendingIntentChecker { 15 | 16 | private final List excludePkgList = new ArrayList(){ 17 | { 18 | add("android.*"); 19 | add("androidx.*"); 20 | add("soot.*"); 21 | add("java.*"); 22 | add("javax.*"); 23 | add("kotlin.*"); 24 | add("kotlinx.*"); 25 | add("retrofit.*"); 26 | add("retrofit2.*"); 27 | add("sun.*"); 28 | add("org.*"); 29 | add("uk.*"); 30 | add("rx.*"); 31 | add("dalvik.*"); 32 | add("io.*"); 33 | add("okio.*"); 34 | add("okhttp.*"); 35 | add("okhttp3.*"); 36 | add("roboguice.util.*"); 37 | add("de.greenrobot.*"); 38 | add("com.google.android.material.*"); 39 | add("com.google.gson.*"); 40 | add("com.google.protobuf.*"); 41 | add("com.google.firebase.*"); 42 | add("com.squareup.*"); 43 | add("com.nineoldandroids.*"); 44 | add("com.airbnb.lottie.*"); 45 | add("com.bumptech.glide.*"); 46 | add("com.reactnativecommunity.*"); 47 | add("com.facebook.litho.*"); 48 | add("com.facebook.react.*"); 49 | add("com.facebook.profilo.*"); 50 | add("com.horcrux.svg.*"); 51 | add("com.handmark.pulltorefresh.*"); 52 | add("com.tekartik.sqflite.*"); 53 | add("com.swmansion.gesturehandler.*"); 54 | add("com.tbruyelle.rxpermissions.*"); 55 | add("com.trello.rxlifecycle.*"); 56 | add("com.alibaba.fastjson.*"); 57 | } 58 | }; 59 | 60 | private final List piMethodSigs = new ArrayList() { 61 | { 62 | add(""); 63 | add(""); 64 | add(""); 65 | add(""); 66 | add(""); 67 | } 68 | }; 69 | 70 | private final int FLAG_IMMUTABLE = 1<<26; 71 | 72 | List allIntentMethods = new ArrayList<>(); 73 | 74 | public PendingIntentChecker(String apkPath, String androidJarPath) { 75 | initSoot(apkPath, androidJarPath); 76 | } 77 | 78 | public void doCheck() { 79 | Map> piStmts = new HashMap<>(); 80 | for (SootClass sootClass : Scene.v().getApplicationClasses()) { 81 | if (isExcludeClass(sootClass)) continue; 82 | List methods = sootClass.getMethods(); 83 | for (int i = 0; i < methods.size(); i++) { 84 | SootMethod method = methods.get(i); 85 | if (!method.isConcrete()) continue; 86 | try { 87 | if (method.retrieveActiveBody() == null) continue; 88 | } catch (Exception e) { 89 | // e.printStackTrace(); 90 | continue; 91 | } 92 | if (method.getActiveBody().toString().contains("android.content.Intent")) 93 | allIntentMethods.add(method); 94 | List stmts = new ArrayList<>(); 95 | for (Unit unit : method.getActiveBody().getUnits()) { 96 | Stmt stmt = (Stmt) unit; 97 | if (!stmt.containsInvokeExpr()) continue; 98 | InvokeExpr invokeExpr = stmt.getInvokeExpr(); 99 | if (piMethodSigs.contains(invokeExpr.getMethod().getSignature())) { 100 | Value flag = invokeExpr.getArg(3); 101 | if (flag instanceof IntConstant) { 102 | int val = ((IntConstant) flag).value; 103 | if ((FLAG_IMMUTABLE & val) == FLAG_IMMUTABLE) continue; 104 | } 105 | Value intent = invokeExpr.getArg(2); 106 | if (intent instanceof NullConstant) continue; 107 | stmts.add(stmt); 108 | } 109 | } 110 | if (!stmts.isEmpty()) 111 | piStmts.put(method, stmts); 112 | } 113 | } 114 | // System.out.println(allIntentMethods.size()); 115 | // System.out.println(piStmts.size()); 116 | Map unsafeRet = new HashMap<>(); 117 | Map unknownRet = new HashMap<>(); 118 | for (Map.Entry> entry : piStmts.entrySet()) { 119 | IntentAnalysis intentAnalysis = new IntentAnalysis(entry.getKey().getActiveBody()); 120 | for (Stmt stmt : entry.getValue()) { 121 | Object ret = intentAnalysis.getRetAtStmt((Local) stmt.getInvokeExpr().getArg(2), stmt); 122 | if (ret instanceof Pair) { 123 | String type = ((Pair) ret).getO1(); 124 | switch (type) { 125 | case "return": 126 | SootMethod method = ((Pair) ret).getO2(); 127 | if (method.isConcrete()) { 128 | String isRetIntentSafe = new IntentAnalysis(method.getActiveBody()).isRetIntentSafe(); 129 | if ("false".equals(isRetIntentSafe)){ 130 | unsafeRet.put(entry.getKey(), stmt); 131 | break; 132 | } else if ("true".equals(isRetIntentSafe)) { 133 | break; 134 | } 135 | } 136 | unknownRet.put(entry.getKey(), stmt); 137 | break; 138 | case "param": 139 | int index = ((Pair) ret).getO2(); 140 | CallerElements caller = findCaller(entry.getKey(), index); 141 | if (caller != null) { 142 | Object callRet = new IntentAnalysis(caller.method.getActiveBody()).getRetAtStmt(caller.local, caller.stmt); 143 | if (callRet instanceof Pair) { 144 | String type1 = ((Pair) callRet).getO1(); 145 | if ("false".equals(type1)){ 146 | unsafeRet.put(entry.getKey(), stmt); 147 | break; 148 | } else if ("true".equals(type1)){ 149 | break; 150 | } 151 | } 152 | } 153 | unknownRet.put(entry.getKey(), stmt); 154 | break; 155 | case "false": 156 | unsafeRet.put(entry.getKey(), stmt); 157 | break; 158 | case "true": 159 | break; 160 | default: 161 | unknownRet.put(entry.getKey(), stmt); 162 | } 163 | } else { 164 | unknownRet.put(entry.getKey(), stmt); 165 | } 166 | } 167 | } 168 | StringBuilder builder = new StringBuilder(); 169 | if (!unsafeRet.isEmpty()) { 170 | builder.append("unsafe ret:\n"); 171 | for (Map.Entry entry : unsafeRet.entrySet()) { 172 | builder.append("\t").append(entry.getKey()).append("\n"); 173 | builder.append("\t\t").append(entry.getValue()).append("\n\n"); 174 | } 175 | } 176 | if (!unknownRet.isEmpty()) { 177 | builder.append("unknown ret:\n"); 178 | for (Map.Entry entry : unknownRet.entrySet()) { 179 | builder.append("\t").append(entry.getKey()).append("\n"); 180 | builder.append("\t\t").append(entry.getValue()).append("\n\n"); 181 | } 182 | } 183 | System.out.println(builder); 184 | } 185 | 186 | private CallerElements findCaller(SootMethod method, int index) { 187 | for (SootMethod intentMethod : allIntentMethods) { 188 | for (Unit unit : intentMethod.getActiveBody().getUnits()) { 189 | if (unit.toString().contains(method.getSignature())) { 190 | Stmt stmt = (Stmt) unit; 191 | Value value = stmt.getInvokeExpr().getArg(index); 192 | if (value == null || value instanceof NullConstant) continue; 193 | return new CallerElements(intentMethod, (Local) value, stmt); 194 | } 195 | } 196 | } 197 | return null; 198 | } 199 | 200 | private void initSoot(String apkPath, String androidJarPath) { 201 | G.reset(); 202 | 203 | Options.v().set_no_bodies_for_excluded(true); 204 | Options.v().set_allow_phantom_refs(true); 205 | Options.v().set_output_format(Options.output_format_none); 206 | Options.v().set_whole_program(true); 207 | Options.v().set_process_dir(Collections.singletonList(apkPath)); 208 | Options.v().set_force_android_jar(androidJarPath); 209 | Options.v().set_src_prec(Options.src_prec_apk_class_jimple); 210 | Options.v().set_keep_offset(false); 211 | Options.v().set_keep_line_number(true); 212 | Options.v().set_throw_analysis(Options.throw_analysis_dalvik); 213 | Options.v().set_process_multiple_dex(true); 214 | Options.v().set_ignore_resolution_errors(true); 215 | Options.v().set_exclude(excludePkgList); 216 | Options.v().set_no_bodies_for_excluded(true); 217 | Options.v().set_soot_classpath(androidJarPath); 218 | Main.v().autoSetOptions(); 219 | Options.v().setPhaseOption("cg.spark", "on"); 220 | Scene.v().loadNecessaryClasses(); 221 | // PackManager.v().getPack("wjpp").apply(); 222 | } 223 | 224 | private boolean isExcludeClass(SootClass sootClass) { 225 | if (sootClass.isPhantom()) return true; 226 | for (String exclude : excludePkgList) { 227 | if (sootClass.getName().startsWith(exclude.replace("*", ""))) return true; 228 | } 229 | return false; 230 | } 231 | 232 | class CallerElements { 233 | public SootMethod method; 234 | public Local local; 235 | public Stmt stmt; 236 | 237 | public CallerElements(SootMethod method, Local local, Stmt stmt) { 238 | this.method = method; 239 | this.local = local; 240 | this.stmt = stmt; 241 | } 242 | } 243 | 244 | } 245 | --------------------------------------------------------------------------------