Initial mitmproxy plugin implementation

The pcapdroid_mitm.py plugin allows PCAPdroid to receive the decrypted data from
mitmproxy and display it into the GUI (currently limited to the HTTP request)
This commit is contained in:
emanuele-f 2022-02-06 00:21:32 +01:00
parent c1276d23b4
commit c2ad297005
9 changed files with 363 additions and 8 deletions

View File

@ -74,6 +74,7 @@ import com.emanuelef.remote_capture.pcap_dump.HTTPServer;
import com.emanuelef.remote_capture.interfaces.PcapDumper;
import com.emanuelef.remote_capture.pcap_dump.UDPDumper;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
@ -100,6 +101,7 @@ public class CaptureService extends VpnService implements Runnable {
private Thread mBlacklistsUpdateThread;
private Thread mConnUpdateThread;
private Thread mDumperThread;
private PlaintextReceiver mPlaintextReceiver;
private final LinkedBlockingDeque<Pair<ConnectionDescriptor[], ConnectionUpdate[]>> mPendingUpdates = new LinkedBlockingDeque<>(32);
private LinkedBlockingDeque<byte[]> mDumpQueue;
private String vpn_ipv4;
@ -288,6 +290,17 @@ public class CaptureService extends VpnService implements Runnable {
}
}
// adb shell run-as com.emanuelef.remote_capture.debug touch cache/BETA_PLAINTEXT_RECEIVER
if(mSettings.socks5_enabled && (new File(getCacheDir() + "/BETA_PLAINTEXT_RECEIVER")).exists()) {
mPlaintextReceiver = new PlaintextReceiver();
try {
mPlaintextReceiver.start();
} catch (IOException e) {
e.printStackTrace();
mPlaintextReceiver = null;
}
}
if ((mSettings.app_filter != null) && (!mSettings.app_filter.isEmpty())) {
try {
app_filter_uid = getPackageManager().getApplicationInfo(mSettings.app_filter, 0).uid;
@ -620,6 +633,15 @@ public class CaptureService extends VpnService implements Runnable {
mDumperThread = null;
mDumper = null;
if(mPlaintextReceiver != null) {
try {
mPlaintextReceiver.stop();
} catch (IOException e) {
e.printStackTrace();
}
mPlaintextReceiver = null;
}
if(mParcelFileDescriptor != null) {
try {
mParcelFileDescriptor.close();

View File

@ -303,6 +303,14 @@ public class ConnectionsRegister {
return -1;
}
public synchronized @Nullable ConnectionDescriptor getConnById(int incr_id) {
int pos = getConnPositionById(incr_id);
if(pos < 0)
return null;
return getConn(pos);
}
public synchronized List<AppStats> getAppsStats() {
ArrayList<AppStats> rv = new ArrayList<>(mAppsStats.size());

View File

@ -0,0 +1,216 @@
/*
* This file is part of PCAPdroid.
*
* PCAPdroid is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PCAPdroid is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright 2022 - Emanuele Faranda
*/
package com.emanuelef.remote_capture;
import android.util.Log;
import android.util.LruCache;
import com.emanuelef.remote_capture.interfaces.ConnectionsListener;
import com.emanuelef.remote_capture.model.ConnectionDescriptor;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
/* An experimental receiver for the TLS decryption plaintext.
*
* A mitmproxy plugin sends TCP messages on port 5750, containing an header and the plaintext.
*
* The header is an ASCII string in the following format:
* "port:payload_type:payload_length\n"
* - port: the TCP local port used by the SOCKS5 client
* - payload_type: http_req|http_rep|ws_climsg|ws_srvmsg
* - payload_length: the payload length in bytes:
*
* The raw payload data follows the header.
*/
public class PlaintextReceiver implements Runnable, ConnectionsListener {
private static final String TAG = "PlaintextReceiver";
public static final int MAX_PLAINTEXT_LENGTH = 1024; // sync with pcapdroid.h
private ServerSocket mSocket;
private Socket mClient;
private Thread mThread;
private boolean mRunning;
private final ConnectionsRegister mReg;
// Shared state
private final LruCache<Integer, Integer> mPortToConnId = new LruCache<>(64);
public PlaintextReceiver() {
mReg = CaptureService.requireConnsRegister();
}
public void start() throws IOException {
mSocket = new ServerSocket();
mSocket.setReuseAddress(true);
mSocket.bind(new InetSocketAddress(5750), 1);
mRunning = true;
mThread = new Thread(this);
mThread.start();
mReg.addListener(this);
}
public void stop() throws IOException {
mRunning = false;
mReg.removeListener(this);
// Possibly generate a socket exception on the thread
mSocket.close();
if(mClient != null)
mClient.close();
while((mThread != null) && (mThread.isAlive())) {
try {
Log.d(TAG, "Joining receiver thread...");
mThread.join();
} catch (InterruptedException e) {
Log.e(TAG, "Joining receiver thread failed");
}
}
}
@Override
public void run() {
while(mRunning) {
try {
mClient = mSocket.accept();
Log.d(TAG, String.format("Client: %s:%d", mClient.getInetAddress().getHostAddress(), mClient.getPort()));
handleClient();
} catch (IOException e) {
if(!mRunning)
Log.d(TAG, "Got termination request");
else
Log.d(TAG, e.getLocalizedMessage());
}
}
try {
if(mClient != null)
mClient.close();
} catch (IOException ignored) {}
mClient = null;
}
private void handleClient() throws IOException {
try(DataInputStream istream = new DataInputStream(mClient.getInputStream())) {
while(mRunning) {
String payload_type;
int port;
int payload_len;
// Read the header
String header = istream.readLine();
StringTokenizer tk = new StringTokenizer(header);
//Log.d(TAG, "[HEADER] " + header);
try {
// port:payload_type:payload_length\n
String tk_port = tk.nextToken(":");
payload_type = tk.nextToken();
String tk_len = tk.nextToken();
port = Integer.parseInt(tk_port);
payload_len = Integer.parseInt(tk_len);
} catch (NoSuchElementException | NumberFormatException e) {
Log.w(TAG, "Invalid header");
return;
}
if((payload_len <= 0) || (payload_len > 1048576)) { /* max 1 MB */
Log.w(TAG, "Bad payload length: " + payload_len);
return;
}
if(payload_type.equals("http_req")) {
byte[] payload = new byte[payload_len];
istream.readFully(payload);
//Log.d(TAG, "HTTP_REQUEST [" + payload_len + "]");
ConnectionDescriptor conn = getConnByLocalPort(port);
if((conn != null) && (conn.l7proto.equals("TLS")) && (conn.request_plaintext.isEmpty())) {
// NOTE: we are accessing conn concurrently, however request_plaintext is
// never set inline for encrypted flows.
conn.request_plaintext = getPlaintextString(payload);
}
} else
istream.skipBytes(payload_len); // ignore for now
}
}
}
@Override
public void connectionsChanges(int num_connetions) {}
@Override
public void connectionsRemoved(int start, ConnectionDescriptor[] conns) {}
@Override
public void connectionsUpdated(int[] positions) {}
@Override
public void connectionsAdded(int start, ConnectionDescriptor[] conns) {
synchronized(mPortToConnId) {
// Save the latest port->ID mapping
for(ConnectionDescriptor conn: conns)
mPortToConnId.put(conn.local_port, conn.incr_id);
}
}
ConnectionDescriptor getConnByLocalPort(int local_port) {
Integer conn_id;
synchronized(mPortToConnId) {
conn_id = mPortToConnId.get(local_port);
}
if(conn_id == null)
return null;
ConnectionDescriptor conn = mReg.getConnById(conn_id);
if((conn == null) || (conn.local_port != local_port))
return null;
// success
return conn;
}
// sync with pcapdroid.c
private boolean is_plaintext(byte c) {
return ((c >= 32) && (c <= 126)) || (c == '\r') || (c == '\n') || (c == '\t');
}
private String getPlaintextString(byte []bytes) {
int i = 0;
int limit = Math.min(bytes.length, MAX_PLAINTEXT_LENGTH);
while(i < limit) {
if(!is_plaintext(bytes[i]))
break;
i++;
}
return new String(bytes, 0, i, StandardCharsets.US_ASCII);
}
}

View File

@ -66,6 +66,7 @@ public class ConnectionDescriptor implements Serializable {
public final String dst_ip;
public final int src_port;
public final int dst_port;
public final int local_port; // in VPN mode, this is the local port of the Internet connection
/* Data */
public long first_seen;
@ -94,7 +95,7 @@ public class ConnectionDescriptor implements Serializable {
public boolean alerted;
public ConnectionDescriptor(int _incr_id, int _ipver, int _ipproto, String _src_ip, String _dst_ip,
int _src_port, int _dst_port, int _uid, int _ifidx, long when) {
int _src_port, int _dst_port, int _local_port, int _uid, int _ifidx, long when) {
incr_id = _incr_id;
ipver = _ipver;
ipproto = _ipproto;
@ -102,6 +103,7 @@ public class ConnectionDescriptor implements Serializable {
dst_ip = _dst_ip;
src_port = _src_port;
dst_port = _dst_port;
local_port = _local_port;
uid = _uid;
ifidx = _ifidx;
first_seen = last_seen = when;

View File

@ -370,6 +370,13 @@ static void update_conn_status(zdtun_t *zdt, const zdtun_pkt_t *pkt, uint8_t fro
/* ******************************************************* */
// TODO with built-in decryption, only proxy encrypted connections
static bool should_proxy(pcapdroid_t *pd, const zdtun_5tuple_t *tuple) {
return pd->socks5.enabled && (tuple->ipproto == IPPROTO_TCP);
}
/* ******************************************************* */
int run_vpn(pcapdroid_t *pd) {
zdtun_t *zdt;
char buffer[VPN_BUFFER_SIZE];
@ -519,7 +526,7 @@ int run_vpn(pcapdroid_t *pd) {
spoof_dns_reply(pd, conn, &pctx);
zdtun_conn_close(zdt, conn, CONN_STATUS_CLOSED);
}
} else if(pd->socks5.enabled && (tuple->ipproto == IPPROTO_TCP))
} else if(should_proxy(pd, tuple))
zdtun_conn_proxy(conn);
}
@ -541,10 +548,30 @@ int run_vpn(pcapdroid_t *pd) {
pd->num_dropped_connections++;
zdtun_conn_close(zdt, conn, CONN_STATUS_ERROR);
goto housekeeping;
} else if(data->vpn.fw_pctx) {
// not accounted in remote2vpn, account here
pd_account_stats(pd, data->vpn.fw_pctx);
data->vpn.fw_pctx = NULL;
} else {
// zdtun_forward was successful
if(data->vpn.fw_pctx) {
// not accounted in remote2vpn, account here
pd_account_stats(pd, data->vpn.fw_pctx);
data->vpn.fw_pctx = NULL;
}
// First forwarded packet
if(data->sent_pkts == 1) {
// The socket is open only after zdtun_forward is called
socket_t sock = zdtun_conn_get_socket(conn);
// In SOCKS5 with the PlaintextReceiver, we need the local port to the SOCKS5 proxy
if((sock != INVALID_SOCKET) && (tuple->ipver == 4)) {
// NOTE: the zdtun SOCKS5 implementation only supports IPv4 right now.
// If it also supported IPv6, than we would need to expose "sock_ipver"
struct sockaddr_in local_addr;
socklen_t addrlen = sizeof(local_addr);
if(getsockname(sock, (struct sockaddr*) &local_addr, &addrlen) == 0)
data->vpn.local_port = local_addr.sin_port;
}
}
}
} else {
pd_refresh_time(pd);

View File

@ -210,10 +210,12 @@ static int dumpNewConnection(pcapdroid_t *pd, const conn_and_tuple_t *conn, jobj
jobject src_string = (*env)->NewStringUTF(env, srcip);
jobject dst_string = (*env)->NewStringUTF(env, dstip);
u_int ifidx = (pd->root_capture ? data->root.ifidx : 0);
u_int local_port = (!pd->root_capture ? data->vpn.local_port : 0);
jobject conn_descriptor = (*env)->NewObject(env, cls.conn, mids.connInit, data->incr_id,
conn_info->ipver, conn_info->ipproto,
src_string, dst_string,
ntohs(conn_info->src_port), ntohs(conn_info->dst_port),
ntohs(local_port),
data->uid, ifidx, data->first_seen);
if((conn_descriptor != NULL) && !jniCheckException(env)) {
@ -452,7 +454,7 @@ Java_com_emanuelef_remote_1capture_CaptureService_runPacketLoop(JNIEnv *env, jcl
mids.getLibprogPath = jniGetMethodID(env, vpn_class, "getLibprogPath", "(Ljava/lang/String;)Ljava/lang/String;");
mids.notifyBlacklistsLoaded = jniGetMethodID(env, vpn_class, "notifyBlacklistsLoaded", "([Lcom/emanuelef/remote_capture/model/Blacklists$NativeBlacklistStatus;)V");
mids.getBlacklistsInfo = jniGetMethodID(env, vpn_class, "getBlacklistsInfo", "()[Lcom/emanuelef/remote_capture/model/BlacklistDescriptor;");
mids.connInit = jniGetMethodID(env, cls.conn, "<init>", "(IIILjava/lang/String;Ljava/lang/String;IIIIJ)V");
mids.connInit = jniGetMethodID(env, cls.conn, "<init>", "(IIILjava/lang/String;Ljava/lang/String;IIIIIJ)V");
mids.connProcessUpdate = jniGetMethodID(env, cls.conn, "processUpdate", "(Lcom/emanuelef/remote_capture/model/ConnectionUpdate;)V");
mids.connUpdateInit = jniGetMethodID(env, cls.conn_update, "<init>", "(I)V");
mids.connUpdateSetStats = jniGetMethodID(env, cls.conn_update, "setStats", "(JJJIIIII)V");

View File

@ -468,6 +468,7 @@ void pd_giveup_dpi(pcapdroid_t *pd, pd_conn_t *data, const zdtun_5tuple_t *tuple
/* ******************************************************* */
// sync with PlaintextReceiver
static int is_plaintext(char c) {
return isprint(c) || (c == '\r') || (c == '\n') || (c == '\t');
}

View File

@ -39,7 +39,7 @@
#define MAX_HOST_LRU_SIZE 256
#define JAVA_PCAP_BUFFER_SIZE (512*1024) // 512K
#define PERIODIC_PURGE_TIMEOUT_MS 5000
#define MAX_PLAINTEXT_LENGTH 1024
#define MAX_PLAINTEXT_LENGTH 1024 // sync with PlaintextReceiver
#define MIN_REQ_PLAINTEXT_CHARS 3 // Minimum length (e.g. of "GET") to avoid reporting non-requests
#define DNS_FLAGS_MASK 0x8000
@ -74,6 +74,7 @@ typedef struct {
} root;
struct {
struct pkt_context *fw_pctx; // context for the forwarded packet
uint16_t local_port; // local port, from zdtun to the Internet
} vpn;
};

76
tools/pcapdroid_mitm.py Executable file
View File

@ -0,0 +1,76 @@
#!/usr/bin/env python3
#
# This file is part of PCAPdroid.
#
# PCAPdroid is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PCAPdroid is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
#
# Copyright 2022 - Emanuele Faranda
#
# mitmdump -q -p 8050 --mode socks5 -s pcapdroid.py
from enum import Enum
import socket
import errno
from mitmproxy import http
from mitmproxy.net.http.http1.assemble import assemble_request, assemble_response
class PayloadType(Enum):
HTTP_REQUEST = "http_req"
HTTP_REPLY = "http_rep"
WEBSOCKET_CLIENT_MSG = "ws_climsg"
WEBSOCKET_SERVER_MSG = "ws_srvmsg"
class PCAPdroid:
def __init__(self):
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect(("127.0.0.1", 5750))
except socket.error as e:
print(e)
exit(1) # NOTE: this does not terminate mitmdump
def send_payload(self, flow: http.HTTPFlow, payload_type: PayloadType, payload):
client_port = flow.client_conn.peername[1]
header = "%u:%s:%u\n" % (client_port, payload_type.value, len(payload))
try:
self.sock.sendall(header.encode('ascii'))
self.sock.sendall(payload)
except socket.error as e:
if e.errno == errno.EPIPE:
print("PCAPdroid closed")
exit(0)
else:
print(e)
exit(1)
def request(self, flow: http.HTTPFlow):
if flow.request:
self.send_payload(flow, PayloadType.HTTP_REQUEST, assemble_request(flow.request))
def response(self, flow: http.HTTPFlow) -> None:
if flow.response:
self.send_payload(flow, PayloadType.HTTP_REPLY, assemble_response(flow.response))
def websocket_message(self, flow: http.HTTPFlow):
msg = flow.websocket.messages[-1]
if not msg:
return
payload_type = PayloadType.WEBSOCKET_CLIENT_MSG if msg.from_client else PayloadType.WEBSOCKET_SERVER_MSG
self.send_payload(flow, payload_type, msg.content)
addons = [
PCAPdroid()
]