答题蜜罐合约分析

概览:
该蜜罐合约的实现原理并不复杂,但是分析过程依赖较多相关知识基础,才不容易走偏
答题合约简介:
1 合约地址 0xfe255D67Ab85a67b51dd96a4f01f64a98Fdf20A4
2 合约开源,提供管理员方法start 和 普通用户方法try
3 步骤
管理员创建合约
管理员给合约存入一定量的eth
管理员调用start方法,输入问题和答案,如"国庆节是几号?" "10月1号"
此时普通用户调用try方法(要求携带至少1eth),合约校验答案通过后,自动将其中的eth转给第一个给出正确答案的人
但是实际不可能有人能回答到正确答案
管理员在一定时间后,收回合约里的所有eth并开启下一个蜜罐(管理员存入的 + 普通用户数量 * 每次调用try携带的1eth)
答题合约原理:(结论前置)

通过2个合约完成操作,即部署答题合约a后,调用另一个payload合约b, 在合约b的被调用方法里,调用合约a的new方法直接更新答案,导致start函数实际无法更新答案。即合约的答案不是start函数携带的参数,所以普通人无法通过try函数验证,合约所有者只进不出。
并且其中payload合约b里对答题合约a的更新,并不是直接发送给合约a的(虽然调用了合约a的方法,更新了storage的变量值), 所以在区块链浏览器的合约a transactions页面中,不会展示该笔交易,从而有了更强烈的迷惑性

分析思路:
1 先尽可能多的了解背景信息,减少不必要的开发量和分析量
2 提出多种猜测,比如可能是在合约部署时追加额外操作,可能是start函数有问题,可能是try函数有问题, 等等
分析步骤:
1 整体分析:
1 合约本身开源,粗略分析后,从合约代码上看不出什么问题
2 start函数未见问题
3 try函数未见问题
2 本地部署合约,确定是否合约关键逻辑有隐含的,粗略分析没有发现的异常
evm编译器版本:^0.4.25
合约代码:区块链浏览器上经过验证的源代码直接部署
构造函数参数:
需要从创建合约参数里进行截取

合约创建交易参数由3个部分的字节码构成:
init_code
runtime_code
construct_params
从合约源代码可知,构造函数携带的参数是bytes32字节数组,
由于构造函数的参数采用abi.encode进行序列化和对齐,自此我们从部署合约的callData里得到构造参数序列化后的字节流

从整个字节流的最后往前,按64个字符(32字节)为单位往前找,最终截取到最后320个字符即为construct params

在remix中部署合约
编译合约,并在构造参数中填入构造参数,点击deploy
1 start方法有modifier isAmdin, 可以通过2个方法绕过
构造函数的admins数组中添加abi.encode(msg.sender), 将当前地址添加到管理员
或者start方法去掉modifier(建议)

2 修改构造函数,添加payable, 方便在部署合约时直接转账eth, 减少操作步骤

3 本地验证合约功能
1 解析原有合约的start函数参数,并作为参数调用本地remix的start方法

2 调用try方法,参数为上一步解析到的response字符串,发现能正常命中逻辑,顺利走到合约往自己转账的流程
如图,尾号ddC4是管理员,部署合约同时转账10eth, 调用start方法
尾号5cb2是普通的答题用户,调用try方法,成功从合约中获取10eth


3 本地验证结论
本地重现后,发现合约逻辑是通畅的,即正确输入response后能获取合约中的eth
那么就有可能在start函数调用前后,someone通过什么方法更新了合约内部的变量值
4 通过合约storage中的关键字段更新倒推可能存在的关键交易
1 合约本地storage变量
共有3个,question字符串,responseHash, admin映射
其中responseHash是try函数主要对比的字段值,所以关注目标即为slot=1, 查看该插槽值变更的情况


2 重点操作的blockNumber
部署blockNumber : 11136466
start方法的blockNumber 11136481
3 分析判定:
如果合约部署完毕就有值,说明是在启动流程中进行了处理,分析重点应该在合约的构造参数中,比如修改init_code字节码
如果过程中突然有的值,就是可能其他交易隐含的修改了合约里的值
4 代码实现
从11136481到11136466, 通过web3j.ethGetStorageAt(contractAddress, slotId, blockNumber)获取每个block执行完成后,slot1的值
最终发现,在11136478这个block执行完毕后,合约里开始有值。

换句话说,在部署合约后,start函数前,答题合约里的responseHash就有值了。之后管理员调用start方法,已无法更新合约内部的response字段

5 定位block11136478里的对应交易
该块里有150多个交易,每个交易都查看并解析显然没有操作性
目前2个方案可选
1 通过geth节点,恢复到11136477这个块,逐个重放478这个块里的交易,直到答题合约的storage slot1被更新。
主要劣势是部署节点并重放交易较为麻烦
2 遍历478块里的交易,校验keccak256(fromAddress or toAddress)的值是否等于答题合约的3个admin之一
优势就是快,劣势就是如果通过2个以上的嵌套合约调用,无法匹配到任何一个
最终定位到目标交易0xa375511d7ead88c924d3568547bdd238ed39605c90c203e12e85c5ea6885e35e
payload合约地址 0xF4e96E2D3b4E27853B5eaBc5b0ddcc664F2b1eD1
5 隐藏的internalTransaction分析
分析到这里,我们基本明确了该蜜罐合约的实现原理,即在start之前,通过内联交易更新合约里存储的responseHash值,最后导致start方法传入的response参数无效。

该合约未开源,对payload合约进行反编译,得到如下
对关键交易的callData进行解析,得到如下

即该函数实际为 function 0x9278a35a(address p1, uint256 ethValue, bytes callData) public payable, 并最终调用到了答题合约的new方法写入了responseHash值

写在最后:
此类问题一个较为简单的解法是直接在tenderly上查询该合约,其中的internal交易会直接显示出来,后续解析类似
本文的方案实际相对复杂,但也在尝试探讨:假设没有tenderly这个平台,通过基本功,是否可能通过其他方式分析出来该蜜罐合约的异常以及对应的实现原理?以及tenderly这个平台自身是如何实现的对internal tx 在目标合约的交易列表里进行关联和展示
下一步考虑在测试链上重现相关交易,进一步加深理解


ps1 该地址上的蜜罐交易提醒 https://etherscan.io/tx/0xf5a744cf53e87251661dbae13bf243bd22144e6056286206deb1a2bcac010219
ps2 近期类似的蜜罐合约地址,有兴趣的话大家也可以自己练练手 ethereum:0x20c45165e4821773bf712416bd5044416cae1f03

水平有限,如有错误,请帮忙指正