从一题看C++逆向与机制

阅读量    46956 | 评论 2   稿费 400

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

最近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师傅友情赞助

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多