1 /* 2 * Copyright 2017 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 com.android.server.pm.dex; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static com.google.common.truth.Truth.assertWithMessage; 21 22 import static org.junit.Assume.assumeFalse; 23 24 import android.app.UiAutomation; 25 import android.content.Context; 26 import android.content.pm.PackageManager; 27 import android.os.Build; 28 import android.os.ParcelFileDescriptor; 29 import android.os.SystemClock; 30 import android.util.EventLog; 31 import android.util.EventLog.Event; 32 33 import androidx.test.filters.LargeTest; 34 import androidx.test.platform.app.InstrumentationRegistry; 35 36 import dalvik.system.DexClassLoader; 37 38 import org.junit.Before; 39 import org.junit.BeforeClass; 40 import org.junit.Test; 41 import org.junit.runner.RunWith; 42 import org.junit.runners.JUnit4; 43 44 import java.io.ByteArrayOutputStream; 45 import java.io.File; 46 import java.io.FileOutputStream; 47 import java.io.InputStream; 48 import java.io.OutputStream; 49 import java.security.MessageDigest; 50 import java.util.ArrayList; 51 import java.util.Formatter; 52 import java.util.List; 53 import java.util.concurrent.TimeUnit; 54 55 /** 56 * Integration tests for {@link DynamicCodeLogger}. 57 * 58 * The setup for the test dynamically loads code in a jar extracted 59 * from our assets (a secondary dex file). 60 * 61 * We then use shell commands to trigger dynamic code logging (and wait 62 * for it to complete). This causes DynamicCodeLogger to log the hash of the 63 * file's name and content. We verify that this message appears in 64 * the event log. 65 * 66 * Run with "atest DynamicCodeLoggerIntegrationTests". 67 */ 68 @LargeTest 69 @RunWith(JUnit4.class) 70 public final class DynamicCodeLoggerIntegrationTests { 71 72 private static final String SHA_256 = "SHA-256"; 73 74 // Event log tag used for SNET related events 75 private static final int SNET_TAG = 0x534e4554; 76 77 // Subtags used to distinguish dynamic code loading events 78 private static final String DCL_DEX_SUBTAG = "dcl"; 79 private static final String DCL_NATIVE_SUBTAG = "dcln"; 80 81 // These are job IDs from DynamicCodeLoggingService 82 private static final int IDLE_LOGGING_JOB_ID = 2030028; 83 private static final int AUDIT_WATCHING_JOB_ID = 203142925; 84 85 // For tests that rely on parsing audit logs, how often to retry. (There are many reasons why 86 // we might not see the audit logs, including throttling and delays in log generation, so to 87 // avoid flakiness we run these tests multiple times, allowing progressively longer between 88 // code loading and checking the logs on each try.) 89 private static final int AUDIT_LOG_RETRIES = 10; 90 private static final int RETRY_DELAY_MS = 500; 91 92 private static Context sContext; 93 private static int sMyUid; 94 95 @BeforeClass setUpAll()96 public static void setUpAll() { 97 sContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 98 sMyUid = android.os.Process.myUid(); 99 } 100 101 @Before setup()102 public void setup() { 103 assumeFalse(sContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)); 104 primeEventLog(); 105 } 106 primeEventLog()107 private void primeEventLog() { 108 // Force a round trip to logd to make sure everything is up to date. 109 // Without this the first test passes and others don't - we don't see new events in the 110 // log. The exact reason is unclear. 111 EventLog.writeEvent(SNET_TAG, "Dummy event"); 112 113 // Audit log messages are throttled by the kernel (at the request of logd) to 5 per 114 // second, so running the tests too quickly in sequence means we lose some and get 115 // spurious failures. Sigh. 116 SystemClock.sleep(1000); 117 } 118 119 @Test testGeneratesEvents_standardClassLoader()120 public void testGeneratesEvents_standardClassLoader() throws Exception { 121 File privateCopyFile = privateFile("copied.jar"); 122 // Obtained via "echo -n copied.jar | sha256sum" 123 String expectedNameHash = 124 "1B6C71DB26F36582867432CCA12FB6A517470C9F9AABE9198DD4C5C030D6DC0C"; 125 String expectedContentHash = copyAndHashResource( 126 "/DynamicCodeLoggerTestLibrary.jar", privateCopyFile); 127 128 // Feed the jar to a class loader and make sure it contains what we expect. 129 ClassLoader parentClassLoader = sContext.getClass().getClassLoader(); 130 ClassLoader loader = 131 new DexClassLoader(privateCopyFile.toString(), null, null, parentClassLoader); 132 loader.loadClass("com.android.dcl.Simple"); 133 134 // And make sure we log events about it 135 long previousEventNanos = mostRecentEventTimeNanos(); 136 runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); 137 138 assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG, 139 expectedNameHash, expectedContentHash); 140 } 141 142 @Test testGeneratesEvents_unknownClassLoader()143 public void testGeneratesEvents_unknownClassLoader() throws Exception { 144 File privateCopyFile = privateFile("copied2.jar"); 145 String expectedNameHash = 146 "202158B6A3169D78F1722487205A6B036B3F2F5653FDCFB4E74710611AC7EB93"; 147 String expectedContentHash = copyAndHashResource( 148 "/DynamicCodeLoggerTestLibrary.jar", privateCopyFile); 149 150 // This time make sure an unknown class loader is an ancestor of the class loader we use. 151 ClassLoader knownClassLoader = sContext.getClass().getClassLoader(); 152 ClassLoader unknownClassLoader = new UnknownClassLoader(knownClassLoader); 153 ClassLoader loader = 154 new DexClassLoader(privateCopyFile.toString(), null, null, unknownClassLoader); 155 loader.loadClass("com.android.dcl.Simple"); 156 157 // And make sure we log events about it 158 long previousEventNanos = mostRecentEventTimeNanos(); 159 runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); 160 161 assertDclLoggedSince(previousEventNanos, DCL_DEX_SUBTAG, 162 expectedNameHash, expectedContentHash); 163 } 164 165 @Test testGeneratesEvents_nativeLibrary()166 public void testGeneratesEvents_nativeLibrary() throws Exception { 167 new TestNativeCodeWithRetries() { 168 @Override 169 protected void loadNativeCode(int tryNumber) throws Exception { 170 // We need to use a different file name for each retry, because once a file is 171 // loaded, re-loading it has no effect. 172 String privateCopyName = "copied" + tryNumber + ".so"; 173 File privateCopyFile = privateFile(privateCopyName); 174 mExpectedNameHash = hashOf(privateCopyName); 175 mExpectedContentHash = copyAndHashResource( 176 libraryPath("DynamicCodeLoggerNativeTestLibrary.so"), privateCopyFile); 177 178 System.load(privateCopyFile.toString()); 179 } 180 }.runTest(); 181 } 182 183 @Test testGeneratesEvents_nativeLibrary_escapedName()184 public void testGeneratesEvents_nativeLibrary_escapedName() throws Exception { 185 new TestNativeCodeWithRetries() { 186 @Override 187 protected void loadNativeCode(int tryNumber) throws Exception { 188 // A file name with a space will be escaped in the audit log; verify we un-escape it 189 // correctly. 190 String privateCopyName = "second copy " + tryNumber + ".so"; 191 File privateCopyFile = privateFile(privateCopyName); 192 mExpectedNameHash = hashOf(privateCopyName); 193 mExpectedContentHash = copyAndHashResource( 194 libraryPath("DynamicCodeLoggerNativeTestLibrary.so"), privateCopyFile); 195 196 System.load(privateCopyFile.toString()); 197 } 198 }.runTest(); 199 } 200 201 @Test testGeneratesEvents_nativeExecutable()202 public void testGeneratesEvents_nativeExecutable() throws Exception { 203 new TestNativeCodeWithRetries() { 204 @Override 205 protected void loadNativeCode(int tryNumber) throws Exception { 206 String privateCopyName = "test_executable" + tryNumber; 207 File privateCopyFile = privateFile(privateCopyName); 208 mExpectedNameHash = hashOf(privateCopyName); 209 mExpectedContentHash = copyAndHashResource( 210 "/DynamicCodeLoggerNativeExecutable", privateCopyFile); 211 assertThat(privateCopyFile.setExecutable(true)).isTrue(); 212 213 Process process = Runtime.getRuntime().exec(privateCopyFile.toString()); 214 int exitCode = process.waitFor(); 215 assertThat(exitCode).isEqualTo(0); 216 } 217 }.runTest(); 218 } 219 220 @Test testGeneratesEvents_spoofed_validFile()221 public void testGeneratesEvents_spoofed_validFile() throws Exception { 222 File privateCopyFile = privateFile("spoofed"); 223 224 String expectedContentHash = copyAndHashResource( 225 "/DynamicCodeLoggerNativeExecutable", privateCopyFile); 226 227 EventLog.writeEvent(EventLog.getTagCode("auditd"), 228 "type=1400 avc: granted { execute_no_trans } " 229 + "path=\"" + privateCopyFile + "\" " 230 + "scontext=u:r:untrusted_app_27: " 231 + "tcontext=u:object_r:app_data_file: " 232 + "tclass=file "); 233 234 String expectedNameHash = 235 "1CF36F503A02877BB775DC23C1C5A47A95F2684B6A1A83B11795B856D88861E3"; 236 237 // Run the job to scan generated audit log entries 238 runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); 239 240 // And then make sure we log events about it 241 long previousEventNanos = mostRecentEventTimeNanos(); 242 runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); 243 244 assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, 245 expectedNameHash, expectedContentHash); 246 } 247 248 @Test testGeneratesEvents_spoofed_validFile_untrustedApp()249 public void testGeneratesEvents_spoofed_validFile_untrustedApp() throws Exception { 250 File privateCopyFile = privateFile("spoofed2"); 251 252 String expectedContentHash = copyAndHashResource( 253 "/DynamicCodeLoggerNativeExecutable", privateCopyFile); 254 255 EventLog.writeEvent(EventLog.getTagCode("auditd"), 256 "type=1400 avc: granted { execute_no_trans } " 257 + "path=\"" + privateCopyFile + "\" " 258 + "scontext=u:r:untrusted_app: " 259 + "tcontext=u:object_r:app_data_file: " 260 + "tclass=file "); 261 262 String expectedNameHash = 263 "3E57AA59249154C391316FDCF07C1D499C26A564E4D305833CCD9A98ED895AC9"; 264 265 // Run the job to scan generated audit log entries 266 runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); 267 268 // And then make sure we log events about it 269 long previousEventNanos = mostRecentEventTimeNanos(); 270 runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); 271 272 assertDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, 273 expectedNameHash, expectedContentHash); 274 } 275 276 @Test testGeneratesEvents_spoofed_pathTraversal()277 public void testGeneratesEvents_spoofed_pathTraversal() throws Exception { 278 File privateDir = privateFile("x").getParentFile(); 279 280 // Transform /a/b/c -> /a/b/c/../../.. so we get back to the root 281 File pathTraversalToRoot = privateDir; 282 File root = new File("/"); 283 while (!privateDir.equals(root)) { 284 pathTraversalToRoot = new File(pathTraversalToRoot, ".."); 285 privateDir = privateDir.getParentFile(); 286 } 287 288 File spoofedFile = new File(pathTraversalToRoot, "dev/urandom"); 289 290 assertWithMessage("Expected " + spoofedFile + " to be readable") 291 .that(spoofedFile.canRead()).isTrue(); 292 293 EventLog.writeEvent(EventLog.getTagCode("auditd"), 294 "type=1400 avc: granted { execute_no_trans } " 295 + "path=\"" + spoofedFile + "\" " 296 + "scontext=u:r:untrusted_app_27: " 297 + "tcontext=u:object_r:app_data_file: " 298 + "tclass=file "); 299 300 String expectedNameHash = 301 "65528FE876BD676B0DFCC9A8ACA8988E026766F99EEC1E1FB48F46B2F635E225"; 302 303 // Run the job to scan generated audit log entries 304 runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); 305 306 // And then trigger generating DCL events 307 long previousEventNanos = mostRecentEventTimeNanos(); 308 runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); 309 310 assertNoDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, expectedNameHash); 311 } 312 313 @Test testGeneratesEvents_spoofed_otherAppFile()314 public void testGeneratesEvents_spoofed_otherAppFile() throws Exception { 315 File ourPath = sContext.getDatabasePath("android_pay"); 316 File targetPath = new File(ourPath.toString() 317 .replace("com.android.frameworks.dynamiccodeloggertest", "com.google.android.gms")); 318 319 assertWithMessage("Expected " + targetPath + " to not be readable") 320 .that(targetPath.canRead()).isFalse(); 321 322 EventLog.writeEvent(EventLog.getTagCode("auditd"), 323 "type=1400 avc: granted { execute_no_trans } " 324 + "path=\"" + targetPath + "\" " 325 + "scontext=u:r:untrusted_app_27: " 326 + "tcontext=u:object_r:app_data_file: " 327 + "tclass=file "); 328 329 String expectedNameHash = 330 "CBE04E8AB9E7199FC19CBAAF9C774B88E56B3B19E823F2251693380AD6F515E6"; 331 332 // Run the job to scan generated audit log entries 333 runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); 334 335 // And then trigger generating DCL events 336 long previousEventNanos = mostRecentEventTimeNanos(); 337 runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); 338 339 assertNoDclLoggedSince(previousEventNanos, DCL_NATIVE_SUBTAG, expectedNameHash); 340 } 341 342 // Abstract out the logic for running a native code loading test multiple times if needed and 343 // leaving time for audit messages to reach the log. 344 private abstract static class TestNativeCodeWithRetries { 345 String mExpectedContentHash; 346 String mExpectedNameHash; 347 loadNativeCode(int tryNumber)348 abstract void loadNativeCode(int tryNumber) throws Exception; 349 runTest()350 final void runTest() throws Exception { 351 List<String> messages = null; 352 353 for (int i = 0; i < AUDIT_LOG_RETRIES; i++) { 354 loadNativeCode(i); 355 356 SystemClock.sleep(i * RETRY_DELAY_MS); 357 358 // Run the job to scan generated audit log entries 359 runDynamicCodeLoggingJob(AUDIT_WATCHING_JOB_ID); 360 361 // And then make sure we log events about it 362 long previousEventNanos = mostRecentEventTimeNanos(); 363 runDynamicCodeLoggingJob(IDLE_LOGGING_JOB_ID); 364 365 messages = findMatchingEvents( 366 previousEventNanos, DCL_NATIVE_SUBTAG, mExpectedNameHash); 367 if (!messages.isEmpty()) { 368 break; 369 } 370 } 371 372 assertHasDclLog(messages, mExpectedContentHash); 373 } 374 } 375 privateFile(String name)376 private static File privateFile(String name) { 377 return new File(sContext.getDir("dcl", Context.MODE_PRIVATE), name); 378 } 379 libraryPath(final String libraryName)380 private String libraryPath(final String libraryName) { 381 // This may be deprecated. but it tells us the ABI of this process which is exactly what we 382 // want. 383 return "/lib/" + Build.CPU_ABI + "/" + libraryName; 384 } 385 copyAndHashResource(String resourcePath, File copyTo)386 private static String copyAndHashResource(String resourcePath, File copyTo) throws Exception { 387 MessageDigest hasher = MessageDigest.getInstance(SHA_256); 388 389 // Copy the jar from our Java resources to a private data directory 390 Class<?> thisClass = DynamicCodeLoggerIntegrationTests.class; 391 try (InputStream input = thisClass.getResourceAsStream(resourcePath); 392 OutputStream output = new FileOutputStream(copyTo)) { 393 byte[] buffer = new byte[1024]; 394 while (true) { 395 int numRead = input.read(buffer); 396 if (numRead < 0) { 397 break; 398 } 399 output.write(buffer, 0, numRead); 400 hasher.update(buffer, 0, numRead); 401 } 402 } 403 404 // Compute the SHA-256 of the file content so we can check that it is the same as the value 405 // we see logged. 406 return toHexString(hasher); 407 } 408 hashOf(String input)409 private String hashOf(String input) throws Exception { 410 MessageDigest hasher = MessageDigest.getInstance(SHA_256); 411 hasher.update(input.getBytes()); 412 return toHexString(hasher); 413 } 414 toHexString(MessageDigest hasher)415 private static String toHexString(MessageDigest hasher) { 416 Formatter formatter = new Formatter(); 417 for (byte b : hasher.digest()) { 418 formatter.format("%02X", b); 419 } 420 421 return formatter.toString(); 422 } 423 runDynamicCodeLoggingJob(int jobId)424 private static void runDynamicCodeLoggingJob(int jobId) throws Exception { 425 // This forces the DynamicCodeLoggingService job to start now. 426 runCommand("cmd jobscheduler run -f android " + jobId); 427 // Wait for the job to have run. 428 long startTime = SystemClock.elapsedRealtime(); 429 while (true) { 430 String response = runCommand( 431 "cmd jobscheduler get-job-state android " + jobId); 432 if (!response.contains("pending") && !response.contains("active")) { 433 break; 434 } 435 // Don't wait forever - if it's taken > 10s then something is very wrong. 436 if (SystemClock.elapsedRealtime() - startTime > TimeUnit.SECONDS.toMillis(10)) { 437 throw new AssertionError("Job has not completed: " + response); 438 } 439 SystemClock.sleep(100); 440 } 441 } 442 runCommand(String command)443 private static String runCommand(String command) throws Exception { 444 ByteArrayOutputStream response = new ByteArrayOutputStream(); 445 byte[] buffer = new byte[1000]; 446 UiAutomation ui = InstrumentationRegistry.getInstrumentation().getUiAutomation(); 447 ParcelFileDescriptor fd = ui.executeShellCommand(command); 448 try (InputStream input = new ParcelFileDescriptor.AutoCloseInputStream(fd)) { 449 while (true) { 450 int count = input.read(buffer); 451 if (count == -1) { 452 break; 453 } 454 response.write(buffer, 0, count); 455 } 456 } 457 return response.toString("UTF-8"); 458 } 459 mostRecentEventTimeNanos()460 private static long mostRecentEventTimeNanos() throws Exception { 461 List<Event> events = readSnetEvents(); 462 return events.isEmpty() ? 0 : events.get(events.size() - 1).getTimeNanos(); 463 } 464 assertDclLoggedSince(long previousEventNanos, String expectedSubTag, String expectedNameHash, String expectedContentHash)465 private static void assertDclLoggedSince(long previousEventNanos, String expectedSubTag, 466 String expectedNameHash, String expectedContentHash) throws Exception { 467 List<String> messages = 468 findMatchingEvents(previousEventNanos, expectedSubTag, expectedNameHash); 469 470 assertHasDclLog(messages, expectedContentHash); 471 } 472 assertHasDclLog(List<String> messages, String expectedContentHash)473 private static void assertHasDclLog(List<String> messages, String expectedContentHash) { 474 assertWithMessage("Expected exactly one matching log entry").that(messages).hasSize(1); 475 assertThat(messages.get(0)).endsWith(expectedContentHash); 476 } 477 assertNoDclLoggedSince(long previousEventNanos, String expectedSubTag, String expectedNameHash)478 private static void assertNoDclLoggedSince(long previousEventNanos, String expectedSubTag, 479 String expectedNameHash) throws Exception { 480 List<String> messages = 481 findMatchingEvents(previousEventNanos, expectedSubTag, expectedNameHash); 482 483 assertWithMessage("Expected no matching log entries").that(messages).isEmpty(); 484 } 485 findMatchingEvents(long previousEventNanos, String expectedSubTag, String expectedNameHash)486 private static List<String> findMatchingEvents(long previousEventNanos, String expectedSubTag, 487 String expectedNameHash) throws Exception { 488 List<String> messages = new ArrayList<>(); 489 490 for (Event event : readSnetEvents()) { 491 if (event.getTimeNanos() <= previousEventNanos) { 492 continue; 493 } 494 495 Object data = event.getData(); 496 if (!(data instanceof Object[])) { 497 continue; 498 } 499 Object[] fields = (Object[]) data; 500 501 // We only care about DCL events that we generated. 502 String subTag = (String) fields[0]; 503 if (!expectedSubTag.equals(subTag)) { 504 continue; 505 } 506 int uid = (int) fields[1]; 507 if (uid != sMyUid) { 508 continue; 509 } 510 511 String message = (String) fields[2]; 512 if (!message.startsWith(expectedNameHash)) { 513 continue; 514 } 515 516 messages.add(message); 517 //assertThat(message).endsWith(expectedContentHash); 518 } 519 return messages; 520 } 521 readSnetEvents()522 private static List<Event> readSnetEvents() throws Exception { 523 List<Event> events = new ArrayList<>(); 524 EventLog.readEvents(new int[] { SNET_TAG }, events); 525 return events; 526 } 527 528 /** 529 * A class loader that does nothing useful, but importantly doesn't extend BaseDexClassLoader. 530 */ 531 private static class UnknownClassLoader extends ClassLoader { UnknownClassLoader(ClassLoader parent)532 UnknownClassLoader(ClassLoader parent) { 533 super(parent); 534 } 535 } 536 } 537