C++ reverse
windows x86 g++编译器生成的代码
以链栈类为例观察C++的反汇编长啥样
链栈类图
mermaid在typora上可以正常显式,但是放到网页上就不知道发生什么事了
代码实现
1 |
|
GCC编译链接
1 | g++ main.cpp -O0 -o main -m32 |
thiscall调用约定
thiscall调用约定
唯一一个不能明确指明的函数修饰,因为thiscall不是关键字。他是C++类成员函数缺省的调用约定。由于成员函数调用还是一个this指针,所以thiscall是专为C++设计的调用方式。
1、参数从右往左入栈
2、如果参数个数确定,this指针通过通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈
3、对参数个数不定的,调用者清理堆栈,否则函数自己清理
main函数反汇编
main函数开端
main遵守cdecl调用约定,只有成员函数才遵守thiscall调用约定
1 | .text:00401530 ; Attributes: bp-based frame fuzzy-sp |
main函数刚开始时,esp指向0077FEDC
,用OD观察这个位置,是主线程的堆栈
主线程的堆栈起于0x77E000,大小是0x2000即8K
由于堆栈倒着生长,因此栈底在0x780000,此时栈顶在0x0077FEDC,方向是0x780000->0x77E000
距离栈底0x780000-0x77FEDC=0x124即292字节
但是这时候主函数才是刚开始啊,也只有主函数的三个参数压栈了啊,怎么就已经使用了292个字节这么大呢?
主函数不是程序的入口点,在主函数执行前还有其他函数要执行,也可能占用线程栈
PE头->NT头->可选头->AddressOfEntryPoint
其RVA是0x14C0
而ImageBase是0x400000
加起来得到入口点的虚拟地址为
0x4014C0
lea ecx, [esp+4]
将要执行此条指令时,esp=0x77FEDC,
此时栈中是啥呢?
1 | 0077FEDC 00401386 ___tmainCRTStartup+226 |
栈顶0077FEDC
存放的是主函数的返回地址,这个地址在___tmainCRTStartu
函数中
1
2
3
4
5
6
7 .text:0040136C mov [esp+90h+lpreserved], eax ; envp
.text:00401370 mov eax, ds:_argv
.text:00401375 mov [esp+90h+dwReason], eax ; argv
.text:00401379 mov eax, ds:_argc
.text:0040137E mov [esp+90h+dwMilliseconds], eax ; argc
.text:00401381 call _main
.text:00401386 mov ecx, ds:_managedapp显然这个返回地址是
call _main
函数压入栈中的
栈顶再往下四个字节,0077FEE0上是1,是main函数的第一个参数int argc
,4个字节
再往下四个字节0077FEE4上是main函数的第二个参数,命令行参数字符串数组const char **argv
的基地址,4个字节
再往下四个字节0077FEE8上市main函数的第三个参数,环境变量字符串数组const char **envp
的基地址,4个字节
esp+4显然就指向main函数的第一个参数int argc
加载有效地址将该参数的地址交给ecx
and esp, 0FFFFFFF0h
esp低4位置零,高28位保持不变,意思是16字节对齐
此举只能导致esp不增,要么esp本来低位有数现在降到0,要么本来esp就是16字节对齐了,不用降为0.
又栈是倒着生长的,因此不用担心此举将会导致新的压栈覆盖三个参数
对齐的目的应该是追求效率
此步执行后的栈帧
1 | 0077FED0 00000002 |
push dword ptr [ecx-4]
ecx在本函数的第一条指令时被置为第一个参数的地址
现在将ecx-4又退到main函数的返回地址
这里又把返回地址压栈
1 | 0077FECC 00401386 ___tmainCRTStarup+226 |
好像把返回值和参数压了两次栈,第一次是调用者___tmainCRTStarup
做的,第二次是main函数做的
为啥要搞重复建设呢?
推测是因为调用约定不同导致的,___tmainCRTStarup
这个函数不是cdecl
调用约定的,而是stdcall
约定的
push ebp
调用者函数的帧指针ebp压栈保存,方便ebp为现在的函数服务
mov ebp, esp
ebp获得当前main函数栈顶指针拷贝
当前栈顶指向二次压栈的返回值地址
push ecx
ecx存放的是第一个参数的地址,现在又把他压栈,相当于这个值前后一共压栈3次
推测这里是保存ecx寄存器值后来再还给他
sub esp, 24h
栈顶下移0x24h个字节,为main函数申请栈帧空间,一次性申请全
到此函数开端完毕
main初始化
call ___main
这个函数进行了一些初始化,它先判断是否已经初始化过了,如果初始化过了则返回
否则记录一下已经初始化过了,然后执行___do_global_ctors
这个函数,推测是对全局位置的对象实例化调用构造函数
奇怪的是,调用函数应该使用call指令,但是____main
中调用___do_global_ctors
使用的是jmp short
跳转指令,使用jmp不会参数压栈,可以认为___do_global_ctors
不需要参数,但是返回地址也没有压栈,___do_global_ctors
执行完毕之后,控制应该交给谁呢?
___main
函数如果jz跳转实现,再loc_40BC70中也是没有返回值的,但是___main
在调用的时候已经把返回到__main
的地址压栈,因此可以推测,___do_global_ctors
相当于___main
的延续,它将会返回到___main
栈帧一开始压入的返回地址
这个__do_global_ctors
干了啥呢?
1 | void __do_global_ctors() |
__CTOR_LIST__
表是一个函数指针表
第零个函数指针__CTOR_LIST__[0]
的值为0xFFFFFFFFh=-1
,这个值总是-1,表征函数指针表的开始,并且填了第0个元素的空,使得真正的函数指针下标从1开始
1 | .text:004CA335 90 90 90 90 90 90 90 90 90 90+ align 10h |
__do_global_dtors
干了啥呢?
推测是遍历了析构函数表,挨个执行每个函数指针
1 | void __cdecl __do_global_dtors() |
这个p_63984
是指向全局析构函数表的指针
1 | .data:004CD004 6C A3 4C 00 _p_63984 dd offset dword_4CA36C ; |
dword_4CA36C
就是__DTOR_LIST_
表的基地址
这个表很长,怎么运作的现在不想操心
现在回到main函数中
lea eax, [ebp+var_10]
将栈中var_10的地址放到eax中,这里var_10作用不是狠清晰,前面都没有提到var_10,
可以猜测一下,主函数下面就开了一个局部的LinkedStack对象,var_10会不会是该对象呢
联系后面调用LinkedStack构造函数,var_10
十有八九是该对象
mov ecx, eax
再转手交给ecx
call __ZN11LinkedStackC1Ev
调用LinkedStack类的构造函数
跟踪一下这个函数
调用LinkedStack实例化一个链栈对象
1 | .text:00422334 ; Attributes: bp-based frame |
ecx->var_C->eax->ecx,兜兜转转还是ecx,这就很乖,为啥要用var_C捯饬?
再看后面的.text:00422343 call __ZN5StackC2Ev ; Stack::Stack(void)
恍然大悟
又要使用当前对象调用父类构造函数了,那么ecx中啃腚还是要存放当前对象
只不过编译器没有优化这件事了
跟踪一下父类构造函数Stack()
1 | .text:00422910 ; Attributes: bp-based frame |
当前对象的栈中地址->ecx->var_4->eax
0->[eax]=当前对象=当前对象的第一个成员
也就是当前对象的第一个双字置0,而Stack对象的第一个成员正好是一个双字的int len
因此这就做了len = 0;
这么一件事
为啥要费六条汇编指令呢?这是调用约定固定的结构
ecx传递的当前对象的栈中地址必须先压入栈中然后eax从栈中获得该对象的地址,
后面使用eax寄存器相对寻址对栈上的对象进行内存读写
这样看直接从ecx交给eax不行吗?不需要压栈中转啊
显然是可以的,但是没有开启编译优化就得这样来
回到LinkedStack()
mov dword ptr [esp], 8 ; size_t
栈顶上放一个8,貌似要调用函数了,但是很奇怪的是,后面要调用的是operator new,它怎么会有参数呢?
还有就是,LinkedStack()构造函数中只有一个句柄head,没有一个int类型的局部变量,为啥现在要再栈上放一个8?
联系后文可知,这个8将作为new开辟空间的大小Size
8临时占用了head句柄的位置,后面开出对象来之后再写回这个位置,节省了空间
LinkedNode就两个成员变量并且都是4个字节,那么一个LinkedNode实例的大小也就是sizeof(LinkedNode)=8
1 | int value; |
因此这里把8临时放到head的位置
call __Znwj ; operator new(uint)
调用new运算符(_Znwj这名字是真tm抽象)
根据源代码的逻辑,此处应该是new一个LinkedNode类实例作为链栈的附加头节点head
这个new干了啥呢?
ebx是被调用者保存寄存器,也就是Znwj要维护其值前后不变
ebx压栈保存后被赋予新值1,然后申请了18h=24字节的栈帧空间
mov eax, [esp+1Ch+arg_0]
这里esp=77fe50,arg_0=4
加起来指向77fe70,栈中这个位置
1 | 0077FE6C 00422354 LinkedStack::LinkedStack(void)+20 |
这个位置是LinkedStack函数局部变量的起始位置
也就是刚才
mov dword ptr [esp], 8 ; size_t
这条指令的目的位置
1 | LinkedStack() : Stack() |
而LinkedStack的局部变量只有一个head
那么这里eax将会是head这个句柄的值,刚才已经被临时置为8表征对象大小,因此现在用ebx保存这个大小,为head的真值让路
再往下到log_4C7F12
中,
上来就把ebx放到栈上作为Size,准备调用malloc,而ebx经过前面的分析可以得知,就是LinkedNode的大小
这是循环的开始,可以看到循环调用了_malloc
函数,推测是,如果堆上申请空间失败则一直重复申请,直到申请成功
怎么判断的?
malloc的返回值用eax承载,如果申请成功则eax承载的是堆上地址,否则eax=0
当eax不为0则jz short loc_4C7F23
跳转失败,执行
1 | .text:004C7F1E add esp, 18h |
函数就返回了,eax承载返回值
当eax为0则jz short loc_4C7F23
后面继续循环
继续循环并没有立刻重新调用malloc函数,而是做了一些手续,具体干了啥呢?
下到loc_4C7F23
中,首先调用了一个无参函数get_new_handler
这个函数就干了一个事mov eax, __ZN12_GLOBAL__N_113__new_handlerE ;
其中__ZN12_GLOBAL__N_113__new_handlerE =0
new_handler
是我们应当人为设置的函数,即使用set_new_handler
设置的函数,作用是在operator
new中,malloc开不出堆空间时,应该执行的函数
显然我们之前并没有使用set_new_handler设置这么一个纠错函数,这种情况下get_new_handler
将返回NULL
显然__ZN12_GLOBAL__N_113__new_handlerE
这个值应该存放的是一个函数地址,我们没有设置set_new_handler
当然这个位置存放的是0,这也就是get_new_handler
返回NULL的原因
说他是一个函数地址,还可以在后文看出,使用get_new_handler
之后eax理论上承载的是函数地址,程序先检查一下eax是否有效,如果有效(即非零)则直接call eax
说明eax中就是函数地址,即__ZN12_GLOBAL__N_113__new_handlerE
理应存放函数地址
如果我们调用set_new_handler
设置过处理函数,则直接跳转loc_4C7F12
重新调用malloc
这可以做一个实验
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using namespace std;
void flag(){
cout<<"flag{dustball}";
exit(0);
}
const int maxn=10000000;
int main()
{
set_new_handler(flag);
while(true){
int *p=new int[maxn];
}
return 0;
}如果堆爆了,开不出来,理应执行flag函数,而主函数中我们丧心病狂地一直索取堆空间,必然会导致堆满
1
2
3 PS C:\Users\86135\Desktop\test> g++ test.cpp -O0 -o test -m32
PS C:\Users\86135\Desktop\test> ./test
flag{dustball}可以看到确实执行了
set_new_handler
设置的flag函数如果把main中的死循环去掉,只索要一个1e7的int数组,显然不会爆堆,此时就不会执行flag函数
至于堆空间有多大呢?
这个在PE头->NT头->附加头->SizeOfHeapReserve
用010editor打开
main.exe
观察这个位置发现给堆预留的空间是100000h=1MB
如果我们没有设置过处理函数,则执行loc_4C7F30
缺省处理过程.该处理过程干了啥呢?
1 | .text:004C7F30 |
这个结构并没有返回到loc_4C7F12重新尝试malloc,
一开始调用了___cxa_allocate_exception
发生了什么事呢?先看结局,
要么左下GE,函数返回了,其效果就相当于malloc
一开始就开出来然后返回了
要么右下BE,还是开不出来,直接调用了terminate()
终止了程序
GE结局有两种达成情况,一个是___cxa_allocate_exception
又尝试了一次malloc,这次开出来了,直接GE
另一种达成清空是,这次又没开出来,两次malloc都没开出来,这时候进入了loc_4C805C
,关键调用了一个__ZN12_GLOBAL__N_14pool8allocateEj_constprop_0
反汇编这个函数看看吧,好家伙都用到了互斥锁,涉及进程安全性了,上网搜一下pool_allocate
吧,说是内存池之类的东西.
这就需要学了CSAPP实现malloc
和STL源码剖析再说了
本次对operator new
的炎鸠就到此位置吧
回到LinkedStack()
函数中
mov ebx, eax
如果能回来,说明new没有寄,那么eax中就是new在堆上开辟的对象的地址
这里eax将堆上对象的地址交给了ebx
mov dword ptr [esp+4], 0
本条指令以及后面的mov dword ptr [esp], 0
参数压栈,马上要调用函数了
mov ecx, ebx
ebx将堆上对象的地址交给ecx,
可是不应该放在栈上head的地方吗?为啥要给ecx呢?
之前我们已经知道,ecx是用来存放当前对象的,其作用也就是this指针
那么后面啃腚要调用一个作用于当前对象的函数
刚才我们从new
的逻辑中只能看到分配了空间,可是这片对空间并没有初始化,而head = new LinkedNode();
这里调用了构造函数.
那么可以推测,马上就要调用LinkedNode
的构造函数了
call __ZN10LinkedNodeC1EiPS_ ; LinkedNode::LinkedNode(int,LinkedNode*)
果然如此,调用了LinkedNode构造函数,他有两个参数,都是4字节类型
并且我们没有在源代码显式地给他传参,而是使用的缺省参数(默认为0)
1 | head = new LinkedNode(); |
这也就解释了刚才压栈两个0是为啥了
跟踪一下LinkedNode()
干了啥
1 | .text:00421FE8 ; Attributes: bp-based frame |
esp-4在栈上申请了4字节空间,然后存放ecx中的对象地址,然后过继给eax
arg_0是左边第一个参数,经过edx中转放到[eax]上,这个寄存器寻址,也就是对象的起始位置,也就是int value
的位置
arg_4,第二个参数,经过edx中转放到[eax+4],也就是对象起始地址偏上4个字节,即第二个成员LinkedNode *next
的地址
retn 8
相当于
1 | add esp, 8h |
即函数尾声
函数的局部变量就一个当前对象地址的拷贝,4个字节,这里为啥要退栈8字节呢?
退栈之前的栈帧状态:
1
2
3
4
5
6
7 LinkedStack的栈帧
...
参数2
参数1
LinkedNode()的栈帧
返回值地址<-ebp
局部变量退栈后
1
2 参数2
参数1显然是合理的,调用者LinkedStack清理参数
也就是说LinkedNode()将堆上LinkedNode对象两个成员都置零
回到LinkedStack中
sub esp, 8
又申请了8字节的空间
eax, [ebp+var_C]
var_C是存放的LinkedStack对象的堆地址
这里将它交给eax,看来是马上对他读写了
[eax+4], ebx
ebx是LinkedNode对象的堆地址,[eax+4]寄存器相对寻址,解引用之后是LinkedStack
对象的第二个成员,即LinkedNode *head
这里就是将head句柄落实了,让他指向了堆上的一片空间
mov ebx, [ebp+var_4]
var_4是函数开端时被调用者保存的上级函数的ebx值
见函数开端
1 | .text:00422334 push ebp |
现在本函数进入尾声了,要归还上级函数的ebx寄存器了,于是从栈里把他弹出来
leave
栈顶指针退回到本函数的帧指针处,帧指针重新指向上级函数的帧底
1 | movq esp, ebp # 使 rsp 和 rbp 指向同一位置,即子栈帧的起始处 |
retn
相当于
1 | add esp, 0h |
函数返回了
从LinkedStack()回到main()
从LinkedStack回来时标绿的部分执行完毕
loc_401557
1 | .text:00401557 loc_401557: |
这里做了一个判断var_14
是否为10
如果var_14>10则跳转loc_40157D
也就是右侧
否则执行左侧循环
左侧循环体中,var_14
每次+1,显然是作为循环变量用的
对应到源代码是
1 | for (int i = 1; i <= 10; ++i) |
这里var_14就是i,判断条件就是10
循环体
1 | .text:0040155F lea eax, [ebp+var_10] |
var_10的地址放到eax中,var_10中存放的是什么,在调用LinkedStack()之前该值被作为唯一的参数传递给LinkedStack,显然是this指针,那么var_10存放的就是LinkedStack对象的首地址
var_14的地址放到edx中,var_14是循环变量,也是每次循环时将要被压入LinkedStack的值
mov [esp], edx
将要压入LinkedStack的值先放到栈顶,作为参数传递
ecx, eax
用ecx承载LinkedStack对象地址,这是调用约定,马上就要调用成员函数了
call __ZN11LinkedStack4pushERKi ; LinkedStack::push(int const&)
该函数的唯一一个参数已经被刚才mov [esp], edx
放到栈顶了
该函数的细节就不需要步入跟踪了,放一张截图,反汇编写的已经很明白了
跳出循环体
当var_14也就是i=11,超过10的时候,跳出了循环
进入loc_40157D
这里面大多数逻辑或者类似逻辑都已经炎鸠过了还差一个cout<<
这个玩意儿
下面炎鸠一下这个怎么实现的
std::cout
源代码
1 | cout << sta.length() << endl; |
反汇编
1 | .text:0040157D lea eax, [ebp+var_10] ;var_10,LinkedStack对象 |
length函数的返回值放在eax中然后放到栈顶,准备参数
然后把__ZSt4cout
的地址放到ecx中,显然作为对象传递
然后调用了__ZNSolsEi,即operator<<函数,打印了length
本函数执行之后,终端上已经打印出10了
可是后来貌似还打印了一些东西
1 | .text:00401594 sub esp, 4 |
又压栈了一个__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
的地址作为参数
这是个啥呢?跟踪它,IDA给出的注释是
1 | std::ostream *__cdecl std::endl<char,std::char_traits<char>>(std::ostream *__os) |
原来是endl,它原来是个函数(函数模板)
1 | std::ostream *__cdecl std::endl<char,std::char_traits<char>>(std::ostream *__os) |
该函数的参数类型为std::ostream *__os
,此os不是操作系统的缩写,而是ostream唯一的一个标准输出对象cout
至于cout,endl,ostream都长啥样,干了啥,现在不做炎鸠,留作后话吧
C++中endl的本质是什么_xiaofei0859的博客-CSDN博客_c++ endl
为啥可以cout.operator<<(endl);
这样调用,endl不是一个函数吗?
显然ostream类中有对operator<<(endl)
的重载函数,咋重载的现在不想了解
总结
调用约定
参数使用栈传递,从右向左压栈,栈顶是最左的参数
this指针使用ecx寄存器传递,成员函数的其他参数使用栈传递
成员函数返回值使用eax传递
成员函数的 调用和普通函数几乎没有区别,就多一个一个ecx传递对象指针
new和构造函数的关系
它俩不存在谁调用谁的关系,调用者函数首先调用operator new函数在堆上申请空间,然后调用者接着调用构造函数初始化这片空间
new的工作细节
在operator new执行之前,调用者会把new应当申请多大空间,写到栈上,这个位置后来还得存放new返回的句柄
new不需要知道开辟空间是为了干什么,只需要一个大小参数
new会调用malloc函数,如果malloc开不出来则尝试调用get_new_handle即用户自定义的处理函数.
如果用户没有定义该函数则走默认的流程,
这个默认的流程还没有全炎鸠明白
构造函数的工作细节
反汇编视角下的构造函数和普通的成员函数没有区别,都是使用ecx表示当前对象地址,或者说new在堆上开出的地址
构造函数通过ecx拿到堆上的一片地址后,构造函数就认为这里就是我要进行初始化的对象,构造函数才不会管这片空间够不够大
因此
1 | class Test{ |
这种代码也是可以通过编译的,但是new只在堆上开了一个int的大小即4字节,显然放不开一个8字节的Test
但是编译器不知道,程序运行的时候也不知道,这就发生了类似数组访问越界的行为.Test.y成员写到堆上的位置是没有申请的空间,下一次申请堆空间就会覆盖掉这个地址
MRCTF2020-EzCPP
main函数
ida给出的反编译伪代码真的是老太太的裹脚--又臭又长
1 | int __cdecl main(int argc, const char **argv, const char **envp) |
其中甚至都有分配器allocator实例的创建,还有析构函数的调用,真的是废话
ida有时候为同一对象创建了好多副本,但是实际上都是只读访问的副本,根本没有必要创建
甚至有的副本创建了根本不访问
为啥ida有时候显得很呆?
他只是刻板地按照堆栈中存在过的局部变量,决定创建或者不创建一个对象,它没法确定后来有没有使用这个对象,或者是否只是只读访问这个对象
main翻译成人话
1 | int main() |
关键点在于三个lambda
和一个depart
第一个lambda已经翻译成人话了,就是一个keys[i]^1
第二个lambda已经翻译成汉字了,"将temp中的一些字符换掉"
具体的更换规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 __int64 __fastcall {lambda(std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>> &)#1}::operator()(__int64 a1, __int64 a2)
{
将a2中的原数->新数
48->79 0->O
49->108 1->l
50->122 2->z
51->69 3->E
52->65 4->A
53->115 5->s
54->71 6->G
55->84 7->T
56->66 8->B
57->113 9->q
32->61 空格->等号=
}
第三个lambda
也翻译成汉字了"temp
和ans[i]
字符串不相同",这个就是判断条件
ans[i]
是程序每次都会自动初始化好的,应该是全局位置的string数组,这个数组的初始化在哪里看呢?
跟踪这个lambda表达式
继续跟踪这个ans数组
发现他在bss段,按下ctrl+x观察交叉引用,发现有一个
__static_initialization_and_destruction_0
函数引用过该数组,追踪该函数,其中有:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 std::ios_base::Init::Init((std::ios_base::Init *)&std::__ioinit);
__cxa_atexit((void (__fastcall *)(void *))&std::ios_base::Init::~Init, &std::__ioinit, &_dso_handle);
std::allocator<char>::allocator(&v3);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(
&ans[abi:cxx11],
"=zqE=z=z=z",
&v3);
std::allocator<char>::~allocator(&v3);
std::allocator<char>::allocator(&v4);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(
(char *)&ans[abi:cxx11] + 32,
"=lzzE",
&v4)
std::allocator<char>::~allocator(&v4);
std::allocator<char>::allocator(&v5);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(
(char *)&ans[abi:cxx11] + 64,
"=ll=T=s=s=E",
&v5);
...相当于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 string ans[9] = {
"=zqE=z=z=z",
"=lzzE",
"=ll=T=s=s=E",
"=zATT",
"=s=s=s=E=E=E",
"=EOll=E",
"=lE=T=E=E=E",
"=EsE=s=z",
"=AT=lE=ll"
};于是就得到了ans数组,后面的工作就是从ans数组开始反回去
depart(v[i], temp);
depart
函数干了啥事呢?将v[i]
分解质因数,然后按照从大到小的顺序,放到temp
字符串里
1 | void depart(int a1, string &s) |
解密脚本
1 |
|
运行结果
1 | PS C:\Users\86135\Desktop\MRCTF2020\EzCPP> ./test |
Linux虚拟机或者wsl上运行EasyCPP然后把keys乎进去
1 | ┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/MRCTF2020/EzCPP] |
32位大写的md5加密:
1 | 4367FB5F42C6E46B2AF79BF409FB84D3 |
因此flag是
1 | MRCTF{4367FB5F42C6E46B2AF79BF409FB84D3} |
交到buuctf上是
1 | flag{4367FB5F42C6E46B2AF79BF409FB84D3} |
胡思乱想
为什么ida会多此一举地创建多个副本呢?
以ida在main函数刚开始时创建的两个vector对象为例
1 | std::vector<int>::vector(v20, argv, envp); |
显然v20和v19是作为句柄用的,两句话实际上相当于
1 | vector<int> v20(argv,envp); |
然而我从来也没有见过传递两个字符串数组指针给vector的构造函数
1
2
3
4
5 vector()://创建一个空vector
vector(int nSize)://创建一个vector,元素个数为nSize
vector(int nSize,const t& t)://创建一个vector,元素个数为nSize,且值均为t
vector(const vector&)://复制构造函数
vector(begin,end)//:复制[begin,end)区间内另一个数组的元素到vector
后面这个vector<bool> v19
更离谱,自从创建了它,后面从来没有使用过,在main函数结尾处调用了它的析构函数
为什么会发生这种诡异的情况呢?只能做一个推测
看一下main函数的开端
1 | push rbp |
本来创建一个vector容器的结构应该是
1 | lea rax, [rbp+var_B0] |
栈上var_B0应该是句柄的位置,通过rax中转将var_B0的地址放到rdi里,rdi用来传递参数给_ZNSt6vectorIiSaIiEEC2Ev
可是这只是交代了vector对象的句柄应该放在哪,并没有指明参数,这意味着只是创建了一个vector
但是ida不认为它没有参数.
他看见前面有两个push压栈,认为是在准备参数,也没管中间还有sub指令导致的栈变化
(因为正常函数调用的时候,准备参数过程中啃腚不会瞎改栈顶指针,可能ida就直接寻找push指令了,根本不管其他指令的事情)
ida认为,在执行call _ZNSt6vectorIiSaIiEEC2Ev
时,刚才压栈的push
rbp和push rbx是保存的调用者main函数的寄存器
一下可能是ida在想什么:
rbp是main函数的栈帧指针,它指向的正是main
的最后一个参数envp
rbx也不知道ida是怎么认为它存放的是argc
数组的
反正它就这样认为的,两个push是在为vector
准备参数
于是F5反编译的时候他就堂而皇之地写了一个
1 | std::vector<int>::vector(v20, argv, envp); |
那为啥第二次vector<bool>
的时候就没有乱写参数呢?
推测因为ida从最近刚调用函数开始计算当前函数的参数是啥,而两次函数调用之间没有压栈操作
1 | call _ZNSt6vectorIiSaIiEEC2Ev ; std::vector<int>::vector(void) |
因此ida认为后来这个vector是无参的
看雪论坛上大佬的解释是
我猜ida有时判断不准要几个参数,有可能是调用者和被调用者的调用约定不同,比如cdecl的main函数调用thiscall的构造函数.
学了堆栈平衡再回来看吧
那为啥有一个vector<bool>
后来却从来不用他?
从反汇编得到的指令来看,确实是有调用vector<bool>
的构造函数的,可能是出题的为了混淆视听吧