零时科技 | 智能合约安全系列文章之反编译篇
零时科技 | 智能合约安全系列文章之反编译篇
前言
近年来,各个大型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()中的内容。
-
总结
本篇主要分享的内容为,通过在线网站反编译智能合约opcode的一种方法,比较适合新手学习,下一篇我们会继续分享逆向智能合约的反汇编手法,希望对读者有所帮助.