11.bank
2023-09-18 11:10:56
# 19.Paradigm CTF 2021
bank
分析
1.全局观
- Setup:初始化题目
- Bank
- ERC20Like:接口
- Bank
- 存款系统,一个用户可以拥有多个account,每个account可以存入多种代币,可以类比成metamask。
- 存取款、关闭最新的account、更新account的名字
- owner更换机制
2.任务
让bank的WETH数量归零
1 | function isSolved() external view returns (bool) { |
3.详细分析
3.1发掘漏洞
先分析资产和状态情况:
ETH | WETH | 授权 | 状态 | |
---|---|---|---|---|
Setup | WETH:=>Bank, max | |||
Bank | 50 | owner=Setup, deposit[Setup]=50WETH | ||
WETH |
一看这道题,就知道是考点在于转账(代码量足够少,能出问题的地方不多)。
- 先来看看可能有问题的方法
- bank合约的owner转让两个方法是没有操作空间的。不可行。
setAccountName()
和closeLastAccount()
:虽然closeLastAccount()
可以使数组长度减小,但没有外部调用因此不可能重入,也就不可能使其向下溢出。不可行。- 那么存取款函数是肯定有问题的
- 将条件检查的代码去掉,剩下主体可以发现,先转账,后检查状态,很明显的重入。
- 存款函数虽然可以重入,但是用垃圾token利用自己实现的
transferFrom()
进行重入,重入增加的数量只是垃圾token,并不会影响到WETH。不可行。 - 取款函数:用垃圾token利用自己实现的
transfer()
进行重入,由于版本小于0.8.0,因此重入之后accounts数组下溢导致数组大小覆盖整个storage,然后我们就可以利用setAccountName()
来修改任何数据。
1 | function depositToken(uint accountId, address token, uint amount) external { |
3.2利用漏洞
1.重入取款函数使得下溢
可以在三个函数balanceOf()
, transferFrom()
, transfer()
可以作为重入,需要选择在合适的方法合适的位置进行重入才能使得数组长度下溢,这是因为取款函数使数组长度减少是有条件的account.uniqueTokens == 0 && accountId == lastAccount
。经过数次尝试,下面的重入逻辑是可行的:左边的数字是重入方法执行的次序,[]中的内容是程序执行过程中全局变量的状态
1 | [len=0, uniqueTokens=0] |
配套的方法如下:
1 | function balanceOf(address) public returns(uint256){ |
2.找到位置,修改内容
既然我们成功修改了数组大小为无穷,那么我们就可以修改Bank的所有内容,通过setAccountName()
:
1 | struct Account { |
我们只能修改accountName,我们需要找到我们在bank合约中记录WETH余额记录的位置,然后修改为50WETH,就可以取走50WETH了。但这是一个复杂的结构体,因此要找位置便有点麻烦。下面是找到第n个account对应WETH余额存储的位置:
- 找到Account[]长度的位置:
a = keccak256(bytes32(msg.sender)+bytes32(0x02))
- 找到第一个Account的初始位置:
b = keccak256(bytes32(a))
- 找到第n个Account,即accountId为0:
c = keccak256(b) + 3*n
- 找到第n个Account的WETH对应的余额:
d = c + 2
e = keccak256(bytes32(WETH)+bytes32(d))
找到第n个account对应WETH余额存储的位置e
之后,因为我们的方法之后修改结构体的第一个参数,因此我们需要比较一下看看第n个account能不能刚好覆盖WETH余额的位置,如果不行,则换下一个account:
找到刚好能够覆盖的account,则修改WETH余额,再withdraw那个account。
解题
1 | // SPDX-License-Identifier: UNLICENSED |