├── README.md ├── bro_typosquatting.bro ├── bro_beacons.bro └── bro_typosquatting_email.bro /README.md: -------------------------------------------------------------------------------- 1 | # Bro IDS Scripts 2 | 3 | Simply put, this is just a collection of bro scripts. 4 | 5 | ## bro_beacons.bro 6 | This is a script that will keep track (in the conn.log) of IP-IP connections. The time interval between connections will be measured against shannons entropy. If the entropy is low enough (a value that is configurable in the script) an log will be written of the beacon-like activity. 7 | 8 | ## bro_typosquatting.bro 9 | This script is a simple measure using a distance algorithm against a list of sites that are provided. An alert will fire when users hit sites that are slightly off. This could indicate that either a misspelling or typosquatted domain was found. 10 | 11 | ## bro_typosquatting_email.bro 12 | This script also uses a distance algorithm to measure domains found in the header that belong to senders against domains that belong to the recipients. A whitelist can be set, as well as a list of legitimate sites that you would like to monitor. 13 | -------------------------------------------------------------------------------- /bro_typosquatting.bro: -------------------------------------------------------------------------------- 1 | #Author: Nick Hoffman / securitykitten.github.io / @infoseckitten 2 | #Description: A bro script to find typosquatted domain names within DNS requests 3 | 4 | module TYPOSQUAT; 5 | 6 | export { 7 | redef enum Notice::Type += { Typosquat, }; 8 | const legit_domains: set [string] &redef; 9 | redef legit_domains = {"google.com","microsoft.com"}; 10 | } 11 | 12 | function typo_split(str: string): string { 13 | local vec = split_all(str,/\./); 14 | if (|vec| > 2) { 15 | local out = vec[|vec|-2] + vec[|vec|-1] + vec[|vec|]; 16 | return out; 17 | } 18 | return str; 19 | } 20 | 21 | function get_max_distance(str: string): double { 22 | return |str| * 0.2; 23 | } 24 | 25 | event dns_request(c: connection, msg: dns_msg, query: string, qtype: count, qclass: count) { 26 | local dist: double; 27 | local max_dist: double; 28 | local length: count = 0; 29 | local clean_query: string; 30 | clean_query = typo_split(query); 31 | for ( i in legit_domains ) { 32 | #Get the distance and maximum distance 33 | dist = levenshtein_distance(clean_query,i); 34 | max_dist = get_max_distance(clean_query); 35 | 36 | #Do a length check to make sure that they are a similar length 37 | if (|query| > |i|) 38 | length = |query| - |i|; 39 | else 40 | length = |i| - |query|; 41 | if (length > 3) 42 | next; 43 | 44 | #if all the tests pass, then lets fire the alert 45 | if ( 0 < dist && dist < max_dist) { 46 | NOTICE([$note=Typosquat, 47 | $msg = fmt("Request to typosquatted domain name %s",clean_query), 48 | $sub = fmt("Legitimate domain: %s",i), 49 | $conn=c]); 50 | 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /bro_beacons.bro: -------------------------------------------------------------------------------- 1 | #Author: Nick Hoffman / securitykitten.github.io / @infoseckitten 2 | #Description: A bro script to find beacons 3 | 4 | module BEACON; 5 | 6 | @load base/protocols/http 7 | 8 | #this is our master collection, we'll use this to store all our information 9 | global master_collection: table[addr,addr] of vector of time &synchronized; 10 | 11 | export { 12 | redef enum Log::ID += { LOG }; 13 | type Info: record { 14 | ts: time &log; 15 | #id: conn_id &log; 16 | local_host: addr &log; 17 | remote_host: addr &log; 18 | entropy: double &log; 19 | }; 20 | global log_beacon: event(rec: Info); 21 | 22 | # Add hosts to ignore with: 23 | # redef BEACON::whitelist += {192.168.0.1/32, 192.168.1.0/24} 24 | const whitelist: set [subnet] = set() &redef; 25 | 26 | } 27 | event bro_init() 28 | { 29 | Log::create_stream(BEACON::LOG, [$columns=Info, $ev=log_beacon]); 30 | } 31 | 32 | function calculate_entropy(host: addr, server: addr): double 33 | { 34 | local collection = master_collection[host,server]; 35 | local entropy: count; 36 | local length = |collection|; 37 | local intervals = vector(); 38 | local pmf: table[time] of double; 39 | local probs: table[time] of double; 40 | local sum: double; 41 | sum = 0; 42 | for (i in collection) { 43 | if ( i+1 >= length ) 44 | break; 45 | else { 46 | intervals[i] = double_to_interval(double_to_count(interval_to_double(collection[i+1] - collection[i]))); 47 | } 48 | } 49 | 50 | #i don't like this solution, oh well 51 | for (i in intervals) { 52 | if ( intervals[i] !in pmf ) 53 | pmf[intervals[i]] = 1; 54 | else 55 | pmf[intervals[i]] += 1; 56 | } 57 | #calculate the probabilities 58 | for (i in intervals) { 59 | probs[intervals[i]] = pmf[intervals[i]] / |intervals|; 60 | } 61 | for (k in probs) { 62 | sum += probs[k] * (log10(probs[k]) / log10(2.0)); 63 | } 64 | if (double_to_time(0.0) in probs) { 65 | if (probs[double_to_time(0.0)] > 0.3) 66 | sum = 4; 67 | } 68 | #debug statement 69 | #print fmt("host:%s,server:%s,entropy:%s,interval:%s",host,server,|sum|,intervals); 70 | return |sum|; 71 | } 72 | 73 | #we'll start with http posts, in the case that 74 | event http_request(c: connection, method: string, original_URI: string, unescaped_URI: string, version: string) { 75 | #declare variables 76 | local host: addr; 77 | local server: addr; 78 | local ts: time; 79 | local uid: string; 80 | local entropy_result: double; 81 | 82 | for (sn in whitelist) { 83 | if (c$id$resp_h in sn || c$id$orig_h in sn ) 84 | return; 85 | } 86 | 87 | if ( method == "POST" || method == "GET" ) { 88 | #grab the relevant information 89 | host = c$id$orig_h; 90 | server = c$id$resp_h; 91 | ts = c$start_time; 92 | uid = c$uid; 93 | if ( [host,server] !in master_collection ){ 94 | master_collection[host,server] = vector(ts) ; 95 | } 96 | else { 97 | master_collection[host,server][|master_collection[host,server]|] = ts; 98 | if ( |master_collection[host,server]| > 12) { 99 | entropy_result = calculate_entropy(host,server); 100 | if (entropy_result < 0.75 ) { 101 | print fmt("%s - beacon %s and %s", ts, host, server); 102 | local rec: BEACON::Info = [$ts=ts, $entropy=entropy_result,$local_host=host,$remote_host=server]; 103 | Log::write(BEACON::LOG, rec); 104 | } 105 | master_collection[host,server] = vector(); 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /bro_typosquatting_email.bro: -------------------------------------------------------------------------------- 1 | #Author: Nick Hoffman / securitykitten.github.io / @infoseckitten 2 | #Company: Morphick Inc. / @MorphickDefense 3 | #Description: A bro script to find typosquatted domain names within SMTP streams 4 | #Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License. 5 | 6 | @load base/frameworks/notice/main 7 | @load base/protocols/smtp/main 8 | 9 | module SMTP; 10 | 11 | export { 12 | redef enum Notice::Type += { Typosquat }; 13 | const whitelist: string_set &redef; 14 | const companylist: string_set &redef; 15 | 16 | ############################################################################################## 17 | # Whitelist is the list of legit sender domains that are triggering FP's and need to be tuned 18 | ############################################################################################## 19 | redef whitelist = set("@gmail.com","example.com"); 20 | 21 | ############################################################################################## 22 | # If there are additional company domains that you'd like to check for fuzzy match 23 | ############################################################################################## 24 | redef companylist = set("mycompany.com"); 25 | } 26 | function extract_email(email: string): string_set { 27 | local temp: string_set ; 28 | temp = find_all(email, /@[a-zA_Z0-9\.\-]{3,}/); 29 | #print fmt("%s",temp); 30 | return temp; 31 | } 32 | 33 | event log_smtp(rec: SMTP::Info) { 34 | local sender: string_set; 35 | local recipient: string_set; 36 | local dist: double; 37 | local max_dist: double; 38 | 39 | ########################################################################################### 40 | # Check the "Mail From" Field 41 | ########################################################################################### 42 | if ( rec?$mailfrom ) { 43 | for (i in extract_email(rec$mailfrom)) { 44 | if ( i !in sender ) { 45 | #print fmt("added sender %s",i); 46 | add sender[i]; 47 | } 48 | } 49 | } 50 | 51 | ########################################################################################### 52 | # Check the "From" Field 53 | ########################################################################################### 54 | if ( rec?$from ) { 55 | for (i in extract_email(rec$from)) { 56 | if ( i !in sender ) { 57 | #print fmt("added sender %s",i); 58 | add sender[i]; 59 | } 60 | } 61 | } 62 | 63 | ########################################################################################### 64 | # Check the "reply to" field 65 | ########################################################################################### 66 | if ( rec?$reply_to ) { 67 | for (i in extract_email(rec$reply_to)) { 68 | if ( i !in sender ) { 69 | #print fmt("added sender %s",i); 70 | add sender[i]; 71 | } 72 | } 73 | } 74 | 75 | ########################################################################################### 76 | # Check the "rcpt to" field 77 | ########################################################################################### 78 | if ( rec?$rcptto ) { 79 | for ( ppl in rec$rcptto ){ 80 | for ( person in extract_email(ppl) ) { 81 | if ( person !in recipient ) { 82 | #print fmt("added recipient %s",person); 83 | add recipient[person]; 84 | } 85 | } 86 | } 87 | } 88 | 89 | ############################################################################################ 90 | # Check the "to" field 91 | ############################################################################################ 92 | if ( rec?$to ) { 93 | for ( ppl in rec$to ){ 94 | for ( person in extract_email(ppl) ) { 95 | if ( person !in recipient ) { 96 | #print fmt("added recipient %s",person); 97 | add recipient[person]; 98 | } 99 | } 100 | } 101 | } 102 | 103 | ############################################################################################# 104 | # Iterate through both the senders and recipients checking for close distances 105 | ############################################################################################# 106 | local length: count = 0; 107 | for (i in recipient) { 108 | for (j in sender) { 109 | 110 | #next if length is too far off 111 | if (|j| > |i|) 112 | length = |j| - |i|; 113 | else 114 | length = |i| - |j|; 115 | if (length > 3) 116 | next; 117 | 118 | #next if in whitelist 119 | if (j in whitelist) 120 | next; 121 | 122 | #adjust distance based on length 123 | max_dist = |j| * 0.2; 124 | 125 | dist = levenshtein_distance(j,i); 126 | if ( 0 < dist && dist < max_dist) { 127 | #print fmt("%s,%s",i,j); 128 | NOTICE([$note=Typosquat, 129 | $msg = fmt("Email from to typosquatted domains %s to %s",i,j), 130 | $id=rec$id]); 131 | 132 | } 133 | } 134 | } 135 | 136 | ############################################################################################## 137 | # Iterate through the senders and see if any are within a distance from our company watchlist 138 | ############################################################################################## 139 | for (j in sender) { 140 | for (i in companylist){ 141 | dist = levenshtein_distance(j,i); 142 | max_dist = |j| * 0.2; 143 | if ( 0 < dist && dist < max_dist) { 144 | #print fmt("%s,%s",i,j); 145 | NOTICE([$note=Typosquat, 146 | $msg = fmt("Email from to typosquatted domains %s to %s",i,j), 147 | $id=rec$id]); 148 | 149 | } 150 | } 151 | } 152 | } 153 | --------------------------------------------------------------------------------