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