FTN 合约漏洞分析
对代码没有敬畏感,对程序员的工作没有足够的尊重,对工程流程不能真正的重视,早晚会获得傲慢的代价。
在 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 操作,让合约暂停转账,反应还是相当及时的。
下面是攻击者攻击合约时的合约方法调用,可以在 这个交易 中看到。可以看到出问题的是 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;
}
有一定经验的合约工程师估计立马会把目标锁定在下面的代码片段。
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 语句的校验,后面的转账就很简单了,这里不多说。
对于这种整数溢出漏洞,最简单的方法是采用 SafeMath 数学计算库来避免。这个库在 FTN 的代码里也有引用,有趣的是 FTN 智能合约代码中,其他的都使用了SafeMath,而出问题的 totalAmount += amounts[i] 却没有使用。很可能是由临时工匆忙打进去的补丁。
这里将 totalAmount += amounts[i]
改为 totalAmount = totalAmount.add(amounts[i])
就把问题解决了。
合约里进行加减乘除,没有特别的原因,使用 SafeMath 总是没错的。 善待每一行代码,不要用损失来证明它的价值。 不要低估智能合约,它并不像看起来的那么简单。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!