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