Paraluni 被黑分析

前言

Paraluni (平行宇宙)是新加坡 Parallel Universe 基金会发布的一个 基于币安智能链的 DeFi 项目,更多相关内容见此处。在 2022 年 03 月 13 日,Paraluni 遭受黑客攻击,损失约 170 万美元。

本次复现是参考学习了各位大佬的复现教程一步一步摸索记录下来的,更多的是对攻击过程中每个步骤的求证(根据这个,我们可以得知它做了什么),而对于一些启发性的思路还有所欠缺(你怎么就知道要看这里?),在文末的引用文章中可以学习一下大佬们的思路。

相关地址

攻击者地址:https://bscscan.com/address/0x94bc1d555e63eea23fe7fdbf937ef3f9ac5fcf8f

攻击交易:https://bscscan.com/tx/0x70f367b9420ac2654a5223cc311c7f9c361736a39fd4e7dff9ed1b85bab7ad54

攻击合约:https://bscscan.com/address/0x4770b5cb9d51ecb7ad5b14f0d4f2cee8e5563645

UBT(ukraine bad token)代币合约:https://bscscan.com/address/0xca2ca459ec6e4f58ad88aeb7285d2e41747b9134

UGT(russia good token)代币合约:https://bscscan.com/address/0xbc5db89ce5ab8035a71c6cd1cd0f0721ad28b508

Masterchef 合约:https://bscscan.com/address/0xa386f30853a7eb7e6a25ec8389337a5c6973421d#code

ParaPair 合约:https://bscscan.com/address/0x3fd4fbd7a83062942b6589a2e9e2436dd8e134d4#code

ParaProxy 合约(代理 Masterchef 合约):https://bscscan.com/address/0x633fa755a83b015cccdc451f82c57ea0bd32b4b4#code

攻击流程分析

首先通过 tenderly 和 blocksec 分析交易过程,可以看到这笔交易的头部和尾部有借款还款操作,可以判断攻击者利用了闪电贷进行攻击。攻击者通过闪电贷贷出了 15w 的 USDT 和 BUSD。

交易的头部,借出代币

交易的尾部,归还代币

然后攻击者调用 ParaRouter.addLiquidity 将 15w 的 USDT 和 BUSD 添加到对应的 ParaPair 合约中,获取流动性代币 LP ,并将其转移到改写了 transferFrom 函数的 ERC20 标准的 UBT 代币合约(0xca2)中。

然后攻击合约(0x477)利用 USDT-BUSD 的 pid 以及 UBT-UGT 代币,通过 ParaProxy 代理的 MasterChef.depositByAddLiquidity 添加流动性。

调用 MasterChef.depositByAddLiquidity 函数时的参数输入情况。

MasterChef 合约中的内部调用 depositByAddLiquiditydepositByAddLiquidityInternaladdLiquidityInternal

相关函数代码

// MasterChef
function depositByAddLiquidity(uint256 _pid, address[2] memory _tokens, uint256[2] memory _amounts) external{
require(_amounts[0] > 0 && _amounts[1] > 0, "!0");
address[2] memory tokens;
uint256[2] memory amounts;
(tokens[0], amounts[0]) = _doTransferIn(msg.sender, _tokens[0], _amounts[0]);
(tokens[1], amounts[1]) = _doTransferIn(msg.sender, _tokens[1], _amounts[1]);
depositByAddLiquidityInternal(msg.sender, _pid, tokens,amounts);
} function depositByAddLiquidityInternal(address _user, uint256 _pid, address[2] memory _tokens, uint256[2] memory _amounts) internal {
PoolInfo memory pool = poolInfo[_pid];
require(address(pool.ticket) == address(0), "T:E");
uint liquidity = addLiquidityInternal(address(pool.lpToken), _user, _tokens, _amounts);
_deposit(_pid, liquidity, _user);
} function addLiquidityInternal(address _lpAddress, address _user, address[2] memory _tokens, uint256[2] memory _amounts) internal returns (uint){
//Stack too deep, try removing local variables
DepositVars memory vars;
approveIfNeeded(_tokens[0], address(paraRouter), _amounts[0]);
approveIfNeeded(_tokens[1], address(paraRouter), _amounts[1]);
vars.oldBalance = IERC20(_lpAddress).balanceOf(address(this));
(vars.amountA, vars.amountB, vars.liquidity) = paraRouter.addLiquidity(_tokens[0], _tokens[1], _amounts[0], _amounts[1], 1, 1, address(this), block.timestamp + 600);
vars.newBalance = IERC20(_lpAddress).balanceOf(address(this));
require(vars.newBalance > vars.oldBalance, "B:E");
vars.liquidity = vars.newBalance.sub(vars.oldBalance);
addChange(_user, _tokens[0], _amounts[0].sub(vars.amountA));
addChange(_user, _tokens[1], _amounts[1].sub(vars.amountB));
return vars.liquidity;
} // ParaRouter
function addLiquidity(
address tokenA,
address tokenB,
uint amountADesired,
uint amountBDesired,
uint amountAMin,
uint amountBMin,
address to,
uint deadline
) external virtual override ensure(deadline) returns (uint amountA, uint amountB, uint liquidity) {
noFees(tokenA, tokenB);
(amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
address pair = ParaLibrary.pairFor(factory, tokenA, tokenB);
TransferHelper.safeTransferFrom(tokenA, msg.sender, pair, amountA);
TransferHelper.safeTransferFrom(tokenB, msg.sender, pair, amountB);
liquidity = IParaPair(pair).mint(to);
FeesOn(tokenA, tokenB);
} // TransferHelper
function safeTransferFrom(
address token,
address from,
address to,
uint256 value
) internal {
// bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
(bool success, bytes memory data) = token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
require(success && (data.length == 0 || abi.decode(data, (bool))), 'TransferHelper: TRANSFER_FROM_FAILED');
}

而攻击点就在于上图框选出来的 safeTransferFrom 函数中。展开来看,UBT 在其 transferFrom 函数中调用了 MasterChef.deposit 函数。

从输入可以得知,UBT 合约的transferFrom 函数通过调用 MasterChef.deposit 将其持有的 USDT-BUSD 的 LP(pid = 18) 全部存入。然后 _deposit 函数记录 UBT 合约(0xca2)存入了 LP 代币。【第一次记录】

相关的 deposit 代码如下

// MasterChef
function deposit(uint256 _pid, uint256 _amount) external {
depositInternal(_pid, _amount, msg.sender, msg.sender);
} function depositInternal(uint256 _pid, uint256 _amount, address _user, address payer) internal {
PoolInfo storage pool = poolInfo[_pid];
pool.lpToken.safeTransferFrom(
address(payer),
address(this),
_amount
);
if (address(pool.ticket) != address(0)) {
UserInfo storage user = userInfo[_pid][_user];
uint256 new_amount = user.amount.add(_amount);
uint256 user_ticket_count = pool.ticket.tokensOfOwner(_user).length;
uint256 staked_ticket_count = ticket_staked_count(_user, address(pool.ticket));
uint256 ticket_level = pool.ticket.level();
(, uint overflow) = check_vip_limit(ticket_level, user_ticket_count + staked_ticket_count, new_amount);
require(overflow == 0, "Exceeding the ticket limit");
deposit_all_tickets(pool.ticket);
}
_deposit(_pid, _amount, _user);
} function _deposit(uint256 _pid, uint256 _amount, address _user) internal {
PoolInfo storage pool = poolInfo[_pid];
UserInfo storage user = userInfo[_pid][_user];
poolsTotalDeposit[_pid] = poolsTotalDeposit[_pid].add(_amount);
updatePool(_pid);
if (user.amount > 0) {
uint256 pending =
user.amount.mul(pool.accT42PerShare).div(1e12).sub(
user.rewardDebt
);
_claim(pool.pooltype, pending);
}
user.amount = user.amount.add(_amount);
user.rewardDebt = user.amount.mul(pool.accT42PerShare).div(1e12);
emit Deposit(_user, _pid, _amount);
}

完成重入攻击以后,函数返回到上一级调用 MasterChef.addLiquidityInternal 处,满足了 _lpAddress 对应的流动性代币增加这一条件。

然后返回到上一级函数, _deposit 函数根据返回的 liquidity 值记录攻击者(0x477)存入了 LP 代币。【第二次记录】

最后就是将手头上的 LP 换取等值的 USDT-BUSD 代币。

UBT 合约先调用 MasterChef.withdraw 函数将存入的 LP 取出,然后发送给攻击合约(0x477)

攻击合约同样先调用 MasterChef.withdraw 函数将存入的 LP 取出,然后调用 ParaRouter.removeliquidity 将价值 31W 的 USDT-BUSD 取走。

最后,归还闪电贷的 15W 代币,然后把剩余代币转账走。

攻击流程

先闪电贷贷 15w 的 USDT 和 BUSD,调用 ParaRouter.addLiquidity 添加流动性,并将获得的 LP 转移到改写了 transferFrom 函数的 ERC20 标准的 UBT 代币合约中

然后利用 USDT-BUSD 的 pid 以及 UBT-UGT 代币,通过 MasterChef.depositByAddLiquidity 添加流动性,内部调用 depositByAddLiquiditydepositByAddLiquidityInternaladdLiquidityInternal

然后 addLiquidityInternal 函数在 paraRouter.addLiquidity 中调用 UBT 代币的 transferFrom 函数进行重入,重入了同合约的 deposit 函数

deposit 函数根据传入的 pid ,将 UBT 代币合约中 USDT-BUSD 的 LP 取走,并为 UBT 合约记录对应的流动性(第一次添加 USDT-BUSD 的 LP)

然后回到 addLiquidityInternal 函数,根据传入的 pid 获取对应的(USDT-BUSD) LP 代币地址,检测添加流动性前后的 LP 差值,并根据差值给攻击合约记录对应的流动性(第二次添加 USDT-BUSD 的 LP)

然后 UBT 合约调用 MasterChef.withdraw 函数将存入的 LP 取出,然后发送给攻击合约。攻击合约同样调用 MasterChef.withdraw 函数将存入的 LP 取出,然后调用 ParaRouter.removeliquidity 将价值 31W 的 USDT-BUSD 取走。

最后,攻击者归还 15w 的闪电贷,并将剩余的 15W 代币进行转移。

总结

本次攻击事件中被利用的漏洞有两处:

  1. MasterChef 合约在涉及到流动性操作的时候没有添加重入锁,使得攻击者可以在添加流动性的过程中进行重入攻击。
  2. MasterChef.depositByAddLiquidity 函数中没有检测 _pid_tokens 两个参数是否为对应关系,这使得攻击者可以自定义 _token 中的函数进行重入攻击。存入的是 _token ,但是计算的是 _pid 对应的池子的流动性。

参考文章

  1. 慢雾@九九
  2. yudan
  3. tenderly
  4. blocksec

【漏洞复现】Paraluni 安全事件分析及复现的更多相关文章

  1. 【Android漏洞复现】StrandHogg漏洞复现及原理分析_Android系统上的维京海盗

    文章作者MG1937 CNBLOG博客:ALDYS4 QQ:3496925334 0x00 StrandHogg漏洞详情 StrandHogg漏洞 CVE编号:暂无 [漏洞危害] 近日,Android ...

  2. DLink 815路由器栈溢出漏洞分析与复现

    DLink 815路由器栈溢出漏洞分析与复现 qemu模拟环境搭建 固件下载地址 File DIR-815_FIRMWARE_1.01.ZIP - Firmware for D-link DIR-81 ...

  3. CVE-2013-2551漏洞成因与利用分析(ISCC2014 PWN6)

    CVE-2013-2551漏洞成因与利用分析 1. 简介 VUPEN在Pwn2Own2013上利用此漏洞攻破了Win8+IE10,5月22日VUPEN在其博客上公布了漏洞的细节.它是一个ORG数组整数 ...

  4. CVE-2014-0322漏洞成因与利用分析

    CVE-2014-0322漏洞成因与利用分析 1. 简介 此漏洞是UAF(Use After Free)类漏洞,即引用了已经释放的内存,对指定内存处的值进行了加1.其特点在于攻击者结合flash实现了 ...

  5. CVE-2013-3897漏洞成因与利用分析

    CVE-2013-3897漏洞成因与利用分析 1. 简介 此漏洞是UAF(Use After Free)类漏洞,即引用了已经释放的内存.攻击者可以利用此类漏洞实现远程代码执行.UAF漏洞的根源源于对对 ...

  6. 漏洞分析:OpenSSH用户枚举漏洞(CVE-2018-15473)分析

    漏洞分析:OpenSSH用户枚举漏洞(CVE-2018-15473)分析 漏洞分析:OpenSSH用户枚举漏洞(CVE-2018-15473)分析 - FreeBuf互联网安全新媒体平台 https: ...

  7. thinkphp5.0.22远程代码执行漏洞分析及复现

    虽然网上已经有几篇公开的漏洞分析文章,但都是针对5.1版本的,而且看起来都比较抽象:我没有深入分析5.1版本,但看了下网上分析5.1版本漏洞的文章,发现虽然POC都是一样的,但它们的漏洞触发原因是不同 ...

  8. Nexus Repository Manager 3(CVE-2019-7238) 远程代码执行漏洞分析和复现

    0x00 漏洞背景 Nexus Repository Manager 3是一款软件仓库,可以用来存储和分发Maven,NuGET等软件源仓库.其3.14.0及之前版本中,存在一处基于OrientDB自 ...

  9. Windows RDP的RCE漏洞分析和复现(CVE-2019-0708)

    0x00 漏洞描述 Windows系列服务器于2019年5月15号,被爆出高危漏洞,该漏洞影响范围较广如:windows2003.windows2008.windows2008 R2.windows ...

随机推荐

  1. PHP 取整

    转载请注明来源:https://www.cnblogs.com/hookjc/ 对于功能强大的PHP语言来说,能掌握到它的函数用法,一般都能灵活的编写各种需求的PHP程序.我们经常用到的PHP取整函数 ...

  2. php include,require,include_once,require_once 的区别

    include(),require(),include_once(),require_once()作用都是包含并运行指定文件,但是使用场景又有很大区别. 1.include()和require()的区 ...

  3. IO复习

    /* 字节流 输入字节流: ---------| InputStream 所有输入字节流的基类. 抽象类 ------------| FileInputStream 读取文件的输入字节流 ------ ...

  4. Python将py文件编译为exe的方法

    使用PyCharm工具写好的Python程序脚本,怎么将.py文件编译为可执行的.exe文件 前提是已经安装了Python环境. 第一步:在PyCharm内下载安装pyinstalle库或使用CMD安 ...

  5. 基于XC7A100T的PCIe千兆电口以太网收发卡

    一.板卡概述 本板卡采用Xilinx公司的Artix7系列的XC7A100T-2FGG484 芯片作为主处理器.包含双路千兆电口网络,双组DDR,PCIeX1 V1.1接口,板卡设计满足工业级要求. ...

  6. Involuting Bunny! (2021.9)

      文化课就很掉头发,文科都能学好我还怕竞赛?(   大概从"刷的题的题解"推广为"所有做的题的题解"吧,兔子比较懒,这样写题解轻松一些.   Gym10305 ...

  7. MyBatis辅助功能点三:延迟加载

    延迟加载即先加载必需信息,然后再根据需要进一步加载信息的方式.实际应用如:常出现先查询表A,再根据表A的输出结果查询表B的情况.而有些时候,从A表查询出来的数据,只有一部分要查询表B.这时用延迟加载就 ...

  8. AQS源码二探-JUC系列

    本文已在公众号上发布,感谢关注,期待和你交流. AQS源码二探-JUC系列 共享模式 doAcquireShared 这个方法是共享模式下获取资源失败,执行入队和等待操作,等待的线程在被唤醒后也在这个 ...

  9. python https 无法访问 SSLError("Can\'t connect to HTTPS URL because the SSL module is not available

    1,需要检查python 安装的时候是否支持 https 进入python 环境,import ssl 如果正常导入就可以使用https,不能导入就需要进入下一步. 2,查看系统是否安装了openss ...

  10. Clickhouse写入问题汇总

    Clickhouse写入问题汇总 Zookeeper相关 当clickhouse在建表时使用了Replicated引擎族时, 会对zookeeper有非常重的依赖, 这时候就要注意zookeeper集 ...