Skip to content

Commit

Permalink
Add ability to decrypt PCAP/Pcapng files
Browse files Browse the repository at this point in the history
A new "Decrypt PCAP file" entry has been added to the main menu,
which allows loading a PCAP+keylog or a Pcapng with secrets and
show the decrypted data in PCAPdroud.

The decryption itself is performed by Wireshark, which is built as
the standalone shared library libushark.so, thanks to ushark.

The shared library is loaded via dlopen to allow proper
re-initialization of the static variables in Wireshark. This also
provides the benefit to avoud unnecessary overhead and possible
inteferences when not used.

HTTP/2 reassembly is properly supported (implemented in ushark)
and content decoding works as expected.

See #351
  • Loading branch information
emanuele-f committed Jun 15, 2024
1 parent 308de20 commit 177d5b3
Show file tree
Hide file tree
Showing 25 changed files with 620 additions and 86 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1624,4 +1624,5 @@ public static boolean hasError() {
public static native List<String> getL7Protocols();
public static native void dumpMasterSecret(byte[] secret);
public static native boolean hasSeenPcapdroidTrailer();
public static native boolean extractKeylogFromPcapng(String pcapng_path, String out_path);
}
9 changes: 9 additions & 0 deletions app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<PCAPdroid> mInstance;
protected static boolean isUnderTest = false;

Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -135,6 +139,8 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig
registerForActivityResult(new StartActivityForResult(), this::peerInfoResult);
private final ActivityResultLauncher<Intent> pcapFileOpenLauncher =
registerForActivityResult(new StartActivityForResult(), this::pcapFileOpenResult);
private final ActivityResultLauncher<Intent> keylogFileOpenLauncher =
registerForActivityResult(new StartActivityForResult(), this::keylogFileOpenResult);

@Override
protected void onCreate(Bundle savedInstanceState) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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);
}
}

Expand Down
Loading

0 comments on commit 177d5b3

Please sign in to comment.