Skip to content

Commit 13f8610

Browse files
author
Benjamin M. Schwartz
authored
Merge pull request #78 from Jigsaw-Code/bemasc-blocksalt
Prevent replays of server data
2 parents 20a6cc7 + 777815a commit 13f8610

File tree

10 files changed

+495
-56
lines changed

10 files changed

+495
-56
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The Outline Shadowsocks service allows for:
1010
- Whitebox monitoring of the service using [prometheus.io](https://prometheus.io)
1111
- Includes traffic measurements and other health indicators.
1212
- Live updates via config change + SIGHUP
13-
- Experimental: optional replay defense (--replay_history).
13+
- Replay defense (add `--replay_history 10000`). See [PROBES](shadowsocks/PROBES.md) for details.
1414

1515
![Graphana Dashboard](https://user-images.githubusercontent.com/113565/44177062-419d7700-a0ba-11e8-9621-db519692ff6c.png "Graphana Dashboard")
1616

server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ func (s *ssServer) loadConfig(filename string) error {
139139
if !ok {
140140
return fmt.Errorf("Only AEAD ciphers are supported. Found %v", keyConfig.Cipher)
141141
}
142-
cipherList.PushBack(&shadowsocks.CipherEntry{ID: keyConfig.ID, Cipher: aead})
142+
cipherList.PushBack(shadowsocks.MakeCipherEntry(keyConfig.ID, aead, keyConfig.Secret))
143143
}
144144
for port := range s.ports {
145145
portChanges[port] = portChanges[port] - 1

shadowsocks/PROBES.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Outline Shadowsocks Probing and Replay Defenses
2+
3+
## Attacks
4+
5+
To ensure that proxied connections have not been modified in transit, the Outline implementation of Shadowsocks only supports modern [AEAD cipher suites](https://shadowsocks.org/en/spec/AEAD-Ciphers.html). This protects users from a wide range of potential attacks. However, even with [AEAD's authenticity guarantees](https://en.wikipedia.org/wiki/Authenticated_encryption), there are still ways for an attacker to abuse the Shadowsocks protocol.
6+
7+
One category of attacks are "probing" attacks, in which the adversary sends test data to the proxy in order to confirm that it is actually a Shadowsocks proxy. This is a violation of the Shadowsocks security design, which is intended to ensure that only an authenticated user can identify the proxy. For example, one [probing attack against Shadowsocks](https://scholar.google.com/scholar?cluster=8542824533765048218) sends different numbers of random bytes to a target server, and identifies how many bytes the server reads before detecting an error and closing the connection. This number can be distinctive, identifying the server software.
8+
9+
Another [reported](https://gfw.report/blog/gfw_shadowsocks/) category of attacks are "replay" attacks, in which an adversary records a conversation between a Shadowsocks client and server, then replays the contents of that connection. The contents are valid Shadowsocks AEAD data, so the proxy will forward the connection to the specified destination, as usual. In some cases, this can cause a duplicated action (e.g. uploading a file twice with HTTP POST). However, modern secure protocols such as HTTPS are not replayable, so this will normally have no ill effect.
10+
11+
A greater concern for Outline is the use of replays in probing attacks to identify Shadowsocks proxies. By sending modified and unmodified replays, an attacker might be able to confirm that a server is in fact a Shadowsocks proxy, by observing distinctive behaviors.
12+
13+
## Outline's defenses
14+
15+
Outline contains several defenses against probing and replay attacks.
16+
17+
### Invalid probe data
18+
19+
If Outline detects that the initial data is invalid, it will continue to read data (exactly as if it were valid), but will not reply, and will not close the connection until a timeout. This leaves the attacker with minimal information about the server.
20+
21+
### Client replays
22+
23+
When client replay protection is enabled, every incoming valid handshake is reduced to a 32-bit checksum and stored in a hash table. When the table is full, it is archived and replaced with a fresh one, ensuring that the recent history is always in memory. Using 32-bit checksums results in a false-positive detection rate of 1 in 4 billion for each entry in the history. At the maximum history size (two sets of 20,000 checksums each), that results in a false-positive failure rate of 1 in 100,000 sockets ... still far lower than the error rate expected from network unreliability.
24+
25+
This feature is on by default in Outline. Admins who are using outline-ss-server directly can enable this feature by adding "--replay_history 10000" to their outline-ss-server invocation. This costs approximately 20 bytes of memory per checksum.
26+
27+
### Server replays
28+
29+
Shadowsocks uses the same Key Derivation Function for both upstream and downstream flows, so in principle an attacker could record data sent from the server to the client, and use it in a "reflected replay" attack as simulated client->server data. The data would appear to be valid and authenticated to the server, but the connection would most likely fail when attempting to parse the destination address header, perhaps leading to a distinctive failure behavior.
30+
31+
To avoid this class of attacks, outline-ss-server uses an [HMAC](https://en.wikipedia.org/wiki/HMAC) with a 32-bit tag to mark all server handshakes, and checks for the presence of this tag in all incoming handshakes. If the tag is present, the connection is a reflected replay, with a false positive probability of 1 in 4 billion.
32+
33+
## Metrics
34+
35+
Outline provides server operators with metrics on a variety of aspects of server activity, including any detected attacks. To observe attacks detected by your server, look at the `tcp_probes` histogram vector in Prometheus. The `status` field will be `"ERR_CIPHER"` (indicating invalid probe data), `"ERR_REPLAY_CLIENT"`, or `"ERR_REPLAY_SERVER"`, depending on the kind of attack your server observed. You can also see what country each probe appeared to originate from, and approximately how many bytes were sent before giving up.

shadowsocks/cipher_list.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,34 @@ import (
2727
// All ciphers must have a nonce size this big or smaller.
2828
const maxNonceSize = 12
2929

30+
// Don't add a tag if it would reduce the salt entropy below this amount.
31+
const minSaltEntropy = 16
32+
3033
// CipherEntry holds a Cipher with an identifier.
31-
// The public fields are constant, but lastAddress is mutable under cipherList.mu.
34+
// The public fields are constant, but lastClientIP is mutable under cipherList.mu.
3235
type CipherEntry struct {
33-
ID string
34-
Cipher shadowaead.Cipher
35-
lastClientIP net.IP
36+
ID string
37+
Cipher shadowaead.Cipher
38+
SaltGenerator ServerSaltGenerator
39+
lastClientIP net.IP
40+
}
41+
42+
// MakeCipherEntry constructs a CipherEntry.
43+
func MakeCipherEntry(id string, cipher shadowaead.Cipher, secret string) CipherEntry {
44+
var saltGenerator ServerSaltGenerator
45+
if cipher.SaltSize()-ServerSaltMarkLen >= minSaltEntropy {
46+
// Mark salts with a tag for reverse replay protection.
47+
saltGenerator = NewServerSaltGenerator(secret)
48+
} else {
49+
// Adding a tag would leave too little randomness to protect
50+
// against accidental salt reuse, so don't mark the salts.
51+
saltGenerator = RandomSaltGenerator
52+
}
53+
return CipherEntry{
54+
ID: id,
55+
Cipher: cipher,
56+
SaltGenerator: saltGenerator,
57+
}
3658
}
3759

3860
// CipherList is a thread-safe collection of CipherEntry elements that allows for

shadowsocks/cipher_testing.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ func MakeTestCiphers(secrets []string) (CipherList, error) {
4343
if err != nil {
4444
return nil, fmt.Errorf("Failed to create cipher %v: %v", i, err)
4545
}
46-
l.PushBack(&CipherEntry{ID: cipherID, Cipher: cipher.(shadowaead.Cipher)})
46+
entry := MakeCipherEntry(cipherID, cipher.(shadowaead.Cipher), secrets[i])
47+
l.PushBack(&entry)
4748
}
4849
cipherList := NewCipherList()
4950
cipherList.Update(l)

shadowsocks/salt_generator.go

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// Copyright 2020 Jigsaw Operations LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package shadowsocks
16+
17+
import (
18+
"bytes"
19+
"crypto"
20+
"crypto/hmac"
21+
"crypto/rand"
22+
"fmt"
23+
"io"
24+
25+
"golang.org/x/crypto/hkdf"
26+
)
27+
28+
// SaltGenerator generates unique salts to use in Shadowsocks connections.
29+
type SaltGenerator interface {
30+
// Returns a new salt
31+
GetSalt(salt []byte) error
32+
}
33+
34+
// ServerSaltGenerator offers the ability to check if a salt was marked as
35+
// server-originated.
36+
type ServerSaltGenerator interface {
37+
SaltGenerator
38+
// IsServerSalt returns true if the salt was created by this generator
39+
// and is marked as server-originated.
40+
IsServerSalt(salt []byte) bool
41+
}
42+
43+
// randomSaltGenerator generates a new random salt.
44+
type randomSaltGenerator struct{}
45+
46+
// GetSalt outputs a random salt.
47+
func (randomSaltGenerator) GetSalt(salt []byte) error {
48+
_, err := rand.Read(salt)
49+
return err
50+
}
51+
52+
func (randomSaltGenerator) IsServerSalt(salt []byte) bool {
53+
return false
54+
}
55+
56+
// RandomSaltGenerator is a basic SaltGenerator.
57+
var RandomSaltGenerator ServerSaltGenerator = randomSaltGenerator{}
58+
59+
// serverSaltGenerator generates unique salts that are secretly marked.
60+
type serverSaltGenerator struct {
61+
key []byte
62+
}
63+
64+
// ServerSaltMarkLen is the number of bytes of salt to use as a marker.
65+
// Increasing this value reduces the false positive rate, but increases
66+
// the likelihood of salt collisions.
67+
const ServerSaltMarkLen = 4 // Must be less than or equal to SHA1.Size()
68+
69+
// Constant to identify this marking scheme.
70+
var serverSaltLabel = []byte("outline-server-salt")
71+
72+
// NewServerSaltGenerator returns a SaltGenerator whose output is apparently
73+
// random, but is secretly marked as being issued by the server.
74+
// This is useful to prevent the server from accepting its own output in a
75+
// reflection attack.
76+
func NewServerSaltGenerator(secret string) ServerSaltGenerator {
77+
// Shadowsocks already uses HKDF-SHA1 to derive the AEAD key, so we use
78+
// the same derivation with a different "info" to generate our HMAC key.
79+
keySource := hkdf.New(crypto.SHA1.New, []byte(secret), nil, serverSaltLabel)
80+
// The key can be any size, but matching the block size is most efficient.
81+
key := make([]byte, crypto.SHA1.Size())
82+
io.ReadFull(keySource, key)
83+
return serverSaltGenerator{key}
84+
}
85+
86+
func (sg serverSaltGenerator) splitSalt(salt []byte) (prefix, mark []byte, err error) {
87+
prefixLen := len(salt) - ServerSaltMarkLen
88+
if prefixLen < 0 {
89+
return nil, nil, fmt.Errorf("Salt is too short: %d < %d", len(salt), ServerSaltMarkLen)
90+
}
91+
return salt[:prefixLen], salt[prefixLen:], nil
92+
}
93+
94+
// getTag takes in a salt prefix and returns the tag.
95+
func (sg serverSaltGenerator) getTag(prefix []byte) []byte {
96+
// Use HMAC-SHA1, even though SHA1 is broken, because HMAC-SHA1 is still
97+
// secure, and we're already using HKDF-SHA1.
98+
hmac := hmac.New(crypto.SHA1.New, sg.key)
99+
hmac.Write(prefix) // Hash.Write never returns an error.
100+
return hmac.Sum(nil)
101+
}
102+
103+
// GetSalt returns an apparently random salt that can be identified
104+
// as server-originated by anyone who knows the Shadowsocks key.
105+
func (sg serverSaltGenerator) GetSalt(salt []byte) error {
106+
prefix, mark, err := sg.splitSalt(salt)
107+
if err != nil {
108+
return err
109+
}
110+
if _, err := rand.Read(prefix); err != nil {
111+
return err
112+
}
113+
tag := sg.getTag(prefix)
114+
copy(mark, tag)
115+
return nil
116+
}
117+
118+
func (sg serverSaltGenerator) IsServerSalt(salt []byte) bool {
119+
prefix, mark, err := sg.splitSalt(salt)
120+
if err != nil {
121+
return false
122+
}
123+
tag := sg.getTag(prefix)
124+
return bytes.Equal(tag[:ServerSaltMarkLen], mark)
125+
}

0 commit comments

Comments
 (0)