这题本来单纯地以为是很简单的题,听家俊师傅讲了一下出题思路才发现他的想法真的比答题人多得多……
main函数里调用了三次get_pwd()这个函数来check输入
get_pwd中接受输入,然后对count自增,调用了Bitcoin对象的一个函数来校验输入
如果熟悉C++逆向的话,一眼就能看出来这是在调用虚函数
因为v2是对象的空间,在C++的对象构造中,开头4个字节指向的是虚函数表
*v2指向的是虚函数表,**v2就是虚函数表的第一个函数了
(图片引自C++对象模型详解https://www.cnblogs.com/tgycoder/p/5426628.html)
做题的时候不是很熟悉C++的模型,以及虚函数反编译的不是很明显,直接动态调试做的
初始状态这个虚函数是init,其中调用了verify,第一次直接返回输入,对应输出列表的需求,要输入0xdeadbeef的小端序表示”efbeadde”
如果纯静态逆向,会继续往下看verify函数的第二、三次校验,但事实上第二次就没有调用init了
我在做的时候因为不熟悉虚函数,所以动态调试直接跟进函数,发现进入了sub_4046D7这个函数,其中的核心函数b58e乍看起来很复杂,但其实通过其中的*24(实际上是*256)、%58,和题目内的信息描述很容易想到比特币地址转换方法–base58
直接进行解密获得bytes类型即可通关(注意最后4字节是sha256的验算字节,不可提交,否则会导致flag的sha256计算错误。因为第二关仅截取19个字符送入,但跟flag有关的sha256却会把所有input全部进行运算,导致最后提示Correct实际上的flag却不对)
话是这么说,直接套来的脚本解密出来其实没看懂,还是自己查资料从加密到解密走了一趟才get到应该是hex格式
第三小关本来以为是脑洞题了,其实是误打误撞做出来的,运气是真的好OTZ
这次虚函数又回到了verify,将Input进行两次sha256然后逆序与结果比较,当时的想法是结合提示语:
If you know what exactly ‘1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa’ is, I believe you must know the correct input corresponding to this secret
查了一下发现这条地址是中本聪在开始比特币时记录的第一个块–创世块
刚开始想到的是根据创世块向区块链后端爆破,某个区块的sha将会满足要求
不过查了一下好像也没什么适合计算的,总不能自己重复一遍挖矿过程吧233
卡了许久,代码中突然发现一个关键点
长度80是个很关键的提示
于是去找了区块链结构解析,发现区块头的长度正好是80个字节
https://webbtc.com/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f.hex
在这里得到了创世块的头部信息,提交即可获得flag
事实上在经过家俊师傅的讲解后,再回头逆才发现这里的memcmp被覆盖到了sub_404A36函数
这个函数中通过异或生成了一个串,然后将输入的字符串与做过两次sha256再逆序的输入进行memcmp
这个两次sha256再逆序的操作,在之前的查资料过程中发现就是比特币的哈希方法,把异或生成的串dump出来去搜索
IDC>auto i;for(i=0;i<80;i++){Message(“%02x”, Byte(0x6d0a00+i));}
000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f4e61
发现是创世块的哈希值,由此倒推出原输入是创世块
比赛的时候从一个长度猜到创世块头部,不得不感叹自己的运气真的是……
最后再分析一下虚函数的覆盖,和家俊师傅挖下的种种坑
首先注意到虚函数表中的第一个函数在初始情况下是Init
逐步跟踪,发现Bitcoin在构造函数中就有玄机
这里跳转到了0x6D0F88处,过去看看
这时是直接一个leave和retn返回了
但是后面有很多不可识别的脏数据,暂且先放着不管,继续往后走
get_pwd函数中就如之前分析的一样,没什么问题
问题在于析构函数里
乍一看好像没什么问题哦,delete释放空间嘛
注意这里的(this+3)指向的就是刚才跳转的0x6D0F88
再点进delete内一看
?!
跟正常调用free的delete完全不一样,左边function列表中也竟然出现了两个同名的函数
另外一个才是调用free的原delete,这个是冒牌的!
这里利用的是IDA的重命名机制–C++编译器为了区分重载函数,会对函数生成一些其他字符来修饰。delete函数被修饰以后的名称是”_ZdaPv”,但是冒牌delete函数的原名是”__ZdaPv”,IDA同样也会将其重命名为delete,导致被忽视
这个delete中将参数指向的空间写为0x90,即NOP的机器码
因此可以将刚才的leave、retn和大量脏数据全部写成NOP,从而使下一次调用构造函数的时候可以执行一些其他代码
而这个机密的函数就是脏数据之后的代码,sub_6D1048
这里的a1是rbp,频繁调用的a1-8就是this指针
可以看到,每次调用都会覆盖一次虚函数
另外当第三次执行的时候会将memcmp重写
整个理透以后这个题目学到的应该是最多的,各种阴险技术,真的很有意思23333
可惜做的时候动态跟过去会忽视掉这里的大量重写,比较可惜