From 4d948fca6bc6cc53db5bdb362d574e2994ea3e54 Mon Sep 17 00:00:00 2001 From: Steven Polley Date: Sat, 6 Apr 2024 21:59:13 -0600 Subject: [PATCH] initial commit --- .gitignore | 4 ++ README.md | 2 +- client/README.md | 9 ++++ client/go.mod | 5 ++ client/go.sum | 2 + client/main.go | 51 ++++++++++++++++++++ go.work | 7 +++ server/README.md | 25 ++++++++++ server/go.mod | 10 ++++ server/go.sum | 19 ++++++++ server/main.go | 74 +++++++++++++++++++++++++++++ server/packet.go | 120 +++++++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 327 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 client/README.md create mode 100644 client/go.mod create mode 100644 client/go.sum create mode 100644 client/main.go create mode 100644 go.work create mode 100644 server/README.md create mode 100644 server/go.mod create mode 100644 server/go.sum create mode 100644 server/main.go create mode 100644 server/packet.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..035fc7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +hyp.secret +*.exe +hypd +hyp \ No newline at end of file diff --git a/README.md b/README.md index 7cd880f..69389f5 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Option 1 is the default option and is analogous to having your services internet ### Brute Force Simple Overview -To put it in simple terms, hyp requires an adversary to guess a number between 1 and 18,446,744,073,709,551,615 within 90 seconds. Each guess attempt requires four ordered UDP datagrams to be sent. The requirement for correct order on arrival, multiple network paths, and network latency means the datagrams have to be spaced out and transmitted one at a time with time spent waiting before the next datagram is sent. An odd but perhaps useful implication of this is that the further away you are (higher latency), the less reliable guess attempts you can make before the number changes. With 20ms of latency, you can perform a maximum of 4,500 reliable guesses. With 100ms of latency, you can only perform a maximum of 900 reliable guesses. +To put it in simple terms, hyp requires an adversary to guess a number between 1 and 18,446,744,073,709,551,615 within 90 seconds. Each guess attempt requires four ordered UDP datagrams to be transmitted. The requirement for correct order on arrival, multiple network paths, and network latency means the datagrams have to be spaced out and transmitted one at a time with time spent waiting before the next datagram is sent. An odd but perhaps useful implication of this is that the further away you are (higher latency), the less reliable guess attempts you can make before the number changes. With 20ms of latency, you can perform a maximum of 4,500 reliable guesses. With 100ms of latency, you can only perform a maximum of 900 reliable guesses. ### Protection Against Replay Attacks diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..dcd7884 --- /dev/null +++ b/client/README.md @@ -0,0 +1,9 @@ +# hyp-client + +The client expects there to be a file named hyp.secret in the same directory. This file is generated by the hypd server using ./hypd generatesecret. + +```bash +# Example Usage +# ./hyp-client +./hyp-client 192.168.50.5 +``` \ No newline at end of file diff --git a/client/go.mod b/client/go.mod new file mode 100644 index 0000000..fd58581 --- /dev/null +++ b/client/go.mod @@ -0,0 +1,5 @@ +module deadbeef.codes/steven/hyp-client + +go 1.22.0 + +require deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407035202-7ccdf4d89fee diff --git a/client/go.sum b/client/go.sum new file mode 100644 index 0000000..d413509 --- /dev/null +++ b/client/go.sum @@ -0,0 +1,2 @@ +deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407035202-7ccdf4d89fee h1:r9xdoIGPhc/tXzyOj+lCDb+ReYa/zkysqK3ZRCqpyIU= +deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407035202-7ccdf4d89fee/go.mod h1:NANpGD/K+nDkW+bkomchwaXMrsXWza58+8AR7Sau3fs= diff --git a/client/main.go b/client/main.go new file mode 100644 index 0000000..3fe0611 --- /dev/null +++ b/client/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "fmt" + "log" + "net" + "os" + "time" + + "deadbeef.codes/steven/hyp/otphyp" +) + +// MaxNetworkLatency specifies the number of milliseconds +// A packet-switched network... more like "race condition factory" +const MaxNetworkLatency = 500 + +func main() { + + // validate number of input arguments + if len(os.Args) < 2 { + usage() + os.Exit(1) + } + + // load secret and generate ports using secret and current time + secretBytes, err := os.ReadFile("hyp.secret") + if err != nil { + log.Fatalf("failed to read file 'hyp.secret': %v", err) + } + sharedSecret := string(secretBytes) + + ports, err := otphyp.GeneratePorts(sharedSecret, time.Now()) + if err != nil { + log.Fatalf("failed to generate ports from shared secret: %v", err) + } + + // Transmit + fmt.Println("Transmitting knock sequence:", ports) + for _, port := range ports { + conn, _ := net.Dial("udp", fmt.Sprintf("%s:%d", os.Args[1], port)) + conn.Write([]byte{0}) + conn.Close() + time.Sleep(time.Millisecond * MaxNetworkLatency) + } +} + +func usage() { + fmt.Println(os.Args[0], "usage") + fmt.Println("Supply an ordered list of ports to knock") + fmt.Println(os.Args[0], "server") +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..9b1a11a --- /dev/null +++ b/go.work @@ -0,0 +1,7 @@ +go 1.22.0 + +use ( + ./client + ./otphyp + ./server +) diff --git a/server/README.md b/server/README.md new file mode 100644 index 0000000..809f90a --- /dev/null +++ b/server/README.md @@ -0,0 +1,25 @@ +# hypd + +hypd is the hyp (Hide Your Ports) server. + +### Requirements + +On the server, libpcap-dev needs to be installed on linux or pcap from nmap.org on Windows. + +### Usage + +##### Generating a new shared key + +```bash +# As user that can write to current directory +./hypd generatesecret +``` + +Then copy the file to a client + +##### Starting the server + +```bash +# As root - or user that can capture packets and modify IPTables +./hypd server eth0 +``` \ No newline at end of file diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..569e82e --- /dev/null +++ b/server/go.mod @@ -0,0 +1,10 @@ +module deadbeef.codes/steven/hypd + +go 1.22.0 + +require ( + deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407035202-7ccdf4d89fee + github.com/google/gopacket v1.1.19 +) + +require golang.org/x/sys v0.19.0 // indirect diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..dedd2c1 --- /dev/null +++ b/server/go.sum @@ -0,0 +1,19 @@ +deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407035202-7ccdf4d89fee h1:r9xdoIGPhc/tXzyOj+lCDb+ReYa/zkysqK3ZRCqpyIU= +deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407035202-7ccdf4d89fee/go.mod h1:NANpGD/K+nDkW+bkomchwaXMrsXWza58+8AR7Sau3fs= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000..a2a1100 --- /dev/null +++ b/server/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/exec" + + "deadbeef.codes/steven/hyp/otphyp" +) + +func main() { + + if len(os.Args) < 2 { + usage() + } + + switch os.Args[1] { + case "generatesecret": + sharedSecret, err := otphyp.GenerateSecret() + if err != nil { + log.Fatalf("failed to generate shared secret: %v", err) + } + f, err := os.Create("hyp.secret") + if err != nil { + log.Fatalf("failed to create file 'hyp.secret': %v", err) + } + _, err = f.WriteString(sharedSecret) + if err != nil { + log.Fatalf("failed to write to file 'hyp.secret': %v", err) + } + f.Close() + fmt.Println("Created file hyp.secret") + case "server": + secretBytes, err := os.ReadFile("hyp.secret") + if err != nil { + log.Fatalf("failed to read file 'hyp.secret': %v", err) + } + sharedSecret = string(secretBytes) + if len(os.Args) < 3 { + usage() + } + packetServer(os.Args[2]) + } +} + +func usage() { + fmt.Print(`hyp + Commands: + generatesecret - creates a pre shared key file named hyp.secret which can be distributed to a trusted client + server - TBD + + Example Usage: + + # Linux + hyp server "/dev/eth0" + + # Windows - get-netadapter | where {$_.Name -eq “Ethernet”} | Select-Object -Property DeviceName + hyp server "\\Device\\NPF_{A066F7DE-CC2D-4E4B-97C4-BF0EC4C03649}" + +`) + os.Exit(1) +} + +// TBD: Implement +func handleSuccess(srcip string) { + fmt.Println("Success for ", srcip) + + cmd := exec.Command("iptables", "-A", "INPUT", "-p", "tcp", "-s", srcip, "--dport", "22", "-j", "ACCEPT") + err := cmd.Run() + if err != nil { + log.Printf("failed to execute iptables command for '%s': %v", srcip, err) + } +} diff --git a/server/packet.go b/server/packet.go new file mode 100644 index 0000000..81a5f02 --- /dev/null +++ b/server/packet.go @@ -0,0 +1,120 @@ +package main + +import ( + "encoding/binary" + "fmt" + "log" + "time" + + "deadbeef.codes/steven/hyp/otphyp" + "github.com/google/gopacket" + "github.com/google/gopacket/pcap" +) + +// type client is used to keept track of a client attempting to perform an authentic knock sequence +type Client struct { + Progress int // index of current progress in sequence. Value of 1 means first port has been matched + Sequence [4]uint16 // stores the knock sequence the current client is attempting. It's set and tracked here to prevent race conditions during a knock sequence being received and key rotations +} + +var ( + clients map[string]*Client // Contains a map of clients + portSequences [][4]uint16 + sharedSecret string // base32 encoded shared secret used for totp +) + +func packetServer(captureDevice string) { + clients = make(map[string]*Client, 0) // key is flow, value is the current progress through the sequence. i.e. value of 1 means that the first port in the sequence was successful + portSequences = [][4]uint16{} // Slice of accepted port sequences, there have to be several to account for clock skew between client and server + + handle, err := pcap.OpenLive(captureDevice, 1600, true, pcap.BlockForever) + if err != nil { + log.Fatalf("failed to open adapter") + } + packetSource := gopacket.NewPacketSource(handle, handle.LinkType()) + + go rotateSequence(handle) + for packet := range packetSource.Packets() { + handlePacket(packet) // Do something with a packet here. + } +} + +// packets that match the BPF filter get passed to handlePacket +func handlePacket(packet gopacket.Packet) { + port := binary.BigEndian.Uint16(packet.TransportLayer().TransportFlow().Dst().Raw()) + srcip := packet.NetworkLayer().NetworkFlow().Src().String() + client, ok := clients[srcip] + if !ok { // create the client, identify which authentic knock sequence is matched + for _, sequence := range portSequences { + if port == sequence[0] { + clients[srcip] = &Client{Progress: 1, Sequence: sequence} + } + } + return + } + + // if it's wrong, reset progress + // TBD: vulnerable to sweep attack - this won't be triggered if a wrong packet doesn't match BPF filter + if port != client.Sequence[client.Progress] { + delete(clients, srcip) + fmt.Printf("port '%d' is in sequence, but came at unexpected order - resetting progress", port) + return + } + + // Client increases progress through sequence and checks if sequence is completed + client.Progress++ + if client.Progress >= len(client.Sequence) { + delete(clients, srcip) + handleSuccess(srcip) + return + } +} + +// Used to rotate the authentic port knock sequence +func rotateSequence(handle *pcap.Handle) { + for { + + if len(portSequences) < 3 { + t := time.Now().Add(time.Second * -30) + for i := 0; i < 3; i++ { + portSequence, err := otphyp.GeneratePorts(sharedSecret, t.Add((time.Second * 30 * time.Duration(i)))) + if err != nil { + log.Fatalf("failed to generate port knock sequence: %v", err) + } + portSequences = append(portSequences, portSequence) + } + } + + fmt.Println("New sequences:", portSequences) + err := setPacketFilter(handle) + if err != nil { + log.Printf("failed to change packet filter: %v", err) + } + + // Sleep until next 30 second offset + time.Sleep(time.Until(time.Now().Truncate(time.Second * 30).Add(time.Second * 30))) + + // TBD: pop first value and only generate latest (time.Now().Add(time.Second*30)) value instead of re-initializing completely + portSequences = [][4]uint16{} + } +} + +// Given a pcap handle and list of authentic port knock sequences, configures a BPF filter +func setPacketFilter(handle *pcap.Handle) error { + filter := "udp && (" + for i, portSequence := range portSequences { + for j, port := range portSequence { + if i == 0 && j == 0 { + filter += fmt.Sprint("port ", port) + } else { + filter += fmt.Sprint(" || port ", port) + } + } + } + filter += ")" + err := handle.SetBPFFilter(filter) + if err != nil { + return fmt.Errorf("failed to set BPF filter '%s': %v", filter, err) + } + return nil +}