技术详解:链上打新局中局,大规模Rug Pull手法解密
近日,CertiK?安全专家团队频繁检测到多起手法相同的“退出骗局”,也就是我们俗称的?Rug Pull。
在我们进行深入挖掘后发现,多起相同手法的事件都指向同一个团伙,最终关联到超过?200?个?Token?退出骗局。这预示着我们可能发现了一个大规模自动化的,通过“退出骗局”方式进行资产收割的黑客团队。
在这些退出骗局中,攻击者会创建一个新的?ERC?20?代币,并用创建时预挖的代币加上一定数量的?WETH?创建一个?Uniswap V2的流动性池。
当链上的打新机器人或用户在该流动性池购买一定次数的新代币后,攻击者则会通过凭空产生的代币,将流动性池中的?WETH?全部耗尽。
由于攻击者在凭空获取的代币没有体现在总供应量中(totalSupply),也不触发?Transfer?事件,在?etherscan?是看不到的,因此外界难以感知。
攻击者不仅考虑了隐蔽性,还设计了一个局中局,用来麻痹拥有初级技术能力,会看?etherscan?的用户,用一个小的问题来掩盖他们真正的目的……
我们以其中一个案例为例,详解一下该退出骗局的细节。
被我们检测到的实际上是攻击者用巨量代币(偷偷?mint?的)耗干流动性池并获利的交易,在该交易中,项目方共计用?416,?483,?104,?164,?831?(约?416?万亿)个?MUMI?兑换出了约?9.736?个?WETH,耗干了池子的流动性。
然而该交易只是整个骗局的最后一环,我们要了解整个骗局,就需要继续往前追溯。
3?月?6?日上午?7?点?52?分(UTC?时间,下文同),攻击者地址(0x?8?AF?8)Rug Pull?部署了名为?MUMI(全名为?MultiMixer AI)的?ERC?20?代币(地址为0x?4894),并预挖了?420,?690,?000?(约?4.2?亿)个代币且全部分配给合约部署者。
预挖代币数量与合约源码相对应。
8?点整(代币创建?8?分钟后),攻击者地址(0x?8?AF?8)开始添加流动性。
攻击者地址(0x?8?AF?8)调用代币合约中的?openTrading?函数,通过?uniswap v2 factory?创建?MUMI-WETH?流动性池,将预挖的所有代币和?3?个?ETH?添加到流动性池中,最后获得约?1.036?个?LP?代币。
从交易细节可以看出,原本用于添加流动性的?420,?690,?000?(约?4.2?亿)个代币中,有?63,?103,?500?(约?6300?万)约个代币又被发送回代币合约(地址0x?4894),通过查看合约源码发现,代币合约会为每笔转账收取一定的手续费,而收取手续费的地址正是代币合约本身(具体实现在“_transfer?函数中”)。
奇怪的是,合约中已经设置了税收地址0x?7?ffb(收取转账手续费的地址),最后手续费却被发到代币合约自身。
因此最后被添加到流动性池的?MUMI?代币数量为扣完税的?357,?586,?500?(约?3.5?亿),而不是?420,?690,?000?(约?4.3?亿)。
8?点?1?分(流动性池创建?1?分钟后),攻击者地址(0x?8?AF?8)锁定了通过添加流动性获取的全部?1.036?个?LP?代币。
LP?被锁定后,理论上攻击者地址(0x?8?AF?8)拥有的所有的?MUMI?代币便被锁定在流动性池内(除开作为手续费的那部分),因此攻击者地址(0x?8?AF?8)也不具备通过移除流动性进行?Rug Pull?的能力。为了让用户放心购买新推出的代币,许多项目方都是将?LP?进行锁定,意思就是项目方在说:“我不会跑路的,大家放心买吧!”,然而事实真的是这样吗?显然不是,这个案例便是如此,让我们继续分析。
8?点?10?分,出现了新的攻击者地址②(0x?9?DF?4),Ta?部署了代币合约中声明的税收地址0x?7?ffb。
这里有三个值得一提的点:
1.部署税收地址的地址和部署代币的地址并不是同一个,这可能说明项目方在有意减少各个操作之间与地址的关联性,提高行为溯源的难度
2.税收地址的合约不开源,也就是说税收地址中可能隐藏有不想暴露的操作
3.税收合约比代币合约晚部署,而代币合约中税收地址已被写死,这意味着项目方可以预知税收合约的地址,由于?CREATE?指令在确定创建者地址和?nonce?的情况下,部署合约地址是确定的,因此项目方提前就使用创建者地址模拟计算出了合约地址
实际上有不少退出骗局都是通过税收地址进行,且税收地址的部署模式特征符合上述的?1、?2?点。
上午?11?点(代币创建?3?小时后),攻击者地址②(0x?9?DF?4)进行了?Rug Pull。他通过调用税收合约(0x?77?fb)的“swapExactETHForTokens”方法,用税收地址中的?416,?483,?104,?164,?831?(约?416?万亿)个?MUMI?代币兑换出了约?9.736?个?ETH,并耗尽了池子中流动性。
由于税收合约(0x?77?fb)不开源,我们对其字节码进行反编译,反编结果如下:https://app.dedaub.com/decompile?md5=01e2888c7691219bb7ea8c6b6befe11c查看完税收合约(0x?77?fb)的“swapExactETHForTokens”方法反编译代码后,我们发现实际上该函数实现的主要功能就是通过?uniswap?V2 router?将数量为“xt”(调用者指定)的税收合约(0x?77?fb)拥有的?MUMI?代币兑换成?ETH,并发送给税收地址中声明的“_manualSwap”地址。
_manualSwap?地址所处的?storage?地址为0x?0?,用?json-rpc?的?getStorageAt?命令进行查询后发现_manualSwap?对应的地址正是税收合约(0x?77?fb)的部署者:攻击者②(0x?9?DF?4)。
该笔?RugPull?交易的输入参数?xt?为?420,?690,?000,?000,?000,?000,?000,?000?,对应?420,?690,?000,?000,?000?(约?420?万亿)个?MUMI?代币(MUMI?代币的?decimal?为?9)。也就是说,最终项目方用?420,?690,?000,?000,?000?(约?420?万亿)个?MUMI?将流动性池中的?WETH?耗干,完成整个退出骗局。
然而这里有一个至关重要的问题,就是税收合约(0x?77?fb)哪来的这么多?MUMI?代币?
从前面的内容我们得知,MUMI?代币在部署时的代币合约时的总供应量为?420,?690,?000?(约?4.2?亿),而在退出骗局结束后,我们在?MUMI?代币合约中查询到的总供应量依旧是?420,?690,?000?(下图中显示为?420,?690,?000,?000,?000,?000?,需要减去?decimal?对应位数的?0?,decimal?为?9),税收合约(0x?77?fb)中的远超总供应量的代币(420,?690,?000,?000,?000?,约?420?万亿)就仿佛凭空出现的一样,要知道,如上文所提,0x?77?fb?作为税收地址甚至没有被用于接收?MUMI?代币转账过程中产生的手续费,税收被代币合约接收了。
税收合约哪来的代币
为了探究税收合约(0x?7?ffb)的代币来源,我们查看了它的?ERC?20?转账事件历史。
结果发现在全部?6?笔关于0x?77?fb?的转账事件中,只有从税收合约(0x?7?ffb)转出的事件,而没有任何?MUMI?代币转入的事件,乍一看,税收合约(0x?7?ffb)的代币还真是凭空出现的。
所以税收合约(0x?7?ffb)地址中凭空出现的巨额?MUMI?代币有两个特点:
1.没有对?MUMI?合约的?totalSupply?产生影响
2.代币的增加没有触发?Transfer?事件
那么思路就很明确了,即?MUMI?代币合约中一定存在后门,这个后门直接对?balance?变量进行修改,且在修改?balabce?的同时不对应修改?totalSupply,也不触发?Transfer?事件。
也就是说,这是一个不标准的、或者说是恶意的?ERC?20?代币实现,用户无法从总供应量的变化和事件中感知到项目方在偷偷?mint?代币。
接着就是验证上面的想法,我们直接在?MUMI?代币合约源码中搜索关键字“balance”。
结果我们发现合约中有一个?private?类型的“swapTokensForEth”函数,传入参数为?uint?256?类型的?tokenAmount,在该函数的第?5?行,项目方直接将_taxWallet,也就是税收合约(0x?7?ffb)的?MUMI?余额修改为?tokenAmount * 10**_decimals,也就是?tokenAmount?的?1,?000,?000,?000?(约?10?亿)倍,然后再从流动性池中将?tokenAmount?数量的?MUMI?兑换为?ETH?并存在代币合约(0x?4894)中。
再接着搜索关键字“swapTokenForEth“。
“swapTokenForEth”函数在“_transfer”函数中被调用,再细看调用条件,会发现:
1.当转账的接收地址?to?地址为?MUMI-WETH?流动性池。
2.当有其他地址在流动性池中购买?MUMI?代币的数量超过_preventSwapBefore(5?次)时,“swapTokenForEth”函数才会被调用
3.传入的?tokenAmount?为代币地址所拥有的?MUMI?代币余额和_maxTaxSwap?之间的较小值
也就是说当合约检测到用户在池子中用?WETH?兑换成?MUMI?代币超过?5?次后,便会为税收地址偷偷?mint?巨量代币,并将一部分代币兑换成?ETH?存储在代币合约中。
一方面,项目方表面上进行收税并定期自动换成少量?ETH?放到代币合约,这是给用户看的,让大家以为这就是项目方的利润来源。
另一方面,项目方真正在做的,则是在用户交易次数达到?5?次后,直接修改账户余额,把流动性池全部抽干。
执行完“swapTokenForEth”函数后,“_transfer”函数还会执行?sendETHToFee?将代币地址中收税获得的?ETH?发送到税收合约(0x?77?fb)。
税收合约(0x?77?fb)中的?ETH?可以被其合约内实现的“rescue”函数取出。
现在再回看整个退出骗局中最后一笔获利交易的兑换记录。
获利交易中共进行了两次兑换,第一次是?4,?164,?831?(约?416?万)个?MUMI?代币换?0.349?个?ETH,第二次是?416,?483,?100,?000,?000?(约?416?万亿)个?MUMI?代币换?9.368?个?ETH。其中第二次兑换即为税收合约(0x?7?ffb)中“swapExactETHForTokens”函数内发起的兑换,之所以数量与输入参数代表的?420,?690,?000,?000,?000?(约?420?万亿)个代币不符,是因为有部分代币作为税收发送给了代币合约(0x?4894),如下图所示:
而第一次兑换对应的,则是在第二次兑换过程中,当代币从税收合约(0x?7?ffb)发送至?router?合约时,由因为满足代币合约内的后门函数触发条件,导致触发“swapTokensForEth”函数所发起的兑换,并非关键操作。
从上文中可以看出,MUMI?代币从部署,到创建流动性池,再到?Rug Pull,整个退出骗局周期才约?3?个小时,但是却以不到约?6.5?个?ETH?的成本(3 ETH?用于添加流动性,?3 ETH?用于从流动性池中兑换?MUMI?以作诱导,不到?0.5 ETH?用于部署合约和发起交易)获得了?9.7?个?ETH,利润超过?50%?。
攻击者用?ETH?换?MUMI?的交易有?5?笔,前文中并没有提到,交易信息如下:
通过分析在流动性中进行操作的?eoa?地址后发现,相当一部分的地址为链上的“打新机器人”,结合整个骗局快进快出的特点,我们有理由认为,这整个骗局针对的对象正是链上十分活跃的各种打新机器人、打新脚本。
因此无论是代币看似没必要但是复杂的合约设计、合约部署、流动性锁定流程,还是中途攻击者相关地址主动用?ETH?换取?MUMI?代币的疑惑行为,都可以理解成是攻击者为了试图骗过链上各类打新机器人的反欺诈程序而做的伪装。
我们通过追踪资金流后发现,攻击所获得的收益最后全被攻击地址②(0x?9?dF?4)发送到了地址资金沉淀地址(0x?DF?1?a)。而实际上我们最近检测到的多起退出骗局最初的资金来源以及最后的资金去向都指向这个地址,因此我们对这个地址的交易进行了大致的分析和统计。
最终发现,该地址在约?2?个月前开始活跃,到今天为止已经发起了超过?7,?000?笔交易,并且该地址已经和超过?200?个代币进行过交互。
我们对其中的约?40?个代币交易记录进行分析,然后发现我们查看的几乎所有代币对应的流动性池中,最后都会有一笔输入数量远大于代币总供应量的兑换交易将流动性池中的?ETH?耗尽,且整个退出骗局的周期都较短。
其中部分代币(名烟中华)的部署交易如下:
https://etherscan.io/tx/0x?0?ca?86151?3d?c?68?eaef?3017?e?7118?e?7538?d?999?f?9?b?4?a?53?e?1?b?477?f?1?f?1?ce?07?d?98?2d?c?3?f因此我们可以认定,该地址实际上就是一个大规模的自动化“退出骗局”收割机,收割的对象就是链上的打新机器人。
该地址现在仍在活跃。
写在最后
如果一个代币在?mint?时不对应修改?totalSupply,也不触发?Transfer?事件,那么我们是很难感知项目方是否有在偷偷?mint?代币的,这也将加剧“代币是否安全,完全依赖于项目方是否自觉”的现状。
因此我们可能需要考虑改进现有的代币机制或者引入一种有效的代币总量检测方案,来保障代币数量变更的公开透明,现在凭借?event?来捕获代币状态变更是不够的。
并且我们需要警醒的是,尽管现在大家的防骗意识在提高,但是攻击者的反防骗手段也在提高,这是一场永不停息的博弈,我们需要保持不断学习和思考,才能在这样的博弈中保全自身。
查看交易基本信息:
合约反编译: