├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── runzero-dns │ ├── deploy.sh │ ├── main.go │ └── runzero-dns.service ├── runzero-dnsrp │ └── main.go ├── runzero-extractors │ └── bin │ │ ├── extract-bridges.rb │ │ └── extract-urls.rb └── runzero-smb2-sessions │ ├── README.md │ └── main.go ├── go.mod ├── go.sum └── pkg └── rnd ├── bytes.go ├── counter_predictor.go ├── ip.go ├── ports.go ├── smb.go ├── strings.go └── time.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "11:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2022 runZero, Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # runZero Open Source 2 | 3 | This repository contains open source tools, libraries, and datasets related to the 4 | [runZero](https://runzero.com) product and associated research. 5 | -------------------------------------------------------------------------------- /cmd/runzero-dns/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SERVER=$1 4 | 5 | if [ "$1" == "" ]; then 6 | echo "usage: deploy.sh [server-name]" 7 | exit 1 8 | fi 9 | 10 | set -x 11 | 12 | export GOOS=linux 13 | export GOARCH=amd64 14 | go build -o runzero-dns || exit 15 | 16 | # Ubuntu systemd must be disabled to run this 17 | # sudo systemctl disable systemd-resolved.service 18 | # sudo systemctl stop systemd-resolved 19 | 20 | ssh root@${SERVER} 'rm -f /usr/local/bin/runzero-dns' && \ 21 | scp ./runzero-dns root@${SERVER}:/usr/local/bin/runzero-dns && \ 22 | scp ./runzero-dns.service root@${SERVER}:/lib/systemd/system/runzero-dns.service && \ 23 | ssh root@${SERVER} 'chmod 755 /usr/local/bin/runzero-dns; systemctl daemon-reload; systemctl enable runzero-dns.service; systemctl restart runzero-dns' 24 | 25 | -------------------------------------------------------------------------------- /cmd/runzero-dns/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018-2019 runZero, Inc 2 | // 3 | // 4 | // Derived from reflect.go via github.com/miekg/exdns 5 | // 6 | 7 | package main 8 | 9 | import ( 10 | "bytes" 11 | "encoding/binary" 12 | "encoding/hex" 13 | "flag" 14 | "fmt" 15 | "net" 16 | "os" 17 | "os/signal" 18 | "runtime" 19 | "runtime/pprof" 20 | "strconv" 21 | "strings" 22 | "syscall" 23 | "time" 24 | 25 | "github.com/runZeroInc/runzero-tools/pkg/rnd" 26 | 27 | log "github.com/sirupsen/logrus" 28 | 29 | "github.com/miekg/dns" 30 | ) 31 | 32 | // encodedClientSubnet is used to decode CNAME-encoded subnet data 33 | type encodedClientSubnet struct { 34 | Family uint16 35 | Code uint16 36 | Netmask uint8 37 | Scope uint8 38 | Address [16]byte 39 | } 40 | 41 | var ( 42 | cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file") 43 | compress = flag.Bool("compress", false, "compress replies") 44 | tsig = flag.String("tsig", "", "use MD5 hmac tsig: keyname:base64") 45 | cpu = flag.Int("cpu", 0, "number of cores to use") 46 | port = flag.Int("port", 53, "port number to listen on") 47 | subdomain = flag.String("subdomain", "v1.nxdomain.us", "subdomain handled by runzero-dns") 48 | ) 49 | 50 | var helperDomain string 51 | 52 | func handleReflect(w dns.ResponseWriter, r *dns.Msg) { 53 | var ( 54 | v4 bool 55 | rr dns.RR 56 | port string 57 | a net.IP 58 | ) 59 | 60 | m := new(dns.Msg) 61 | m.SetReply(r) 62 | m.Compress = *compress 63 | if ip, ok := w.RemoteAddr().(*net.UDPAddr); ok { 64 | port = strconv.Itoa(ip.Port) + "/udp" 65 | a = ip.IP 66 | v4 = a.To4() != nil 67 | } 68 | if ip, ok := w.RemoteAddr().(*net.TCPAddr); ok { 69 | port = strconv.Itoa(ip.Port) + "/tcp" 70 | a = ip.IP 71 | v4 = a.To4() != nil 72 | } 73 | 74 | if len(r.Question) == 0 { 75 | log.Printf("%s:%s requested no questions", a, port) 76 | return 77 | } 78 | 79 | log.Printf("%s:%s requested %s (type:%d/class:%d) with XID %d", a, port, r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass, r.Id) 80 | 81 | prefix := strings.ToLower(r.Question[0].Name[0:2]) 82 | switch prefix { 83 | // T0: Return the source address of the resolver in the response 84 | // Handles A, AAAA, and TXT query types. 85 | case "t0": 86 | decodeAndLogTracer(a.String(), port, r) 87 | 88 | if v4 { 89 | rr = &dns.A{ 90 | Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, 91 | A: a.To4(), 92 | } 93 | } else { 94 | rr = &dns.AAAA{ 95 | Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 60}, 96 | AAAA: a, 97 | } 98 | } 99 | 100 | t := &dns.TXT{ 101 | Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeTXT, Class: dns.ClassCHAOS, Ttl: 60}, 102 | Txt: []string{fmt.Sprintf("%s:%s", a.String(), port)}, 103 | } 104 | 105 | switch r.Question[0].Qtype { 106 | case dns.TypeTXT: 107 | m.Answer = append(m.Answer, t) 108 | m.Extra = append(m.Extra, rr) 109 | default: 110 | fallthrough 111 | case dns.TypeAAAA, dns.TypeA: 112 | m.Answer = append(m.Answer, rr) 113 | m.Extra = append(m.Extra, t) 114 | } 115 | 116 | // E0: Return an encoded EDNS0 Client Subnet field as a CNAME 117 | case "e0": 118 | dk, ok := decodeAndLogTracer(a.String(), port, r) 119 | o := r.IsEdns0() 120 | if ok && o != nil { 121 | for _, s := range o.Option { 122 | switch s.(type) { 123 | case *dns.EDNS0_SUBNET: 124 | subnet := s.(*dns.EDNS0_SUBNET) 125 | 126 | // Get the decode key as a set of bytes 127 | dkb := make([]byte, 4) 128 | binary.BigEndian.PutUint32(dkb, dk) 129 | 130 | // Use a helper struct to encode the fields 131 | csInfo := encodedClientSubnet{ 132 | Family: subnet.Family, 133 | Netmask: subnet.SourceNetmask, 134 | Scope: subnet.SourceScope, 135 | Code: subnet.Code, 136 | } 137 | 138 | // Store the address if the length is right 139 | if len([]byte(subnet.Address)) == 16 { 140 | copy(csInfo.Address[:], subnet.Address) 141 | } 142 | 143 | buf := new(bytes.Buffer) 144 | binary.Write(buf, binary.BigEndian, csInfo) 145 | 146 | rr = &dns.CNAME{ 147 | Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeCNAME, Class: dns.ClassINET, Ttl: 60}, 148 | Target: fmt.Sprintf("c0%.8x%s.%s", dk, hex.EncodeToString(rnd.XorBytesWithBytes(buf.Bytes(), dkb)), helperDomain), 149 | } 150 | m.Answer = append(m.Answer, rr) 151 | } 152 | } 153 | } 154 | if len(m.Answer) == 0 { 155 | m.Rcode = 3 156 | } 157 | 158 | // A0: Return an A or AAAA pointing to the encoded address 159 | case "a0": 160 | rr, err := decodeAndLogAddress(a.String(), port, strings.ToLower(r.Question[0].Name), r) 161 | if err != nil { 162 | log.Printf("%s:%s returned error for %s: %s", a, port, r.Question[0].Name, err) 163 | return 164 | } 165 | 166 | m.Answer = append(m.Answer, rr) 167 | 168 | default: 169 | // S0 and .S0 : Return a NS record for the encoded addresses 170 | if idx := strings.Index(strings.ToLower(r.Question[0].Name), "s0"); idx != -1 { 171 | 172 | // Create an A0 query pointing to the target address 173 | nsName := "a" + strings.ToLower(string(r.Question[0].Name[idx+1:])) 174 | m.Authoritative = true 175 | rr = &dns.NS{ 176 | Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeNS, Class: dns.ClassINET, Ttl: 60}, 177 | Ns: nsName, 178 | } 179 | m.Ns = append(m.Ns, rr) 180 | 181 | // Append the matching A or AAAA record 182 | rr, err := decodeAndLogAddress(a.String(), port, nsName, r) 183 | if err != nil { 184 | log.Printf("%s:%s returned error for %s: %s", a, port, nsName, err) 185 | return 186 | } 187 | m.Extra = append(m.Extra, rr) 188 | 189 | } 190 | } 191 | 192 | if r.IsTsig() != nil { 193 | if w.TsigStatus() == nil { 194 | m.SetTsig(r.Extra[len(r.Extra)-1].(*dns.TSIG).Hdr.Name, dns.HmacMD5, 300, time.Now().UTC().Unix()) 195 | } else { 196 | log.Printf("%s:%s triggered tsig error for %s: %s", a, port, r.Question[0].Name, w.TsigStatus().Error()) 197 | } 198 | } 199 | 200 | err := w.WriteMsg(m) 201 | if err != nil { 202 | log.Printf("%s:%s triggered error for %s: %s", a, port, r.Question[0].Name, err) 203 | } 204 | } 205 | 206 | func decodeAndLogTracer(ip string, port string, r *dns.Msg) (uint32, bool) { 207 | var tracerDecodeKey uint32 208 | encodedName := strings.SplitN(r.Question[0].Name[2:], ".", 2) 209 | encodedBytes, err := hex.DecodeString(encodedName[0]) 210 | if err != nil { 211 | log.Printf("%s:%s requested invalid tracer name %s wth XID %d (%s)", ip, port, r.Question[0].Name, r.Id, err) 212 | return tracerDecodeKey, false 213 | } 214 | 215 | // [XXXX] [AAAABBBBCCCCDDDD] [YYYYZZZZ] 216 | if len(encodedBytes) != 28 { 217 | log.Printf("%s:%s requested invalid tracer name length %s wth XID %d", ip, port, r.Question[0].Name, r.Id) 218 | return tracerDecodeKey, false 219 | } 220 | 221 | tracerDecodeKey = binary.BigEndian.Uint32(encodedBytes[0:4]) 222 | decodedBytes := rnd.XorBytesWithBytes(encodedBytes[4:], encodedBytes[0:4]) 223 | tracerIP := net.IP(decodedBytes[0:16]) 224 | tracerTS := binary.BigEndian.Uint64(decodedBytes[16:24]) 225 | 226 | log.Printf("%s:%s requested trace %s (type:%d/class:%d) with XID %d (ip:%s ts:%s)", 227 | ip, port, r.Question[0].Name, r.Question[0].Qtype, r.Question[0].Qclass, r.Id, 228 | tracerIP.String(), time.Unix(0, int64(tracerTS)).UTC().String(), 229 | ) 230 | return tracerDecodeKey, true 231 | } 232 | 233 | func decodeAndLogAddress(ip string, port string, qname string, r *dns.Msg) (dns.RR, error) { 234 | encodedName := strings.SplitN(qname, ".", 2) 235 | 236 | if len(encodedName) != 2 { 237 | return &dns.A{}, fmt.Errorf("invalid subdomain name (dots=%d)", len(encodedName)) 238 | } 239 | 240 | if len(encodedName[0]) != 58 { 241 | return &dns.A{}, fmt.Errorf("invalid subdomain length (%d)", len(encodedName[0])) 242 | } 243 | 244 | encodedBytes, err := hex.DecodeString(encodedName[0][2:]) 245 | if err != nil { 246 | return &dns.A{}, fmt.Errorf("invalid subdomain name (%s)", err) 247 | } 248 | 249 | // [XXXX] [AAAABBBBCCCCDDDD] [YYYYZZZZ] 250 | if len(encodedBytes) != 28 { 251 | return &dns.A{}, fmt.Errorf("invalid subdomain name length (%d): #%v", len(encodedBytes), encodedBytes) 252 | } 253 | 254 | decodedBytes := rnd.XorBytesWithBytes(encodedBytes[4:], encodedBytes[0:4]) 255 | tracerIP := net.IP(decodedBytes[0:16]) 256 | tracerTS := binary.BigEndian.Uint64(decodedBytes[16:24]) 257 | 258 | log.Printf("%s:%s requested referral %s (type:%d/class:%d) with XID %d (ip:%s ts:%s)", 259 | ip, port, qname, r.Question[0].Qtype, r.Question[0].Qclass, r.Id, 260 | tracerIP.String(), time.Unix(0, int64(tracerTS)).UTC().String(), 261 | ) 262 | 263 | // Handle IPv4 referrals 264 | if tracerIP.To4() != nil { 265 | return &dns.A{ 266 | Hdr: dns.RR_Header{Name: qname, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60}, 267 | A: tracerIP, 268 | }, nil 269 | } 270 | 271 | // Handle IPv6 referrals 272 | return &dns.AAAA{ 273 | Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 60}, 274 | AAAA: tracerIP, 275 | }, nil 276 | } 277 | 278 | func serveDNS(net, name, secret string, soreuseport bool) { 279 | switch name { 280 | case "": 281 | server := &dns.Server{Addr: fmt.Sprintf("[::]:%d", *port), Net: net, TsigSecret: nil} 282 | if err := server.ListenAndServe(); err != nil { 283 | log.Printf("failed to setup the "+net+" server: %s", err.Error()) 284 | os.Exit(1) 285 | } 286 | default: 287 | server := &dns.Server{Addr: fmt.Sprintf(":%d", *port), Net: net, TsigSecret: map[string]string{name: secret}} 288 | if err := server.ListenAndServe(); err != nil { 289 | log.Printf("failed to setup the "+net+" server: %s", err.Error()) 290 | os.Exit(1) 291 | } 292 | } 293 | } 294 | 295 | func main() { 296 | var name, secret string 297 | flag.Usage = func() { 298 | flag.PrintDefaults() 299 | } 300 | flag.Parse() 301 | 302 | log.SetFormatter(&log.TextFormatter{ 303 | DisableColors: true, 304 | FullTimestamp: true, 305 | }) 306 | log.SetOutput(os.Stdout) 307 | 308 | if *tsig != "" { 309 | a := strings.SplitN(*tsig, ":", 2) 310 | name, secret = dns.Fqdn(a[0]), a[1] // fqdn the name, which everybody forgets... 311 | } 312 | if *cpuprofile != "" { 313 | f, err := os.Create(*cpuprofile) 314 | if err != nil { 315 | log.Fatal(err) 316 | } 317 | pprof.StartCPUProfile(f) 318 | defer pprof.StopCPUProfile() 319 | } 320 | 321 | if *cpu != 0 { 322 | runtime.GOMAXPROCS(*cpu) 323 | } else { 324 | runtime.GOMAXPROCS(runtime.NumCPU()) 325 | } 326 | 327 | helperDomain = rnd.EnsureTrailingDot(*subdomain) 328 | 329 | log.Printf("runzero-dns-server starting on port %d", *port) 330 | 331 | dns.HandleFunc(helperDomain, handleReflect) 332 | go serveDNS("tcp", name, secret, false) 333 | go serveDNS("udp", name, secret, false) 334 | 335 | sig := make(chan os.Signal) 336 | signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) 337 | s := <-sig 338 | log.Printf("signal (%s) received, stopping", s) 339 | } 340 | -------------------------------------------------------------------------------- /cmd/runzero-dns/runzero-dns.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=runzero-dns 3 | After=multi-user.target 4 | 5 | [Service] 6 | Type=idle 7 | ExecStart=/usr/local/bin/runzero-dns 8 | StandardOutput=syslog 9 | 10 | [Install] 11 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /cmd/runzero-dnsrp/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (C) 2018-2020 runZero, Inc 4 | 5 | DNS Remote Ping 6 | =============== 7 | 8 | Use a runzero DNS server to identify hosts reachable by an open resolver. 9 | 10 | Usage: 11 | 12 | $ runzero-dnsrp -quiet 192.168.0.3 192.168.30.0/24 13 | 192.168.30.29 alive via 192.168.0.3:53 60ms code:2 14 | 192.168.30.34 alive via 192.168.0.3:53 69ms code:2 15 | 192.168.30.143 alive via 192.168.0.3:53 267ms code:2 16 | 17 | */ 18 | 19 | package main 20 | 21 | import ( 22 | "encoding/hex" 23 | "flag" 24 | "fmt" 25 | "math/rand" 26 | "net" 27 | "os" 28 | "runtime" 29 | "sync" 30 | "time" 31 | 32 | "github.com/runZeroInc/runzero-tools/pkg/rnd" 33 | 34 | "github.com/miekg/dns" 35 | ) 36 | 37 | var ( 38 | port = flag.Int("port", 53, "port number to send queries to") 39 | threads = flag.Int("threads", runtime.NumCPU(), "number of parallel threads") 40 | subdomain = flag.String("subdomain", "helper.rumble.network", "subdomain handled by runzero-dns") 41 | quiet = flag.Bool("quiet", false, "quiet mode, only show positive results") 42 | help = flag.Bool("help", false, "show usage information") 43 | h = flag.Bool("h", false, "show usage information") 44 | ) 45 | 46 | func main() { 47 | 48 | flag.Parse() 49 | 50 | if len(flag.Args()) < 2 { 51 | fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) 52 | flag.PrintDefaults() 53 | os.Exit(1) 54 | } 55 | 56 | if *help || *h { 57 | flag.Usage() 58 | os.Exit(1) 59 | } 60 | 61 | rnd.SeedMathRand() 62 | rnd.RandomizeObfuscationKeys() 63 | 64 | dst := flag.Args()[0] 65 | resolver := net.JoinHostPort(dst, fmt.Sprintf("%d", *port)) 66 | 67 | cidrs := flag.Args()[1:] 68 | 69 | wg := new(sync.WaitGroup) 70 | ipc := make(chan string) 71 | stp := make(chan int) 72 | 73 | helperDomain := rnd.EnsureTrailingDot(*subdomain) 74 | for i := 0; i < *threads; i++ { 75 | go remoteSense(wg, ipc, resolver, helperDomain) 76 | wg.Add(1) 77 | } 78 | 79 | for _, cidr := range cidrs { 80 | err := rnd.AddressesFromCIDR(cidr, ipc, stp) 81 | if err != nil { 82 | fmt.Printf("input: %s\n", err) 83 | continue 84 | } 85 | } 86 | close(ipc) 87 | wg.Wait() 88 | } 89 | 90 | func remoteSense(wg *sync.WaitGroup, ipc chan string, resolver string, helperDomain string) { 91 | for addr := range ipc { 92 | c := new(dns.Client) 93 | m := &dns.Msg{ 94 | Question: make([]dns.Question, 1), 95 | } 96 | m.RecursionDesired = true 97 | 98 | ip := net.ParseIP(addr) 99 | if ip == nil { 100 | fmt.Printf("invalid address: %s\n", addr) 101 | continue 102 | } 103 | 104 | tracerBytes := make([]byte, 28) 105 | copy(tracerBytes[0:4], rnd.ObfuscationKey32Bytes[:]) 106 | copy(tracerBytes[4:20], rnd.XorBytesWithBytes(ip, rnd.ObfuscationKey32Bytes[:])) 107 | copy(tracerBytes[20:28], rnd.XorBytesWithBytes(rnd.TimestampToBytes(time.Now().UTC()), rnd.ObfuscationKey32Bytes[:])) 108 | 109 | tracerName := fmt.Sprintf("%.8x.s0%s.%s", rand.Uint32(), hex.EncodeToString(tracerBytes), helperDomain) 110 | 111 | m.Question[0] = dns.Question{Name: tracerName, Qtype: dns.TypeA, Qclass: dns.ClassINET} 112 | start := time.Now().UTC() 113 | in, _, err := c.Exchange(m, resolver) 114 | 115 | valid := false 116 | rstr := "" 117 | if err != nil { 118 | rstr = "error:" + err.Error() 119 | } else { 120 | if in.MsgHdr.Rcode == 2 { 121 | valid = true 122 | } 123 | 124 | rstr = fmt.Sprintf("code:%d", in.MsgHdr.Rcode) 125 | } 126 | 127 | diff := time.Now().UTC().Sub(start) / time.Millisecond 128 | 129 | if !valid { 130 | if !*quiet { 131 | fmt.Printf("%-20s unreachable via %-25s %6dms %s\n", addr, resolver, diff, rstr) 132 | } 133 | } else { 134 | fmt.Printf("%-20s alive via %-25s %6dms %s\n", addr, resolver, diff, rstr) 135 | } 136 | } 137 | wg.Done() 138 | } 139 | -------------------------------------------------------------------------------- /cmd/runzero-extractors/bin/extract-bridges.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Extract critical network bridges from a runZero Asset Export (JSONL format) 5 | # 6 | 7 | require 'json' 8 | require 'ipaddr' 9 | 10 | def checkNetworks(nets, a) 11 | addr = IPAddr.new(a).to_i 12 | nets.each do |net| 13 | begAddr = net.to_range.first.to_i 14 | endAddr = net.to_range.last.to_i 15 | if addr >= begAddr && addr <= endAddr 16 | return true 17 | end 18 | end 19 | return false 20 | end 21 | 22 | defaultSize = 24 23 | 24 | criticalNets = [] 25 | ARGV.each do |net| 26 | criticalNets.push IPAddr.new(net) 27 | end 28 | 29 | if criticalNets.length == 0 30 | $stderr.puts "usage: ./extract-bridges.rb [critical-net/24].. [critical-net/24].. < assets.jsonl" 31 | exit(0) 32 | end 33 | 34 | $stdin.each_line do |line| 35 | asset = JSON.parse(line.strip) 36 | netCrit = [] 37 | netNonCrit = [] 38 | addrs = asset['addresses'] + asset['addresses_extra'] 39 | addrs.each do |a| 40 | # Skip IPv6 for now 41 | next if a.include?(":") 42 | 43 | # Create a network of the default mask size 44 | c = checkNetworks(criticalNets, a) 45 | if c 46 | netCrit.push(a) 47 | else 48 | netNonCrit.push(a) 49 | end 50 | end 51 | 52 | if netCrit.length > 0 and netNonCrit.length > 0 53 | $stdout.puts " * #{asset['addresses'][0]} bridges non-critical networks (#{netNonCrit.join(", ")}) to critical networks via #{netCrit.join(", ")}\n\n" 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /cmd/runzero-extractors/bin/extract-urls.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # 4 | # Extract URLs from a runZero Asset Export (JSONL format) 5 | # 6 | 7 | require 'json' 8 | 9 | $stdin.each_line do |line| 10 | asset = JSON.parse(line.strip) 11 | asset['services'].each_pair do |n,s| 12 | next if s['protocol'] != "http" 13 | addr,port,sname = n.split("/") 14 | if s.keys.include?("tls.cipher") 15 | puts "https://#{addr}:#{port}" 16 | else 17 | puts "http://#{addr}:#{port}" 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /cmd/runzero-smb2-sessions/README.md: -------------------------------------------------------------------------------- 1 | # SMB2 Predictable Session ID Demonstration 2 | 3 | Microsoft Windows and Apple macOS use predictable Session IDs for SMB2 sessions. This code 4 | demonstrates how to predict these IDs and determine some information about remote sessions. 5 | 6 | Please see this blog post for more information: 7 | 8 | - https://www.runzero.com/2020/03/smb2-session-prediction-consequences/ 9 | 10 | On Windows the Signature field of the returned SESSION_SETUP response is signed with the 11 | original remote session key. This seems bad, but doesn't appear to be exploitable, as the 12 | input to this key includes a client and server challenge (among other fields), that are 13 | not visible as a remote third-party. 14 | 15 | On newer macOS systems (10.15) the smbd Session ID increments by 1, which leaks session activity, but Session Bind requests fail and no information about the active sessions is obtained. 16 | 17 | On Samba-based systems, the Session IDs are not predictable. 18 | 19 | The predictable session IDs have been in place for years and seem to be a design choice. 20 | 21 | The session binding and signature calculation process is well-documented: 22 | - Handle of session binding requests: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/5ed93f06-a1d2-4837-8954-fa8b833c2654 23 | - Signature calculation: https://docs.microsoft.com/en-us/archive/blogs/openspecification/smb-2-and-smb-3-security-in-windows-10-the-anatomy-of-signing-and-cryptographic-keys 24 | 25 | 26 | ## Remote Session Monitoring 27 | 28 | ``` 29 | $ go run main.go 192.168.0.220 watch 30 | 31 | 2020/03/28 16:16:31 192.168.0.220: determining the session cycle for map[ntlmssp.DNSComputer:WIN-EM7GG1U0LV3 ntlmssp.DNSDomain:WIN-EM7GG1U0LV3 ntlmssp.NTLMRevision:15 ntlmssp.NegotiationFlags:0xe28a8215 ntlmssp.NetbiosComputer:WIN-EM7GG1U0LV3 ntlmssp.NetbiosDomain:WIN-EM7GG1U0LV3 ntlmssp.TargetName:WIN-EM7GG1U0LV3 ntlmssp.Timestamp:0x01d6054627286627 ntlmssp.Version:10.0.14393 smb.Capabilities:0x0000002f smb.CipherAlg:aes-128-gcm 32 | smb.Dialect:0x0311 smb.GUID:6edc815a-7bea-cb41-a1dd-6079352c4fce smb.HashAlg:sha512 smb.HashSaltLen:32 smb.SessionID:0x00002c328000002d smb.Signing:enabled smb.Status:0xc0000016] 33 | 34 | 2020/03/28 16:16:48 192.168.0.220: cycle found after 205 requests: fffffffffffffffc-fffffffffffffff0-fffffffff800004c-7ffffcc-ffffffffcc000030-33ffffd4-fffffffffffffff0-18-14-fffffffffc00001c-fffffffff8000014-bffffc4-4-fffffffff8000028-ffffffffd000001c-37ffffb4-1c-fffffffffc000034-ffffffffc3ffffa8-40000030-c-fffffffff8000008-7fffffc-ffffffd6cfffffd4-2930000038-ffffffffebffffd0-14000034-3ffff8c-8-4-ffffffff5c000038-a3ffffd4 35 | 36 | 2020/03/28 16:16:48 192.168.0.220: watching for new sessions... 37 | 2020/03/28 16:16:54 192.168.0.220: SESSION 0x00002c329c000011 is EXPIRED 38 | 2020/03/28 16:16:59 192.168.0.220: SESSION 0x00002c329c000031 is ACTIVE dialect:0x0311 sig:526ec3d5a65947888677c43fee02604f 39 | 2020/03/28 16:17:03 192.168.0.220: SESSION 0x00002c329c000049 is EXPIRED 40 | ``` 41 | 42 | ## Remote Session Hunting 43 | 44 | ``` 45 | $ go run main.go 192.168.0.220 hunt 46 | 47 | 2020/03/29 21:47:21 192.168.0.220: warning: hunt mode is unreliable and unlikely to find older sessions 48 | 49 | 2020/03/29 21:47:21 192.168.0.220: determining the session cycle for map[ntlmssp.DNSComputer:WIN-EM7GG1U0LV3 ntlmssp.DNSDomain:WIN-EM7GG1U0LV3 ntlmssp.NTLMRevision:15 ntlmssp.NegotiationFlags:0xe28a8215 ntlmssp.NetbiosComputer:WIN-EM7GG1U0LV3 ntlmssp.NetbiosDomain:WIN-EM7GG1U0LV3 ntlmssp.TargetName:WIN-EM7GG1U0LV3 ntlmssp.Timestamp:0x01d6063d88af7af5 ntlmssp.Version:10.0.14393 smb.Capabilities:0x0000002f smb.CipherAlg:aes-128-gcm 50 | smb.Dialect:0x0311 smb.GUID:6edc815a-7bea-cb41-a1dd-6079352c4fce smb.HashAlg:sha512 smb.HashSaltLen:32 smb.SessionID:0x00002c3880000069 smb.Signing:enabled smb.Status:0xc0000016] 51 | 52 | 2020/03/29 21:47:23 192.168.0.220: cycle found after 129 requests: ffffffffc8000014-37ffffe8-ffffffd6cfffffd8-2930000038-ffffffffebffffd0-14000034-3ffff8c-ffffffff5c000044-a3ffffc4-10-fffffffffffffff4-8-fffffffffffffff0-fffffffff800004c-7ffffcc-ffffffffcc000030-33ffffc4-10-1c-fffffffffc00001c-3ffffdc-fffffffff8000028-7ffffd4-fffffffffffffffc-1c-ffffffffffffffe0-fffffffffc000054-ffffffffc3ffffa8-34000048-bffffe8-fffffffff8000014-7fffffc 53 | 2020/03/29 21:47:23 192.168.0.220: hunting for existing sessions... 54 | 2020/03/29 21:47:25 192.168.0.220: sent 1000 requests (2c37e4000041) 55 | ``` 56 | 57 | ## Remote Session ID Sampling 58 | 59 | ``` 60 | $ go run main.go 192.168.0.220 sample 61 | 62 | 2020/03/29 21:47:50 192.168.0.220: sample 100 session IDs for map[ntlmssp.DNSComputer:WIN-EM7GG1U0LV3 ntlmssp.DNSDomain:WIN-EM7GG1U0LV3 ntlmssp.NTLMRevision:15 ntlmssp.NegotiationFlags:0xe28a8215 ntlmssp.NetbiosComputer:WIN-EM7GG1U0LV3 ntlmssp.NetbiosDomain:WIN-EM7GG1U0LV3 ntlmssp.TargetName:WIN-EM7GG1U0LV3 ntlmssp.Timestamp:0x01d6063d9a0b8f3e ntlmssp.Version:10.0.14393 smb.Capabilities:0x0000002f smb.CipherAlg:aes-128-gcm smb.Dialect:0x0311 smb.GUID:6edc815a-7bea-cb41-a1dd-6079352c4fce smb.HashAlg:sha512 smb.HashSaltLen:32 smb.SessionID:0x00002c3898000061 smb.Signing:enabled smb.Status:0xc0000016] 63 | 64 | 2020/03/29 21:47:50 0x00002c3898000061 65 | 2020/03/29 21:47:50 0x00002c0f68000039 66 | 2020/03/29 21:47:50 0x00002c3898000071 67 | 2020/03/29 21:47:50 0x00002c3884000041 68 | 2020/03/29 21:47:50 0x00002c3898000075 69 | 2020/03/29 21:47:50 0x00002c389c000001 70 | 2020/03/29 21:47:50 0x00002c37f8000045 71 | ``` -------------------------------------------------------------------------------- /cmd/runzero-smb2-sessions/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Copyright (C) 2020 runZero, Inc 4 | 5 | import ( 6 | "encoding/binary" 7 | "fmt" 8 | "log" 9 | "net" 10 | "os" 11 | "strconv" 12 | "strings" 13 | "time" 14 | 15 | "github.com/runZeroInc/runzero-tools/pkg/rnd" 16 | ) 17 | 18 | func main() { 19 | usage := fmt.Sprintf("Usage: "+ 20 | "\t%s watch\n"+ 21 | "\t%s hunt\n", 22 | "\t%s sample\n", os.Args[0], os.Args[0], os.Args[0], 23 | ) 24 | 25 | if len(os.Args) < 3 { 26 | fmt.Fprintln(os.Stderr, usage) 27 | os.Exit(1) 28 | } 29 | 30 | dst := os.Args[1] 31 | mod := os.Args[2] 32 | switch mod { 33 | case "watch": 34 | doMonitor(dst) 35 | case "hunt": 36 | doHunt(dst) 37 | case "sample": 38 | doSample(dst) 39 | default: 40 | fmt.Fprintln(os.Stderr, usage) 41 | } 42 | } 43 | 44 | func doMonitor(dst string) { 45 | showInfo := false 46 | 47 | c := rnd.NewCounterPredictor(3, 10) 48 | for { 49 | info, err := probe(dst, "") 50 | if err != nil { 51 | log.Printf("%s: %s", dst, err) 52 | break 53 | } 54 | 55 | if info["smb.SessionID"] == "" { 56 | log.Printf("%s: no sid: %#v", dst, info) 57 | break 58 | } 59 | 60 | if !showInfo { 61 | log.Printf("%s: determining the session cycle for %v", dst, info) 62 | showInfo = true 63 | } 64 | 65 | newSID := decodeSessionID(info["smb.SessionID"]) 66 | if !c.Ready() { 67 | 68 | if c.GetSampleCount() > 250 { 69 | log.Printf("%s: could not determine cycle after 250 requests", dst) 70 | return 71 | } 72 | 73 | if c.SubmitSample(newSID) { 74 | log.Printf("%s: cycle found after %d requests: %s", dst, c.GetSampleCount(), rnd.U64SliceToSeq(c.GetCycle())) 75 | log.Printf("%s: watching for new sessions...", dst) 76 | } 77 | 78 | continue 79 | } 80 | 81 | foundSessions, err := c.Check(newSID) 82 | if err != nil { 83 | log.Printf("%s: %s, recalibrating...", dst, err) 84 | c = rnd.NewCounterPredictor(3, 10) 85 | showInfo = false 86 | continue 87 | } 88 | 89 | for _, found := range foundSessions { 90 | res, _ := probe(dst, fmt.Sprintf("0x%.16x", found)) 91 | sig := "" 92 | if res["smb.Signature"] != "" { 93 | sig = "sig:" + res["smb.Signature"] 94 | } 95 | 96 | status := res["smb.Status"] 97 | 98 | switch status { 99 | case "0xc0000203": 100 | status = "EXPIRED" 101 | case "0xc0000022": 102 | status = fmt.Sprintf("ACTIVE dialect:%s", res["smb.Dialect"]) 103 | case "0xc000000d": 104 | status = fmt.Sprintf("ACTIVE dialect:!%s", res["smb.Dialect"]) 105 | } 106 | 107 | log.Printf("%s: SESSION 0x%.16x is %s %s", dst, found, status, sig) 108 | } 109 | 110 | time.Sleep(time.Second) 111 | } 112 | } 113 | 114 | func doHunt(dst string) { 115 | log.Printf("%s: warning: hunt mode is unreliable and unlikely to find older sessions", dst) 116 | showInfo := false 117 | 118 | c := rnd.NewCounterPredictor(3, 10) 119 | 120 | Predict: 121 | for { 122 | info, err := probe(dst, "") 123 | if err != nil { 124 | log.Printf("%s: %s", dst, err) 125 | break 126 | } 127 | 128 | if info["smb.SessionID"] == "" { 129 | log.Printf("%s: no sid: %#v", dst, info) 130 | break 131 | } 132 | 133 | if !showInfo { 134 | log.Printf("%s: determining the session cycle for %v", dst, info) 135 | showInfo = true 136 | } 137 | 138 | newSID := decodeSessionID(info["smb.SessionID"]) 139 | if !c.Ready() { 140 | 141 | if c.GetSampleCount() > 250 { 142 | log.Printf("%s: could not determine cycle after 250 requests", dst) 143 | return 144 | } 145 | 146 | if c.SubmitSample(newSID) { 147 | log.Printf("%s: cycle found after %d requests: %s", dst, c.GetSampleCount(), rnd.U64SliceToSeq(c.GetCycle())) 148 | log.Printf("%s: hunting for existing sessions...", dst) 149 | } 150 | 151 | continue 152 | } 153 | 154 | sid := newSID 155 | 156 | cnt := 0 157 | for { 158 | 159 | if cnt > 10000 { 160 | log.Printf("%s: giving up...", dst) 161 | break Predict 162 | } 163 | 164 | sid, err = c.Previous(sid) 165 | if err != nil { 166 | log.Printf("%s: %s, exiting...", dst, err) 167 | break Predict 168 | } 169 | 170 | res, err := probe(dst, fmt.Sprintf("0x%.16x", sid)) 171 | if err != nil { 172 | log.Printf("%s: %s, exiting...", dst, err) 173 | break Predict 174 | } 175 | cnt++ 176 | 177 | if cnt%1000 == 0 { 178 | log.Printf("%s: sent %d requests (%x)", dst, cnt, sid) 179 | } 180 | 181 | sig := "" 182 | if res["smb.Signature"] != "" { 183 | sig = "sig:" + res["smb.Signature"] 184 | } 185 | 186 | status := res["smb.Status"] 187 | 188 | switch status { 189 | case "0xc0000203": 190 | continue 191 | case "0xc0000022": 192 | status = fmt.Sprintf("ACTIVE dialect:%s", res["smb.Dialect"]) 193 | case "0xc000000d": 194 | status = fmt.Sprintf("ACTIVE dialect:!%s", res["smb.Dialect"]) 195 | default: 196 | status = fmt.Sprintf("UNKNOWN %v", res) 197 | } 198 | 199 | log.Printf("%s: SESSION 0x%.16x is %s %s", dst, sid, status, sig) 200 | } 201 | } 202 | } 203 | 204 | func doSample(dst string) { 205 | showInfo := false 206 | 207 | for x := 0; x <= 100; x++ { 208 | info, err := probe(dst, "") 209 | if err != nil { 210 | log.Printf("%s: %s", dst, err) 211 | break 212 | } 213 | 214 | if info["smb.SessionID"] == "" { 215 | log.Printf("%s: no sid: %#v", dst, info) 216 | break 217 | } 218 | 219 | if !showInfo { 220 | log.Printf("%s: sample 100 session IDs for %v", dst, info) 221 | showInfo = true 222 | } 223 | 224 | log.Printf("%s", info["smb.SessionID"]) 225 | } 226 | } 227 | 228 | func probe(dip string, patchSID string) (map[string]string, error) { 229 | info := make(map[string]string) 230 | dst := dip + ":445" 231 | 232 | conn, err := net.DialTimeout("tcp", dst, rnd.SMBReadTimeout) 233 | if err != nil { 234 | return info, err 235 | } 236 | defer conn.Close() 237 | 238 | err = rnd.SMBSendData(conn, rnd.SMB1NegotiateProtocolRequest) 239 | if err != nil { 240 | return info, err 241 | } 242 | 243 | data, err := rnd.SMBReadFrame(conn, rnd.SMBReadTimeout) 244 | if err != nil { 245 | return info, err 246 | } 247 | 248 | err = rnd.SMBSendData(conn, rnd.SMB2NegotiateProtocolRequest(dip)) 249 | if err != nil { 250 | return info, err 251 | } 252 | 253 | data, _ = rnd.SMBReadFrame(conn, rnd.SMBReadTimeout) 254 | rnd.SMB2ExtractFieldsFromNegotiateReply(data, info) 255 | 256 | setup := make([]byte, len(rnd.SMB2SessionSetupNTLMSSP)) 257 | copy(setup, rnd.SMB2SessionSetupNTLMSSP) 258 | 259 | // Set the ProcessID 260 | binary.LittleEndian.PutUint16(setup[4+32:], 0xfeff) 261 | 262 | if patchSID != "" { 263 | // Set the SMB2_SESSION_FLAG_BINDING flag 264 | setup[4+66] = 1 265 | 266 | // Set the Signed PDU flag 267 | binary.LittleEndian.PutUint16(setup[4+16:], 0x08) 268 | 269 | // Set the SessionID 270 | sid := decodeSessionID(patchSID) 271 | binary.LittleEndian.PutUint64(setup[4+40:], sid) 272 | } 273 | 274 | err = rnd.SMBSendData(conn, setup) 275 | if err != nil { 276 | return info, err 277 | } 278 | 279 | data, err = rnd.SMBReadFrame(conn, rnd.SMBReadTimeout) 280 | rnd.SMB2ExtractSIDFromSessionSetupReply(data, info) 281 | rnd.SMBExtractFieldsFromSecurityBlob(data, info) 282 | 283 | return info, err 284 | } 285 | 286 | func decodeSessionID(sid string) uint64 { 287 | sid = strings.Replace(sid, "0x", "", -1) 288 | sidV, err := strconv.ParseUint(sid, 16, 64) 289 | if err != nil { 290 | return 0 291 | } 292 | return sidV 293 | } 294 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/runZeroInc/runzero-tools 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/gofrs/uuid v4.4.0+incompatible 7 | github.com/miekg/dns v1.1.62 8 | github.com/sirupsen/logrus v1.9.3 9 | ) 10 | 11 | require ( 12 | golang.org/x/mod v0.22.0 // indirect 13 | golang.org/x/net v0.34.0 // indirect 14 | golang.org/x/sync v0.10.0 // indirect 15 | golang.org/x/sys v0.29.0 // indirect 16 | golang.org/x/tools v0.29.0 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= 5 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 6 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 7 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 8 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 9 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 10 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 12 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 15 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 16 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 17 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 18 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 19 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 20 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 21 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 22 | golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 23 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 24 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 25 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 26 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 27 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 28 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 29 | golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= 30 | golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 31 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 32 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 33 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 34 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 35 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 36 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 37 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 38 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 39 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 40 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 41 | golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 42 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 43 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 44 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 45 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 46 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 50 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 51 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 52 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 53 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 54 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 55 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 56 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 58 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 59 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 63 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 65 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 66 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 67 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 68 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 69 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 70 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 71 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 72 | golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= 73 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 74 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 75 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 76 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 77 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 78 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 79 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 80 | golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 81 | golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 82 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 83 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 84 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 85 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 86 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 87 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 88 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 89 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 90 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 91 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 92 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 93 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 94 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 95 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 96 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 97 | golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= 98 | golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= 99 | golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE= 100 | golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588= 101 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 102 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 103 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 104 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 105 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 106 | -------------------------------------------------------------------------------- /pkg/rnd/bytes.go: -------------------------------------------------------------------------------- 1 | package rnd 2 | 3 | import ( 4 | crand "crypto/rand" 5 | "encoding/binary" 6 | "math/rand" 7 | ) 8 | 9 | // RandomBytes generates a random byte sequence of the requested length 10 | func RandomBytes(numbytes int) []byte { 11 | randBytes := make([]byte, numbytes) 12 | binary.Read(crand.Reader, binary.BigEndian, &randBytes) 13 | return randBytes 14 | } 15 | 16 | // XorBytesWithBytes xor encodes a byte array with another byte array 17 | func XorBytesWithBytes(src []byte, key []byte) []byte { 18 | dst := make([]byte, len(src)) 19 | ksz := len(key) 20 | for i := 0; i < len(src); i++ { 21 | dst[i] = src[i] ^ key[i%ksz] 22 | } 23 | return dst 24 | } 25 | 26 | // SeedMathRand seeds the PRNG for things like transaction IDs 27 | func SeedMathRand() { 28 | var randomSeed int64 29 | binary.Read(crand.Reader, binary.BigEndian, &randomSeed) 30 | rand.Seed(randomSeed) 31 | } 32 | -------------------------------------------------------------------------------- /pkg/rnd/counter_predictor.go: -------------------------------------------------------------------------------- 1 | package rnd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | "sync" 8 | ) 9 | 10 | // CounterPredictor tries to predict the next sequence based on a detected cyclical pattern 11 | type CounterPredictor struct { 12 | MinRep int 13 | MinLen int 14 | cycle []uint64 15 | samples []uint64 16 | history []uint64 17 | cycleIndex int 18 | lastSample uint64 19 | m sync.Mutex 20 | } 21 | 22 | // SubmitSample is used to train the predictor 23 | func (c *CounterPredictor) SubmitSample(v uint64) bool { 24 | c.m.Lock() 25 | defer c.m.Unlock() 26 | 27 | if c.lastSample == 0 { 28 | c.lastSample = v 29 | return false 30 | } 31 | 32 | c.samples = append(c.samples, v-c.lastSample) 33 | c.history = append(c.history, v) 34 | c.lastSample = v 35 | return c.predictCycle() 36 | } 37 | 38 | // Ready indicates if the predictor has calculted the cycle 39 | func (c *CounterPredictor) Ready() bool { 40 | c.m.Lock() 41 | defer c.m.Unlock() 42 | return len(c.cycle) > 0 43 | } 44 | 45 | // GetCycle returns the predicted cycle 46 | func (c *CounterPredictor) GetCycle() []uint64 { 47 | c.m.Lock() 48 | defer c.m.Unlock() 49 | if len(c.cycle) == 0 { 50 | return []uint64{} 51 | } 52 | res := make([]uint64, len(c.cycle)) 53 | copy(res, c.cycle) 54 | return res 55 | } 56 | 57 | // GetSampleCount returns the number of samples stored 58 | func (c *CounterPredictor) GetSampleCount() int { 59 | c.m.Lock() 60 | defer c.m.Unlock() 61 | return len(c.samples) 62 | } 63 | 64 | func (c *CounterPredictor) predictCycle() bool { 65 | if len(c.samples) < (c.MinRep * c.MinLen) { 66 | return false 67 | } 68 | 69 | endSearch := len(c.samples) - ((c.MinRep - 1) * c.MinLen) 70 | // log.Printf("samples: %d (rep:%d, len:%d) end:%d", len(c.samples), c.MinRep, c.MinLen, endSearch) 71 | 72 | allSamples := U64SliceToSeq(c.samples[:]) 73 | 74 | for i := 0; i < endSearch; i++ { 75 | for x := c.MinLen; x <= (c.MinLen * 10); x++ { 76 | 77 | canLen := i + x 78 | if canLen >= len(c.samples) { 79 | break 80 | } 81 | 82 | can := c.samples[i:canLen] 83 | 84 | canStr := U64SliceToSeq(can) 85 | 86 | canTest := "" 87 | for y := 0; y <= c.MinRep; y++ { 88 | canTest = canTest + "-" + canStr 89 | } 90 | 91 | // log.Printf("[%d/%d/%d] can: %v", i, x, canLen, canStr) 92 | if strings.Contains(allSamples, canTest) { 93 | c.cycle = can 94 | c.calculateCycleIndex() 95 | return true 96 | } 97 | } 98 | } 99 | 100 | return false 101 | } 102 | 103 | // Check submits a new value and returns the list of missing sequences if any 104 | func (c *CounterPredictor) Check(v uint64) ([]uint64, error) { 105 | c.m.Lock() 106 | defer c.m.Unlock() 107 | 108 | foundSessions := []uint64{} 109 | 110 | if len(c.cycle) == 0 { 111 | return foundSessions, nil 112 | } 113 | 114 | last := c.history[len(c.history)-1] 115 | pred := last + c.cycle[c.cycleIndex] 116 | 117 | for pred != v { 118 | 119 | foundSessions = append(foundSessions, pred) 120 | 121 | // The predictor lost sync, better to recalibrate than try to fix it 122 | if len(foundSessions) > 100 { 123 | return []uint64{}, fmt.Errorf("predictor lost sync") 124 | } 125 | 126 | c.cycleIndex = (c.cycleIndex + 1) % len(c.cycle) 127 | 128 | c.history = append(c.history, pred) 129 | if len(c.history) > 512 { 130 | c.history = c.history[len(c.history)-512:] 131 | } 132 | 133 | last = c.history[len(c.history)-1] 134 | pred = last + c.cycle[c.cycleIndex] 135 | } 136 | 137 | // Update cycle index 138 | c.cycleIndex = (c.cycleIndex + 1) % len(c.cycle) 139 | 140 | // Determine the expected value based on the cycle 141 | 142 | // Update the history 143 | c.history = append(c.history, v) 144 | if len(c.history) > 512 { 145 | c.history = c.history[len(c.history)-512:] 146 | } 147 | 148 | return foundSessions, nil 149 | } 150 | 151 | // Previous rolls back to the prior session ID using the predicted counter 152 | func (c *CounterPredictor) Previous(v uint64) (uint64, error) { 153 | c.m.Lock() 154 | defer c.m.Unlock() 155 | 156 | if len(c.cycle) == 0 { 157 | return 0, fmt.Errorf("no cycle") 158 | } 159 | 160 | // Update cycle index 161 | c.cycleIndex = c.cycleIndex - 1 162 | 163 | if c.cycleIndex < 0 { 164 | c.cycleIndex = len(c.cycle) - 1 165 | } 166 | 167 | // Calculate the previous sequence 168 | prev := v - c.cycle[c.cycleIndex] 169 | 170 | return prev, nil 171 | } 172 | 173 | func (c *CounterPredictor) calculateCycleIndex() { 174 | hs := U64SliceToSeq(c.samples) 175 | cs := U64SliceToSeq(c.cycle) 176 | oc := strings.LastIndex(hs, cs) 177 | if oc == -1 { 178 | log.Printf("failed to calculate cycle index: %s in %s", cs, hs) 179 | return 180 | } 181 | 182 | cycleStart := hs[oc:] 183 | // log.Printf("cycle start: %s (%d)", cycleStart, oc) 184 | 185 | bits := strings.Split(cycleStart, "-") 186 | c.cycleIndex = len(bits) % len(c.cycle) 187 | // log.Printf("cycleIndex: %d (%x)", c.cycleIndex, c.cycle[c.cycleIndex]) 188 | } 189 | 190 | // NewCounterPredictor returns a new instance of the predictor 191 | func NewCounterPredictor(rep int, len int) *CounterPredictor { 192 | return &CounterPredictor{MinRep: rep, MinLen: len} 193 | } 194 | -------------------------------------------------------------------------------- /pkg/rnd/ip.go: -------------------------------------------------------------------------------- 1 | package rnd 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "fmt" 7 | "math" 8 | "math/big" 9 | "math/rand" 10 | "net" 11 | "regexp" 12 | "strings" 13 | ) 14 | 15 | // IPv42UInt converts IPv4 addresses to unsigned integers 16 | func IPv42UInt(ips string) (uint32, error) { 17 | ip := net.ParseIP(ips) 18 | if ip == nil { 19 | return 0, errors.New("invalid IPv4 address") 20 | } 21 | ip = ip.To4() 22 | return binary.BigEndian.Uint32(ip), nil 23 | } 24 | 25 | // IPv42UIntLE converts IPv4 addresses to unsigned integers (little endian) 26 | func IPv42UIntLE(ips string) (uint32, error) { 27 | ip := net.ParseIP(ips) 28 | if ip == nil { 29 | return 0, errors.New("invalid IPv4 address") 30 | } 31 | ip = ip.To4() 32 | return binary.LittleEndian.Uint32(ip), nil 33 | } 34 | 35 | // UInt2IPv4 converts unsigned integers to IPv4 addresses 36 | func UInt2IPv4(ipi uint32) string { 37 | ipb := make([]byte, 4) 38 | binary.BigEndian.PutUint32(ipb, ipi) 39 | ip := net.IP(ipb) 40 | return ip.String() 41 | } 42 | 43 | // IPv42Bytes converts an IPv4 address to a byte array 44 | func IPv42Bytes(ips string) ([]byte, error) { 45 | ipBytes := make([]byte, 4) 46 | ip := net.ParseIP(ips) 47 | if ip == nil { 48 | return ipBytes, errors.New("invalid IPv4 address") 49 | } 50 | ip = ip.To4() 51 | ipInt := binary.BigEndian.Uint32(ip) 52 | binary.BigEndian.PutUint32(ipBytes, ipInt) 53 | return ipBytes, nil 54 | } 55 | 56 | // Bytes2IPv4 converts a byte array to an IPv4 addresse 57 | func Bytes2IPv4(ipb []byte) string { 58 | ip := net.IP(ipb) 59 | return ip.String() 60 | } 61 | 62 | // ObfuscationKey32 provides an XOR key for encoding 63 | var ObfuscationKey32 uint32 = 0x50505050 64 | 65 | // ObfuscationKey32Bytes are the 32-bit XOR key as a byte array 66 | var ObfuscationKey32Bytes = [4]byte{} 67 | 68 | // ObfuscationKey64 provides an XOR key for encoding 69 | var ObfuscationKey64 uint64 = 0x5050505050505050 70 | 71 | // ObfuscationKey64Bytes are the 64-bit XOR key as a byte array 72 | var ObfuscationKey64Bytes = [8]byte{} 73 | 74 | // ObfuscateIPv4FromBytesToBytes XORs an IPv4 byte array with the obfuscation key 75 | func ObfuscateIPv4FromBytesToBytes(ipb []byte) []byte { 76 | return ObfuscateBytes4(ipb) 77 | } 78 | 79 | // ObfuscateBytes4 XORs a 4-byte array with the obfuscation key 80 | func ObfuscateBytes4(b []byte) []byte { 81 | resp := make([]byte, 4) 82 | ival := binary.BigEndian.Uint32(b) 83 | binary.BigEndian.PutUint32(resp, ival^ObfuscationKey32) 84 | return resp 85 | } 86 | 87 | // ObfuscateBytes8 XORs a 8-byte array with the obfuscation key 88 | func ObfuscateBytes8(b []byte) []byte { 89 | resp := make([]byte, 8) 90 | ival := binary.BigEndian.Uint64(b) 91 | binary.BigEndian.PutUint64(resp, ival^ObfuscationKey64) 92 | return resp 93 | } 94 | 95 | // ObfuscateIPv4FromStringToBytes XORs an IPv4 string with the obfuscation key, returning bytes 96 | func ObfuscateIPv4FromStringToBytes(ip string) []byte { 97 | ipb, _ := IPv42Bytes(ip) 98 | return ObfuscateIPv4FromBytesToBytes(ipb) 99 | } 100 | 101 | // ObfuscateIPv4FromStringToString XORs an IPv4 string with the obfuscation key, returning a string 102 | func ObfuscateIPv4FromStringToString(ip string) string { 103 | ipb, _ := IPv42Bytes(ip) 104 | return Bytes2IPv4(ObfuscateIPv4FromBytesToBytes(ipb)) 105 | } 106 | 107 | // ObfuscateIPv4FromBytesToString XORs an IPv4 string with the obfuscation key, returning a string 108 | func ObfuscateIPv4FromBytesToString(ipb []byte) string { 109 | return Bytes2IPv4(ObfuscateIPv4FromBytesToBytes(ipb)) 110 | } 111 | 112 | // RandomizeObfuscationKeys resets the default obfuscation keys 113 | func RandomizeObfuscationKeys() { 114 | ObfuscationKey32 = rand.Uint32() 115 | binary.BigEndian.PutUint32(ObfuscationKey32Bytes[:], ObfuscationKey32) 116 | ObfuscationKey64 = rand.Uint64() 117 | binary.BigEndian.PutUint64(ObfuscationKey64Bytes[:], ObfuscationKey64) 118 | } 119 | 120 | // MatchIPv6 is a regular expression for validating IPv6 addresses 121 | var MatchIPv6 = regexp.MustCompile(`^((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?$`) 122 | 123 | // MatchIPv4 is a regular expression for validating IPv4 addresses 124 | var MatchIPv4 = regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})[.](?:25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2}))$`) 125 | 126 | // MatchHostname is a regular expression for validating hostnames 127 | var MatchHostname = regexp.MustCompile(`^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$`) 128 | 129 | // ValidIP returns a true/false on whether the input is a valid IPv4 or IPv6 address 130 | func ValidIP(addr string) bool { 131 | return (MatchIPv4.MatchString(addr) || MatchIPv6.MatchString(addr)) 132 | } 133 | 134 | // ValidIP4 returns a true/false on whether the input is a valid IPv4 address 135 | func ValidIP4(addr string) bool { 136 | return MatchIPv4.MatchString(addr) 137 | } 138 | 139 | // ValidIP6 returns a true/false on whether the input is a valid IPv6 address 140 | func ValidIP6(addr string) bool { 141 | return MatchIPv6.MatchString(addr) 142 | } 143 | 144 | // EgressDestinationIPv4 defines an internet-reachable IPv4 address (currently cloudflare) 145 | var EgressDestinationIPv4 = "1.1.1.1" 146 | 147 | // EgressDestinationIPv6 defines an internet-reachable IPv6 address (currently cloudflare) 148 | var EgressDestinationIPv6 = "[2606:4700:4700::1111]" 149 | 150 | // GetEgressAddress return the IPv4 or IPv6 address used to route to the specified destination 151 | func GetEgressAddress(dst string) string { 152 | conn, err := net.Dial("udp", dst+":53") 153 | if err != nil { 154 | return "127.0.0.1" 155 | } 156 | 157 | host, _, err := net.SplitHostPort(conn.LocalAddr().String()) 158 | conn.Close() 159 | if err != nil { 160 | return "127.0.0.1" 161 | } 162 | 163 | bits := strings.Split(host, "%") 164 | 165 | return bits[0] 166 | } 167 | 168 | // AddressesFromCIDR parses a CIDR and writes individual IPs to a channel 169 | func AddressesFromCIDR(cidr string, out chan string, quit chan int) error { 170 | if len(cidr) == 0 { 171 | return fmt.Errorf("invalid CIDR: empty") 172 | } 173 | 174 | // We may receive bare IP addresses, add a mask if needed 175 | if !strings.Contains(cidr, "/") { 176 | if strings.Contains(cidr, ":") { 177 | cidr = cidr + "/128" 178 | } else { 179 | cidr = cidr + "/32" 180 | } 181 | } 182 | 183 | // Parse CIDR into base address + mask 184 | _, net, err := net.ParseCIDR(cidr) 185 | if err != nil { 186 | return fmt.Errorf("invalid CIDR: %s %s", cidr, err.Error()) 187 | } 188 | 189 | // Verify IPv4 for now 190 | ip4 := net.IP.To4() 191 | if ip4 == nil { 192 | return fmt.Errorf("invalid IPv4 CIDR: %s", cidr) 193 | } 194 | 195 | netBase, err := IPv42UInt(net.IP.String()) 196 | if err != nil { 197 | return fmt.Errorf("invalid IPv4: %s %s", cidr, err) 198 | } 199 | 200 | maskOnes, maskTotal := net.Mask.Size() 201 | 202 | // Does not work for IPv6 due to cast to uint32 203 | netSize := uint32(math.Pow(2, float64(maskTotal-maskOnes))) 204 | curBase := netBase 205 | endBase := netBase + netSize 206 | 207 | // Iterate the range semi-randomly 208 | randomWalkIPv4Range(curBase, endBase, out, quit) 209 | 210 | return nil 211 | } 212 | 213 | // AddressCountFromCIDR parses a CIDR and returns the numnber of included IP addresses 214 | func AddressCountFromCIDR(cidr string) (uint64, error) { 215 | var count uint64 216 | if len(cidr) == 0 { 217 | return count, fmt.Errorf("invalid CIDR: empty") 218 | } 219 | 220 | // We may receive bare IP addresses, not CIDRs sometimes 221 | if !strings.Contains(cidr, "/") { 222 | if strings.Contains(cidr, ":") { 223 | cidr = cidr + "/128" 224 | } else { 225 | cidr = cidr + "/32" 226 | } 227 | } 228 | 229 | // Parse CIDR into base address + mask 230 | _, net, err := net.ParseCIDR(cidr) 231 | if err != nil { 232 | return count, fmt.Errorf("invalid CIDR: %s %s", cidr, err.Error()) 233 | } 234 | 235 | // Verify IPv4 for now 236 | ip4 := net.IP.To4() 237 | if ip4 == nil { 238 | return count, fmt.Errorf("invalid IPv4 CIDR: %s", cidr) 239 | } 240 | 241 | maskOnes, maskTotal := net.Mask.Size() 242 | 243 | // Does not work for IPv6 due to cast to uint32 244 | netSize := uint64(math.Pow(2, float64(maskTotal-maskOnes))) 245 | 246 | return netSize, nil 247 | } 248 | 249 | // findPrimeOverMin returns a prime int64 of at least min 250 | func findPrimeOverMin(min int64) int64 { 251 | var randomSeed int64 252 | for i := 0; ; i++ { 253 | randomSeed = rand.Int63() 254 | // ProbablyPrime is 100% accurate for inputs less than 2⁶⁴ 255 | if big.NewInt(randomSeed).ProbablyPrime(1) { 256 | if randomSeed > min { 257 | return randomSeed 258 | } 259 | } 260 | } 261 | } 262 | 263 | // randomWalkIPv4Range iterates over an IPv4 range using a prime, writing IPs to the output channel 264 | func randomWalkIPv4Range(min uint32, max uint32, out chan string, quit chan int) { 265 | s := int64(max - min) 266 | p := findPrimeOverMin(int64(s)) 267 | if s == 0 { 268 | return 269 | } 270 | 271 | q := p % s 272 | for v := int64(0); v < s; v++ { 273 | ip := UInt2IPv4(min + uint32(q)) 274 | select { 275 | case <-quit: 276 | return 277 | case out <- ip: 278 | q = (q + p) % s 279 | } 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /pkg/rnd/ports.go: -------------------------------------------------------------------------------- 1 | package rnd 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // ValidPort determines if a port number is valid 10 | func ValidPort(pnum int) bool { 11 | if pnum < 0 || pnum > 65535 { 12 | return false 13 | } 14 | return true 15 | } 16 | 17 | // CrackPorts turns a comma-delimited port list into an array 18 | func CrackPorts(pspec string) ([]int, error) { 19 | results := []int{} 20 | 21 | // Use a map to dedup and shuffle ports 22 | ports := make(map[int]bool) 23 | 24 | bits := strings.Split(pspec, ",") 25 | for _, bit := range bits { 26 | 27 | // Split based on dash 28 | prange := strings.Split(bit, "-") 29 | 30 | // No port range 31 | if len(prange) == 1 { 32 | pnum, err := strconv.Atoi(bit) 33 | if err != nil || !ValidPort(pnum) { 34 | return results, fmt.Errorf("invalid port %s", bit) 35 | } 36 | // Record the valid port 37 | ports[pnum] = true 38 | continue 39 | } 40 | 41 | if len(prange) != 2 { 42 | return results, fmt.Errorf("invalid port range %s (%d)", prange, len(prange)) 43 | } 44 | 45 | pstart, err := strconv.Atoi(prange[0]) 46 | if err != nil || !ValidPort(pstart) { 47 | return results, fmt.Errorf("invalid start port %d", pstart) 48 | } 49 | 50 | pstop, err := strconv.Atoi(prange[1]) 51 | if err != nil || !ValidPort(pstop) { 52 | return results, fmt.Errorf("invalid stop port %d", pstop) 53 | } 54 | 55 | if pstart > pstop { 56 | return results, fmt.Errorf("invalid port range %d-%d", pstart, pstop) 57 | } 58 | 59 | for pnum := pstart; pnum <= pstop; pnum++ { 60 | ports[pnum] = true 61 | } 62 | } 63 | 64 | // Create the results from the map 65 | for port := range ports { 66 | results = append(results, port) 67 | } 68 | return results, nil 69 | } 70 | -------------------------------------------------------------------------------- /pkg/rnd/smb.go: -------------------------------------------------------------------------------- 1 | package rnd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | "net" 10 | "strings" 11 | "time" 12 | 13 | "github.com/gofrs/uuid" 14 | ) 15 | 16 | // 17 | // Warning: This is not intended to be a useful SMB implementation 18 | // 19 | 20 | // SMBReadTimeout sets a default timeout for read operations 21 | var SMBReadTimeout = time.Second * 2 22 | 23 | // SMB1NegotiateProtocolRequest is a SMB1 request that advertises support for SMB2 24 | var SMB1NegotiateProtocolRequest = []byte{ 25 | 0x00, 0x00, 0x00, 0xd4, 0xff, 0x53, 0x4d, 0x42, 26 | 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x43, 0xc8, 27 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 28 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 29 | 0x00, 0x00, 0x00, 0x00, 0x00, 0xb1, 0x00, 0x02, 30 | 0x50, 0x43, 0x20, 0x4e, 0x45, 0x54, 0x57, 0x4f, 31 | 0x52, 0x4b, 0x20, 0x50, 0x52, 0x4f, 0x47, 0x52, 32 | 0x41, 0x4d, 0x20, 0x31, 0x2e, 0x30, 0x00, 0x02, 33 | 0x4d, 0x49, 0x43, 0x52, 0x4f, 0x53, 0x4f, 0x46, 34 | 0x54, 0x20, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 35 | 0x4b, 0x53, 0x20, 0x31, 0x2e, 0x30, 0x33, 0x00, 36 | 0x02, 0x4d, 0x49, 0x43, 0x52, 0x4f, 0x53, 0x4f, 37 | 0x46, 0x54, 0x20, 0x4e, 0x45, 0x54, 0x57, 0x4f, 38 | 0x52, 0x4b, 0x53, 0x20, 0x33, 0x2e, 0x30, 0x00, 39 | 0x02, 0x4c, 0x41, 0x4e, 0x4d, 0x41, 0x4e, 0x31, 40 | 0x2e, 0x30, 0x00, 0x02, 0x4c, 0x4d, 0x31, 0x2e, 41 | 0x32, 0x58, 0x30, 0x30, 0x32, 0x00, 0x02, 0x44, 42 | 0x4f, 0x53, 0x20, 0x4c, 0x41, 0x4e, 0x4d, 0x41, 43 | 0x4e, 0x32, 0x2e, 0x31, 0x00, 0x02, 0x4c, 0x41, 44 | 0x4e, 0x4d, 0x41, 0x4e, 0x32, 0x2e, 0x31, 0x00, 45 | 0x02, 0x53, 0x61, 0x6d, 0x62, 0x61, 0x00, 0x02, 46 | 0x4e, 0x54, 0x20, 0x4c, 0x41, 0x4e, 0x4d, 0x41, 47 | 0x4e, 0x20, 0x31, 0x2e, 0x30, 0x00, 0x02, 0x4e, 48 | 0x54, 0x20, 0x4c, 0x4d, 0x20, 0x30, 0x2e, 0x31, 49 | 0x32, 0x00, 0x02, 0x53, 0x4d, 0x42, 0x20, 0x32, 50 | 0x2e, 0x30, 0x30, 0x32, 0x00, 0x02, 0x53, 0x4d, 51 | 0x42, 0x20, 0x32, 0x2e, 0x3f, 0x3f, 0x3f, 0x00, 52 | } 53 | 54 | // SMB1OnlyNegotiateProtocolRequest is a SMB1 request 55 | var SMB1OnlyNegotiateProtocolRequest = []byte{ 56 | 0x00, 0x00, 0x00, 0xbe, 0xff, 0x53, 0x4d, 0x42, 57 | 0x72, 0x00, 0x00, 0x00, 0x00, 0x18, 0x43, 0xc8, 58 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 59 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 60 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x9b, 0x00, 0x02, 61 | 0x50, 0x43, 0x20, 0x4e, 0x45, 0x54, 0x57, 0x4f, 62 | 0x52, 0x4b, 0x20, 0x50, 0x52, 0x4f, 0x47, 0x52, 63 | 0x41, 0x4d, 0x20, 0x31, 0x2e, 0x30, 0x00, 0x02, 64 | 0x4d, 0x49, 0x43, 0x52, 0x4f, 0x53, 0x4f, 0x46, 65 | 0x54, 0x20, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 66 | 0x4b, 0x53, 0x20, 0x31, 0x2e, 0x30, 0x33, 0x00, 67 | 0x02, 0x4d, 0x49, 0x43, 0x52, 0x4f, 0x53, 0x4f, 68 | 0x46, 0x54, 0x20, 0x4e, 0x45, 0x54, 0x57, 0x4f, 69 | 0x52, 0x4b, 0x53, 0x20, 0x33, 0x2e, 0x30, 0x00, 70 | 0x02, 0x4c, 0x41, 0x4e, 0x4d, 0x41, 0x4e, 0x31, 71 | 0x2e, 0x30, 0x00, 0x02, 0x4c, 0x4d, 0x31, 0x2e, 72 | 0x32, 0x58, 0x30, 0x30, 0x32, 0x00, 0x02, 0x44, 73 | 0x4f, 0x53, 0x20, 0x4c, 0x41, 0x4e, 0x4d, 0x41, 74 | 0x4e, 0x32, 0x2e, 0x31, 0x00, 0x02, 0x4c, 0x41, 75 | 0x4e, 0x4d, 0x41, 0x4e, 0x32, 0x2e, 0x31, 0x00, 76 | 0x02, 0x53, 0x61, 0x6d, 0x62, 0x61, 0x00, 0x02, 77 | 0x4e, 0x54, 0x20, 0x4c, 0x41, 0x4e, 0x4d, 0x41, 78 | 0x4e, 0x20, 0x31, 0x2e, 0x30, 0x00, 0x02, 0x4e, 79 | 0x54, 0x20, 0x4c, 0x4d, 0x20, 0x30, 0x2e, 0x31, 80 | 0x32, 0x00, 81 | } 82 | 83 | // SMB1SessionSetupNTLMSSP is a SMB1 SessionSetup NTLMSSP request 84 | var SMB1SessionSetupNTLMSSP = []byte{ 85 | 0x00, 0x00, 0x00, 0x9c, 0xff, 0x53, 0x4d, 0x42, 86 | 0x73, 0x00, 0x00, 0x00, 0x00, 0x18, 0x43, 0xc8, 87 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 88 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 89 | 0x00, 0x00, 0x01, 0x00, 0x0c, 0xff, 0x00, 0x00, 90 | 0x00, 0xff, 0xff, 0x02, 0x00, 0x01, 0x00, 0x00, 91 | 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 92 | 0x00, 0x54, 0xc0, 0x00, 0x80, 0x61, 0x00, 0x60, 93 | 0x48, 0x06, 0x06, 0x2b, 0x06, 0x01, 0x05, 0x05, 94 | 0x02, 0xa0, 0x3e, 0x30, 0x3c, 0xa0, 0x0e, 0x30, 95 | 0x0c, 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, 96 | 0x82, 0x37, 0x02, 0x02, 0x0a, 0xa2, 0x2a, 0x04, 97 | 0x28, 0x4e, 0x54, 0x4c, 0x4d, 0x53, 0x53, 0x50, 98 | 0x00, 0x01, 0x00, 0x00, 0x00, 0x15, 0x82, 0x08, 99 | 0x62, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 100 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 101 | 0x00, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 102 | 0x0f, 0x00, 0x55, 0x00, 0x6e, 0x00, 0x69, 0x00, 103 | 0x78, 0x00, 0x00, 0x00, 0x53, 0x00, 0x61, 0x00, 104 | 0x6d, 0x00, 0x62, 0x00, 0x61, 0x00, 0x00, 0x00, 105 | } 106 | 107 | // SMB2SessionSetupNTLMSSP is a SMB2 SessionSetup NTLMSSP request 108 | var SMB2SessionSetupNTLMSSP = []byte{ 109 | 0x00, 0x00, 0x00, 0xa2, 0xfe, 0x53, 0x4d, 0x42, 110 | 0x40, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 111 | 0x01, 0x00, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 112 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 113 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 114 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 115 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 116 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 117 | 0x00, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x01, 118 | 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 119 | 0x58, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, 0x00, 120 | 0x00, 0x00, 0x00, 0x00, 0x60, 0x48, 0x06, 0x06, 121 | 0x2b, 0x06, 0x01, 0x05, 0x05, 0x02, 0xa0, 0x3e, 122 | 0x30, 0x3c, 0xa0, 0x0e, 0x30, 0x0c, 0x06, 0x0a, 123 | 0x2b, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x02, 124 | 0x02, 0x0a, 0xa2, 0x2a, 0x04, 0x28, 0x4e, 0x54, 125 | 0x4c, 0x4d, 0x53, 0x53, 0x50, 0x00, 0x01, 0x00, 126 | 0x00, 0x00, 0x97, 0x82, 0x08, 0xe2, 0x00, 0x00, 127 | 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 128 | 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x0a, 0x00, 129 | 0xba, 0x47, 0x00, 0x00, 0x00, 0x0f, 130 | } 131 | 132 | // SMBSendData writes a SMB request to a socket 133 | func SMBSendData(conn net.Conn, data []byte) error { 134 | err := conn.SetWriteDeadline(time.Now().Add(SMBReadTimeout)) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | n, err := conn.Write(data) 140 | if err != nil { 141 | return err 142 | } 143 | _ = n 144 | 145 | return nil 146 | } 147 | 148 | // SMBReadFrame reads the netbios header then the full response 149 | func SMBReadFrame(conn net.Conn, t time.Duration) ([]byte, error) { 150 | timeout := time.Now().Add(t) 151 | res := []byte{} 152 | nbh := make([]byte, 4) 153 | 154 | err := conn.SetReadDeadline(timeout) 155 | if err != nil { 156 | return res, err 157 | } 158 | 159 | // Read the NetBIOS header 160 | n, err := conn.Read(nbh[:]) 161 | if err != nil { 162 | // Return if EOF is reached 163 | if err == io.EOF { 164 | return res, nil 165 | } 166 | 167 | // Return if timeout is reached 168 | if err, ok := err.(net.Error); ok && err.Timeout() { 169 | return res, nil 170 | } 171 | 172 | // If we have data and received an error, it was probably a reset 173 | if len(res) > 0 { 174 | return res, nil 175 | } 176 | 177 | return res, err 178 | } 179 | 180 | if n != 4 { 181 | return res, nil 182 | } 183 | 184 | res = append(res[:], nbh[:n]...) 185 | dlen := binary.BigEndian.Uint32(nbh[:]) & 0x00ffffff 186 | buf := make([]byte, dlen) 187 | n, err = conn.Read(buf[:]) 188 | if err != nil { 189 | // Return if EOF is reached 190 | if err == io.EOF { 191 | return res, nil 192 | } 193 | 194 | // Return if timeout is reached 195 | if err, ok := err.(net.Error); ok && err.Timeout() { 196 | return res, nil 197 | } 198 | 199 | // If we have data and received an error, it was probably a reset 200 | if len(res) > 0 { 201 | return res, nil 202 | } 203 | 204 | return res, err 205 | } 206 | res = append(res[:], buf[:n]...) 207 | return res, nil 208 | } 209 | 210 | // SMB2NegotiateProtocolRequest generates a new Negotiate request with the specified target name 211 | func SMB2NegotiateProtocolRequest(dst string) []byte { 212 | 213 | base := []byte{ 214 | 0xfe, 0x53, 0x4d, 0x42, 215 | 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 216 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 217 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 218 | 0x00, 0x00, 0x00, 0x00, 0xff, 0xfe, 0x00, 0x00, 219 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 220 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 221 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 222 | 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x05, 0x00, 223 | 0x01, 0x00, 0x00, 0x00, 0x7f, 0x00, 0x00, 0x00, 224 | } 225 | 226 | // Client GUID (16) 227 | base = append(base[:], RandomBytes(16)...) 228 | 229 | base = append(base[:], []byte{ 230 | 0x70, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 231 | 0x02, 0x02, 0x10, 0x02, 0x00, 0x03, 0x02, 0x03, 232 | 0x11, 0x03, 0x00, 0x00, 0x01, 0x00, 0x26, 0x00, 233 | 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x20, 0x00, 234 | 0x01, 0x00, 235 | }...) 236 | 237 | // SHA-512 Salt (32) 238 | base = append(base[:], RandomBytes(32)...) 239 | 240 | base = append(base[:], []byte{ 241 | 0x00, 0x00, 0x02, 0x00, 0x06, 0x00, 242 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 243 | 0x01, 0x00, 0x00, 0x00, 0x03, 0x00, 0x0e, 0x00, 244 | 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 245 | 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x03, 0x00, 246 | 0x01, 0x00, 0x00, 0x00, 247 | }...) 248 | 249 | encodedDst := make([]byte, len(dst)*2) 250 | for i, b := range []byte(dst) { 251 | encodedDst[i*2] = b 252 | } 253 | 254 | netname := []byte{0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 255 | binary.LittleEndian.PutUint16(netname[2:], uint16(len(encodedDst))) 256 | netname = append(netname, encodedDst...) 257 | 258 | base = append(base, netname...) 259 | 260 | nbhd := make([]byte, 4) 261 | binary.BigEndian.PutUint32(nbhd, uint32(len(base))) 262 | nbhd = append(nbhd, base...) 263 | return nbhd 264 | } 265 | 266 | // SMBExtractValueFromOffset peels a field out of a SMB buffer 267 | func SMBExtractValueFromOffset(blob []byte, idx int) ([]byte, int, error) { 268 | res := []byte{} 269 | 270 | if len(blob) < (idx + 6) { 271 | return res, idx, fmt.Errorf("data truncated") 272 | } 273 | 274 | len1 := binary.LittleEndian.Uint16(blob[idx:]) 275 | idx += 2 276 | 277 | // len2 := binary.LittleEndian.Uint16(blob[idx:]) 278 | idx += 2 279 | 280 | off := binary.LittleEndian.Uint32(blob[idx:]) 281 | idx += 4 282 | 283 | // Allow zero length values 284 | if len1 == 0 { 285 | return res, idx, nil 286 | } 287 | 288 | if len(blob) < int(off+uint32(len1)) { 289 | return res, idx, fmt.Errorf("data value truncated") 290 | } 291 | 292 | res = append(res, blob[off:off+uint32(len1)]...) 293 | return res, idx, nil 294 | } 295 | 296 | // SMB1ExtractNativeFieldsFromSessionSetupReply tries to extract NativeOS/NativeLM fields from a SMB1 session setup response 297 | func SMB1ExtractNativeFieldsFromSessionSetupReply(blob []byte, info map[string]string) { 298 | 299 | smbOffset := bytes.Index(blob, []byte{0xff, 'S', 'M', 'B'}) 300 | if smbOffset < 0 { 301 | return 302 | } 303 | 304 | data := blob[smbOffset:] 305 | if len(data) < 41 { 306 | return 307 | } 308 | 309 | // Read the Security Blob Length and then skip past it 310 | sbl := int(binary.LittleEndian.Uint16(data[39:])) 311 | nbi := sbl + 39 + 4 312 | if len(data) < nbi { 313 | return 314 | } 315 | 316 | data = data[nbi:] 317 | bits := strings.Split(string(data), "\x00\x00") 318 | if len(bits) < 3 { 319 | return 320 | } 321 | 322 | // Extract the last three elements 323 | info["smb.NativeOS"] = TrimName(bits[0]) 324 | info["smb.NativeLM"] = TrimName(bits[1]) 325 | info["smb.Domain"] = TrimName(bits[2]) 326 | } 327 | 328 | // SMB2ExtractSIDFromSessionSetupReply tries to extract the SessionID and Signature from a SMB2 reply 329 | func SMB2ExtractSIDFromSessionSetupReply(blob []byte, info map[string]string) { 330 | smbOffset := bytes.Index(blob, []byte{0xfe, 'S', 'M', 'B'}) 331 | if smbOffset < 0 { 332 | return 333 | } 334 | 335 | smbData := blob[smbOffset:] 336 | if len(smbData) < 48 { 337 | return 338 | } 339 | 340 | status := binary.LittleEndian.Uint32(smbData[8:]) 341 | info["smb.Status"] = fmt.Sprintf("0x%.8x", status) 342 | 343 | sessID := binary.LittleEndian.Uint64(smbData[40:]) 344 | info["smb.SessionID"] = fmt.Sprintf("0x%.16x", sessID) 345 | 346 | if len(smbData) >= 64 { 347 | sigData := hex.EncodeToString(smbData[48:64]) 348 | if sigData != "00000000000000000000000000000000" { 349 | info["smb.Signature"] = sigData 350 | } 351 | } 352 | } 353 | 354 | // SMBExtractFieldsFromSecurityBlob extracts fields from the NTLMSSP response 355 | func SMBExtractFieldsFromSecurityBlob(blob []byte, info map[string]string) { 356 | var err error 357 | 358 | ntlmsspOffset := bytes.Index(blob, []byte{'N', 'T', 'L', 'M', 'S', 'S', 'P', 0x00, 0x02, 0x00, 0x00, 0x00}) 359 | if ntlmsspOffset < 0 { 360 | return 361 | } 362 | 363 | data := blob[ntlmsspOffset:] 364 | 365 | // Basic sanity check 366 | if len(data) < (12 + 6 + 12 + 8 + 6 + 8) { 367 | return 368 | } 369 | 370 | idx := 12 371 | 372 | targetName, idx, err := SMBExtractValueFromOffset(data, idx) 373 | if err != nil { 374 | return 375 | } 376 | 377 | // Negotiate Flags 378 | negotiateFlags := binary.LittleEndian.Uint32(data[idx:]) 379 | info["ntlmssp.NegotiationFlags"] = fmt.Sprintf("0x%.8x", negotiateFlags) 380 | idx += 4 381 | 382 | // NTLM Server Challenge 383 | idx += 8 384 | 385 | // Reserved 386 | idx += 8 387 | 388 | // Target Info 389 | targetInfo, idx, err := SMBExtractValueFromOffset(data, idx) 390 | if err != nil { 391 | return 392 | } 393 | 394 | // Version 395 | versionMajor := uint8(data[idx]) 396 | idx++ 397 | 398 | versionMinor := uint8(data[idx]) 399 | idx++ 400 | 401 | versionBuild := binary.LittleEndian.Uint16(data[idx:]) 402 | idx += 2 403 | 404 | ntlmRevision := binary.BigEndian.Uint32(data[idx:]) 405 | 406 | // macOS reverses the endian order of this field for some reason 407 | if ntlmRevision == 251658240 { 408 | ntlmRevision = binary.LittleEndian.Uint32(data[idx:]) 409 | } 410 | 411 | info["ntlmssp.Version"] = fmt.Sprintf("%d.%d.%d", versionMajor, versionMinor, versionBuild) 412 | info["ntlmssp.NTLMRevision"] = fmt.Sprintf("%d", ntlmRevision) 413 | info["ntlmssp.TargetName"] = TrimName(string(targetName)) 414 | 415 | idx = 0 416 | for { 417 | if idx+4 > len(targetInfo) { 418 | break 419 | } 420 | 421 | attrType := binary.LittleEndian.Uint16(targetInfo[idx:]) 422 | idx += 2 423 | 424 | // End of List 425 | if attrType == 0 { 426 | break 427 | } 428 | 429 | attrLen := binary.LittleEndian.Uint16(targetInfo[idx:]) 430 | idx += 2 431 | 432 | if idx+int(attrLen) > len(targetInfo) { 433 | // log.Printf("too short: %d/%d", idx+int(attrLen), len(targetInfo)) 434 | break 435 | } 436 | 437 | attrVal := targetInfo[idx : idx+int(attrLen)] 438 | idx += int(attrLen) 439 | 440 | switch attrType { 441 | case 1: 442 | info["ntlmssp.NetbiosComputer"] = TrimName(string(attrVal)) 443 | case 2: 444 | info["ntlmssp.NetbiosDomain"] = TrimName(string(attrVal)) 445 | case 3: 446 | info["ntlmssp.DNSComputer"] = TrimName(string(attrVal)) 447 | case 4: 448 | info["ntlmssp.DNSDomain"] = TrimName(string(attrVal)) 449 | case 7: 450 | ts := binary.LittleEndian.Uint64(attrVal[:]) 451 | info["ntlmssp.Timestamp"] = fmt.Sprintf("0x%.16x", ts) 452 | 453 | } 454 | 455 | // End of List 456 | if attrType == 0 { 457 | break 458 | } 459 | } 460 | } 461 | 462 | // SMB2ExtractFieldsFromNegotiateReply extracts useful fields from the SMB2 negotiate response 463 | func SMB2ExtractFieldsFromNegotiateReply(blob []byte, info map[string]string) { 464 | 465 | smbOffset := bytes.Index(blob, []byte{0xfe, 'S', 'M', 'B'}) 466 | if smbOffset < 0 { 467 | return 468 | } 469 | 470 | data := blob[smbOffset:] 471 | 472 | // Basic sanity check 473 | if len(data) < (64 + 8 + 16 + 36) { 474 | return 475 | } 476 | 477 | switch binary.LittleEndian.Uint16(data[64+2:]) { 478 | case 0: 479 | info["smb.Signing"] = "disabled" 480 | case 1: 481 | info["smb.Signing"] = "enabled" 482 | case 2, 3: 483 | info["smb.Signing"] = "required" 484 | } 485 | 486 | info["smb.Dialect"] = fmt.Sprintf("0x%.4x", binary.LittleEndian.Uint16(data[64+4:])) 487 | info["smb.GUID"] = uuid.FromBytesOrNil(data[64+8 : 64+8+16]).String() 488 | info["smb.Capabilities"] = fmt.Sprintf("0x%.8x", binary.LittleEndian.Uint32(data[64+8+16:])) 489 | 490 | negCtxCount := int(binary.LittleEndian.Uint16(data[64+6:])) 491 | negCtxOffset := int(binary.LittleEndian.Uint32(data[64+8+16+36:])) 492 | if negCtxCount == 0 || negCtxOffset == 0 || negCtxOffset+(negCtxCount*8) > len(data) { 493 | return 494 | } 495 | 496 | negCtxData := data[negCtxOffset:] 497 | idx := 0 498 | for { 499 | if idx+8 > len(negCtxData) { 500 | break 501 | } 502 | negType := int(binary.LittleEndian.Uint16(negCtxData[idx:])) 503 | negLen := int(binary.LittleEndian.Uint16(negCtxData[idx+2:])) 504 | idx += 8 505 | 506 | if idx+negLen > len(negCtxData) { 507 | break 508 | } 509 | negData := negCtxData[idx : idx+negLen] 510 | 511 | SMB2ParseNegotiateContext(negType, negData, info) 512 | 513 | // Move the index to the next context 514 | idx += negLen 515 | // Negotiate Contexts are aligned on 64-bit boundaries 516 | for idx%8 != 0 { 517 | idx++ 518 | } 519 | } 520 | } 521 | 522 | // SMB2ParseNegotiateContext decodes fields from the SMB2 Negotiate Context values 523 | func SMB2ParseNegotiateContext(t int, data []byte, info map[string]string) { 524 | switch t { 525 | case 1: 526 | // SMB2_PREAUTH_INTEGRITY_CAPABILITIES 527 | if len(data) < 6 { 528 | return 529 | } 530 | hashCount := int(binary.LittleEndian.Uint16(data[:])) 531 | // MUST only be one in responses 532 | if hashCount != 1 { 533 | return 534 | } 535 | hashSaltLen := int(binary.LittleEndian.Uint16(data[2:])) 536 | hashType := int(binary.LittleEndian.Uint16(data[4:])) 537 | hashName := "sha512" 538 | if hashType != 1 { 539 | hashName = fmt.Sprintf("unknown-%d", hashType) 540 | } 541 | info["smb.HashAlg"] = hashName 542 | info["smb.HashSaltLen"] = fmt.Sprintf("%d", hashSaltLen) 543 | 544 | case 2: 545 | // SMB2_ENCRYPTION_CAPABILITIES 546 | if len(data) < 4 { 547 | return 548 | } 549 | cipherCount := int(binary.LittleEndian.Uint16(data[:])) 550 | if len(data) < 2+(2*cipherCount) { 551 | return 552 | } 553 | // MUST only be one in responses 554 | if cipherCount != 1 { 555 | return 556 | } 557 | cipherList := []string{} 558 | for i := 0; i < cipherCount; i++ { 559 | cipherID := int(binary.LittleEndian.Uint16(data[2+(i*2):])) 560 | cipherName := "" 561 | switch cipherID { 562 | case 1: 563 | cipherName = "aes-128-ccm" 564 | case 2: 565 | cipherName = "aes-128-gcm" 566 | default: 567 | cipherName = fmt.Sprintf("unknown-%d", cipherID) 568 | } 569 | cipherList = append(cipherList, cipherName) 570 | } 571 | 572 | info["smb.CipherAlg"] = strings.Join(cipherList, "\t") 573 | 574 | case 3: 575 | // SMB2_COMPRESSION_CAPABILITIES 576 | if len(data) < 10 { 577 | return 578 | } 579 | compCount := int(binary.LittleEndian.Uint16(data[:])) 580 | if len(data) < 2+2+4+(2*compCount) { 581 | return 582 | } 583 | // MUST only be one in responses 584 | if compCount != 1 { 585 | return 586 | } 587 | compList := []string{} 588 | for i := 0; i < compCount; i++ { 589 | compID := int(binary.LittleEndian.Uint16(data[8+(i*2):])) 590 | compName := "" 591 | switch compID { 592 | case 0: 593 | compName = "none" 594 | case 1: 595 | compName = "lznt1" 596 | case 2: 597 | compName = "lz77" 598 | case 3: 599 | compName = "lz77+huff" 600 | case 4: 601 | compName = "patternv1" 602 | default: 603 | compName = fmt.Sprintf("unknown-%d", compID) 604 | } 605 | compList = append(compList, compName) 606 | } 607 | info["smb.CompressionFlags"] = fmt.Sprintf("0x%.4x", binary.LittleEndian.Uint32(data[4:])) 608 | info["smb.CompressionAlg"] = strings.Join(compList, "\t") 609 | } 610 | } 611 | -------------------------------------------------------------------------------- /pkg/rnd/strings.go: -------------------------------------------------------------------------------- 1 | package rnd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | // SanitizeStringForJSON scrubs a given string of invalid UTF8 for JSON encoding 9 | func SanitizeStringForJSON(str string) string { 10 | // Remove null bytes, which break some json implementations 11 | str = strings.Replace(str, "\x00", "", -1) 12 | 13 | // Remove invalid UTF-8 sequences (per the Go definition) 14 | return string([]rune(str)) 15 | } 16 | 17 | // EnsureTrailingDot returns a copy of the string with a trailing dot, if one does not exist 18 | func EnsureTrailingDot(s string) string { 19 | // Ensure that the name has a trailing dot 20 | if len(s) > 0 && s[len(s)-1:len(s)] != "." { 21 | return s + "." 22 | } 23 | return s 24 | } 25 | 26 | // TrimName removes null bytes and trims leading and trailing spaces from a string 27 | func TrimName(name string) string { 28 | return strings.TrimSpace(strings.Replace(name, "\x00", "", -1)) 29 | } 30 | 31 | // U64SliceToSeq turns an array of ints into a hex string 32 | func U64SliceToSeq(s []uint64) string { 33 | seq := []string{} 34 | for _, v := range s { 35 | seq = append(seq, fmt.Sprintf("%x", v)) 36 | } 37 | return strings.Join(seq, "-") 38 | } 39 | -------------------------------------------------------------------------------- /pkg/rnd/time.go: -------------------------------------------------------------------------------- 1 | package rnd 2 | 3 | import "time" 4 | 5 | /* 6 | 7 | https://github.com/tatsushid/go-fastping 8 | 9 | The MIT License (MIT) 10 | 11 | Copyright (c) 2013 Tatsushi Demachi 12 | 13 | Permission is hereby granted, free of charge, to any person obtaining a copy of 14 | this software and associated documentation files (the "Software"), to deal in 15 | the Software without restriction, including without limitation the rights to 16 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 17 | the Software, and to permit persons to whom the Software is furnished to do so, 18 | subject to the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be included in all 21 | copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 25 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 26 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 27 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 28 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | */ 31 | 32 | // TimestampToBytes converts a timestamp to an 8-byte array 33 | func TimestampToBytes(t time.Time) []byte { 34 | nsec := t.UnixNano() 35 | b := make([]byte, 8) 36 | for i := uint8(0); i < 8; i++ { 37 | b[i] = byte((nsec >> ((7 - i) * 8)) & 0xff) 38 | } 39 | return b 40 | } 41 | 42 | // BytesToTimestamp converts an 8-byte array to a timestamp 43 | func BytesToTimestamp(b []byte) time.Time { 44 | var nsec int64 45 | for i := uint8(0); i < 8; i++ { 46 | nsec += int64(b[i]) << ((7 - i) * 8) 47 | } 48 | return time.Unix(nsec/1000000000, nsec%1000000000) 49 | } 50 | --------------------------------------------------------------------------------