Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Brook 服务端实现缺陷可能导致主动探测 #708

Closed
ghost opened this issue Aug 27, 2020 · 6 comments
Closed

Brook 服务端实现缺陷可能导致主动探测 #708

ghost opened this issue Aug 27, 2020 · 6 comments

Comments

@ghost
Copy link

ghost commented Aug 27, 2020

为了避免不了解技术细节的讨论者不必要的恐慌, 我将下面的几个comment连接到这里

不要恐慌, 请看这里

还有这里

append by @txthinking


Brook 服务端实现缺陷可能导致主动探测

注意:并非协议设计缺陷,此缺陷可以通过软件更新修复,请勿恐慌

方法

在到 Brook 服务端的 TCP 连接上逐字节发送随机数,连接会在服务端接收到第 30 字节时断开。

附 PoC 源代码

命令行参数:main.exe [服务器地址 [测量次数]],服务器地址默认 127.0.0.1:8388,测量次数默认 16 次

如果怀疑是 Brook 服务端,则会输出 brook true,反之输出 brook false

package main

import (
	"log"
	"math/rand"
	"net"
	"os"
	"strconv"
	"sync"
	"time"
)

func test(ep string) int {
	conn, err := net.Dial("tcp", ep)
	if err != nil {
		log.Panicln("dial:", err)
	}
	defer conn.Close()
	buf := make([]byte, 1)

	for i := 0; i < 256; i++ {
		buf[0] = byte(rand.Intn(256))
		n, err := conn.Write(buf)
		if err != nil || n != 1 {
			log.Println("error at", i)
			return i
		}
		time.Sleep(10 * time.Millisecond)
	}
	return 99999
}

func main() {
	fuck := true

	target := "127.0.0.1:8388"
	if len(os.Args) >= 2 {
		target = os.Args[1]
	}

	count := 16
	if len(os.Args) >= 3 {
		var e error
		if count, e = strconv.Atoi(os.Args[2]); e != nil {
			log.Fatalln(e)
		}
	}

	var wg sync.WaitGroup
	wg.Add(count)
	for i := 0; i < count; i++ {
		go func() {
			defer wg.Done()
			if test(target) != 31 {
				fuck = false
			}
		}()
		time.Sleep(100 * time.Millisecond)
	}
	wg.Wait()
	log.Println("brook", fuck)
}

分析

Brook 加密协议结构如下

[iv][chunk][chunk]...

其中 chunk 有以下结构

[encrypted_length][encrypted_length_tag][encrypted_data][encrypted_data_tag]

Brook 加密协议的布局与 Shadowsocks AEAD 一致,可参见 shadowsocks/shadowsocks-rust#292 (实际上 ss-rust 的缺陷是在验证 Brook 的缺陷的过程中发现)

Brook 对数据完整性的验证完全依靠 AEAD 解密过程保证——只有在第一次 AEAD 解密后,我们才能知道数据流是否伪造。如果一个服务端在发现解密失败后直接关闭连接,则可以使用 v2ray/v2ray-core#2523 中提到的逐字节发送数据包并观测连接何时关闭的手段测量验证过程至少需要多少字节。

对于 Brook,完成验证需要接收 iv ,第一个 chunkencrypted_lengthencrypted_length_tag 。其中 iv 长 12 字节,encrypted_length_tag 因使用 AES-256-GCM ,长 16 字节,encrypted_length 长度为 2。显然随机数不可能通过 AEAD 解密过程的验证,服务端会在接收到完成验证所需长度的数据后关闭连接。因此当向某个开放的 TCP 端口逐字节输入随机数,连接始终在输入 12 + 16 + 2 字节后断开,则可怀疑此端口是 Brook 端口。

防御

按照 https://censorbib.nymity.ch/#Frolov2020a 的结论,最稳妥的防御是从不主动关闭异常的连接。我们不可能改变完成验证所需长度,但从不主动关闭连接应足以阻止此类依赖服务端响应的主动探测。

@dlccontributor

This comment has been minimized.

@txthinking
Copy link
Owner

@dlccontributor Please understand the POC. The POC ONLY know it [MAY] be AES(please know again, even AES, it sill MAY), so you CAN NOT say Brook is detectable.
I will copy a simple service from our company’s IoT project to illustrate this later

@txthinking
Copy link
Owner

This is a IoT service, the POC prints it is Brook, but it is not.

package main

import (
        "io"
        "log"
        "net"

        "github.com/txthinking/encrypt"
)

func main() {
        l, err := net.Listen("tcp", ":6896")
        if err != nil {
                panic(err)
        }
        defer l.Close()
        for {
                c, err := l.Accept()
                if err != nil {
                        panic(err)
                }
                go func(c net.Conn) {
                        defer c.Close()
                        for {
                                // Read the air conditioner temperature
                                b := make([]byte, 30)
                                if _, err := io.ReadFull(c, b); err != nil {
                                        return
                                }
                                temperature, err := encrypt.AESGCMDecrypt(b[12:], []byte("12345678901234567890123456789012"), b[:12])
                                if err != nil {
                                        return
                                }
                                log.Println(temperature, c.RemoteAddr())
                                // Write temperature into Kafka based on device IP c.RemoteAddr().
                                // Omit, send control command to device code and more
                        }
                }(c)
        }
}

@txthinking
Copy link
Owner

And in out old system. (Please don't laugh at us, we no longer use it now)
The IP of many devices can be different from the factory IP, so we will have a device ID.

So this is NO AES, yet another IoT service, the POC still print it is Brook, but it is really not.

deviceid(10) + province code(4) + agent id(2) + activation id(6) + temperature(2) + factory IP(4)

In fact, the device id only has 6 digits at the beginning, changed to 10 digits a few years later

package main

import (
        "io"
        "net"
)

func main() {
        l, err := net.Listen("tcp", ":6896")
        if err != nil {
                panic(err)
        }
        defer l.Close()
        for {
                c, err := l.Accept()
                if err != nil {
                        panic(err)
                }
                go func(c net.Conn) {
                        defer c.Close()
                        for {
                                // deviceid(10) + provice code(4) + agent id(2) + activation id(6) + temperature(2) + factory IP(4)
                                b := make([]byte, 30)
                                if _, err := io.ReadFull(c, b); err != nil {
                                        return
                                }
                                // find deviceid from SQL Database or not found then close
                                notfounddeviceid := false
                                if !notfounddeviceid {
                                        // not a device, return
                                        return
                                }
                                if notfounddeviceid {
                                        // Write info into SQL Server
                                }
                        }
                }(c)
        }
}

@txthinking txthinking reopened this Aug 28, 2020
@txthinking
Copy link
Owner

@studentmain
In any case, we are still very grateful to you for discussing this issue, and we will still adopt your simple as an option. Even though we removed the code to close illegal connections randomly before

@txthinking
Copy link
Owner

txthinking commented Aug 28, 2020

✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅

The new commit bypassed the POC. Recheck steps:

On server

# Create a new Linux 64bit server
$ sudo su
$ cd /usr/local/
$ wget https://golang.org/dl/go1.15.linux-amd64.tar.gz
$ tar zxf go1.15.linux-amd64.tar.gz
$ export PATH=$PATH:/usr/local/go/bin/
$ go get -u github.com/txthinking/brook/cli/brook
$ ~/go/bin/brook server -l :80 -p 1

On client(POC)

$ go run poc.go server_ip:80
2020/08/28 12:06:39 brook false

✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants