C++逆向之容器vector篇入门(本文首发于安全客)
前言:说实话,我自己也不会c++的逆向。然后,现在太多的题目是c++的逆向了,一上来就是一堆容器,搞得我不得不去补补c++逆向部分的知识了,我这篇文章以西湖论剑的easyCpp为例,希望能给那些跟我一样是c++逆向的新手的朋友们一点启发。下面我就开始我的抛砖引玉篇幅吧,在这篇文章里,我会以题目中出现的逆向出来的代码以及C++的代码进行对比,让你们更好的知道,c++容器入门篇其实不难,开始正文:
我将先给你们介绍每个容器操作的代码以及ida反汇编出来的代码进行对比
vector的构造以及析构
1 |
|
这是C++代码,接下来是ida F5出现的代码
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
小结:
- 从代码里可以看出,在ida的识别世界里,他会先创建一个临时变量,然后将他的地址传到vector的构造函数里
- 而不同的vector构造函数,只是参数不同,第二个为初始容量,第三个为初始数值的地址,第四个为allocator用于分配内存
- 可以看出构造函数和析构函数是同时存在的
- 要学会简化所识别出来的C++代码,括号里的模板类可以不仔细看,只需要看他具体是什么函数就行
重点:
v3 = std::vector<int,std::allocator
v4 = std::vector<int,std::allocator
这两句要会识别,这是常用的,他是取容器的begin和end,相当于C++的v14.begin();v14.end();
vector的常用操作识别
先进行vector操作知识的复习
vector对象最重要的几种操作
- v.push_back(t) 在容器的最后添加一个值为t的数据,容器的size变大。
- v.size() 返回容器中数据的个数,size返回相应vector类定义的size_type的值。
- v.empty() 判断vector是否为空
- v[n] 或 v.at(n) 返回v中位置为n的元素,后者更加安全
- v.insert(pointer,number, content) 向v中pointer指向的位置插入number个content的内容。
还有v. insert(pointer, content),v.insert(pointer,a[2],a[4])将a[2]到a[4]三个元素插入。 - v.pop_back() 删除容器的末元素,并不返回该元素。
- v.erase(pointer1,pointer2) 删除pointer1到pointer2中间(包括pointer1所指)的元素。
vector中删除一个元素后,此位置以后的元素都需要往前移动一个位置,虽然当前迭代器位置没有自动加1,
但是由于后续元素的顺次前移,也就相当于迭代器的自动指向下一个位置一样。 - v1==v2 判断v1与v2是否相等。
- !=、<、<=、>、>= 保持这些操作符惯有含义。
- vector
::iterator p=v1.begin( ); p初始值指向v1的第一个元素。*p取所指向元素的值。
对于const vector只能用vector ::const_iterator类型的指针访问。 - p=v1.end( ); p指向v1的最后一个元素的下一位置。
- v.clear() 删除容器中的所有元素。
- v.resize(2v.size)或v.resize(2v.size, 99) 将v的容量翻倍(并把新元素的值初始化为99)
1 |
|
以下是ida f5识别出来的代码,我相信很多新手看到这么多代码,已经开始晕了,不要紧,一步步给你分析下,我将每一步用getchar进行分割,方便你们看懂,你们自己调试的时候也可以这么做
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
小结:
- c++ vector的逆向其实不难,最主要你要耐心去看,如果你看多几次,你会发觉这个不难,也就是基本操作而已
- 具体的重要步骤详解,我都在上面注释写的很清楚,一一对应,你可以根据getchar一个个对应去看,看多几遍就知道了
- 要学会简化ida识别的代码,不要盯着模板一直在那看
好了,vector的基本操作完了,接下来拿一道题来实战吧。我相信各位的技术,接下来直接上代码你们也是可以看懂了,看不懂就往上面翻一翻,查下基本操作
西湖论剑之EasyCpp
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
我的注释部分只写到了获得正确flag的过程部分,也就是前半部分,后面部分其实也不难,你们可以作为练习分析下,
下面根据这里面的难点和重点进行具体分析,整个过程最难的部分就是Add和accumulate,这两部分是重点,如果不理解这两部分是无法得到正确的flag的
我先对Add附近的进行分析
1 | v9 = __gnu_cxx::__normal_iterator<int *,std::vector<int,std::allocator<int>>>::operator+(&v29, 1LL);// 传v29的地址,在里面在用指针,相当于传了v24.begin(),这个操作过后就是相当于v9 = v24.begin() + 1; |
这句我原来以为是将v29+1,后面才发觉这是取容器v29的第一个元素,如果这里看不懂的话,可以跟进去看看,双击这行
1 | __int64 __fastcall __gnu_cxx::__normal_iterator<int *,std::vector<int,std::allocator<int>>>::operator+(_QWORD *v24, __int64 num) |
进行的是这个,他传入的是num,作为偏移,他取出来的是容器的第1个元素,我以下标为0为第一个元素,以后不在赘述
4*num + *24 这种写法很常见,在ida6.8尤其显著
他将int数组识别为char数组,取值的时候通常也是这样取,假设有个int数组 int num[5] = {1,2,3,4,5}; 在ida6.8里他识别为char num[20]; 取值的时候就num[4*i],i是循环里的循环变量
接下来是Add部分
1 | __int64 __fastcall Add(__int64 &num[1], __int64 &num[n], __int64 p, __int64 &num[0]) |
具体注释我也写好了,匿名函数你在外部看不出什么,然后你双击进去后就能看出他是干什么了,这里就相当于
1 | for(int i=1 ; i< num.Length; i++) |
accumulate
这部分对我来说可能最难理解的吧,他有好多层,我一层层进去后,最后才理解他是如何将容器进行倒置的
1 | __int64 __fastcall std::accumulate<__gnu_cxx::__normal_iterator<int *,std::vector<int,std::allocator<int>>>,std::vector<int,std::allocator<int>>,main::{lambda(std::vector<int,std::allocator<int>>,int)#2}>(__int64 a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6, char a7) |
先告诉你们我调试出来的结果吧,这部分让我自己看这ida代码,我看了好久,都没看懂他在干嘛,应该还是太菜了,所以我用gdb调试了一波,发觉他是每次取出一个元素,假设第一个元素,取出,第二个元素取出的时候,将他作为容器,将第一组的元素一个个push入栈达到逆置,然后保存这个容器,在取出一个元素,在创建一个只含这个元素的容器,将其作为主容器,将上次保存的容器的每个元素一个个进行push_back();然后循环一直下去就可以达到逆置的效果
举个例子说明吧: 假设元素为 1 2 3 4 5 6 7 8 9 10
第一次:创建一个只含1的容器(1),其余什么都不做
第二次:创建一个只含2的容器(2),将第一次创建的容器(1)里的元素,全部push到容器(2)里,保存容器(2)
第三次:创建一个只含3的容器(3),将容器2里的元素全部push到容器3里面
具体观察过程可以在循环里下断点进行观察,或者直接步过这部分,直接得到结果知道,由于我这里是分析文章,所以就进行了具体的分析
1 | __int64 __fastcall std::__copy_move<false,false,std::random_access_iterator_tag>::__copy_m<int *,std::back_insert_iterator<std::vector<int,std::allocator<int>>>>(__int64 a1, __int64 a2, __int64 a3) |
在上一部分我标注的重点里,一直点进去能看到这里的代码,在这里下断,随你用gdb还是ida都可以在这里观察整个过程,
1 | Num Type Disp Enb Address What |
我这里用info b让你看下我下的断点,具体也可以自己进行调试,这样会让你更加理解这部分代码
我这里截了部分图,这是第一次循环的时候得到的结果,他只push了8进去,具体调试地址可以从ida里看,在代码界面右键Copy to assembly,在右键
可以得到如下图
这里便可以获得具体地址,然后调试部分就不讲了,有时间在写篇gdb如何调试的吧,在这题目里需要用的指令有
- x/10wx 显示的如第一张图所显示的一样
- n 下一步
- s 步进,也就是步进函数内部
- c 继续
- start 在开始处下断点
具体的话:
这道题就是输入的第2-16个元素依次加上第一个元素,然后倒序排列,等于斐波那契数列就得出flag了,所以,反推之就是斐波那契数列倒序排列,在2-16个元素减去第一个元素就完美了,贴上代码
1 | #! /usr/bin/python |
运行截图
把这段复制到linux上运行即可得到flag,或者直接逆向也得到了
我的这篇文章文字不多,大部分文字都在代码里写注释了,因为这篇文章针对的就是如何分析C++的vector的反汇编代码,具体多余的文字赘述我也就没写了
总结下:
- 在ida的f5插件识别出来的不会是你理想的c++代码,比如v24.begin(); 他会变成std::vector<int,std::allocator
>::end(&v24); - 在ida的f5插件识别出来的代码下,不清楚的部分可以跟进去,看看具体是什么操作
- 需要了解常见的vector容器的基本操作,在自己遇到的时候可以快速识别,不需要步进了解具体过程
- 在不了解具体过程的情况下,可以进行动态调试,方便自己理解
好了,就说这么多了,我这篇图贴的不多,大部分都是代码,似乎都是代码,希望大佬们不要见怪
本文作者:NoOne
本文地址: https://noonegroup.xyz/posts/6e2e265a/
版权声明:转载请注明出处!