From cb4bbc454db0ed8f22480789623b8a60ddff3887 Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Wed, 20 Oct 2021 15:32:29 +0200 Subject: [PATCH] Implement blacklist-based malware detection A notification is generated when a connection matches known malicious domains or IP addresses. The connections view reports malicious connections with a skull icon. A filter can be set to only show them. Needed for #105 --- .../com/emanuelef/remote_capture/Billing.java | 24 +++ .../remote_capture/CaptureService.java | 107 ++++++++--- .../remote_capture/ConnectionsRegister.java | 12 ++ .../emanuelef/remote_capture/PCAPdroid.java | 18 +- .../com/emanuelef/remote_capture/Utils.java | 47 +++++ .../activities/AppDetailsActivity.java | 2 +- .../activities/EditFilterActivity.java | 7 +- .../activities/LogviewActivity.java | 6 +- .../activities/SettingsActivity.java | 17 ++ .../adapters/ConnectionsAdapter.java | 4 + .../fragments/ConnectionsFragment.java | 8 +- .../model/ConnectionDescriptor.java | 14 +- .../model/FilterDescriptor.java | 3 + .../remote_capture/model/MatchList.java | 4 + .../emanuelef/remote_capture/model/Prefs.java | 8 + app/src/main/jni/vpnproxy-jni/CMakeLists.txt | 1 + app/src/main/jni/vpnproxy-jni/blacklist.c | 177 ++++++++++++++++++ app/src/main/jni/vpnproxy-jni/blacklist.h | 21 +++ app/src/main/jni/vpnproxy-jni/capture_root.c | 2 +- app/src/main/jni/vpnproxy-jni/vpnproxy.c | 67 ++++++- app/src/main/jni/vpnproxy-jni/vpnproxy.h | 10 + app/src/main/res/drawable/ic_skull.xml | 8 + app/src/main/res/layout/connection_item.xml | 13 +- .../main/res/layout/edit_filter_activity.xml | 11 +- app/src/main/res/values/strings.xml | 5 + app/src/main/res/xml/root_preferences.xml | 9 + 26 files changed, 548 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/emanuelef/remote_capture/Billing.java create mode 100644 app/src/main/jni/vpnproxy-jni/blacklist.c create mode 100644 app/src/main/jni/vpnproxy-jni/blacklist.h create mode 100644 app/src/main/res/drawable/ic_skull.xml diff --git a/app/src/main/java/com/emanuelef/remote_capture/Billing.java b/app/src/main/java/com/emanuelef/remote_capture/Billing.java new file mode 100644 index 00000000..b4715472 --- /dev/null +++ b/app/src/main/java/com/emanuelef/remote_capture/Billing.java @@ -0,0 +1,24 @@ +package com.emanuelef.remote_capture; + +import android.content.Context; + +/* Billing stub */ +public class Billing { + // SKUs + public static final String NO_ADS_SKU = "no_ads"; + public static final String MALWARE_DETECTION_SKU = "malware_detection"; + + protected final Context mContext; + + public Billing(Context ctx) { + mContext = ctx; + } + + public boolean isAvailable(String sku) { + return false; + } + + public boolean isPurchased(String sku) { + return false; + } +} 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 dd5c50d6..adece687 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java +++ b/app/src/main/java/com/emanuelef/remote_capture/CaptureService.java @@ -27,6 +27,7 @@ import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.net.ConnectivityManager; import android.net.Network; @@ -49,12 +50,17 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; +import com.emanuelef.remote_capture.activities.ConnectionsActivity; import com.emanuelef.remote_capture.activities.MainActivity; +import com.emanuelef.remote_capture.fragments.ConnectionsFragment; import com.emanuelef.remote_capture.model.AppDescriptor; import com.emanuelef.remote_capture.model.CaptureSettings; import com.emanuelef.remote_capture.model.ConnectionDescriptor; import com.emanuelef.remote_capture.model.ConnectionUpdate; +import com.emanuelef.remote_capture.model.FilterDescriptor; +import com.emanuelef.remote_capture.model.MatchList; import com.emanuelef.remote_capture.model.Prefs; import com.emanuelef.remote_capture.model.VPNStats; import com.emanuelef.remote_capture.pcap_dump.FileDumper; @@ -71,6 +77,7 @@ public class CaptureService extends VpnService implements Runnable { private static final String TAG = "CaptureService"; private static final String VpnSessionName = "PCAPdroid VPN"; private static final String NOTIFY_CHAN_VPNSERVICE = "VPNService"; + private static final String NOTIFY_CHAN_BLACKLISTED = "Blacklisted"; private static final int NOTIFY_ID_VPNSERVICE = 1; private static CaptureService INSTANCE; private ParcelFileDescriptor mParcelFileDescriptor; @@ -86,10 +93,12 @@ public class CaptureService extends VpnService implements Runnable { private PcapDumper mDumper; private ConnectionsRegister conn_reg; private Uri mPcapUri; - private NotificationCompat.Builder mNotificationBuilder; + private NotificationCompat.Builder mStatusBuilder; + private NotificationCompat.Builder mBlacklistedBuilder; private long mMonitoredNetwork; private ConnectivityManager.NetworkCallback mNetworkCallback; private AppsResolver appsResolver; + private boolean mMalwareDetectionEnabled; /* The maximum connections to log into the ConnectionsRegister. Older connections are dropped. * Max Estimated max memory usage: less than 4 MB. */ @@ -135,7 +144,7 @@ public class CaptureService extends VpnService implements Runnable { private int abortStart() { // NOTE: startForeground must be called before stopSelf, otherwise an exception will occur setupNotifications(); - startForeground(NOTIFY_ID_VPNSERVICE, getNotification()); + startForeground(NOTIFY_ID_VPNSERVICE, getStatusNotification()); stopSelf(); sendServiceStatus(SERVICE_STATUS_STOPPED); @@ -231,6 +240,13 @@ public class CaptureService extends VpnService implements Runnable { } else app_filter_uid = -1; + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + mMalwareDetectionEnabled = Prefs.isMalwareDetectionEnabled(this, prefs); + if(mMalwareDetectionEnabled) { + // TODO load from URL + Utils.unzip(getResources().openRawResource(R.raw.malware_blacklist), getCacheDir().getPath()); + } + if(!mSettings.root_capture) { Log.i(TAG, "Using DNS server " + dns_server); @@ -289,7 +305,7 @@ public class CaptureService extends VpnService implements Runnable { mThread.start(); setupNotifications(); - startForeground(NOTIFY_ID_VPNSERVICE, getNotification()); + startForeground(NOTIFY_ID_VPNSERVICE, getStatusNotification()); return START_STICKY; } @@ -327,16 +343,21 @@ public class CaptureService extends VpnService implements Runnable { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + // VPN running notification channel NotificationChannel chan = new NotificationChannel(NOTIFY_CHAN_VPNSERVICE, NOTIFY_CHAN_VPNSERVICE, NotificationManager.IMPORTANCE_LOW); // low: no sound nm.createNotificationChannel(chan); + + // Blacklisted connection notification channel + chan = new NotificationChannel(NOTIFY_CHAN_BLACKLISTED, + NOTIFY_CHAN_BLACKLISTED, NotificationManager.IMPORTANCE_HIGH); + nm.createNotificationChannel(chan); } - // Notification builder + // Status notification builder PendingIntent pi = PendingIntent.getActivity(this, 0, - new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); - - mNotificationBuilder = new NotificationCompat.Builder(this, NOTIFY_CHAN_VPNSERVICE) + new Intent(this, MainActivity.class), Utils.getIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT)); + mStatusBuilder = new NotificationCompat.Builder(this, NOTIFY_CHAN_VPNSERVICE) .setSmallIcon(R.drawable.ic_logo) .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) .setContentIntent(pi) @@ -344,40 +365,64 @@ public class CaptureService extends VpnService implements Runnable { .setAutoCancel(false) .setContentTitle(getResources().getString(R.string.capture_running)) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCategory(NotificationCompat.CATEGORY_STATUS) .setPriority(NotificationCompat.PRIORITY_LOW); // see IMPORTANCE_LOW - // CATEGORY_RECOMMENDATION makes the notification visible on the home screen of Android TVs. - // However this is not what CATEGORY_RECOMMENDATION is designed for, so it should be avoided. - /* - if(Utils.isTv(this)) { - // This is the icon which is visualized - Drawable banner = ContextCompat.getDrawable(this, R.drawable.banner); - BitmapDrawable largeIcon = Utils.scaleDrawable(getResources(), banner, - banner.getIntrinsicWidth(), banner.getIntrinsicHeight()); - - if(largeIcon != null) - mNotificationBuilder.setLargeIcon(largeIcon.getBitmap()); - - // On Android TV it must be shown as a recommendation - mNotificationBuilder.setCategory(NotificationCompat.CATEGORY_RECOMMENDATION); - } else*/ - mNotificationBuilder.setCategory(NotificationCompat.CATEGORY_STATUS); + // Blacklisted notification builder + mBlacklistedBuilder = new NotificationCompat.Builder(this, NOTIFY_CHAN_BLACKLISTED) + .setSmallIcon(R.drawable.ic_skull) + .setAutoCancel(true) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCategory(NotificationCompat.CATEGORY_STATUS) + .setPriority(NotificationCompat.PRIORITY_HIGH); // see IMPORTANCE_HIGH } - private Notification getNotification() { + private Notification getStatusNotification() { String msg = String.format(getString(R.string.notification_msg), Utils.formatBytes(last_bytes), Utils.formatNumber(this, last_connections)); - mNotificationBuilder.setContentText(msg); + mStatusBuilder.setContentText(msg); - return mNotificationBuilder.build(); + return mStatusBuilder.build(); } private void updateNotification() { - Notification notification = getNotification(); + Notification notification = getStatusNotification(); NotificationManagerCompat.from(this).notify(NOTIFY_ID_VPNSERVICE, notification); } + public void notifyBlacklistedConnection(ConnectionDescriptor conn) { + int uid = conn.uid; + + AppDescriptor app = appsResolver.get(conn.uid, 0); + assert app != null; + + FilterDescriptor filter = new FilterDescriptor(); + filter.onlyBlacklisted = true; + + Intent intent = new Intent(this, ConnectionsActivity.class) + .putExtra(ConnectionsFragment.FILTER_EXTRA, filter) + .putExtra(ConnectionsFragment.QUERY_EXTRA, app.getPackageName()); + PendingIntent pi = PendingIntent.getActivity(this, 0, + intent, Utils.getIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT)); + + String rule_label; + if(conn.blacklisted_domain) + rule_label = MatchList.getLabel(this, MatchList.RuleType.HOST, conn.info); + else + rule_label = MatchList.getLabel(this, MatchList.RuleType.IP, conn.dst_ip); + + mBlacklistedBuilder + .setContentIntent(pi) + .setWhen(System.currentTimeMillis()) + .setContentTitle(String.format(getResources().getString(R.string.malicious_connection_app), app.getName())) + .setContentText(rule_label); + Notification notification = mBlacklistedBuilder.build(); + + // Use the UID as the notification ID to group alerts from the same app + mHandler.post(() -> NotificationManagerCompat.from(this).notify(uid, notification)); + } + @RequiresApi(api = Build.VERSION_CODES.M) private void registerNetworkCallbacks() { if(mNetworkCallback != null) @@ -578,6 +623,8 @@ public class CaptureService extends VpnService implements Runnable { public int isRootCapture() { return(mSettings.root_capture ? 1 : 0); } + public int malwareDetectionEnabled() { return(mMalwareDetectionEnabled ? 1 : 0); } + public int addPcapdroidTrailer() { return(mSettings.pcapdroid_trailer ? 1 : 0); } public int getAppFilterUid() { return(app_filter_uid); } @@ -688,11 +735,11 @@ public class CaptureService extends VpnService implements Runnable { }); } - public static String getPcapdWorkingDir(Context ctx) { + public static String getWorkingDir(Context ctx) { return ctx.getCacheDir().getAbsolutePath(); } - public String getPcapdWorkingDir() { - return getPcapdWorkingDir(this); + public String getWorkingDir() { + return getWorkingDir(this); } public String getLibprogPath(String prog_name) { diff --git a/app/src/main/java/com/emanuelef/remote_capture/ConnectionsRegister.java b/app/src/main/java/com/emanuelef/remote_capture/ConnectionsRegister.java index 4943efec..37bd1741 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/ConnectionsRegister.java +++ b/app/src/main/java/com/emanuelef/remote_capture/ConnectionsRegister.java @@ -65,6 +65,13 @@ public class ConnectionsRegister { return (mTail - 1 + mSize) % mSize; } + private void processConnectionStatus(ConnectionDescriptor conn) { + if(!conn.alerted && conn.isBlacklisted()) { + CaptureService.requireInstance().notifyBlacklistedConnection(conn); + conn.alerted = true; + } + } + public synchronized void newConnections(ConnectionDescriptor[] conns) { if(conns.length > mSize) { // take the most recent @@ -79,6 +86,7 @@ public class ConnectionsRegister { //Log.d(TAG, "newConnections[" + mNumItems + "/" + mSize +"]: insert " + conns.length + // " items at " + mTail + " (removed: " + out_items + " at " + firstPos() + ")"); + // Remove old connections if(out_items > 0) { int pos = firstPos(); removedItems = new ConnectionDescriptor[out_items]; @@ -101,6 +109,7 @@ public class ConnectionsRegister { } } + // Add new connections for(ConnectionDescriptor conn: conns) { mItemsRing[mTail] = conn; mTail = (mTail + 1) % mSize; @@ -115,6 +124,8 @@ public class ConnectionsRegister { mAppsStats.put(uid, stats); } + processConnectionStatus(conn); + stats.num_connections++; stats.bytes += conn.rcvd_bytes + conn.sent_bytes; } @@ -155,6 +166,7 @@ public class ConnectionsRegister { //Log.d(TAG, "update " + update.incr_id + " -> " + update.update_type); conn.processUpdate(update); + processConnectionStatus(conn); changed_pos[k++] = (pos + mSize - first_pos) % mSize; } 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 6067d5fc..57424080 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java +++ b/app/src/main/java/com/emanuelef/remote_capture/PCAPdroid.java @@ -37,6 +37,15 @@ public class PCAPdroid extends Application { Utils.setAppTheme(theme); } + @Override + public Resources getResources() { + if(mLocalizedContext == null) + return super.getResources(); + + // Ensure that the selected locale is used + return mLocalizedContext.getResources(); + } + public static PCAPdroid getInstance() { return mInstance.get(); } @@ -48,12 +57,7 @@ public class PCAPdroid extends Application { return mVisMask; } - @Override - public Resources getResources() { - if(mLocalizedContext == null) - return super.getResources(); - - // Ensure that the selected locale is used - return mLocalizedContext.getResources(); + public Billing getBilling(Context ctx) { + return new Billing(ctx); } } \ No newline at end of file diff --git a/app/src/main/java/com/emanuelef/remote_capture/Utils.java b/app/src/main/java/com/emanuelef/remote_capture/Utils.java index bcdab7fa..1266f9ca 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/Utils.java +++ b/app/src/main/java/com/emanuelef/remote_capture/Utils.java @@ -22,6 +22,7 @@ package com.emanuelef.remote_capture; import android.Manifest; import android.app.Activity; import android.app.Dialog; +import android.app.PendingIntent; import android.app.UiModeManager; import android.content.ClipData; import android.content.ClipboardManager; @@ -69,7 +70,11 @@ import com.emanuelef.remote_capture.model.AppDescriptor; import com.emanuelef.remote_capture.model.Prefs; import com.emanuelef.remote_capture.views.AppsListView; +import java.io.BufferedOutputStream; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.math.BigInteger; import java.net.Inet4Address; import java.net.InetAddress; @@ -85,6 +90,8 @@ import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; public class Utils { public static final int UID_UNKNOWN = -1; @@ -632,4 +639,44 @@ public class Utils { return builder.toString(); } + + public static int getIntentFlags(int flags) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + flags |= PendingIntent.FLAG_IMMUTABLE; + return flags; + } + + public static boolean unzip(InputStream is, String dstpath) { + try(ZipInputStream zipIn = new ZipInputStream(is)) { + ZipEntry entry = zipIn.getNextEntry(); + + while (entry != null) { + File dst = new File(dstpath + File.separator + entry.getName()); + + if (entry.isDirectory()) { + if(!dst.mkdirs()) { + Log.w("unzip", "Could not create directories"); + return false; + } + } else { + // Extract file + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dst)); + + byte[] bytesIn = new byte[4096]; + int read = 0; + while ((read = zipIn.read(bytesIn)) != -1) + bos.write(bytesIn, 0, read); + bos.close(); + } + + zipIn.closeEntry(); + entry = zipIn.getNextEntry(); + } + + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/AppDetailsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/AppDetailsActivity.java index 6dfb0b31..2bacbb15 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/AppDetailsActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/AppDetailsActivity.java @@ -119,7 +119,7 @@ public class AppDetailsActivity extends BaseActivity { findViewById(R.id.show_connections).setOnClickListener(v -> { Intent intent = new Intent(this, ConnectionsActivity.class); - intent.putExtra(ConnectionsFragment.FILTER_EXTRA, dsc.getPackageName()); + intent.putExtra(ConnectionsFragment.QUERY_EXTRA, dsc.getPackageName()); startActivity(intent); }); } 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 dc130aca..85e79d4a 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 @@ -5,7 +5,6 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.Button; import android.widget.CheckBox; import androidx.annotation.NonNull; @@ -23,6 +22,7 @@ public class EditFilterActivity extends BaseActivity { private static final String TAG = "FilterEditActivity"; private FilterDescriptor mFilter; private CheckBox mShowMasked; + private CheckBox mOnlyBlacklisted; private Chip mStatusOpen; private Chip mStatusClosed; private Chip mStatusUnreachable; @@ -50,12 +50,13 @@ public class EditFilterActivity extends BaseActivity { mFilter = new FilterDescriptor(); mShowMasked = findViewById(R.id.show_masked); + mOnlyBlacklisted = findViewById(R.id.only_blacklisted); mStatusOpen = findViewById(R.id.status_open); mStatusClosed = findViewById(R.id.status_closed); mStatusUnreachable = findViewById(R.id.status_unreachable); mStatusError = findViewById(R.id.status_error); - ((Button)findViewById(R.id.edit_mask)).setOnClickListener(v -> { + findViewById(R.id.edit_mask).setOnClickListener(v -> { Intent editIntent = new Intent(this, EditMaskActivity.class); startActivity(editIntent); }); @@ -73,6 +74,7 @@ public class EditFilterActivity extends BaseActivity { private void model2view() { mShowMasked.setChecked(mFilter.showMasked); + mOnlyBlacklisted.setChecked(mFilter.onlyBlacklisted); Chip selected_status = null; switch(mFilter.status) { @@ -87,6 +89,7 @@ public class EditFilterActivity extends BaseActivity { private void view2model() { mFilter.showMasked = mShowMasked.isChecked(); + mFilter.onlyBlacklisted = mOnlyBlacklisted.isChecked(); if(mStatusOpen.isChecked()) mFilter.status = Status.STATUS_OPEN; diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/LogviewActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/LogviewActivity.java index a8e99786..f995ccc6 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/LogviewActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/LogviewActivity.java @@ -21,8 +21,6 @@ package com.emanuelef.remote_capture.activities; import android.content.Intent; import android.os.Bundle; -import android.text.Html; -import android.text.method.LinkMovementMethod; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -35,10 +33,8 @@ import com.emanuelef.remote_capture.R; import com.emanuelef.remote_capture.Utils; import java.io.BufferedReader; -import java.io.File; import java.io.FileReader; import java.io.IOException; -import java.io.InputStreamReader; public class LogviewActivity extends BaseActivity { private static final String TAG = "LogviewActivity"; @@ -58,7 +54,7 @@ public class LogviewActivity extends BaseActivity { private String readLog() { try { - String logpath = CaptureService.getPcapdWorkingDir(this) + "/pcapd.log"; + String logpath = CaptureService.getWorkingDir(this) + "/pcapd.log"; BufferedReader reader = new BufferedReader(new FileReader(logpath)); StringBuilder builder = new StringBuilder(); diff --git a/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java b/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java index 7b160950..e47a275e 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java +++ b/app/src/main/java/com/emanuelef/remote_capture/activities/SettingsActivity.java @@ -33,6 +33,8 @@ import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceManager; import androidx.preference.SwitchPreference; +import com.emanuelef.remote_capture.Billing; +import com.emanuelef.remote_capture.PCAPdroid; import com.emanuelef.remote_capture.Utils; import com.emanuelef.remote_capture.model.Prefs; import com.emanuelef.remote_capture.R; @@ -79,15 +81,19 @@ public class SettingsActivity extends BaseActivity { private Preference mProxyPrefs; private Preference mIpv6Enabled; private DropDownPreference mCapInterface; + private SwitchPreference mMalwareDetectionEnabled; + private Billing mIab; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.root_preferences, rootKey); + mIab = PCAPdroid.getInstance().getBilling(requireContext()); setupUdpExporterPrefs(); setupHttpServerPrefs(); setupSocks5ProxyPrefs(); setupCapturePrefs(); + setupSecurityPrefs(); setupOtherPrefs(); socks5ProxyHideShow(mTlsDecryptionEnabled.isChecked()); @@ -157,6 +163,17 @@ public class SettingsActivity extends BaseActivity { refreshInterfaces(); } + private void setupSecurityPrefs() { + mMalwareDetectionEnabled = findPreference(Prefs.PREF_MALWARE_DETECTION); + + if(!mIab.isAvailable(Billing.MALWARE_DETECTION_SKU)) { + getPreferenceScreen().removePreference(findPreference("security")); + return; + } + + // Billing code here + } + private void setupSocks5ProxyPrefs() { mProxyPrefs = findPreference("proxy_prefs"); mTlsHelp = findPreference("tls_how_to"); 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 2e153c48..edb37918 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 @@ -73,6 +73,7 @@ public class ConnectionsAdapter extends RecyclerView.Adapter " + diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/FilterDescriptor.java b/app/src/main/java/com/emanuelef/remote_capture/model/FilterDescriptor.java index 1a7714cb..53c51bb7 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/FilterDescriptor.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/FilterDescriptor.java @@ -27,14 +27,17 @@ import java.io.Serializable; public class FilterDescriptor implements Serializable { public Status status = Status.STATUS_INVALID; public boolean showMasked = false; + public boolean onlyBlacklisted = false; public boolean isSet() { return (status != Status.STATUS_INVALID) + || onlyBlacklisted || (!showMasked && !PCAPdroid.getInstance().getVisualizationMask().isEmpty()); } public boolean matches(ConnectionDescriptor conn) { return (showMasked || !PCAPdroid.getInstance().getVisualizationMask().matches(conn)) + && (!onlyBlacklisted || conn.isBlacklisted()) && ((status == Status.STATUS_INVALID) || (conn.getStatus().equals(status))); } } diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/MatchList.java b/app/src/main/java/com/emanuelef/remote_capture/model/MatchList.java index 977b820e..e7f5b69c 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/MatchList.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/MatchList.java @@ -75,6 +75,10 @@ public class MatchList { mValue = value; } + public Rule(Context ctx, RuleType tp, Object value) { + this(tp, value, MatchList.getLabel(ctx, tp, value.toString())); + } + public String getLabel() { return mLabel; } diff --git a/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java b/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java index 0ef6fa52..69a5342e 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java +++ b/app/src/main/java/com/emanuelef/remote_capture/model/Prefs.java @@ -19,8 +19,11 @@ package com.emanuelef.remote_capture.model; +import android.content.Context; import android.content.SharedPreferences; +import com.emanuelef.remote_capture.Billing; +import com.emanuelef.remote_capture.PCAPdroid; import com.emanuelef.remote_capture.Utils; public class Prefs { @@ -32,6 +35,7 @@ public class Prefs { public static final String PREF_SOCKS5_PROXY_IP_KEY = "socks5_proxy_ip_address"; public static final String PREF_SOCKS5_PROXY_PORT_KEY = "socks5_proxy_port"; public static final String PREF_CAPTURE_INTERFACE = "capture_interface"; + public static final String PREF_MALWARE_DETECTION = "malware_detection"; public static final String PREF_TLS_DECRYPTION_ENABLED_KEY = "tls_decryption_enabled"; public static final String PREF_APP_FILTER = "app_filter"; public static final String PREF_HTTP_SERVER_PORT = "http_server_port"; @@ -77,4 +81,8 @@ public class Prefs { public static boolean isRootCaptureEnabled(SharedPreferences p) { return(Utils.isRootAvailable() && p.getBoolean(PREF_ROOT_CAPTURE, false)); } public static boolean isPcapdroidTrailerEnabled(SharedPreferences p) { return(p.getBoolean(PREF_PCAPDROID_TRAILER, false)); } public static String getCaptureInterface(SharedPreferences p) { return(p.getString(PREF_CAPTURE_INTERFACE, "@inet")); } + public static boolean isMalwareDetectionEnabled(Context ctx, SharedPreferences p) { + return(PCAPdroid.getInstance().getBilling(ctx).isPurchased(Billing.MALWARE_DETECTION_SKU) + && p.getBoolean(PREF_MALWARE_DETECTION, false)); + } } diff --git a/app/src/main/jni/vpnproxy-jni/CMakeLists.txt b/app/src/main/jni/vpnproxy-jni/CMakeLists.txt index 5e047eca..b62f10e8 100644 --- a/app/src/main/jni/vpnproxy-jni/CMakeLists.txt +++ b/app/src/main/jni/vpnproxy-jni/CMakeLists.txt @@ -7,6 +7,7 @@ add_library(vpnproxy-jni SHARED ip_lru.c ndpi_master_protos.c crc32.c + blacklist.c pcap_utils.c) # nDPI diff --git a/app/src/main/jni/vpnproxy-jni/blacklist.c b/app/src/main/jni/vpnproxy-jni/blacklist.c new file mode 100644 index 00000000..c4765fec --- /dev/null +++ b/app/src/main/jni/vpnproxy-jni/blacklist.c @@ -0,0 +1,177 @@ +/* + * This file is part of PCAPdroid. + * + * PCAPdroid is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PCAPdroid is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PCAPdroid. If not, see . + * + * Copyright 2020-21 - Emanuele Faranda + */ + +#include "vpnproxy.h" +#include "common/utils.h" + +typedef struct string_entry { + char *key; + UT_hash_handle hh; +} string_entry_t; + +struct blacklist { + string_entry_t *domains; + struct ndpi_detection_module_struct *ndpi; + bool locked; + uint64_t num_ips; + uint64_t num_domains; +}; + +/* ******************************************************* */ + +blacklist_t* blacklist_init(struct ndpi_detection_module_struct *ndpi) { + if(!ndpi) + return NULL; + + blacklist_t *bl = (blacklist_t*) calloc(1, sizeof(blacklist_t)); + if(!bl) + return NULL; + + bl->ndpi = ndpi; + return bl; +} + +/* ******************************************************* */ + +int blacklist_load_file(blacklist_t *bl, const char *path) { + FILE *f; + char buffer[256]; + int num_dm_ok = 0, num_dm_fail = 0; + int num_ip_ok = 0, num_ip_fail = 0; + + if(bl->locked) { + log_e("Blacklist is locked. Run blacklist_clear and load it again."); + return -1; + } + + f = fopen(path, "r"); + if(!f) { + log_e("Could not open blacklist file \"%s\" [%d]: %s", path, errno, strerror(errno)); + return -1; + } + + while(1) { + struct in_addr in_addr; + char *item = fgets(buffer, sizeof(buffer), f); + if(!item) + break; + + if(!item[0] || (item[0] == '#') || (item[0] == '\n')) + continue; + + item[strcspn(buffer, "\r\n")] = '\0'; + bool is_net = strchr(buffer, '/'); + + if(strchr(buffer, ':')) { + // IPv6 not supported + num_ip_fail++; + continue; + } else if(is_net || inet_pton(AF_INET, item, &in_addr) == 1) { + // IPv4 Address/subnet + if(!is_net && ((in_addr.s_addr == 0) || (in_addr.s_addr == 0xFFFFFFFF) || (in_addr.s_addr == 0x7F000001))) + continue; // invalid + + if(ndpi_load_ip_category(bl->ndpi, item, PCAPDROID_NDPI_CATEGORY_MALWARE) == 0) + num_ip_ok++; + else + num_ip_fail++; + } else { + // Domain + if(blacklist_match_domain(bl, item)) + continue; // duplicate domain + + string_entry_t *entry = malloc(sizeof(string_entry_t)); + if(!entry) { + num_dm_fail++; + continue; + } + + entry->key = strdup(item); + if(!entry->key) { + free(entry); + num_dm_fail++; + continue; + } + + HASH_ADD_KEYPTR(hh, bl->domains, entry->key, strlen(entry->key), entry); + num_dm_ok++; + } + } + + fclose(f); + log_d("Blacklist loaded[%s]: %d domains (%d failed), %d IPs (%d failed)", + strrchr(path, '/') + 1, num_dm_ok, num_dm_fail, num_ip_ok, num_ip_fail); + + bl->num_domains += num_dm_ok; + bl->num_ips += num_ip_ok; + + return 0; +} + +/* ******************************************************* */ + +void blacklist_clear(blacklist_t *bl) { + string_entry_t *entry, *tmp; + + HASH_ITER(hh, bl->domains, entry, tmp) { + HASH_DELETE(hh, bl->domains, entry); + free(entry->key); + free(entry); + } + + bl->domains = NULL; + bl->locked = false; + bl->num_ips = bl->num_domains = 0; +} + +/* ******************************************************* */ + +void blacklist_destroy(blacklist_t *bl) { + blacklist_clear(bl); + free(bl); +} + +/* ******************************************************* */ + +bool blacklist_match_ip(blacklist_t *bl, uint32_t ip) { + char ipstr[INET_ADDRSTRLEN]; + struct in_addr addr; + ipstr[0] = '\0'; + ndpi_protocol_category_t cat = 0; + + if(!bl->locked) { + ndpi_enable_loaded_categories(bl->ndpi); + bl->locked = true; + } + + addr.s_addr = ip; + inet_ntop(AF_INET, &addr, ipstr, sizeof(ipstr)); + + ndpi_get_custom_category_match(bl->ndpi, ipstr, strlen(ipstr), &cat); + return(cat == PCAPDROID_NDPI_CATEGORY_MALWARE); +} + +/* ******************************************************* */ + +bool blacklist_match_domain(blacklist_t *bl, const char *domain) { + string_entry_t *entry = NULL; + + HASH_FIND_STR(bl->domains, domain, entry); + return(entry != NULL); +} \ No newline at end of file diff --git a/app/src/main/jni/vpnproxy-jni/blacklist.h b/app/src/main/jni/vpnproxy-jni/blacklist.h new file mode 100644 index 00000000..4dc0723b --- /dev/null +++ b/app/src/main/jni/vpnproxy-jni/blacklist.h @@ -0,0 +1,21 @@ +// +// Created by emanuele on 10/20/21. +// + +#ifndef PCAPDROID_BLACKLIST_H +#define PCAPDROID_BLACKLIST_H + +#include "ndpi_api.h" + +#define PCAPDROID_NDPI_CATEGORY_MALWARE NDPI_PROTOCOL_CATEGORY_CUSTOM_1 + +typedef struct blacklist blacklist_t; + +blacklist_t* blacklist_init(struct ndpi_detection_module_struct *ndpi); +void blacklist_destroy(blacklist_t *bl); +void blacklist_clear(blacklist_t *bl); +int blacklist_load_file(blacklist_t *bl, const char *path); +bool blacklist_match_ip(blacklist_t *bl, uint32_t ip); +bool blacklist_match_domain(blacklist_t *bl, const char *domain); + +#endif //PCAPDROID_BLACKLIST_H diff --git a/app/src/main/jni/vpnproxy-jni/capture_root.c b/app/src/main/jni/vpnproxy-jni/capture_root.c index b580485f..aee7b2f1 100644 --- a/app/src/main/jni/vpnproxy-jni/capture_root.c +++ b/app/src/main/jni/vpnproxy-jni/capture_root.c @@ -173,7 +173,7 @@ static int connectPcapd(vpnproxy_data_t *proxy) { char capture_interface[16]; getStringPref(proxy, "getPcapDumperBpf", bpf, sizeof(bpf)); - getStringPref(proxy, "getPcapdWorkingDir", workdir, PATH_MAX); + getStringPref(proxy, "getWorkingDir", workdir, PATH_MAX); getStringPref(proxy, "getCaptureInterface", capture_interface, sizeof(capture_interface)); get_libprog_path(proxy, "pcapd", pcapd, sizeof(pcapd)); diff --git a/app/src/main/jni/vpnproxy-jni/vpnproxy.c b/app/src/main/jni/vpnproxy-jni/vpnproxy.c index c3035e9d..58afc69f 100644 --- a/app/src/main/jni/vpnproxy-jni/vpnproxy.c +++ b/app/src/main/jni/vpnproxy-jni/vpnproxy.c @@ -320,15 +320,15 @@ conn_data_t* new_connection(vpnproxy_data_t *proxy, const zdtun_5tuple_t *tuple, data->uid = uid; // Try to resolve host name via the LRU cache - zdtun_ip_t ip = tuple->dst_ip; - data->info = ip_lru_find(proxy->ip_to_host, &ip); + const zdtun_ip_t dst_ip = tuple->dst_ip; + data->info = ip_lru_find(proxy->ip_to_host, &dst_ip); if(data->info) { char resip[INET6_ADDRSTRLEN]; int family = (tuple->ipver == 4) ? AF_INET : AF_INET6; resip[0] = '\0'; - inet_ntop(family, &ip, resip, sizeof(resip)); + inet_ntop(family, &dst_ip, resip, sizeof(resip)); log_d("Host LRU cache HIT: %s -> %s", resip, data->info); @@ -361,6 +361,17 @@ conn_data_t* new_connection(vpnproxy_data_t *proxy, const zdtun_5tuple_t *tuple, } } + if(proxy->malware_detection.bl && (tuple->ipver == 4)) { + data->blacklisted_ip = blacklist_match_ip(proxy->malware_detection.bl, tuple->dst_ip.ip4); + if(data->blacklisted_ip) { + char appbuf[64]; + char buf[256]; + + get_appname_by_uid(proxy, data->uid, appbuf, sizeof(appbuf)); + log_w("Blacklisted dst ip: %s[%s]", zdtun_5tuple2str(tuple, buf, sizeof(buf)), appbuf); + } + } + return(data); } @@ -431,6 +442,18 @@ void conn_end_ndpi_detection(conn_data_t *data, vpnproxy_data_t *proxy, const zd break; } + if(proxy->malware_detection.bl && data->info && data->info[0] && !data->blacklisted_domain) { + // TODO early match + data->blacklisted_domain = blacklist_match_domain(proxy->malware_detection.bl, data->info); + if(data->blacklisted_domain) { + char appbuf[64]; + char buf[512]; + + get_appname_by_uid(proxy, data->uid, appbuf, sizeof(appbuf)); + log_w("Blacklisted domain [%s]: %s[%s]", data->info, zdtun_5tuple2str(tuple, buf, sizeof(buf)), appbuf); + } + } + data->update_type |= CONN_UPDATE_INFO; conn_free_ndpi(data); } @@ -642,7 +665,8 @@ static jobject getConnUpdate(vpnproxy_data_t *proxy, const vpn_conn_t *conn) { if(data->update_type & CONN_UPDATE_STATS) { (*env)->CallVoidMethod(env, update, mids.connUpdateSetStats, data->last_seen, data->sent_bytes, data->rcvd_bytes, data->sent_pkts, data->rcvd_pkts, - (data->tcp_flags[0] << 8) | data->tcp_flags[1], data->status); + (data->tcp_flags[0] << 8) | data->tcp_flags[1], + (data->blacklisted_domain << 9) | (data->blacklisted_ip << 8) | (data->status & 0xFF)); } if(data->update_type & CONN_UPDATE_INFO) { jobject info = (*env)->NewStringUTF(env, data->info ? data->info : ""); @@ -1006,6 +1030,15 @@ void account_packet(vpnproxy_data_t *proxy, const zdtun_pkt_t *pkt, uint8_t from /* ******************************************************* */ +static char* get_path(const char *subpath) { + strncpy(global_proxy->workdir + global_proxy->basepath_len, subpath, + sizeof(global_proxy->workdir) - global_proxy->basepath_len - 1); + global_proxy->workdir[sizeof(global_proxy->workdir) - 1] = 0; + return global_proxy->workdir; +} + +/* ******************************************************* */ + static int run_tun(JNIEnv *env, jclass vpn, int tunfd, jint sdk) { netd_resolve_waiting = 0; jclass vpn_class = (*env)->GetObjectClass(env, vpn); @@ -1058,8 +1091,13 @@ static int run_tun(JNIEnv *env, jclass vpn, int tunfd, jint sdk) { .ipv6 = { .enabled = (bool) getIntPref(env, vpn, "getIPv6Enabled"), .dns_server = getIPv6Pref(env, vpn, "getIpv6DnsServer"), + }, + .malware_detection = { + .enabled = (bool) getIntPref(env, vpn, "malwareDetectionEnabled"), } }; + getStringPref(&proxy, "getWorkingDir", proxy.workdir, sizeof(proxy.workdir)); + proxy.basepath_len = strlen(proxy.workdir); // Enable or disable the PCAPdroid trailer pcap_set_pcapdroid_trailer((bool)getIntPref(env, vpn, "addPcapdroidTrailer")); @@ -1073,12 +1111,27 @@ static int run_tun(JNIEnv *env, jclass vpn, int tunfd, jint sdk) { /* nDPI */ proxy.ndpi = init_ndpi(); init_protocols_bitmask(&masterProtos); - if(proxy.ndpi == NULL) { log_f("nDPI initialization failed"); return(-1); } + // Load blacklist + if(proxy.malware_detection.enabled) { + blacklist_t *bl = blacklist_init(proxy.ndpi); + if(bl == NULL) { + log_f("Blacklist initialization failed"); + ndpi_exit_detection_module(proxy.ndpi); + return (-1); + } + + blacklist_load_file(bl, get_path("/maltrail-malware-domains.txt")); + blacklist_load_file(bl, get_path("/emerging-Block-IPs.txt")); + blacklist_load_file(bl, get_path("/abuse_sslipblacklist.txt")); + blacklist_load_file(bl, get_path("/feodotracker_ipblocklist.txt")); + proxy.malware_detection.bl = bl; + } + signal(SIGPIPE, SIG_IGN); if(proxy.pcap_dump.enabled) { @@ -1108,6 +1161,8 @@ static int run_tun(JNIEnv *env, jclass vpn, int tunfd, jint sdk) { conns_clear(&proxy.new_conns, true); conns_clear(&proxy.conns_updates, true); + if(proxy.malware_detection.bl) + blacklist_destroy(proxy.malware_detection.bl); ndpi_exit_detection_module(proxy.ndpi); if(proxy.pcap_dump.buffer) { @@ -1193,4 +1248,4 @@ Java_com_emanuelef_remote_1capture_CaptureService_getPcapHeader(JNIEnv *env, jcl } return barray; -} \ No newline at end of file +} diff --git a/app/src/main/jni/vpnproxy-jni/vpnproxy.h b/app/src/main/jni/vpnproxy-jni/vpnproxy.h index 4deba933..ab1fc469 100644 --- a/app/src/main/jni/vpnproxy-jni/vpnproxy.h +++ b/app/src/main/jni/vpnproxy-jni/vpnproxy.h @@ -24,6 +24,7 @@ #include #include "zdtun.h" #include "ip_lru.h" +#include "blacklist.h" #include "ndpi_api.h" #include "common/uid_resolver.h" #include "third_party/uthash.h" @@ -82,6 +83,8 @@ typedef struct conn_data { bool pending_notification; bool to_purge; bool request_done; + bool blacklisted_ip; + bool blacklisted_domain; char *request_data; char *url; uint8_t update_type; @@ -118,6 +121,8 @@ typedef struct vpnproxy_data { ndpi_ptree_t *known_dns_servers; uid_resolver_t *resolver; ip_lru_t *ip_to_host; + char workdir[PATH_MAX]; + int basepath_len; struct timeval last_pkt_ts; // Packet timestamp, reported into the exported PCAP uint64_t now_ms; // Monotonic timestamp, see refresh_time u_int num_dropped_pkts; @@ -153,6 +158,11 @@ typedef struct vpnproxy_data { struct in6_addr dns_server; } ipv6; + struct { + bool enabled; + blacklist_t *bl; + } malware_detection; + capture_stats_t capture_stats; } vpnproxy_data_t; diff --git a/app/src/main/res/drawable/ic_skull.xml b/app/src/main/res/drawable/ic_skull.xml new file mode 100644 index 00000000..05e02f8e --- /dev/null +++ b/app/src/main/res/drawable/ic_skull.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/layout/connection_item.xml b/app/src/main/res/layout/connection_item.xml index bad31687..5efc227b 100644 --- a/app/src/main/res/layout/connection_item.xml +++ b/app/src/main/res/layout/connection_item.xml @@ -40,13 +40,14 @@ android:layout_height="wrap_content" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintEnd_toStartOf="@+id/status_ind" + app:layout_constraintEnd_toStartOf="@+id/blacklisted" app:layout_constraintHorizontal_bias="0" tools:text="Android" /> + + + android:layout_width="match_parent" + android:layout_marginBottom="15dp"> + + Edit Rules Hidden Connections Rules No rules + Malicious connection detected (%1$s) + Only malicious connections + Security + Malware Detection + "Detect possibly malicious connections and trigger alerts. NOTE: the detection can produce false positives and is not a valid alternative to an antivirus." diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index f0abb83a..3c9114c0 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -105,6 +105,15 @@ + + + +