dustland

dustball in dustland

C++ reverse

C++ reverse

windows x86 g++编译器生成的代码

以链栈类为例观察C++的反汇编长啥样

链栈类图

image-20220727214639205

mermaid在typora上可以正常显式,但是放到网页上就不知道发生什么事了

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
#include <iostream>
using namespace std;
class Stack
{
protected:
int len;

public:
Stack()
{
len = 0;
}
void push(const int &x);
void pop();
int top() const;
int length() const
{
return len;
}
bool empty() const
{
return len == 0;
}
};

class LinkedNode
{
protected:
int value;
LinkedNode *next;

public:
LinkedNode(int v = 0, LinkedNode *n = NULL) : value(v), next(n) {}
void setValue(int v = 0)
{
value = v;
}
int getValue() const
{
return value;
}
void setNext(LinkedNode *n = NULL)
{
next = n;
}
LinkedNode *getNext() const
{
return next;
}

friend ostream &operator<<(ostream &os, const LinkedNode &node)
{
os << node.value;
return os;
}
};
class LinkedStack : public Stack
{
protected:
LinkedNode *head;

public:
LinkedStack() : Stack()
{
head = new LinkedNode();
}
void push(const int &x)
{
LinkedNode *node = new LinkedNode(x, head->getNext());
head->setNext(node);//头插
++len;
}
void pop()
{
if (empty())
return;
LinkedNode *p = head->getNext();
head->setNext(p->getNext());
delete p;
--len;
}
int top() const
{
if (empty())
return -1;
return head->getNext()->getValue();
}
};

int main()
{
LinkedStack sta;
for (int i = 1; i <= 10; ++i)
{
sta.push(i);//测试压栈功能
}
cout << sta.length() << endl;
while (!sta.empty())
{
cout << sta.top() << " ";//测试取栈顶功能
sta.pop();//测试退栈功能
}
return 0;
}

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调用约定

image-20220428152115878
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.text:00401530 ; Attributes: bp-based frame fuzzy-sp
.text:00401530
.text:00401530 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401530 public _main
.text:00401530 _main proc near ; CODE XREF: ___tmainCRTStartup+221↑p
.text:00401530
.text:00401530 var_14 = dword ptr -14h
.text:00401530 var_10 = byte ptr -10h
.text:00401530 var_4 = dword ptr -4
.text:00401530 argc = dword ptr 8
.text:00401530 argv = dword ptr 0Ch
.text:00401530 envp = dword ptr 10h
.text:00401530
.text:00401530 lea ecx, [esp+4]
.text:00401534 and esp, 0FFFFFFF0h
.text:00401537 push dword ptr [ecx-4]
.text:0040153A push ebp
.text:0040153B mov ebp, esp
.text:0040153D push ecx
.text:0040153E sub esp, 24h

main函数刚开始时,esp指向0077FEDC,用OD观察这个位置,是主线程的堆栈

image-20220726180155902

主线程的堆栈起于0x77E000,大小是0x2000即8K

由于堆栈倒着生长,因此栈底在0x780000,此时栈顶在0x0077FEDC,方向是0x780000->0x77E000

距离栈底0x780000-0x77FEDC=0x124即292字节

但是这时候主函数才是刚开始啊,也只有主函数的三个参数压栈了啊,怎么就已经使用了292个字节这么大呢?

主函数不是程序的入口点,在主函数执行前还有其他函数要执行,也可能占用线程栈

PE头->NT头->可选头->AddressOfEntryPoint

其RVA是0x14C0

image-20220726170200724

而ImageBase是0x400000

image-20220726170228334

加起来得到入口点的虚拟地址为0x4014C0

lea ecx, [esp+4]

将要执行此条指令时,esp=0x77FEDC,

此时栈中是啥呢?

1
2
3
4
5
0077FEDC  00401386  ___tmainCRTStartup+226
0077FEE0 00000001
0077FEE4 00CA1650 debug034:00CA1650
0077FEE8 00CA22B8 debug034:00CA22B8
...

栈顶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
2
3
4
0077FED0  00000002  
0077FED4 00DB1618 debug035:00DB1618
0077FED8 00DB1670 debug035:00DB1670
0077FEDC 00401386 ___tmainCRTStartup+226
push dword ptr [ecx-4]

ecx在本函数的第一条指令时被置为第一个参数的地址

现在将ecx-4又退到main函数的返回地址

这里又把返回地址压栈

1
2
3
4
5
6
7
8
9
0077FECC  00401386  ___tmainCRTStarup+226
0077FED0 00000002
0077FED4 00DA1618 debug035:00DA16
0077FED8 00DA1670 debug035:00DA16
0077FEDC 00401386 ___tmainCRTStarup+226
0077FEE0 00000004
0077FEE4 00DA1660 debug035:00DA16
0077FEE8 00DA22B8 debug035:00DA22
0077FEEC 00000000

好像把返回值和参数压了两次栈,第一次是调用者___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这个函数,推测是对全局位置的对象实例化调用构造函数

image-20220726190345716

奇怪的是,调用函数应该使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void __do_global_ctors()
{
func_ptr v0; // ebx
func_ptr v1; // eax

v0 = __CTOR_LIST__[0];
if ( __CTOR_LIST__[0] == (func_ptr)-1 )//func_ptr是指针类型,(func_ptr)-1是将-1强制转换为指针类型,需要结合 __CTOR_LIST__[0] 来立即这里的判断条件
{
v1 = 0;
do
{
v0 = v1;//v0指向上一个函数指针
v1 = (func_ptr)((char *)v1 + 1);//v1后移一个单位
}
while ( __CTOR_LIST__[(_DWORD)v1] );//当v1遍历完整个函数指针表时,最后一项为全0,此时while条件不满足,跳出循环
}
for ( ; v0; v0 = (func_ptr)((char *)v0 - 1) )//v0逆序遍历整个函数指针表
__CTOR_LIST__[(_DWORD)v0]();//后面加了小括号意思是当作函数执行了
atexit(__do_global_dtors);//注册函数,当程序正常终止的时候,执行__do_global_dtors函数.
//当程序执行到此时并不会执行执行__do_global_dtors函数,而是当整个exe程序执行完毕才会执行__do_global_dtors函数
//atexit只是起到注册作用
}

__CTOR_LIST__表是一个函数指针表

第零个函数指针__CTOR_LIST__[0]的值为0xFFFFFFFFh=-1,这个值总是-1,表征函数指针表的开始,并且填了第0个元素的空,使得真正的函数指针下标从1开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:004CA335 90 90 90 90 90 90 90 90 90 90+                align 10h
.text:004CA340 public ___CTOR_LIST__
.text:004CA340 ; func_ptr __CTOR_LIST__[]
.text:004CA340 FF FF FF FF ___CTOR_LIST__ dd 0FFFFFFFFh ; DATA XREF: ___do_global_ctors+4↑r
.text:004CA340 ; ___do_global_ctors:loc_40BC18↑r ...
.text:004CA344 3C 16 40 00 dd offset __GLOBAL__sub_I_main
.text:004CA348 00 96 4C 00 dd offset __GLOBAL__sub_I__ZNSt12ctype_bynameIcEC2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEj
.text:004CA34C 90 96 4C 00 dd offset __GLOBAL__sub_I__ZNSt12ctype_bynameIwEC2ERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEj
.text:004CA350 60 9D 4C 00 dd offset __GLOBAL__sub_I__ZNSt12ctype_bynameIcEC2ERKSsj
.text:004CA354 30 9E 4C 00 dd offset __GLOBAL__sub_I__ZNSt12ctype_bynameIwEC2ERKSsj
.text:004CA358 00 9F 4C 00 dd offset __GLOBAL__sub_I__ZN9__gnu_cxx9__freeresEv
.text:004CA35C 50 A2 4C 00 dd offset __GLOBAL__sub_I__ZSt20__throw_system_errori
.text:004CA360 30 A3 4C 00 dd offset _register_frame_ctor
.text:004CA364 00 00 00 00 align 8

__do_global_dtors干了啥呢?

推测是遍历了析构函数表,挨个执行每个函数指针

1
2
3
4
5
6
7
8
9
10
void __cdecl __do_global_dtors()
{
void (*i)(void); // eax

for ( i = *p_63984; i; ++p_63984 )
{
i();
i = p_63984[1];
}
}

这个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.text:00422334 ; Attributes: bp-based frame
.text:00422334
.text:00422334 ; LinkedStack *LinkedStack::LinkedStack(LinkedStack *__hidden this)
.text:00422334 public __ZN11LinkedStackC1Ev
.text:00422334 __ZN11LinkedStackC1Ev proc near
.text:00422334
.text:00422334 var_C= dword ptr -0Ch
.text:00422334 var_4= dword ptr -4
.text:00422334 this= dword ptr 8
.text:00422334
.text:00422334 push ebp
.text:00422335 mov ebp, esp
.text:00422337 push ebx
.text:00422338 sub esp, 24h
.text:0042233B mov [ebp+var_C], ecx
.text:0042233E mov eax, [ebp+var_C]
.text:00422341 mov ecx, eax
.text:00422343 call __ZN5StackC2Ev ; Stack::Stack(void)
.text:00422348 mov dword ptr [esp], 8 ; size_t
.text:0042234F call __Znwj ; operator new(uint)
.text:00422354 mov ebx, eax
.text:00422356 mov dword ptr [esp+4], 0
.text:0042235E mov dword ptr [esp], 0
.text:00422365 mov ecx, ebx
.text:00422367 call __ZN10LinkedNodeC1EiPS_ ; LinkedNode::LinkedNode(int,LinkedNode*)
.text:0042236C sub esp, 8
.text:0042236F mov eax, [ebp+var_C]
.text:00422372 mov [eax+4], ebx
.text:00422375 nop
.text:00422376 mov ebx, [ebp+var_4]
.text:00422379 leave
.text:0042237A retn
.text:0042237A __ZN11LinkedStackC1Ev endp

ecx->var_C->eax->ecx,兜兜转转还是ecx,这就很乖,为啥要用var_C捯饬?

再看后面的.text:00422343 call __ZN5StackC2Ev ; Stack::Stack(void)恍然大悟

又要使用当前对象调用父类构造函数了,那么ecx中啃腚还是要存放当前对象

只不过编译器没有优化这件事了

跟踪一下父类构造函数Stack()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:00422910 ; Attributes: bp-based frame
.text:00422910
.text:00422910 ; Stack *Stack::Stack(Stack *__hidden this)
.text:00422910 public __ZN5StackC2Ev
.text:00422910 __ZN5StackC2Ev proc near
.text:00422910
.text:00422910 var_4= dword ptr -4
.text:00422910 this= dword ptr 8
.text:00422910
.text:00422910 push ebp
.text:00422911 mov ebp, esp
.text:00422913 sub esp, 4
.text:00422916 mov [ebp+var_4], ecx
.text:00422919 mov eax, [ebp+var_4]
.text:0042291C mov dword ptr [eax], 0
.text:00422922 nop
.text:00422923 leave
.text:00422924 retn
.text:00422924 __ZN5StackC2Ev endp

当前对象的栈中地址->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
2
int value;
LinkedNode *next;

因此这里把8临时放到head的位置

call __Znwj ; operator new(uint)

调用new运算符(_Znwj这名字是真tm抽象)

根据源代码的逻辑,此处应该是new一个LinkedNode类实例作为链栈的附加头节点head

这个new干了啥呢?

image-20220726213033815

ebx是被调用者保存寄存器,也就是Znwj要维护其值前后不变

ebx压栈保存后被赋予新值1,然后申请了18h=24字节的栈帧空间

mov eax, [esp+1Ch+arg_0]

这里esp=77fe50,arg_0=4

加起来指向77fe70,栈中这个位置

1
2
0077FE6C  00422354  LinkedStack::LinkedStack(void)+20
0077FE70 00000008

这个位置是LinkedStack函数局部变量的起始位置

也就是刚才mov dword ptr [esp], 8 ; size_t这条指令的目的位置

1
2
3
4
LinkedStack() : Stack()
{
head = new LinkedNode();
}

而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
2
3
.text:004C7F1E add     esp, 18h
.text:004C7F21 pop ebx
.text:004C7F22 retn

函数就返回了,eax承载返回值

当eax为0则jz short loc_4C7F23后面继续循环

继续循环并没有立刻重新调用malloc函数,而是做了一些手续,具体干了啥呢?

image-20220726215953952

下到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
#include <iostream>
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观察这个位置

image-20220726222343978

发现给堆预留的空间是100000h=1MB

如果我们没有设置过处理函数,则执行loc_4C7F30缺省处理过程.该处理过程干了啥呢?

1
2
3
4
5
6
7
8
9
10
.text:004C7F30
.text:004C7F30 loc_4C7F30: ; thrown_size
.text:004C7F30 mov [esp+1Ch+Size], 4
.text:004C7F37 call ___cxa_allocate_exception
.text:004C7F3C mov dword ptr [eax], offset off_4D8C80
.text:004C7F42 mov [esp+1Ch+var_14], offset __ZNSt9bad_allocD1Ev ; void (__cdecl *)(void *)
.text:004C7F4A mov [esp+1Ch+lptinfo], offset __ZTISt9bad_alloc ; lptinfo
.text:004C7F52 mov [esp+1Ch+Size], eax ; void *
.text:004C7F55 call ___cxa_throw
.text:004C7F55 __Znwj endp

这个结构并没有返回到loc_4C7F12重新尝试malloc,

一开始调用了___cxa_allocate_exception

image-20220726223944814

发生了什么事呢?先看结局,

要么左下GE,函数返回了,其效果就相当于malloc一开始就开出来然后返回了

要么右下BE,还是开不出来,直接调用了terminate()终止了程序

GE结局有两种达成情况,一个是___cxa_allocate_exception又尝试了一次malloc,这次开出来了,直接GE

另一种达成清空是,这次又没开出来,两次malloc都没开出来,这时候进入了loc_4C805C,关键调用了一个__ZN12_GLOBAL__N_14pool8allocateEj_constprop_0

反汇编这个函数看看吧,好家伙都用到了互斥锁,涉及进程安全性了,上网搜一下pool_allocate吧,说是内存池之类的东西.

这就需要学了CSAPP实现mallocSTL源码剖析再说了

本次对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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.text:00421FE8 ; Attributes: bp-based frame
.text:00421FE8
.text:00421FE8 ; LinkedNode::LinkedNode(int, LinkedNode*)
.text:00421FE8 public __ZN10LinkedNodeC1EiPS_
.text:00421FE8 __ZN10LinkedNodeC1EiPS_ proc near
.text:00421FE8
.text:00421FE8 var_4= dword ptr -4
.text:00421FE8 arg_0= dword ptr 8
.text:00421FE8 arg_4= dword ptr 0Ch
.text:00421FE8
.text:00421FE8 push ebp
.text:00421FE9 mov ebp, esp
.text:00421FEB sub esp, 4
.text:00421FEE mov [ebp+var_4], ecx
.text:00421FF1 mov eax, [ebp+var_4]
.text:00421FF4 mov edx, [ebp+arg_0]
.text:00421FF7 mov [eax], edx
.text:00421FF9 mov eax, [ebp+var_4]
.text:00421FFC mov edx, [ebp+arg_4]
.text:00421FFF mov [eax+4], edx
.text:00422002 nop
.text:00422003 leave
.text:00422004 retn 8
.text:00422004 __ZN10LinkedNodeC1EiPS_ endp

esp-4在栈上申请了4字节空间,然后存放ecx中的对象地址,然后过继给eax

arg_0是左边第一个参数,经过edx中转放到[eax]上,这个寄存器寻址,也就是对象的起始位置,也就是int value的位置

arg_4,第二个参数,经过edx中转放到[eax+4],也就是对象起始地址偏上4个字节,即第二个成员LinkedNode *next的地址

retn 8相当于

1
2
add esp, 8h
ret

即函数尾声

函数的局部变量就一个当前对象地址的拷贝,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
2
3
.text:00422334 push    ebp
.text:00422335 mov ebp, esp
.text:00422337 push ebx

现在本函数进入尾声了,要归还上级函数的ebx寄存器了,于是从栈里把他弹出来

leave

栈顶指针退回到本函数的帧指针处,帧指针重新指向上级函数的帧底

1
2
movq esp, ebp    # 使 rsp 和 rbp 指向同一位置,即子栈帧的起始处
popq ebp #弹出开端时压栈保存的上级函数帧指针
retn

相当于

1
2
add esp, 0h
ret

函数返回了

从LinkedStack()回到main()

从LinkedStack回来时标绿的部分执行完毕

image-20220727002010882
loc_401557
1
2
3
4
.text:00401557 loc_401557:
.text:00401557 mov eax, [ebp+var_14]
.text:0040155A cmp eax, 0Ah
.text:0040155D jg short loc_40157D

这里做了一个判断var_14是否为10

如果var_14>10则跳转loc_40157D

也就是右侧

image-20220727002224727

否则执行左侧循环

image-20220727002247712

左侧循环体中,var_14每次+1,显然是作为循环变量用的

对应到源代码是

1
2
3
4
for (int i = 1; i <= 10; ++i)
{
sta.push(i);
}

这里var_14就是i,判断条件就是10

循环体
1
2
3
4
5
6
7
8
9
10
.text:0040155F lea     eax, [ebp+var_10]
.text:00401562 lea edx, [ebp+var_14]
.text:00401565 mov [esp], edx ; this
.text:00401568 mov ecx, eax
.text:0040156A call __ZN11LinkedStack4pushERKi ; LinkedStack::push(int const&)
.text:0040156F sub esp, 4
.text:00401572 mov eax, [ebp+var_14]
.text:00401575 add eax, 1
.text:00401578 mov [ebp+var_14], eax
.text:0040157B jmp short loc_401557

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放到栈顶了

该函数的细节就不需要步入跟踪了,放一张截图,反汇编写的已经很明白了

image-20220727003212521
跳出循环体

当var_14也就是i=11,超过10的时候,跳出了循环

进入loc_40157D

image-20220727004848001

这里面大多数逻辑或者类似逻辑都已经炎鸠过了还差一个cout<<这个玩意儿

下面炎鸠一下这个怎么实现的

std::cout

源代码

1
cout << sta.length() << endl;

反汇编

1
2
3
4
5
6
.text:0040157D lea     eax, [ebp+var_10]	;var_10,LinkedStack对象
.text:00401580 mov ecx, eax ;var_10经过eax中转放到ecx,为了遵守调用约定
.text:00401582 call __ZNK5Stack6lengthEv ; Stack::length(void)
.text:00401587 mov [esp], eax
.text:0040158A mov ecx, offset __ZSt4cout ; std::cout
.text:0040158F call __ZNSolsEi ; std::ostream::operator<<(int)

length函数的返回值放在eax中然后放到栈顶,准备参数

然后把__ZSt4cout的地址放到ecx中,显然作为对象传递

然后调用了__ZNSolsEi,即operator<<函数,打印了length

本函数执行之后,终端上已经打印出10了

可是后来貌似还打印了一些东西

1
2
3
4
.text:00401594 sub     esp, 4
.text:00401597 mov dword ptr [esp], offset __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ ; this
.text:0040159E mov ecx, eax
.text:004015A0 call __ZNSolsEPFRSoS_E ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))

又压栈了一个__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_的地址作为参数

这是个啥呢?跟踪它,IDA给出的注释是

1
std::ostream *__cdecl std::endl<char,std::char_traits<char>>(std::ostream *__os)

原来是endl,它原来是个函数(函数模板)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
std::ostream *__cdecl std::endl<char,std::char_traits<char>>(std::ostream *__os)
{
int v1; // eax
_BYTE *v2; // ebx
int v3; // eax
int (__stdcall *v5)(char); // edx
char v6; // [esp+4h] [ebp-18h]

v1 = *(_DWORD *)(*(_DWORD *)__os - 12);
v2 = *(_BYTE **)((char *)__os + v1 + 124);
if ( !v2 )
std::__throw_bad_cast();
if ( v2[28] )
{
v3 = (char)v2[39];
}
else
{
std::ctype<char>::_M_widen_init(*(_DWORD *)((char *)__os + v1 + 124));
v5 = *(int (__stdcall **)(char))(*(_DWORD *)v2 + 24);
v3 = 10;
if ( v5 != std::ctype<char>::do_widen )
v3 = ((char (__thiscall *)(_BYTE *, int))v5)(v2, 10);
}
std::ostream::put((std::ostream *)v3, v6);
return std::ostream::flush(__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
2
3
4
5
6
7
8
9
class Test{
int x;
int y;
};
int main()
{
Test *a=(Test*)new int;
return 0;
}

这种代码也是可以通过编译的,但是new只在堆上开了一个int的大小即4字节,显然放不开一个8字节的Test

但是编译器不知道,程序运行的时候也不知道,这就发生了类似数组访问越界的行为.Test.y成员写到堆上的位置是没有申请的空间,下一次申请堆空间就会覆盖掉这个地址

MRCTF2020-EzCPP

main函数

ida给出的反编译伪代码真的是老太太的裹脚--又臭又长

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rax
__int64 v4; // rbx
__int64 v5; // rax
bool v6; // bl
__int64 v7; // rax
__int64 v8; // rax
__int64 v9; // rax
__int64 v10; // rax
__int64 v11; // rax
__int64 v12; // rax
char v14[40]; // [rsp+0h] [rbp-140h] BYREF
__int64 v15; // [rsp+28h] [rbp-118h] BYREF
__int64 v16; // [rsp+30h] [rbp-110h] BYREF
int v17; // [rsp+3Ch] [rbp-104h] BYREF
char v18[32]; // [rsp+40h] [rbp-100h] BYREF
char v19[48]; // [rsp+60h] [rbp-E0h] BYREF
char v20[31]; // [rsp+90h] [rbp-B0h] BYREF
char v21; // [rsp+AFh] [rbp-91h] BYREF
char v22[47]; // [rsp+B0h] [rbp-90h] BYREF
char v23; // [rsp+DFh] [rbp-61h] BYREF
char v24[36]; // [rsp+E0h] [rbp-60h] BYREF
int v25; // [rsp+104h] [rbp-3Ch]
char *v26; // [rsp+108h] [rbp-38h]
int *v27; // [rsp+110h] [rbp-30h]
_DWORD *v28; // [rsp+118h] [rbp-28h]
int *v29; // [rsp+120h] [rbp-20h]
int i; // [rsp+128h] [rbp-18h]
int v31; // [rsp+12Ch] [rbp-14h]

v31 = 0;
std::vector<int>::vector(v20, argv, envp);
std::vector<bool>::vector(v19);
std::allocator<char>::allocator(&v21);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v18, &unk_500E, &v21);
std::allocator<char>::~allocator(&v21);
v3 = std::operator<<<std::char_traits<char>>(&std::cout, "give me your key!");
std::ostream::operator<<(v3, &std::endl<char,std::char_traits<char>>);
for ( i = 0; i <= 8; ++i )
{
std::istream::operator>>(&std::cin, &keys[i]);
std::__cxx11::to_string((std::__cxx11 *)v22, keys[i]);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=(v18, v22);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v22);
}
v28 = keys;
v29 = keys;
v27 = (int *)&unk_83E4;
while ( v29 != v27 )
{
v17 = *v29;
std::vector<int>::push_back(v20, &v17);
++v29;
}
v4 = std::vector<std::shared_ptr<SQLStorage::AddUpdateTable>>::end(v20);
v5 = fmt::v6::internal::get_container<fmt::v6::internal::buffer<char>>(v20);
std::for_each<__gnu_cxx::__normal_iterator<char *,std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>,boost::unit_test::output::s_replace_chars>(
v5,
v4);
v26 = v20;
v16 = fmt::v6::internal::get_container<fmt::v6::internal::buffer<char>>(v20);
v15 = std::vector<std::shared_ptr<SQLStorage::AddUpdateTable>>::end(v26);
while ( __gnu_cxx::operator!=<spdlog::details::log_msg_buffer const*,std::vector<spdlog::details::log_msg_buffer>>(
(const __gnu_cxx::__normal_iterator<const std::shared_ptr<sio::message>*,std::vector<std::shared_ptr<sio::message>> > *)&v16,
(const __gnu_cxx::__normal_iterator<const std::shared_ptr<sio::message>*,std::vector<std::shared_ptr<sio::message>> > *)&v15) )
{
v25 = *(_DWORD *)__gnu_cxx::__normal_iterator<int *,std::vector<int>>::operator*(&v16);
std::allocator<char>::allocator(&v23);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v14, &unk_500E, &v23);
std::allocator<char>::~allocator(&v23);
depart(v25, (__int64)v14);
{lambda(std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>> &)#1}::operator()(
(__int64)&func,
(__int64)v14);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v24, v14);
v6 = !{lambda(std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>,int)#2}::operator()(
(__int64)&check,
(__int64)v24,
v31);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v24);
if ( v6 )
{
v7 = std::operator<<<std::char_traits<char>>(&std::cout, "Wrong password!");
std::ostream::operator<<(v7, &std::endl<char,std::char_traits<char>>);
system("pause");
exit(0);
}
++v31;
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v14);
__gnu_cxx::__normal_iterator<unsigned int *,std::vector<unsigned int>>::operator++(&v16);
}
v8 = std::operator<<<std::char_traits<char>>(&std::cout, "right!");
std::ostream::operator<<(v8, &std::endl<char,std::char_traits<char>>);
v9 = std::operator<<<std::char_traits<char>>(&std::cout, "flag:MRCTF{md5(");
v10 = std::operator<<<char>(v9, v18);
v11 = std::operator<<<std::char_traits<char>>(v10, ")}");
std::ostream::operator<<(v11, &std::endl<char,std::char_traits<char>>);
v12 = std::operator<<<std::char_traits<char>>(
&std::cout,
"md5()->{32/upper case/put the string into the function and transform into md5 hash}");
std::ostream::operator<<(v12, &std::endl<char,std::char_traits<char>>);
system("pause");
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(v18);
std::vector<bool>::~vector(v19);
std::__cxx1998::vector<double,std::allocator<double>>::~vector((std::vector<std::shared_ptr<sio::message>> *const)v20);
return 0;
}

其中甚至都有分配器allocator实例的创建,还有析构函数的调用,真的是废话

ida有时候为同一对象创建了好多副本,但是实际上都是只读访问的副本,根本没有必要创建

甚至有的副本创建了根本不访问

为啥ida有时候显得很呆?

他只是刻板地按照堆栈中存在过的局部变量,决定创建或者不创建一个对象,它没法确定后来有没有使用这个对象,或者是否只是只读访问这个对象

main翻译成人话

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int main()
{
vector<int> v;
string final;
cout << "give me your key!" << endl;
for (int i = 0; i <= 8; ++i)
{
cin >> keys[i];
final += to_string(keys[i]);
v.push_back(keys[i] ^ 1);//放到向量v里面的是输入与1的按位异或
}
for (int i = 0; i < v.length(); ++i)
{
string temp;
depart(v[i], temp);

将temp中的一些字符换掉

if (temp和ans[i] 字符串不相同)
{
cout << "wrong password!" << endl;
system("pause");
exit(0);
}
}

cout << "right!" << endl;
cout << "flag:MRCTF{md5(" << final << ")}" << endl;
cout << "md5()->{32/upper case/put the string into the function and transform into md5 hash}" << endl;
system("pause");

return 0;
}

关键点在于三个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也翻译成汉字了"tempans[i]字符串不相同",这个就是判断条件

ans[i]是程序每次都会自动初始化好的,应该是全局位置的string数组,

这个数组的初始化在哪里看呢?

跟踪这个lambda表达式

image-20220727215518968

继续跟踪这个ans数组

image-20220727215551499

发现他在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
2
3
4
5
6
7
8
9
10
11
12
void depart(int a1, string &s)
{
for (int i = 2; sqrt(a1) >= i; ++i) // i从2根号a1,遍历求a1的乘法因子
{
if ((a1 % i) == 0)
{ //当a1%i能够除开则i是a1的除法因子
depart(a1 / i, s); // a1中去掉刚刚找到的乘法因子i,然后继续寻找剩下的乘法银子
break;
}
}
s = s + " " + to_string(v6); //由于最深处的递归函数首先执行本行,因此,最大的因子最先添加到a2上
}

解密脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <iostream>
#include <vector>
#include <string>
using namespace std;
string ans[11] = {
"=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"
};
void change(string &str)
{
for (int i = 0; i < str.length(); ++i)
{
switch (str[i])
{
case 'O':
str[i] = '0';
break;
case 'l':
str[i] = '1';
break;
case 'z':
str[i] = '2';
break;
case 'E':
str[i] = '3';
break;
case 'A':
str[i] = '4';
break;
case 's':
str[i] = '5';
break;
case 'G':
str[i] = '6';
break;
case 'T':
str[i] = '7';
break;

case 'B':
str[i] = '8';
break;
case 'q':
str[i] = '9';
break;
case '=':
str[i] = ' ';
break;
}
}
}
int calc(const string &str){
string temp;
int ans=1;
for(int i=1;i<str.length();++i){
if(str[i]==' '){
ans*=stoi(temp);
temp="";
}
else{
temp+=str[i];
}
}
ans*=stoi(temp);
return ans;
}
int main(int argc, char **argv, char **envp)
{
for (int i = 0; i < 11; ++i)
{
change(ans[i]);
cout<<(calc(ans[i])^1)<<endl;
}
return 0;
}

运行结果

1
2
3
4
5
6
7
8
9
10
PS C:\Users\86135\Desktop\MRCTF2020\EzCPP> ./test
2345
1222
5774
2476
3374
9032
2456
3531
6720

Linux虚拟机或者wsl上运行EasyCPP然后把keys乎进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌──(root㉿Executor)-[/mnt/c/Users/86135/Desktop/MRCTF2020/EzCPP]
└─# ./EasyCPP
give me your key!
2345
1222
5774
2476
3374
9032
2456
3531
6720
right!
flag:MRCTF{md5(234512225774247633749032245635316720)}
md5()->{32/upper case/put the string into the function and transform into md5 hash}
sh: 1: pause: not found

32位大写的md5加密:

1
4367FB5F42C6E46B2AF79BF409FB84D3

因此flag是

1
MRCTF{4367FB5F42C6E46B2AF79BF409FB84D3}

交到buuctf上是

1
flag{4367FB5F42C6E46B2AF79BF409FB84D3}

胡思乱想

为什么ida会多此一举地创建多个副本呢?

以ida在main函数刚开始时创建的两个vector对象为例

1
2
std::vector<int>::vector(v20, argv, envp);
std::vector<bool>::vector(v19);

显然v20和v19是作为句柄用的,两句话实际上相当于

1
2
vector<int> v20(argv,envp);
vector<bool> v19;

然而我从来也没有见过传递两个字符串数组指针给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
2
3
4
5
6
7
8
9
10
11
12
13
push    rbp
mov rbp, rsp
push rbx
sub rsp, 138h
mov [rbp+var_14], 0
lea rax, [rbp+var_B0]
mov rdi, rax
call _ZNSt6vectorIiSaIiEEC2Ev ; std::vector<int>::vector(void)
lea rax, [rbp+var_E0]
mov rdi, rax
call _ZNSt6vectorIbSaIbEEC2Ev ; std::vector<bool>::vector(void)
lea rax, [rbp+var_91]
mov rdi, rax

本来创建一个vector容器的结构应该是

1
2
3
lea     rax, [rbp+var_B0]
mov rdi, rax
call _ZNSt6vectorIiSaIiEEC2Ev

栈上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
2
3
4
call    _ZNSt6vectorIiSaIiEEC2Ev ; std::vector<int>::vector(void)
lea rax, [rbp+var_E0]
mov rdi, rax
call _ZNSt6vectorIbSaIbEEC2Ev ; std::vector<bool>::vector(void)

因此ida认为后来这个vector是无参的

看雪论坛上大佬的解释是

image-20220727213433217

我猜ida有时判断不准要几个参数,有可能是调用者和被调用者的调用约定不同,比如cdecl的main函数调用thiscall的构造函数.

学了堆栈平衡再回来看吧

那为啥有一个vector<bool>后来却从来不用他?

从反汇编得到的指令来看,确实是有调用vector<bool>的构造函数的,可能是出题的为了混淆视听吧