mirror of
https://github.com/emanuele-f/PCAPdroid.git
synced 2026-05-08 21:12:26 +00:00
Fix mismatched HTTP/2 replies with multiplexing (ushark)
When decrypting PCAP/Pcapng, the HTTP/2 replies would be mismatched when HTTP/2 multiplexing was used and replies were received out of order. To fix this, keep track of the HTTP/2 stream IDs to correcly match HTTP requests to replies
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@ local.properties
|
||||
.DS_Store
|
||||
build/
|
||||
captures/
|
||||
pcap/
|
||||
/pcap/
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
app/release/
|
||||
|
||||
@@ -108,8 +108,15 @@ public class HTTPReassembly {
|
||||
/* The request/response tab shows reassembled HTTP chunks.
|
||||
* Reassembling chunks is requires when using a content-encoding like gzip since we can only
|
||||
* decode the data when we have the full chunk and we cannot determine data bounds.
|
||||
*
|
||||
* When reading data via the MitmReceiver, mitmproxy already performs chunks reassembly and
|
||||
* also handles HTTP/2 so that we only get the payload. */
|
||||
* also handles HTTP/2 so that we only get the payload.
|
||||
*
|
||||
* Similarly, also ushark performs HTTP/2 request/reply reassembly, so we always get the whole
|
||||
* HTTP headers and body in a single chunk. With ushark, though, HTTP/2 replies may arrive in
|
||||
* a different order (due to HTTP/2 multiplexing). chunk.stream_id is used by ConnectionDescriptor
|
||||
* to associate the reply to the correct request.
|
||||
*/
|
||||
public void handleChunk(PayloadChunk chunk) {
|
||||
int body_start = 0;
|
||||
byte[] payload = chunk.payload;
|
||||
@@ -158,6 +165,8 @@ public class HTTPReassembly {
|
||||
}
|
||||
|
||||
mFirstChunk.httpPath = path;
|
||||
|
||||
//log_d(mFirstChunk.httpMethod + " " + mFirstChunk.httpPath);
|
||||
}
|
||||
} else if (line.startsWith("HTTP/")) {
|
||||
int first_space = line.indexOf(' ');
|
||||
@@ -255,6 +264,12 @@ public class HTTPReassembly {
|
||||
if(!mReassembleChunks)
|
||||
mReadingHeaders = false;
|
||||
|
||||
boolean httpRst = false;
|
||||
if (mReadingHeaders && chunk.isHttp2Rst()) {
|
||||
mReadingHeaders = false;
|
||||
httpRst = true;
|
||||
}
|
||||
|
||||
if(!mReadingHeaders) {
|
||||
// Reading HTTP body
|
||||
int body_size = payload.length - body_start;
|
||||
@@ -349,6 +364,11 @@ public class HTTPReassembly {
|
||||
to_add.httpQuery = mFirstChunk.httpQuery;
|
||||
to_add.httpBodyLength = mBodySize;
|
||||
|
||||
if (httpRst)
|
||||
// this is necessary when mDumpPayload=false, to ensure that
|
||||
// the chunk is marked as HTTP RST
|
||||
to_add.setHttpRst();
|
||||
|
||||
// Fix the chunk type after upgrade when read from ushark
|
||||
if (mSwitchedProtocols && (to_add.type == PayloadChunk.ChunkType.HTTP)) {
|
||||
to_add.type = mWebsocketUpgrade ? PayloadChunk.ChunkType.WEBSOCKET : PayloadChunk.ChunkType.RAW;
|
||||
@@ -359,6 +379,12 @@ public class HTTPReassembly {
|
||||
}
|
||||
|
||||
mBodySize = 0;
|
||||
|
||||
if ((to_add.type == PayloadChunk.ChunkType.HTTP)) {
|
||||
Log.d(TAG, "Reassembled HTTP " +
|
||||
(to_add.isHttp2Rst() ? "RST" : (to_add.is_sent ? "request" : "response")));
|
||||
}
|
||||
|
||||
mListener.onChunkReassembled(to_add);
|
||||
reset(); // mReadingHeaders = true
|
||||
|
||||
|
||||
@@ -43,8 +43,10 @@ public class HttpLog {
|
||||
public String path = "";
|
||||
public String query = "";
|
||||
public HttpReply reply;
|
||||
public int bodyLength;
|
||||
public long timestamp;
|
||||
public int bodyLength = 0;
|
||||
public int streamId = 0;
|
||||
public long timestamp = 0;
|
||||
public boolean httpRst = false;
|
||||
private int idx = -1;
|
||||
|
||||
public HttpRequest(ConnectionDescriptor conn, int firstChunkPos) {
|
||||
|
||||
@@ -327,7 +327,7 @@ public class MitmReceiver implements Runnable, ConnectionsListener, MitmListener
|
||||
} else if(type == MsgType.JS_INJECTED) {
|
||||
conn.js_injected_scripts = new String(message, StandardCharsets.US_ASCII);
|
||||
} else
|
||||
conn.addPayloadChunkMitm(new PayloadChunk(message, getChunkType(type), isSent(type), tstamp));
|
||||
conn.addPayloadChunkMitm(new PayloadChunk(message, getChunkType(type), isSent(type), tstamp, 0));
|
||||
}
|
||||
|
||||
private synchronized void addPendingMessage(PendingMessage pending) {
|
||||
|
||||
@@ -126,7 +126,7 @@ public class PayloadAdapter extends RecyclerView.Adapter<PayloadAdapter.PayloadV
|
||||
return req.conn.getHttpRequestChunk(req.firstChunkPos);
|
||||
|
||||
// return an empty chunk instead of null to activate the single-chunk mode
|
||||
return new PayloadChunk(new byte[0], ChunkType.HTTP, true, 0);
|
||||
return new PayloadChunk(new byte[0], ChunkType.HTTP, true, 0, 0);
|
||||
}
|
||||
|
||||
public void setExportPayloadHandler(ExportPayloadHandler handler) {
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.emanuelef.remote_capture.AppsResolver;
|
||||
import com.emanuelef.remote_capture.CaptureService;
|
||||
import com.emanuelef.remote_capture.HTTPReassembly;
|
||||
import com.emanuelef.remote_capture.HttpLog;
|
||||
import com.emanuelef.remote_capture.Log;
|
||||
import com.emanuelef.remote_capture.PCAPdroid;
|
||||
import com.emanuelef.remote_capture.R;
|
||||
|
||||
@@ -47,6 +48,8 @@ import java.util.concurrent.atomic.AtomicReference;
|
||||
* or sets a previously null field to a non-null value.
|
||||
*/
|
||||
public class ConnectionDescriptor implements HTTPReassembly.ReassemblyListener {
|
||||
public static final String TAG = "ConnectionDescriptor";
|
||||
|
||||
// sync with zdtun_conn_status_t
|
||||
public static final int CONN_STATUS_NEW = 0,
|
||||
CONN_STATUS_CONNECTING = 1,
|
||||
@@ -233,11 +236,11 @@ public class ConnectionDescriptor implements HTTPReassembly.ReassemblyListener {
|
||||
|
||||
// will call onChunkReassembled
|
||||
if (chunk.is_sent) {
|
||||
if (mFirstReqChunkPos == -1)
|
||||
if ((mFirstReqChunkPos == -1) && !chunk.isHttp2Rst())
|
||||
mFirstReqChunkPos = chunk_pos;
|
||||
mHttpReqReassembly.handleChunk(chunk);
|
||||
} else {
|
||||
if (mFirstReplyChunkPos == -1)
|
||||
if ((mFirstReplyChunkPos == -1) && !chunk.isHttp2Rst())
|
||||
mFirstReplyChunkPos = chunk_pos;
|
||||
mHttpReplyReassembly.handleChunk(chunk);
|
||||
}
|
||||
@@ -439,31 +442,62 @@ public class ConnectionDescriptor implements HTTPReassembly.ReassemblyListener {
|
||||
if (httplog == null)
|
||||
return;
|
||||
|
||||
if (chunk.is_sent) {
|
||||
if (chunk.is_sent && !chunk.isHttp2Rst()) {
|
||||
HttpLog.HttpRequest request = new HttpLog.HttpRequest(this, mFirstReqChunkPos);
|
||||
request.host = !chunk.httpHost.isEmpty() ? chunk.httpHost : info;
|
||||
request.method = chunk.httpMethod;
|
||||
request.path = chunk.httpPath;
|
||||
request.query = chunk.httpQuery;
|
||||
request.bodyLength = chunk.httpBodyLength;
|
||||
request.streamId = chunk.stream_id;
|
||||
request.timestamp = chunk.timestamp;
|
||||
httplog.addHttpRequest(request);
|
||||
|
||||
mPendingRequests.add(request);
|
||||
mFirstReqChunkPos = -1;
|
||||
} else if (!mPendingRequests.isEmpty()) {
|
||||
// find the first un-replied request
|
||||
HttpLog.HttpRequest request = mPendingRequests.remove(0);
|
||||
} else {
|
||||
// match the reply to the request
|
||||
HttpLog.HttpRequest request = null;
|
||||
|
||||
HttpLog.HttpReply reply = new HttpLog.HttpReply(request, mFirstReplyChunkPos);
|
||||
reply.responseCode = chunk.httpResponseCode;
|
||||
reply.responseStatus = chunk.httpResponseStatus;
|
||||
reply.contentType = chunk.httpContentType;
|
||||
reply.bodyLength = chunk.httpBodyLength;
|
||||
request.reply = reply;
|
||||
httplog.addHttpReply(reply);
|
||||
if (chunk.stream_id == 0) {
|
||||
// if HTTP/1, then the request is the first in the list
|
||||
if (!mPendingRequests.isEmpty())
|
||||
request = mPendingRequests.remove(0);
|
||||
} else {
|
||||
// if HTTP/2, use the stream ID for the matching
|
||||
int idx = 0;
|
||||
for (HttpLog.HttpRequest req: mPendingRequests) {
|
||||
if (req.streamId == chunk.stream_id)
|
||||
break;
|
||||
|
||||
mFirstReplyChunkPos = -1;
|
||||
idx++;
|
||||
}
|
||||
|
||||
if (idx < mPendingRequests.size())
|
||||
request = mPendingRequests.remove(idx);
|
||||
}
|
||||
|
||||
if (request != null) {
|
||||
if (!chunk.isHttp2Rst()) {
|
||||
HttpLog.HttpReply reply = new HttpLog.HttpReply(request, mFirstReplyChunkPos);
|
||||
reply.responseCode = chunk.httpResponseCode;
|
||||
reply.responseStatus = chunk.httpResponseStatus;
|
||||
reply.contentType = chunk.httpContentType;
|
||||
reply.bodyLength = chunk.httpBodyLength;
|
||||
request.reply = reply;
|
||||
|
||||
httplog.addHttpReply(reply);
|
||||
mFirstReplyChunkPos = -1;
|
||||
} else {
|
||||
request.httpRst = true;
|
||||
Log.d(TAG, "Got RST: " + request.getUrl());
|
||||
}
|
||||
} else if (!chunk.is_sent) { // ignore HTTP requests with RST
|
||||
if (chunk.isHttp2Rst())
|
||||
Log.w(TAG, "Unmatched HTTP RST (sent=" + chunk.is_sent + ", stream=" + chunk.stream_id + ")");
|
||||
else
|
||||
Log.w(TAG, "Unmatched HTTP reply (sent=" + chunk.is_sent + ", stream=" + chunk.stream_id + ")");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ public class PayloadChunk implements Serializable {
|
||||
public boolean is_sent;
|
||||
public long timestamp;
|
||||
public ChunkType type;
|
||||
public int stream_id;
|
||||
|
||||
// HTTP
|
||||
public int httpResponseCode = 0;
|
||||
@@ -37,6 +38,7 @@ public class PayloadChunk implements Serializable {
|
||||
public String httpQuery = "";
|
||||
public String httpContentType = "";
|
||||
public int httpBodyLength = 0;
|
||||
private boolean mHttpRst = false;
|
||||
|
||||
// Serializable need in ConnectionPayload fragment
|
||||
public enum ChunkType implements Serializable {
|
||||
@@ -45,11 +47,13 @@ public class PayloadChunk implements Serializable {
|
||||
WEBSOCKET
|
||||
}
|
||||
|
||||
public PayloadChunk(byte[] _payload, ChunkType _type, boolean _is_sent, long _timestamp) {
|
||||
// the stream_id is the HTTP/2 stream ID; use 0 for HTTP/1
|
||||
public PayloadChunk(byte[] _payload, ChunkType _type, boolean _is_sent, long _timestamp, int _stream_id) {
|
||||
payload = _payload;
|
||||
type = _type;
|
||||
is_sent = _is_sent;
|
||||
timestamp = _timestamp;
|
||||
stream_id = _stream_id;
|
||||
}
|
||||
|
||||
public PayloadChunk subchunk(int start, int size) {
|
||||
@@ -58,10 +62,20 @@ public class PayloadChunk implements Serializable {
|
||||
|
||||
byte[] subarr = new byte[size];
|
||||
System.arraycopy(payload, start, subarr, 0, size);
|
||||
return new PayloadChunk(subarr, type, is_sent, timestamp);
|
||||
return new PayloadChunk(subarr, type, is_sent, timestamp, stream_id);
|
||||
}
|
||||
|
||||
public PayloadChunk withPayload(byte[] the_payload) {
|
||||
return new PayloadChunk(the_payload, type, is_sent, timestamp);
|
||||
return new PayloadChunk(the_payload, type, is_sent, timestamp, stream_id);
|
||||
}
|
||||
|
||||
public void setHttpRst() {
|
||||
mHttpRst = true;
|
||||
}
|
||||
|
||||
public boolean isHttp2Rst() {
|
||||
// http2.c uses a 0 length payload to indicate HTTP2 reset messages
|
||||
return mHttpRst || ((type == PayloadChunk.ChunkType.HTTP) &&
|
||||
(payload != null) && (payload.length == 0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ extern void (*logcallback)(int lvl, const char *msg);
|
||||
#define log_e(...) log_android(ANDROID_LOG_ERROR, __VA_ARGS__)
|
||||
#define log_f(...) log_android(ANDROID_LOG_FATAL, __VA_ARGS__)
|
||||
|
||||
void set_log_level(int lvl);
|
||||
void log_android(int lvl, const char *fmt, ...);
|
||||
ssize_t xwrite(int fd, const void *buf, size_t count);
|
||||
ssize_t xread(int fd, void *buf, size_t count);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with PCAPdroid. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Copyright 2021-25 - Emanuele Faranda
|
||||
* Copyright 2021-26 - Emanuele Faranda
|
||||
*/
|
||||
|
||||
#include <sys/un.h>
|
||||
@@ -406,34 +406,78 @@ static int get_ip_offset(int linktype) {
|
||||
/* ******************************************************* */
|
||||
|
||||
static plain_data_t g_plain_data = {};
|
||||
static pkt_context_t *g_cur_ctx = NULL;
|
||||
|
||||
static void handle_tls_data(const unsigned char *plain_data, unsigned int data_len) {
|
||||
if (data_len == 0)
|
||||
return;
|
||||
static void handle_http_data(bool is_tx, uint64_t ms, uint32_t stream_id, const unsigned char *plain_data, unsigned int data_len) {
|
||||
// NOTE: (plain_data == NULL) / (data_len == 0) signals HTTP/2 resets
|
||||
|
||||
//log_w("<DATA> %s", plain_data);
|
||||
g_plain_data.data = (unsigned char *) pd_realloc(g_plain_data.data, g_plain_data.tot_length + data_len);
|
||||
g_plain_data.lengths = (unsigned int *) pd_realloc(g_plain_data.lengths, (g_plain_data.n_items + 1) * sizeof(unsigned int));
|
||||
if (!g_plain_data.data || !g_plain_data.lengths) {
|
||||
// allocate the data first, as a failure in this allocation is easier to handle now rather
|
||||
// than after a plain_data_item_t allocation
|
||||
unsigned char *new_item_data_buf = NULL;
|
||||
if (plain_data) {
|
||||
new_item_data_buf = pd_malloc(data_len);
|
||||
if (!new_item_data_buf) {
|
||||
log_e("alloc(http_data) failed[%d]: %s", errno, strerror(errno));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
plain_data_item_t* new_items = (plain_data_item_t*) pd_realloc(g_plain_data.items, (g_plain_data.n_items + 1) * sizeof(plain_data_item_t));
|
||||
if (!new_items) {
|
||||
log_e("realloc(tls_data) failed[%d]: %s", errno, strerror(errno));
|
||||
|
||||
if (g_plain_data.data)
|
||||
pd_free(g_plain_data.data);
|
||||
if (g_plain_data.lengths)
|
||||
pd_free(g_plain_data.lengths);
|
||||
if (new_item_data_buf)
|
||||
pd_free(new_item_data_buf);
|
||||
return;
|
||||
}
|
||||
|
||||
g_plain_data.data = NULL;
|
||||
g_plain_data.lengths = NULL;
|
||||
g_plain_data.tot_length = 0;
|
||||
g_plain_data.n_items = 0;
|
||||
} else {
|
||||
memcpy(g_plain_data.data + g_plain_data.tot_length, plain_data, data_len);
|
||||
g_plain_data.lengths[g_plain_data.n_items] = data_len;
|
||||
g_plain_data.tot_length += data_len;
|
||||
g_plain_data.n_items++;
|
||||
g_plain_data.items = new_items;
|
||||
plain_data_item_t *item = &g_plain_data.items[g_plain_data.n_items++];
|
||||
|
||||
memset(item, 0, sizeof(plain_data_item_t));
|
||||
item->ms = ms;
|
||||
item->is_tx = is_tx;
|
||||
item->stream_id = stream_id;
|
||||
|
||||
if (new_item_data_buf) {
|
||||
memcpy(new_item_data_buf, plain_data, data_len);
|
||||
|
||||
item->data = new_item_data_buf;
|
||||
item->data_length = data_len;
|
||||
}
|
||||
}
|
||||
|
||||
static void handle_ushark_http1_data(uint32_t conv_id, const unsigned char *plain_data, size_t data_len) {
|
||||
if (!g_cur_ctx)
|
||||
return;
|
||||
|
||||
// HTTP/1 is sequential, use 0 as the stream ID
|
||||
handle_http_data(g_cur_ctx->is_tx, g_cur_ctx->ms, 0, plain_data, data_len);
|
||||
}
|
||||
|
||||
static void handle_ushark_http2_request(uint32_t conv_id, uint32_t stream_id, const unsigned char *plain_data, size_t data_len) {
|
||||
if (!g_cur_ctx)
|
||||
return;
|
||||
|
||||
handle_http_data(g_cur_ctx->is_tx, g_cur_ctx->ms, stream_id, plain_data, data_len);
|
||||
}
|
||||
|
||||
static void handle_ushark_http2_response(uint32_t conv_id, uint32_t stream_id, const unsigned char *plain_data, size_t data_len) {
|
||||
if (!g_cur_ctx)
|
||||
return;
|
||||
|
||||
handle_http_data(g_cur_ctx->is_tx, g_cur_ctx->ms, stream_id, plain_data, data_len);
|
||||
}
|
||||
|
||||
static void handle_ushark_http2_reset(uint32_t conv_id, uint32_t stream_id) {
|
||||
if (!g_cur_ctx)
|
||||
return;
|
||||
|
||||
handle_http_data(g_cur_ctx->is_tx, g_cur_ctx->ms, stream_id, NULL, 0);
|
||||
}
|
||||
|
||||
/* ******************************************************* */
|
||||
|
||||
/* Returns true if packet is valid. If false is returned, the pkt must still be dumped, so a call to
|
||||
* pd_dump_packet is required. */
|
||||
static bool handle_packet(pcapdroid_t *pd, pcapd_hdr_t *hdr, const char *buffer, int ipoffset) {
|
||||
@@ -534,17 +578,21 @@ static bool handle_packet(pcapdroid_t *pd, pcapd_hdr_t *hdr, const char *buffer,
|
||||
struct timeval tv = hdr->ts;
|
||||
pkt_context_t pinfo;
|
||||
pd_init_pkt_context(&pinfo, &pkt, is_tx, &conn_tuple, conn->data, &tv);
|
||||
g_cur_ctx = &pinfo;
|
||||
|
||||
if (pd->pcap.usk && (pkt.len > 0)) {
|
||||
struct pcap_pkthdr pcap_hdr;
|
||||
pcap_hdr.len = pcap_hdr.caplen = pkt.len;
|
||||
pcap_hdr.ts = hdr->ts;
|
||||
|
||||
ushark_dissect_tls(pd->pcap.usk,
|
||||
// This disables the reporting of leaks inside libushark.
|
||||
// The reporting is enabled again in the individual callbacks (e.g. handle_ushark_http2_request)
|
||||
// to detect leaks in pcapdroid
|
||||
ushark_dissect(pd->pcap.usk,
|
||||
(const unsigned char*) pkt.l3,
|
||||
&pcap_hdr, handle_tls_data);
|
||||
&pcap_hdr);
|
||||
|
||||
if (g_plain_data.data)
|
||||
if (g_plain_data.n_items > 0)
|
||||
pinfo.plain_data = &g_plain_data;
|
||||
}
|
||||
|
||||
@@ -555,11 +603,14 @@ static bool handle_packet(pcapdroid_t *pd, pcapd_hdr_t *hdr, const char *buffer,
|
||||
|
||||
pd_account_stats(pd, &pinfo);
|
||||
|
||||
if (g_plain_data.data) {
|
||||
pd_free(g_plain_data.data);
|
||||
pd_free(g_plain_data.lengths);
|
||||
if (g_plain_data.items) {
|
||||
for (size_t i = 0; i < g_plain_data.n_items; i++)
|
||||
pd_free(g_plain_data.items[i].data);
|
||||
|
||||
pd_free(g_plain_data.items);
|
||||
memset(&g_plain_data, 0, sizeof(g_plain_data));
|
||||
}
|
||||
g_cur_ctx = NULL;
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -714,6 +765,23 @@ static reader_rv read_file(pcapdroid_t *pd, pd_reader_t *reader, pcapd_hdr_t* hd
|
||||
|
||||
/* ******************************************************* */
|
||||
|
||||
// Wrapper function for ushark initialization to allow LSAN suppression
|
||||
static void init_ushark_for_pcap(pcapdroid_t *pd, const char *keylog_path) {
|
||||
if (ushark_init(pd)) {
|
||||
pd->pcap.usk = ushark_new(PCAPD_DLT_RAW, "");
|
||||
|
||||
ushark_data_callbacks_t cbs = {
|
||||
.on_http1_data = handle_ushark_http1_data,
|
||||
.on_http2_request = handle_ushark_http2_request,
|
||||
.on_http2_response = handle_ushark_http2_response,
|
||||
.on_http2_reset = handle_ushark_http2_reset,
|
||||
};
|
||||
ushark_set_callbacks(pd->pcap.usk, &cbs);
|
||||
|
||||
ushark_set_pref("tls.keylog_file", keylog_path);
|
||||
}
|
||||
}
|
||||
|
||||
int run_pcap(pcapdroid_t *pd) {
|
||||
int sock = -1;
|
||||
pd_reader_t *reader = NULL;
|
||||
@@ -726,15 +794,14 @@ int run_pcap(pcapdroid_t *pd) {
|
||||
|
||||
if (pd->pcap_file_capture) {
|
||||
// check if the SSL keylog exists
|
||||
const char *keylog_path = get_cache_path(pd, "sslkeylog.txt");
|
||||
// Use override path if provided (for tests), otherwise use default location
|
||||
const char *keylog_path = pd->keylog_path_override
|
||||
? pd->keylog_path_override
|
||||
: get_cache_path(pd, "sslkeylog.txt");
|
||||
|
||||
if (access(keylog_path, F_OK) == 0) {
|
||||
log_i("Use ushark for TLS decryption");
|
||||
|
||||
if (ushark_init(pd)) {
|
||||
pd->pcap.usk = ushark_new(PCAPD_DLT_RAW, "");
|
||||
ushark_set_pref("tls.keylog_file", keylog_path);
|
||||
}
|
||||
init_ushark_for_pcap(pd, keylog_path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -432,16 +432,16 @@ static void notifyBlacklistsLoaded(pcapdroid_t *pd, bl_status_arr_t *status_arr)
|
||||
|
||||
/* ******************************************************* */
|
||||
|
||||
static bool dumpPayloadChunk(struct pcapdroid *pd, const pkt_context_t *pctx, const char *dump_data, int dump_size) {
|
||||
static bool dumpPayloadChunk(struct pcapdroid *pd, pd_conn_t *conn, bool is_tx, uint64_t ms, uint32_t stream_id, const char *dump_data, int dump_size) {
|
||||
JNIEnv *env = pd->env;
|
||||
bool rv = false;
|
||||
|
||||
if(pctx->data->payload_chunks == NULL) {
|
||||
if(conn->payload_chunks == NULL) {
|
||||
// Directly allocating an ArrayList<bytes> rather than creating it afterwards saves us from a data copy.
|
||||
// However, this creates a local reference, which is retained until sendConnectionsDump is called.
|
||||
// NOTE: Android only allows up to 512 local references.
|
||||
pctx->data->payload_chunks = (*env)->NewObject(env, cls.arraylist, mids.arraylistNew);
|
||||
if((pctx->data->payload_chunks == NULL) || jniCheckException(env))
|
||||
conn->payload_chunks = (*env)->NewObject(env, cls.arraylist, mids.arraylistNew);
|
||||
if((conn->payload_chunks == NULL) || jniCheckException(env))
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -449,12 +449,13 @@ static bool dumpPayloadChunk(struct pcapdroid *pd, const pkt_context_t *pctx, co
|
||||
if(jniCheckException(env))
|
||||
return false;
|
||||
|
||||
jobject chunk_type = (pctx->data->l7proto == NDPI_PROTOCOL_HTTP) ? enums.chunktype_http : enums.chunktype_raw;
|
||||
jobject chunk_type = (conn->l7proto == NDPI_PROTOCOL_HTTP) ? enums.chunktype_http : enums.chunktype_raw;
|
||||
|
||||
jobject chunk = (*env)->NewObject(env, cls.payload_chunk, mids.payloadChunkInit, barray, chunk_type, pctx->is_tx, pctx->ms);
|
||||
jobject chunk = (*env)->NewObject(env, cls.payload_chunk, mids.payloadChunkInit, barray, chunk_type, is_tx, ms, stream_id);
|
||||
if(chunk && !jniCheckException(env)) {
|
||||
(*env)->SetByteArrayRegion(env, barray, 0, dump_size, (jbyte*) dump_data);
|
||||
rv = (*env)->CallBooleanMethod(env, pctx->data->payload_chunks, mids.arraylistAdd, chunk);
|
||||
if (dump_data) // can be NULL for RST reporting in HTTP/2
|
||||
(*env)->SetByteArrayRegion(env, barray, 0, dump_size, (jbyte*) dump_data);
|
||||
rv = (*env)->CallBooleanMethod(env, conn->payload_chunks, mids.arraylistAdd, chunk);
|
||||
}
|
||||
|
||||
//log_d("Dump chunk [size=%d]: %d", rv, dump_size);
|
||||
@@ -567,7 +568,7 @@ static void init_jni(JNIEnv *env) {
|
||||
mids.listGet = jniGetMethodID(env, cls.list, "get", "(I)Ljava/lang/Object;");
|
||||
mids.arraylistNew = jniGetMethodID(env, cls.arraylist, "<init>", "()V");
|
||||
mids.arraylistAdd = jniGetMethodID(env, cls.arraylist, "add", "(Ljava/lang/Object;)Z");
|
||||
mids.payloadChunkInit = jniGetMethodID(env, cls.payload_chunk, "<init>", "([BLcom/emanuelef/remote_capture/model/PayloadChunk$ChunkType;ZJ)V");
|
||||
mids.payloadChunkInit = jniGetMethodID(env, cls.payload_chunk, "<init>", "([BLcom/emanuelef/remote_capture/model/PayloadChunk$ChunkType;ZJI)V");
|
||||
|
||||
/* Fields */
|
||||
fields.bldescr_fname = jniFieldID(env, cls.blacklist_descriptor, "fname", "Ljava/lang/String;");
|
||||
|
||||
@@ -588,8 +588,9 @@ void pd_giveup_dpi(pcapdroid_t *pd, pd_conn_t *data, const zdtun_5tuple_t *tuple
|
||||
/* ******************************************************* */
|
||||
|
||||
// dumps the payload and returns true if fully dumped, false if failed or truncated
|
||||
static bool dump_payload(pcapdroid_t *pd, pkt_context_t *pctx, const char *to_dump, int dump_size) {
|
||||
pd_conn_t *data = pctx->data;
|
||||
static bool dump_payload(pcapdroid_t *pd, pd_conn_t *conn, bool is_tx, uint64_t ms, uint32_t stream_id,
|
||||
const char *to_dump, int dump_size)
|
||||
{
|
||||
bool truncated = false;
|
||||
|
||||
if((pd->payload_mode == PAYLOAD_MODE_MINIMAL) && (dump_size > MINIMAL_PAYLOAD_MAX_DIRECTION_SIZE)) {
|
||||
@@ -597,8 +598,8 @@ static bool dump_payload(pcapdroid_t *pd, pkt_context_t *pctx, const char *to_du
|
||||
truncated = true;
|
||||
}
|
||||
|
||||
if(pd->cb.dump_payload_chunk(pd, pctx, to_dump, dump_size))
|
||||
data->has_payload[pctx->is_tx] = true;
|
||||
if(pd->cb.dump_payload_chunk(pd, conn, is_tx, ms, stream_id, to_dump, dump_size))
|
||||
conn->has_payload[is_tx] = true;
|
||||
else
|
||||
truncated = true;
|
||||
|
||||
@@ -630,17 +631,18 @@ static void process_payload(pcapdroid_t *pd, pkt_context_t *pctx) {
|
||||
data->has_decrypted_data = true;
|
||||
}
|
||||
|
||||
unsigned int data_offset = 0;
|
||||
truncated = false;
|
||||
|
||||
for (unsigned int i = 0; i < pctx->plain_data->n_items; i++) {
|
||||
truncated |= !dump_payload(pd, pctx, (const char*) pctx->plain_data->data + data_offset,
|
||||
(int) pctx->plain_data->lengths[i]);
|
||||
const plain_data_item_t *item = &pctx->plain_data->items[i];
|
||||
|
||||
data_offset += pctx->plain_data->lengths[i];
|
||||
// use the item is_tx and ms timestamp data, rather than the ones from pctx because
|
||||
// http2.c may buffer http responses/resets so they may be processed with a different pctx
|
||||
truncated |= !dump_payload(pd, pctx->data, item->is_tx, item->ms, item->stream_id,
|
||||
(const char*) item->data, (int) item->data_length);
|
||||
}
|
||||
} else
|
||||
truncated = !dump_payload(pd, pctx, pkt->l7, pkt->l7_len);
|
||||
truncated = !dump_payload(pd, pctx->data, pctx->is_tx, pctx->ms, 0, pkt->l7, pkt->l7_len);
|
||||
|
||||
updated = true;
|
||||
} else
|
||||
|
||||
@@ -153,8 +153,14 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
unsigned char *data;
|
||||
unsigned int *lengths;
|
||||
unsigned int tot_length;
|
||||
unsigned int data_length;
|
||||
uint64_t ms;
|
||||
uint32_t stream_id;
|
||||
bool is_tx;
|
||||
} plain_data_item_t;
|
||||
|
||||
typedef struct {
|
||||
plain_data_item_t *items;
|
||||
unsigned int n_items;
|
||||
} plain_data_t;
|
||||
|
||||
@@ -184,7 +190,7 @@ typedef struct {
|
||||
void (*stop_pcap_dump)(struct pcapdroid *pd);
|
||||
void (*notify_service_status)(struct pcapdroid *pd, const char *status);
|
||||
void (*notify_blacklists_loaded)(struct pcapdroid *pd, bl_status_arr_t *status_arr);
|
||||
bool (*dump_payload_chunk)(struct pcapdroid *pd, const pkt_context_t *pctx, const char *dump_data, int dump_size);
|
||||
bool (*dump_payload_chunk)(struct pcapdroid *pd, pd_conn_t *conn, bool is_tx, uint64_t ms, uint32_t stream_id, const char *dump_data, int dump_size);
|
||||
void (*clear_payload_chunks)(struct pcapdroid *pd, const pkt_context_t *pctx);
|
||||
} pd_callbacks_t;
|
||||
|
||||
@@ -214,6 +220,7 @@ typedef struct pcapdroid {
|
||||
jint mitm_addon_uid;
|
||||
bool vpn_capture;
|
||||
bool pcap_file_capture;
|
||||
const char *keylog_path_override; // For tests: override sslkeylog.txt location
|
||||
payload_mode_t payload_mode;
|
||||
|
||||
// stats
|
||||
|
||||
@@ -11,9 +11,9 @@ static void (*sk_init)();
|
||||
static void (*sk_cleanup)();
|
||||
static ushark_t* (*sk_new)(int, const char *);
|
||||
static void (*sk_set_pref)(const char *, const char *);
|
||||
static void (*sk_set_callbacks)(ushark_t *, const ushark_data_callbacks_t *);
|
||||
static void (*sk_destroy)(ushark_t*);
|
||||
static void (*sk_dissect_tls)(ushark_t*, const unsigned char *, const struct pcap_pkthdr *,
|
||||
ushark_tls_data_callback);
|
||||
static const char* (*sk_dissect)(ushark_t*, const unsigned char *, const struct pcap_pkthdr *);
|
||||
|
||||
bool ushark_init(pcapdroid_t *pd) {
|
||||
assert(!sk_dll);
|
||||
@@ -44,10 +44,11 @@ bool ushark_init(pcapdroid_t *pd) {
|
||||
sk_cleanup = dlsym(sk_dll, "ushark_cleanup");
|
||||
sk_new = dlsym(sk_dll, "ushark_new");
|
||||
sk_set_pref = dlsym(sk_dll, "ushark_set_pref");
|
||||
sk_set_callbacks = dlsym(sk_dll, "ushark_set_callbacks");
|
||||
sk_destroy = dlsym(sk_dll, "ushark_destroy");
|
||||
sk_dissect_tls = dlsym(sk_dll, "ushark_dissect_tls");
|
||||
sk_dissect = dlsym(sk_dll, "ushark_dissect");
|
||||
|
||||
if (!sk_init || !sk_cleanup || !sk_new || !sk_set_pref || !sk_destroy || !sk_dissect_tls) {
|
||||
if (!sk_init || !sk_cleanup || !sk_new || !sk_set_pref || !sk_set_callbacks || !sk_destroy || !sk_dissect) {
|
||||
dlclose(sk_dll);
|
||||
sk_dll = NULL;
|
||||
log_e("libushark.so misses some required symbols");
|
||||
@@ -60,14 +61,16 @@ bool ushark_init(pcapdroid_t *pd) {
|
||||
|
||||
void ushark_cleanup() {
|
||||
assert(sk_dll);
|
||||
|
||||
sk_cleanup();
|
||||
|
||||
sk_init = NULL;
|
||||
sk_cleanup = NULL;
|
||||
sk_new = NULL;
|
||||
sk_set_pref = NULL;
|
||||
sk_set_callbacks = NULL;
|
||||
sk_destroy = NULL;
|
||||
sk_dissect_tls = NULL;
|
||||
sk_dissect = NULL;
|
||||
|
||||
// deallocates the static variables in wireshark, necessary to run cleanly again
|
||||
dlclose(sk_dll);
|
||||
@@ -76,20 +79,30 @@ void ushark_cleanup() {
|
||||
|
||||
ushark_t* ushark_new(int pcap_encap, const char *dfilter) {
|
||||
assert(sk_new);
|
||||
|
||||
return sk_new(pcap_encap, dfilter);
|
||||
}
|
||||
|
||||
void ushark_destroy(ushark_t *sk) {
|
||||
assert(sk_destroy);
|
||||
return sk_destroy(sk);
|
||||
|
||||
sk_destroy(sk);
|
||||
}
|
||||
|
||||
void ushark_set_pref(const char *name, const char *val) {
|
||||
assert(sk_set_pref);
|
||||
return sk_set_pref(name, val);
|
||||
|
||||
sk_set_pref(name, val);
|
||||
}
|
||||
|
||||
void ushark_dissect_tls(ushark_t *sk, const unsigned char *buf, const struct pcap_pkthdr *hdr, ushark_tls_data_callback cb) {
|
||||
assert(sk_dissect_tls);
|
||||
return sk_dissect_tls(sk, buf, hdr, cb);
|
||||
}
|
||||
void ushark_set_callbacks(ushark_t *sk, const ushark_data_callbacks_t *cbs) {
|
||||
assert(sk_set_callbacks);
|
||||
|
||||
sk_set_callbacks(sk, cbs);
|
||||
}
|
||||
|
||||
const char* ushark_dissect(ushark_t *sk, const unsigned char *buf, const struct pcap_pkthdr *hdr) {
|
||||
assert(sk_dissect);
|
||||
|
||||
return sk_dissect(sk, buf, hdr);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,14 @@ ushark_t* ushark_new(int pcap_encap, const char *dfilter);
|
||||
void ushark_destroy(ushark_t *sk);
|
||||
void ushark_set_pref(const char *name, const char *val);
|
||||
|
||||
typedef void (*ushark_tls_data_callback)(const unsigned char *plain_data, unsigned int data_len);
|
||||
void ushark_dissect_tls(ushark_t *sk, const unsigned char *buf, const struct pcap_pkthdr *hdr, ushark_tls_data_callback cb);
|
||||
typedef struct {
|
||||
void (*on_http1_data)(uint32_t conversation_id, const unsigned char *plain_data, size_t data_len);
|
||||
void (*on_http2_request)(uint32_t conversation_id, uint32_t stream_id, const unsigned char *plain_data, size_t data_len);
|
||||
void (*on_http2_response)(uint32_t conversation_id, uint32_t stream_id, const unsigned char *plain_data, size_t data_len);
|
||||
void (*on_http2_reset)(uint32_t conversation_id, uint32_t stream_id);
|
||||
} ushark_data_callbacks_t;
|
||||
void ushark_set_callbacks(ushark_t *sk, const ushark_data_callbacks_t *cbs);
|
||||
|
||||
const char* ushark_dissect(ushark_t *sk, const unsigned char *buf, const struct pcap_pkthdr *hdr);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -6,7 +6,7 @@ clean:
|
||||
run_tests:
|
||||
mkdir -p build/test
|
||||
cd build/test && \
|
||||
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_C_FLAGS="-fsanitize=address,leak -DHAVE_GNU_STRERROR_R -fno-omit-frame-pointer -O1" ../.. && \
|
||||
cmake -DCMAKE_C_COMPILER=clang -DCMAKE_C_FLAGS="-fsanitize=address,leak -DHAVE_GNU_STRERROR_R -fno-omit-frame-pointer -g -O0" ../.. && \
|
||||
$(MAKE) run_tests
|
||||
|
||||
fuzz:
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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
|
||||
* Copyright 2022-26 - Emanuele Faranda
|
||||
*/
|
||||
|
||||
#include "test_utils.h"
|
||||
@@ -37,8 +37,15 @@ static void free_payload_chunks(pcapdroid_t *pd);
|
||||
|
||||
/* ******************************************************* */
|
||||
|
||||
static void getPcapdPath(struct pcapdroid *pd, const char *prog_name, char *buf, int bufsize) {
|
||||
snprintf(buf, bufsize, "../main/pcapd/libpcapd.so");
|
||||
static void getNativeLibPath(struct pcapdroid *pd, const char *prog_name, char *buf, int bufsize) {
|
||||
if (strcmp(prog_name, "pcapd") == 0)
|
||||
snprintf(buf, bufsize, "../main/pcapd/libpcapd.so");
|
||||
else {
|
||||
fprintf(stderr, "Unknown native library: %s", prog_name);
|
||||
|
||||
if (bufsize > 0)
|
||||
buf[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/* ******************************************************* */
|
||||
@@ -89,7 +96,7 @@ pcapdroid_t* pd_init_test(const char *ifname) {
|
||||
pd->pcap_file_capture = true;
|
||||
pd->pcap.capture_interface = (char*) ifname;
|
||||
pd->pcap.as_root = false; // don't run as root
|
||||
pd->cb.get_libprog_path = getPcapdPath;
|
||||
pd->cb.get_libprog_path = getNativeLibPath;
|
||||
pd->payload_mode = PAYLOAD_MODE_FULL;
|
||||
|
||||
strcpy(pd->cachedir, ".");
|
||||
@@ -215,8 +222,8 @@ u_char* next_pcap_record(pcap_rec_t *rec) {
|
||||
/* ******************************************************* */
|
||||
|
||||
/* Dumps all the payload chunks into a linked list. The linked list is accessible via
|
||||
* (payload_chunk_t*)data->payload_chunks */
|
||||
bool dump_cb_payload_chunk(pcapdroid_t *pd, const pkt_context_t *pctx, const char *dump_data, int dump_size) {
|
||||
* (payload_chunk_t*)conn->payload_chunks */
|
||||
bool dump_cb_payload_chunk(pcapdroid_t *pd, pd_conn_t *conn, bool is_tx, uint64_t ms, uint32_t stream_id, const char *dump_data, int dump_size) {
|
||||
payload_chunk_t *chunk = calloc(1, sizeof(payload_chunk_t));
|
||||
assert(chunk != NULL);
|
||||
chunk->payload = (u_char*)malloc(dump_size);
|
||||
@@ -224,10 +231,10 @@ bool dump_cb_payload_chunk(pcapdroid_t *pd, const pkt_context_t *pctx, const cha
|
||||
|
||||
memcpy(chunk->payload, dump_data, dump_size);
|
||||
chunk->size = dump_size;
|
||||
chunk->is_tx = pctx->is_tx;
|
||||
chunk->is_tx = is_tx;
|
||||
|
||||
// append to the linked list
|
||||
payload_chunk_t *last = (payload_chunk_t*)pctx->data->payload_chunks;
|
||||
payload_chunk_t *last = (payload_chunk_t*) conn->payload_chunks;
|
||||
if(last) {
|
||||
while(last->next)
|
||||
last = last->next;
|
||||
@@ -237,7 +244,7 @@ bool dump_cb_payload_chunk(pcapdroid_t *pd, const pkt_context_t *pctx, const cha
|
||||
num_chunks_lists++;
|
||||
chunks_lists_heads = realloc(chunks_lists_heads, num_chunks_lists * sizeof(void*));
|
||||
chunks_lists_heads[num_chunks_lists - 1] = chunk;
|
||||
pctx->data->payload_chunks = chunk;
|
||||
conn->payload_chunks = chunk;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* 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
|
||||
* Copyright 2022-26 - Emanuele Faranda
|
||||
*/
|
||||
|
||||
#ifndef __TEST_UTILS_H__
|
||||
@@ -52,7 +52,7 @@ void assert_pcap_header(pcap_hdr_t *hdr);
|
||||
u_char* next_pcap_record(pcap_rec_t *rec);
|
||||
|
||||
// Callbacks
|
||||
bool dump_cb_payload_chunk(pcapdroid_t *pd, const pkt_context_t *pctx, const char *dump_data, int dump_size);
|
||||
bool dump_cb_payload_chunk(pcapdroid_t *pd, pd_conn_t *conn, bool is_tx, uint64_t ms, uint32_t stream_id, const char *dump_data, int dump_size);
|
||||
|
||||
conn_and_tuple_t* assert_conn(pcapdroid_t *pd, int ipproto, const char *dst_ip, uint16_t dst_port, const char *info);
|
||||
|
||||
|
||||
Submodule submodules/PCAPdroid-ushark-bin updated: 905499ae27...996a45cdc9
+1
-1
Submodule submodules/nDPI updated: ce606bf28a...2083215659
Reference in New Issue
Block a user