1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.security.cts;
18 
19 import com.android.compatibility.common.util.CrashUtils;
20 import com.android.ddmlib.NullOutputReceiver;
21 import com.android.tradefed.device.CollectingOutputReceiver;
22 import com.android.tradefed.device.ITestDevice;
23 import com.android.tradefed.device.NativeDevice;
24 import com.android.tradefed.log.LogUtil.CLog;
25 
26 import java.io.BufferedOutputStream;
27 import java.io.File;
28 import java.io.FileOutputStream;
29 import java.io.InputStream;
30 import java.io.OutputStream;
31 import java.util.concurrent.TimeoutException;
32 import java.util.List;
33 import java.util.regex.Pattern;
34 import java.util.concurrent.TimeUnit;
35 import java.util.Scanner;
36 import java.util.Arrays;
37 import java.util.ArrayList;
38 import java.util.concurrent.Callable;
39 
40 import org.json.JSONArray;
41 import org.json.JSONException;
42 import org.json.JSONObject;
43 
44 import java.util.regex.Pattern;
45 import java.lang.Thread;
46 import static org.junit.Assert.*;
47 import junit.framework.Assert;
48 
49 public class AdbUtils {
50 
51     final static String TMP_PATH = "/data/local/tmp/";
52     final static int TIMEOUT_SEC = 9 * 60;
53     final static String RESOURCE_ROOT = "/";
54 
55     /** Runs a commandline on the specified device
56      *
57      * @param command the command to be ran
58      * @param device device for the command to be ran on
59      * @return the console output from running the command
60      */
runCommandLine(String command, ITestDevice device)61     public static String runCommandLine(String command, ITestDevice device) throws Exception {
62         if ("reboot".equals(command)) {
63             throw new IllegalArgumentException(
64                     "You called a forbidden command! Please fix your tests.");
65         }
66         return device.executeShellCommand(command);
67     }
68 
69     /**
70      * Pushes and runs a binary to the selected device
71      *
72      * @param pocName name of the poc binary
73      * @param device device to be ran on
74      * @return the console output from the binary
75      */
runPoc(String pocName, ITestDevice device)76     public static String runPoc(String pocName, ITestDevice device) throws Exception {
77         device.executeShellCommand("chmod +x /data/local/tmp/" + pocName);
78         return device.executeShellCommand("/data/local/tmp/" + pocName);
79     }
80 
81     /**
82      * Pushes and runs a binary to the selected device
83      *
84      * @param pocName name of the poc binary
85      * @param device device to be ran on
86      * @param timeout time to wait for output in seconds
87      * @return the console output from the binary
88      */
runPoc(String pocName, ITestDevice device, int timeout)89     public static String runPoc(String pocName, ITestDevice device, int timeout) throws Exception {
90         return runPoc(pocName, device, timeout, null);
91     }
92 
93     /**
94      * Pushes and runs a binary to the selected device
95      *
96      * @param pocName name of the poc binary
97      * @param device device to be ran on
98      * @param timeout time to wait for output in seconds
99      * @param arguments the input arguments for the poc
100      * @return the console output from the binary
101      */
runPoc(String pocName, ITestDevice device, int timeout, String arguments)102     public static String runPoc(String pocName, ITestDevice device, int timeout, String arguments)
103             throws Exception {
104         device.executeShellCommand("chmod +x /data/local/tmp/" + pocName);
105         CollectingOutputReceiver receiver = new CollectingOutputReceiver();
106         if (arguments != null) {
107             device.executeShellCommand("/data/local/tmp/" + pocName + " " + arguments, receiver,
108                     timeout, TimeUnit.SECONDS, 0);
109         } else {
110             device.executeShellCommand("/data/local/tmp/" + pocName, receiver, timeout,
111                     TimeUnit.SECONDS, 0);
112         }
113         String output = receiver.getOutput();
114         return output;
115     }
116 
117     /**
118      * Pushes and runs a binary to the selected device and ignores any of its output.
119      *
120      * @param pocName name of the poc binary
121      * @param device device to be ran on
122      * @param timeout time to wait for output in seconds
123      */
runPocNoOutput(String pocName, ITestDevice device, int timeout)124     public static void runPocNoOutput(String pocName, ITestDevice device, int timeout)
125             throws Exception {
126         runPocNoOutput(pocName, device, timeout, null);
127     }
128 
129     /**
130      * Pushes and runs a binary with arguments to the selected device and
131      * ignores any of its output.
132      *
133      * @param pocName name of the poc binary
134      * @param device device to be ran on
135      * @param timeout time to wait for output in seconds
136      * @param arguments input arguments for the poc
137      */
runPocNoOutput(String pocName, ITestDevice device, int timeout, String arguments)138     public static void runPocNoOutput(String pocName, ITestDevice device, int timeout,
139             String arguments) throws Exception {
140         device.executeShellCommand("chmod +x /data/local/tmp/" + pocName);
141         NullOutputReceiver receiver = new NullOutputReceiver();
142         if (arguments != null) {
143             device.executeShellCommand("/data/local/tmp/" + pocName + " " + arguments, receiver,
144                     timeout, TimeUnit.SECONDS, 0);
145         } else {
146             device.executeShellCommand("/data/local/tmp/" + pocName, receiver, timeout,
147                     TimeUnit.SECONDS, 0);
148         }
149     }
150 
151     /**
152      * Enables malloc debug on a given process.
153      *
154      * @param processName the name of the process to run with libc malloc debug
155      * @param device the device to use
156      * @return true if enabling malloc debug succeeded
157      */
enableLibcMallocDebug(String processName, ITestDevice device)158     public static boolean enableLibcMallocDebug(String processName, ITestDevice device) throws Exception {
159         device.executeShellCommand("setprop libc.debug.malloc.program " + processName);
160         device.executeShellCommand("setprop libc.debug.malloc.options \"backtrace guard\"");
161         /**
162          * The pidof command is being avoided because it does not exist on versions before M, and
163          * it behaves differently between M and N.
164          * Also considered was the ps -AoPID,CMDLINE command, but ps does not support options on
165          * versions before O.
166          * The [^]] prefix is being used for the grep command to avoid the case where the output of
167          * ps includes the grep command itself.
168          */
169         String cmdOut = device.executeShellCommand("ps -A | grep '[^]]" + processName + "'");
170         /**
171          * .hasNextInt() checks if the next token can be parsed as an integer, not if any remaining
172          * token is an integer.
173          * Example command: $ ps | fgrep mediaserver
174          * Out: media     269   1     77016  24416 binder_thr 00f35142ec S /system/bin/mediaserver
175          * The second field of the output is the PID, which is needed to restart the process.
176          */
177         Scanner s = new Scanner(cmdOut).useDelimiter("\\D+");
178         if(!s.hasNextInt()) {
179             CLog.w("Could not find pid for process: " + processName);
180             return false;
181         }
182 
183         String result = device.executeShellCommand("kill -9 " + s.nextInt());
184         if(!result.equals("")) {
185             CLog.w("Could not restart process: " + processName);
186             return false;
187         }
188 
189         TimeUnit.SECONDS.sleep(1);
190         return true;
191     }
192 
193     /**
194      * Pushes and installs an apk to the selected device
195      *
196      * @param pathToApk a string path to apk from the /res folder
197      * @param device device to be ran on
198      * @return the output from attempting to install the apk
199      */
installApk(String pathToApk, ITestDevice device)200     public static String installApk(String pathToApk, ITestDevice device) throws Exception {
201 
202         String fullResourceName = pathToApk;
203         File apkFile = File.createTempFile("apkFile", ".apk");
204         try {
205             apkFile = extractResource(fullResourceName, apkFile);
206             return device.installPackage(apkFile, true);
207         } finally {
208             apkFile.delete();
209         }
210     }
211 
212     /**
213      * Extracts a resource and pushes it to the device
214      *
215      * @param fullResourceName a string path to resource from the res folder
216      * @param deviceFilePath the remote destination absolute file path
217      * @param device device to be ran on
218      */
pushResource(String fullResourceName, String deviceFilePath, ITestDevice device)219     public static void pushResource(String fullResourceName, String deviceFilePath,
220                                     ITestDevice device) throws Exception {
221         File resFile = File.createTempFile("CTSResource", "");
222         try {
223             resFile = extractResource(fullResourceName, resFile);
224             device.pushFile(resFile, deviceFilePath);
225         } finally {
226             resFile.delete();
227         }
228     }
229 
230     /**
231      * Pushes the specified files to the specified destination directory
232      *
233      * @param inputFiles files required as input
234      * @param inputFilesDestination destination directory to which input files are
235      *        pushed
236      * @param device device to be run on
237      */
pushResources(String[] inputFiles, String inputFilesDestination, ITestDevice device)238     public static void pushResources(String[] inputFiles, String inputFilesDestination,
239             ITestDevice device) throws Exception {
240         if ( (inputFiles != null) && (inputFilesDestination != null)) {
241             for (String tempFile : inputFiles) {
242                 pushResource(RESOURCE_ROOT + tempFile, inputFilesDestination + tempFile, device);
243             }
244         }
245     }
246 
247     /**
248      * Removes the specified files from the specified destination directory
249      *
250      * @param inputFiles files required as input
251      * @param inputFilesDestination destination directory where input files are
252      *        present
253      * @param device device to be run on
254      */
removeResources(String[] inputFiles, String inputFilesDestination, ITestDevice device)255     public static void removeResources(String[] inputFiles, String inputFilesDestination,
256             ITestDevice device) throws Exception {
257         if ( (inputFiles != null) && (inputFilesDestination != null)) {
258             for (String tempFile : inputFiles) {
259                 runCommandLine("rm " + inputFilesDestination + tempFile, device);
260             }
261         }
262     }
263 
264    /**
265      * Extracts the binary data from a resource and writes it to a temp file
266      */
extractResource(String fullResourceName, File file)267     private static File extractResource(String fullResourceName, File file) throws Exception {
268         try (InputStream in = AdbUtils.class.getResourceAsStream(fullResourceName);
269             OutputStream out = new BufferedOutputStream(new FileOutputStream(file))) {
270             if (in == null) {
271                 throw new IllegalArgumentException("Resource not found: " + fullResourceName);
272             }
273             byte[] buf = new byte[65536];
274             int chunkSize;
275             while ((chunkSize = in.read(buf)) != -1) {
276                 out.write(buf, 0, chunkSize);
277             }
278             return file;
279         }
280 
281     }
282     /**
283      * Utility function to help check the exit code of a shell command
284      */
runCommandGetExitCode(String cmd, ITestDevice device)285     public static int runCommandGetExitCode(String cmd, ITestDevice device) throws Exception {
286         long time = System.currentTimeMillis();
287         String exitStatus = runCommandLine(
288                 "(" + cmd + ") > /dev/null 2>&1; echo $?", device).trim();
289         time = System.currentTimeMillis() - time;
290         try {
291             return Integer.parseInt(exitStatus);
292         } catch (NumberFormatException e) {
293             throw new IllegalArgumentException(String.format(
294                     "Could not get the exit status (%s) for '%s' (%d ms).",
295                     exitStatus, cmd, time));
296         }
297     }
298 
299     /**
300      * Pushes and runs a binary to the selected device and checks exit code
301      * Return code 113 is used to indicate the vulnerability
302      *
303      * @param pocName a string path to poc from the /res folder
304      * @param device device to be ran on
305      * @param timeout time to wait for output in seconds
306      */
307     @Deprecated
runPocCheckExitCode(String pocName, ITestDevice device, int timeout)308     public static boolean runPocCheckExitCode(String pocName, ITestDevice device,
309                                               int timeout) throws Exception {
310 
311        //Refer to go/asdl-sts-guide Test section for knowing the significance of 113 code
312        return runPocGetExitStatus(pocName, device, timeout) == 113;
313     }
314 
315     /**
316      * Pushes and runs a binary to the device and returns the exit status.
317      * @param pocName a string path to poc from the /res folder
318      * @param device device to be ran on
319      * @param timeout time to wait for output in seconds
320 
321      */
runPocGetExitStatus(String pocName, ITestDevice device, int timeout)322     public static int runPocGetExitStatus(String pocName, ITestDevice device, int timeout)
323             throws Exception {
324        return runPocGetExitStatus(pocName, null, device, timeout);
325     }
326 
327     /**
328      * Pushes and runs a binary to the device and returns the exit status.
329      * @param pocName a string path to poc from the /res folder
330      * @param arguments input arguments for the poc
331      * @param device device to be ran on
332      * @param timeout time to wait for output in seconds
333 
334      */
runPocGetExitStatus(String pocName, String arguments, ITestDevice device, int timeout)335     public static int runPocGetExitStatus(String pocName, String arguments, ITestDevice device,
336             int timeout) throws Exception {
337         device.executeShellCommand("chmod +x " + TMP_PATH + pocName);
338         CollectingOutputReceiver receiver = new CollectingOutputReceiver();
339         String cmd = TMP_PATH + pocName + " " + arguments + " > /dev/null 2>&1; echo $?";
340         long time = System.currentTimeMillis();
341         device.executeShellCommand(cmd, receiver, timeout, TimeUnit.SECONDS, 0);
342         time = System.currentTimeMillis() - time;
343         String exitStatus = receiver.getOutput().trim();
344         try {
345             return Integer.parseInt(exitStatus);
346         } catch (NumberFormatException e) {
347             throw new IllegalArgumentException(String.format(
348                     "Could not get the exit status (%s) for '%s' (%d ms).",
349                     exitStatus, cmd, time));
350         }
351     }
352 
353     /**
354      * Pushes and runs a binary and asserts that the exit status isn't 113: vulnerable.
355      * @param pocName a string path to poc from the /res folder
356      * @param device device to be ran on
357      * @param timeout time to wait for output in seconds
358      */
runPocAssertExitStatusNotVulnerable( String pocName, ITestDevice device, int timeout)359     public static void runPocAssertExitStatusNotVulnerable(
360             String pocName, ITestDevice device, int timeout) throws Exception {
361         runPocAssertExitStatusNotVulnerable(pocName, null, device, timeout);
362     }
363 
364     /**
365      * Pushes and runs a binary and asserts that the exit status isn't 113: vulnerable.
366      * @param pocName a string path to poc from the /res folder
367      * @param arguments input arguments for the poc
368      * @param device device to be ran on
369      * @param timeout time to wait for output in seconds
370      */
runPocAssertExitStatusNotVulnerable(String pocName, String arguments, ITestDevice device, int timeout)371     public static void runPocAssertExitStatusNotVulnerable(String pocName, String arguments,
372             ITestDevice device, int timeout) throws Exception {
373         assertTrue("PoC returned exit status 113: vulnerable",
374                 runPocGetExitStatus(pocName, arguments, device, timeout) != 113);
375     }
376 
377 
runProxyAutoConfig(String pacName, ITestDevice device)378     public static int runProxyAutoConfig(String pacName, ITestDevice device) throws Exception {
379         runCommandLine("chmod +x /data/local/tmp/pacrunner", device);
380         String targetPath = "/data/local/tmp/" + pacName + ".pac";
381         AdbUtils.pushResource("/" + pacName + ".pac", targetPath, device);
382         int code = runCommandGetExitCode("/data/local/tmp/pacrunner " + targetPath, device);
383         runCommandLine("rm " + targetPath, device);
384         return code;
385     }
386 
387     /**
388      * Runs the poc binary and asserts that there are no security crashes that match the expected
389      * process pattern.
390      * @param pocName a string path to poc from the /res folder
391      * @param device device to be ran on
392      * @param processPatternStrings a Pattern string to match the crash tombstone process
393      */
runPocAssertNoCrashes(String pocName, ITestDevice device, String... processPatternStrings)394     public static void runPocAssertNoCrashes(String pocName, ITestDevice device,
395             String... processPatternStrings) throws Exception {
396         runPocAssertNoCrashes(pocName, device,
397                 new CrashUtils.Config().setProcessPatterns(processPatternStrings));
398     }
399 
400     /**
401      * Runs the poc binary and asserts that there are no security crashes that match the expected
402      * process pattern.
403      * @param pocName a string path to poc from the /res folder
404      * @param device device to be ran on
405      * @param config a crash parser configuration
406      */
runPocAssertNoCrashes(String pocName, ITestDevice device, CrashUtils.Config config)407     public static void runPocAssertNoCrashes(String pocName, ITestDevice device,
408             CrashUtils.Config config) throws Exception {
409         AdbUtils.runCommandLine("logcat -c", device);
410         AdbUtils.runPocNoOutput(pocName, device, SecurityTestCase.TIMEOUT_NONDETERMINISTIC);
411         assertNoCrashes(device, config);
412     }
413 
414     /**
415      * Runs the poc binary and asserts following 2 conditions.
416      *  1. There are no security crashes in the binary.
417      *  2. The exit status isn't 113 (Code 113 is used to indicate the vulnerability condition).
418      *
419      * @param binaryName name of the binary
420      * @param arguments arguments for running the binary
421      * @param device device to be run on
422      */
runPocAssertNoCrashesNotVulnerable(String binaryName, String arguments, ITestDevice device)423     public static void runPocAssertNoCrashesNotVulnerable(String binaryName, String arguments,
424             ITestDevice device) throws Exception {
425         runPocAssertNoCrashesNotVulnerable(binaryName, arguments, null, null, device, null);
426     }
427 
428     /**
429      * Runs the poc binary and asserts following 2 conditions.
430      *  1. There are no security crashes in the binary.
431      *  2. The exit status isn't 113 (Code 113 is used to indicate the vulnerability condition).
432      *
433      * @param binaryName name of the binary
434      * @param arguments arguments for running the binary
435      * @param device device to be run on
436      * @param processPatternStrings a Pattern string to match the crash tombstone
437      *        process
438      */
runPocAssertNoCrashesNotVulnerable(String binaryName, String arguments, ITestDevice device, String processPatternStrings[])439     public static void runPocAssertNoCrashesNotVulnerable(String binaryName, String arguments,
440             ITestDevice device, String processPatternStrings[]) throws Exception {
441         runPocAssertNoCrashesNotVulnerable(binaryName, arguments, null, null, device,
442                 processPatternStrings);
443     }
444 
445     /**
446      * Runs the poc binary and asserts following 2 conditions.
447      *  1. There are no security crashes in the binary.
448      *  2. The exit status isn't 113 (Code 113 is used to indicate the vulnerability condition).
449      *
450      * @param binaryName name of the binary
451      * @param arguments arguments for running the binary
452      * @param inputFiles files required as input
453      * @param inputFilesDestination destination directory to which input files are
454      *        pushed
455      * @param device device to be run on
456      */
runPocAssertNoCrashesNotVulnerable(String binaryName, String arguments, String inputFiles[], String inputFilesDestination, ITestDevice device)457     public static void runPocAssertNoCrashesNotVulnerable(String binaryName, String arguments,
458             String inputFiles[], String inputFilesDestination, ITestDevice device)
459             throws Exception {
460         runPocAssertNoCrashesNotVulnerable(binaryName, arguments, inputFiles, inputFilesDestination,
461                 device, null);
462     }
463 
464     /**
465      * Runs the poc binary and asserts following 3 conditions.
466      *  1. There are no security crashes in the binary.
467      *  2. There are no security crashes that match the expected process pattern.
468      *  3. The exit status isn't 113 (Code 113 is used to indicate the vulnerability condition).
469      *
470      * @param binaryName name of the binary
471      * @param arguments arguments for running the binary
472      * @param inputFiles files required as input
473      * @param inputFilesDestination destination directory to which input files are
474      *        pushed
475      * @param device device to be run on
476      * @param processPatternStrings a Pattern string to match the crash tombstone
477      *        process
478      */
runPocAssertNoCrashesNotVulnerable(String binaryName, String arguments, String inputFiles[], String inputFilesDestination, ITestDevice device, String processPatternStrings[])479     public static void runPocAssertNoCrashesNotVulnerable(String binaryName, String arguments,
480             String inputFiles[], String inputFilesDestination, ITestDevice device,
481             String processPatternStrings[]) throws Exception {
482         pushResources(inputFiles, inputFilesDestination, device);
483         runCommandLine("logcat -c", device);
484         try {
485             runPocAssertExitStatusNotVulnerable(binaryName, arguments, device, TIMEOUT_SEC);
486         } catch (IllegalArgumentException e) {
487             /*
488              * Since 'runPocGetExitStatus' method raises IllegalArgumentException upon
489              * hang/timeout, catching the exception here and ignoring it. Hangs are of
490              * Moderate severity and hence patches may not be ported. This piece of code can
491              * be removed once 'runPocGetExitStatus' is updated to handle hangs.
492              */
493             CLog.w("Ignoring IllegalArgumentException: " + e);
494         } finally {
495             removeResources(inputFiles, inputFilesDestination, device);
496         }
497         List<String> processPatternList = new ArrayList<>();
498         if (processPatternStrings != null) {
499             processPatternList.addAll(Arrays.asList(processPatternStrings));
500         }
501         processPatternList.add(binaryName);
502         String[] processPatternStringsWithSelf = new String[processPatternList.size()];
503         processPatternList.toArray(processPatternStringsWithSelf);
504         assertNoCrashes(device, processPatternStringsWithSelf);
505     }
506 
507     /**
508      * Dumps logcat and asserts that there are no security crashes that match the expected process.
509      * By default, checks min crash addresses
510      * pattern. Ensure that adb logcat -c is called beforehand.
511      * @param device device to be ran on
512      * @param processPatternStrings a Pattern string to match the crash tombstone process
513      */
assertNoCrashes(ITestDevice device, String... processPatternStrings)514     public static void assertNoCrashes(ITestDevice device, String... processPatternStrings)
515             throws Exception {
516         assertNoCrashes(device, new CrashUtils.Config().setProcessPatterns(processPatternStrings));
517     }
518 
519     /**
520      * Dumps logcat and asserts that there are no security crashes that match the expected process
521      * pattern. Ensure that adb logcat -c is called beforehand.
522      * @param device device to be ran on
523      * @param config a crash parser configuration
524      */
assertNoCrashes(ITestDevice device, CrashUtils.Config config)525     public static void assertNoCrashes(ITestDevice device,
526             CrashUtils.Config config) throws Exception {
527         String logcat = AdbUtils.runCommandLine("logcat -d *:S DEBUG:V", device);
528 
529         JSONArray crashes = CrashUtils.addAllCrashes(logcat, new JSONArray());
530         JSONArray securityCrashes = CrashUtils.matchSecurityCrashes(crashes, config);
531 
532         if (securityCrashes.length() == 0) {
533             return; // no security crashes detected
534         }
535 
536         StringBuilder error = new StringBuilder();
537         error.append("Security crash detected:\n");
538         error.append("Process patterns:");
539         for (Pattern pattern : config.getProcessPatterns()) {
540             error.append(String.format(" '%s'", pattern.toString()));
541         }
542         error.append("\nCrashes:\n");
543         for (int i = 0; i < crashes.length(); i++) {
544             try {
545                 JSONObject crash = crashes.getJSONObject(i);
546                 error.append(String.format("%s\n", crash));
547             } catch (JSONException e) {}
548         }
549         fail(error.toString());
550      }
551 }
552