-
N.IDA调试功能.switch分支的识别(逆向)
2009-09-13
程序的结构有顺序结构、选择结构、循环结构和分支结构。接下来看看分支结构switch。
在控制台工程ida4中通过项目设置把所有.cpp文件都Exclude出去。再新建一个C++源文件switch.cpp,内容为:#include <stdio.h>
bool isleap(int y)
{
return (y%4==0&&y%100!=0||y%400==0);
}
void main()
{
int year;
int month;
printf("input year and month:");
scanf("%d%d", &year, &month);
switch(month){
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
puts("31 days in the month.");
break;
case 4:
case 6:
case 9:
case 11:
puts("30 days in the month.");
break;
case 2:
printf("%d days in the month.\n", 28+isleap(year));
break;
default:
puts("invalid month!");
}
puts("finished");
}在Debug模式下用F7构建成可执行文件ida4.exe,然后用IDA打开ida4.exe。
为了观察有没有break的区别,我有的case后面有break有的后面没有。
加载完毕之后,在文本形式下,把变量var_4改名为year,把变量var_8改名为month,把变量var_C改名为temp。再来看代码。.text:004010A0 main proc near ; CODE XREF: _mainj
.text:004010A0
.text:004010A0 var_4C = byte ptr -4Ch
.text:004010A0 temp = dword ptr -0Ch
.text:004010A0 month = dword ptr -8
.text:004010A0 year = dword ptr -4
.text:004010A0
.text:004010A0 push ebp
.text:004010A1 mov ebp, esp
.text:004010A3 sub esp, 4Ch
.text:004010A6 push ebx
.text:004010A7 push esi
.text:004010A8 push edi
.text:004010A9 lea edi, [ebp+var_4C]
.text:004010AC mov ecx, 13h
.text:004010B1 mov eax, 0CCCCCCCCh
.text:004010B6 rep stosd
这一段是函数头。.text:004010B8 push offset aInputYearAndMo ; "input year and month:"
.text:004010BD call printf
.text:004010C2 add esp, 4
.text:004010C5 lea eax, [ebp+month]
.text:004010C8 push eax
.text:004010C9 lea ecx, [ebp+year]
.text:004010CC push ecx
.text:004010CD push offset aDD ; "%d%d"
.text:004010D2 call scanf
.text:004010D7 add esp, 0Ch
这一段让用户输入年和月保存到year和month中。.text:004010DA mov edx, [ebp+month]
.text:004010DD mov [ebp+temp], edx
.text:004010E0 mov eax, [ebp+temp]
.text:004010E3 sub eax, 1
.text:004010E6 mov [ebp+temp], eax
这一段是switch结构的开始,根据month计算跳转表的下标保存到temp中。.text:004010E9 cmp [ebp+temp], 0Bh
.text:004010ED ja short loc_40113B
这一段用temp跟0Bh(跳转表的最大下标)进行无符号大小比较,超过了就跳转到default。.text:004010EF mov ecx, [ebp+temp]
.text:004010F2 jmp ds:off_401166[ecx*4]
这一段根据temp转到跳转表中相应项指定的地方。off_401166是跳转表的地址。双击它可以到跳转表,跳转表中都是一些代码入口偏移量,如图:.text:004010F9
.text:004010F9 loc_4010F9: ; DATA XREF: .text:off_401166o
.text:004010F9 ; .text:0040116Eo ...
.text:004010F9 push offset a31DaysInTheMon ; "31 days in the month."
.text:004010FE call puts
.text:00401103 add esp, 4
.text:00401106 jmp short loc_401148
.text:00401108 ; ---------------------------------------------------------------------------
这一段调用puts输出"31 days in the month.",然后跳转到整个switch结构之后。.text:00401108
.text:00401108 loc_401108: ; CODE XREF: main+52j
.text:00401108 ; DATA XREF: .text:00401172o ...
.text:00401108 push offset a30DaysInTheMon ; "30 days in the month."
.text:0040110D call puts
.text:00401112 add esp, 4
.text:00401115 jmp short loc_401148
.text:00401117 ; ---------------------------------------------------------------------------
这一段调用puts输出"30 days in the month.",然后跳转到整个switch结构之后。.text:00401117
.text:00401117 loc_401117: ; CODE XREF: main+52j
.text:00401117 ; DATA XREF: .text:0040116Ao
.text:00401117 mov edx, [ebp+year]
.text:0040111A push edx
.text:0040111B call j_isleap
.text:00401120 add esp, 4
这一段调用isleap计算year是否是闰年,结果1或者0保存在al中。.text:00401123 and eax, 0FFh
.text:00401128 add eax, 1Ch
.text:0040112B push eax
.text:0040112C push offset aDDaysInTheMont ; "%d days in the month.\n"
.text:00401131 call printf
.text:00401136 add esp, 8
.text:00401139 jmp short loc_401148
.text:0040113B ; ---------------------------------------------------------------------------
这一段根据al中的布尔值加上28得到这个月的天数,调用printf输出"%d days in the month.\n",然后跳转到整个switch结构之后。.text:0040113B
.text:0040113B loc_40113B: ; CODE XREF: main+4Dj
.text:0040113B push offset aInvalidMonth ; "invalid month!"
.text:00401140 call puts
.text:00401145 add esp, 4
这一段是switch结构中的default部分,调用puts输出"invalid month!"。
switch结构到这里结束。.text:00401148
.text:00401148 loc_401148: ; CODE XREF: main+66j
.text:00401148 ; main+75j ...
.text:00401148 push offset aFinished ; "finished"
.text:0040114D call puts
.text:00401152 add esp, 4
.text:00401155 pop edi
.text:00401156 pop esi
.text:00401157 pop ebx
.text:00401158 add esp, 4Ch
.text:0040115B cmp ebp, esp
.text:0040115D call __chkesp
.text:00401162 mov esp, ebp
.text:00401164 pop ebp
.text:00401165 retn
.text:00401165 main endp
这一段调用puts输出"finished",检查堆栈,main函数结束。咱们注意看IDA View-A文字形式左侧边的箭头。如图。
这是有史以来最大的一张图,我用拼接的办法做的,为的是看到箭头的全貌。
从图里可以看到switch结构的箭头跟前面的都不一样。
1号箭头是有default的情况的处理箭头,实际上跟if/else一样。如果没有default应该就没有这个箭头。
2号箭头是一个起点多个终点,这就是跳转表,是switch结构的特征箭头。
/*************************************************************
老大的视频教程上的例子,因为case比较少,编译器没有用跳转表,用的是一系列的if/else来实现,所以没有这种特征。
*************************************************************/
3号箭头是多个起点一个终点,这是break的特征。转到图形形式,可以看到分块的程序汇编代码,用右键菜单Group nodes功能把每个模块换成跟汇编指令对应的C源程序语句,调整条件判断与颜色一致。
完成后程序的流程图为:注意图中我用椭圆标记的部分,那就是根据跳转表跳转的控制。
用传说中的F5来看看。如图。
虽然得到的代码有些错误(对printf和scanf的调用),但基本上能还原switch结构,不过会把default处理成if/else。
-
L.IDA调试功能.对象(逆向)
2009-09-13
C++程序的基本语法已经大体了解了,该进入面向对象的部分了。今天来初识对象。
在控制台工程ida4中通过项目设置把所有.cpp文件都Exclude出去。再新建一个C++源文件object.cpp,内容为:#include <iostream>
using namespace std;
#include <cstring>class Person{
char name[20];
int age;
public:
Person(char const* name, int age):age(age){
strcpy(this->name, name);
}
void show(){
cout << "I am " << name << ", I am born in " << 2009-age << endl;
}
void welcome(Person const& other){
cout << "Hello " << other.name << ", I am " << name << ", nice to see you!" << endl;
}
};
int main()
{
Person niu("Voldemort", 25);
Person chen("ChenZongquan", 36);
chen.show();
chen.welcome(niu);
return 0;
}这次完全按照标准C++的格式来写了,大家可能注意到头文件和main函数已经变了。没关系,咱们专注到对象上。
在Debug模式下用F7构建成可执行文件ida4.exe,然后用IDA打开ida4.exe。加载完毕之后,在文本形式下,把变量var_30改名为chen,把变量var_18改名为niu,把立即数19h和24h分别改成十进制的25和36。再来看main函数的代码,看看是如何创建对象和调用对象成员函数的。
.text:00401590 main proc near ; CODE XREF: _mainj
.text:00401590
.text:00401590 var_70 = byte ptr -70h
.text:00401590 chen = byte ptr -30h
.text:00401590 niu = byte ptr -18h
.text:00401590
.text:00401590 push ebp
.text:00401591 mov ebp, esp
.text:00401593 sub esp, 70h
.text:00401596 push ebx
.text:00401597 push esi
.text:00401598 push edi
.text:00401599 lea edi, [ebp+var_70]
.text:0040159C mov ecx, 1Ch
.text:004015A1 mov eax, 0CCCCCCCCh
.text:004015A6 rep stosd
这一段是函数的栈初始化代码,跟以往的一样。.text:004015A8 push 25
.text:004015AA push offset aVoldemort ; "Voldemort"
.text:004015AF lea ecx, [ebp+niu]
.text:004015B2 call j_Person__Person
这一段是对应代码“Person niu("Voldemort", 25)”,创建对象niu,可以看到它在调用构造函数Person::Person。注意除了把两个参数25和"Voldemort"入栈外,它还把变量niu的地址存到了寄存器ecx中。等会儿到构造函数中再看这个ecx的作用,推测是到成员函数中当成this用。.text:004015B7 push 36
.text:004015B9 push offset aChenzongquan ; "ChenZongquan"
.text:004015BE lea ecx, [ebp+chen]
.text:004015C1 call j_Person__Person
这一段则是创建另一个对象chen,也是把参数入栈,把地址存到了ecx中。.text:004015C6 lea ecx, [ebp+chen]
.text:004015C9 call j_Person__show
这一段是“chen.show()”,调用对象chen的成员函数show,机器指令的操作是把chen的地址存入ecx中后调用的Person::show函数。.text:004015CE lea eax, [ebp+niu]
.text:004015D1 push eax
.text:004015D2 lea ecx, [ebp+chen]
.text:004015D5 call j_Person__welcome
这一段是“chen.welcome(niu)”,先是把参数niu的地址入栈,再把chen的地址保存到ecx中,调用Person::welcome函数。
/**********************************************************
可以看出对引用形参传递的实参是地址,实质上是用指针实现的。
**********************************************************/.text:004015DA xor eax, eax
.text:004015DC pop edi
.text:004015DD pop esi
.text:004015DE pop ebx
.text:004015DF add esp, 70h
.text:004015E2 cmp ebp, esp
.text:004015E4 call __chkesp
.text:004015E9 mov esp, ebp
.text:004015EB pop ebp
.text:004015EC retn
.text:004015EC main endp
这一段对应“return 0”,跟原来void main的时候比多了一句xor eax, eax,也就是把eax置0作为main函数的返回值,然后检查堆栈,main函数结束。
/******************************************************
整个main函数到这里结束了,对象niu和chen应该释放了,但我们并没有看到传说中的默认析构函数的调用。这意味着编译器只是在必须的时候才为类产生默认的析构函数。
******************************************************/----------------------------------------
再看看几个成员函数。
先来看构造函数。找到那个函数,把变量var_4改成this,变量arg_0改成arg_name,arg_4改名arg_age,把立即数14h改成20,代码如下:.text:00401610 Person__Person proc near ; CODE XREF: j_Person__Personj
.text:00401610
.text:00401610 var_44 = byte ptr -44h
.text:00401610 this = dword ptr -4
.text:00401610 arg_name = dword ptr 8
.text:00401610 arg_age = dword ptr 0Ch
.text:00401610
.text:00401610 push ebp
.text:00401611 mov ebp, esp
.text:00401613 sub esp, 44h
.text:00401616 push ebx
.text:00401617 push esi
.text:00401618 push edi
.text:00401619 push ecx
.text:0040161A lea edi, [ebp+var_44]
.text:0040161D mov ecx, 11h
.text:00401622 mov eax, 0CCCCCCCCh
.text:00401627 rep stosd
这一段是函数的栈初始化代码,略过。.text:00401629 pop ecx
.text:0040162A mov [ebp+this], ecx
这一段把前面放在ecx中的对象地址取出来保存到函数中的局部变量this中。.text:0040162D mov eax, [ebp+this] ;eax=this
.text:00401630 mov ecx, [ebp+arg_age] ;ecx=arg_age
.text:00401633 mov [eax+20], ecx ;this->age=ecx
这一段处理初始化列表,[eax+20]表示这个对象的age成员,其中20是age成员的偏移量。把形参age的值传送到成员变量age中。.text:00401636 mov edx, [ebp+arg_name] ;edx=arg_name
.text:00401639 push edx
.text:0040163A mov eax, [ebp+this] ;eax=this->name,因为成员name的偏移为0,所以没有写。
.text:0040163D push eax
.text:0040163E call strcpy
.text:00401643 add esp, 8 ;恢复堆栈
.text:00401646 mov eax, [ebp+this] ;以this为返回值。
这一段是“strcpy(this->name, name)”,调用strcpy把形参name的字符串复制到成员name的字符串中。
/********************************************************
因为strcpy是声明为__cdecl的,所以这个函数由调用方来恢复堆栈。
构造函数以this为返回值,有点不好理解。构造函数不是没有返回值吗?
********************************************************/.text:00401649 pop edi
.text:0040164A pop esi
.text:0040164B pop ebx
.text:0040164C add esp, 44h
.text:0040164F cmp ebp, esp
.text:00401651 call __chkesp
.text:00401656 mov esp, ebp
.text:00401658 pop ebp
.text:00401659 retn 8 ;自行恢复堆栈。
.text:00401659 Person__Person endp
这一段是构造函数结束,自行恢复堆栈。----------------------------------------
再看看成员函数show()。这个函数就一句话:
cout << "I am " << name << ", I am born in " << 2009-age << endl;
找到那个函数,把变量var_4改成this,把loc_4010CD改成endl,把立即数7d9h改成2009,代码如下:.text:00401670 Person__show proc near ; CODE XREF: j_Person__showj
.text:00401670
.text:00401670 var_44 = byte ptr -44h
.text:00401670 this = dword ptr -4
.text:00401670
.text:00401670 push ebp
.text:00401671 mov ebp, esp
.text:00401673 sub esp, 44h
.text:00401676 push ebx
.text:00401677 push esi
.text:00401678 push edi
.text:00401679 push ecx
.text:0040167A lea edi, [ebp+var_44]
.text:0040167D mov ecx, 11h
.text:00401682 mov eax, 0CCCCCCCCh
.text:00401687 rep stosd
这一段是函数的栈初始化代码,略过。.text:00401689 pop ecx
.text:0040168A mov [ebp+this], ecx
这一段设置this,跟前面的构造函数一样。所有非静态成员函数应该都会有这个。.text:0040168D push offset endl ;endl入栈
.text:00401692 mov eax, [ebp+this]
.text:00401695 mov ecx, 2009
.text:0040169A sub ecx, [eax+14h] ;计算2009-this->age
.text:0040169D push ecx ;把计算结果入栈
.text:0040169E push offset aIAmBornIn;把", I am born in "入栈
.text:004016A3 mov edx, [ebp+this]
.text:004016A6 push edx ;把this->name入栈
.text:004016A7 push offset aIAm ;把"I am "入栈
.text:004016AC push offset std__cout ;把cout入栈
.text:004016B1 call j_std__operator__;调operator<<输出“I am”,返回值为cout在eax中
.text:004016B6 add esp, 8 ;恢复堆栈
.text:004016B9 push eax ;把cout入栈
.text:004016BA call j_std__operator__;调operator<<输出this->name,返回值为cout在eax中
.text:004016BF add esp, 8 ;恢复堆栈
.text:004016C2 push eax ;把cout入栈
.text:004016C3 call j_std__operator__;调operator<<输出“, I am born in ”,返回值为cout在eax中
.text:004016C8 add esp, 8 ;恢复堆栈
.text:004016CB mov ecx, eax ;把cout入栈
.text:004016CD call sub_401104 ;调operator<<输出2009-this->age的结果,返回值为cout在eax中
.text:004016D2 mov ecx, eax ;把cout入栈
.text:004016D4 call j_std__basic_ostream_char_std__char_traits_char_____operator__;调operator<<输出endl
这一段是执行输出语句,编译器对连续的输出采用了一种特殊的处理办法:把要输出的全部数据按相反的顺序计算、入栈,然后反复调用相应的输出运算符函数来输出,每次输出之后下次输出之前都把上次输出的返回值cout(在eax中)传给输出运算符函数。.text:004016D9 pop edi
.text:004016DA pop esi
.text:004016DB pop ebx
.text:004016DC add esp, 44h
.text:004016DF cmp ebp, esp
.text:004016E1 call __chkesp
.text:004016E6 mov esp, ebp
.text:004016E8 pop ebp
.text:004016E9 retn
.text:004016E9 Person__show endp
这一段是show函数结束。----------------------------------------
最后看看成员函数welcome(Person const& other)。这个函数就一句话:
cout << "Hello " << other.name << ", I am " << name << ", nice to see you!" << endl;
找到那个函数,把变量var_4改成this,把arg_0改成other,把立即数7d9h改成2009,代码如下:.text:00401710 Person__welcome proc near ; CODE XREF: j_Person__welcomej
.text:00401710
.text:00401710 var_44 = byte ptr -44h
.text:00401710 this = dword ptr -4
.text:00401710 other = dword ptr 8
.text:00401710
.text:00401710 push ebp
.text:00401711 mov ebp, esp
.text:00401713 sub esp, 44h
.text:00401716 push ebx
.text:00401717 push esi
.text:00401718 push edi
.text:00401719 push ecx
.text:0040171A lea edi, [ebp+var_44]
.text:0040171D mov ecx, 11h
.text:00401722 mov eax, 0CCCCCCCCh
.text:00401727 rep stosd
这一段是函数的栈初始化代码,略过。.text:00401729 pop ecx
.text:0040172A mov [ebp+this], ecx
这一段设置this,跟前面的构造函数和show函数一样。.text:0040172D push offset endl ;endl入栈
.text:00401732 push offset aNiceToSeeYou ;", nice to see you!"入栈
.text:00401737 mov eax, [ebp+this] ;this->name入栈
.text:0040173A push eax
.text:0040173B push offset aIAm_0 ;", I am "入栈
.text:00401740 mov ecx, [ebp+other] ;other->name入栈
.text:00401743 push ecx
.text:00401744 push offset aHello ;"Hello "入栈
.text:00401749 push offset std__cout ;cout入栈
.text:0040174E call j_std__operator__ ;调用输出运算符函数输出“Hello ”
.text:00401753 add esp, 8
.text:00401756 push eax ;cout入栈
.text:00401757 call j_std__operator__ ;调用输出运算符函数输出other->name
.text:0040175C add esp, 8
.text:0040175F push eax ;cout入栈
.text:00401760 call j_std__operator__ ;调用输出运算符函数输出“, I am ”
.text:00401765 add esp, 8
.text:00401768 push eax ;cout入栈
.text:00401769 call j_std__operator__ ;调用输出运算符函数输出this->name
.text:0040176E add esp, 8
.text:00401771 push eax ;cout入栈
.text:00401772 call j_std__operator__ ;调用输出运算符函数输出“, nice to see you!”
.text:00401777 add esp, 8
.text:0040177A mov ecx, eax ;cout入栈
.text:0040177C call j_std__basic_ostream_char_std__char_traits_char_____operator__;输出endl
这一段调用输出运算符函数来输出一系列数据。.text:00401781 pop edi
.text:00401782 pop esi
.text:00401783 pop ebx
.text:00401784 add esp, 44h
.text:00401787 cmp ebp, esp
.text:00401789 call __chkesp
.text:0040178E mov esp, ebp
.text:00401790 pop ebp
.text:00401791 retn 4
.text:00401791 Person__welcome endp
这一段是welcome函数结束,自行恢复堆栈。这一次尝试的体会,基本上所谓的面向对象,也就是在创建对象时会调用构造函数,调用成员函数前把对象的地址保存到寄存器ecx中,到成员函数中再把ecx中的对象地址转存到var_4也就是this中。
另一个就是发现cout连续输出的处理跟printf输出数据的方式很类似,按从右向左的方向把数据全部入栈之后才开始输出。
这一次没有处理虚函数的问题,下一次再深入。 -
K.IDA调试功能.形参和返回值类型.续补(逆向)
2009-09-13
续补
; 为用rep movsd指令传递结构变量做准备(20h/4=8)
.text:00401300 lea esi, [esp+0BCh+s_name_0]
.text:00401304 lea edi, [esp+0BCh+r] ; 准备把Person结构变量s(也就是big的返回值)复制到结构变量r中
.text:0040130B push eax ; 把字符ch入栈准备输出
.text:0040130C rep movsd ; 复制结构变量s到r
这一段把刚才保存的s复制到结构变量r中,中间插入了一个push eax是后面的输出函数用的。.text:0040130E push offset cout_addr ; 把对象cout地地址入栈
.text:00401313 call operator_output_char
.text:00401318 add esp, 4Ch
.text:0040131B mov esi, eax ; eax中是operator<<(cout,ch)的返回值,cout的地址
.text:0040131D mov ecx, esi ; 把cout的地址放到ecx中准备传给this
.text:0040131F push 0Ah ; 换行符入栈准备输出(endl被直接处理成了换行符)
.text:00401321 call cout_operator_output_char ; 调用cout.operator<<输出换行符
这一段输出字符变量ch,直接用的保存在eax中的数据,没有重新从内存中读取变量ch,这也是编译器的优化。另外输出endl被直接变成了输出换行符'\n',同样是编译器的优化结果。.text:00401326 mov ecx, [esi] ; 以下部分为异常处理
.text:00401328 xor edi, edi
.text:0040132A mov edx, [ecx+4]
.text:0040132D mov cl, [edx+esi+4]
.text:00401331 test cl, 6
.text:00401334 lea eax, [edx+esi]
.text:00401337 jnz short loc_40134D
.text:00401339 mov eax, [eax+28h]
.text:0040133C mov ecx, eax
.text:0040133E mov edx, [eax]
.text:00401340 call dword ptr [edx+2Ch]
.text:00401343 cmp eax, 0FFFFFFFFh
.text:00401346 jnz short loc_40134D
.text:00401348 mov edi, 4
.text:0040134D
.text:0040134D loc_40134D: ; CODE XREF: _main+147j
.text:0040134D ; _main+156j
.text:0040134D mov eax, [esi]
.text:0040134F mov ecx, [eax+4]
.text:00401352 add ecx, esi
.text:00401354 test edi, edi
.text:00401356 jz short loc_40136E
.text:00401358 mov eax, [ecx+4]
.text:0040135B mov edx, [ecx+28h]
.text:0040135E or eax, edi
.text:00401360 test edx, edx
.text:00401362 jnz short loc_401366
.text:00401364 or al, 4
.text:00401366
.text:00401366 loc_401366: ; CODE XREF: _main+172j
.text:00401366 push 0
.text:00401368 push eax
.text:00401369 call ?clear@ios_base@std@@QAEXH_N@Z ; std::ios_base::clear(int,bool)
这一段是输出异常情况的检查和处理,我没有去分析,如果有充分的时间我会重新去分析的。.text:0040136E
.text:0040136E loc_40136E: ; CODE XREF: _main+166j
.text:0040136E push ebx ; 把在.text:004012B5 mov ebx, eax指令用ebx保存的调用mul函数的返回值num入栈
.text:0040136F mov ecx, offset cout_addr ; cout的地址入栈
.text:00401374 call operator_output_int ; 输出整数num,返回值为cout的地址保存在eax中
.text:00401379 mov esi, eax
.text:0040137B push 0Ah ; 换行符入栈(代替endl)
.text:0040137D mov ecx, esi ; 用ecx保存上一个输出函数返回的cout地址准备传递给this
.text:0040137F call cout_operator_output_char ; 输出换行符
这一段输出num的值,经过编译器优化同样是直接用保存在ebx中的数据而没有重新读取内存中的num,而且endl也直接处理成了'\n'。.text:00401384 mov ecx, [esi] ; 以下部分为异常处理
.text:00401386 xor edi, edi
.text:00401388 mov edx, [ecx+4]
.text:0040138B mov cl, [edx+esi+4]
.text:0040138F test cl, 6
.text:00401392 lea eax, [edx+esi]
.text:00401395 jnz short loc_4013AB
.text:00401397 mov eax, [eax+28h]
.text:0040139A mov ecx, eax
.text:0040139C mov edx, [eax]
.text:0040139E call dword ptr [edx+2Ch]
.text:004013A1 cmp eax, 0FFFFFFFFh
.text:004013A4 jnz short loc_4013AB
.text:004013A6 mov edi, 4
.text:004013AB
.text:004013AB loc_4013AB: ; CODE XREF: _main+1A5j
.text:004013AB ; _main+1B4j
.text:004013AB mov eax, [esi]
.text:004013AD mov ecx, [eax+4]
.text:004013B0 add ecx, esi
.text:004013B2 test edi, edi
.text:004013B4 jz short loc_4013CC
.text:004013B6 mov eax, [ecx+4]
.text:004013B9 mov edx, [ecx+28h]
.text:004013BC or eax, edi
.text:004013BE test edx, edx
.text:004013C0 jnz short loc_4013C4
.text:004013C2 or al, 4
.text:004013C4
.text:004013C4 loc_4013C4: ; CODE XREF: _main+1D0j
.text:004013C4 push 0
.text:004013C6 push eax
.text:004013C7 call ?clear@ios_base@std@@QAEXH_N@Z ; std::ios_base::clear(int,bool)
这一段又是输出异常的检查和处理。.text:004013CC
.text:004013CC loc_4013CC: ; CODE XREF: _main+1C4j
.text:004013CC mov ecx, dword ptr [esp+78h+res+4]
.text:004013D0 mov edx, dword ptr [esp+78h+res]
.text:004013D4 push ecx ; 把res的高4字节入栈
.text:004013D5 push edx ; 把res的低4字节入栈
.text:004013D6 mov ecx, offset cout_addr ; cout的地址入栈
.text:004013DB call operator_output_double ; 输出res中的小数
.text:004013E0 mov esi, eax
.text:004013E2 push 0Ah
.text:004013E4 mov ecx, esi
.text:004013E6 call cout_operator_output_char ; 输出换行符
这一段输出double变量res中的值。变量值不在寄存器中,编译器从内存中分两次读取了res的8个字节数据入栈,再把cout的地址入栈,调用operator<<(cout,double),再输出换行符。.text:004013EB mov eax, [esi] ; 以下为异常处理
.text:004013ED xor edi, edi
.text:004013EF mov ecx, [eax+4]
.text:004013F2 lea eax, [ecx+esi]
.text:004013F5 mov cl, [ecx+esi+4]
.text:004013F9 test cl, 6
.text:004013FC jnz short loc_401412
.text:004013FE mov eax, [eax+28h]
.text:00401401 mov ecx, eax
.text:00401403 mov edx, [eax]
.text:00401405 call dword ptr [edx+2Ch]
.text:00401408 cmp eax, 0FFFFFFFFh
.text:0040140B jnz short loc_401412
.text:0040140D mov edi, 4
.text:00401412
.text:00401412 loc_401412: ; CODE XREF: _main+20Cj
.text:00401412 ; _main+21Bj
.text:00401412 mov eax, [esi]
.text:00401414 mov ecx, [eax+4]
.text:00401417 add ecx, esi
.text:00401419 test edi, edi
.text:0040141B jz short loc_401433
.text:0040141D mov eax, [ecx+4]
.text:00401420 mov edx, [ecx+28h]
.text:00401423 or eax, edi
.text:00401425 test edx, edx
.text:00401427 jnz short loc_40142B
.text:00401429 or al, 4
.text:0040142B
.text:0040142B loc_40142B: ; CODE XREF: _main+237j
.text:0040142B push 0
.text:0040142D push eax
.text:0040142E call ?clear@ios_base@std@@QAEXH_N@Z ; std::ios_base::clear(int,bool)
这一段又是输出异常的检查和处理。.text:00401433
.text:00401433 loc_401433: ; CODE XREF: _main+22Bj
.text:00401433 lea ecx, [esp+78h+r]
.text:00401437 push ecx ; Person结构变量r的地址入栈
.text:00401438 push offset cout_addr ; cout的地址入栈
.text:0040143D call operator_output_Person ; 调用自定义的operator<<(cout,Person)输出r
.text:00401442 add esp, 8
.text:00401445 mov esi, eax
.text:00401447 mov ecx, esi
.text:00401449 push 0Ah
.text:0040144B call cout_operator_output_char ; 输出换行符
这一段调用自定义的operator<<函数来输出结构变量r,再输出换行符。由于参数是引用传递,所以没有象调用big函数那样复制结构变量,只传递了结构变量r的地址。.text:00401450 mov edx, [esi] ; 以下为异常处理
.text:00401452 xor edi, edi
.text:00401454 mov eax, [edx+4]
.text:00401457 add eax, esi
.text:00401459 test byte ptr [eax+4], 6
.text:0040145D jnz short loc_401473
.text:0040145F mov eax, [eax+28h]
.text:00401462 mov ecx, eax
.text:00401464 mov edx, [eax]
.text:00401466 call dword ptr [edx+2Ch]
.text:00401469 cmp eax, 0FFFFFFFFh
.text:0040146C jnz short loc_401473
.text:0040146E mov edi, 4
.text:00401473
.text:00401473 loc_401473: ; CODE XREF: _main+26Dj
.text:00401473 ; _main+27Cj
.text:00401473 mov eax, [esi]
.text:00401475 mov ecx, [eax+4]
.text:00401478 add ecx, esi
.text:0040147A test edi, edi
.text:0040147C jz short loc_401494
.text:0040147E mov eax, [ecx+4]
.text:00401481 mov edx, [ecx+28h]
.text:00401484 or eax, edi
.text:00401486 test edx, edx
.text:00401488 jnz short loc_40148C
.text:0040148A or al, 4
.text:0040148C
.text:0040148C loc_40148C: ; CODE XREF: _main+298j
.text:0040148C push 0
.text:0040148E push eax
.text:0040148F call ?clear@ios_base@std@@QAEXH_N@Z ; std::ios_base::clear(int,bool)
这一段又是输出异常的检查和处理。.text:00401494
.text:00401494 loc_401494: ; CODE XREF: _main+28Cj
.text:00401494 pop edi
.text:00401495 pop esi
.text:00401496 xor eax, eax ; eax清零作为main函数的返回值
.text:00401498 pop ebx
.text:00401499 mov esp, ebp ; 恢复栈寄存器
.text:0040149B pop ebp
.text:0040149C retn
.text:0040149C _main endp
main函数到此结束,返回eax中的0。关于release模式,总结一下:
/*************************************************************
由于不需要调试,没有符号表了,所以在用IDA加载的时候不提示是否加载符号表的问题了。
调用函数不再是通过一个只有一个jmp指令的函数间接调用了,而是直接调用目标函数了。
在函数中尽可能地去掉了临时局部变量,不再额外开出0x40个字节的保护性栈空间,不再对局部变量进行默认0xCC初始化。
对栈空间进行随机访问时不再用ebp作基地址,直接用esp+偏移作基地址。
指令的顺序可能出于优化的目的被编译器打乱了。
对变量的访问,如果寄存器中有就直接使用寄存器的,寄存器中没有才重新从内存中读取。
在输出输出指令之后直接进行了输出输出的异常检查及处理。
特别有意思的是,在release模式下,cout<<endl直接处理成cout<<'\n'了。
*************************************************************/ -
M.IDA调试功能.封装、继承与多态(逆向)
2009-09-13
这次咱们进入面向对象:封装、继承和多态。
新建一个空的控制台工程ida5-1,再新建一个C++源文件object.cpp,内容为:#include <iostream>
using namespace std;
#include <cstring>class Person{
char name[20];
int age;
public:
Person(char const* name, int age):age(age){
strcpy(this->name, name);
}
virtual void show()const{
cout << "大家好,我叫" << name << ",今年" << age << endl;
}
void welcome(Person const& other)const{
cout << "很高兴认识你," << other.name << ",我是" << name << endl;
}
char const* getName()const{return name;}
int getAge()const{return age;}
virtual ~Person(){}
};
class Teacher : public Person{
char course[10];
public:
Teacher(char const* name, int age, char const* course):Person(name,age){
strcpy(this->course, course);
}
virtual void show()const{
cout << "同学们好,我叫" << getName() << ",教大家" << course << endl;
}
void welcome(Person const& other)const{
cout << "欢迎你来我这里学习," << other.getName() << endl;
}
};
class Student : public Person{
int num;
public:
Student(char const* name, int age, int num):Person(name,age),num(num){}
virtual void show()const{
cout << "大家好,我是" << num << "号学员" << getName() << endl;
}
void welcome(Person const& other)const{
cout << "你好," << other.getName() << ",欢迎你加入我们班!" << endl;
}
};
int main()
{
Person nb("wxf", 22);
Person* pt[3]={NULL};
pt[0] = new Teacher("Voldemort", 25, "软件逆向");
pt[1] = new Student("陈宗权", 36, 5);
pt[2] = new Student("上善若水", 23, 7);
int i;
for(i=0; i<3; i++){
pt[i]->show();
pt[i]->welcome(nb);
}
for(i=0; i<3; i++)
delete pt[i];
return 0;
}这次完全按照标准C++的格式来写的,标准头文件和标准main函数。
在这个程序中用了:
一个基类Person,其中有两个数据成员name和age,一个带参构造函数,一个虚成员函数show,一个普通成员函数welcome,两个getter函数,一个虚析构函数。
一个派生类Teacher,其中有一个数据成员course,一个带参构造函数,一个虚成员函数show,一个普通成员函数welcome。
一个派生类Student,其中有一个数据成员num,一个带参构造函数,一个虚成员函数show,一个普通成员函数welcome。
在Debug模式下用F7构建成可执行文件ida5-1.exe,然后用IDA打开ida5-1.exe。加载完毕之后,在文本形式下,把变量var_28改名为nb,把变量var_34、var_30、var_2C改名为pt_0、pt_1、pt_2,把变量var_38改名为i,把立即数16h改成十进制的22,把立即数19h改成十进制的25,把立即数24h改成十进制的36,把立即数17h改成十进制的23,把变量aA的数组长度改为9,把变量asc_46F01C的数组长度也改为9。
再来看main函数的代码,如此之长,超过了以往每一次的代码。不过也在意料之中,因为咱们源程序就比以往每次都长很多。还是分段来看吧。.text:00401620 main proc near ; CODE XREF: _mainj
.text:00401620
这一段是main函数开始。.text:00401620 var_AC = byte ptr -0ACh
.text:00401620 var_6C = dword ptr -6Ch
.text:00401620 var_68 = dword ptr -68h
.text:00401620 var_64 = dword ptr -64h
.text:00401620 var_60 = dword ptr -60h
.text:00401620 var_5C = dword ptr -5Ch
.text:00401620 var_58 = dword ptr -58h
.text:00401620 var_54 = dword ptr -54h
.text:00401620 var_50 = dword ptr -50h
.text:00401620 var_4C = dword ptr -4Ch
.text:00401620 var_48 = dword ptr -48h
.text:00401620 var_44 = dword ptr -44h
.text:00401620 var_40 = dword ptr -40h
.text:00401620 var_3C = dword ptr -3Ch
.text:00401620 i = dword ptr -38h
.text:00401620 pt_0 = dword ptr -34h
.text:00401620 pt_1 = dword ptr -30h
.text:00401620 pt_2 = dword ptr -2Ch
.text:00401620 nb = byte ptr -28h
.text:00401620 var_C = dword ptr -0Ch
.text:00401620 var_4 = dword ptr -4
.text:00401620
这一段是变量表。咱们显式定义的变量只有对象nb、指针数组pt[3]和变量i,其它都是为临时变量准备的。.text:00401620 push ebp
.text:00401621 mov ebp, esp
这一段是保存ebp。.text:00401623 push 0FFFFFFFFh
.text:00401625 push offset unknown_libname_16 ; Microsoft VisualC 2-8/net runtime
.text:0040162A mov eax, large fs:0
.text:00401630 push eax
.text:00401631 mov large fs:0, esp
这一段是在做什么呢?本来不想这么早涉及异常,不过既然遇到了就简单说说吧。首先把本块的异常ID(也就是0FFFFFFFFh)和异常处理函数的地址入栈,依次在ebp-4和ebp-8位置。然后取得原来的异常处理链入口(在fs:0处)并入栈保存在ebp-0Ch位置,最后把当前异常处理结构登记为异常处理链第一个入口。.text:00401638 sub esp, 0A0h
.text:0040163E push ebx
.text:0040163F push esi
.text:00401640 push edi
.text:00401641 lea edi, [ebp+var_AC]
.text:00401647 mov ecx, 28h
.text:0040164C mov eax, 0CCCCCCCCh
.text:00401651 rep stosd
这一段就是常规的栈初始化代码。注意跟以往不同的是,栈初始化不是从ebp-0A0h到ebp-00h,而是从ebp-0ACh到ebp-0Ch,因为ebp-0Ch到ebp-00h留出来保存异常处理信息了。
.text:00401653 push 22
.text:00401655 push offset aWxf ; "wxf"
.text:0040165A lea ecx, [ebp+nb]
.text:0040165D call j_Person__Person
这一段是创建Person对象nb,传了两个参数"wxf"和22,对应代码为
Person nb("wxf", 22);.text:00401662 mov [ebp+var_4], 0
这一段把新异常ID设置为0。.text:00401669 mov [ebp+pt_0], 0
.text:00401670 xor eax, eax
.text:00401672 mov [ebp+pt_1], eax
.text:00401675 mov [ebp+pt_2], eax
这一段定义指针数组pt[3]并初始化成全部为NULL。NULL在经过编译器后就已经是0了。对应代码:
Person* pt[3]={NULL};.text:00401678 push 28h ; sizeof(Teacher)
.text:0040167A call ??2@YAPAXI@Z ; operator new(uint)
.text:0040167F add esp, 4
.text:00401682 mov [ebp+var_40], eax
这一小段是new Teacher并恢复堆栈,把new返回的堆空间地址保存到临时空间var_40中。.text:00401685 mov byte ptr [ebp+var_4], 1
这一小段把新异常ID设置为1。.text:00401689 cmp [ebp+var_40], 0
.text:0040168D jz short loc_4016A8
这一小段检查内存空间分配是否成功,如果失败则跳转到4016a8处把临时变量var_60设置为NULL,成功则调用构造函数。.text:0040168F push offset aA ; "软件逆向"
.text:00401694 push 25
.text:00401696 push offset aVoldemort ; "Voldemort"
.text:0040169B mov ecx, [ebp+var_40]
.text:0040169E call j_Teacher__Teacher
这一小段调用Teacher类的构造函数,传递对象地址var_40、"Voldemort"、25、"软件逆向"作为参数.text:004016A3 mov [ebp+var_60], eax
.text:004016A6 jmp short loc_4016AF
.text:004016A8 ; ---------------------------------------------------------------------------
.text:004016A8
这一小段把构造函数的返回值(对象地址this)保存到临时变量var_60中,然后跳过001618。
.text:004016A8 loc_4016A8: ; CODE XREF: main+6Dj
.text:004016A8 mov [ebp+var_60], 0
.text:004016AF
这一小段把var_60设置为NULL作为构造函数的返回值,指示不成功。.text:004016AF loc_4016AF: ; CODE XREF: main+86j
.text:004016AF mov ecx, [ebp+var_60]
.text:004016B2 mov [ebp+var_3C], ecx
这一小段把构造函数的返回值(对象地址或者NULL)转存到var_3C中,对象创建并初始化工作完成。.text:004016B5 mov byte ptr [ebp+var_4], 0
这一小段把异常ID恢复为0,因为new Teacher("Voldemort", 25, "软件逆向")中可能发生异常的指令已经执行完毕。.text:004016B9 mov edx, [ebp+var_3C]
.text:004016BC mov [ebp+pt_0], edx
这一小段把var_3C中的值转存到pt[0]中。
前面几个小段是用new创建一个Teacher对象,把对象地址保存到pt[0]中,对应源代码为:
pt[0] = new Teacher("Voldemort", 25, "软件逆向");
/********************************************************
从new返回对象地址到最后保存到pt[0]中,经历了var_40、var_60、var_3C三次中转,不知道是什么原因?
********************************************************/
.text:004016BF push 20h ; sizeof(Student)
.text:004016C1 call ??2@YAPAXI@Z ; operator new(uint)
.text:004016C6 add esp, 4
.text:004016C9 mov [ebp+var_48], eax
这一小段是new Student并恢复堆栈,把new返回的堆空间地址保存到临时空间var_48中。.text:004016CC mov byte ptr [ebp+var_4], 2
这一小段把新异常ID设置为2。.text:004016D0 cmp [ebp+var_48], 0
.text:004016D4 jz short loc_4016EC
这一小段检查内存空间分配是否成功,如果失败则跳转到4016EC处把临时变量var_64设置为NULL,成功则调用构造函数。.text:004016D6 push 5
.text:004016D8 push 36
.text:004016DA push offset aI ; "陈宗权"
.text:004016DF mov ecx, [ebp+var_48]
.text:004016E2 call j_Student__Student
.text:004016E7 mov [ebp+var_64], eax
.text:004016EA jmp short loc_4016F3
.text:004016EC ; ---------------------------------------------------------------------------
.text:004016EC
这一小段调用Student类的构造函数,传递对象地址var_48、"陈宗权"、36和5作为参数,把返回值也就是对象地址保存到变量var_64中,然后跳过4016EC直接到4016F3。.text:004016EC loc_4016EC: ; CODE XREF: main+B4j
.text:004016EC mov [ebp+var_64], 0
.text:004016F3
这一小段把var_64设置为NULL作为构造函数的返回值,指示不成功。.text:004016F3 loc_4016F3: ; CODE XREF: main+CAj
.text:004016F3 mov eax, [ebp+var_64]
.text:004016F6 mov [ebp+var_44], eax
这一小段把构造函数的返回值(对象地址或者NULL)转存到var_44中,对象创建并初始化工作完成。.text:004016F9 mov byte ptr [ebp+var_4], 0
这一小段把异常ID恢复为0,因为new Student("陈宗权", 36, 5)中可能发生异常的指令已经执行完毕。.text:004016FD mov ecx, [ebp+var_44]
.text:00401700 mov [ebp+pt_1], ecx
这一小段把var_44中的值转存到pt[1]中。
前面几个小段是用new创建一个Student对象,把对象地址保存到pt[1]中,对应源代码为:
pt[1] = new Student("陈宗权", 36, 5);.text:00401703 push 20h ; sizeof(Student)
.text:00401705 call ??2@YAPAXI@Z ; operator new(uint)
.text:0040170A add esp, 4
.text:0040170D mov [ebp+var_50], eax
.text:00401710 mov byte ptr [ebp+var_4], 3
.text:00401714 cmp [ebp+var_50], 0
.text:00401718 jz short loc_401730
.text:0040171A push 7
.text:0040171C push 17h
.text:0040171E push offset asc_46F01C ; "上善若水"
.text:00401723 mov ecx, [ebp+var_50]
.text:00401726 call j_Student__Student
.text:0040172B mov [ebp+var_68], eax
.text:0040172E jmp short loc_401737
.text:00401730 ; ---------------------------------------------------------------------------
.text:00401730
.text:00401730 loc_401730: ; CODE XREF: main+F8j
.text:00401730 mov [ebp+var_68], 0
.text:00401737
.text:00401737 loc_401737: ; CODE XREF: main+10Ej
.text:00401737 mov edx, [ebp+var_68]
.text:0040173A mov [ebp+var_4C], edx
.text:0040173D mov byte ptr [ebp+var_4], 0
.text:00401741 mov eax, [ebp+var_4C]
.text:00401744 mov [ebp+pt_2], eax
这一段跟前面一段几乎是一样的,就不再分析了,对应的源代码为:
pt[2] = new Student("上善若水", 23, 7);.text:00401747 mov [ebp+i], 0
.text:0040174E jmp short loc_401759
.text:00401750 ; ---------------------------------------------------------------------------
.text:00401750
这一段是for循环的准备部分i=0,然后就跳转到条件判断部分。.text:00401750 loc_401750: ; CODE XREF: main+16Aj
.text:00401750 mov ecx, [ebp+i]
.text:00401753 add ecx, 1
.text:00401756 mov [ebp+i], ecx
.text:00401759
这一段是for循环的调整部分i++。.text:00401759 loc_401759: ; CODE XREF: main+12Ej
.text:00401759 cmp [ebp+i], 3
.text:0040175D jge short loc_40178C
这一段是for循环的条件判断部分i<3,不满足则跳出循环,到40178C处。.text:0040175F mov edx, [ebp+i] ;edx=i
.text:00401762 mov ecx, [ebp+edx*4+pt_0] ;ecx=pt[i]
这一小段取得对象地址保存到ecx中,作为后面调用成员函数时传递给this指针的参数。.text:00401766 mov eax, [ebp+i] ;eax=i
.text:00401769 mov edx, [ebp+eax*4+pt_0] ;edx=pt[i]
.text:0040176D mov eax, [edx] ;eax=pt[i]->vfptr
这一小段取得虚表指针vfptr,它在对象的最前面4个字节。.text:0040176F mov esi, esp
.text:00401771 call dword ptr [eax] ;通过虚函数表调用第一个虚函数
.text:00401773 cmp esi, esp
.text:00401775 call __chkesp
这一小段保存esp,调用虚表指针指向的第一个成员函数,也就是show函数,然后再检查堆栈。对应源代码为:
pt[i]->show();.text:0040177A lea ecx, [ebp+nb]
.text:0040177D push ecx
.text:0040177E mov edx, [ebp+i]
.text:00401781 mov ecx, [ebp+edx*4+pt_0]
.text:00401785 call j_Person__welcome
这一段调用成员函数welcome,把pt[i]作为this参数,nb作为实参,对应源代码为。
pt[i]->welcome(nb);.text:0040178A jmp short loc_401750
这里跳转回到for循环的条件判断部分,构成循环。.text:0040178C ; ---------------------------------------------------------------------------
.text:0040178C
.text:0040178C loc_40178C: ; CODE XREF: main+13Dj
.text:0040178C mov [ebp+i], 0
.text:00401793 jmp short loc_40179E
.text:00401795 ; ---------------------------------------------------------------------------
.text:00401795
.text:00401795 loc_401795: ; CODE XREF: main:loc_4017DCj
.text:00401795 mov eax, [ebp+i]
.text:00401798 add eax, 1
.text:0040179B mov [ebp+i], eax
.text:0040179E
.text:0040179E loc_40179E: ; CODE XREF: main+173j
.text:0040179E cmp [ebp+i], 3
.text:004017A2 jge short loc_4017DE
这一段是第二个for循环的准备、调整和条件判断部分,对应源代码为:
for(i=0; i<3; i++).text:004017A4 mov ecx, [ebp+i] ;ecx=i
.text:004017A7 mov edx, [ebp+ecx*4+pt_0] ;edx=pt[i]
.text:004017AB mov [ebp+var_58], edx ;var_58=pt[i]
.text:004017AE mov eax, [ebp+var_58] ;eax=var_58
.text:004017B1 mov [ebp+var_54], eax ;var_54=eax
.text:004017B4 cmp [ebp+var_54], 0 ;检查对象地址是否为NULL
.text:004017B8 jz short loc_4017D5 ;如果为空就不执行delete相关操作了,直接跳到4017D5,以NULL作为操作的返回值
.text:004017BA mov esi, esp ;保存堆栈当前位置
.text:004017BC push 1 ;把1入栈
.text:004017BE mov ecx, [ebp+var_54] ;取得pt[i]所指的对象的地址
.text:004017C1 mov edx, [ecx] ;通过对象地址取得虚函数表指针保存到edx中
.text:004017C3 mov ecx, [ebp+var_54] ;把pt[i]所指的对象的地址作为this
.text:004017C6 call dword ptr [edx+4] ;调用虚函数表中的偏移量为4的函数,也就是deleting链函数,它会调用析构函数和delete函数
.text:004017C9 cmp esi, esp
.text:004017CB call __chkesp ;检查堆栈是否恢复
.text:004017D0 mov [ebp+var_6C], eax ;把deleting函数的返回值(也就是对象的地址)保存到var_6C中
.text:004017D3 jmp short loc_4017DC ;跳转到for循环的调整部分
.text:004017D5 ; ---------------------------------------------------------------------------
.text:004017D5
.text:004017D5 loc_4017D5: ; CODE XREF: main+198j
.text:004017D5 mov [ebp+var_6C], 0 ;以NULL作为deleting函数的返回值
.text:004017DC
.text:004017DC loc_4017DC: ; CODE XREF: main+1B3j
.text:004017DC jmp short loc_401795 ;跳转到for循环的调整部分
这一段是循环体,通过虚函数表调用对象的deleting成员函数,并把1作为实参,把对象地址作为返回值。对应的源代码为:
delete pt[i];
/********************************************************
deleting成员函数不是析构函数,也不是delete运算符函数,但是它会调用这两个函数。
以1为实参的作用还有待后面分析。
********************************************************/.text:004017DE ; ---------------------------------------------------------------------------
.text:004017DE
.text:004017DE loc_4017DE: ; CODE XREF: main+182j
.text:004017DE mov [ebp+var_5C], 0 ;设置main函数的返回值为0
.text:004017E5 mov [ebp+var_4], 0FFFFFFFFh ;把异常id恢复成0FFFFFFFFh也就是-1
.text:004017EC lea ecx, [ebp+nb] ;把nb的地址传给this
.text:004017EF call j_Person___Person ;为nb对象调用析构函数Person::~Person()
.text:004017F4 mov eax, [ebp+var_5C] ;把main函数的返回值0保存到eax中
.text:004017F7 mov ecx, [ebp+var_C] ;取得原来的异常链入口地址
.text:004017FA mov large fs:0, ecx ;恢复原来的异常链入口
这一段恢复最初的异常处理链,设置main函数的返回值为0。.text:00401801 pop edi
.text:00401802 pop esi
.text:00401803 pop ebx
.text:00401804 add esp, 0ACh
.text:0040180A cmp ebp, esp
.text:0040180C call __chkesp
.text:00401811 mov esp, ebp
.text:00401813 pop ebp
.text:00401814 retn
.text:00401814 main endp
这一段恢复和检查堆栈,main函数结束。===================================================
总结一下,因为程序太长了。程序中有几个由编译器自动在用new创建对象的地方加上的if/else控制,检查new的结果是否为空。这里截取其中一个的图,另外几个类似。顺便回顾一下for循环的箭头图。
在整个main函数中,主要涉及的是异常处理、new/delete、构造函数、析构函数、虚函数表。
1、在这个例子中的异常处理是由系统自动加的,用来处理new/delete过程中可能产生的异常。
2、new和delete本身涉及的内存分配在这里没有深入,以后再去分析。new涉及到调用构造,在new之后自动就会调用构造。delete涉及到调用析构,但是咱们这里是虚析构,所以通过虚函数表指针vftable找到的scalar_deleting_destructor函数来调用,在那个函数中进行的析构函数和delete函数的调用,这个问题我会在下面继续分析。
3、构造函数本身的情况在上一次课4-8中已经分析过,但是这一次加入了初始化列表,加入了继承,所以下面会再以新加入的为重点来分析。
4、析构函数在以前的课程中没有涉及到,也在下面分析。
5、虚函数表是属于一个类的表,每个有虚函数的类,编译时都会产生一张表,所有属于这个类的对象共享一张表,多态机制就是靠它来实现的。每个对象中有一个隐含的指针成员指向这个虚函数表,所以每个对象都能访问到虚函数表。这个指针成员是如何指向虚函数表的,还有待下面对构造函数进行进一步的分析。虚函数表中是这个类中所有函数指针组成的一个数组,每个对象都能通过指针找到自己那个类的虚函数表,所以调用虚函数时总是能调用到对象自己所属的那个类中定义的虚函数,这就是多态的本质。
为此,后面继续分析父子类的scalar_deleting_destructor函数、构造函数、析构函数。
===================================================
★来分析scalar_deleting_destructor函数。三个类就会有三个,父类Person,子类Teacher和Student。两个子类的应该是一样的,咱们看Person类的和Teacher类的就行了。先看Person类的。找到Person___scalar_deleting_destructor_函数,把变量名var_4改名为this就行了。
---------------------------------------
.text:00401A90 Person___scalar_deleting_destructor_ proc near
.text:00401A90 ; CODE XREF: j_Person___scalar_deleting_destructor_j
.text:00401A90 ; .text:00401307j
.text:00401A90
.text:00401A90 var_44 = byte ptr -44h
.text:00401A90 this = dword ptr -4
.text:00401A90 arg_0 = dword ptr 8
.text:00401A90
.text:00401A90 push ebp
.text:00401A91 mov ebp, esp
.text:00401A93 sub esp, 44h
.text:00401A96 push ebx
.text:00401A97 push esi
.text:00401A98 push edi
.text:00401A99 push ecx ;保存传过来的对象地址
.text:00401A9A lea edi, [ebp+var_44]
.text:00401A9D mov ecx, 11h
.text:00401AA2 mov eax, 0CCCCCCCCh
.text:00401AA7 rep stosd
这一段是栈的局部变量区初始化。.text:00401AA9 pop ecx ;取得传过来的对象地址到ecx中
.text:00401AAA mov [ebp+this], ecx ;把ecx保存到this指针中
.text:00401AAD mov ecx, [ebp+this] ;又把this保存到ecx中
.text:00401AB0 call j_Person___Person ;调用析构函数
这一段直接调用Person的析构函数。
/******************************************
注意调析构的时候没有再次使用虚函数表。也就是说,所谓的虚析构,并不是析构函数是虚函数,而是这个类的scalar_deleting_destructor_函数是虚函数。
******************************************/.text:00401AB5 mov eax, [ebp+arg_0] ;取得传递过来的参数到eax中
.text:00401AB8 and eax, 1 ;检测最低位是否为1
.text:00401ABB test eax, eax
.text:00401ABD jz short loc_401ACB ;如果是0就跳过后面delete操作直接到401ACB
.text:00401ABF mov ecx, [ebp+this] ;又把this保存到ecx中
.text:00401AC2 push ecx ;把ecx也就是this入栈。
.text:00401AC3 call operator_delete ;调用delete运算符函数
.text:00401AC8 add esp, 4 ;恢复堆栈
这一段检测传递过来的参数是否低位是1,如果是1就调用delete来释放对象的空间。
/******************************************
* 对这个1,我这样推测:如果在源程序中是用delete来释放对象,在调用scalar_deleting_destructor_函数之前编译器先push 1,而不是用delete来释放对象的时候就把push 1换成push 0。可以把这个push的值当成一个布尔值,由它决定要不要调用delete运算符函数。不过,当不是用delete来释放对象的时候,对象应该不是用new创建的,系统能完全掌握它的生命期,完全可以直接调用那个类的构造函数,根本不需要用到这个虚的scalar_deleting_destructor_函数。不明白为什么编译器要费劲插入这么个函数来代替原来的虚析构函数。
* 注意到调用成员函数的时候是直接把对象地址放在寄存器ecx中就call的,但是调用operator_delete的时候却是把对象地址用push入栈后再call的,不知道为什么要这么处理。
******************************************/.text:00401ACB
.text:00401ACB loc_401ACB: ; CODE XREF: Person___scalar_deleting_destructor_+2Dj
.text:00401ACB mov eax, [ebp+this]
.text:00401ACE pop edi
.text:00401ACF pop esi
.text:00401AD0 pop ebx
.text:00401AD1 add esp, 44h
.text:00401AD4 cmp ebp, esp
.text:00401AD6 call __chkesp
.text:00401ADB mov esp, ebp
.text:00401ADD pop ebp
.text:00401ADE retn 4
.text:00401ADE Person___scalar_deleting_destructor_ endp
这一段把this作为函数的返回值,恢复并检查堆栈。
---------------------------------------再来看看Teacher类的。同样是找到Teacher___scalar_deleting_destructor_函数,把变量名var_4改名为this就行了。
---------------------------------------
.text:00401C60 Teacher___scalar_deleting_destructor_ proc near ; CODE XREF: .text:00401195j
.text:00401C60 ; j_Teacher___scalar_deleting_destructor_j
.text:00401C60
.text:00401C60 var_44 = byte ptr -44h
.text:00401C60 this = dword ptr -4
.text:00401C60 arg_0 = dword ptr 8
.text:00401C60
.text:00401C60 push ebp
.text:00401C61 mov ebp, esp
.text:00401C63 sub esp, 44h
.text:00401C66 push ebx
.text:00401C67 push esi
.text:00401C68 push edi
.text:00401C69 push ecx
.text:00401C6A lea edi, [ebp+var_44]
.text:00401C6D mov ecx, 11h
.text:00401C72 mov eax, 0CCCCCCCCh
.text:00401C77 rep stosd
.text:00401C79 pop ecx
.text:00401C7A mov [ebp+this], ecx
.text:00401C7D mov ecx, [ebp+this]
.text:00401C80 call j_Teacher___Teacher
.text:00401C85 mov eax, [ebp+arg_0]
.text:00401C88 and eax, 1
.text:00401C8B test eax, eax
.text:00401C8D jz short loc_401C9B
.text:00401C8F mov ecx, [ebp+this]
.text:00401C92 push ecx
.text:00401C93 call operator_delete
.text:00401C98 add esp, 4
.text:00401C9B
.text:00401C9B loc_401C9B: ; CODE XREF: Teacher___scalar_deleting_destructor_+2Dj
.text:00401C9B mov eax, [ebp+this]
.text:00401C9E pop edi
.text:00401C9F pop esi
.text:00401CA0 pop ebx
.text:00401CA1 add esp, 44h
.text:00401CA4 cmp ebp, esp
.text:00401CA6 call __chkesp
.text:00401CAB mov esp, ebp
.text:00401CAD pop ebp
.text:00401CAE retn 4
.text:00401CAE Teacher___scalar_deleting_destructor_ endp
---------------------------------------
可以看到Teacher类的这个函数跟Person类的这个函数唯一不同的地方就是调析构函数的时候调用的是Teacher类的析构函数。
由此咱们可以推测Student类的Student___scalar_deleting_destructor_函数跟这个函数唯一不同的地方也应该是调用析构函数的地方调用的是Student类的析构函数。★来看构造函数。还是三个类就会有三个,父类Person,子类Teacher和Student。两个子类的应该是一样的,咱们看Person类的和Teacher类的就行了。这一次咱们重点关注初始化列表、虚表指针和对父类构造函数的调用。
先来看Person类的构造函数Person(char const* name, int age)。把变量var_4改名为this,把arg_0改名为name,arg_4改名为age。
---------------------------------------
.text:004018A0 Person__Person proc near ; CODE XREF: j_Person__Personj
.text:004018A0
.text:004018A0 var_44 = byte ptr -44h
.text:004018A0 this = dword ptr -4
.text:004018A0 name = dword ptr 8
.text:004018A0 age = dword ptr 0Ch
.text:004018A0
.text:004018A0 push ebp
.text:004018A1 mov ebp, esp
.text:004018A3 sub esp, 44h
.text:004018A6 push ebx
.text:004018A7 push esi
.text:004018A8 push edi
.text:004018A9 push ecx
.text:004018AA lea edi, [ebp+var_44]
.text:004018AD mov ecx, 11h
.text:004018B2 mov eax, 0CCCCCCCCh
.text:004018B7 rep stosd
这一段是栈初始化。.text:004018B9 pop ecx
.text:004018BA mov [ebp+this], ecx
.text:004018BD -
K.IDA调试功能.形参和返回值类型(逆向)
2009-09-13
其实咱们在前面已经好多次涉及到函数了,所以这次的逆向试验要深入一点,有两个目的:
一是深入观察值传递时各种函数的参数和返回值的传递情况,
二是尝试逆向release模式。新建一个空的控制台工程ida5-2,再新建一个C++源文件function.cpp,内容为:
#include <iostream>
using namespace std;struct Person{
char name[20];
int age;
double weight;
};void welcome(char const* msg)
{
cout << msg << endl;
}
void welcome()
{
cout << "欢迎来到牛B论坛" << endl;
}
char inputChar()
{
welcome("请输入一个字母:");
char ch;
cin >> ch;
return ch;
}
char toUpper( char ch )
{
if(ch>='a'&&ch<='z')
ch+=('A'-'a');
return ch;
}
int mul( int x, float y )
{
float res = x * y;
return (int)res;
}
double div( double x, int y )
{
double res = x / y;
return res;
}
Person big(Person a, Person b)
{
if(a.age return b;
if(b.age return a;
return a.weight<?xml:namespace prefix = b.weight?b />}
ostream& operator<<(ostream& os, Person const& one)
{
os << one.name << ':' << one.age << ',' << one.weight;
return os;
}
int main()
{
Person s={"上善若水",23,62.5}, c={"陈宗权",35,54.3}, r;
char ch = toUpper('v');
int num = mul(5, 6.7F);
double res = div(123.4, 5);
r = big(s,c);
cout << ch << endl;
cout << num << endl;
cout << res << endl;
cout << r << endl;
return 0;
}字符类型用于比较的时候会先扩展成整数类型再比较。
●int mul( int x, float y )函数
.text:00401880 ; int __cdecl mul(int x, float y)
.text:00401880 mul proc near ; CODE XREF: j_mulj
.text:00401880
.text:00401880 var_44 = byte ptr -44h
.text:00401880 res = dword ptr -4
.text:00401880 x = dword ptr 8
.text:00401880 y = dword ptr 0Ch
.text:00401880
.text:00401880 push ebp
.text:00401881 mov ebp, esp
.text:00401883 sub esp, 44h
.text:00401886 push ebx
.text:00401887 push esi
.text:00401888 push edi
.text:00401889 lea edi, [ebp+var_44]
.text:0040188C mov ecx, 11h
.text:00401891 mov eax, 0CCCCCCCCh
.text:00401896 rep stosd
.text:00401898 fild [ebp+x] ;从整数x转换成单精度浮点数放到浮点寄存器中
.text:0040189B fmul [ebp+y] ;把浮点寄存器中的浮点数乘以y,结果还在FPU寄存器中
.text:0040189E fst [ebp+res] ;把相乘结果传送到变量res中
.text:004018A1 call __ftol ;把浮点数转换成整数类型,具体如何转换到调试时再看
.text:004018A6 pop edi
.text:004018A7 pop esi
.text:004018A8 pop ebx
.text:004018A9 mov esp, ebp
.text:004018AB pop ebp
.text:004018AC retn
.text:004018AC mul endp●double div( double x, int y )函数
.text:004018C0 ; double __cdecl div(double x, int y)
.text:004018C0 div proc near ; CODE XREF: j_divj
.text:004018C0
.text:004018C0 var_48 = byte ptr -48h
.text:004018C0 res = qword ptr -8
.text:004018C0 x = qword ptr 8
.text:004018C0 y = dword ptr 10h
.text:004018C0
.text:004018C0 push ebp
.text:004018C1 mov ebp, esp
.text:004018C3 sub esp, 48h
.text:004018C6 push ebx
.text:004018C7 push esi
.text:004018C8 push edi
.text:004018C9 lea edi, [ebp+var_48]
.text:004018CC mov ecx, 12h
.text:004018D1 mov eax, 0CCCCCCCCh
.text:004018D6 rep stosd
.text:004018D8 fild [ebp+y] ;从整数y转换成单精度浮点数放到浮点寄存器中
.text:004018DB fdivr [ebp+x] ;把x的值除以浮点寄存器中的浮点数,结果还在FPU寄存器中
.text:004018DE fst [ebp+res] ;把相除结果传送到变量res中,同时也保留在浮点寄存器中作为返回值
.text:004018E1 pop edi
.text:004018E2 pop esi
.text:004018E3 pop ebx
.text:004018E4 mov esp, ebp
.text:004018E6 pop ebp
.text:004018E7 retn
.text:004018E7 div endp●Person big(Person a, Person b)函数
.text:00401900 big proc near ; CODE XREF: j_bigj
.text:00401900
.text:00401900 var_44 = byte ptr -44h
.text:00401900 bigger_ptr = dword ptr -4 ;函数内临时用的一个指针,用来指向a和b中比较大的那个
.text:00401900 bigger_copy_ptr = dword ptr 8 ;编译器自动产生的作为参数传过来的指针,用来存放a和b中比较大的那个的拷贝
.text:00401900 a_name = byte ptr 0Ch ;形参Person结构变量a
.text:00401900 a_age = dword ptr 20h
.text:00401900 a_weight = qword ptr 24h
.text:00401900 b_name = byte ptr 2Ch ;形参Person结构变量b
.text:00401900 b_age = dword ptr 40h
.text:00401900 b_weight = qword ptr 44h
.text:00401900
.text:00401900 push ebp
.text:00401901 mov ebp, esp
.text:00401903 sub esp, 44h
.text:00401906 push ebx
.text:00401907 push esi
.text:00401908 push edi
.text:00401909 lea edi, [ebp+var_44]
.text:0040190C mov ecx, 11h
.text:00401911 mov eax, 0CCCCCCCCh
.text:00401916 rep stosd
.text:00401918 mov eax, [ebp+a_age]
.text:0040191B cmp eax, [ebp+b_age]
.text:0040191E jge short loc_401932 ;如果a.age>=b.age就跳转到401932,再进一步比较
.text:00401920 mov ecx, 8
.text:00401925 lea esi, [ebp+b_name] ;否则就以b作为结果,把b的数据复制到bigger_copy_ptr指向的地方
.text:00401928 mov edi, [ebp+bigger_copy_ptr]
.text:0040192B rep movsd
.text:0040192D mov eax, [ebp+bigger_copy_ptr] ;并且以复制品的地址bigger_copy_ptr作为返回值
.text:00401930 jmp short loc_401977 ;跳转到函数结束处
.text:00401932 ; ---------------------------------------------------------------------------
.text:00401932
.text:00401932 loc_401932: ; CODE XREF: big+1Ej
.text:00401932 mov ecx, [ebp+b_age]
.text:00401935 cmp ecx, [ebp+a_age]
.text:00401938 jge short loc_40194C ;如果b.age>=a.age意味着age相等,就跳转到40194C,比weight
.text:0040193A mov ecx, 8
.text:0040193F lea esi, [ebp+a_name] ;否则就以a作为结果,把b的数据复制到bigger_copy_ptr指向的地方
.text:00401942 mov edi, [ebp+bigger_copy_ptr]
.text:00401945 rep movsd
.text:00401947 mov eax, [ebp+bigger_copy_ptr] ;并且以复制品的地址bigger_copy_ptr作为返回值
.text:0040194A jmp short loc_401977 ;跳转到函数结束处
.text:0040194C ; ---------------------------------------------------------------------------
.text:0040194C
.text:0040194C loc_40194C: ; CODE XREF: big+38j
.text:0040194C fld [ebp+a_weight] ;把a.weight装入到浮点寄存器中
.text:0040194F fcomp [ebp+b_weight] ;跟b.weight比较,结果在浮点标志寄存器中
.text:00401952 fnstsw ax ;把浮点标志寄存器复制到ax寄存器中
.text:00401954 test ah, 1 ;检查ah中最后一位
.text:00401957 jz short loc_401961 ;为0表示a.weight大,跳转到401961
.text:00401959 lea edx, [ebp+b_name] ;不为0表示b.weight大,让bigger_ptr指向b
.text:0040195C mov [ebp+bigger_ptr], edx
.text:0040195F jmp short loc_401967 ;转到401967进行数据复制
.text:00401961 ; ---------------------------------------------------------------------------
.text:00401961
.text:00401961 loc_401961: ; CODE XREF: big+57j
.text:00401961 lea eax, [ebp+a_name] ;a.weight大就让bigger_ptr指向a
.text:00401964 mov [ebp+bigger_ptr], eax
.text:00401967
.text:00401967 loc_401967: ; CODE XREF: big+5Fj
.text:00401967 mov ecx, 8 ;数据复制,把bigger_ptr指向的Person结构变量的数据复制到bigger_copy_ptr指向的地方
.text:0040196C mov esi, [ebp+bigger_ptr]
.text:0040196F mov edi, [ebp+bigger_copy_ptr]
.text:00401972 rep movsd
.text:00401974 mov eax, [ebp+bigger_copy_ptr] ;并且以复制品的地址bigger_copy_ptr作为返回值
.text:00401977
.text:00401977 loc_401977: ; CODE XREF: big+30j
.text:00401977 ; big+4Aj
.text:00401977 pop edi
.text:00401978 pop esi
.text:00401979 pop ebx
.text:0040197A mov esp, ebp
.text:0040197C pop ebp
.text:0040197D retn ;函数结束
.text:0040197D big endp
这个函数的形参和返回类型是结构类型。实参是复制过来的,返回值也是复制过来的。有意思的是,返回类型是结构类型的时候,编译器会在调用前为存放返回值的结构变量分配好空间并且隐含把空间地址作为一个参数传递给函数,并且在函数中会把那个结构变量的地址传为机器码一级的返回值保存到eax中。
函数内部的跳转也不少,看看图形形式的流程图可能更清楚一些。●ostream& operator<<(ostream& os, Person const& one)函数
想把结构复习一下,在IDA中定义了一个Person结构。
.text:004019A0 operator__ proc near ; CODE XREF: j_operator__j
.text:004019A0
.text:004019A0 var_40 = byte ptr -40h
.text:004019A0 cout_ptr = dword ptr 8 ;第一个参数cout的引用
.text:004019A0 one_ptr = dword ptr 0Ch ;第二个参数one的引用
.text:004019A0
.text:004019A0 push ebp
.text:004019A1 mov ebp, esp
.text:004019A3 sub esp, 40h
.text:004019A6 push ebx
.text:004019A7 push esi
.text:004019A8 push edi
.text:004019A9 lea edi, [ebp+var_40]
.text:004019AC mov ecx, 10h
.text:004019B1 mov eax, 0CCCCCCCCh
.text:004019B6 rep stosd
.text:004019B8 mov eax, [ebp+one_ptr]
.text:004019BB mov ecx, [eax+Person.weight2]
.text:004019BE push ecx
.text:004019BF mov edx, [eax+Person.weight1]
.text:004019C2 push edx ;这两个push把one.weight入栈
.text:004019C3 push 2Ch ;‘,’字符入栈
.text:004019C5 mov eax, [ebp+one_ptr]
.text:004019C8 mov ecx, [eax+Person.age]
.text:004019CB push ecx ;one.age入栈
.text:004019CC push 3Ah ;‘:’字符入栈
.text:004019CE mov edx, [ebp+one_ptr]
.text:004019D1 push edx ;one.name入栈
.text:004019D2 mov eax, [ebp+cout_ptr]
.text:004019D5 push eax ;cout入栈
.text:004019D6 call j_std__operator__ ;调用operator<<(cout,one.name)
.text:004019DB add esp, 8
.text:004019DE push eax
.text:004019DF call sub_401235 ;调用operator<<(cout,':')
.text:004019E4 add esp, 8
.text:004019E7 mov ecx, eax
.text:004019E9 call sub_401145 ;调用cout.operator<<(one.age)
.text:004019EE push eax
.text:004019EF call sub_401235 ;调用operator<<(cout,',')
.text:004019F4 add esp, 8
.text:004019F7 mov ecx, eax
.text:004019F9 call sub_401186 ;调用cout.operator<<(one.weight)
.text:004019FE mov eax, [ebp+cout_ptr] ;以cout为返回值
.text:00401A01 pop edi
.text:00401A02 pop esi
.text:00401A03 pop ebx
.text:00401A04 add esp, 40h
.text:00401A07 cmp ebp, esp
.text:00401A09 call __chkesp
.text:00401A0E mov esp, ebp
.text:00401A10 pop ebp
.text:00401A11 retn
.text:00401A11 operator__ endp几个函数分析完毕了,接下来该进入调试了,观察各个函数调用时栈、参数和返回值的情况。
由于程序执行的时候实际上是由mainCRTStartup函数来调用的_main过程,_main过程直接通过jmp main实现转到main函数,所以先来看看这个mainCRTStartup函数。
.text:00425470 public mainCRTStartup
.text:00425470 mainCRTStartup proc near
.text:00425470
.text:00425470 var_1C= dword ptr -1Ch
.text:00425470 var_18= dword ptr -18h
.text:00425470 var_4= dword ptr -4
.text:00425470
.text:00425470 push ebp
.text:00425471 mov ebp, esp
.text:00425473 push 0FFFFFFFFh
.text:00425475 push offset stru_4720F8
.text:0042547A push offset __except_handler3
.text:0042547F mov eax, large fs:0
.text:00425485 push eax
.text:00425486 mov large fs:0, esp
.text:0042548D add esp, 0FFFFFFF0h
.text:00425490 push ebx
.text:00425491 push esi
.text:00425492 push edi
.text:00425493 mov [ebp+var_18], esp
.text:00425496 call ds:__imp__GetVersion@0 ; GetVersion()
.text:0042549C mov _osver, eax
.text:004254A1 mov eax, _osver
.text:004254A6 shr eax, 8
.text:004254A9 and eax, 0FFh
.text:004254AE mov _winminor, eax
.text:004254B3 mov ecx, _osver
.text:004254B9 and ecx, 0FFh
.text:004254BF mov _winmajor, ecx
.text:004254C5 mov edx, _winmajor
.text:004254CB shl edx, 8
.text:004254CE add edx, _winminor
.text:004254D4 mov _winver, edx
.text:004254DA mov eax, _osver
.text:004254DF shr eax, 10h
.text:004254E2 and eax, 0FFFFh
.text:004254E7 mov _osver, eax
.text:004254EC push 0
.text:004254EE call _heap_init
.text:004254F3 add esp, 4
.text:004254F6 test eax, eax
.text:004254F8 jnz short loc_425504
.text:004254FA push 1Ch
.text:004254FC call fast_error_exit
.text:00425501 ; ---------------------------------------------------------------------------
.text:00425501 add esp, 4
.text:00425504
.text:00425504 loc_425504: ; CODE XREF: mainCRTStartup+88j
.text:00425504 mov [ebp+var_4], 0
.text:0042550B call _ioinit
.text:00425510 call ds:__imp__GetCommandLineA@0 ; GetCommandLineA()
.text:00425516 mov _acmdln, eax
.text:0042551B call __crtGetEnvironmentStringsA
.text:00425520 mov _aenvptr, eax
.text:00425525 call _setargv
.text:0042552A call _setenvp
.text:0042552F call _cinit
.text:00425534 mov ecx, _environ
.text:0042553A mov __initenv, ecx
.text:00425540 mov edx, _environ
.text:00425546 push edx ; envp
.text:00425547 mov eax, __argv
.text:0042554C push eax ; argv
.text:0042554D mov ecx, __argc
.text:00425553 push ecx ; argc
.text:00425554 call _main
.text:00425559 add esp, 0Ch
.text:0042555C mov [ebp+var_1C], eax
.text:0042555F mov edx, [ebp+var_1C]
.text:00425562 push edx
.text:00425563 call exit
.text:00425563 mainCRTStartup endp
这个函数中先是异常处理,再是运行环境检查,堆初始化,io初始化,取得命令行参数,取得环境变量,C语言环境初始化,这些都是看不见的准备工作。
接下来是把命令行参数个数、命令行字符串序列、环境变量字符串序列作为参数调用main函数,main函数才得以执行。然后是用main函数的返回值作为参数调用exit函数终止程序。
在main函数的栈初始化之后(00401A4E处)按F2键设置断点,按F9键开始执行,在IDA View-ESP窗口中找到EBP,那就是main函数中局部变量的底部,往上的就是main中的局部变量。把它们的类型进行适当调整,如图:在图里EBP下面还有一个saved_fp,这是保存的进入main函数时EBP的值。再下面还有一个retaddr,值为425559h这是main函数执行完毕后要返回的地方,也就是mainCRTStartup函数中call _main指令的下一个指令。
因为这个程序中会涉及到小数运算,所以通过Debugger菜单下的FPU registers把浮点处理器寄存器窗口也显示出来,如图。准备就绪之后,按F8继续调试,咱们就发现IDA View-ESP窗口又定位到ESP,不是咱们关心的EBP的地方了。在这个窗口中找个空白地方,点右键,在Synchronize with中可以看到它是设置为与ESP同步的,改成EBP就行了。如图:
/******************************************************
不过只要继续调试,所以在这个窗口中改的类型都会丢失,不知道怎么能让它们保留下来。
******************************************************/
继续调试执行到下面语句:
.text:00401ACB call sub_401334
到这里已经完成两个结构变量s和c的初始化。可以在IDA View-ESP窗口中用+Q把c_name和s_name设置成Person结构类型,看到结构变量各个成员中的数据:
0013FF40 c_name Person <'陈宗权', 23h, 5.43e1>
0013FF60 s_name Person <'上善若水', 17h, 6.25e1>回到IDA View-EIP窗口准备继续调试。
这时候记录:
1.下一个指令的地址为.text:00401aD0。
2.ESP寄存器的值为0013FE84。
按F7单步调试,程序执行进入到调用的函数sub_401334内部,这个所谓的函数不是真正的函数,只是一个跳转语句,如下:
.text:00401334 sub_401334 proc near ; CODE XREF: main+9Bp
.text:00401334 jmp sub_401770
.text:00401334 sub_401334 endp
真正的函数是sub_401770,咱们暂时不理会。
看看ESP,现在变成0013FE80了,也就是有一个数据入栈了。在IDA View-ESP窗口中找到ESP,可以看到新入栈的数是00401aD0,正好是执行call指令之前记录的下一个指令的地址,也就是函数的返回地址。函数调用时总会把调用处下一个指令的地址入栈,这样在函数执行完毕后总是能通过栈中保存的这个地址正确返回到调用这个函数的地方的下一个指令继续执行。按F7跟踪到函数内部,执行完
.text:00401770 push ebp
.text:00401771 mov ebp, esp
.text:00401773 sub esp, 40h
.text:00401776 push ebx
.text:00401777 push esi
.text:00401778 push edi
.text:00401779 lea edi, [ebp+var_40]
.text:0040177C mov ecx, 10h
.text:00401781 mov eax, 0CCCCCCCCh
.text:00401786 rep stosd
这些是welcome函数保存寄存器和栈初始化部分,还是记录ESP,这里是0013FE30。
继续F7执行,
.text:00401788 push offset loc_4010FF
.text:0040178D push offset aNGb ; "欢迎来到牛B论坛"
.text:00401792 push offset std__cout
这几句执行完毕之后ESP是0013FE24。
下面是
.text:00401797 call j_std__operator__
它的下一指令的地址是0040179C。
继续F7执行跟踪到函数调用内部,ESP又减4了,变成了0013FE20,栈中又多了一个数据0040179C,就是调用这个函数的下一个指令的地址。
这个函数是输出一个字符串的。用Ctrl+F7执行到从这个函数返回,再观察ESP是0012FE24,恢复到了call指令之前的栈顶,但是,调用函数时传参数占用的栈空间还没有释放,可以看看栈:
0013FE24 dd 47EE80h ; .data:std__cout
0013FE28 dd 47101Ch ; .rdata:aNGb
0013FE2C dd 4010FFh ; .text:loc_4010FF
0013FE30 ; [BEGIN OF STACK FRAME sub_401770. PRESS KEYPAD "-" TO COLLAPSE]
接下来用
.text:0040179C add esp, 8
把cout和字符串地址aNGb从栈中清除,只留下loc_4010FF也就是endl还在栈中等待输出,eax寄存器中是返回值0047EE80,正好是cout的地址。
.text:0040179F mov ecx, eax
这个指令把eax中的数据也就是cout的地址保存到ecx中了。在调用对象的成员函数时,往往把对象地址保存到ecx中,到成员函数中直接把ecx中的数据传递给函数内的this指针,这样让this指向用来调用这个成员函数的对象。
又要调用函数了,
.text:004017A1 call j_std__basic_ostream_char_std__char_traits_char_____operator__
同样先记录ESP:0013FE2C和call指令的下一个指令的地址:004017A6。再继续F7跟踪进这个call指令,依然是ESP减少4,下一个指令的地址004017A6自动入栈了。继续跟踪这个运算符函数,无非是调用了传递过去的那个函数endl。用Ctrl+F7从函数中返回,ESP恢复到了0013FE30,它把参数占用的空间已经释放了,可见它是__stdcall的。继续Ctrl+F7返回到main函数。在.text:00401AE1处用F2设置断点。按F9执行,期间需要到控制台窗口输入一个字符,然后到这里停住了,记录ESP:0x0013FE84。继续F7执行:
.text:00401AE1 push 40D66666h
.text:00401AE6 push 5
.text:00401AE8 call j_mul
用来调用mul函数的两个参数6.7和5入栈了,咱们从前面的调试知道下一个指令的地址00401AED也入栈了。栈内数据如下:
0013FE78 ; [BEGIN OF STACK FRAME mul. PRESS KEYPAD "-" TO COLLAPSE]
0013FE78 dd 401AEDh ; main+BD
0013FE7C arg_0 dd 5
0013FE80 arg_4 dd 6.6999998
0013FE80 ; [END OF STACK FRAME mul. PRESS KEYPAD "-" TO COLLAPSE]
可以看到6.7F在计算机中的表示是6.6999998,不精确。
继续执行到下面内容时,开始仔细观察。目前FPU寄存器
.text:00401898 fild [ebp+arg_0]
执行完这一指令后,arg_0中的整数5对应的小数5.0装入到FPU的寄存器ST0中了。一些FPU状态也发生了变化,如图:可以跟前面的图ida523对比。
.text:0040189B fmul [ebp+arg_4]
这一指令应该是用arg_4中的6.7与ST0中的5.0相乘。
/************************************************************
我一直以为计算结果应该保存在ST0中,但是执行完这一指令后ST0中没有数据了,FPU中各个寄存器的状态也都没有变化,那结果存哪里了呢?按照指令本身的规定是结果保存到ST0中了,但是ST0显示没有数据,一片空白,估计是IDA的问题。
************************************************************/
.text:0040189E fst [ebp+var_4]
这一指令是把刚才相乘的结果传送到变量var_4中,结果var_4是变成了3.35e1,也就是33.5。不过ST0中还是没有数据,如图:下面又该调用函数_ftol了,这是系统内部的函数,同样来记录ESP:0013FE24,再下一指令地址:004018A6。继续F7调试:
.text:004018A1 call __ftol
进入这个函数依然是下一个指令地址入栈,跟调用别的函数一样。
.text:004221BC __ftol proc near ; CODE XREF: mul+21p
.text:004221BC
.text:004221BC var_C= qword ptr -0Ch
.text:004221BC var_4= word ptr -4
.text:004221BC var_2= word ptr -2
.text:004221BC
.text:004221BC push ebp
.text:004221BD mov ebp, esp
.text:004221BF add esp, 0FFFFFFF4h
这一步为三个局部变量留出空间,一共12个字节。
.text:004221C2 fstcw [ebp+var_2]
.text:004221C6 wait
这一步把FPU处理器控制字CTRL传送到变量var_2中。
.text:004221C7 mov ax, [ebp+var_2]
.text:004221CB or ah, 0Ch
.text:004221CE mov [ebp+var_4], ax
把控制字最高两位置1后保存到变量var_4中。
.text:004221D2 fldcw [ebp+var_4]
把var_4中的控制字数据重新装入到FPU控制寄存器中。
.text:004221D5 fistp [ebp+var_C]
把ST0中的数据的整数部分以8字节整数格式保存到变量var_C中并将ST0中数据出浮点栈,结果var_C中是0000000000000021h,也就是33.5的整数部分33。
.text:004221D8 fldcw [ebp+var_2]
恢复最初保存在var_2中的FPU控制字。
.text:004221DB mov eax, dword ptr [ebp+var_C]
.text:004221DE mov edx, dword ptr [ebp+var_C+4]
把变量var_C中保存的8个字节的整数数据保存到edx和eax寄存器中作为返回值。
.text:004221E1 leave
.text:004221E2 retn
.text:004221E2 __ftol endp
到这里__ftol函数结束。
转换过程跟踪完毕,回到上一级函数mul,继续返回到main函数,执行main函数中的指令:
.text:00401AED add esp, 8
把调用mul函数时传递的两个参数从栈中清除。
.text:00401AF0 mov [ebp+num], eax
把调用mul函数的结果保存到变量num中。结果只保留了低32位,其中高32位在edx中,丢弃了。
后面是继续调用其它几个函数,跟前面已经进行过的分析大同小异,不再跟踪调试了。关于函数调用,总结一下。
/***************************************************
调用函数时,一般实参是按反序入栈的,调用成员函数时对象地址则是通过寄存器ecx来传递的。
执行到调用一个near函数的call指令的时候会自动把call的下一个指令的地址入栈以便函数执行完毕之后能据此返回到call后面的指令继续执行。
在函数中,首先是push ebp,然后把esp传递给ebp,之后就以ebp为各个局部变量(包括形参变量)的基地址。在函数结束的时候再把ebp回传给esp,然后pop ebp恢复上一级函数中各局部变量的基地址。
函数的返回值一般保存在eax中(也可能只有ax或者al有效),返回值为小数时一般结果在ST0中,返回结构变量则是在调用时就开辟好存放返回值的空间,把这个空间的地址作为第一个参数传递到函数中,在函数结束时把要返回的数据复制到这个空间中,并且用eax返回这个空间地址。
***************************************************/下面开始第二个任务,逆向release模式的程序,跟debug模式的程序作个对比。
同样是这个程序,用release模式构建成ida5-2.exe,然后用IDA加载。
现在来看看main函数吧,第一次分析release的代码,我差点累死了。.text:004011F0 ; int __cdecl main(int argc, const char **argv, const char *envp)
.text:004011F0 _main proc near ; CODE XREF: start+AFp
.text:004011F0
.text:004011F0 ch_= dword ptr -6Ch
.text:004011F0 res= qword ptr -68h
.text:004011F0 c_name_0= dword ptr -60h
.text:004011F0 c_name_4= word ptr -5Ch
.text:004011F0 c_name_6= byte ptr -5Ah
.text:004011F0 c_name_7= dword ptr -59h
.text:004011F0 c_name_11= dword ptr -55h
.text:004011F0 c_name_15= dword ptr -51h
.text:004011F0 c_name_19= byte ptr -4Dh
.text:004011F0 c_age= dword ptr -4Ch
.text:004011F0 c_weight1= dword ptr -48h
.text:004011F0 c_weight2= dword ptr -44h
.text:004011F0 s_name_0= dword ptr -40h
.text:004011F0 s_name_4= dword ptr -3Ch
.text:004011F0 s_name_8= byte ptr -38h
.text:004011F0 s_name_9= dword ptr -37h
.text:004011F0 s_name_13= dword ptr -33h
.text:004011F0 s_name_17= word ptr -2Fh
.text:004011F0 s_name_19= byte ptr -2Dh
.text:004011F0 s_age= dword ptr -2Ch
.text:004011F0 s_weight1= dword ptr -28h
.text:004011F0 s_weight2= dword ptr -24h
.text:004011F0 r= byte ptr -20h
.text:004011F0 argc= dword ptr 8
.text:004011F0 argv= dword ptr 0Ch
.text:004011F0 envp= dword ptr 10h
上面是变量表,可以看到release模式下不再有多余的变量了。当然,它还是把Person结构的char[20]成员name拆成了多个部分,以便进行初始值的传送操作。.text:004011F0
.text:004011F0 push ebp
.text:004011F1 mov ebp, esp
.text:004011F3 and esp, 0FFFFFFF8h ;让栈esp的位置8字节对齐
.text:004011F6 sub esp, 6Ch ;直接开出局部变量的空间
这一段开出局部变量的空间,不再象debug模式下那样多开40h字节空间,而且也不再用0xCC填充局部变量的空间。.text:004011F9 mov eax, dword ptr asc_4160D8 ; "上善若水",9字节
.text:004011FE mov ecx, dword ptr asc_4160D8+4
.text:00401204 mov dl, byte ptr asc_4160D8+8 ;dl中保存初始值字符串"上善若水"的第9个字节
.text:0040120A mov [esp+6Ch+s_name_0], eax ;初始化s.name的第1-4字节
.text:0040120E mov [esp+6Ch+s_name_4], ecx ;初始化s.name的第5-8字节
.text:00401212 mov ecx, dword ptr aI ; "陈宗权",7字节
.text:00401218 xor eax, eax ;eax清零
.text:0040121A mov [esp+6Ch+c_name_0], ecx ;初始化c.name的第1-4字节
.text:0040121E mov [esp+6Ch+s_name_9], eax ;初始化s.name的第10-13字节填充0
.text:00401222 xor ecx, ecx ;ecx清零
.text:00401224 mov [esp+6Ch+s_name_13], eax ;初始化s.name的第14-17字节填充0
.text:00401228 mov [esp+6Ch+c_name_7], ecx ;初始化c.name的第8-11字节填充0
.text:0040122C mov [esp+6Ch+s_name_17], ax ;初始化s.name的第18-19字节填充0
.text:00401231 mov [esp+6Ch+c_name_11], ecx ;初始化c.name的第12-15字节填充0
.text:00401235 push ebx
.text:00401236 mov [esp+70h+s_name_8], dl ;初始化s.name的第9字节
.text:0040123A mov dx, word ptr aI+4 ;dx中保存初始值字符串"陈宗权"的第5-6个字节
.text:00401241 mov [esp+70h+s_name_19], al ;s.name的第20字节填充0
.text:00401245 mov [esp+70h+s_weight1], eax ;初始化s.weight的低4字节(正好是0,所以用eax),也就是62.5的低4字节
.text:00401249 mov al, byte ptr aI+6 ;al中保存初始值字符串"陈宗权"的第7个字节
.text:0040124E push esi
.text:0040124F mov [esp+74h+c_name_15], ecx ;初始化c.name的第16-19字节填充0
.text:00401253 push edi
.text:00401254 mov [esp+78h+s_age], 23 ;初始化s.age为23
.text:0040125C mov [esp+78h+s_weight2], 404F4000h ;初始化s.weight高4字节为62.5的高4字节
.text:00401264 mov [esp+78h+c_name_4], dx ;初始化c.name的第5-6字节
.text:00401269 mov [esp+78h+c_name_6], al ;初始化c.name的第7字节
.text:0040126D mov [esp+78h+c_name_19], cl ;初始化c.name的第20字节填充0
.text:00401271 mov [esp+78h+c_age], 35 ;初始化c.age为35
.text:00401279 mov [esp+78h+c_weight1], 66666666h ;初始化c.weight低4字节为54.3的低4字节
.text:00401281 mov [esp+78h+c_weight2], 404B2666h ;初始化c.weight高4字节为54.3的高4字节
这一段是初始化Person结构变量s和c,可以看出初始化的顺序很乱,而且还夹杂着保存了3个寄存器ebx、esi、edi入栈。编译器出于优化的目的打乱了操作顺序,这样一来可读性就很差了,给逆向带来了很大的困难。.text:00401289 call welcome ; 显示欢迎信息
.text:0040128E call inputChar ; 从键盘输入一个字符,结果保存在eax中
.text:00401293 push eax
.text:00401294 call toUpper ; 把从键盘输入的字符转换成大写字符,结果保存在eax中
.text:00401299 push 40D66666h ; float 6.7
.text:0040129E push 5 ; int
.text:004012A0 mov byte ptr [esp+84h+ch_], al ; 把toUpper的结果保存在变量ch中
.text:004012A4 call mul
.text:004012A9 push 5 ; int
.text:004012AB push 405ED999h ; 123.4 part1
.text:004012B0 push 9999999Ah ; 123.4 part2
.text:004012B5 mov ebx, eax ; mul的结果由eax转存到ebx
.text:004012B7 call div
.text:004012BC sub esp, 8
.text:004012BF mov ecx, 8 ; 为用rep movsd指令传递结构变量做准备(20h/4=8)
.text:004012C4 fstp [esp+98h+res] ; div的结果保存到res中
这一段是调用welcome函数、inputChar函数、mul函数和div函数,跟debug模式下没有太大的区别。.text:004012C8 lea esi, [esp+98h+c_name_0] ; 传递Person结构变量c作参数
.text:004012CC mov edi, esp
.text:004012CE rep movsd
.text:004012D0 sub esp, 20h
.text:004012D3 mov ecx, 8 ; 为用rep movsd指令传递结构变量做准备(20h/4=8)
.text:004012D8 lea esi, [esp+0B8h+s_name_0] ; 传递Person结构变量s作参数
.text:004012DC mov edi, esp
.text:004012DE lea edx, [esp+0B8h+s_name_0] ; 把Person结构变量s的地址保存到edx中
.text:004012E2 rep movsd
.text:004012E4 push edx ; 再把Person结构变量s的地址作参数(为什么啊?)
这里在为调用big正常传递完参数s和c之后,又把s的地址作为参数传递到栈中了,难道返回结构变量的函数都会如此?.text:004012E5 call big
.text:004012EA mov ecx, 8 ; 为用rep movsd指令传递结构变量做准备(20h/4=8)
.text:004012EF mov esi, eax ; 把返回值的地址传递给esi
.text:004012F1 mov eax, [esp+0BCh+ch_] ; 把ch保存到eax中为后面的输出做准备
.text:004012F5 lea edi, [esp+0BCh+s_name_0] ; 把返回值复制到Person结构变量s中(s中原来的值就被覆盖了啊)
.text:004012F9 rep movsd
这一段调用big函数并把返回值复制到结构变量s中覆盖了s中原来的值,因为后面不再用s了,所以这样节省一个变量的空间。.text:004012FB mov ecx, 8






















