1 /*
2  * Copyright (C) 2010 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.appsecurity.cts.listeningports;
18 
19 import android.app.UiAutomation;
20 import android.content.pm.PackageManager;
21 import android.os.Build;
22 import android.os.Bundle;
23 import android.os.Process;
24 import android.os.UserHandle;
25 import android.test.AndroidTestCase;
26 import android.util.Log;
27 
28 import androidx.test.InstrumentationRegistry;
29 
30 import junit.framework.AssertionFailedError;
31 
32 import java.io.IOException;
33 import java.net.InetAddress;
34 import java.net.InetSocketAddress;
35 import java.net.Socket;
36 import java.net.UnknownHostException;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.List;
40 import java.util.Scanner;
41 import java.util.regex.Pattern;
42 
43 /**
44  * Verifies that Android devices are not listening on accessible
45  * open ports. Open ports are often targeted by attackers looking to break
46  * into computer systems remotely, and minimizing the number of open ports
47  * is considered a security best practice.
48  */
49 public class ListeningPortsTest extends AndroidTestCase {
50     private static final String TAG = "ListeningPortsTest";
51 
52     private static final String PROC_FILE_CONTENTS_PARAM = "procFileContents";
53     private static final String IS_TCP_PARAM = "isTcp";
54     private static final String LOOPBACK_PARAM = "loopback";
55 
56     private static final int CONN_TIMEOUT_IN_MS = 5000;
57 
58     /** Ports that are allowed to be listening. */
59     private static final List<String> EXCEPTION_PATTERNS = new ArrayList<String>(6);
60 
61     static {
62         // IPv4 exceptions
63         // Patterns containing ":" are allowed address port combinations
64         // Patterns contains " " are allowed address UID combinations
65         // Patterns containing both are allowed address, port, and UID combinations
66         EXCEPTION_PATTERNS.add("0.0.0.0:5555");     // emulator port
67         EXCEPTION_PATTERNS.add("0.0.0.0:9101");     // verified ports
68         EXCEPTION_PATTERNS.add("0.0.0.0:9551");     // verified ports
69         EXCEPTION_PATTERNS.add("0.0.0.0:9552");     // verified ports
70         EXCEPTION_PATTERNS.add("10.0.2.15:5555");   // net forwarding for emulator
71         EXCEPTION_PATTERNS.add("127.0.0.1:5037");   // adb daemon "smart sockets"
72         EXCEPTION_PATTERNS.add("0.0.0.0 1020");     // used by the cast receiver
73         EXCEPTION_PATTERNS.add("0.0.0.0 10000");    // used by the cast receiver
74         EXCEPTION_PATTERNS.add("127.0.0.1 10000");  // used by the cast receiver
75         EXCEPTION_PATTERNS.add(":: 1002");          // used by remote control
76         EXCEPTION_PATTERNS.add(":: 1020");          // used by remote control
77         EXCEPTION_PATTERNS.add("0.0.0.0:7275");     // used by supl
78         EXCEPTION_PATTERNS.add("0.0.0.0:68");       // DHCP server for Tethering
79         // b/150186547 ports
80         EXCEPTION_PATTERNS.add("192.168.17.10:48881");
81         EXCEPTION_PATTERNS.add("192.168.17.10:48896");
82         EXCEPTION_PATTERNS.add("192.168.17.10:48897");
83         EXCEPTION_PATTERNS.add("192.168.17.10:48898");
84         EXCEPTION_PATTERNS.add("192.168.17.10:48899");
85         // Thread network exceptions; all Thread processes should run under uid 1084 and can
86         // potentially listen on ephemeral ports.
87         EXCEPTION_PATTERNS.add("127.0.0.1 1084");
88         EXCEPTION_PATTERNS.add("::1 1084");
89         EXCEPTION_PATTERNS.add("224.0.0.116 1084");
90         EXCEPTION_PATTERNS.add("ff02::116 1084");
91         EXCEPTION_PATTERNS.add("0.0.0.0 1084");
92         EXCEPTION_PATTERNS.add(":: 1084");
93         //no current patterns involve address, port and UID combinations
94         //Example for when necessary: EXCEPTION_PATTERNS.add("0.0.0.0:5555 10000")
95 
96         // IPv6 exceptions
97         // TODO: this is not standard notation for IPv6. Use [$addr]:$port instead as per RFC 3986.
98         EXCEPTION_PATTERNS.add(":::5555");          // emulator port for adb
99         EXCEPTION_PATTERNS.add(":::7275");          // used by supl
100 
101         // DHCP: This port is open when a network is connected before DHCP is resolved
102         // And can also be opened on boot for ethernet networks.
103         // Thus a device connected via wifi with an ethernet port can encounter this.
104         EXCEPTION_PATTERNS.add("0.0.0.0:68");
105     }
106 
107     private static final List<String> USERDEBUG_EXCEPTION_PATTERNS = new ArrayList<>(2);
108 
109     static {
110         USERDEBUG_EXCEPTION_PATTERNS.add("127.0.0.1:50002");  // Diagnostic Monitor Daemon port
111         USERDEBUG_EXCEPTION_PATTERNS.add("127.0.0.1:60002");  // vcd port
112         USERDEBUG_EXCEPTION_PATTERNS.add("127.0.0.1:7555 1021");  // gnssd running under GPS UID
113     }
114 
115     private static final List<String> OEM_EXCEPTION_PATTERNS = new ArrayList<String>();
116 
117     static {
118         // PTP vendor OEM service
119         OEM_EXCEPTION_PATTERNS.add("0.0.0.0:319");
120         OEM_EXCEPTION_PATTERNS.add("0.0.0.0:320");
121     }
122 
isOemUid(int uid)123     private static boolean isOemUid(int uid) {
124         return (uid >= 2900 && uid <= 2999) || (uid >= 5000 && uid <= 5999);
125     }
126 
isTv()127     private boolean isTv() {
128         return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
129     }
130 
131     /**
132      * Remotely accessible ports (loopback==false) are often used by
133      * attackers to gain unauthorized access to computers systems without
134      * user knowledge or awareness.
135      *
136      * Locally accessible ports (loopback==true) are often targeted by
137      * malicious locally installed programs to gain unauthorized access to
138      * program data or cause system corruption.
139      *
140      * Since direct /proc/net access is no longer possible the contents of the file and the boolean
141      * values are received as parameters from the host side test.
142      */
testNoAccessibleListeningPorts()143     public void testNoAccessibleListeningPorts() throws Exception {
144         final Bundle testArgs = InstrumentationRegistry.getArguments();
145         final String procFileContents = testArgs.getString(PROC_FILE_CONTENTS_PARAM);
146         final boolean isTcp = Boolean.valueOf(testArgs.getString(IS_TCP_PARAM));
147         final boolean loopback = Boolean.valueOf(testArgs.getString(LOOPBACK_PARAM));
148 
149         final boolean tv = isTv();
150 
151         String errors = "";
152         List<ParsedProcEntry> entries = ParsedProcEntry.parse(procFileContents);
153         for (ParsedProcEntry entry : entries) {
154             String addrPort = entry.localAddress.getHostAddress() + ':' + entry.port;
155             String addrUid = entry.localAddress.getHostAddress() + ' ' + entry.uid;
156             String addrPortUid = addrPort + ' ' + entry.uid;
157 
158             if (isPortListening(entry.state, isTcp)
159                     && !(isException(addrPort) || isException(addrUid) || isException(addrPortUid))
160                     && !(isUserDebugException(addrPort) || isUserDebugException(addrPortUid))
161                     && !(tv && isOemUid(entry.uid) && isOemException(addrPort))
162                     && (!entry.localAddress.isLoopbackAddress() ^ loopback)) {
163                 if (isTcp && !isTcpConnectable(entry.localAddress, entry.port)) {
164                     continue;
165                 }
166                 // allow non-system processes to listen
167                 int appId = UserHandle.getAppId(entry.uid);
168                 if (appId >= Process.FIRST_APPLICATION_UID
169                         && appId <= Process.LAST_APPLICATION_UID) {
170                     continue;
171                 }
172                 errors += "\nFound port listening on addr="
173                         + entry.localAddress.getHostAddress() + ", port="
174                         + entry.port + ", UID=" + entry.uid
175                         + " " + uidToPackage(entry.uid);
176             }
177         }
178         if (!errors.equals("")) {
179             throw new ListeningPortsAssertionError(errors);
180         }
181     }
182 
uidToPackage(int uid)183     private String uidToPackage(int uid) {
184         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
185         PackageManager pm = this.getContext().getPackageManager();
186         String[] packages;
187         try {
188             uiAutomation.adoptShellPermissionIdentity();
189             packages = pm.getPackagesForUid(uid);
190         } finally {
191             uiAutomation.dropShellPermissionIdentity();
192         }
193         if (packages == null) {
194             return "[unknown]";
195         }
196         return Arrays.asList(packages).toString();
197     }
198 
isTcpConnectable(InetAddress address, int port)199     private boolean isTcpConnectable(InetAddress address, int port) {
200         Socket socket = new Socket();
201 
202         try {
203             if (Log.isLoggable(TAG, Log.DEBUG)) {
204                 Log.d(TAG, "Trying to connect " + address + ":" + port);
205             }
206             socket.connect(new InetSocketAddress(address, port), CONN_TIMEOUT_IN_MS);
207         } catch (IOException ioe) {
208             if (Log.isLoggable(TAG, Log.DEBUG)) {
209                 Log.d(TAG, "Unable to connect:" + ioe);
210             }
211             return false;
212         } finally {
213             try {
214                 socket.close();
215             } catch (IOException closeError) {
216                 Log.e(TAG, "Unable to close socket: " + closeError);
217             }
218         }
219 
220         if (Log.isLoggable(TAG, Log.DEBUG)) {
221             Log.d(TAG, address + ":" + port + " is connectable.");
222         }
223         return true;
224     }
225 
isException(String localAddress)226     private static boolean isException(String localAddress) {
227         return isPatternMatch(EXCEPTION_PATTERNS, localAddress);
228     }
229 
isUserDebugException(String localAddress)230     private static boolean isUserDebugException(String localAddress) {
231         if (!(Build.IS_USERDEBUG || Build.IS_ENG)) {
232             return false;
233         }
234         return isPatternMatch(USERDEBUG_EXCEPTION_PATTERNS, localAddress);
235     }
236 
isOemException(String localAddress)237     private static boolean isOemException(String localAddress) {
238         return isPatternMatch(OEM_EXCEPTION_PATTERNS, localAddress);
239     }
240 
isPatternMatch(List<String> patterns, String input)241     private static boolean isPatternMatch(List<String> patterns, String input) {
242         for (String pattern : patterns) {
243             pattern = Pattern.quote(pattern);
244             if (Pattern.matches(pattern, input)) {
245                 return true;
246             }
247         }
248         return false;
249     }
250 
isPortListening(String state, boolean isTcp)251     private static boolean isPortListening(String state, boolean isTcp) {
252         // 0A = TCP_LISTEN from include/net/tcp_states.h
253         String listeningState = isTcp ? "0A" : "07";
254         return listeningState.equals(state);
255     }
256 
257     private static class ListeningPortsAssertionError extends AssertionFailedError {
ListeningPortsAssertionError(String msg)258         private ListeningPortsAssertionError(String msg) {
259             super(msg);
260         }
261     }
262 
263     private static class ParsedProcEntry {
264         private final InetAddress localAddress;
265         private final int port;
266         private final String state;
267         private final int uid;
268 
ParsedProcEntry(InetAddress addr, int port, String state, int uid)269         private ParsedProcEntry(InetAddress addr, int port, String state, int uid) {
270             this.localAddress = addr;
271             this.port = port;
272             this.state = state;
273             this.uid = uid;
274         }
275 
276 
parse(String procFileContents)277         private static List<ParsedProcEntry> parse(String procFileContents) throws IOException {
278 
279             List<ParsedProcEntry> retval = new ArrayList<ParsedProcEntry>();
280             /*
281             * Sample output of "cat /proc/net/tcp" on emulator:
282             *
283             * sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  ...
284             * 0: 0100007F:13AD 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0   ...
285             * 1: 00000000:15B3 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0   ...
286             * 2: 0F02000A:15B3 0202000A:CE8A 01 00000000:00000000 00:00000000 00000000     0   ...
287             *
288             */
289 
290             Scanner scanner = null;
291             try {
292                 scanner = new Scanner(procFileContents);
293                 while (scanner.hasNextLine()) {
294                     String line = scanner.nextLine().trim();
295 
296                     // Skip column headers
297                     if (line.startsWith("sl")) {
298                         continue;
299                     }
300 
301                     String[] fields = line.split("\\s+");
302                     final int expectedNumColumns = 12;
303                     assertTrue(line + " should have at least " + expectedNumColumns
304                             + " columns of output " + Arrays.toString(fields),
305                             fields.length >= expectedNumColumns);
306 
307                     String state = fields[3];
308                     int uid = Integer.parseInt(fields[7]);
309                     InetAddress localIp = addrToInet(fields[1].split(":")[0]);
310                     int localPort = Integer.parseInt(fields[1].split(":")[1], 16);
311 
312                     retval.add(new ParsedProcEntry(localIp, localPort, state, uid));
313                 }
314             } finally {
315                 if (scanner != null) {
316                     scanner.close();
317                 }
318             }
319             return retval;
320         }
321 
322         /**
323          * Convert a string stored in little endian format to an IP address.
324          */
addrToInet(String s)325         private static InetAddress addrToInet(String s) throws UnknownHostException {
326             int len = s.length();
327             if (len != 8 && len != 32) {
328                 throw new IllegalArgumentException(len + "");
329             }
330             byte[] retval = new byte[len / 2];
331 
332             for (int i = 0; i < len / 2; i += 4) {
333                 retval[i] = (byte) ((Character.digit(s.charAt(2*i + 6), 16) << 4)
334                         + Character.digit(s.charAt(2*i + 7), 16));
335                 retval[i + 1] = (byte) ((Character.digit(s.charAt(2*i + 4), 16) << 4)
336                         + Character.digit(s.charAt(2*i + 5), 16));
337                 retval[i + 2] = (byte) ((Character.digit(s.charAt(2*i + 2), 16) << 4)
338                         + Character.digit(s.charAt(2*i + 3), 16));
339                 retval[i + 3] = (byte) ((Character.digit(s.charAt(2*i), 16) << 4)
340                         + Character.digit(s.charAt(2*i + 1), 16));
341             }
342             return InetAddress.getByAddress(retval);
343         }
344     }
345 }
346