├── .gitattributes ├── .gitignore ├── dns2snort.py ├── readme.txt ├── sample.rules └── sampledns.txt /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /dns2snort.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Version 1.0 3 | ##Imports## 4 | #Argparse for fancy cli args 5 | #textwrap for a fancy help/description output 6 | 7 | import argparse 8 | import textwrap 9 | import re 10 | 11 | #Initialize argparse and print the big ass description and help usage block if -h or --help is used 12 | 13 | parser = argparse.ArgumentParser( 14 | formatter_class=argparse.RawDescriptionHelpFormatter, 15 | description=textwrap.dedent(''' 16 | dns2snort.py 17 | Brought to you by: 18 | @da_667 19 | With Special Thanks from: 20 | @botnet_hunter 21 | @3XPlo1T2 22 | --------------------- 23 | Generates DNS snort rules from a list of domains. 24 | Usage: dns2rule.py -i -o -s 1000000 25 | Infile format: 26 | www.evil.com 27 | Outfile format: 28 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS known malware domain www.evil.com"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|03|www|04|evil|03|com|00|"; fast_pattern:only; metadata:service dns; sid:1000000; rev:1;) 29 | ''')) 30 | 31 | #Infile, outfile, and sid arguments via ArgParse are all required. 32 | 33 | parser.add_argument('-i', dest="infile", required=True, 34 | help="The name of the file containing a list of Domains, One domain per line.") 35 | parser.add_argument('-o', dest="outfile", required=True, help="The name of the file to output your snort rules to.") 36 | parser.add_argument('-s', dest="sid", type=int, required=True, 37 | help="The snort sid to start numbering incrementally at. This number should be between 1000000 and 2000000.") 38 | parser.add_argument('-w', dest="www", required=False, action='store_true', help="Remove the 'www' subdomain from domains that have it.") 39 | args = parser.parse_args() 40 | 41 | #This is a small check to ensure -s is set to a valid value between one and two million - the local rules range. 42 | 43 | if args.sid < 1000000: 44 | print "The Value for sid (-s) is less than 1000000. Valid sid range is 1000000 to 2000000 (one million to two million)" 45 | exit() 46 | elif args.sid > 2000000: 47 | print "The Value for sid (-s) is greater than 2000000. Valid sid range is 1000000 to 2000000 (one million to two million)" 48 | exit() 49 | 50 | #fout is the file we will be outputting our rules to. 51 | #f is the file we will read a list of domains from. 52 | #This script iterates through each line (via for line loop) and splits on periods (.), creating a list for each line. 53 | #The script calculates the segments of the domain in question (can handle 1-4 segments -- e.g. .ru (1 segments, TLD) all the way to this.is.evil.ru (4 segments)) 54 | #Each segment of a domain has it's string length calculated and converted to hex. 55 | #If the segment is less than or equal to 0xf, this is converted to "0f" (padded with a zero, since snort rules expect this) 56 | #The hexidecmal letter is converted to upper case, and the rule is written to a file. 57 | #after the rule is written the SID number is incremented by 1 for the next rule. 58 | 59 | with open(args.outfile, 'w') as fout: 60 | with open(args.infile, 'r') as f: 61 | for line in f: 62 | domain = line.rstrip() 63 | if args.www == True: 64 | domain = re.sub('^www\.', '', domain, flags=re.IGNORECASE) 65 | segment = domain.split('.') 66 | #try/except fixes a bug with TLD rule creation where segment has 2 elements and element 0 is '' for some reason. 67 | try: 68 | segment.remove('') 69 | except ValueError: 70 | pass 71 | if len(segment) == 1: 72 | sega = (hex(len(segment[0])))[2:] 73 | if int(len(sega)) == 1: 74 | sega = "0%s" % sega 75 | rule = ("alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:\"BLACKLIST DNS domain %s\"; flow:to_server; byte_test:1,!&,0xF8,2; content:\"|%s|%s|00|\"; fast_pattern:only; metadata:service dns; sid:%s; rev:1;)\n" % (domain, sega.upper(), segment[0], args.sid)) 76 | fout.write(rule) 77 | print rule 78 | args.sid += 1 79 | elif len(segment) == 2: 80 | sega = (hex(len(segment[0])))[2:] 81 | if int(len(sega)) == 1: 82 | sega = "0%s" % sega 83 | segb = (hex(len(segment[1])))[2:] 84 | if int(len(segb)) == 1: 85 | segb = "0%s" % segb 86 | rule = ("alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:\"BLACKLIST DNS domain %s\"; flow:to_server; byte_test:1,!&,0xF8,2; content:\"|%s|%s|%s|%s|00|\"; fast_pattern:only; metadata:service dns; sid:%s; rev:1;)\n" % (domain, sega.upper(), segment[0], segb.upper(), segment[1], args.sid)) 87 | fout.write(rule) 88 | print rule 89 | args.sid += 1 90 | elif len(segment) == 3: 91 | sega = (hex(len(segment[0])))[2:] 92 | if int(len(sega)) == 1: 93 | sega = "0%s" % sega 94 | segb = (hex(len(segment[1])))[2:] 95 | if int(len(segb)) == 1: 96 | segb = "0%s" % segb 97 | segc = (hex(len(segment[2])))[2:] 98 | if int(len(segc)) == 1: 99 | segc = "0%s" % segc 100 | rule = ("alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:\"BLACKLIST DNS domain %s\"; flow:to_server; byte_test:1,!&,0xF8,2; content:\"|%s|%s|%s|%s|%s|%s|00|\"; fast_pattern:only; metadata:service dns; sid:%s; rev:1;)\n" % (domain, sega.upper(), segment[0], segb.upper(), segment[1], segc.upper(), segment[2], args.sid)) 101 | fout.write(rule) 102 | print rule 103 | args.sid += 1 104 | elif len(segment) == 4: 105 | sega = (hex(len(segment[0])))[2:] 106 | if int(len(sega)) == 1: 107 | sega = "0%s" % sega 108 | segb = (hex(len(segment[1])))[2:] 109 | if int(len(segb)) == 1: 110 | segb = "0%s" % segb 111 | segc = (hex(len(segment[2])))[2:] 112 | if int(len(segc)) == 1: 113 | segc = "0%s" % segc 114 | segd = (hex(len(segment[3])))[2:] 115 | if int(len(segd)) == 1: 116 | segd = "0%s" % segd 117 | rule = ("alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:\"BLACKLIST DNS domain %s\"; flow:to_server; byte_test:1,!&,0xF8,2; content:\"|%s|%s|%s|%s|%s|%s|%s|%s|00|\"; fast_pattern:only; metadata:service dns; sid:%s; rev:1;)\n" % (domain, sega.upper(), segment[0], segb.upper(), segment[1], segc.upper(), segment[2], segd.upper(), segment[3], args.sid)) 118 | print rule 119 | fout.write(rule) 120 | args.sid += 1 121 | else: 122 | print "the number of segments in the domain %s is greater than 4. Skipping." % domain 123 | pass 124 | exit() 125 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | dns2snort.py 2 | -by da_667 3 | -with code contributions from @botnet_hunter and @3XPlo1T2 4 | 5 | Purpose: Given a file containing a list of fully qualified DNS domains, generate snort rules for those domains. Incredibly useful if you are sitting on top of a pile of IOCs, but want an efficiently lazy way to generate snort sigs for them. 6 | Requires: argparse, textwrap, python 2.7 7 | Tested on: OSX, Ubuntu, Debian 8 | 9 | Options (INFILE, OUTFILE, and SID arguments are all required!): 10 | -i : input file. Point the script to the file that contains the list of domains (one domain per line) 11 | -o : output file: File to output your new snort rules 12 | -s : SID. This is integer value that must be between 1000000 and 2000000 (one million and two million) 13 | -w : Remove the 'www' subdomain from domains that have it. 14 | -h : prints out the most badass help message you've ever seen. 15 | 16 | This script supports TLDS, and FQDNS up to four subdomains deep - see the example below, in addition to the sampledns.txt and sample.rules files provided with this script. 17 | 18 | Example input: 19 | 20 | .pw 21 | evilcorp.co 22 | www.evil.com 23 | seemstoteslegit.notreally.tk 24 | stupidlylongsubdomain.lol.wutski.biz 25 | 26 | ..becomes: 27 | 28 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS domain .pw"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|02|pw|00|"; fast_pattern:only; metadata:service dns; sid:1000000; rev:1;) 29 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS domain evilcorp.co"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|08|evilcorp|02|co|00|"; fast_pattern:only; metadata:service dns; sid:1000001; rev:1;) 30 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS domain www.evil.com"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|03|www|04|evil|03|com|00|"; fast_pattern:only; metadata:service dns; sid:1000002; rev:1;) 31 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS domain seemstoteslegit.notreally.tk"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|0F|seemstoteslegit|09|notreally|02|tk|00|"; fast_pattern:only; metadata:service dns; sid:1000003; rev:1;) 32 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS known malware domain stupidlylongsubdomain.lol.wutski.biz"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|15|stupidlylongsubdomain|03|lol|06|wutski|03|biz|00|"; fast_pattern:only; metadata:service dns; sid:1000004; rev:1;) 33 | 34 | These are all properly formatted snort rules ready to be pushed to a sensor. 35 | 36 | Notes: 37 | - Please note that none of these sample domains are malicious. They are samples for testing only. 38 | - The domain list input file should not have ANY trailing spaces on any of the individual user-agent lines. Additionally, there should be ZERO blank lines in the domain list file that will be used to generate the snort rules. If the script encounters any FQDNs greater than 4 subdomains deep (such as "lol.wut.you.doin.it [5-level subdomain]") the script will skip over them, notify the user, and print it to the terminal. 39 | - If you think this is a concern, the Mandiant APT1 report has over 2000 domains as IOCs associated with the campaign. dns2snort only encountered two FQDNs with 5+ subdomains; dns2snort successfully created rules for every other domain in the list of IOCs. Maybe you can look over my code and implement a way to support FQDNs of varying length? I'm not good enough at python to do this currently without a huge CF of if/then statements. Help is welcome. -------------------------------------------------------------------------------- /sample.rules: -------------------------------------------------------------------------------- 1 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS domain .pw"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|02|pw|00|"; fast_pattern:only; metadata:service dns; sid:1000000; rev:1;) 2 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS domain evilcorp.co"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|08|evilcorp|02|co|00|"; fast_pattern:only; metadata:service dns; sid:1000001; rev:1;) 3 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS domain www.evil.com"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|03|www|04|evil|03|com|00|"; fast_pattern:only; metadata:service dns; sid:1000002; rev:1;) 4 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS domain seemstoteslegit.notreally.tk"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|0F|seemstoteslegit|09|notreally|02|tk|00|"; fast_pattern:only; metadata:service dns; sid:1000003; rev:1;) 5 | alert udp $HOME_NET any -> $EXTERNAL_NET 53 (msg:"BLACKLIST DNS known malware domain stupidlylongsubdomain.lol.wutski.biz"; flow:to_server; byte_test:1,!&,0xF8,2; content:"|15|stupidlylongsubdomain|03|lol|06|wutski|03|biz|00|"; fast_pattern:only; metadata:service dns; sid:1000004; rev:1;) -------------------------------------------------------------------------------- /sampledns.txt: -------------------------------------------------------------------------------- 1 | .pw 2 | evilcorp.co 3 | www.evil.com 4 | seemstoteslegit.notreally.tk 5 | stupidlylongsubdomain.lol.wutski.biz --------------------------------------------------------------------------------