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