├── .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, Integer>) 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 |
--------------------------------------------------------------------------------