## 0x00: 背景 [R0ARStaking](https://etherscan.io/address/0xbd2cd71630f2da85399565f6f2b49c9d4ce0e77f#code) 智能合约是一个质押协议,用户可以存入LP Token,以定期获取收益。但令人意想不到的是,合约开发人员在构建该去中心化金库的同时,留下了一个独属于自己的**后门**。 该恶意开发人员使用特定的[账户](https://etherscan.io/address/0x8149f77504007450711023cf0ec11bdd6348401f),往协议中存入极少的LP Token。而在后续,却能够提取出协议的全部资金。那么,这其中发生了什么? <br> ## 0x01: 原因定位 从R0ARStaking合约的交易记录上看,该恶意账户进行了三个交易: ![image](https://hackmd.io/_uploads/rk4KV1xkxx.png) <br> 在最后一步,账户调用了合约的`EmergencyWithdraw()`函数来提取资金。该函数中有两处涉及资金转移,转移数量分别由`rewardAmount`和`withdrawAmount`决定,而这两个数量又是根据`user.amount`计算得来。 ![image](https://hackmd.io/_uploads/HJwIQ1xyll.png) <br> 那`user.amount`中的值是怎么来的?`Harvest()`函数不会改变该变量,那么来看看`Deposit()`: ![image](https://hackmd.io/_uploads/BJlJ9P1gJeg.png) 在`Deposit()`函数中,把传入的`_amount`叠加到`user.amount`。但问题是,该账户调用`Deposit()`时,传入的`_amount`是很小很小的,完全不足以提取出协议的全部资金。 <br> 与此同时,发现了一段奇怪的代码: ![image](https://hackmd.io/_uploads/H1t9yJgylx.png) 为什么说奇怪?这里定义了`NAME_SLOT`和`NAME_HASH`,在把`NAME_HASH`的值存到`NAME_SLOT`所指定的槽位后,这两个变量却没有在其他地方明确使用到。 <br> 其实结合上面`user.amount`的“灵异现象”,可以进行猜测:`NAME_SLOT`所指定的槽位可能是该恶意账户`user.amount`变量的槽位。对该槽位存入`NAME_HASH`, 相当于对`user.amount`赋了一个很大的值。 <br> ## 0x02: 验证想法 为了验证想法,可以计算一下该恶意账户`user.amount`变量所占的槽位是多少。 计算槽位需要对存储布局有一定的了解,如果相关知识欠缺,相信RareSkills的这两篇文章会对你很有帮助: 1. https://www.rareskills.io/post/evm-solidity-storage-layout 2. https://www.rareskills.io/post/solidity-dynamic <br> 首先,我们来看看R0ARStaking的继承关系,以及合约中包含的变量: ![image](https://hackmd.io/_uploads/rJ_xtxxJxg.png) 恶意账户在调用`EmergencyWithdraw()`时,传入的`_pid`为0,同时知道账户地址为`0x8149f77504007450711023cf0eC11BDd6348401F`。 因此,该恶意账户的`user.amount`,也相当于为: ``` userInfo[0][0x8149f77504007450711023cf0eC11BDd6348401F].amount ``` <br> 具体计算步骤: 1. 获取`userInfos`变量的基础槽位:`userInfo_baseSolt` = 4; 2. 通过第一个key(pid = 0)获取内部的基础槽位:`userInfo_innerBaseSlot = keccak256(key01, userInfo_baseSolt)`; 3. 通过第二个key(恶意账户地址)获取`UserInfo`结构体的槽位:`UserInfo_slot = keccak256(key02, userInfo_innerBaseSlot)` 4. `user.amount`的槽位等于`UserInfo_slot + 1`; <br> 计算代码: ``` solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract calcSlot { function calcslot() pure external returns(bytes32) { // struct UserInfo { Deposit[] deposits; uint256 amount; } // mapping (uint256 => mapping (address => UserInfo)) public userInfo; uint256 userInfo_baseSolt = 4; uint256 key01 = 0; // pid bytes32 userInfo_innerBaseSlot = keccak256(abi.encode(key01, userInfo_baseSolt)); address key02 = 0x8149f77504007450711023cf0eC11BDd6348401F; // hacker address bytes32 UserInfo_slot = keccak256(abi.encode(key02, userInfo_innerBaseSlot)); bytes32 userAmount_slot = bytes32(uint256(UserInfo_slot) + 1); return userAmount_slot; } } ``` <br> 计算结果: ![image](https://hackmd.io/_uploads/SkGhtegJxx.png) 可以看出,得到的槽位和`NAME_SLOT`是一致的: ``` bytes32 private constant NAME_SLOT = 0x7320715e23a405b30bfe5ed8f0781f194fa396c23546626bf8c46dcb790e6597; ``` <br> ## 0x03: 总结 总的来说,就是恶意开发人员在部署质押合约,执行构造函数时,就给自己的账户设置了极大的金额数值。在等到他人存入资金后,可以随时使用该账户收一波网。因为设置的数值极大,足以把协议的资金全部取出来。 <br>