众所周知,智能合约的特性之一是公开透明,该特性的表现之一就是,区块链上的所有交易也是公开可见的,任何地址与智能合约所进行的交易都会被存储在链上。
在正常情况下,我们可能很少去关注这些交易信息,但在某些情况下,这些交易信息可能成为辅助我们研究的利器。比如当智能合约爆出漏洞时,为了利用智能合约的漏洞,攻击者往往需要构造特定的交易来触发漏洞,而这些交易信息最终将会被存储在链上。通过分析这些存储在链上的交易信息,我们就能推断出攻击者是如何利用漏洞,进而达到复现或者是修复漏洞的目的。
交易数据分析
首先,以最经典的 ERC 20 代币为例,笔者在测试链上部署了如下代码:
pragma solidity ^0.4.24;
interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }
contract TokenERC20 {
string public name;
string public symbol;
string public hint;
uint8 public decimals = 18;
uint256 public totalSupply;
mapping (address => uint256) public balanceOf; //
mapping (address => mapping (address => uint256)) public allowance;
event Transfer(address indexed from, address indexed to, uint256 value);
event Burn(address indexed from, uint256 value);
function TokenERC20(uint256 initialSupply, string tokenName, string tokenSymbol) public {
totalSupply = initialSupply * 10 ** uint256(decimals);
balanceOf[msg.sender] = totalSupply;
name = tokenName;
symbol = tokenSymbol;
}
function _transfer(address _from, address _to, uint _value) internal {
require(_to != 0x0);
require(balanceOf[_from] >= _value);
require(balanceOf[_to] + _value > balanceOf[_to]);
uint previousBalances = balanceOf[_from] + balanceOf[_to];
balanceOf[_from] -= _value;
balanceOf[_to] += _value;
Transfer(_from, _to, _value);
assert(balanceOf[_from] + balanceOf[_to] == previousBalances);
}
function transfer(address _to, uint256 _value) public returns (bool) {
_transfer(msg.sender, _to, _value);
return true;
}
function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
require(_value <= allowance[_from][msg.sender]);
allowance[_from][msg.sender] -= _value;
_transfer(_from, _to, _value);
return true;
}
function approve(address _spender, uint256 _value) public
returns (bool success) {
allowance[msg.sender][_spender] = _value;
return true;
}
function approveAndCall(address _spender, uint256 _value, bytes _extraData) public returns (bool success) {
tokenRecipient spender = tokenRecipient(_spender);
if (approve(_spender, _value)) {
spender.receiveApproval(msg.sender, _value, this, _extraData);
return true;
}
}
function burn(uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value);
balanceOf[msg.sender] -= _value;
totalSupply -= _value;
Burn(msg.sender, _value);
return true;
}
function burnFrom(address _from, uint256 _value) public returns (bool success) {
require(balanceOf[_from] >= _value);
require(_value <= allowance[_from][msg.sender]);
balanceOf[_from] -= _value;
allowance[_from][msg.sender] -= _value;
totalSupply -= _value;
Burn(_from, _value);
return true;
}
function gift() public returns (bool success) {
require(balanceOf[msg.sender] == 0);
balanceOf[msg.sender] += 1000;
return true;
}
function setHint(string _hint) public {
hint = _hint;
}
}
然后随机调用了几个函数,利用 etherscan 平台,可以清晰地看到这几个交易:
可以看到一条记录由七个字段构成:Txn Hash
/ Block
/ Age
/ From
/ To
/ Value
/ Txn Fee
,一般来说较有价值的是以下几个字段:
-
Age
=> 交易的时间戳 -
From
=> 交易的发起者 -
Value
=> 交易所携带 eth 数目
下面再看一个交易的具体细节:
可以看到大部分数据,如 Transaction Hash
、Block
、From
、To
等已经出现在上一张表格中,这里需要关注的是 Input Data
,也就是我们具体调用的函数和相应参数。点击 Decode Input Data
,原始数据会被解码成具体的参数:
根据 decode 的结果,可以看到,调用的是 setHint
函数,函数参数名是 _hint
,参数类型是 string
,值为 this is a simple hint
,对应的原始代码如下:
function setHint(string _hint) public {
hint = _hint;
}
再看多参数的函数调用,可以看到和上一个函数一样,etherscan 对函数参数名、参数类型、具体的值做了解析,然后参数的顺序跟函数声明里的参数顺序保持一致。
function approve(address _spender, uint256 _value) public returns (bool success) {
allowance[msg.sender][_spender] = _value;
return true;
}
Internal Transaction
看完了普通的 Transaction,再看 Internal Transcation,可以看到 etherscan 对这两种交易做了很明显的区分:
按网上的描述和笔者的理解,Internal Transaction
并不是一个真正的 Transaction
,它是在一笔交易执行过程中,合约根据一定条件,进行转账或者是调用新合约等一系列动作产生的结果,正如 etherscan 上标注的一样,Internal Transactions as a result of Contract Execution
。
或者换种更简单的理解方式,以太坊中有两种账户:
-
外部账户
外部(用户)直接控制的账户,通过私钥控制,没有相关代码 -
合约账户
合约控制的账户,存在相关代码且可以执行
而 Transaction
就是外部账户发起的,比如说向某个账户转账,调用合约函数等;而 Internal Transaction
则是合约账户发起的,比如 A 合约调用了 B 合约的 gift()
函数,那么这个动作就会被 etherscan 标记为内部交易。
下面结合具体的例子,继续细看 Internal Transaction
:
交易链接如下:https://ropsten.etherscan.io/tx/0xf0b5ca1c856d5594fc6e89fc31b84b550fcb1f3d4c56529da3fac637cac497d8 ,可以看到是外部账户 0x10108bab01d0811f233b703dccb005db27df764f 向合约账户 0xcf05704193697c509ba64e941ea34f3fc2477614 发起的一个交易。
继续看 Internal Transactions
标签,可以看到由于交易的过程中,0xcf05704193697c509ba64e941ea34f3fc2477614 会调用地址为 0xDb73fb49aAD40119149387CB5583BF31660457B6 的合约,所以这两个合约间的操作被 etherscan 标记为 Internal Transaction
。
很多情况下,Internal Transaction
是攻击者用于隐藏自己的一种方式,但由于智能合约公开透明的特性,这种隐藏的手段最终也只能略微增加对流量分析的难度。
利用交易信息重现攻击
前面已经铺垫了很多基础知识,下面来看如何利用交易信息来重现攻击。以去年 hctf 一道智能合约的题目 ez2win 为例,我们将通过对交易信息分析进行解题。
合约地址:https://ropsten.etherscan.io/address/0x71feca5f0ff0123a60ef2871ba6a6e5d289942ef ,关键部分的源码如下:
contract D2GBToken is ERC20 {
string public constant name = "D2GB";
string public constant symbol = "D2GB";
uint8 public constant decimals = 18;
uint256 public constant INITIAL_SUPPLY = 20000000000 * (10 ** uint256(decimals));
/**
* @dev Constructor that gives msg.sender all of existing tokens.
*/
constructor() public {
_totalSupply = INITIAL_SUPPLY;
_balances[msg.sender] = INITIAL_SUPPLY;
initialized[msg.sender] = true;
emit Transfer(address(0), msg.sender, INITIAL_SUPPLY);
}
//flag
function PayForFlag(string b64email) public payable returns (bool success){
require (_balances[msg.sender] > 10000000);
emit GetFlag(b64email, "Get flag!");
}
}
首先,为了快速定位到成功获得 flag 队伍的交易,我们可以利用 PayForFlag
函数会触发 event 的特性,从 Events
中定位关键交易:
选取第二条交易,定位交易者的地址:
然后根据合约地址,定位跟该合约相关的所有交易:
其中 Txn Hash
为 0xe1f5645b9ead7a8…
和 0xa2a7e26c2ea1a4…
的交易为最终获得 flag 的交易,那么可以推测后两条交易则是利用合约漏洞,使其满足 _balances[msg.sender] > 10000000
的关键交易,继续查看相应交易的内容:
可以看到这两个交易其实都是执行了 _transfer
函数,查看相应源码,可以很明显地看到,由于没有 private 修饰符的修饰,该函数可以被外部调用,导致的实际效果就是,任意用户可以从其他用户那转走任意数目的 balances(每次的数目不超过 10000000):
function _transfer(address from, address to, uint256 value) {
require(value <= _balances[from]);
require(to != address(0));
require(value <= 10000000);
_balances[from] = _balances[from].sub(value);
_balances[to] = _balances[to].add(value);
}
那我们直接模仿构造相应交易:
txn1 – _transfer
# | Name | Type | Data |
---|---|---|---|
0 | from | address | 56d08c5d7ccee25aebdc4ae0274557c462ce1fd7 |
1 | to | address | 10108bab01d0811f233b703dccb005db27df764f |
2 | value | uint256 | 100000 |
txn2 – _transfer
# | Name | Type | Data |
---|---|---|---|
0 | from | address | f41010e4ec32715c0690f7baecdc61d56ba8b1b1 |
1 | to | address | 10108bab01d0811f233b703dccb005db27df764f |
2 | value | uint256 | 10000000 |
txn3 – PayForFlag
# | Name | Type | Data |
---|---|---|---|
0 | b64email | string | MTE0NTE0QDE5MTkub2lzaGlp |
成功触发 GetFlag
event :
总结
可以看到,合理地对智能合约的交易进行分析,能有效地帮助我们定位智能合约漏洞,对漏洞的复现和修复有着极大的意义。
到目前为止, 本文介绍的都是有源码智能合约的交易分析,无源码的智能合约交易分析更为复杂,在下一篇文章中,我将更进一步介绍,如何在没有源代码的情况下,对智能合约的交易进行分析。
发表评论
您还未登录,请先登录。
登录