FTN 合约漏洞分析

  • Ashton
  • 更新于 2018-12-28 14:49
  • 阅读 3720

FTN 合约漏洞分析

对代码没有敬畏感,对程序员的工作没有足够的尊重,对工程流程不能真正的重视,早晚会获得傲慢的代价。

0x01 攻击回顾

在 FTN 持币用户数还不是很大的时候被攻击,是 FTN 的幸运,影响远没有美图 BEC 那次攻击事件大。下面是 FTN 在被攻击那天的公告。

Fountain 项目通证 FTN 于新加坡时间 2018 年 12 月 26 日晚 23 时出现数笔异常链上交易。  
在触发紧急响应预案后,Fountain 基金会第一时间启动了链上交易冻结措施,并立即向 CoinTiger 及 CoinBene 两家合作交易所发出了暂时关闭 FTN/USDT、FTN/BTC 交易对的请求。

下面是第一笔攻击交易的截图,这个交易也可以从这个 链接 查看。交易发生在 UTC 时间 03:23:24 PM,也就是北京时间晚 23 点 24 分。攻击者转出 115,792,089,237,316,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000.584007913129639935 个 FTN 到某个地址。

攻击交易

在攻击发生后一个多小时,FTN 做出反应,执行了 pause 操作,让合约暂停转账,反应还是相当及时的。

0x02 攻击分析

下面是攻击者攻击合约时的合约方法调用,可以在 这个交易 中看到。可以看到出问题的是 batchTransfers(address[] receivers, uint256[] amounts) 这个方法。这里看到的 8 个参数数据,前面两个表示的是两个数组参数的位置,第三个表示第一个数组的大小是 2,第四和五个数据是第一个数组参数的实际数据,第六个数据表示第二个数组参数的大小是 2, 第七和第八个数据是第二个数组参数的实际数据。转换一下,这个方法调用实际是 batchTransfers(["0x5aaa48f6734e2e1c2d7d723fb9182755c9486704", "0x8ce6ae7e954a5a95ff02161b83308955ebc832cf"], ["2", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"])

Function: batchTransfers(address[] receivers, uint256[] amounts)

MethodID: 0x3badca25
[0]:  0000000000000000000000000000000000000000000000000000000000000040
[1]:  00000000000000000000000000000000000000000000000000000000000000a0
[2]:  0000000000000000000000000000000000000000000000000000000000000002
[3]:  0000000000000000000000005aaa48f6734e2e1c2d7d723fb9182755c9486704
[4]:  0000000000000000000000008ce6ae7e954a5a95ff02161b83308955ebc832cf
[5]:  0000000000000000000000000000000000000000000000000000000000000002
[6]:  0000000000000000000000000000000000000000000000000000000000000002
[7]:  ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff

为什么这么一个方法调用会攻击成功呢? 我们来看一下源码,源码都是公开的, 这里 可以看到。这就是去中心化的魅力,如果这个合约不是在部署在以太坊上,而是部署在 BAT 的服务器上,估计这个痕迹早就被销毁了。

我们直接截取被攻击的那段代码,看看有何神奇之处。

function batchTransfers (address[] receivers, uint256[] amounts) public whenRunning returns (bool) {
        uint receiveLength = receivers.length;
        require(receiveLength == amounts.length);

        uint receiverCount = 0;
        uint256 totalAmount = 0;
        uint i;
        address r;
        for (i = 0; i < receiveLength; i ++) {
            r = receivers[i];
            if (r == address(0) || r == owner) continue;
            receiverCount ++;
            totalAmount += amounts[i];
        }
        require(totalAmount > 0);
        require(canPay(msg.sender, totalAmount));

        wallets[msg.sender] -= totalAmount;
        uint256 amount;
        for (i = 0; i < receiveLength; i++) {
            r = receivers[i];
            if (r == address(0) || r == owner) continue;
            amount = amounts[i];
            if (amount == 0) continue;
            wallets[r] = wallets[r].add(amount);
            emit Transfer(msg.sender, r, amount);
        }
        return true;
    }

有一定经验的合约工程师估计立马会把目标锁定在下面的代码片段。

  1. 这里在 for 循环里直接对参数进行累加是合约编写的大忌。因为外部传参是我们无法控制的,非常容易导致数据溢出。
  2. 合约攻击往往会从突破 require 语句下手。这次攻击成功,很大可能就是突破了这个代码片段里的两个逻辑上非常合理的 require 语句,使后面的转账操作可以顺利完成。
       for (i = 0; i < receiveLength; i ++) {
            r = receivers[i];
            if (r == address(0) || r == owner) continue;
            receiverCount ++;
            totalAmount += amounts[i];
        }
        require(totalAmount > 0);
        require(canPay(msg.sender, totalAmount));

事实是不是这样呢?我们把攻击者传入的参数带入进去看一下。 当攻击者通过 batchTransfers(["0x5aaa48f6734e2e1c2d7d723fb9182755c9486704", "0x8ce6ae7e954a5a95ff02161b83308955ebc832cf"], ["2", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"]) 进行调用时,receiveLength 为 2,amounts 数组里的数据是 ["2", "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"] ,将数组里的数据通过 totalAmount += amounts[i] 进行累加,因为 “ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff” 已经是 uint256 的最大值,再加上 2 肯定会发生溢出,2 的二进制表示为"10", 相加之后得到的数据为“10000000000000000000000000000000000000000000000000000000000000001”,超出 uint256 范围的最高位被舍弃,得到最终的 totalAmount 的值为 1。可以轻松通过两个 require 语句的校验,后面的转账就很简单了,这里不多说。

0x03 如何修复

对于这种整数溢出漏洞,最简单的方法是采用 SafeMath 数学计算库来避免。这个库在 FTN 的代码里也有引用,有趣的是 FTN 智能合约代码中,其他的都使用了SafeMath,而出问题的 totalAmount += amounts[i] 却没有使用。很可能是由临时工匆忙打进去的补丁。 这里将 totalAmount += amounts[i] 改为 totalAmount = totalAmount.add(amounts[i]) 就把问题解决了。

0x04 忠告

合约里进行加减乘除,没有特别的原因,使用 SafeMath 总是没错的。 善待每一行代码,不要用损失来证明它的价值。 不要低估智能合约,它并不像看起来的那么简单。

点赞 0
收藏 0
分享

0 条评论

请先 登录 后评论
Ashton
Ashton
专注于 EVM 和比特币生态的区块链开发者