TrueSec在2017年4月对以太坊的GO语言(https://ethereum.github.io/go-ethereum/)实现进行了代码审计。审计结果表明代码质量是比较高的,且开发者具备一定的安全意识。在审计过程中没有发现严重的安全漏洞。最严重的一个漏洞是当客户端的RPC HTTP开启时,web浏览器同源策略的绕过。其他发现的问题并没有直接的攻击向量可供利用,报告的其他部分为通用的评论和建议。
1. P2P和网络
2. 交易和区块处理
3. IPC和RPC接口
4. Javascript引擎和API
5. EVM实现
intPool
导致廉价的内存消耗6. 杂项
TrueSec对p2p和网络部分的代码进行了审计,主要关注:
TrueSec还通过go-fuzz(https://github.com/dvyukov/go-fuzz/)对RLP解码进行fuzz,没有发现节点崩溃的现象。
(1)已知问题
虽然共享secrets在encryption handshake中实现得比较好,但是由于在对称加密算法中的two-time-pad缺陷,使得通道缺乏保密性。这个是已知的问题。(详情参考https://github.com/ethereum/devp2p/issues/32和https://github.com/ethereum/go-ethereum/issues/1315)。由于现在通道只传输公开的区块链数据,这个问题暂时不必解决。
另外一个一直存在的问题是在安全的通道等级(在以太坊开发者讨论中提到过一个默认的基于时间的重放保护机制)中缺乏重放保护。TrueSec建议协议的下一个版本通过控制消息数量来实现重放保护
。
(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推荐内存分配也定义为相同大小。在encryption handshake过程中,可以给握手信息分配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建议对握手消息的大小作限制。
TrueSec对交易和区块下载,区块处理的部分进行了代码审计,主要关注:
(1)零除风险
在Go中,除以零会导致一个panic。在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建议所有非常数的被除数应该在进行除法之前进行检查。
(2)代码复杂性
TrueSec发现交易和区块处理的代码部分相对其他部分代码来说更加复杂,更难阅读和审计。这部分的方法相对更大,在fetcher.go,downloader.go 和blockchain.行的go中有超过200代码。同步的实现有时候会结合互斥锁和通道消息。比如说,结构体Downloader定义需要60行代码,包含3个互斥锁和11个通道。
难以阅读和理解的代码是滋生安全问题的肥沃土壤。特别是eth包中存在一些代码量大的方法,结构体,接口与扩展的互斥锁和通道。TrueSec建议花一些功夫重构和简化代码,来防止未来安全问题的发生。
TrueSec对IPC和RPC(HTTP和Websocket)接口进行了审计,关注于潜在的访问控制问题,从公共API提权到私有API(admin, debug等)的问题。
(1)CORS:在默认的HTTP RPC里允许所有域
HTTP RPC接口可以通过geth的--rpc参数开启。这会启动一个web服务器,用于监听8545端口的HTTP请求,且任何人都可以对其进行访问。由于潜在暴露端口的可能性(比如连接到不可信的网络),默认只有公共API允许HTTP RPC接口。
同源策略和默认的跨域资源共享(CORS)配置限制了web浏览器的访问,并且限制通过XSS攻击RPC API的可能性。allowed origins能够通过--rpccorsdomain "domain"来配置,也可以通过逗号分隔来配置多个域名-- rpccorsdomain "domain1,domain2",或者配置为--rpccorsdomain "*",使得所有的域都可以通过标准web浏览器访问。如果没有进行配置,CORS头将不会被设置——并且浏览器不会允许跨域请求。如图1
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').
但是,在commit 5e29f4b(https://github.com/ethereum/go-ethereum/commit/5e29f4be935ff227bbf07a0c6e80e8809f5e0202)中(从2017年4月12日开始)——同源策略可以被绕过,RPC可以从web浏览器被访问。HTTP RPC的CORS配置被改变为处理allowed origins的字符数组——而不是在内部作为一个单引号分隔的字符串传输。
在此之前,逗号分隔的字符串被分成一个数组,在实例化cors(https://github.com/rs/cors)中间件之前(请见Listing 1)。with默认值(防用户没有显性配置任何设置时,如使用--rpccorsdomain)空字符串,这会导致一个字符数组包含一个空字符串。
在commit 5e29f4b之后,默认值是一个空的数组,这个数组传递给位于newCorsHandler的中间件cors(请见 Listing 2)。cors中间件随后检查allowed origins数组的长度(请见 Listing 3)。如果长度为0,在这里即代表空数组,cors中间件将会变成默认值并且允许所有域。
这个问题可以通过运行geth -rpc来复现,不需要指定任何allowed origins,并检查commit 5e29f4b前后带有OPTION请求的CORS头。第二个输出的Access-Control-Allow-Origin值得注意。注意即使是改变之前,这里也是这样。如果不是因为字符串分割导致在cors没有解释输入值(一个数组包含一个空字符串)为空。
这个问题可以通过下面的JavaScript代码来利用,从任意域来执行(甚至可以是本地文件系统,即无效或者null origin)
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的默认配置进行显性的限制,(如将allowed origin设置为localhost,或根本不设置CORS头),而不是依赖外界来选择一个正常(安全)的默认设置
Listing 1: rpc/http.go, before commit 5e29f4be935ff227bbf07a0c6e80e8809f5e0202
Listing 2: rpc/http.go, after commit 5e29f4be935ff227bbf07a0c6e80e8809f5e0202
Listing 3: vendor/github.com/rs/cors/cors.go
Listing 4: CORS headers before commit 5e29f4b
Listing 5: CORS headers after commit 5e29f4b
JavaScript引擎otto是Go Ethereum中的CLI脚本接口,一个IPC/RPC接口的终端交互解释器,也是私有debug API的一部分。考虑到其代码有限,在审计中优先级比较低。
(1)伪随机数生成的弱随机数种子
在jsre中对伪随机数生成器进行初始化的时候,如果crypto/rand(crypto/rand返回密码学安全地伪随机数)方法失败,随机数种子将会依赖于当时的UNIX时间。在listing 6中,这个弱随机数种子将会被用于初始化math/rand的实例。
这个PRNG没有被用于任何敏感信息,而且显然也不应该被用作于密码学安全的RNG,但是由于用户可以通过命令行运行脚本来使用PRNG,使其失败而不是制造出弱随机数种子显然是更安全的。从crypto/rand中得到错误意味着其他地方可能也存在问题。即使是得到了安全的随机数种子,在文档中也应该指出PRNG
并不是密码学安全的。
Listing 6: internal/jsre/jsre.go
TrueSec对以太坊虚拟机(EVM)部分的代码进行了审计,主要关注由滥用内存分配和IO操作而引起的拒绝服务。EVM解释器(runtime/fuzz.go)存在一个go-fuzz的入口点,这个入口点成功地被使用。TrueSec确认了其功能性,但是在fuzzing过程中没有发现有影响的漏洞。
(1)滥用intPool导致的廉价的内存消耗
由于性能的原因,在EVM的执行过程中,使用大整数会进入整数池intPool(intpool.go)。由于没有对整数池大小进行限制,使用特定的opcode组合,将导致意外出现廉价使用内存的情况。
比如说,合约代码将会消耗3.33e9单位的gas(在当时大约价值3300USD),分配10G内存给intPool。以太坊虚拟机中分配10GB内存的预期gas成本是1.95e14(大约195,000,000USD)
当intPool产生out of memory panic时,会导致拒绝服务攻击。但是共识算法对gaslimit进行了限制,能够阻止该拒绝服务攻击的发生。但是考虑到攻击者可能发现一种更有效的填充intPool的方式,或者gaslimit target增长过于迅速等,TrueSec仍然推荐对intPool的大小进行限制。
(2)在挖矿区块中脆弱的负值保护
账户之间以太坊的转账是通过core/evm.go里的Transfer方法进行的。
输入amount是一个指向有符号类型的指针,可能存在负的引用值。一个负的amount将会把以太坊从收款方转移到转账方,使得转账方可以从收款方那里盗窃以太坊。
当接收到一个没有被打包的交易时,将会验证交易的值是否为正。如tx_pool.go, validateTx():
但是在区块处理过程中却不存在这样显性的验证;存在负值的交易只是隐性地被p2p
序列化格式(RLP)阻止,而RLP不能解码负值。假设一个邪恶的矿工为了非法获取以太坊,发布了具有负值交易的区块,这时依赖于特定的序列化格式来提供保护,似乎有些脆弱。TrueSec推荐在区块处理过程中也显性地检查交易的值。或者使用无符号类型来强制指定交易的值为正。
(1)在挖矿代码中的条件竞争
TrueSec使用"-race"来构建标志位,并通过Go语言内置的条件竞争探测特性来寻找条件竞争。在ethash/ethash.go中发现了一个与在挖矿时使用的ethash datasets时间戳相关的条件竞争。
为了去除条件竞争,通过使用current.lock互斥锁可以保护第一个current.used的设置。 TrueSec没有研究条件竞争是否会对节点的挖矿造成影响。
(2)过多第三方依赖
Go Ethereum依赖于71个第三方包(通过govendor list +vend列举)
由于每个依赖都可能引入新的攻击向量,并且需要时间和精力来监控安全漏洞,TrueSec总是建议将第三方包的数量控制到最小。
71个依赖对任何一个项目来说都是比较多的。TrueSec推荐以太坊开发者调研是否所有的依赖都是真正需要的,或者说其中一些是否可以用代码来替代。
1. 声明
我们努力提供准确的翻译,可能有些部分不太准确,部分内容不太重要并没有进行翻译,如有需要请参见原文。
2. 原文地址
https://github.com/ethereum/go-ethereum/blob/master/docs/audits/2017-04-25_Geth-audit_Truesec.pdf
3. 参考链接