在做CTF比赛中,我们经常会碰到出题方为了加大题目难度而不给出合约源码的智能合约赛题。通常在借助工具做了静态分析后,有了解题思路,但是发现不知该如何与合约进行交互。因为自己之前在做题时遇到过这个问题,在网上找了很久,包括官方接口、别人的js脚本等,总是无法执行成功。还好最终解决了问题,然后趁最近不忙,总结一下。
当时是在打RoarCTF,第二题,题目给了源码(后来发现是部分)和合约地址:https://ropsten.etherscan.io/address/0x8d73365bb00a9a1a06100fdfdc22fd8a61cfff93#code
因为第一次做比赛题,以为附件给出的就是全部源码,疑惑了半天。具体解题思路这里不细说了,有兴趣的可以见我的博客RoarCTF智能合约writeup。这里用这个这个题目为背景,来演示如何和源码的合约交互。
大致描述一下题目:
读附件源码,调用CaptureTheFlag,必须满足两个条件:
takeRecord[msg.sender] == true;
balances[msg.sender] == 0
function CaptureTheFlag(string b64email) public returns(bool){
require (takeRecord[msg.sender] == true);
require (balances[msg.sender] == 0);
emit FLAG(b64email, "Congratulations to capture the flag!");
}
第一个条件有两个地方可以得到满足:构造函数HoneyLock()和takeMoney(),显然是要调takeMoney(),但是调用该函数后会得到一个空投,balances[owner] = airDrop;
,从而无法满足上述的条件2,那么就要找找怎么把账户的余额转出去。
中间尝试了一些解决方法没有成功,最后用在线反编译工具恢复合约源码Online Solidity Decompiler (反编译结果很长,不贴了,后面会贴具体的函数)。发现原来给的不是全部源码呀。然后反编译出来的代码一个无法反编译出函数名的函数,和transferFrom()有点类似,但是多了一个参数,这个参数对该转账加了一个限制条件:
//https://ropsten.etherscan.io反编译的伪代码
function 5ad0ae39() public {
require((_arg2 <= allowance[_arg0]));
require(((storage[2] + msg.sender) == _arg3));
balanceOf[_arg0] -= _arg2;
balanceOf[_arg1] = (balanceOf[_arg1] + _arg2);
allowance[_arg0] -= _arg2;
return 1;
}
限制条件就是storage[2] + msg.sender == _arg3,storage[2]的内容对应十进制是53231323(出题人莫不是在玩吉他的时候想出的题?),也就是说第四个参数是msg.sende加上53231323。
调用5ad0ae39() 之前需要调用approve(),把账户A的余额委托给B,然后由B调用5ad0ae39()将A的钱转给C,简单一点,就将账户A余额委托给A,然后A调用5ad0ae39()将钱转给地址0x00:
因此5ad0ae39()包含了四个参数,第一个:msg.sender,第二个:0x00,第三个:53231323转16进制,第四个:msg.sender+53231323,构造参数:
0x5ad0ae39
000000000000000000000000967f8ac6502ecba2635d9e4eea2f65ad4940b1b1
0000000000000000000000000000000000000000000000000000000000000000
00000000000000000000000000000000000000000000000000000000000003e8
000000000000000000000000967f8ac6502ecba2635d9e4eea2f65ad4c6cf08c
这里就引出了我们今天要说了问题,如何在没有源码的情况下与合约交互!
由上面的分析我们知道最终要调用approve()和5ad0ae39(),这两个还是有区别的,对于approve(),即便没有给出“伪合约”,我们也可以根据反编译工具获取他的名字和参数.
function approve(var arg0, var arg1) returns (var r0) {
var temp0 = arg1;
memory[0x00:0x20] = msg.sender;
memory[0x20:0x40] = 0x03;
var temp1 = keccak256(memory[0x00:0x40]);
var temp2 = arg0;
memory[0x00:0x20] = temp2 & 0xffffffffffffffffffffffffffffffffffffffff;
memory[0x20:0x40] = temp1;
storage[keccak256(memory[0x00:0x40])] = temp0;
var temp3 = memory[0x40:0x60];
memory[temp3:temp3 + 0x20] = temp0;
var temp4 = memory[0x40:0x60];
log(memory[temp4:temp4 + (temp3 + 0x20) - temp4], [0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925, msg.sender, stack[-2] & 0xffffffffffffffffffffffffffffffffffffffff]);
return 0x01;
}
我们可以知道approve的函数名和参数类型,就可以通过remix来写“伪合约”进行调用了,操作如下图所示,在部署合约的时候要指定At Address
为题目所给的合约地址,然后正常调用就好了。这里提一下这个网站,可以根据函数签名查原函数名,但是因为是和彩虹表类似,所以不是所有的都能还原出来的,而且有些时候可能会出现碰撞,一个签名会还原出2个或以上的原函数。
总之,对于我们能够获知要调用的函数的函数名和参数类型的时候,我们靠remix就可以完成交互,最方便的方式。
然后我们看5ad0ae39()函数,他没有还原出函数名,如何进行交互呢?其实我在做题的时候尝试过用web3.sha3("transferFrom(xxx)")
来碰撞的,换了好几组参数类型都没有碰出来,有点傻