零时科技 | 智能合约安全系列文章之反编译篇 前言 近年来,各个大型CTF(Capture The Flag,中文一般译作夺旗赛,在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式)比赛中都有了区块链攻防的身影,而且基本都是区块链智能合约攻防。本此系列文章我们也以智能合约攻防为中心,来刨析智能合约攻防的要点,包括合约反编译,CTF常见题型及解题思路,相信会给读者带来不一样的收获。由于CTF比赛中的智能合约源代码没有开源,所以就需要从EVM编译后的opcode进行逆向来得到源代码逻辑,之后根据反编译后的源代码编写攻击合约,最终拿到flag。 基础 本篇我们主要来讲智能合约opcode逆向,推荐的在线工具为Online Solidity Decompiler。该网站逆向的优点比较明显,逆向后会得到合约反编译的伪代码和反汇编的字节码,并且会列出合约的所有函数签名(识别到的函数签名会直接给出,未识别到的会给出UNknown),使用方式为下图: 第一种方式是输入智能合约地址,并选择所在网络 第二钟方式是输入智能合约的opcode 逆向后的合约结果有两个,一种是反编译后的伪代码(偏向于逻辑代码,比较好理解),如下图 另一种是反汇编后的字节码(需要学习字节码相关知识,不容易理解)。 本次演示使用的工具有: Remix(在线编辑器):https://remix.ethereum.org/ Metamask(谷歌插件):https://metamask.io/ Online Solidity Decompiler(逆向网站):https://ethervm.io/decompile/ 案例一 先来看一份简单的合约反编译,合约代码如下: pragma?solidity?^0.4.0; ? ?contract?Data?{ ?????uint?De; ????? ?????function?set(uint?x)?public?{ ?????????De?=?x; ?????} ????? ?????function?get()?public?constant?returns?(uint)?{ ?????????return?De; ?????} ?} 编译后得到的opcode如下: 606060405260a18060106000396000f360606040526000357c01000000000000000000000000000000000000000000000000000000009004806360fe47b11460435780636d4ce63c14605d57603f565b6002565b34600257605b60048080359060200190919050506082565b005b34600257606c60048050506090565b6040518082815260200191505060405180910390f35b806000600050819055505b50565b60006000600050549050609e565b9056 利用在线逆向工具反编译后(相关伪代码的含义已在代码段中详细标注): contract?Contract?{ ?????function?main()?{ ?????????//分配内存空间 ?????????memory[0x40:0x60]?=?0x60;?? ?????????//获取data值?? ?????????var?var0?=?msg.data[0x00:0x20]?/?0x0100000000000000000000000000000000000000000000000000000000;?? ?????????//判断调用是否和set函数签名匹配,如果匹配,就继续执行 ?????????if?(var0?!=?0x60fe47b1)?{?goto?label_0032;?}??? ????? ?????label_0043: ?????????//表示不接受msg.value ?????????if?(msg.value)?{????? ?????????label_0002: ?????????????memory[0x40:0x60]?=?var0; ?????????????//获取data值 ?????????????var0?=?msg.data[0x00:0x20]?/?0x0100000000000000000000000000000000000000000000000000000000;?? ????????????? ?????????????//判断调用是否和set函数签名匹配,如果匹配,就继续执行 ?????????????//?Dispatch?table?entry?for?set(uint256)???? ?????????????//这里可得知set传入的参数类型为uint256??????? ?????????????if?(var0?==?0x60fe47b1)?{?goto?label_0043;?}???? ????????? ?????????label_0032: ????????? ?????????????//判断调用是否和get函数签名匹配,如果匹配,就继续执行 ?????????????if?(var0?!=?0x6d4ce63c)?{?goto?label_0002;?}?? ? ?????????????//表示不接受msg.value???????? ?????????????if?(msg.value)?{?goto?label_0002;?}???? ????????? ?????????????var?var1?=?0x6c; ?????????????//这里调用get函数 ?????????????var1?=?func_0090();???? ?????????????var?temp0?=?memory[0x40:0x60]; ?????????????memory[temp0:temp0?+?0x20]?=?var1; ?????????????var?temp1?=?memory[0x40:0x60]; ?????????????//if语句后有return表示有返回值,前四行代码都是这里的判断条件,这里返回值最终为var1 ?????????????return?memory[temp1:temp1?+?(temp0?+?0x20)?-?temp1];??? ?????????}?else?{ ?????????????var1?=?0x5b; ?????????????//在这里传入的参数 ?????????????var?var2?=?msg.data[0x04:0x24];??? ?????????????//调用get函数中var2参数? ?????????????func_0082(var2);??????? ?????????????stop(); ?????????} ?????} ????? ?????//下面定义了两个函数,也就是网站列出的两个函数签名set和get ?????//这里函数传入一个参数 ?????function?func_0082(var?arg0)?{???? ?????//slot[0]=arg0?函数传进来的参数 ?????????storage[0x00]?=?arg0;??????????????? ?????} ?????//全局变量标记:?EVM将合约中的全局变量存放在一个叫Storage的键值对虚拟空间, ?????//?????????????并且对不同的数据类型有对应的组织方法,存放方式为Storage[keccak256(add,?0x00)]。 ?????//??????storage也可以理解成连续的数组,称为?`slot[]`,每个位置可以存放32字节的数据 ????? ?????//函数未传入参数,但有返回值 ?????function?func_0090()?returns?(var?r0)?{???? ?????//这里比较清楚,将上个函数传入的参数slot[0]的值赋值给var0 ?????????var?var0?=?storage[0x00];???????????? ?????????return?var0;????????????????????????? ?????//最终返回?var0值 ?????} ?} 通过上面的伪代码可以得到两个函数set和get。set函数中,有明显的传参arg0,分析主函数main内容后,可得到该函数不接收以太币,并且传入的参数类型为uint256;get函数中,可明显看出未传入参数,但有返回值,也是不接收以太币,通过storage[0x00]的相关调用可以得到返回值为set函数中传入的参数。最终分析伪代码得到的源码如下: contract?AAA?{ ?????uint256?storage; ????? ?????function?set(uint256?a)?{ ?????????storage?=?a; ?????} ????? ?????function?get()?returns?(uint256?storage)?{ ?????????return?storage; ?????} ?} 相对而言,该合约反编译后的伪代码比较简单,只需要看反编译后的两个函数就可判断出合约逻辑,不过对于逻辑函数较复杂的合约,反编译后的伪代码就需要进一步判断主函数main()中的内容。 案例二 简单入门之后,我们直接来分析一道CTF智能合约的反编译代码 合约地址:https://ropsten.etherscan.io/address/0x93466d15A8706264Aa70edBCb69B7e13394D049f#code 反编译后得到的合约函数签名及方法参数调用如下: 合约伪代码如下(相关伪代码的含义已在代码段中详细标注,标注为重点): contract?Contract?{ ?????function?main()?{ ?????????memory[0x40:0x60]?=?0x80; ????? ?????????//判断函数签名是否为4字节 ?????????//?EVM里对函数的调用都是取`bytes4(keccak256(函数名(参数类型1,参数类型2))`传递的,即对函数签名做keccak256哈希后取前4字节 ?????????if?(msg.data.length?<?0x04)?{?revert(memory[0x00:0x00]);?}?? ? ????????? ?????????//取函数签名,前四个字节(函数签名四个字节表示为0xffffffff类型) ?????????var?var0?=?msg.data[0x00:0x20]?/?0x0100000000000000000000000000000000000000000000000000000000?&?0xffffffff;?? ????????? ?????????if?(var0?==?0x2e1a7d4d)?{ ?????????????//?Dispatch?table?entry?for?withdraw(uint256) ?????????????var?var1?=?msg.value; ????????????? ?????????????//表示不接受?`msg.value` ?????????????if?(var1)?{?revert(memory[0x00:0x00]);?}??? ????????? ?????????????var1?=?0x00be; ?????????????var?var2?=?msg.data[0x04:0x24]; ?????????????withdraw(var2); ?????????????//stop表示该函数无返回值 ?????????????stop();???? ?????????}?else?if?(var0?==?0x66d16cc3)?{ ?????????????//?Dispatch?table?entry?for?profit() ?????????????var1?=?msg.value; ????????? ?????????????if?(var1)?{?revert(memory[0x00:0x00]);?} ????????? ?????????????var1?=?0x00d5; ?????????????profit(); ?????????????stop(); ?????????}?else?if?(var0?==?0x8c0320de)?{ ?????????????//?Dispatch?table?entry?for?payforflag(string,string) ?????????????var1?=?msg.value; ????????? ?????????????if?(var1)?{?revert(memory[0x00:0x00]);?} ????????? ?????????????var1?=?0x0184; ?????????????var?temp0?=?msg.data[0x04:0x24]?+?0x04; ?????????????var?temp1?=?msg.data[temp0:temp0?+?0x20]; ?????????????var?temp2?=?memory[0x40:0x60]; ?????????????memory[0x40:0x60]?=?temp2?+?(temp1?+?0x1f)?/?0x20?*?0x20?+?0x20; ?????????????memory[temp2:temp2?+?0x20]?=?temp1; ?????????????memory[temp2?+?0x20:temp2?+?0x20?+?temp1]?=?msg.data[temp0?+?0x20:temp0?+?0x20?+?temp1]; ?????????????var2?=?temp2; ?????????????var?temp3?=?msg.data[0x24:0x44]?+?0x04; ?????????????var?temp4?=?msg.data[temp3:temp3?+?0x20]; ?????????????var?temp5?=?memory[0x40:0x60]; ?????????????memory[0x40:0x60]?=?temp5?+?(temp4?+?0x1f)?/?0x20?*?0x20?+?0x20; ?????????????memory[temp5:temp5?+?0x20]?=?temp4; ?????????????memory[temp5?+?0x20:temp5?+?0x20?+?temp4]?=?msg.data[temp3?+?0x20:temp3?+?0x20?+?temp4]; ?????????????var?var3?=?temp5; ?????????????payforflag(var2,?var3); ?????????????stop(); ?????????}?else?if?(var0?==?0x9189fec1)?{ ?????????????//?Dispatch?table?entry?for?guess(uint256) ?????????????var1?=?msg.value; ????????? ?????????????if?(var1)?{?revert(memory[0x00:0x00]);?} ????????? ?????????????var1?=?0x01b1; ?????????????var2?=?msg.data[0x04:0x24]; ?????????????guess(var2); ?????????????stop(); ?????????}?else?if?(var0?==?0xa5e9585f)?{ ?????????????//?Dispatch?table?entry?for?xxx(uint256) ?????????????var1?=?msg.value; ????????? ?????????????if?(var1)?{?revert(memory[0x00:0x00]);?} ????????? ?????????????var1?=?0x01de; ?????????????var2?=?msg.data[0x04:0x24]; ?????????????xxx(var2); ?????????????stop(); ?????????}?else?if?(var0?==?0xa9059cbb)?{ ?????????????//?Dispatch?table?entry?for?transfer(address,uint256) ?????????????var1?=?msg.value; ????????? ?????????????if?(var1)?{?revert(memory[0x00:0x00]);?} ????????? ?????????????var1?=?0x022b; ?????????????var2?=?msg.data[0x04:0x24]?&?0xffffffffffffffffffffffffffffffffffffffff; ?????????????var3?=?msg.data[0x24:0x44]; ?????????????transfer(var2,?var3); ?????????????stop();?????????? ?????????}?else?if?(var0?==?0xd41b6db6)?{ ?????????????//?Dispatch?table?entry?for?level(address) ?????????????var1?=?msg.value; ????????? ?????????????if?(var1)?{?revert(memory[0x00:0x00]);?} ????????? ?????????????var1?=?0x026e; ?????????????var2?=?msg.data[0x04:0x24]?&?0xffffffffffffffffffffffffffffffffffffffff; ?????????????var2?=?level(var2); ?????????????var?temp6?=?memory[0x40:0x60]; ?????????????memory[temp6:temp6?+?0x20]?=?var2; ?????????????var?temp7?=?memory[0x40:0x60]; ?????????????//return表示该函数有返回值 ?????????????return?memory[temp7:temp7?+?(temp6?+?0x20)?-?temp7];???? ?????????}?else?if?(var0?==?0xe3d670d7)?{ ?????????????//?Dispatch?table?entry?for?balance(address) ?????????????var1?=?msg.value; ????????? ?????????????if?(var1)?{?revert(memory[0x00:0x00]);?} ????????? ?????????????var1?=?0x02c5; ?????????????var2?=?msg.data[0x04:0x24]?&?0xffffffffffffffffffffffffffffffffffffffff; ?????????????var2?=?balance(var2); ?????????????var?temp8?=?memory[0x40:0x60]; ?????????????memory[temp8:temp8?+?0x20]?=?var2; ?????????????var?temp9?=?memory[0x40:0x60]; ?????????????return?memory[temp9:temp9?+?(temp8?+?0x20)?-?temp9]; ?????????}?else?{?revert(memory[0x00:0x00]);?} ?????} ????? ?????function?withdraw(var?arg0)?{ ?????????//在函数签名处,已给出该函数传参类型为uint256,判断传入的参数arg0是否等于2,如果为2,则继续执行下面代码,否则退出 ?????????if?(arg0?!=?0x02)?{?revert(memory[0x00:0x00]);?}???? ? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//定义这个msg.sender的第一种类型,可通过balance函数判断出,这里为balance ?????????memory[0x20:0x40]?=?0x00;??????????????????????????? ? ????? ?????????//等同于require(arg0?<=?balance[msg.sender]) ?????????if?(arg0?>?storage[keccak256(memory[0x00:0x40])])?{?revert(memory[0x00:0x00]);?}???? ????? ?????????var?temp0?=?arg0;????? ?????????var?temp1?=?memory[0x40:0x60]; ?????????//将主要内容提取出来,可表示为address(msg.sender).call.gas(msg.gas).value(temp0?*?0x5af3107a4000) ?????????memory[temp1:temp1?+?0x00]?=?address(msg.sender).call.gas(msg.gas).value(temp0?*?0x5af3107a4000)(memory[temp1:temp1?+?memory[0x40:0x60]?-?temp1]); ? ?????????memory[0x00:0x20]?=?msg.sender;??????????????? ?????????memory[0x20:0x40]?=?0x00; ?????????var?temp2?=?keccak256(memory[0x00:0x40]);?? ?????????//可写为storage[temp2]?-=?temp0,?由之前代码可知temp0=arg0,由前一句的temp2?=?keccak256(memory[0x00:0x40]);向上推理可得知这里为msg.sender ?????????storage[temp2]?=?storage[temp2]?-?temp0;?????? ? ?????} ????? ?????function?profit()?{ ?????????memory[0x00:0x20]?=?msg.sender; ?????????//定义这个msg.sender为第二种类型,可通过level函数判断出,这里为level ?????????memory[0x20:0x40]?=?0x01;????????????????????? ? ????? ?????????//这里就等同于require(mapping2[msg.sender]?==?0) ?????????if?(storage[keccak256(memory[0x00:0x40])]?!=?0x00)?{?revert(memory[0x00:0x00]);?}??? ? ????? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//启用第一个类型balance进行后续运算 ?????????memory[0x20:0x40]?=?0x00;?????????????????????? ?????????var?temp0?=?keccak256(memory[0x00:0x40]); ?????????//这里进行第一种类型balance的自加一,storage[arg0]?+=?1 ?????????storage[temp0]?=?storage[temp0]?+?0x01;???????? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//启用第二个类型level进行后续运算 ?????????memory[0x20:0x40]?=?0x01;?????????????????????? ?????????var?temp1?=?keccak256(memory[0x00:0x40]); ?????????//这里进行第二种类型level的自加一,storage[0x80]?+=?1?? ?????????storage[temp1]?=?storage[temp1]?+?0x01;???????? ?????} ????? ?????//传入两个string类型的参数 ?????function?payforflag(var?arg0,?var?arg1)?{?????????? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//启用第一个类型balance进行后续运算 ?????????memory[0x20:0x40]?=?0x00;?????????????????????? ????????? ?????????//require(balance[msg.sender]?>=?0x02540be400) ?????????if?(storage[keccak256(memory[0x00:0x40])]?<?0x02540be400)?{?revert(memory[0x00:0x00]);?}?? ????? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//启用第一个类型balance进行后续运算 ?????????memory[0x20:0x40]?=?0x00;?? ?????????//将第一个类型balance赋值为0,等同于balance[msg.sender]?=?0?????????????????? ?????????storage[keccak256(memory[0x00:0x40])]?=?0x00;??? ?????????var?temp0?=?address(address(this)).balance; ?????????var?temp1?=?memory[0x40:0x60];? ?????????var?temp2; ?????????temp2,?memory[temp1:temp1?+?0x00]?=?address(storage[0x02]?&?0xffffffffffffffffffffffffffffffffffffffff).call.gas(!temp0?*?0x08fc).value(temp0)(memory[temp1:temp1?+?memory[0x40:0x60]?-?temp1]); ?????????var?var0?=?!temp2; ??? ?????//传入一个uint256类型的参数 ?????function?guess(var?arg0)?{????????????????????? ?????????if?(arg0?!=?storage[0x03])?{?revert(memory[0x00:0x00]);?}??????????????????????????//判断传入的参数是否和storage[0x03]值匹配, ????? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//启用第二个类型level进行后续运算 ?????????memory[0x20:0x40]?=?0x01;?????????????????? ????? ?????????//判断require(mapping1[msg.sender]?==?1) ?????????if?(storage[keccak256(memory[0x00:0x40])]?!=?0x01)?{?revert(memory[0x00:0x00]);?}??? ????? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//启用第一个类型balance进行后续运算 ?????????memory[0x20:0x40]?=?0x00;?????????????????? ?????????var?temp0?=?keccak256(memory[0x00:0x40]); ?????????//这里进行第一种类型balance的自加一,storage[0x80]?+=?1 ?????????storage[temp0]?=?storage[temp0]?+?0x01;???? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//启用第二个类型level进行后续运算 ?????????memory[0x20:0x40]?=?0x01;?????????????????? ?????????var?temp1?=?keccak256(memory[0x00:0x40]); ?????????//这里进行第二种类型level的自加一,storage[0x80]?+=?1???????? ?????????storage[temp1]?=?storage[temp1]?+?0x01;???? ?????} ????? ?????function?xxx(var?arg0)?{ ?????????//storage[0x02]?&?0xffffffffffffffffffffffffffffffffffffffff?表示storage[0x02]为一个地址类型 ?????????//判断调用者发起人的地址是否为匹配 ?????????if?(msg.sender?!=?storage[0x02]?&?0xffffffffffffffffffffffffffffffffffffffff)?{?revert(memory[0x00:0x00]);?}???? ?????????//将传入的uint256数值赋值给storage[0x03] ?????????storage[0x03]?=?arg0;??????????????????? ?????} ???? ?????//传入两个参数分别为address和uint256 ?????function?transfer(var?arg0,?var?arg1)?{????? ? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//启用第一个类型balance进行后续运算 ?????????memory[0x20:0x40]?=?0x00;??????????????? ????? ?????????//这里为require(balance[msg.sender]?>=?arg1) ?????????if?(storage[keccak256(memory[0x00:0x40])]?<?arg1)?{?revert(memory[0x00:0x00]);?}???? ????? ?????????//判断arg1是否等于2,require(arg1?==?2) ?????????if?(arg1?!=?0x02)?{?revert(memory[0x00:0x00]);?}??? ????? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//启用第二个类型level进行后续运算 ?????????memory[0x20:0x40]?=?0x01;??????????????? ????? ?????????if?(storage[keccak256(memory[0x00:0x40])]?!=?0x02)?{?revert(memory[0x00:0x00]);?}??//判断条件,为require(level[msg.sender]?==?2) ????? ?????????memory[0x00:0x20]?=?msg.sender; ?????????//启用第一个类型balance进行后续运算 ?????????memory[0x20:0x40]?=?0x00;????????? ?????????//赋值操作:balance[msg.sender]?=?0 ?????????storage[keccak256(memory[0x00:0x40])]?=?0x00;?????? ?????????memory[0x00:0x20]?=?arg0?&?0xffffffffffffffffffffffffffffffffffffffff; ?????????//启用第一个类型balance进行后续运算 ?????????memory[0x20:0x40]?=?0x00;?? ?????????//balance[address]?=?arg1??? ?????????storage[keccak256(memory[0x00:0x40])]?=?arg1;?????? ?????} ????? ?????function?level(var?arg0)?returns?(var?arg0)?{ ?????????memory[0x20:0x40]?=?0x01; ?????????memory[0x00:0x20]?=?arg0; ?????????return?storage[keccak256(memory[0x00:0x40])]; ?????} ????? ?????function?balance(var?arg0)?returns?(var?arg0)?{ ?????????memory[0x20:0x40]?=?0x00; ?????????memory[0x00:0x20]?=?arg0; ?????????return?storage[keccak256(memory[0x00:0x40])]; ?????} ?} 通过分析上面经过详细标注的反编译伪代码,我们写出合约源码: contract?babybank?{ ????? ?????address?owner; ?????uint?secret; ? ?????event?sendflag(string?base1,string?base2);? ? ?????constructor()public{ ?????????owner?=?msg.sender; ?????} ? ?????function?payforflag(string?base1,string?base2)?public{ ?????????require(balance[msg.sender]?>=?10000000000); ?????????balance[msg.sender]=0; ?????????owner.transfer(address(this).balance); ?????????emit?sendflag(base1,base2); ?????} ????? ?????modifier?onlyOwner(){ ?????????require(msg.sender?==?owner); ?????????_; ?????} ? ?????function?withdraw(uint256?amount)?public?{ ?????????require(amount?==?2); ?????????require(amount?<=?balance[msg.sender]); ?????????address(msg.sender).call.gas(msg.gas).value(amount?*?0x5af3107a4000)(); ?????????balance[msg.sender]?-=?amount; ?????} ? ?????function?profit()?public?{ ?????????require(level[msg.sender]?==?0); ?????????balance[msg.sender]?+=?1; ?????????level[msg.sender]?+=?1; ?????} ? ?????function?xxx(uint256?number)?public?onlyOwner?{ ?????????secret?=?number; ?????} ? ?????function?guess(uint256?number)?public?{ ?????????require(number?==?secret); ?????????require(level[msg.sender]?==?1); ????????? ?????????balance[msg.sender]?+=?1; ?????????level[msg.sender]?+=?1; ?????} ? ?????function?transfer(address?to,?uint256?amount)?public?{ ?????????require(balance[msg.sender]?>=?amount); ?????????require(amount?==?2); ?????????require(level[msg.sender]?==?2); ? ?????????balance[msg.sender]?=?0; ?????????balance[to]?=?amount; ?????} ?} 该反编译合约中,需要判断分析的点为合约中的逻辑函数和主函数main()的相关判断。逻辑函数(withdraw,profit,payforflag,guess,xxx,transfer)中和主函数main()需要关注的点为: memory[0x20:0x40] = 0x00和memory[0x20:0x40] = 0x01分别代表balance和level if (arg1 != 0x02) { revert(memory[0x00:0x00]); }代表require(arg1 == 2),其他条件判断与此相似 if (msg.sender != storage[0x02] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); } 表示为require(msg.sender == owner) storage[temp1] = storage[temp1] + 0x01;表示为level[msg.sender] += 1; if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); } ?//判断函数签名是否为4字节 var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff; ?//取函数签名,前四个字节(函数签名四个字节表示为0xffffffff类型) ,EVM里对函数的调用都是取bytes4(keccak256(函数名(参数类型1,参数类型2))传递的,即对函数签名做keccak256哈希后取前4字节 if (var1) { revert(memory[0x00:0x00]); } ? //表示不接受 msg.value stop(); ? ?//stop表示该函数无返回值 return memory[temp7:temp7 + (temp6 + 0x20) - temp7]; ? ?//return表示该函数有返回值 总结 本篇主要分享的内容为,通过在线网站反编译智能合约opcode的一种方法,比较适合新手学习,下一篇我们会继续分享逆向智能合约的反汇编手法,希望对读者有所帮助。 —- 编译者/作者:深圳零时科技 玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。 |
零时科技|智能合约安全系列文章之反编译篇
2020-12-16 深圳零时科技 来源:区块链网络
- 上一篇:握手监管交易所上牌12/16行情分析
- 下一篇:薪火言币:门头沟要砸盘了吗别担心
LOADING...
相关阅读:
- Ideachain(ICH)-提供智能和创新的解决方案2020-12-16
- 高师谈币:12.16BTC午间行情简述2020-12-16
- 文森话币:比特币早盘行情分析高位整理顶背离形成直接做空2020-12-16
- 张公解币:12.16BTC早间行情分析2020-12-16
- 开哥说币:12.16早间BTC行情分析2020-12-16