分享:我是如何为公司追回价值 8 百万人民币的虚拟货币的

Mar 20 2019 blockchain

几个月之前,钱包部门的同事和我说有一个 ERC20 Token 无法进行转账,转账交易发出去后是交易成功了但是余额没有变化,然后给我发了交易的 txid 让我来看看有没有解决办法。在 etherscan 上查看这个交易状态是成功的,但是并没有发出 ERC20 Transfer 事件,而是一个 RejectedPaymentFromLockedUpWallet 事件。然后我查看了合约代码,不得不说实在太长了,还有一些非英文的注释,发现了 transfer 调用时存在锁定验证,如下所示,transfer 调用时会检查限制地址收付款。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
mapping (address => LockedInfo) public lockedWalletInfo;

struct LockedInfo {
uint timeLockUpEnd;
bool sendLock;
bool receiveLock;
}

event RejectedPaymentToLockedUpWallet (address indexed from, address indexed to, uint256 value);
event RejectedPaymentFromLockedUpWallet (address indexed from, address indexed to, uint256 value);

// token transfer
function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(this));
// 这里验证转出是否已经被锁定
if(lockedWalletInfo[msg.sender].timeLockUpEnd > now && lockedWalletInfo[msg.sender].sendLock == true) {
emit RejectedPaymentFromLockedUpWallet(msg.sender, _to, _value);
return false;
}
// 这里验证收款是否已经被锁定
else if(lockedWalletInfo[_to].timeLockUpEnd > now && lockedWalletInfo[_to].receiveLock == true) {
emit RejectedPaymentToLockedUpWallet(msg.sender, _to, _value);
return false;
}
else {
// 验证通过后可直接调用标准 ERC20 转账
return super.transfer(_to, _value);
}
}

按照 ERC20 的规范,转账失败是不能返回 true 而且不能没有报错的。而这个合约验证锁定失败后直接发出一个调用失败的事件没有报错,这导致同事发现问题时候账户都已经被锁定快一个月了。之前这个合约是被专人审核过的,当时的 checklist 基本都是看其是否有溢出漏洞、权限漏洞等,这样的锁定漏洞没有被重视。

根据上述代码,存在一个 lockedWalletInfo 合约状态,根据名称可以看出来和锁定有一定关系,其可见性是公开的,然后我根据根据被锁定的地址来获取这个状态,返回的结果是收款没有被锁定而转账被锁定了了几千年。之后只能看看锁定调用有没有其它漏洞,代码如下所示,锁定和解锁调用只能被管理员调用。这个情况下技术上几乎是没有解决方式了,只能通过商务谈判。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
event Locked (address indexed target, uint timeLockUpEnd, bool sendLock, bool receiveLock);
event Unlocked (address indexed target);

function walletLock(address _targetWallet, uint _timeLockEnd, bool _sendLock, bool _receiveLock) onlyOwner public {
require(_targetWallet != 0x0);

// 如果设置 _sendLock 和 _receiveLock 则代表解锁
if(_sendLock == false && _receiveLock == false) {
_timeLockEnd = 0;
}

lockedWalletInfo[_targetWallet].timeLockUpEnd = _timeLockEnd;
lockedWalletInfo[_targetWallet].sendLock = _sendLock;
lockedWalletInfo[_targetWallet].receiveLock = _receiveLock;

if(_timeLockEnd > 0) {
emit Locked(_targetWallet, _timeLockEnd, _sendLock, _receiveLock);
} else {
emit Unlocked(_targetWallet);
}
}

不过过了一个晚上,我发现其代码的 transferFrom 和 approve 代码是有漏洞的,如下所示可以看到合约程序员已经偷懒了,估计是复制粘贴了其它合约的代码,这两个地方没有进行任何权限限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mapping (address => mapping (address => uint256)) internal allowed;

// ERC20 标准 transferFrom
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);

balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
emmit Transfer(_from, _to, _value);
return true;
}

// ERC20 标准 approve
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
emit Approval(msg.sender, _spender, _value);
return true;
}

那么这个时候就很简单了,让被锁定的地址调用 approve 给一个新地址所有的 Token 余额,然后新地址调用 transferFrom 就可以进行转账了,就这样被锁定的几亿个 Token 拿回来了,按照当时市价差不多接近八百万人民币。

solidity, ERC20