主页 > imtoken最新下载 > go-ethereum security audit 以太坊链审计报告
go-ethereum security audit 以太坊链审计报告
1.2.4. Javascript 引擎和 API
伪随机数生成器的弱随机种子
1.2.5. EVM 实现
滥用 intPool 导致廉价的内存消耗
开采区块中的弱负值保护
1.2.6. 各种各样的
挖掘代码中的竞争条件
许多第三方依赖
1.3. 结果详情 1.3.1。 P2P和网络
TrueSec审计了p2p和网络部分的代码,主要关注:
Secure channel implementation - 握手和共享秘密的实现
安全通道属性 - 机密性和完整性
消息序列化
节点发现
针对 DOS 的保护:超时和消息大小限制
TrueSec 还通过 go-fuzz 对 RLP 解码进行了模糊处理,没有发现节点崩溃。
1.3.1.1. 已知的问题
虽然在加密握手中更好地实现了共享秘密,但由于对称加密算法中的二次填充缺陷,通道缺乏机密性。 这是一个已知的问题。 (有关详细信息,请参阅 []() 和 []())。 由于通道现在只传输公链数据,所以这个问题暂时不需要解决。
另一个持续存在的问题是在安全通道级别缺乏重放保护(以太坊开发讨论中提到了默认的基于时间的重放保护机制)。 TrueSec 提议下一版本的协议通过控制消息的数量来实现重放保护。
1.3.1.2。 内存分配太大
在 rlpx.go 中,TrueSec 发现了两个用户可控的超大内存分配。 TrueSec 没有发现可利用的 DOS 场景,但建议正确验证它们。
读取协议消息时,可以分配16.8MB内存:
func (rw *rlpxFrameRW) ReadMsg() (msg Msg, err error) { ... fsize := readInt24(headbuf) // ignore protocol type for now // read the frame content var rsize = fsize // frame size rounded up to 16 byte boundary if padding := fsize % 16; padding > 0 { rsize += 16 - padding } // TRUESEC: user-controlled allocation of 16.8MB: framebuf := make([]byte, rsize) ...}
由于以太坊协议中将最大消息大小定义为 10MB,因此 TrueSec 推荐的内存分配也定义为相同大小。
在加密握手过程中,可以分配65KB内存给握手信息。
func readHandshakeMsg(msg plainDecoder, plainSize int, prv *ecdsa.PrivateKey, r io.Reader) ([]byte, error) { ... // Could be EIP-8 format, try that. prefix := buf[:2] size := binary.BigEndian.Uint16(prefix) if size < uint16(plainSize) { return buf, fmt.Errorf("size underflow, need at least ...") } // TRUESEC: user-controlled allocation of 65KB: buf = append(buf, make([]byte, size-uint16(plainSize)+2)...) ...}
除非握手消息确实包含 65KB 的数据,否则 TrueSec 建议限制握手消息的大小。
1.3.2. 事务和块处理
TrueSec 对交易、区块下载和区块处理进行了代码审计,重点关注:
内存分配、goroutine 泄漏和 IO 操作导致的拒绝服务
同步问题
1.3.2.1。 除以零风险
在 Go 中,被零除会导致恐慌。 在downloader.go的qosReduceConfidence方法中,是否发生除零取决于调用者是否正确调用:
func (d *Downloader) qosReduceConfidence() { peers := uint64(d.peers.Len()) ... // TRUESEC: no zero-check of peers here conf := atomic.LoadUint64(&d.rttConfidence) * (peers - 1) / peers ...}
TrueSec 没有发现可导致节点崩溃的漏洞,但仅依靠调用者来确保 d.peers.Len() 不为零是不安全的。 TrueSec 建议在执行除法之前应检查所有非常数股息。
1.3.2.2。 代码复杂度
TrueSec 发现交易和区块处理代码部分比其他部分代码更复杂,更难阅读和审计。 这部分方法比较大,在fetcher.go、downloader.go和blockchain.go中有200多行代码。 同步的实现有时会结合互斥锁和通道消息。 例如,结构体Downloader的定义需要60行代码,包括3个mutexes和11个channel。
难以阅读和理解的代码是安全问题的温床。 特别是,eth 包中有一些代码量很大的方法、结构、接口以及扩展的互斥锁和通道。 TrueSec 建议花一些时间重构和简化代码,以防止将来出现安全问题。
1.3.3. IPC 和 RPC 接口
TrueSec 审核 IPC 和 RPC(HTTP 和 Websocket)接口,重点关注潜在的访问控制问题,从公共 API 权限升级到私有 API(管理、调试等)。
1.3.3.1. CORS:允许默认 HTTP RPC 中的所有域
可以使用 geth 的 --rpc 参数启用 HTTP RPC 接口。 这将启动一个 Web 服务器,监听端口 8545 上的 HTTP 请求,任何人都可以访问它。 由于可能暴露端口(例如连接到不受信任的网络),默认情况下只有公共 API 允许 HTTP RPC 接口。
同源策略和默认跨源资源共享 (CORS) 配置限制 Web 浏览器访问并限制通过 XSS 攻击 RPC API 的可能性。 允许的来源可以通过--rpccorsdomain "domain"配置,也可以配置多个域名,用逗号隔开--rpccorsdomain "domain1, domain2", 或者--rpccorsdomain "*",这样所有的域都可以通过标准网络浏览器访问。 如果未配置,则不会设置 CORS 标头 - 并且浏览器将不允许跨域请求:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:8545/. (Reason: CORS header 'Access-Control-Allow-Origin missing').
由于缺少 CORS 标头,Firefox 禁止跨域请求。
然而,在 commit 5e29f4b(自 2017 年 4 月 12 日起)中 - 可以绕过同源策略并且可以从 Web 浏览器访问 RPC。
HTTP RPC 的 CORS 配置已更改为处理允许来源的字符数组 - 而不是作为单引号分隔的字符串在内部传输。
在此之前,在实例化 cors 中间件之前,以逗号分隔的字符串被拆分成一个数组(参见清单 1)。 使用默认值(如果用户未明确配置任何设置,例如使用 --rpccorsdomain )空字符串,这会导致字符数组包含空字符串。
在提交 5e29f4b 之后,默认值为一个空数组,它被传递给位于 newCorsHandler 的中间件 cors(参见清单 2)。
然后 cors 中间件检查允许的 origins 数组的长度(参见清单 3)。 如果长度为 0,在本例中表示空数组,则 cors 中间件将成为默认值并允许所有域。
可以通过运行 geth -rpc 而不指定任何允许的来源来重现此问题,并在提交 5e29f4b 之前和之后使用 OPTION 请求检查 CORS 标头。 第二个输出的 Access-Control-Allow-Origin 值得注意。
请注意,即使在更改之前也是如此。 如果不是因为字符串拆分,则输入值不会在 cors(包含空字符串的数组)中解释为空。
此问题可以通过以下 JavaScript 代码加以利用,从任何域执行(甚至是本地文件系统以太坊挖矿d池,即无效或空源)
var xhr = new XMLHttpRequest();xhr.open("POST", "http://localhost:8545", true);xhr.setRequestHeader("Content-Type", "application/json");xhr.onreadystatechange = function() { if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) { console.log("Modules: " + xhr.responseText); }}xhr.send('{"jsonrpc":"2.0","method":"rpc_modules","params":[],"id":67}')
TrueSec 建议明确限制 CORS 的默认配置(例如将允许的来源设置为本地主机,或者根本不设置 CORS 标头),而不是依赖外界选择正常(安全)的默认设置
165 func newCorsHandler(srv *Server, corsString string) http.Handler {166 var allowedOrigins []string167 for _, domain := range strings.Split(corsString, ",") {168 allowedOrigins = append(allowedOrigins, strings.TrimSpace(domain))169 }170 c := cors.New(cors.Options{171 AllowedOrigins: allowedOrigins,172 AllowedMethods: []string{"POST", "GET"},173 MaxAge: 600,174 AllowedHeaders: []string{"*"},175 })176 return c.Handler(srv)177 }
清单 1:rpc/http.go,在提交 5e29f4be935ff227bbf07a0c6e80e8809f5e0202 之前
164 func newCorsHandler(srv *Server, allowedOrigins []string) http.Handler {165 c := cors.New(cors.Options{166 AllowedOrigins: allowedOrigins,167 AllowedMethods: []string{"POST", "GET"},168 MaxAge: 600,169 AllowedHeaders: []string{"*"},170 })171 return c.Handler(srv)172 }
清单 2:rpc/http.go,在提交 5e29f4be935ff227bbf07a0c6e80e8809f5e0202 之后
113 // Allowed Origins114 if len(options.AllowedOrigins) == 0 {115 // Default is all origins116 c.allowedOriginsAll = true117 }
清单 3:vendor/github.com/rs/cors/cors.go
$ curl -i -X OPTIONS -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type" -H "Origin: foobar" http://localhost:8545HTTP/1.1 200 OKVary: OriginVary: Access-Control-Request-MethodVary: Access-Control-Request-HeadersDate: Tue, 25 Apr 2017 08:49:10 GMTContent-Length: 0Content-Type: text/plain; charset=utf-8
清单 4:提交 5e29f4b 之前的 CORS 标头
$ curl -i -X OPTIONS -H "Access-Control-Request-Method: POST" -H "Access-Control-Request-Headers: content-type" -H "Origin: foobar" http://localhost:8545HTTP/1.1 200 OKAccess-Control-Allow-Headers: Content-TypeAccess-Control-Allow-Methods: POSTAccess-Control-Allow-Origin: foobarAccess-Control-Max-Age: 600Vary: OriginVary: Access-Control-Request-MethodVary: Access-Control-Request-HeadersDate: Tue, 25 Apr 2017 08:47:24 GMTContent-Length: 0Content-Type: text/plain; charset=utf-8
清单 5:提交 5e29f4b 后的 CORS 标头
1.3.4. JavaScript 引擎和 API
JavaScript引擎otto是Go Ethereum中的CLI脚本接口,IPC/RPC接口的终端交互解释器,私有调试API的一部分。 考虑到它的代码有限,审计优先级比较低。
1.3.4.1。 用于伪随机数生成的弱随机数种子
在jsre中初始化伪随机数生成器时,如果crypto/rand(crypto/rand返回一个密码安全的伪随机数)方法失败,随机数种子将取决于当时的UNIX时间。 在清单 6 中,这个弱随机数种子将用于初始化 math/rand 实例。
此 PRNG 未用于任何敏感信息,显然不应该用作加密安全 RNG,但由于用户可以通过命令行运行脚本来使用 PRNG,使其失败而不是生成弱随机数种子显然更安全。 从 crypto/rand 获取错误意味着其他地方也可能存在问题。 即使使用安全的随机数种子,也应该在文档中指出 PRNG 不是加密安全的。
84 // randomSource returns a pseudo random value generator.85 func randomSource() *rand.Rand {86 bytes := make([]byte, 8)87 seed := time.Now().UnixNano() // 不是完全随机88 if _, err := crand.Read(bytes); err == nil {89 seed = int64(binary.LittleEndian.Uint64(bytes))90 }9192 src := rand.NewSource(seed)93 return rand.New(src)94 }
清单 6:internal/jsre/jsre.go
1.3.5. 以太坊虚拟机(EVM)的实现
TrueSec审计了以太坊虚拟机(EVM)部分的代码,重点关注滥用内存分配和IO操作导致的拒绝服务。 EVM解释器(runtime/fuzz.go)有一个go-fuzz入口点,使用成功。 TrueSec 确认了其功能,但在模糊测试过程中未发现有影响的漏洞。
1.3.5.1。 滥用intPool导致的廉价内存消耗
出于性能原因,在EVM执行过程中,使用大整数会进入整数池intPool(intpool.go)。 由于对整数池大小没有限制,使用特定的操作码组合将导致意想不到的廉价内存使用。
0 JUMPDEST // 1 gas1 COINBASE // 2 gas2 ORIGIN // 2 gas3 EQ // 3 gas, puts 20 + 20 bytes on the intpool4 JUMP // 8 gas, puts 4-8 bytes on the intpool
例如,合约代码会消耗3.33e9单位的gas(当时约合3300美元),分配10G的内存给intPool。 在以太坊虚拟机中分配 10GB 内存的预期 gas 成本为 1.95e14(约 195,000,000 美元)
当intPool产生内存不足恐慌时,会引起拒绝服务攻击。 但是共识算法限制了gas limit,可以防止拒绝服务攻击的发生。 但是,考虑到攻击者可能会找到更有效的方式来填充 intPool,或者 gaslimit 目标增长过快以太坊挖矿d池,TrueSec 仍然建议限制 intPool 的大小。
1.3.5.2。 开采区块中易受攻击的负值保护
以太币在账户之间的转账是通过core/evm.go中的Transfer方法完成的。
func Transfer(db vm.StateDB, sender, recipient common.Address, amount *big.Int) { db.SubBalance(sender, amount) db.AddBalance(recipient, amount)}
输入金额是指向有符号类型的指针,可能具有负参考值。 负数会将 ETH 从接收方转移到转账方,允许转账方从接收方窃取 ETH。
当收到一个解包的交易时,会验证交易的价值是否为正。 如tx_pool.go、validateTx():
if tx.Value().Sign() < 0 { return ErrNegativeValue}
但是在块处理过程中没有这种显式验证; 具有负值的交易只会被 p2p 序列化格式(RLP)隐式阻止,它不能解码负值。 假设一个邪恶的矿工为了非法获得以太坊而发布带有负交易的区块,依靠特定的序列化格式来保护似乎有点脆弱。 TrueSec 还建议在块处理期间明确检查交易值。 或者使用无符号类型强制指定事务的值为正。
1.3.6. 各种各样的
1.3.6.1。 挖矿代码中的条件竞争
TrueSec使用“-race”来构建flags,利用Go语言内置的条件竞争检测特性来寻找条件竞争。 在 ethash/ethash.go 中发现了与挖矿时使用的 ethash 数据集的时间戳相关的竞争条件。
func (ethash *Ethash) dataset(block uint64) []uint32 { epoch := block / epochLength // If we have a PoW for that epoch, use that ethash.lock.Lock() ... current.used = time.Now() // TRUESEC: race ethash.lock.Unlock() // Wait for generation finish, bump the timestamp and finalize the cache current.generate(ethash.dagdir, ethash.dagsondisk, ethash.tester) current.lock.Lock() current.used = time.Now() current.lock.Unlock()...}
要消除竞争条件,可以使用 current.lock 互斥锁来保护第一个 current.used 设置。 TrueSec 没有研究条件竞争是否会影响节点的挖矿。
1.3.6.2。 第三方依赖太多
Go Ethereum依赖71个第三方包(由govendor list +vend列出)
由于每个依赖项都可能引入新的攻击向量,并且需要花费时间和精力来监控安全漏洞,因此 TrueSec 始终建议将第三方包的数量保持在最低限度。
71 个依赖项对于任何项目来说都是很多的。 TrueSec 建议以太坊开发人员调查是否所有依赖项实际上都是必需的,或者其中一些是否可以用代码替换。