Compare commits
4 Commits
0.0.1
...
bd7fff97b3
Author | SHA1 | Date | |
---|---|---|---|
bd7fff97b3 | |||
19388ca140 | |||
b95f764fc9 | |||
27c2f28429 |
@ -1,5 +1,5 @@
|
||||
module deadbeef.codes/steven/hyp-client
|
||||
module deadbeef.codes/steven/hyp
|
||||
|
||||
go 1.22.0
|
||||
|
||||
require deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407035202-7ccdf4d89fee
|
||||
require deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407135923-27c2f284299f
|
||||
|
@ -1,2 +1,4 @@
|
||||
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=
|
||||
deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407135923-27c2f284299f h1:M+QxtB9hQ/+9MmvPtqOvXSL+LGU/f54VlPZHsM4Knao=
|
||||
deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407135923-27c2f284299f/go.mod h1:NANpGD/K+nDkW+bkomchwaXMrsXWza58+8AR7Sau3fs=
|
||||
|
@ -45,7 +45,16 @@ func main() {
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Println(os.Args[0], "usage")
|
||||
fmt.Println("Supply an ordered list of ports to knock")
|
||||
fmt.Println(os.Args[0], "server")
|
||||
fmt.Print(`hyp <server>
|
||||
|
||||
Example Usage:
|
||||
|
||||
# Transmit an authentic knock sequence to a server
|
||||
hyp 10.69.4.20
|
||||
|
||||
# You can use a DNS name too
|
||||
hyp hyp.stevenpolley.net
|
||||
|
||||
`)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ func GeneratePorts(sharedSecret string, t time.Time) (ports [4]uint16, err error
|
||||
// 2. Save the secret to a file
|
||||
// 3. Distribute the secret to a client
|
||||
func GenerateSecret() (sharedSecret string, err error) {
|
||||
sharedSecretBytes := make([]byte, 20)
|
||||
sharedSecretBytes := make([]byte, 500)
|
||||
r := rand.Reader
|
||||
_, err = r.Read([]byte(sharedSecretBytes))
|
||||
if err != nil {
|
||||
|
@ -3,7 +3,7 @@ module deadbeef.codes/steven/hypd
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407035202-7ccdf4d89fee
|
||||
deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407135923-27c2f284299f
|
||||
github.com/google/gopacket v1.1.19
|
||||
)
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
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=
|
||||
deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407135923-27c2f284299f h1:M+QxtB9hQ/+9MmvPtqOvXSL+LGU/f54VlPZHsM4Knao=
|
||||
deadbeef.codes/steven/hyp/otphyp v0.0.0-20240407135923-27c2f284299f/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=
|
||||
|
@ -41,28 +41,35 @@ func main() {
|
||||
usage()
|
||||
}
|
||||
packetServer(os.Args[2])
|
||||
default:
|
||||
usage()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func usage() {
|
||||
fmt.Print(`hyp <command>
|
||||
fmt.Print(`hypd <command>
|
||||
|
||||
Commands:
|
||||
generatesecret - creates a pre shared key file named hyp.secret which can be distributed to a trusted client
|
||||
server <device> - TBD
|
||||
generatesecret - creates a pre shared key file named hyp.secret which can be distributed to a trusted client
|
||||
server <device> - runs the hypd server watching for an authentic knock sequence
|
||||
|
||||
Example Usage:
|
||||
|
||||
# Linux
|
||||
hyp server "/dev/eth0"
|
||||
# Generate a secret, to be shared with a trusted client
|
||||
hypd generatesecret
|
||||
|
||||
# Windows - get-netadapter | where {$_.Name -eq “Ethernet”} | Select-Object -Property DeviceName
|
||||
hyp server "\\Device\\NPF_{A066F7DE-CC2D-4E4B-97C4-BF0EC4C03649}"
|
||||
# Linux - ip link
|
||||
hypd server eth0
|
||||
|
||||
# Windows - get-netadapter | where {$_.Name -eq “Ethernet”} | Select-Object -Property DeviceName
|
||||
hypd server "\\Device\\NPF_{A066F7DE-CC2D-4E4B-97C4-BF0EC4C03649}"
|
||||
|
||||
`)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// TBD: Implement
|
||||
// TBD: Implement - this is a temporary routine to demonstrate an application
|
||||
func handleSuccess(srcip string) {
|
||||
fmt.Println("Success for ", srcip)
|
||||
|
||||
|
@ -11,29 +11,42 @@ import (
|
||||
"github.com/google/gopacket/pcap"
|
||||
)
|
||||
|
||||
// type client is used to keept track of a client attempting to perform an authentic knock sequence
|
||||
// Client is used to keep 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
|
||||
}
|
||||
|
||||
// KnockSequence is used keep track of an ordered knock sequence and whether it's been marked for use (to prevent replay attacks)
|
||||
type KnockSequence struct {
|
||||
Used bool // If true, that means this knock sequence has already been used once. It may still be within the valid time window, but it can't be used again
|
||||
PortSequence [4]uint16 // Each knock sequence is four ports long
|
||||
}
|
||||
|
||||
var (
|
||||
clients map[string]*Client // Contains a map of clients
|
||||
portSequences [][4]uint16
|
||||
sharedSecret string // base32 encoded shared secret used for totp
|
||||
clients map[string]*Client // Contains a map of clients
|
||||
knockSequences []KnockSequence // We have 3 valid knock sequences at any time to account for clock skew
|
||||
sharedSecret string // base32 encoded shared secret used for totp
|
||||
)
|
||||
|
||||
// packetServer is the main function when operating in server mode
|
||||
// it sets up the pcap on the capture device and starts a goroutine
|
||||
// to rotate the knock sequence
|
||||
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
|
||||
clients = make(map[string]*Client, 0) // key is source IP address, value is the current progress through the sequence. i.e. value of 1 means that the first port in the sequence was successful
|
||||
knockSequences = []KnockSequence{} // Slice of accepted port sequences, there have to be several to account for clock skew between client and server
|
||||
|
||||
// Open pcap handle on device
|
||||
handle, err := pcap.OpenLive(captureDevice, 1600, true, pcap.BlockForever)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open adapter")
|
||||
}
|
||||
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
|
||||
|
||||
// Setup a goroutine to periodically rotate the authentic knock sequence
|
||||
go rotateSequence(handle)
|
||||
|
||||
// Read from the pcap handle until we exit
|
||||
for packet := range packetSource.Packets() {
|
||||
handlePacket(packet) // Do something with a packet here.
|
||||
}
|
||||
@ -43,11 +56,17 @@ func packetServer(captureDevice string) {
|
||||
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}
|
||||
if !ok { // client doesn't exist yet
|
||||
for i, knockSequence := range knockSequences { // identify which of the 3 authentic knock sequences is matched
|
||||
if knockSequence.Used { // skip over sequences that are already used to prevent replay attack
|
||||
continue
|
||||
}
|
||||
if port == knockSequence.PortSequence[0] {
|
||||
// Create the client and mark the knock sequence as used
|
||||
clients[srcip] = &Client{Progress: 1, Sequence: knockSequence.PortSequence}
|
||||
knockSequences[i].Used = true
|
||||
}
|
||||
}
|
||||
return
|
||||
@ -55,6 +74,7 @@ func handlePacket(packet gopacket.Packet) {
|
||||
|
||||
// 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
|
||||
// TBD: make the sweep attack fix on by default, but configurable to be off to allow for limited BPF filter for extremely low overhead as compromise.
|
||||
if port != client.Sequence[client.Progress] {
|
||||
delete(clients, srcip)
|
||||
fmt.Printf("port '%d' is in sequence, but came at unexpected order - resetting progress", port)
|
||||
@ -65,7 +85,7 @@ func handlePacket(packet gopacket.Packet) {
|
||||
client.Progress++
|
||||
if client.Progress >= len(client.Sequence) {
|
||||
delete(clients, srcip)
|
||||
handleSuccess(srcip)
|
||||
handleSuccess(srcip) // The magic function, the knock is completed
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -74,18 +94,19 @@ func handlePacket(packet gopacket.Packet) {
|
||||
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)
|
||||
// Generate new knock sequences with time skew support
|
||||
t := time.Now().Add(time.Second * -30)
|
||||
for i := len(knockSequences); 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)
|
||||
}
|
||||
knockSequence := KnockSequence{PortSequence: portSequence}
|
||||
knockSequences = append(knockSequences, knockSequence)
|
||||
}
|
||||
fmt.Println("New sequences:", knockSequences)
|
||||
|
||||
fmt.Println("New sequences:", portSequences)
|
||||
// Set BPF filter
|
||||
err := setPacketFilter(handle)
|
||||
if err != nil {
|
||||
log.Printf("failed to change packet filter: %v", err)
|
||||
@ -94,16 +115,16 @@ func rotateSequence(handle *pcap.Handle) {
|
||||
// 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{}
|
||||
// pop first value, next iteration pushes new value
|
||||
knockSequences = knockSequences[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
for i, knockSequence := range knockSequences {
|
||||
for j, port := range knockSequence.PortSequence {
|
||||
if i == 0 && j == 0 {
|
||||
filter += fmt.Sprint("port ", port)
|
||||
} else {
|
||||
|
Reference in New Issue
Block a user