From 653cb60b9e8b12d7311ebe5e97ed4eeedefbdcd4 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Tue, 19 Sep 2023 19:34:44 +0200 Subject: [PATCH] Manually hook libc connect() to capture even more traffic --- native-hook.js | 105 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 native-hook.js diff --git a/native-hook.js b/native-hook.js new file mode 100644 index 0000000..676e642 --- /dev/null +++ b/native-hook.js @@ -0,0 +1,105 @@ +/** + * In some cases, proxy configuration by itself won't work. This notably includes Flutter apps (which ignore + * system/JVM configuration entirely) and plausibly other apps intentionally ignoring proxies. To handle that + * we hook libc's connect() directly to redirect traffic on all recognized ports. + * + * This handles all calls to connect an outgoing socket, and for all TCP connections opened on recognized ports, + * it will manually replace the connect parameters, so that the socket connects to the proxy instead of the + * 'real' destination. + * + * This doesn't help with certificate trust (you still need some kind of certificate setup) but it does ensure + * the proxy receives all connections (and so will see if connections don't trust its CA). It's still useful + * to do proxy config alongside this, primarily to capture traffic that might be sent on any non-standard ports. + */ + +const PROXY_PORT = 8000; +const PROXY_HOST = '127.0.0.1'; + +// Ports which we recognize, and forcibly redirect to capture, if not captured already. +const RECOGNIZED_PORTS = [ + 80, + 443, + 4443, + 8000, + 8080, + 8443, + 8888, + 9000 +]; + +const PROXY_HOST_IPv4_BYTES = PROXY_HOST.split('.').map(part => parseInt(part, 10)); +const IPv6_MAPPING_PREFIX_BYTES = [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff]; +const PROXY_HOST_IPv6_BYTES = IPv6_MAPPING_PREFIX_BYTES.concat(PROXY_HOST_IPv4_BYTES); + +const connectFn = ( + Module.findExportByName('libc.so', 'connect') ?? // Android + Module.findExportByName('libc.so.6', 'connect') // Linux +); + +if (!connectFn) { // Should always be set, but just in case + console.warn('Could not find libc connect() function to hook raw traffic'); +} else { + Interceptor.attach(connectFn, { + onEnter(args) { + const fd = this.sockFd = args[0].toInt32(); + const sockType = Socket.type(fd); + + const addrPtr = ptr(args[1]); + const addrLen = args[2].toInt32(); // TODO: Probably not right? + const addrData = addrPtr.readByteArray(addrLen); + + if (sockType === 'tcp' || sockType === 'tcp6') { + const portAddrBytes = new DataView(addrData.slice(2, 4)); + const port = portAddrBytes.getUint16(0, false); // Big endian! + + const shouldBeIntercepted = RECOGNIZED_PORTS.includes(port); + if (!shouldBeIntercepted) return; // Unrecognized port - probably not HTTP(S) + + const isIPv6 = sockType === 'tcp6'; + + const hostBytes = isIPv6 + // 16 bytes offset by 8 (2 for family, 2 for port, 4 for flowinfo): + ? new Uint8Array(addrData.slice(8, 8 + 16)) + // 4 bytes, offset by 4 (2 for family, 2 for port) + : new Uint8Array(addrData.slice(4, 4 + 4)); + + const isIntercepted = port === PROXY_PORT && areArraysEqual(hostBytes, + isIPv6 + ? PROXY_HOST_IPv6_BYTES + : PROXY_HOST_IPv4_BYTES + ); + + if (isIntercepted) return; + + console.log(`Manually intercepting connection to ${ + isIPv6 + ? `[${[...hostBytes].map(x => x.toString(16)).join(':')}]` + : [...hostBytes].map(x => x.toString()).join('.') + }:${port}`); + + // Overwrite the port with the proxy port: + portAddrBytes.setUint16(0, PROXY_PORT, false); // Big endian + addrPtr.add(2).writeByteArray(portAddrBytes.buffer); + + // Overwrite the address with the proxy address: + if (isIPv6) { + // Skip 8 bytes: 2 family, 2 port, 4 flowinfo + addrPtr.add(8).writeByteArray(PROXY_HOST_IPv6_BYTES); + } else { + // Skip 4 bytes: 2 family, 2 port + addrPtr.add(4).writeByteArray(PROXY_HOST_IPv4_BYTES); + } + } + + // N.b. we ignore all non-TCP connections: both UDP and Unix streams + } + }); + + console.log(`Hooked connect() at ${connectFn}`); +} + +const areArraysEqual = (arrayA, arrayB) => { + if (arrayA.length !== arrayB.length) return false; + return arrayA.every((x, i) => arrayB[i] === x); +}; +