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:
emanuele-f
2025-12-29 15:05:36 +01:00
parent 48044e7dc9
commit 39b4456aba
19 changed files with 285 additions and 104 deletions
+1 -1
View File
@@ -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));
}
}
+1
View File
@@ -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);
+100 -33
View File
@@ -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);
}
}
+10 -9
View File
@@ -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;");
+11 -9
View File
@@ -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
+10 -3
View File
@@ -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
+24 -11
View File
@@ -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);
}
+9 -2
View File
@@ -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
+1 -1
View File
@@ -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:
+16 -9
View File
@@ -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;
+2 -2
View File
@@ -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);