与无源码智能合约交互的三种方式
概述 在做CTF比赛中,我们经常会碰到出题方为了加大题目难度而不给出合约源码的智能合约赛题。通常在借助工具做了静态分析后,有了解题思路,但是发现不知该如何与合约进行交互。因为自己之前在做题时遇到过这个问题,在网上找了很久,包括官方接口、别人的js脚本等,总是无法执行成功。还好最终解决了问题,然后趁最近不忙,总结一下。 题目背景 当时是在打RoarCTF,第二题,题目给了源码(后来发现是部分)和合约
概述
在做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
这里就引出了我们今天要说了问题,如何在没有源码的情况下与合约交互!
方式一 remix调用“伪合约”
由上面的分析我们知道最终要调用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)")来碰撞的,换了好几组参数类型都没有碰出来,有点傻。
方式二 js脚本交互
第二种就是用ethereum提供的web3js接口来进行交互,这里也没有什么深奥的,主要就是要怎么写脚本。但是一开始查了官方提供的接口写,一直交易失败,后来在网上找别人分享脚本,还是交易失败,有一种失败是交易显示执行成功了,但是总是不能被成功打包。后面改改查查终于可以了,代码如下:
let Web3 = require("web3");
let Tx = require('ethereumjs-tx').Transaction;
let abi = require('ethereumjs-abi');
let fromAddress = "0x967f8ac6502ecba2635d9e4eea2f65ad4940b1b1";
let toAddress = "0x8d73365bb00a9a1a06100fdfdc22fd8a61cfff93";
var provider = new Web3.providers.HttpProvider('https://ropsten.infura.io/v3/xxxxxxxxxxx');//可以通过注册https://infura.io/,申请自己的PROJECT ID
var web3 = new Web3(provider);
const privateKey = new Buffer.from('xxxxxxxxxxxxxxxxxxxxxxxxx', 'hex');//可以在metamask中导出账户私钥
async function genRawTx(data){
//获取当前账户的交易数量
let number = web3.utils.toHex(await web3.eth.getTransactionCount(fromAddress));
let rawTx = {
nonce: number,
gasPrice: web3.utils.toHex(web3.utils.toWei('10', 'gwei')),
gasLimit: web3.utils.toHex(3000000), // '0x2dc6c0',
to: toAddress,//接收方地址
from: fromAddress,//发送方地址
value: "0x00",//发送金额,单位为wei
data: data
}
return rawTx
}
async function sendTx(){
var rawTx = await genRawTx("0x5ad0ae39000000000000000000000000967f8ac6502ecba2635d9e4eea2f65ad4940b1b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000967f8ac6502ecba2635d9e4eea2f65ad4c6cf08c");//根据题目要求构造的data
var tx = new Tx(rawTx,{'chain':'ropsten'});
tx.sign(privateKey);
var serializedTx = tx.serialize();
//发送交易
await web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex')).on('receipt', console.log);
}
sendTx();


方式三 控制台交互
第三种方式就是直接利用控制台来执行发起交易的命令,以为浏览器本身就支持web3js,这比自己写脚本方便的多。先把先把data整理一下:函数名+四个参数:
0x5ad0ae39000000000000000000000000967f8ac6502ecba2635d9e4eea2f65ad4940b1b1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003e8000000000000000000000000967f8ac6502ecba2635d9e4eea2f65ad4c6cf08c
然后在console里调用eth.sendTransaction():

用控制台来执行最大的好处就是不用输入手续费,回车之后插件metamask会自己打开,调整交易费,确定就可以了。
到这里就满足takeRecord[msg.sender] == true并且账户余额为0,可以调用CaptureTheFlag方法,坐等flag。
flag:RoarCTF{wm-87fc255216991be9173a59aa8b6845a0}
小结
以上就是三种与源码智能合约交互的方式,总体来说,在可以反编译出函数名和参数类型的情况下,用remix写“伪合约”调用比较方便;如果没有函数名和参数类型的情况下,用控制台执行命令比较方便。
文章没有贴全,麻烦小编重新处理一下