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