diff --git a/.gitmodules b/.gitmodules index fac2d962c..b5519bdfe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,6 @@ [submodule "submodules/MaxMind-DB-Reader-java"] path = submodules/MaxMind-DB-Reader-java url = https://github.com/emanuele-f/MaxMind-DB-Reader-java +[submodule "submodules/PCAPdroid-ushark-bin"] + path = submodules/PCAPdroid-ushark-bin + url = https://github.com/emanuele-f/PCAPdroid-ushark-bin diff --git a/app/build.gradle b/app/build.gradle index 95808e8f0..498218f8e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -59,6 +59,7 @@ android { sourceSets { main.java.srcDirs += '../submodules/MaxMind-DB-Reader-java/src/main/java' + main.jniLibs.srcDirs += '../submodules/PCAPdroid-ushark-bin/release' } testOptions { diff --git a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java index fb40a613f..e86ff0f5b 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java +++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java @@ -1624,4 +1624,5 @@ public static boolean hasError() { public static native List getL7Protocols(); public static native void dumpMasterSecret(byte[] secret); public static native boolean hasSeenPcapdroidTrailer(); + public static native boolean extractKeylogFromPcapng(String pcapng_path, String out_path); } diff --git a/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java b/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java index 40ce5e36e..795146d75 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java +++ b/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java @@ -60,6 +60,7 @@ public class PCAPdroid extends Application { private Blacklists mBlacklists; private CtrlPermissions mCtrlPermissions; private Context mLocalizedContext; + private boolean mIsDecryptingPcap = false; private static WeakReference mInstance; protected static boolean isUnderTest = false; @@ -250,4 +251,12 @@ public CtrlPermissions getCtrlPermissions() { mCtrlPermissions = new CtrlPermissions(this); return mCtrlPermissions; } + + public void setIsDecryptingPcap(boolean val) { + mIsDecryptingPcap = val; + } + + public boolean isDecryptingPcap() { + return mIsDecryptingPcap; + } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java b/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java index 723ea7e4c..65dc8da41 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/CaptureCtrl.java @@ -251,6 +251,8 @@ private void processRequest(Intent req_intent, @NonNull String action) { return; } + PCAPdroid.getInstance().setIsDecryptingPcap(false); + if(!settings.pcap_uri.isEmpty()) { persistableUriPermission.checkPermission(settings.pcap_uri, settings.pcapng_format, granted_uri -> { Log.d(TAG, "persistable uri granted? " + granted_uri); diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/EditFilterActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/EditFilterActivity.java index 204d1f017..3071aaeb1 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/EditFilterActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/EditFilterActivity.java @@ -37,6 +37,7 @@ import com.emanuelef.remote_capture.Billing; import com.emanuelef.remote_capture.CaptureService; import com.emanuelef.remote_capture.ConnectionsRegister; +import com.emanuelef.remote_capture.PCAPdroid; import com.emanuelef.remote_capture.R; import com.emanuelef.remote_capture.Utils; import com.emanuelef.remote_capture.model.ConnectionDescriptor.Status; @@ -114,7 +115,13 @@ protected void onCreate(Bundle savedInstanceState) { new Pair<>(DecryptionStatus.ERROR, findViewById(R.id.dec_status_error)) )); - if(CaptureService.isDecryptingTLS()) { + if (PCAPdroid.getInstance().isDecryptingPcap()) { + // unable to show the following statuses + findViewById(R.id.dec_status_not_decryptable).setVisibility(View.GONE); + findViewById(R.id.dec_status_error).setVisibility(View.GONE); + } + + if(CaptureService.isDecryptingTLS() || PCAPdroid.getInstance().isDecryptingPcap()) { findViewById(R.id.decryption_status_label).setVisibility(View.VISIBLE); findViewById(R.id.decryption_status_group).setVisibility(View.VISIBLE); mOnlyCleartext.setVisibility(View.GONE); diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java index 080d8410f..a1add151d 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/MainActivity.java @@ -101,12 +101,16 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig private NavigationView mNavView; private CaptureHelper mCapHelper; private AlertDialog mPcapLoadDialog; + private Uri mPcapUri; + private ExecutorService mPcapExecutor; // helps detecting duplicate state reporting of STOPPED in MutableLiveData private boolean mWasStarted = false; private boolean mStartPressed = false; private boolean mDecEmptyRulesNoticeShown = false; private boolean mTrailerNoticeShown = false; + private boolean mOpenPcapDecrypt = false; + private boolean mDecryptPcap = false; private static final String TAG = "Main"; @@ -135,6 +139,8 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig registerForActivityResult(new StartActivityForResult(), this::peerInfoResult); private final ActivityResultLauncher pcapFileOpenLauncher = registerForActivityResult(new StartActivityForResult(), this::pcapFileOpenResult); + private final ActivityResultLauncher keylogFileOpenLauncher = + registerForActivityResult(new StartActivityForResult(), this::keylogFileOpenResult); @Override protected void onCreate(Bundle savedInstanceState) { @@ -608,12 +614,19 @@ private void checkDecryptionRulesNotice() { } } - private void checkLoadedPcap() { + private void dismissPcapLoadDialog() { if(mPcapLoadDialog != null) { mPcapLoadDialog.dismiss(); mPcapLoadDialog = null; } + mPcapExecutor = null; + mPcapUri = null; + } + + private void checkLoadedPcap() { + dismissPcapLoadDialog(); + if(!CaptureService.hasError()) { // pcap file loaded successfully ConnectionsRegister reg = CaptureService.getConnsRegister(); @@ -686,7 +699,10 @@ public boolean onOptionsItemSelected(@NonNull MenuItem item) { stopCapture(); return true; } else if(id == R.id.open_pcap) { - startOpenPcapFile(); + selectOpenPcapFile(false); + return true; + } else if(id == R.id.decrypt_pcap) { + selectOpenPcapFile(true); return true; } else if (id == R.id.action_settings) { Intent intent = new Intent(MainActivity.this, SettingsActivity.class); @@ -715,6 +731,9 @@ private void initAppState() { private void doStartCaptureService(String input_pcap_path) { appStateStarting(); + PCAPdroid.getInstance().setIsDecryptingPcap(mDecryptPcap); + mDecryptPcap = false; + CaptureSettings settings = new CaptureSettings(this, mPrefs); settings.input_pcap_path = input_pcap_path; mCapHelper.startCapture(settings); @@ -881,78 +900,165 @@ private void sslkeyfileExportResult(final ActivityResult result) { } } - private void startOpenPcapFile() { + private void selectOpenPcapFile(boolean decrypt) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); - Log.d(TAG, "startOpenPcapFile: launching dialog"); + Log.d(TAG, "selectOpenPcapFile: launching dialog"); + mOpenPcapDecrypt = decrypt; + if (mOpenPcapDecrypt) + Utils.showToast(this, R.string.select_the_pcap_file); Utils.launchFileDialog(this, intent, pcapFileOpenLauncher); } private void pcapFileOpenResult(final ActivityResult result) { - if((result.getResultCode() == RESULT_OK) && (result.getData() != null)) { + if ((result.getResultCode() == RESULT_OK) && (result.getData() != null)) { Uri uri = result.getData().getData(); - if(uri == null) + if (uri == null) return; Log.d(TAG, "pcapFileOpenResult: " + uri); - ExecutorService executor = Executors.newSingleThreadExecutor(); + if (mOpenPcapDecrypt && + (!mIab.isPurchased(Billing.PCAPNG_SKU) || !uri.toString().endsWith(".pcapng")) + ) { + // Ask to select the keylog + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + + Log.d(TAG, "pcapFileOpenResult: launching dialog"); + mPcapUri = uri; + Utils.showToast(this, R.string.select_the_keylog_file); + Utils.launchFileDialog(this, intent, keylogFileOpenLauncher); + } else + startOpenPcap(uri, null); + } + } + + private void keylogFileOpenResult(final ActivityResult result) { + if ((result.getResultCode() == RESULT_OK) && (result.getData() != null)) { + Uri uri = result.getData().getData(); + if (uri == null) + return; + + Log.d(TAG, "keylogFileOpenResult: " + uri); + startOpenPcap(mPcapUri, uri); + } + } - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.loading); - builder.setMessage(R.string.pcap_load_in_progress); + private void startOpenPcap(Uri pcap_uri, Uri keylog_uri) { + mPcapExecutor = Executors.newSingleThreadExecutor(); - mPcapLoadDialog = builder.create(); - mPcapLoadDialog.setCanceledOnTouchOutside(false); - mPcapLoadDialog.show(); + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.loading); + builder.setMessage(R.string.pcap_load_in_progress); - mPcapLoadDialog.setOnCancelListener(dialogInterface -> { - Log.i(TAG, "Abort PCAP loading"); - executor.shutdownNow(); + mPcapLoadDialog = builder.create(); + mPcapLoadDialog.setCanceledOnTouchOutside(false); + mPcapLoadDialog.setOnCancelListener(dialogInterface -> { + Log.i(TAG, "Abort PCAP loading"); - if (CaptureService.isServiceActive()) - CaptureService.stopService(); + if (mPcapExecutor != null) { + mPcapExecutor.shutdownNow(); + mPcapExecutor = null; + } - Utils.showToastLong(this, R.string.pcap_file_load_aborted); + if (CaptureService.isServiceActive()) + CaptureService.stopService(); + + Utils.showToastLong(this, R.string.pcap_file_load_aborted); + }); + mPcapLoadDialog.setOnDismissListener(dialog -> mPcapLoadDialog = null); + mPcapLoadDialog.show(); + + // get an actual file path which can be read from the native side + String path = Utils.uriToFilePath(this, pcap_uri); + if((path == null) || !Utils.isReadable(path)) { + // Unable to get a direct file path (e.g. for files in Downloads). Copy file to the + // cache directory + File out = getTmpPcapPath(); + out.deleteOnExit(); + String abs_path = out.getAbsolutePath(); + + // PCAP file can be big, copy in a different thread + mPcapExecutor.execute(() -> { + try (InputStream in_stream = getContentResolver().openInputStream(pcap_uri)) { + Utils.copy(in_stream, out); + } catch (IOException | SecurityException e) { + e.printStackTrace(); + + runOnUiThread(() -> { + Utils.showToastLong(this, R.string.copy_error); + dismissPcapLoadDialog(); + }); + return; + } + + runOnUiThread(() -> continueOpenPcap(abs_path, keylog_uri)); }); - mPcapLoadDialog.setOnDismissListener(dialog -> mPcapLoadDialog = null); - - String path = Utils.uriToFilePath(this, uri); - if((path == null) || !Utils.isReadable(path)) { - // Unable to get a direct file path (e.g. for files in Downloads). Copy file to the - // cache directory - File out = getTmpPcapPath(); - out.deleteOnExit(); - String abs_path = out.getAbsolutePath(); - - // PCAP file can be big, copy in a different thread - executor.execute(() -> { - try (InputStream in_stream = getContentResolver().openInputStream(uri)) { - Utils.copy(in_stream, out); - } catch (IOException | SecurityException e) { - e.printStackTrace(); - - runOnUiThread(() -> { - Utils.showToastLong(this, R.string.copy_error); - if(mPcapLoadDialog != null) { - mPcapLoadDialog.dismiss(); - mPcapLoadDialog = null; - } - }); - return; - } + } else { + Log.d(TAG, "pcapFileOpenResult: path: " + path); + continueOpenPcap(path, keylog_uri); + } + } - runOnUiThread(() -> doStartCaptureService(abs_path)); + private void continueOpenPcap(String pcap_path, Uri keylog_uri) { + //noinspection ResultOfMethodCallIgnored + getKeylogPath().delete(); + + if (mOpenPcapDecrypt) + loadKeylogfile(pcap_path, keylog_uri); + else + doStartCaptureService(pcap_path); + } + + private void loadKeylogfile(String pcap_path, Uri keylog_uri) { + mPcapExecutor.execute(() -> { + File out = getKeylogPath(); + out.deleteOnExit(); + + if (keylog_uri != null) { + // keylog is in a separate file + try (InputStream in_stream = getContentResolver().openInputStream(keylog_uri)) { + Utils.copy(in_stream, out); + } catch (IOException | SecurityException e) { + e.printStackTrace(); + + runOnUiThread(() -> { + Utils.showToastLong(this, R.string.keylog_read_error); + dismissPcapLoadDialog(); + }); + return; + } + + runOnUiThread(() -> { + mDecryptPcap = true; + doStartCaptureService(pcap_path); }); } else { - Log.d(TAG, "pcapFileOpenResult: path: " + path); - doStartCaptureService(path); + // keylog is from PCAPNG + boolean success = CaptureService.extractKeylogFromPcapng(pcap_path, out.getAbsolutePath()); + + runOnUiThread(() -> { + if (success && out.exists()) { + mDecryptPcap = true; + doStartCaptureService(pcap_path); + } else { + Utils.showToastLong(this, R.string.keylog_read_error); + dismissPcapLoadDialog(); + } + }); } - } + }); } private File getTmpPcapPath() { return new File(getCacheDir() + "/tmp.pcap"); } + + private File getKeylogPath() { + // NOTE: keep in sync with run_libpcap + return new File(getCacheDir() + "/sslkeylog.txt"); + } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/adapters/ConnectionsAdapter.java b/app/src/main/java/com/emanuelef/remote_capture/adapters/ConnectionsAdapter.java index bb724e75b..938b703bf 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/adapters/ConnectionsAdapter.java +++ b/app/src/main/java/com/emanuelef/remote_capture/adapters/ConnectionsAdapter.java @@ -164,7 +164,7 @@ else if((conn.status == ConnectionDescriptor.CONN_STATUS_CLOSED) blockedInd.setVisibility(conn.is_blocked ? View.VISIBLE : View.GONE); redirectedInd.setVisibility((conn.isPortMappingApplied() && !conn.is_blocked) ? View.VISIBLE : View.GONE); - if(CaptureService.isDecryptingTLS()) { + if(CaptureService.isDecryptingTLS() || PCAPdroid.getInstance().isDecryptingPcap()) { decryptionInd.setVisibility(View.VISIBLE); Utils.setDecryptionIcon(decryptionInd, conn); } else diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java index 6360d2726..ccff84e8c 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/StatusFragment.java @@ -69,6 +69,7 @@ public class StatusFragment extends Fragment implements AppStateListener, MenuPr private MenuItem mStartBtn; private MenuItem mStopBtn; private MenuItem mOpenPcap; + private MenuItem mDecryptPcap; private ImageView mFilterIcon; private MenuItem mMenuSettings; private TextView mInterfaceInfo; @@ -168,6 +169,7 @@ public void onCreateMenu(@NonNull Menu menu, MenuInflater menuInflater) { mStopBtn = mMenu.findItem(R.id.action_stop); mMenuSettings = mMenu.findItem(R.id.action_settings); mOpenPcap = mMenu.findItem(R.id.open_pcap); + mDecryptPcap = mMenu.findItem(R.id.decrypt_pcap); refreshStatus(); } @@ -315,12 +317,14 @@ public void appStateChanged(AppState state) { mStopBtn.setVisible(!CaptureService.isAlwaysOnVPN()); mMenuSettings.setEnabled(false); mOpenPcap.setEnabled(false); + mDecryptPcap.setEnabled(false); } else { // ready || starting mStopBtn.setVisible(false); mStartBtn.setEnabled(true); mStartBtn.setVisible(!CaptureService.isAlwaysOnVPN()); mMenuSettings.setEnabled(true); mOpenPcap.setEnabled(true); + mDecryptPcap.setEnabled(true); } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/ConnectionDescriptor.java b/app/src/main/java/com/emanuelef/remote_capture/model/ConnectionDescriptor.java index a5d859d0f..870601161 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/ConnectionDescriptor.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/ConnectionDescriptor.java @@ -27,6 +27,7 @@ import com.emanuelef.remote_capture.AppsResolver; import com.emanuelef.remote_capture.CaptureService; import com.emanuelef.remote_capture.HTTPReassembly; +import com.emanuelef.remote_capture.PCAPdroid; import com.emanuelef.remote_capture.R; import java.net.InetAddress; @@ -106,6 +107,7 @@ public enum FilteringStatus { public final int ifidx; public final int incr_id; private final boolean mitm_decrypt; // true if the connection is under mitm for TLS decryption + private boolean internal_decrypt; public int status; private int tcp_flags; private boolean blacklisted_ip; @@ -145,6 +147,7 @@ public ConnectionDescriptor(int _incr_id, int _ipver, int _ipproto, String _src_ asn = new Geomodel.ASN(); payload_chunks = new ArrayList<>(); mitm_decrypt = _mitm_decrypt; + internal_decrypt = false; } public void processUpdate(ConnectionUpdate update) { @@ -181,7 +184,7 @@ public void processUpdate(ConnectionUpdate update) { } if((update.update_type & ConnectionUpdate.UPDATE_PAYLOAD) != 0) { // Payload for decryptable connections should be received via the MitmReceiver - assert(decryption_ignored || isNotDecryptable()); + assert(decryption_ignored || isNotDecryptable() || PCAPdroid.getInstance().isDecryptingPcap()); // Some pending updates with payload may still be received after low memory has been // triggered and payload disabled @@ -190,6 +193,7 @@ public void processUpdate(ConnectionUpdate update) { if(update.payload_chunks != null) payload_chunks.addAll(update.payload_chunks); payload_truncated = update.payload_truncated; + internal_decrypt = update.payload_decrypted; } } } @@ -257,7 +261,7 @@ else if(decryption_error != null) return DecryptionStatus.ERROR; else if(isNotDecryptable()) return DecryptionStatus.NOT_DECRYPTABLE; - else if(decryption_ignored) + else if(decryption_ignored || (PCAPdroid.getInstance().isDecryptingPcap() && !internal_decrypt)) return DecryptionStatus.ENCRYPTED; else if(isDecrypted()) return DecryptionStatus.DECRYPTED; @@ -307,8 +311,8 @@ public void setPayloadTruncatedByAddon() { public boolean isPayloadTruncated() { return payload_truncated; } public boolean isPortMappingApplied() { return port_mapping_applied; } - public boolean isNotDecryptable() { return !decryption_ignored && (encrypted_payload || !mitm_decrypt); } - public boolean isDecrypted() { return !decryption_ignored && !isNotDecryptable() && (getNumPayloadChunks() > 0); } + public boolean isNotDecryptable() { return !decryption_ignored && (encrypted_payload || !mitm_decrypt) && !PCAPdroid.getInstance().isDecryptingPcap(); } + public boolean isDecrypted() { return !decryption_ignored && !isNotDecryptable() && (mitm_decrypt || internal_decrypt) && (getNumPayloadChunks() > 0); } public boolean isCleartext() { return !encrypted_payload && !encrypted_l7; } public synchronized int getNumPayloadChunks() { return payload_chunks.size(); } @@ -346,8 +350,9 @@ private synchronized String getHttp(boolean is_sent) { // Need to wrap the String to set it from the lambda final AtomicReference rv = new AtomicReference<>(); - HTTPReassembly reassembly = new HTTPReassembly(CaptureService.getCurPayloadMode() == Prefs.PayloadMode.FULL, chunk -> - rv.set(new String(chunk.payload, StandardCharsets.UTF_8))); + HTTPReassembly reassembly = new HTTPReassembly(CaptureService.getCurPayloadMode() == Prefs.PayloadMode.FULL, + chunk -> rv.set(new String(chunk.payload, StandardCharsets.UTF_8)) + ); // Possibly reassemble/decode the request for(PayloadChunk chunk: payload_chunks) { diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/ConnectionUpdate.java b/app/src/main/java/com/emanuelef/remote_capture/model/ConnectionUpdate.java index c435fd7f2..a0722f0e1 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/ConnectionUpdate.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/ConnectionUpdate.java @@ -49,6 +49,7 @@ public class ConnectionUpdate { /* set if update_type & UPDATE_PAYLOAD */ public ArrayList payload_chunks; public boolean payload_truncated; + public boolean payload_decrypted; public ConnectionUpdate(int _incr_id) { incr_id = _incr_id; @@ -79,10 +80,11 @@ public void setInfo(String _info, String _url, String _l7proto, int flags) { info_flags = flags; } - public void setPayload(ArrayList _chunks, boolean _payload_truncated) { + public void setPayload(ArrayList _chunks, int flags) { update_type |= UPDATE_PAYLOAD; payload_chunks = _chunks; - payload_truncated = _payload_truncated; + payload_truncated = (flags & 0x1) != 0; + payload_decrypted = (flags & 0x2) != 0; } } diff --git a/app/src/main/jni/core/CMakeLists.txt b/app/src/main/jni/core/CMakeLists.txt index cfb9a1db7..78ec84ef3 100644 --- a/app/src/main/jni/core/CMakeLists.txt +++ b/app/src/main/jni/core/CMakeLists.txt @@ -11,6 +11,7 @@ add_library(capture SHARED pcap_dump.c log_writer.c port_map.c + ushark_dll.c jni_impl.c) # nDPI @@ -26,7 +27,6 @@ configure_file(${NDPI_GEN}/ndpi_define.h ${NDPI_ROOT}/src/include/ndpi_define.h) ADD_LIBRARY(ndpi SHARED ${ndpiSources} ${NDPI_ROOT}/src/include/ndpi_api.h) - if(ANDROID) find_library(log-lib log) set(extra-libs ${log-lib}) diff --git a/app/src/main/jni/core/capture_libpcap.c b/app/src/main/jni/core/capture_libpcap.c index 2aa89e79f..bcc366b29 100644 --- a/app/src/main/jni/core/capture_libpcap.c +++ b/app/src/main/jni/core/capture_libpcap.c @@ -25,6 +25,8 @@ #include "pcapd/pcapd.h" #include "common/utils.h" #include "third_party/uthash.h" +#include "pcap_dump.h" +#include "ushark_dll.h" #ifdef FUZZING extern int openPcap(pcapdroid_t *pd); @@ -407,6 +409,23 @@ static int get_ip_offset(int linktype) { /* ******************************************************* */ +static plain_data_t g_plain_data = {}; + +static void handle_tls_data(const unsigned char *plain_data, unsigned int data_len) { + if (data_len == 0) + return; + + //log_w(" %s", plain_data); + g_plain_data.data = (unsigned char *) pd_realloc(g_plain_data.data, g_plain_data.data_len + data_len); + if (!g_plain_data.data) { + log_e("realloc(tls_data) failed[%d]: %s", errno, strerror(errno)); + g_plain_data.data_len = 0; + } else { + memcpy(g_plain_data.data + g_plain_data.data_len, plain_data, data_len); + g_plain_data.data_len += data_len; + } +} + /* 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) { @@ -519,12 +538,34 @@ static bool handle_packet(pcapdroid_t *pd, pcapd_hdr_t *hdr, const char *buffer, struct timeval tv = hdr->ts; pkt_context_t pinfo; - pd_process_packet(pd, &pkt, is_tx, &conn_tuple, conn->data, &tv, &pinfo); + pd_init_pkt_context(&pinfo, &pkt, is_tx, &conn_tuple, conn->data, &tv); + + 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, + (const unsigned char*) pkt.l3, + &pcap_hdr, handle_tls_data); + + if (g_plain_data.data) + pinfo.plain_data = &g_plain_data; + } + + pd_process_packet(pd, &pinfo); // NOTE: this may free the conn update_connection_status(pd, conn, &pkt, !is_tx); pd_account_stats(pd, &pinfo); + + if (g_plain_data.data) { + pd_free(g_plain_data.data); + g_plain_data.data = NULL; + g_plain_data.data_len = 0; + } + return true; } @@ -620,6 +661,20 @@ int run_libpcap(pcapdroid_t *pd) { u_int64_t next_purge_ms; zdtun_callbacks_t callbacks = {.send_client = (void*)1}; + if (pd->pcap_file_capture) { + // check if the SSL keylog exists + const char *keylog_path = 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); + } + } + } + #if ANDROID char capture_interface[PATH_MAX] = "@inet"; char bpf[256]; @@ -788,5 +843,12 @@ int run_libpcap(pcapdroid_t *pd) { } #endif + if (pd->pcap.usk) { + ushark_destroy(pd->pcap.usk); + pd->pcap.usk = NULL; + + ushark_cleanup(); + } + return rv; } diff --git a/app/src/main/jni/core/capture_vpn.c b/app/src/main/jni/core/capture_vpn.c index 0e681eafb..8743cb41e 100644 --- a/app/src/main/jni/core/capture_vpn.c +++ b/app/src/main/jni/core/capture_vpn.c @@ -97,7 +97,8 @@ static int remote2vpn(zdtun_t *zdt, zdtun_pkt_t *pkt, const zdtun_conn_t *conn_i pkt_context_t pctx; pd_refresh_time(pd); - pd_process_packet(pd, pkt, false, tuple, data, get_pkt_timestamp(pd, &tv), &pctx); + pd_init_pkt_context(&pctx, pkt, false, tuple, data, get_pkt_timestamp(pd, &tv)); + pd_process_packet(pd, &pctx); if(data->to_block) { data->blocked_pkts++; data->update_type |= CONN_UPDATE_STATS; @@ -590,7 +591,8 @@ int run_vpn(pcapdroid_t *pd) { } } - pd_process_packet(pd, &pkt, true, tuple, data, get_pkt_timestamp(pd, &tv), &pctx); + pd_init_pkt_context(&pctx, &pkt, true, tuple, data, get_pkt_timestamp(pd, &tv)); + pd_process_packet(pd, &pctx); if(data->sent_pkts == 0) { // Newly created connections if (!data->port_mapping_applied) diff --git a/app/src/main/jni/core/jni_impl.c b/app/src/main/jni/core/jni_impl.c index 262c15079..1cc91ca9d 100644 --- a/app/src/main/jni/core/jni_impl.c +++ b/app/src/main/jni/core/jni_impl.c @@ -24,6 +24,7 @@ #include "common/utils.h" #include "log_writer.h" #include "port_map.h" +#include "pcap_dump.h" // This files contains functions to make the capture core communicate // with the Android system. @@ -191,7 +192,8 @@ static jobject getConnUpdate(pcapdroid_t *pd, const conn_and_tuple_t *conn) { } if(data->update_type & CONN_UPDATE_PAYLOAD) { (*env)->CallVoidMethod(env, update, mids.connUpdateSetPayload, data->payload_chunks, - data->payload_truncated); + data->payload_truncated | + (data->has_decrypted_data << 1)); (*pd->env)->DeleteLocalRef(pd->env, data->payload_chunks); data->payload_chunks = NULL; } @@ -423,7 +425,7 @@ static void notifyBlacklistsLoaded(pcapdroid_t *pd, bl_status_arr_t *status_arr) /* ******************************************************* */ -static bool dumpPayloadChunk(struct pcapdroid *pd, const pkt_context_t *pctx, int dump_size) { +static bool dumpPayloadChunk(struct pcapdroid *pd, const pkt_context_t *pctx, const char *dump_data, int dump_size) { JNIEnv *env = pd->env; bool rv = false; @@ -444,7 +446,7 @@ static bool dumpPayloadChunk(struct pcapdroid *pd, const pkt_context_t *pctx, in jobject chunk = (*env)->NewObject(env, cls.payload_chunk, mids.payloadChunkInit, barray, chunk_type, pctx->is_tx, pctx->ms); if(chunk && !jniCheckException(env)) { - (*env)->SetByteArrayRegion(env, barray, 0, dump_size, (jbyte*)pctx->pkt->l7); + (*env)->SetByteArrayRegion(env, barray, 0, dump_size, (jbyte*) dump_data); rv = (*env)->CallBooleanMethod(env, pctx->data->payload_chunks, mids.arraylistAdd, chunk); } @@ -457,6 +459,17 @@ static bool dumpPayloadChunk(struct pcapdroid *pd, const pkt_context_t *pctx, in /* ******************************************************* */ +static void clearPayloadChunks(struct pcapdroid *pd, const pkt_context_t *pctx) { + JNIEnv *env = pd->env; + + if (pctx->data->payload_chunks) { + (*env)->DeleteLocalRef(env, pctx->data->payload_chunks); + pctx->data->payload_chunks = NULL; + } +} + +/* ******************************************************* */ + // TODO rename static void getLibprogPath(pcapdroid_t *pd, const char *prog_name, char *buf, int bufsize) { JNIEnv *env = pd->env; @@ -535,7 +548,7 @@ static void init_jni(JNIEnv *env) { mids.connUpdateInit = jniGetMethodID(env, cls.conn_update, "", "(I)V"); mids.connUpdateSetStats = jniGetMethodID(env, cls.conn_update, "setStats", "(JJJJIIIII)V"); mids.connUpdateSetInfo = jniGetMethodID(env, cls.conn_update, "setInfo", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V"); - mids.connUpdateSetPayload = jniGetMethodID(env, cls.conn_update, "setPayload", "(Ljava/util/ArrayList;Z)V"); + mids.connUpdateSetPayload = jniGetMethodID(env, cls.conn_update, "setPayload", "(Ljava/util/ArrayList;I)V"); mids.statsInit = jniGetMethodID(env, cls.stats, "", "()V"); mids.statsSetData = jniGetMethodID(env, cls.stats, "setData", "(Ljava/lang/String;JJJJJIIIIIIIII)V"); mids.blacklistStatusInit = jniGetMethodID(env, cls.blacklist_status, "", "(Ljava/lang/String;I)V"); @@ -585,6 +598,7 @@ Java_com_emanuelef_remote_1capture_CaptureService_runPacketLoop(JNIEnv *env, jcl .notify_service_status = notifyServiceStatus, .notify_blacklists_loaded = notifyBlacklistsLoaded, .dump_payload_chunk = dumpPayloadChunk, + .clear_payload_chunks = clearPayloadChunks, }, .mitm_addon_uid = getIntPref(env, vpn, "getMitmAddonUid"), .vpn_capture = (bool) getIntPref(env, vpn, "isVpnCapture"), @@ -1287,9 +1301,22 @@ Java_com_emanuelef_remote_1capture_CaptureService_dumpMasterSecret(JNIEnv *env, /* ******************************************************* */ JNIEXPORT jboolean JNICALL -Java_com_emanuelef_remote_1capture_CaptureService_hasSeenPcapdroidTrailer(JNIEnv *env, - jclass clazz) { +Java_com_emanuelef_remote_1capture_CaptureService_hasSeenPcapdroidTrailer(JNIEnv *env, jclass clazz) { return has_seen_pcapdroid_trailer; } -#endif // ANDROID +JNIEXPORT jboolean JNICALL +Java_com_emanuelef_remote_1capture_CaptureService_extractKeylogFromPcapng(JNIEnv *env, jclass clazz, + jstring pcapng_path, jstring out_path +) { + const char *pcapng_s = (*env)->GetStringUTFChars(env, pcapng_path, 0); + const char *out_s = (*env)->GetStringUTFChars(env, out_path, 0); + + bool rv = pcapng_to_keylog(pcapng_s, out_s); + + (*env)->ReleaseStringUTFChars(env, out_path, out_s); + (*env)->ReleaseStringUTFChars(env, pcapng_path, pcapng_s); + return rv; +} + +#endif // ANDROID \ No newline at end of file diff --git a/app/src/main/jni/core/pcap_dump.c b/app/src/main/jni/core/pcap_dump.c index e3b51f2ad..09c340a09 100644 --- a/app/src/main/jni/core/pcap_dump.c +++ b/app/src/main/jni/core/pcap_dump.c @@ -14,9 +14,10 @@ * You should have received a copy of the GNU General Public License * along with PCAPdroid. If not, see . * - * Copyright 2023 - Emanuele Faranda + * Copyright 2023-24 - Emanuele Faranda */ +#include #include #include #include "common/utils.h" @@ -26,6 +27,10 @@ #define LINKTYPE_ETHERNET 1 #define LINKTYPE_RAW 101 +#define PCAPNG_MAGIC 0x1a2b3c4d +#define PCAPNG_DSB_BLOCK 0x0000000A +#define PCAPNG_DSB_TLS_KEYLOG 0x544c534b + #define MAX_PCAP_DUMP_DELAY_MS 1000 #define PCAP_BUFFER_SIZE (512*1024) // 512K #define PCAP_BUFFER_ALMOST_FULL_SIZE (450*1024) // 450K @@ -109,9 +114,9 @@ static void export_keylog_buffer(pcap_dumper_t *dumper) { // prepare the block header pcapng_decr_secrets_block_t *dsb = (pcapng_decr_secrets_block_t*) dumper->keylog_buf; - dsb->type = 0x0000000A; + dsb->type = PCAPNG_DSB_BLOCK; dsb->total_length = block_size; - dsb->secrets_type = 0x544c534b /* TLS_KEYLOG */; + dsb->secrets_type = PCAPNG_DSB_TLS_KEYLOG; dsb->secrets_length = sec_len; // padding @@ -226,7 +231,7 @@ static int get_pcapng_preamble(pcap_dumper_t *dumper, char **out) { pcapng_section_hdr_block_t *shb = (pcapng_section_hdr_block_t*) preamble; shb->type = 0x0A0D0D0A; shb->total_length = shb_length; - shb->magic = 0x1a2b3c4d; + shb->magic = PCAPNG_MAGIC; shb->version_major = 1; shb->version_minor = 0; shb->section_length = -1; @@ -446,4 +451,104 @@ bool pcap_dump_secret(pcap_dumper_t *dumper, int8_t *sec_data, int sec_len) { pthread_mutex_unlock(&dumper->keylog_mutex); return true; +} + +/* ******************************************************* */ + +// Generates an SSLKEYLOG from a PCAPNG file +bool pcapng_to_keylog(const char *pcapng_path, const char *out_path) { + FILE *fin = fopen(pcapng_path, "rb"); + if (!fin) { + log_e("Open PCAPNG failed[%d]: %s", errno, strerror(errno)); + return false; + } + + FILE *fout = fopen(out_path, "w"); + if (!fout) { + fclose(fin); + log_e("Open keylog failed[%d]: %s", errno, strerror(errno)); + return false; + } + + bool rv = false; + + pcapng_section_hdr_block_t section_block; + if (!fread(§ion_block, sizeof(section_block), 1, fin)) { + log_e("Error reading the Section Header Block"); + goto done; + } + + if ((section_block.type != 0x0A0D0D0A) || + (section_block.magic != PCAPNG_MAGIC) || + (section_block.version_major != 1) + ) { + log_e("Invalid Section Header Block"); + goto done; + } + + rewind(fin); + pcapng_generic_block_t block; + char secbuf[4096]; + + while (!ferror(fin) && !ferror(fout)) { + int block_start = ftell(fin); + + if (fread(&block, sizeof(block), 1, fin) != 1) { + if (ferror(fin)) { + log_e("Error reading the next block"); + goto done; + } else + break; + } + + if (block.total_length < 12) { + log_e("Invalid Block length: %u", block.total_length); + goto done; + } + + //log_d("Block: %08x - %u B", block.type, block.total_length); + + if ((block.type == PCAPNG_DSB_BLOCK) && (block.total_length > sizeof(pcapng_decr_secrets_block_t))) { + pcapng_decr_secrets_block_t dsb; + + fseek(fin, block_start, SEEK_SET); + if (fread(&dsb, sizeof(dsb), 1, fin) != 1) { + log_e("Error reading the DSB block"); + goto done; + } + + if (dsb.secrets_type == PCAPNG_DSB_TLS_KEYLOG) { + uint32_t secrets_len = dsb.secrets_length; + + if (secrets_len > 0) { + if (secrets_len <= sizeof(secbuf)) { + if (fread(secbuf, secrets_len, 1, fin) != 1) { + log_e("Error reading the DSB secrets"); + goto done; + } + + if (fwrite(secbuf, secrets_len, 1, fout) != 1) { + log_e("Error writing the KEYLOG file[%d]: %s", errno, strerror(errno)); + goto done; + } + } else + log_w("Invalid secrets length (%u), ignored", secrets_len); + } + } + } + + // skip block + if (fseek(fin, block_start + block.total_length, SEEK_SET) != 0) { + log_e("fseek failed [%d]: %s", errno, strerror(errno)); + goto done; + } + } + + fflush(fout); + rv = !ferror(fin) && !ferror(fout); + +done: + fclose(fin); + fclose(fout); + return rv; } \ No newline at end of file diff --git a/app/src/main/jni/core/pcap_dump.h b/app/src/main/jni/core/pcap_dump.h index 5b327d657..4f42ca72b 100644 --- a/app/src/main/jni/core/pcap_dump.h +++ b/app/src/main/jni/core/pcap_dump.h @@ -22,6 +22,7 @@ #include #include +#include /* Packet dump module, dumping packet records in the PCAP/PCAPNG format. * Packets are first buffered and then exported periodically to the callback. pcap_check_export must @@ -32,6 +33,13 @@ * PCAP header. */ typedef struct pcap_dumper pcap_dumper_t; +// compatible with pcap.h +struct pcap_pkthdr { + struct timeval ts; + uint32_t caplen; + uint32_t len; +}; + /* ******************************************************* */ typedef struct pcap_hdr { @@ -53,6 +61,13 @@ typedef struct pcap_rec { /* ******************************************************* */ +typedef struct pcapng_generic_block { + uint32_t type; + uint32_t total_length; + + /* ..options.. */ +} __attribute__((packed)) pcapng_generic_block_t; + // NOTE: all the PCAPNG block addresses are aligned to 32-bits typedef struct pcapng_section_hdr_block { uint32_t type; @@ -132,5 +147,6 @@ bool pcap_dump_secret(pcap_dumper_t *dumper, int8_t *sec_data, int seclen); int pcap_get_preamble(pcap_dumper_t *dumper, char **out); uint64_t pcap_get_dump_size(pcap_dumper_t *dumper); bool pcap_check_export(pcap_dumper_t *dumper); +bool pcapng_to_keylog(const char *pcapng_path, const char *out_path); #endif // __MY_PCAP_H__ diff --git a/app/src/main/jni/core/pcapdroid.c b/app/src/main/jni/core/pcapdroid.c index 02e7fcc90..085abd853 100644 --- a/app/src/main/jni/core/pcapdroid.c +++ b/app/src/main/jni/core/pcapdroid.c @@ -581,18 +581,36 @@ static void process_payload(pcapdroid_t *pd, pkt_context_t *pctx) { if((pd->payload_mode == PAYLOAD_MODE_NONE) || (pd->cb.dump_payload_chunk == NULL) || (pkt->l7_len <= 0) || + (data->has_decrypted_data && !pctx->plain_data) || (pd->tls_decryption.enabled && data->proxied)) // NOTE: when performing TLS decryption, TCP connections data is handled by the MitmReceiver return; if((pd->payload_mode != PAYLOAD_MODE_MINIMAL) || !data->has_payload[pctx->is_tx]) { - int to_dump = pkt->l7_len; + const char *to_dump; + int dump_size; + + if (pctx->plain_data) { + // if there is plaintext (decrypted) data, dump it instead of the encrypted data + to_dump = (const char*) pctx->plain_data->data; + dump_size = pctx->plain_data->data_len; + + if (!data->has_decrypted_data) { + // existing chunks are encrypted, so drop them + if (pd->cb.clear_payload_chunks) + pd->cb.clear_payload_chunks(pd, pctx); + data->has_decrypted_data = true; + } + } else { + to_dump = pkt->l7; + dump_size = pkt->l7_len; + } - if((pd->payload_mode == PAYLOAD_MODE_MINIMAL) && (pkt->l7_len > MINIMAL_PAYLOAD_MAX_DIRECTION_SIZE)) { - to_dump = MINIMAL_PAYLOAD_MAX_DIRECTION_SIZE; + if((pd->payload_mode == PAYLOAD_MODE_MINIMAL) && (dump_size > MINIMAL_PAYLOAD_MAX_DIRECTION_SIZE)) { + dump_size = MINIMAL_PAYLOAD_MAX_DIRECTION_SIZE; truncated = true; } - if(pd->cb.dump_payload_chunk(pd, pctx, to_dump)) { + if(pd->cb.dump_payload_chunk(pd, pctx, to_dump, dump_size)) { data->has_payload[pctx->is_tx] = true; updated = true; } else @@ -1083,15 +1101,25 @@ void pd_refresh_time(pcapdroid_t *pd) { /* ******************************************************* */ -/* Process the packet (e.g. perform DPI) and fill the packet context. */ -void pd_process_packet(pcapdroid_t *pd, zdtun_pkt_t *pkt, bool is_tx, const zdtun_5tuple_t *tuple, - pd_conn_t *data, struct timeval *tv, pkt_context_t *pctx) { +void pd_init_pkt_context(pkt_context_t *pctx, + zdtun_pkt_t *pkt, bool is_tx, const zdtun_5tuple_t *tuple, + pd_conn_t *data, struct timeval *tv +) { pctx->pkt = pkt; pctx->tv = *tv; pctx->ms = (uint64_t)tv->tv_sec * 1000 + tv->tv_usec / 1000; pctx->is_tx = is_tx; pctx->tuple = tuple; pctx->data = data; + pctx->plain_data = NULL; // managed by capture_libpcap +} + +/* ******************************************************* */ + +/* Process the packet (e.g. perform DPI) and fill the packet context. */ +void pd_process_packet(pcapdroid_t *pd, pkt_context_t *pctx) { + pd_conn_t *data = pctx->data; + zdtun_pkt_t *pkt = pctx->pkt; // NOTE: pd_account_stats will not be called for blocked connections data->last_seen = pctx->ms; @@ -1104,6 +1132,15 @@ void pd_process_packet(pcapdroid_t *pd, zdtun_pkt_t *pkt, bool is_tx, const zdtu perform_dpi(pd, pctx); } + if (pctx->plain_data && (data->alpn != NDPI_PROTOCOL_UNKNOWN) && (data->alpn != pctx->data->l7proto)) { + // we have the L7 decrypted data + pd_giveup_dpi(pd, data, pctx->tuple); + pctx->data->l7proto = data->alpn; + + data->update_type |= CONN_UPDATE_INFO; + pd_notify_connection_update(pd, pctx->tuple, data); + } + process_payload(pd, pctx); } diff --git a/app/src/main/jni/core/pcapdroid.h b/app/src/main/jni/core/pcapdroid.h index e95a556eb..83d7593e9 100644 --- a/app/src/main/jni/core/pcapdroid.h +++ b/app/src/main/jni/core/pcapdroid.h @@ -127,6 +127,7 @@ typedef struct { bool encrypted_l7; bool payload_truncated; bool has_payload[2]; // [0]: rx, [1] tx + bool has_decrypted_data; char *url; uint8_t update_type; } pd_conn_t; @@ -148,6 +149,11 @@ typedef struct { UT_hash_handle hh; } uid_to_app_t; +typedef struct { + unsigned char *data; + unsigned int data_len; +} plain_data_t; + typedef struct pkt_context { zdtun_pkt_t *pkt; struct timeval tv; // Packet timestamp, need by pcap_dump_rec @@ -155,8 +161,11 @@ typedef struct pkt_context { bool is_tx; const zdtun_5tuple_t *tuple; pd_conn_t *data; + plain_data_t *plain_data; } pkt_context_t; +struct ushark; + /* ******************************************************* */ struct pcapdroid; @@ -171,7 +180,8 @@ 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, int dump_size); + bool (*dump_payload_chunk)(struct pcapdroid *pd, const pkt_context_t *pctx, const char *dump_data, int dump_size); + void (*clear_payload_chunks)(struct pcapdroid *pd, const pkt_context_t *pctx); } pd_callbacks_t; /* ******************************************************* */ @@ -234,6 +244,7 @@ typedef struct pcapdroid { char *bpf; char *capture_interface; int pcapd_pid; + struct ushark *usk; int *app_filter_uids; int app_filter_uids_size; @@ -387,8 +398,10 @@ extern char *pd_os; // capture API int pd_run(pcapdroid_t *pd); void pd_refresh_time(pcapdroid_t *pd); -void pd_process_packet(pcapdroid_t *pd, zdtun_pkt_t *pkt, bool is_tx, const zdtun_5tuple_t *tuple, - pd_conn_t *data, struct timeval *tv, pkt_context_t *pctx); +void pd_init_pkt_context(pkt_context_t *pctx, + zdtun_pkt_t *pkt, bool is_tx, const zdtun_5tuple_t *tuple, + pd_conn_t *data, struct timeval *tv); +void pd_process_packet(pcapdroid_t *pd, pkt_context_t *pctx); void pd_account_stats(pcapdroid_t *pd, pkt_context_t *pctx); void pd_dump_packet(pcapdroid_t *pd, const char *pktbuf, int pktlen, const struct timeval *tv, int uid); void pd_housekeeping(pcapdroid_t *pd); diff --git a/app/src/main/jni/core/ushark_dll.c b/app/src/main/jni/core/ushark_dll.c new file mode 100644 index 000000000..a4f7da158 --- /dev/null +++ b/app/src/main/jni/core/ushark_dll.c @@ -0,0 +1,95 @@ +#include +#include +#include +#include + +#include "ushark_dll.h" +#include "../common/utils.h" + +static void *sk_dll; +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_destroy)(ushark_t*); +static void (*sk_dissect_tls)(ushark_t*, const unsigned char *, const struct pcap_pkthdr *, + ushark_tls_data_callback); + +bool ushark_init(pcapdroid_t *pd) { + assert(!sk_dll); + + if (!pd->cb.get_libprog_path) { + log_e("get_libprog_path not defined"); + return false; + } + + char ushark_lib[PATH_MAX]; + ushark_lib[0] = '\0'; + pd->cb.get_libprog_path(pd, "ushark", ushark_lib, sizeof(ushark_lib)); + + if (!ushark_lib[0]) { + log_e("cannot find libushark.so"); + return false; + } + + log_d("libushark found: %s", ushark_lib); + + sk_dll = dlopen(ushark_lib, RTLD_NOW | RTLD_LOCAL); + if (!sk_dll) { + log_d("loading libushark.so failed: %s", dlerror()); + return false; + } + + sk_init = dlsym(sk_dll, "ushark_init"); + sk_cleanup = dlsym(sk_dll, "ushark_cleanup"); + sk_new = dlsym(sk_dll, "ushark_new"); + sk_set_pref = dlsym(sk_dll, "ushark_set_pref"); + sk_destroy = dlsym(sk_dll, "ushark_destroy"); + sk_dissect_tls = dlsym(sk_dll, "ushark_dissect_tls"); + + if (!sk_init || !sk_cleanup || !sk_new || !sk_set_pref || !sk_destroy || !sk_dissect_tls) { + dlclose(sk_dll); + sk_dll = NULL; + log_e("libushark.so misses some required symbols"); + return false; + } + + sk_init(); + return true; +} + +void ushark_cleanup() { + assert(sk_dll); + sk_cleanup(); + + sk_init = NULL; + sk_cleanup = NULL; + sk_new = NULL; + sk_set_pref = NULL; + sk_destroy = NULL; + sk_dissect_tls = NULL; + + // deallocates the static variables in wireshark, necessary to run cleanly again + dlclose(sk_dll); + sk_dll = NULL; +} + +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); +} + +void ushark_set_pref(const char *name, const char *val) { + assert(sk_set_pref); + return 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); +} \ No newline at end of file diff --git a/app/src/main/jni/core/ushark_dll.h b/app/src/main/jni/core/ushark_dll.h new file mode 100644 index 000000000..5d2f2521c --- /dev/null +++ b/app/src/main/jni/core/ushark_dll.h @@ -0,0 +1,19 @@ +#ifndef _USHARK_H_ +#define _USHARK_H_ + +#include "pcapdroid.h" + +typedef struct ushark ushark_t; +struct pcap_pkthdr; + +bool ushark_init(pcapdroid_t *pd); +void ushark_cleanup(); + +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); + +#endif diff --git a/app/src/main/jni/tests/test_utils.c b/app/src/main/jni/tests/test_utils.c index c2bec727c..02940fe92 100644 --- a/app/src/main/jni/tests/test_utils.c +++ b/app/src/main/jni/tests/test_utils.c @@ -216,13 +216,13 @@ 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, int dump_size) { +bool dump_cb_payload_chunk(pcapdroid_t *pd, const pkt_context_t *pctx, 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); assert(chunk->payload != NULL); - memcpy(chunk->payload, pctx->pkt->l7, dump_size); + memcpy(chunk->payload, dump_data, dump_size); chunk->size = dump_size; chunk->is_tx = pctx->is_tx; diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml index c386fe243..420f31269 100644 --- a/app/src/main/res/menu/main_menu.xml +++ b/app/src/main/res/menu/main_menu.xml @@ -29,4 +29,10 @@ android:title="@string/open_pcap_file" android:enabled="true" app:showAsAction="never" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 57351745e..4380f919d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,13 @@ - Brotli decoder: MIT\n\n - mitmproxy: MIT\n\n - Chaquopy: MIT\n\n + - Wireshark: GPL-2.0\n\n + - ushark: GPL-2.0\n\n + - libiconv: LGPL-2.1\n\n + - glib: LGPL-2.1\n\n + - gpgerror: LGPL-2.1\n\n + - gcrypt: LGPL-2.1\n\n + - nghttp2: MIT\n\n - MaxMind DB Reader: Apache-2.0\n\n - FlagKit: MIT\n\n - IP Geolocation by DB-IP\n\n @@ -479,6 +486,7 @@ Injected Open PCAP file… PCAP file loaded + Decrypt PCAP file… Invalid PCAP file Could not open the capture interface PCAP file has an unsupported datalink @@ -486,6 +494,7 @@ Capture start failure. Ensure that the device is rooted with Magisk Capture start failure. Make sure you grant root access to PCAPdroid PCAP read error + SSL keylog read error PCAP file loading aborted "Could not resolve host %1$s To show the actual apps instead of \"%1$s\", be sure to enable the \"%2$s\" option before exporting the PCAP file @@ -513,4 +522,6 @@ VPN reconnection VPN reconnection aborted Waiting for the active VPN to disconnect… + Select the PCAP/Pcapng file + Select the SSL keylog file diff --git a/submodules/PCAPdroid-ushark-bin b/submodules/PCAPdroid-ushark-bin new file mode 160000 index 000000000..923be0ef3 --- /dev/null +++ b/submodules/PCAPdroid-ushark-bin @@ -0,0 +1 @@ +Subproject commit 923be0ef38b8616c75f71e4ff35a19ae1543ad5c