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
This commit is contained in:
emanuele-f 2021-10-20 15:32:29 +02:00
parent 6b623ea34d
commit cb4bbc454d
26 changed files with 548 additions and 57 deletions

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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;
}

View File

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

View File

@ -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;
}
}
}

View File

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

View File

@ -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;

View File

@ -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();

View File

@ -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");

View File

@ -73,6 +73,7 @@ public class ConnectionsAdapter extends RecyclerView.Adapter<ConnectionsAdapter.
public static class ViewHolder extends RecyclerView.ViewHolder {
ImageView icon;
ImageView blacklistedInd;
TextView statusInd;
TextView remote;
TextView l7proto;
@ -91,6 +92,7 @@ public class ConnectionsAdapter extends RecyclerView.Adapter<ConnectionsAdapter.
statusInd = itemView.findViewById(R.id.status_ind);
appName = itemView.findViewById(R.id.app_name);
lastSeen = itemView.findViewById(R.id.last_seen);
blacklistedInd = itemView.findViewById(R.id.blacklisted);
Context context = itemView.getContext();
mProtoAndPort = context.getString(R.string.proto_and_port);
@ -134,6 +136,8 @@ public class ConnectionsAdapter extends RecyclerView.Adapter<ConnectionsAdapter.
else
color = R.color.statusError;
statusInd.setTextColor(context.getResources().getColor(color));
blacklistedInd.setVisibility(conn.isBlacklisted() ? View.VISIBLE : View.INVISIBLE);
}
}

View File

@ -79,6 +79,7 @@ import java.util.Objects;
public class ConnectionsFragment extends Fragment implements ConnectionsListener, SearchView.OnQueryTextListener {
private static final String TAG = "ConnectionsFragment";
public static final String FILTER_EXTRA = "filter";
public static final String QUERY_EXTRA = "query";
private Handler mHandler;
private ConnectionsAdapter mAdapter;
private FloatingActionButton mFabDown;
@ -225,8 +226,13 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener
Intent intent = requireActivity().getIntent();
if(intent != null) {
search = intent.getStringExtra(FILTER_EXTRA);
FilterDescriptor filter = (FilterDescriptor) intent.getSerializableExtra(FILTER_EXTRA);
if(filter != null) {
mAdapter.mFilter = filter;
fromIntent = true;
}
search = intent.getStringExtra(QUERY_EXTRA);
if((search != null) && !search.isEmpty()) {
// Avoid hiding the interesting items
mAdapter.mFilter.showMasked = true;

View File

@ -73,6 +73,11 @@ public class ConnectionDescriptor implements Serializable {
public int incr_id;
public int status;
private int tcp_flags;
public boolean blacklisted_ip;
public boolean blacklisted_domain;
/* Internal */
public boolean alerted = false;
public ConnectionDescriptor(int _incr_id, int _ipver, int _ipproto, String _src_ip, String _dst_ip,
int _src_port, int _dst_port, int _uid, long when) {
@ -88,12 +93,15 @@ public class ConnectionDescriptor implements Serializable {
}
public void processUpdate(ConnectionUpdate update) {
// The "update_type" is used to limit the amount of data sent via the JNI
if((update.update_type & ConnectionUpdate.UPDATE_STATS) != 0) {
sent_bytes = update.sent_bytes;
rcvd_bytes = update.rcvd_bytes;
sent_pkts = update.sent_pkts;
rcvd_pkts = update.rcvd_pkts;
status = update.status;
status = (update.status & 0x00FF);
blacklisted_ip = (update.status & 0x0100) != 0;
blacklisted_domain = (update.status & 0x0200) != 0;
last_seen = update.last_seen;
tcp_flags = update.tcp_flags;
}
@ -156,6 +164,10 @@ public class ConnectionDescriptor implements Serializable {
return (tcp_flags & 0xFF);
}
public boolean isBlacklisted() {
return blacklisted_ip || blacklisted_domain;
}
@Override
public @NonNull String toString() {
return "[proto=" + ipproto + "/" + l7proto + "]: " + src_ip + ":" + src_port + " -> " +

View File

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

View File

@ -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;
}

View File

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

View File

@ -7,6 +7,7 @@ add_library(vpnproxy-jni SHARED
ip_lru.c
ndpi_master_protos.c
crc32.c
blacklist.c
pcap_utils.c)
# nDPI

View File

@ -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 <http://www.gnu.org/licenses/>.
*
* 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);
}

View File

@ -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

View File

@ -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));

View File

@ -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;
}
}

View File

@ -24,6 +24,7 @@
#include <stdbool.h>
#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;

View File

@ -0,0 +1,8 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="512" android:viewportWidth="512"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<!--
All brand icons are trademarks of their respective owners.
-->
<path android:fillColor="@android:color/white" android:pathData="M256,0C114.6,0 0,100.3 0,224c0,70.1 36.9,132.6 94.5,173.7 9.6,6.9 15.2,18.1 13.5,29.9l-9.4,66.2c-1.4,9.6 6,18.2 15.7,18.2L192,512v-56c0,-4.4 3.6,-8 8,-8h16c4.4,0 8,3.6 8,8v56h64v-56c0,-4.4 3.6,-8 8,-8h16c4.4,0 8,3.6 8,8v56h77.7c9.7,0 17.1,-8.6 15.7,-18.2l-9.4,-66.2c-1.7,-11.7 3.8,-23 13.5,-29.9C475.1,356.6 512,294.1 512,224 512,100.3 397.4,0 256,0zM160,320c-35.3,0 -64,-28.7 -64,-64s28.7,-64 64,-64 64,28.7 64,64 -28.7,64 -64,64zM352,320c-35.3,0 -64,-28.7 -64,-64s28.7,-64 64,-64 64,28.7 64,64 -28.7,64 -64,64z"/>
</vector>

View File

@ -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" />
<TextView
android:id="@+id/status_ind"
android:textSize="12sp"
android:minEms="3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
@ -54,6 +55,16 @@
tools:textColor="#FF28BC36"
tools:text="@string/conn_status_open" />
<ImageView
android:id="@+id/blacklisted"
android:layout_width="wrap_content"
android:layout_height="12sp"
app:layout_constraintEnd_toStartOf="@id/status_ind"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/status_ind"
app:tint="@color/colorTabText"
android:src="@drawable/ic_skull" />
<TextView
android:id="@+id/last_seen"
android:textSize="12sp"

View File

@ -15,7 +15,8 @@
<RelativeLayout
android:id="@+id/connections_mask"
android:layout_height="wrap_content"
android:layout_width="match_parent">
android:layout_width="match_parent"
android:layout_marginBottom="15dp">
<CheckBox
android:id="@+id/show_masked"
@ -30,12 +31,18 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginBottom="15dp"
android:text="@string/edit_rules"
style="?attr/materialButtonOutlinedStyle"
android:textColor="@color/colorTabText" />
</RelativeLayout>
<CheckBox
android:id="@+id/only_blacklisted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:text="@string/show_only_malicious" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@ -185,5 +185,10 @@
<string name="edit_rules">Edit Rules</string>
<string name="hidden_connections_rules">Hidden Connections Rules</string>
<string name="no_rules">No rules</string>
<string name="malicious_connection_app">Malicious connection detected (%1$s)</string>
<string name="show_only_malicious">Only malicious connections</string>
<string name="security">Security</string>
<string name="malware_detection">Malware Detection</string>
<string name="malware_detection_summary">"Detect possibly malicious connections and trigger alerts. NOTE: the detection can produce false positives and is not a valid alternative to an antivirus."</string>
</resources>

View File

@ -105,6 +105,15 @@
</Preference>
</PreferenceCategory>
<PreferenceCategory app:title="@string/security" app:iconSpaceReserved="false" app:key="security">
<SwitchPreference
app:key="malware_detection"
app:title="@string/malware_detection"
app:iconSpaceReserved="false"
app:summary="@string/malware_detection_summary"
app:defaultValue="false" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/other_prefs" app:iconSpaceReserved="false" >
<DropDownPreference
app:key="app_theme"