6月28日深夜22点多,币圈链圈安全圈突然爆料 USDT 出现重大漏洞,攻击者可在不损失任何 USDT 的情况下向交易所成功充值USDT,并且可以正常交易以及提币。
国内一些区块链安全厂商对此发出了预警(来自慢雾):
交易所在进行 USDT 充值交易确认是否成功时存在逻辑缺陷,未校验区块链上交易详情中 valid 字段值是否为 true,导致“假充值”,用户未损失任何 USDT 却成功向交易所充值了 USDT,而且这些 USDT 可以正常进行交易。我们已经确认真实攻击发生!相关交易所应尽快暂停 USDT 充值功能,并自查代码是否存在该逻辑缺陷。
这里 valid 作用是什么?什么情况下会是false?
实验室的同事们扫描了Bitcoin区块中的USDT交易数据,找到了一笔无效的交易:
1b5c80f487d2bf8b69e1bbba2b1979aacb1aca7a094c00bcb9abd85f9af738ea
根据omniexplorer网站提供的 api 接口,我们可以看到这笔交易的具体信息如下 json 列表
{
"amount": "28.59995822",
"block": 502358,
"blockhash": "0000000000000000005968fa48c49d7c4fb2363369d59db82897853fd937c71a",
"blocktime": 1514985094,
"confirmations": 27488,
"divisible": true,
"fee": "0.00200000",
"invalidreason": "Sender has insufficient balance",
"ismine": false,
"positioninblock": 301,
"propertyid": 31,
"propertyname": "TetherUS",
"referenceaddress": "1Po1oWkD2LmodfkBYiAktwh76vkF93LKnh",
"sendingaddress": "18DmsHjHU6YM2ckFzub4pBneD8QXCXRTLR",
"txid": "1b5c80f487d2bf8b69e1bbba2b1979aacb1aca7a094c00bcb9abd85f9af738ea",
"type": "Simple Send",
"type_int": 0,
"valid": false, // 注意这里与上面的不同
"version": 0
}
上面找到的交易获取到的 valid
值为 false
,与慢雾区提供的有点不一致。
下面我们来分析一下问题发生的原因。
我们知道 USDT
的是在比特币的链上去构造自己的交易(Omni协议[1])的,上面 json
结构里面的发送人的地址 sendingaddress
(相当于 sender
),接收人的地址 referenceaddress
(相当于 to
),发送金额使用的是 amount
字段,那么这里的发送者和接收者以及金额在比特币的交易里面如何体现?
首先,我们都知道比特币的一笔交易,是有交易输入和交易输出, 我们先来看下一笔比特币交易的 json
结构:
1b5c80f487d2bf8b69e1bbba2b1979aacb1aca7a094c00bcb9abd85f9af738ea
{
"txid": "1b5c80f487d2bf8b69e1bbba2b1979aacb1aca7a094c00bcb9abd85f9af738ea",
"hash": "1b5c80f487d2bf8b69e1bbba2b1979aacb1aca7a094c00bcb9abd85f9af738ea",
"version": 1,
"size": 289,
"vsize": 289,
"locktime": 0,
"vin": [
{
"txid": "c246660965e7a383b028f07620149a0ddea7299b1986d6e3f862a1e4de953dcf",
"vout": 0,
"scriptSig": {
"asm": "...", // 这里我省略了,太长
"hex": "..." // 这里我省略了,太长
},
"sequence": 4294967295
}
],
"vout": [
{
"value": 0.00002184,
"n": 0,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 4f33533cecb86d55db0b5f89b9fdc2d5c2e51448 OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a9144f33533cecb86d55db0b5f89b9fdc2d5c2e5144888ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"18DmsHjHU6YM2ckFzub4pBneD8QXCXRTLR"
]
}
},
{
"value": 0.00000000,
"n": 1,
"scriptPubKey": {
"asm": "OP_RETURN 6f6d6e69000000000000001f00000000aa7812ae",
"hex": "6a146f6d6e69000000000000001f00000000aa7812ae",
"type": "nulldata"
}
},
{
"value": 0.00000546,
"n": 2,
"scriptPubKey": {
"asm": "OP_DUP OP_HASH160 fa0692278afe508514b5ffee8fe5e97732ce0669 OP_EQUALVERIFY OP_CHECKSIG",
"hex": "76a914fa0692278afe508514b5ffee8fe5e97732ce066988ac",
"reqSigs": 1,
"type": "pubkeyhash",
"addresses": [
"1Po1oWkD2LmodfkBYiAktwh76vkF93LKnh"
]
}
}
],
"hex": "...", // 这里我省略了,太长
"blockhash": "0000000000000000005968fa48c49d7c4fb2363369d59db82897853fd937c71a",
"confirmations": 27489,
"time": 1514985094,
"blocktime": 1514985094
}
上面结构中 vin
数组就是交易的输入,vout
数组就是交易的输出。我们可以看到上面交易的输出中(第二个输出),是一个非标准的交易输出,你看到了 OP_RETURN
是吧,这里你可以先理解就是要构造一笔 USDT
交易的标志吧
USDT
的交易的发送者是基于比特币交易的第一个输入的,这个怎么理解了,看下面的列子
vin[0] = 18DmsHjHU6YM2ckFzub4pBneD8QXCXRTLR << sender,这个地址就是 USDT 交易的发送者
vin[1] = xxxxx // 当然上面结构中只存在一个输入,这里只是取例说明多个的情况
接收者是基于比特币交易的输出的 vout
, 从 vout
数组的最后一个元素开始查找,如果最后一个元素的地址和发送者不相同,那么最后一个元素的地址就是接收者
sender = 18DmsHjHU6YM2ckFzub4pBneD8QXCXRTLR
vout[0] = 18DmsHjHU6YM2ckFzub4pBneD8QXCXRTLR
vout[1] = payload
vout[2] = 1Po1oWkD2LmodfkBYiAktwh76vkF93LKnh < to,这个是接收者
我们来考虑一种情况,如果最后一个元素的地址和发送者相同,则跳过
sender = 18DmsHjHU6YM2ckFzub4pBneD8QXCXRTLR
vout[0] = payload
vout[1] = 1Po1oWkD2LmodfkBYiAktwh76vkF93LKnh < to,这个是接收者
vout[2] = 18DmsHjHU6YM2ckFzub4pBneD8QXCXRTLR < 跳过,因为和发送者地址相同
我们在来考虑另外一种情况,如果 vout
数组的倒数第二个元素也和发送者相同,那么倒数第二个元素也会是接收者
sender = 18DmsHjHU6YM2ckFzub4pBneD8QXCXRTLR
vout[0] = payload
vout[1] = 18DmsHjHU6YM2ckFzub4pBneD8QXCXRTLR < to,这个是接收者
vout[2] = 18DmsHjHU6YM2ckFzub4pBneD8QXCXRTLR < 跳过,因为和发送者地址相同
到这里,就把 USDT
里面一笔交易的发送者和接收者介绍清楚了,那么交易里面还有一个关键点就是交易金额了,那这个在比特币的一笔交易里面如何体现?我们继续来看上面比特币交易的第二个输出
OP_RETURN 6f6d6e69000000000000001f00000000aa7812ae
6f6d6e69 00000000 0000001f 00000000aa7812ae
我们来剖析一下后面的 20 个字节
6f6d6e69 => 把十六进制转成 utf8, 得到 omni 这个单词,你可以理解成一个特殊标志吧
00000000 => 对应的交易类型为 Simple Send,一般的 USDT 转账都是这种类型,当然Omni协议本身还定义了其他交易类型,具体可以参考Omni协议[1]文档,这里不再赘述。
0000001f => 对应十进制数字 31,表示 TetherUS 公司发行的资产代币,也就是我们知道的 USDT
其他币种的话,这里的 propertyid 不一样
最后要说的就是金额了,占用 8 个字节,单位和比特币一样,10 的 8 次方
00000000aa7812ae => 对应的十进制 2859995822,相当于 28.59995822 个 USDT
与我们获取到的 amount 字段一致
这里解释完如何通过比特币的交易规则去构造一笔 USDT
的交易,那么上面 valid
也就可以解释了:构造出来的交易是完全符合比特币的交易规则的,它是合法的,那么这笔交易就能被比特币的矿工打包进区块,这笔交易也是会被比特币区块链所承认。
那么问题来了,这笔交易在 USDT
的节点上能被承认吗?USDT
会承认发送者有那么多要发送的币吗?这可不一定,我们继续来看。
https://github.com/OmniLayer/omnicore/blob/v0.3.0/src/omnicore/tx.cpp#L1012
上面 CMPTransaction::logicMath_SimpleSend
这个方法,会是 Sample Send
模式最终会调用的的一个逻辑。通过上面我标注的来看,USDT 其实内部也有自己的一套基于地址的记账模型,通过地址可以获取到这个地址上面的余额,它会把发送者当前的余额和发送者想发送的金额做一个对比,如果发送者的余额小于要发送的的金额,就会报出余额不足的问题,那么这比 USDT
的交易就会因为余额不足而无效,就会给 valid
字段设置成 false
。
总结一下,如果一笔 USDT 的交易合法的,要至少满足以下2个条件:
上面的问题就好解释了,valid => false
的情况,就是这笔交易在比特币上面是合法的,但是在 USDT
上面是不合法的。
除了余额检查,Tether还会检查账号是否被冻结。
缘起于2017年的一次恶意攻击(黑客所为还是监守自盗,众说纷纭)
北京时间2017年11月21日,Tether官网发布公告表示,由于外部攻击者的恶意行为,$30,950,010USDT于2017年11月19日从Tether Treasury钱包中移除。
Tether官方对此事件反应是在事发后34小时冻结该钱包地址。
Tether紧急更新 type_int 为 185 表示冻结钱包
此前交易类型只有包含Simple Send在内的13种
不过,冻结钱包这么重要的操作也只有 Tether Treasury 才能有此权限了。
2018年3月份又冻结了一部分钱包地址
https://omniexplorer.info/address/3MbYQMMmSkC3AgWkj9FMo5LsPTW1zBTwXL/
查看该冻结交易 > 203e932783961d986bccb29deed2b4084fa6fb9d1e3cda8c58c705f2a5fb4c68
所有这些地址冻结后的任何交易信息都不能接受。
作为交易平台来讲,如果在处理用户充币的时候没有判断这个valid字段,那么就会产生“虚拟充值”,导致平台方损失。
实验室的同事们扫描了最近Bitcoin区块中的USDT交易数据,跟踪到一个疑似恶意攻击者的钱包地址:
16k5MgZHm2yxiKzrdeaY2vmn13xSSu5xg6
通过查询该钱包地址的交易记录发现该攻击者仍然在持续的构造虚假交易。
该钱包已经列入本实验室的 “恶意钱包地址库”, 同时更多的疑似恶意钱包地址被发现,详细恶意钱包地址库请联系2345新科技研究院 。
国内外的交易所如雨后春笋,但是代码在上线前可能并没有做过专业审计,尤其是某些开发人员素质参差不齐或者一时大意会埋下很大的隐患。
版权所有©2345.com 沪ICP备12023051号-3