Skip to content

Commit

Permalink
Move LMS scoring under new Domain Heuristics
Browse files Browse the repository at this point in the history
  • Loading branch information
ppacher committed Aug 11, 2020
1 parent 85e4bea commit 3b896ee
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 16 deletions.
13 changes: 6 additions & 7 deletions detection/dga/lms.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import (
"strings"
)

// LmsScoreOfDomain calculates the mean longest meaningful substring of a domain. It follows some special rules to increase accuracy. It returns a value between 0 and 100, representing the length-based percentage of the meaningful substring.
// LmsScoreOfDomain calculates the mean longest meaningful substring of a domain.
// It follows some special rules to increase accuracy. It returns a value between
// 0 and 100, representing the length-based percentage of the meaningful substring.
func LmsScoreOfDomain(domain string) float64 {
var totalScore float64
domain = strings.ToLower(domain)
subjects := strings.Split(domain, ".")
// ignore the last two parts
if len(subjects) <= 3 {
return 100
}
subjects = subjects[:len(subjects)-3]
var totalLength int
for _, subject := range subjects {
totalLength += len(subject)
Expand All @@ -27,7 +24,9 @@ func LmsScoreOfDomain(domain string) float64 {
return totalScore
}

// LmsScore calculates the longest meaningful substring of a domain. It returns a value between 0 and 100, representing the length-based percentage of the meaningful substring.
// LmsScore calculates the longest meaningful substring of a domain. It returns a
// value between 0 and 100, representing the length-based percentage of the
// meaningful substring.
func LmsScore(subject string) float64 {
lmsStart := -1
lmsStop := -1
Expand Down
4 changes: 2 additions & 2 deletions detection/dga/lms_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import "testing"
func TestLmsScoreOfDomain(t *testing.T) {
testDomain(t, "g.symcd.com.", 100, 100)
testDomain(t, "www.google.com.", 100, 100)
testDomain(t, "55ttt5.12abc3.test.com.", 50, 50)
testDomain(t, "mbtq6opnuodp34gcrma65fxacgxv5ukr7lq6xuhr4mhoibe7.yvqptrozfbnqyemchpovw3q5xwjibuxfsgb72mix3znhpfhc.i2n7jh2gadqaadck3zs3vg3hbv5pkmwzeay4gc75etyettbb.isi5mhmowtfriu33uxzmgvjur5g2p3tloynwohfrggee6fkn.meop7kqyd5gwxxa3.er.spotify.com.", 0, 30)
testDomain(t, "55ttt5.12abc3.test.com.", 68, 69)
testDomain(t, "mbtq6opnuodp34gcrma65fxacgxv5ukr7lq6xuhr4mhoibe7.yvqptrozfbnqyemchpovw3q5xwjibuxfsgb72mix3znhpfhc.i2n7jh2gadqaadck3zs3vg3hbv5pkmwzeay4gc75etyettbb.isi5mhmowtfriu33uxzmgvjur5g2p3tloynwohfrggee6fkn.meop7kqyd5gwxxa3.er.spotify.com.", 0, 31)
}

func testDomain(t *testing.T, domain string, min, max float64) {
Expand Down
59 changes: 52 additions & 7 deletions firewall/master.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"github.com/safing/portmaster/detection/dga"
"github.com/safing/portmaster/netenv"
"golang.org/x/net/publicsuffix"

"github.com/safing/portbase/log"
"github.com/safing/portmaster/network"
Expand Down Expand Up @@ -59,7 +60,7 @@ func DecideOnConnection(ctx context.Context, conn *network.Connection, pkt packe
checkBypassPrevention,
checkFilterLists,
checkInbound,
checkLMSScore,
checkDomainHeuristics,
checkDefaultPermit,
checkAutoPermitRelated,
checkDefaultAction,
Expand Down Expand Up @@ -283,19 +284,63 @@ func checkFilterLists(ctx context.Context, conn *network.Connection, pkt packet.
return false
}

func checkLMSScore(ctx context.Context, conn *network.Connection, _ packet.Packet) bool {
func checkDomainHeuristics(ctx context.Context, conn *network.Connection, _ packet.Packet) bool {
p := conn.Process().Profile()

if !p.DomainHeuristics() {
return false
}

if conn.Entity.Domain == "" {
return false
}

// check for possible DNS tunneling / data transmission
lms := dga.LmsScoreOfDomain(conn.Entity.Domain)
if lms < 10 {
log.Tracer(ctx).Warningf("nameserver: possible data tunnel by %s: %s has lms score of %f, returning nxdomain", conn.Process(), conn.Entity.Domain, lms)
conn.BlockWithContext("Possible data tunnel", conn.ReasonContext)
trimmedDomain := strings.TrimRight(conn.Entity.Domain, ".")
etld1, err := publicsuffix.EffectiveTLDPlusOne(trimmedDomain)
if err != nil {
// we don't apply any checks here and let the request through
// because a malformed domain-name will likely be dropped by
// checks better suited for that.
log.Tracer(ctx).Warningf("nameserver: failed to get eTLD+1: %s", err)
return false
}

domainToCheck := strings.Split(etld1, ".")[0]
score := dga.LmsScore(domainToCheck)
if score < 5 {
log.Tracer(ctx).Warningf(
"nameserver: possible data tunnel by %s in eTLD+1 %s: %s has an lms score of %.2f, returning nxdomain",
conn.Process(),
etld1,
domainToCheck,
score,
)
conn.Block("Possible data tunnel")
return true
}
log.Tracer(ctx).Infof("LMS score of eTLD+1 %s is %.2f", etld1, score)

// 100 is a somewhat arbitrary threshold to ensure we don't mess
// around with CDN domain names to early. They use short second-level
// domains that would trigger LMS checks but are to small to actually
// exfiltrate data.
if len(conn.Entity.Domain) > len(etld1)+100 {
domainToCheck = trimmedDomain[0:len(etld1)]
score := dga.LmsScoreOfDomain(domainToCheck)
if score < 10 {
log.Tracer(ctx).Warningf(
"nameserver: possible data tunnel by %s in subdomain %s: %s has an lms score of %.2f, returning nxdomain",
conn.Process(),
conn.Entity.Domain,
domainToCheck,
score,
)
conn.Block("Possible data tunnel")
return true
}
log.Tracer(ctx).Infof("LMS score of entire domain is %.2f", score)
}

return false
}

Expand Down
22 changes: 22 additions & 0 deletions profile/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ var (
cfgOptionRemoveBlockedDNS config.IntOption // security level option
cfgOptionRemoveBlockedDNSOrder = 113

CfgOptionDomainHeuristicsKey = "filter/domainHeuristics"
cfgOptionDomainHeuristics config.IntOption // security level option
cfgOptionDomainHeuristicsOrder = 114

// Permanent Verdicts Order = 128
)

Expand Down Expand Up @@ -378,6 +382,24 @@ Examples:
cfgOptionRemoveBlockedDNS = config.Concurrent.GetAsInt(CfgOptionRemoveBlockedDNSKey, int64(status.SecurityLevelsAll))
cfgIntOptions[CfgOptionRemoveBlockedDNSKey] = cfgOptionRemoveBlockedDNS

// Domain heuristics
err = config.Register(&config.Option{
Name: "Enable Domain Heuristics",
Key: CfgOptionDomainHeuristicsKey,
Description: "Domain Heuristics checks for suspicious looking domain names and blocks them. Ths option currently targets domains generated by malware and DNS data tunnels.",
Order: cfgOptionDomainHeuristicsOrder,
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelExpert,
ExternalOptType: "security level",
DefaultValue: status.SecurityLevelsAll,
ValidationRegex: "^(0|4|6|7)$",
})
if err != nil {
return err
}
cfgOptionDomainHeuristics = config.Concurrent.GetAsInt(CfgOptionDomainHeuristicsKey, int64(status.SecurityLevelsAll))

// Bypass prevention
err = config.Register(&config.Option{
Name: "Prevent Bypassing",
Key: CfgOptionPreventBypassingKey,
Expand Down
5 changes: 5 additions & 0 deletions profile/profile-layered.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type LayeredProfile struct {
FilterSubDomains config.BoolOption
FilterCNAMEs config.BoolOption
PreventBypassing config.BoolOption
DomainHeuristics config.BoolOption
}

// NewLayeredProfile returns a new layered profile based on the given local profile.
Expand Down Expand Up @@ -108,6 +109,10 @@ func NewLayeredProfile(localProfile *Profile) *LayeredProfile {
CfgOptionPreventBypassingKey,
cfgOptionPreventBypassing,
)
new.DomainHeuristics = new.wrapSecurityLevelOption(
CfgOptionDomainHeuristicsKey,
cfgOptionDomainHeuristics,
)

// TODO: load linked profiles.

Expand Down

0 comments on commit 3b896ee

Please sign in to comment.