initial commit
This commit is contained in:
parent
7ccdf4d89f
commit
4d948fca6b
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
hyp.secret
|
||||
*.exe
|
||||
hypd
|
||||
hyp
|
@ -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
|
||||
|
||||
|
9
client/README.md
Normal file
9
client/README.md
Normal 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
5
client/go.mod
Normal 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
2
client/go.sum
Normal 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
51
client/main.go
Normal 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")
|
||||
}
|
25
server/README.md
Normal file
25
server/README.md
Normal 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
10
server/go.mod
Normal 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
19
server/go.sum
Normal 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
74
server/main.go
Normal 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
120
server/packet.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user