智能合约逆向初探

xiaoxiao2025-04-12  19

前言

说起以太坊的智能合约,因为区块链上所有的数据都是公开透明的,所以合约的代码也都是公开的。但是其实它公开的都是经过编译的OPCODE,真正的源代码公开与否就得看发布合约的人了。如果要真正的掌握一个合约会干什么,就得从OPCODE逆向成solidity代码。下面进行练手和实战,实战的是今年PHDays安全会议的比赛里的一道题。

在etherscan上看到的合约的代码示例:

工欲善其事,必先利其器

现在网上免费的工具不太多,我会在结尾贴出我知道的其他工具。 在这里我用的是IDA-EVM(半年没更新啦)和ethervm.io的反编译工具:IDA-EVMethervm 如果要查手册:solidity手册 用来查一些EVM的特性OPCODE 用来查OPCODE的特性

练手1

contract Demo {     uint256 private c;     function a() public returns (uint256) { factorial(2); }     function b() public { c++; }     function factorial(uint n) internal returns (uint256) {         if (n <= 1) { return 1; }         return n * factorial(n - 1);     } }

编译后:

6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630dbe671f14604e5780634df7e3d0146076575b600080fd5b348015605957600080fd5b506060608a565b6040518082815260200191505060405180910390f35b348015608157600080fd5b5060886098565b005b60006094600260ab565b5090565b6000808154809291906001019190505550565b600060018211151560be576001905060cd565b60c86001830360ab565b820290505b9190505600a165627a7a7230582016d61ab556bcec17631dad0b32eae60becb475ff83dae1ed39cbecde69e0cec00029

反编译后:

contract Contract {     function main() {         memory[0x40:0x60] = 0x80;         if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }         var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;         if (var0 == 0x0dbe671f) {             // Dispatch table entry for a()             var var1 = msg.value;             if (var1) { revert(memory[0x00:0x00]); }//因为没有payable修饰,不能接收eth             var1 = 0x5f;             var1 = a();             var temp0 = memory[0x40:0x60];//0x80             memory[temp0:temp0 + 0x20] = var1;//[0x80:0xa0]=a()             var temp1 = memory[0x40:0x60];//0x80             return memory[temp1:temp1 + temp0 - temp1 + 0x20];//return [0x80:0xa0] 也就是a()         } else if (var0 == 0x4df7e3d0) {             // Dispatch table entry for b()             var1 = msg.value;             if (var1) { revert(memory[0x00:0x00]); }             var1 = 0x83;             b();             stop();         } else { revert(memory[0x00:0x00]); }     }     function a() returns (var r0) {         var var0 = 0x00;         var var1 = 0x8f;         var var2 = 0x02;         var1 = func_009E(var2);         return var0;     }     function b() {         storage[0x00] = storage[0x00] + 0x01;     }     function func_009E(var arg0) returns (var r0) {         var var0 = 0x00;         if (arg0 > 0x01) {             var var1 = 0xb8;             var var2 = arg0 - 0x01;             var1 = func_009E(var2);             var0 = arg0 * var1;         label_00BD:             return var0;         } else {             var0 = 0x01;             goto label_00BD;         }     } }

第一句if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }是因为EVM里对函数的调用都是取bytes4(keccak256(函数名(参数类型1,参数类型2))传递的,即对函数签名做keccak256哈希后取前4字节。

下一行对msg.data进行 & 0xffffffff就是这个作用,到下一个if语句下面,我们会发现有个// Dispatch table entry for a(),这其实是反编译器自动注释的。在 Ethereum Function Signature Database 这个网站里有对各种函数签名进行keccak256()的数据库,如果在反编译的时候就会自动查询并注释,知道函数签名会大大方便我们的工作。

从 if (var1) { revert(memory[0x00:0x00]); 这个语句里,我们就可以知道函数 a()是没有paypable修饰的。然后转进a(),又转进func_009E(2),这里其实就是一个if语句,递归自己,当参数小于等于1的时候返回1。仔细一想,这有点像阶乘嘛。至于b(),就只有一个storage[0x00]位自加一了。 然后试着写一下,清楚多了:

contract gogogo{     function a() public returns(uint) {         func_009E(2)         return 0     }     function b()public {         storage[0x00] += 0x01;         return 0;//和stop()等价     }     function func_009E( arg0) returns ( r0){         if(arg0<=1) {             return 1;         }         return arg0*func_009E(arg0-1)     } }

练手2

这个是一个有漏洞的合约。

pragma solidity ^0.4.21; contract TokenSaleChallenge {     mapping(address => uint256) public balanceOf;     uint256 constant PRICE_PER_TOKEN = 1 ether;     function TokenSaleChallenge(address _player) public payable {         require(msg.value == 1 ether);     }     function isComplete() public view returns (bool) {         return address(this).balance < 1 ether;     }     function buy(uint256 numTokens) public payable {         require(msg.value == numTokens * PRICE_PER_TOKEN);         balanceOf[msg.sender] += numTokens;     }     function sell(uint256 numTokens) public {         require(balanceOf[msg.sender] >= numTokens);         balanceOf[msg.sender] -= numTokens;         msg.sender.transfer(numTokens * PRICE_PER_TOKEN);     } }

反编译后:

contract Contract {     function main() {         memory[0x40:0x60] = 0x80;         if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }         var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;         if (var0 == 0x70a08231) {             // Dispatch table entry for balanceOf(address)             var var1 = msg.value;             if (var1) { revert(memory[0x00:0x00]); }             var1 = 0x0094;             var var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff;             var2 = balanceOf(var2);             var temp0 = memory[0x40:0x60];             memory[temp0:temp0 + 0x20] = var2;             var temp1 = memory[0x40:0x60];             return memory[temp1:temp1 + temp0 - temp1 + 0x20];         } else if (var0 == 0xb2fa1c9e) {             // Dispatch table entry for isComplete()             var1 = msg.value;             if (var1) { revert(memory[0x00:0x00]); }             var1 = 0x00bb;             var1 = isComplete();             var temp2 = memory[0x40:0x60];             memory[temp2:temp2 + 0x20] = !!var1;             var temp3 = memory[0x40:0x60];             return memory[temp3:temp3 + temp2 - temp3 + 0x20];         } else if (var0 == 0xd96a094a) {             // Dispatch table entry for buy(uint256)             var1 = 0x00da;             var2 = msg.data[0x04:0x24];             buy(var2);             stop();         } else if (var0 == 0xe4849b32) {             // Dispatch table entry for sell(uint256)             var1 = msg.value;             if (var1) { revert(memory[0x00:0x00]); }             var1 = 0x00da;             var2 = msg.data[0x04:0x24];             sell(var2);             stop();         } else { revert(memory[0x00:0x00]); }     }     function balanceOf(var arg0) returns (var arg0) {         memory[0x20:0x40] = 0x00;         memory[0x00:0x20] = arg0;         return storage[keccak256(memory[0x00:0x40])];     }     function isComplete() returns (var r0) { return address(address(this)).balance < 0x0de0b6b3a7640000; }     function buy(var arg0) {         if (arg0 * 0x0de0b6b3a7640000 != msg.value) { revert(memory[0x00:0x00]); }         memory[0x00:0x20] = msg.sender;         memory[0x20:0x40] = 0x00;         var temp0 = keccak256(memory[0x00:0x40]);         storage[temp0] = arg0 + storage[temp0];     }     function sell(var arg0) {         memory[0x00:0x20] = msg.sender;         memory[0x20:0x40] = 0x00;         if (arg0 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); }         var temp0 = msg.sender;         memory[0x00:0x20] = temp0;         memory[0x20:0x40] = 0x00;         var temp1 = keccak256(memory[0x00:0x40]);         var temp2 = arg0;         storage[temp1] = storage[temp1] - temp2;         var temp3 = memory[0x40:0x60];         var temp4 = temp2 * 0x0de0b6b3a7640000;         memory[temp3:temp3 + 0x00] = address(temp0).call.gas(!temp4 * 0x08fc).value(temp4)(memory[temp3:temp3 + 0x00]);         var var0 = !address(temp0).call.gas(!temp4 * 0x08fc).value(temp4)(memory[temp3:temp3 + 0x00]);         if (!var0) { return; }         var temp5 = returndata.length;         memory[0x00:0x00 + temp5] = returndata[0x00:0x00 + temp5];         revert(memory[0x00:0x00 + returndata.length]);     } }

首先看balanceOf(address),这里需要知道映射的储存方式,映射mapping 中的键 k 所对应的值会位于 keccak256(k.p), 其中 . 是连接符。

通过var2在定义的时候& 0xffffffffffffffffffffffffffffffffffffffff可以确定var2是address类型(160位),传入balanceOf()后可以发现返回storage[keccak256(address+0x00)],从+0x00可以看出来这就是最开始就定义的从地址到某个类型(分析其他地方可得出)的映射。 isComplete()非常明了了,直接跳过。 buy(arg0)第一行明显要我们传入 arg0 数量的 ether 进去,这里可以还原成require() 下面的代码把arg0加到上面的映射中。再结合函数名(这次运气好函数名都有)可以猜出这个合约到底要干啥了。

sell()里要注意的是 memory[temp3:temp3 + 0x00] = address(temp0).call.gas(!temp4 * 0x08fc).value(temp4)(memory[temp3:temp3 + 0x00]);这一行包括下面的代码都是代表一个transfer(),因为transfer要处理返回值,所以分开写看起来比较多。这里我们可以看到安全的transfer也是通过.call.value来实现的,只不过对gas做了严格控制,杜绝重入漏洞。 试着写一下:

contract gogogo{     function balanceOf(address add)public returns (){         return storage[keccak256(add+0x00)];         //映射的储存方法         //也就是 mapping(address => uint256) public balanceOf;     }     function isComplete(){         return address(this).balance < 1 ether;     }     function buy( arg0)public {         require(msg.value==arg0*1 ether);         mapping[msg.sender] += arg0;     }     function sell( arg0)public{         require(mapping[msg.sender]>=arg0)         mapping[msg.sender] -=arg0;                 msg.sender.transfer(arg0 * 1 ehter);     } }

最后说一下,这段代码是require(msg.value==arg0*1 ether);有溢出点,可以绕过。

实战

实战的是今年PHDays安全会议的比赛里的一道逆向题The Lock。这道题没有源码(废话)。 做这个题的时候我们要再加上IDA-EVM,更方便分析。 已知信息:解锁这个合约就胜利,函数签名unlock(bytes4 pincode),每次尝试支付0.5 ehter 直接上反编译后的:

contract Contract {     function main() {         memory[0x40:0x60] = 0x60;         if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }         var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff;         if (var0 == 0x6a5e2650) {             // Dispatch table entry for unlocked()             if (msg.value) { revert(memory[0x00:0x00]); }             var var1 = 0x0064;             var var2 = unlocked();             var temp0 = memory[0x40:0x60];             memory[temp0:temp0 + 0x20] = !!var2;             var temp1 = memory[0x40:0x60];             return memory[temp1:temp1 + (temp0 + 0x20) - temp1];         } else if (var0 == 0x75a4e3a0) {             // Dispatch table entry for 0x75a4e3a0 (unknown)             //unlock(bytes4)             var1 = 0x00b3;             var2 = msg.data[0x04:0x24] & ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff;             var1 = func_00DF(var2);             var temp2 = memory[0x40:0x60];             memory[temp2:temp2 + 0x20] = !!var1;             var temp3 = memory[0x40:0x60];             return memory[temp3:temp3 + (temp2 + 0x20) - temp3];         } else { revert(memory[0x00:0x00]); }     }     function unlocked() returns (var r0) { return storage[0x00] & 0xff; }     //unlock(bytes4 )     function func_00DF(var arg0) returns (var r0) {         var var0 = 0x00;         var var1 = var0;         var var2 = 0x00;         var var3 = var2;         var var4 = 0x00;         var var5 = var4;         if (msg.value < 0x06f05b59d3b20000) { revert(memory[0x00:0x00]); }         var3 = 0x00;         if (var3 & 0xff >= 0x04) {         label_01A4:             if (var2 != var1) { return 0x00; }             storage[0x00] = (storage[0x00] & ~0xff) | 0x01;             return 0x01;         } else {         label_0111:             var var6 = arg0;             var var7 = var3 & 0xff;//0x00             if (var7 >= 0x04) { assert(); }             var4 = (byte(var6, var7) * 0x0100000000000000000000000000000000000000000000000000000000000000) / 0x0100000000000000000000000000000000000000000000000000000000000000;             var6 = var4 >= 0x30;             if (!var6) {                 if (!var6) {                 label_0197:                     var3 = var3 + 0x01;                 label_0104:                     if (var3 & 0xff >= 0x04) { goto label_01A4; }                     else { goto label_0111; }                 } else {                 label_0181:                     var temp0 = var4 - 0x30;                     var5 = temp0;                     var2 = var2 + var5 ** 0x04;                     var1 = var1 * 0x0a + var5;                     var3 = var3 + 0x01;                     goto label_0104;                 }             } else if (var4 > 0x39) { goto label_0197; }             else { goto label_0181; }         }     } }

先修正一个错误!反编译后的byte(var6, var7)里两个参数的位置是错误的,byte(var7, var6)应该是这样.这个地方搞了我好久,但是人家也标注了工具是"experimental"性质的嘛。byte()的作用是把栈顶替换成栈顶下面一个元素的第栈顶值个字节的值。byte()的图解

整体看下来目的比较明确,就是要通过func_00DF(var arg0)函数把storage[0x00]改为1,使unlocked()返回1即可。我们先在IDA里看看,发现进入label_104代码段后会进入一个大循环。

开门先判断 var7>=4 ,下面又使var4为输入的第var7个字节。var6判断这个字节是不是大于0x30,可以猜出来出来0x30是0的ASCII码,应该有点关系。发现当var6为1时,会判断var4是否大于0x39,这不就是9的ascii码么.然后我们从label_0197开始看,发现如果不符合要求var3自加一后会继续循环,直到它为4时进入可以改变storage[0x00]的代码段。再看一下label_0181代码段,这里就是把提取出的单字节字符转换成数字后,var2加上它的4次方,var1加上它的10倍。 分析到这里,就可以试着写一下大致逻辑了:

contract gogogo{     uint8 isunLocke;     function unlocked() public {         return isunLocke;     }     function unlock(arg0) payable public{             require(msg.sender.value>=0.5 ether);             for(i = 0; i < 4; i++ ){                 chr = byte(i,arg0)                 if(chr < 0x30 || chr > 0x39  ){                     continue;                 }                 number = chr - 0x30 //字符转数字                 var2 = var2 + number**4                 var1 = var1 + number*10             }         if(var2 != var1){             return         }         isunLocke = 1;         //(0 & ~0xff) | 0x01         //1     } }

可以看出,我们要提供各位4次方之和为它本身的数字。稍稍爆破一下就有1634可以满足

结尾与总结

从逆向的过程来看,要熟知EVM的各种原理,比如各种不同的变量的储存方式等等。看逆向的代码也能帮我们更深入的了解solidity的一些漏洞,比如变量覆盖,很明显我们写的storage变量在编译后全都变成对storage位的操作了,运用不当肯定会造成变量覆盖漏洞嘛。

其他工具:https://github.com/radare/radare2https://github.com/radare/radare2-extrashttps://github.com/trailofbits/ethersplayhttps://github.com/meyer9/ethdasmhttps://github.com/comaeio/porosity

转载请注明原文地址: https://www.6miu.com/read-5028113.html

最新回复(0)