前言
前段时间看了有关智能合约里蜜罐合约的一些资料,感觉还是非常有意思的,这些蜜罐合约的利用点大都很巧妙,目的都是为了诱惑你往合约里送钱,而且目标人群也不是什么小白,恰恰是相关的技术人员反而容易着了他们的道。这里我也想起了早前见到的某个合约,现在再看确实也是个蜜罐合约,下面我们来看看它的利用点。
开端
说起来这份合约当时也是某位师傅分享给我,因为乍看起来问题很大,当时还在开玩笑要不要拿下它把钱转出来
合约的代码很简单,如下
pragma solidity ^0.4.20;
contract GUESS_IT
{
function Play(string _response)
external
payable
{
require(msg.sender == tx.origin);
if(responseHash == keccak256(_response) && msg.value>1 ether)
{
msg.sender.transfer(this.balance);
}
}
string public question;
address questionSender;
bytes32 responseHash;
function StartGame(string _question,string _response)
public
payable
{
if(responseHash==0x0)
{
responseHash = keccak256(_response);
question = _question;
questionSender = msg.sender;
}
}
function StopGame()
public
payable
{
require(msg.sender==questionSender);
msg.sender.transfer(this.balance);
}
function NewQuestion(string _question, bytes32 _responseHash)
public
payable
{
require(msg.sender==questionSender);
question = _question;
responseHash = _responseHash;
}
function() public payable{}
}
乍一看是不是问题多多,实际上也确实是问题多多,要成功地play game需要给出正确的response,经过sha3加密后与responseHash进行比较即可成功提取所有的eth,同时我们又发现在StartGame函数中response直接作为参数传送进来了,我们知道链上的交易都是公开透明的,所以合约的创建者执行这一函数时我们是可以看到他传递的值的,所以我们直接去查看到response的值就可以成功play game了,当时这个合约里还存入了一个eth,相当于发送一个eth过去可以拿到两个eth,想想还是挺刺激的,不过也只能是想想,真的发了你就得哭了
这个合约看起来其实看起来跟一个叫新年礼物的蜜罐合约有点像,404的团队的文章里也有提到以太坊蜜罐智能合约分析,不过实际上利用点还是有些区别。
初看完这个合约,你可能会觉得这个作者是不是个小白,一点都不了解以太坊运行的机制就随便在主链上创建了合约,而且还存入了一个以太币,更是把源码都发布上来让你参观,其实这时候你应该有点感觉到不对劲了,这天上难道还真能掉馅饼么,当时一个以太币也不是个小数目了,不过怎么看也找不到问题所在,不管了,先动手试试再说。
尝试
第一步我们当然要先确定response的值,前面也提到这个可以在调用startgame函数时查看,我们在etherscan上查看该合约的交易记录
第一步创建了合约,那么下一步应该就是startgame了,我们查看该交易的内容
在这里我们可以直接选择将交易的内容解码,这样就可以看到里面包含的这部分信息,所以respose的值就是A snowflakE了,看到这个是不是有点激动,说实话我当时也有点激动,不过现在还是得冷静,后面一个交易是创建者往里面冲了一个ether,再下面竟然是一个老哥发了一个交易把eth给提出来了,不过我看的时候比较早,那时候还没有这笔交易,其实这是合约主人把币给提出来而已,现在看来应该是别人部署来测试的,再看这笔交易的内容
竟然真的是用的前面的response进行提币的,难道这个合约真的可以利用么?其实这里就是创建者的恶趣味了,我们接着往下看。
深入
前面我们已经在交易里看到了response的值,我相信很多人可能已经蠢蠢欲动了,不过为了以防万一我们还是多做点验证工作
我们知道合约里使用storage存储的变量都是可以在链上查到的,所以此处的responseHash是可以读取的,那么我们可以用它来进行验证,按照变量定义的顺序,存储位slot 0存放的是string变量question的长度,slot 1存放的是questionsender地址,slot 2存放的是responseHash,所以我们读取slot 2里的值,然后与前面的response的sha3进行比较
这里因为这个蜜罐已经被废弃了,所以值确实是一样的,然而当时我进行尝试的时候slot 2里存储的并不是这串hash,当时我得到的是下面这串
0x490a2750bb759c739d4e8657ebad54ae2175d222146b95118e76f6c9a6f9bf6a
当时我真的是非常纳闷,这咋就对不上号呢,我又看了其它变量存储的位置
这question的长度倒还对得上号,但是这个sender的地址是怎么冒出来的,按道理不是应该是合约创建者的地址么,前面我们可以看到其地址如下
0xac413e7f9c2a5ed2fde919ce3d1e1e98f8d33a55
而存储里的这串却是个合约地址,这让我很是头疼,后来找到相关资料才知道玄机在于etherscan上可见交易的机制,在etherscan上对于合约与合约之间的消息传送,当msg.value为0时它是不显示的,因为它们被视为合约间的相互调用而不是一笔交易,但这部分的信息可以通过etherchain来查看,现在我们使用它来查看该合约间的调用信息
果然,现在交易信息就多了很多,我们发现在创建合约后的第一步行动并不是来自创建者,而是来自一个合约,而这正是我们前面读取到的sender的地址,这下子就都说得通了,我们来看看这个合约在这次调用里都干了些啥,进入以后我们点击Parity Trace来追踪这两次调用的信息,于是得到了这两部分inputdata
因为这里不像etherscan那样自带decode,所以需要我们手动进行解码,这里我们可以使用abi-decoder工具,这个可以用node.js部署,不过简单点我们直接把abi-decoder.js下载下来就行了,然后我们直接在浏览器里使用
首先导入abi,可以直接在etherscan的源码部分复制,然后将inputdata放入进行解码即可
const abi =[{"constant":false,"inputs":[{"name":"_question","type":"string"},{"name":"_response","type":"string"}],"name":"StartGame","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[{"name":"_question","type":"string"},{"name":"_responseHash","type":"bytes32"}],"name":"NewQuestion","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[],"name":"question","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_response","type":"string"}],"name":"Play","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":false,"inputs":[],"name":"StopGame","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"}];
abiDecoder.addABI(abi);
const input1='0x1f1c827f000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004f5768617420666c696573207768656e206974e280997320626f726e2c206c696573207768656e206974e280997320616c6976652c20616e642072756e73207768656e206974e280997320646561643f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003735a730000000000000000000000000000000000000000000000000000000000';
const input2='0x3e3ee8590000000000000000000000000000000000000000000000000000000000000040490a2750bb759c739d4e8657ebad54ae2175d222146b95118e76f6c9a6f9bf6a000000000000000000000000000000000000000000000000000000000000004f5768617420666c696573207768656e206974e280997320626f726e2c206c696573207768656e206974e280997320616c6976652c20616e642072756e73207768656e206974e280997320646561643f0000000000000000000000000000000000';
结果如下
果然玄机就在这里,真正的responsehash是在这里设置的,该合约首先调用startgame来使自己成为questionSender,然后再设置全新的resonseHash,这里的response也不过是瞎写的,真是很狡诈啊,至于后面的那几个调用感觉就是创建者的恶趣味了,在接下来的几个调用里就是拿他的地址假装调用startgame设置了response值,然而事实上这里responseHash已经不为0,所以是没有响应的,然后就等你上钩了,然后他又使用前面已经成为questionSender的合约将responseHash的值改为了我们在etherscan上交易里看到的response的hash值,接下来他便使用另一钱包发送1 ether来提取合约内的ether,正常来讲这里是多次一举的,因为他直接调用stopgame就可以拿回钱了,而他还费gas取改responseHash,大概是想伪造出一种有人成功拿钱走人的错觉吧。
结语
希望这次的分析能让大家感受到蜜罐合约的趣味性,对于那些对以太坊有一定了解手上又有点币的人来说就得小心了,讲道理第一次见的话是很容易被忽悠到的,毕竟以太坊上神奇的机制这么多,稍不小心可能就会栽了跟头,最好是时刻牢记天上是不会掉馅饼的。