From bc5589d2e10b6d26fdc7f7d900aff5db28e65885 Mon Sep 17 00:00:00 2001 From: emanuele-f Date: Thu, 19 May 2022 12:20:07 +0200 Subject: [PATCH] 2nd-level domain rules now match all subdomains A rule like "example.com" is now interpreted as "*.example.com", so it will match all its sub-domains (e.g. "yet.another.example.com"). This is importantant, in particular, to match malware subdomains. --- .../com/emanuelef/remote_capture/Utils.java | 2 +- .../fragments/ConnectionsFragment.java | 19 +++++++--- .../remote_capture/model/MatchList.java | 37 +++++++++++-------- app/src/main/jni/core/blacklist.c | 31 +++++++++++++++- app/src/main/jni/tests/test/blacklist.c | 3 +- .../main/res/menu/connection_context_menu.xml | 8 +++- 6 files changed, 76 insertions(+), 24 deletions(-) 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 aed780a8..75367dc4 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/Utils.java +++ b/app/src/main/java/com/emanuelef/remote_capture/Utils.java @@ -797,7 +797,7 @@ public class Utils { } // a.example.org -> example.org - public static String getRootDomain(String domain) { + public static String getSecondLevelDomain(String domain) { int tldPos = domain.lastIndexOf("."); if(tldPos <= 0) diff --git a/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionsFragment.java b/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionsFragment.java index 11634419..0b712f4c 100644 --- a/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionsFragment.java +++ b/app/src/main/java/com/emanuelef/remote_capture/fragments/ConnectionsFragment.java @@ -351,11 +351,17 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener item.setVisible(true); String dm_clean = Utils.cleanDomain(conn.info); - String rootDomain = Utils.getRootDomain(dm_clean); + String rootDomain = Utils.getSecondLevelDomain(dm_clean); if(!rootDomain.equals(dm_clean)) { - item = menu.findItem(R.id.hide_root_domain); - item.setTitle(Utils.shorten(MatchList.getRuleLabel(ctx, RuleType.ROOT_DOMAIN, rootDomain), max_length)); + label = Utils.shorten(MatchList.getRuleLabel(ctx, RuleType.HOST, rootDomain), max_length); + + item = menu.findItem(R.id.hide_domain); + item.setTitle(label); + item.setVisible(true); + + item = menu.findItem(R.id.block_domain); + item.setTitle(label); item.setVisible(true); } @@ -426,8 +432,8 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener } else if(id == R.id.hide_proto) { mAdapter.mMask.addProto(conn.l7proto); mask_changed = true; - } else if(id == R.id.hide_root_domain) { - mAdapter.mMask.addRootDomain(Utils.getRootDomain(conn.info)); + } else if(id == R.id.hide_domain) { + mAdapter.mMask.addHost(Utils.getSecondLevelDomain(conn.info)); mask_changed = true; } else if(id == R.id.hide_country) { mAdapter.mMask.addCountry(conn.country); @@ -459,6 +465,9 @@ public class ConnectionsFragment extends Fragment implements ConnectionsListener } else if(id == R.id.block_host) { blocklist.addHost(conn.info); blocklist_changed = true; + } else if(id == R.id.block_domain) { + blocklist.addHost(Utils.getSecondLevelDomain(conn.info)); + blocklist_changed = true; } else if(id == R.id.open_app_details) { Intent intent = new Intent(requireContext(), AppDetailsActivity.class); intent.putExtra(AppDetailsActivity.APP_UID_EXTRA, conn.uid); 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 99585b9d..43b5a951 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 @@ -66,7 +66,6 @@ public class MatchList { APP, IP, HOST, - ROOT_DOMAIN, PROTOCOL, COUNTRY } @@ -146,7 +145,6 @@ public class MatchList { switch(tp) { case APP: resid = R.string.app_val; break; case IP: resid = R.string.ip_address_val; break; - case ROOT_DOMAIN: value = "*" + value; // fallthrough case HOST: resid = R.string.host_val; break; case PROTOCOL: resid = R.string.protocol_val; break; case COUNTRY: resid = R.string.country_val; break; @@ -196,18 +194,24 @@ public class MatchList { for(JsonElement el: ruleArray) { JsonObject ruleObj = el.getAsJsonObject(); + String typeStr = ruleObj.get("type").getAsString(); + String val = ruleObj.get("value").getAsString(); RuleType type; try { - type = RuleType.valueOf(ruleObj.get("type").getAsString()); + type = RuleType.valueOf(typeStr); } catch (IllegalArgumentException e) { - // can happen if format is changed, ignore - e.printStackTrace(); - continue; + // can happen if format is changed + if(typeStr.equals("ROOT_DOMAIN")) { + Log.i(TAG, String.format("ROOT_DOMAIN %s migrated", val)); + type = RuleType.HOST; + mFormatMigration = true; + } else { + e.printStackTrace(); + continue; + } } - String val = ruleObj.get("value").getAsString(); - // Handle migration from old uid-based format if(type == RuleType.APP) { try { @@ -241,7 +245,6 @@ public class MatchList { public void addIp(String ip) { addRule(new Rule(RuleType.IP, ip)); } public void addHost(String info) { addRule(new Rule(RuleType.HOST, Utils.cleanDomain(info))); } public void addProto(String proto) { addRule(new Rule(RuleType.PROTOCOL, proto)); } - public void addRootDomain(String domain) { addRule(new Rule(RuleType.ROOT_DOMAIN, domain)); } public void addCountry(String country_code) { addRule(new Rule(RuleType.COUNTRY, country_code)); } public void addApp(int uid) { @@ -325,11 +328,16 @@ public class MatchList { } public boolean matchesHost(String host) { - return mMatches.containsKey(matchKey(RuleType.HOST, Utils.cleanDomain(host))); - } + // Keep in sync with the native blacklist_match_domain + host = Utils.cleanDomain(host); - public boolean matchesRootDomain(String root_domain) { - return mMatches.containsKey(matchKey(RuleType.ROOT_DOMAIN, root_domain)); + // exact domain match + if(mMatches.containsKey(matchKey(RuleType.HOST, host))) + return true; + + // 2nd-level domain match + String domain = Utils.getSecondLevelDomain(host); + return !domain.equals(host) && mMatches.containsKey(matchKey(RuleType.HOST, domain)); } public boolean matchesCountry(String country_code) { @@ -345,8 +353,7 @@ public class MatchList { matchesIP(conn.dst_ip) || matchesProto(conn.l7proto) || matchesCountry(conn.country) || - (hasInfo && matchesHost(conn.info))) || - (hasInfo && matchesRootDomain(Utils.getRootDomain(conn.info))); + (hasInfo && matchesHost(conn.info))); } public Iterator iterRules() { diff --git a/app/src/main/jni/core/blacklist.c b/app/src/main/jni/core/blacklist.c index 068bc56c..f3daf4c0 100644 --- a/app/src/main/jni/core/blacklist.c +++ b/app/src/main/jni/core/blacklist.c @@ -17,6 +17,8 @@ * Copyright 2020-21 - Emanuele Faranda */ +#define _GNU_SOURCE +#include #include "pcapdroid.h" #include "common/utils.h" @@ -265,14 +267,41 @@ bool blacklist_match_ipstr(blacklist_t *bl, const char *ip_str) { /* ******************************************************* */ +static char* get_second_level_domain(const char *domain) { + char *dot = (char*) memrchr(domain, '.', strlen(domain)); + if(!dot || (dot == domain)) + return (char*)domain; + + dot = (char*) memrchr(domain, '.', dot - domain); + if(!dot) + return (char*)domain; + + return dot + 1; +} + +/* ******************************************************* */ + bool blacklist_match_domain(blacklist_t *bl, const char *domain) { + // Keep in sync with MatchList.matchesHost string_entry_t *entry = NULL; if(strncmp(domain, "www.", 4) == 0) domain += 4; + // exact domain match HASH_FIND_STR(bl->domains, domain, entry); - return(entry != NULL); + if(entry != NULL) + return true; + + // 2nd-level domain match + char *domain2 = get_second_level_domain(domain); + if(domain2 != domain) { + HASH_FIND_STR(bl->domains, domain2, entry); + if(entry != NULL) + return true; + } + + return false; } /* ******************************************************* */ diff --git a/app/src/main/jni/tests/test/blacklist.c b/app/src/main/jni/tests/test/blacklist.c index 623e7f04..f24715be 100644 --- a/app/src/main/jni/tests/test/blacklist.c +++ b/app/src/main/jni/tests/test/blacklist.c @@ -34,7 +34,8 @@ static void test_match() { // Use blacklist assert1(blacklist_match_domain(bl, "www.example.org")); - //assert1(blacklist_match_domain(bl, "some.example.org")); // TODO support subdomains matching + assert1(blacklist_match_domain(bl, "some.example.org")); + assert1(blacklist_match_domain(bl, "evil.some.example.org")); assert0(blacklist_match_ipstr(bl, "1.2.3.0")); assert1(blacklist_match_ipstr(bl, "1.2.3.4")); diff --git a/app/src/main/res/menu/connection_context_menu.xml b/app/src/main/res/menu/connection_context_menu.xml index 5f620d94..a74c4d95 100644 --- a/app/src/main/res/menu/connection_context_menu.xml +++ b/app/src/main/res/menu/connection_context_menu.xml @@ -24,7 +24,7 @@ android:visible="false" /> @@ -59,6 +59,12 @@ android:id="@+id/block_host" android:title="" tools:title="@string/host_val" /> + +