最近CTF出现了很多C++逆向题,偶然间看到看雪CTF的一题”半加器”,关于C++全局类对象的机制,拿来练手学习
定位main函数
首先用PEID
查壳,发现是VC++系列编译器写的程序,无壳
接着运行程序,发现有用户交互Please Input:
于是直接在IDA中搜索字符串,根据交叉引用定位到关键代码
另一种方法
如果程序没有给出字符串交互,如何定位main呢?尤其是在本题去除了很多函数信息的情况下
IDA中start
函数连续跟进后,来到这一大段代码
这时还是不知道关键代码在哪,正好我本地有visual studio,在debug下写一个x86程序,并用IDA打开,也跟进到这一处
发现有注释Code
,于是跟进sub_413C10
,发现main函数
同样的,本题的main函数的关键,v4
相当于Code
,跟进v4,发现关键函数sub_4A2890
,也就是main函数
输入部分
又回到了这个Please input:
,我这里就算重新分析,按P也会报sp错误
简单算一下alt+k
修复,然后就可以愉快的F5了
我试着把retn指令直接nop掉,发现也可以F5,但是后面调试的时候会报错
这里if ( v2 <= 30 && v2 >= 10 )
看起来像是对输入的长度进行限制,动态调试验证确实是strlen的结果
接着从sub_4917A4
继续跟进,注意这里第二个参数是30,正好是上面字符串最大长度,一直进到sub_54B010
发现了一堆诡异的代码,不过可以发现common_tcscpy_s
这样的字符串,结合动态调试,发现这就是strcpy系列的函数
也就是说,函数sub_4917A4(dword_5FD088, 30, (int)&unk_5FD068);
是在复制输入的字符串
而sub_490507
跟进后发现,对复制的字符串每个字符都异或了0x1F
唯一找到的要求相等是第八位必须是A
,之后被替换为#
跟到这里,再后面思路就有些断了,只有对输入进行操作,验证部分呢?
验证部分
再次拿出IDA的交叉引用,输入字符串的引用已经全部看到了,那就对复制的字符串查找看看
又有对字符串的异或操作,pause
字符串也提示我们,flag验证或许就在这附近,因为system("pause")常用来防止输入后黑框一闪而过
猜测sub_490CD7
或许是平凡的strcmp
,在if(!xxx)
处下断,用IDA调试一番
得到a1
的地址后,在内存区看到有字符串
像调试gdb一样,把返回值eax设置成0,程序输出ok
,看来我们已经找到了验证逻辑
解题的话,依次把字符串urj}pux<}n{iqyrh
异或0x1CF,得到这一串
jmubojg#bqdvnfmw`
再替换第八位为A
,即是最后的flag jmubojgAbqdvnfmw
做到这里,想用angr试着跑跑,但是有点问题,跑不出来,也没想清楚为什么
一些思考
到这里题目已经做完了,但是作者是如何实现的?main函数中并没有找到验证部分
看了其他人的解析,发现是在类的析构函数中,但应该是全局对象,析构函数发生在main结束之后
在《C++反汇编与逆向分析技术揭秘》中有详细的讨论
局部对象:作用域结束前调用析构函数
堆对象:释放堆空间前调用析构函数
参数对象:退出函数前,调用参数对象的析构函数
返回对象:如无对象引用定义,退出函数后,调用返回对象的析构函数,否则与对象引用的作用域一致
全局对象:main函数退出后调用析构函数
静态对象:main函数退出后调用析构函数
在main函数结束之后,会由exit
函数终止程序,全局对象的析构也在其中,其中有构造代理函数
和析构代理函数
,后者将所有的全局对象析构,因此main函数中无法发现验证过程
此外,突然想到一个实验,看以下代码
#include<iostream>
#include<stdio.h>
using namespace std;
class test {
public:
int num;
test(int num){
this->num = num;
}
~test() {
printf("%d:printf test destructedn",this->num);
cout << "cout test destructed" << endl;
}
};
static test t1(1);
test t2(2);
int main() {
return 0;
}
由于vs2017会在main结束之后一闪而过,因此在ubuntu下编译运行
猜测这可以说明,test对象先于cout对象析构,否则将不会输出cout的一行
为了获得更多的信息,添加了带参数的构造函数
发现全局对象后声明,先析构,而静态对象先声明,后析构
再把声明顺序交换,输出结果正好相反
可以猜测:析构顺序和对象是全局或是静态没有必然的联系?
查资料发现,这种现象并不是语言层面的特性,而是跟编译器的具体实现有关
因此,如果在全局对象或者静态对象析构时,用printf代替cout进行输出
实际场景可能并不多,某些时候debug时可能会用到输出,也算是C++的一个小坑吧
@vct师傅友情赞助
发表评论
您还未登录,请先登录。
登录