initial commit
This commit is contained in:
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
|
||||
}
|
Reference in New Issue
Block a user