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. Android 实用开源库(不定期更新)

    ZXing 极其好用的二维码开源库. GayHub:https://github.com/zxing/zxing MPAndroidChart MPAndroidChart 是 Android 一个强 ...

  2. VC 窗口置顶并激活为当前窗体

    转载请注明来源:https://www.cnblogs.com/hookjc/ if (this != GetForegroundWindow())                           ...

  3. Foundation框架介绍

    1.Foundation框架介绍 什么是框架? 众多功能\API的集合 框架是由许多类.方法.函数.文档按照一定的逻辑组织起来的集合,以便使研发程序变得更容易,在OS X下的Mac操作系统中大约有80 ...

  4. java中Statement 对象

    1.创建Statement对象建立了到特定数据库的连接之后,就可用该连接发送 SQL 语句.Statement 对象用 Connection 的方法 createStatement 创建,如下列代码段 ...

  5. 【密码学工具】Ciphey和WinDecrypto的使用随笔

    1.Ciphey 官方文档 这个工具安装起来也很容易,用pip下载即可. pip install ciphey -i https://pypi.mirrors.ustc.edu.cn/simple/ ...

  6. 4G无线全网通太阳能水文设备电源监测系统BMS110

    钡铼技术BMS110模块可实现4路电池电压.2路模拟量.2路数字量和1路温度测量,支持Modbus RTU over TCP和MQTT通讯协议,DC9-36V电源供电.BMS110可应用于各种有使用蓄 ...

  7. 5.Flink实时项目之业务数据准备

    1. 流程介绍 在上一篇文章中,我们已经把客户端的页面日志,启动日志,曝光日志分别发送到kafka对应的主题中.在本文中,我们将把业务数据也发送到对应的kafka主题中. 通过maxwell采集业务数 ...

  8. Graph Based SLAM 基本原理

    作者 | Alex 01 引言 SLAM 基本框架大致分为两大类:基于概率的方法如 EKF, UKF, particle filters 和基于图的方法 .基于图的方法本质上是种优化方法,一个以最小化 ...

  9. pagehelper 自循环启动报错

    问题原因 问题产生的原因是 ServiceA实现类中引入了ServiceB,而在ServiceB实现类中又引入了ServiceA,导致循环依赖注入. 其实在代码开发过程中应该尽量避免这种操作的出现,即 ...

  10. vue从后台拿数据渲染页面图片

    <div class="list-content"> <div v-for="goods in goodsList" class=" ...