mirror of
https://github.com/emanuele-f/PCAPdroid.git
synced 2026-06-11 21:01:45 +08:00
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:
parent
c1276d23b4
commit
c2ad297005
@ -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();
|
||||
|
||||
@ -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());
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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
76
tools/pcapdroid_mitm.py
Executable 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()
|
||||
]
|
||||
Loading…
Reference in New Issue
Block a user