initial commit

This commit is contained in:
Steven Polley 2024-04-06 21:59:13 -06:00
parent 7ccdf4d89f
commit 4d948fca6b
12 changed files with 327 additions and 1 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
hyp.secret
*.exe
hypd
hyp

View File

@ -15,7 +15,7 @@ Option 1 is the default option and is analogous to having your services internet
### Brute Force Simple Overview ### 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 ### Protection Against Replay Attacks

9
client/README.md Normal file
View File

@ -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 <server>
./hyp-client 192.168.50.5
```

5
client/go.mod Normal file
View File

@ -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

2
client/go.sum Normal file
View File

@ -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=

51
client/main.go Normal file
View File

@ -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")
}

7
go.work Normal file
View File

@ -0,0 +1,7 @@
go 1.22.0
use (
./client
./otphyp
./server
)

25
server/README.md Normal file
View File

@ -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
```

10
server/go.mod Normal file
View File

@ -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

19
server/go.sum Normal file
View File

@ -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=

74
server/main.go Normal file
View File

@ -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 <command>
Commands:
generatesecret - creates a pre shared key file named hyp.secret which can be distributed to a trusted client
server <device> - 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)
}
}

120
server/packet.go Normal file
View File

@ -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
}