C++反汇编与逆向分析技术揭秘小笔记

时间:2023-02-11 23:36:51

题记:分析能力很大程度体现在分析效率上,语法细节乃至数据代码间的复杂性不应该成为主要困难

 

所谓逆向分析,不应该只是单纯的逆向代码,真正目标应该是逆向出代码作者的思维、意图

即透过代码分析意图 

 

第二章:

编译器一旦发现代码中有浮点计算,则初始化浮点寄存器

汉字编码方式有些特殊,ascii和unicode都有与之匹配的编码格式,所以即使汉字使用2个字节,在char型字符串中任然可以显示中文,但这2个版本在内存中编码是不同的

C++为简化指针操作,对指针操作进行了封装,产生了引用类型。实际上引用类型就是指针类型,只不过它用于存放地址的内存空间对用户隐藏,引用通过编译器实现寻址,而指针需要手动寻址(解引用)。

const实现的常量是个伪常量,由编译器实现,在汇编中和普通变量没有区别。

 

第三章:

对于C++的全局对象和静态对象必须在main函数前构造,其实际就是遍历调用一个有构造函数的指针数组。

 

第四章:

如果有符号数和无符号数混除,结果则是无符号的,有符号最高位被当作数据位对待。

数据进位只是进位后的1位数据不在自身的存储空间中,而在标志位CF中。

由于数据进位破坏了有符号数最高位--符号位 就是溢出,所以溢出只针对有符号数。

编译器工作阶段:预处理--->词法分析--->语法分析--->语义分析--->中间代码生成--->目标代码生成

 

第五章:

C语言根据代码行的位置------低行数对应低地址,高行数对应高地址,因为有时会使用标号相减得到代码段等地址

if else

jxx  ELSE_BEGIN

IF_BEGIN:

....

IF_END:

JMP ELSE_END

ELSE_BEGIN:

....

ELSE_END

switch 优化:跳转表。  非线性索引表---索引到一个跳转表,最大256个索引(case),每个字节一个索引。判定树。混合优化。

do/while/for

所有的循环都能优化成do循环。

 

第六章:

程序调试版本栈平衡检测:__chkesp-----即对比esp,ebp来检测是否平衡,不平衡则报错。

_fastcall调用方式只使用了ecx,edx存第一个和第二个参数

 

第七章:

对于作用域{ }的实现,内部局部变量的生命周期和函数一致,但是编译器靠编译前检查语法,限制块外代码对其访问。

对于全局变量,先定义的变量在低地址,后定义的在高地址

全局静态变量的实现是靠编译器限制外部源码文件访问的。

局部静态变量的初始化工作由编译器执行,如果初始化是个变量,就用一个字节中的一位来记录这个局部静态变量是否初始化来达到静态局部变量的行为。

局部静态变量对其他作用域不可见的实现是通过名称粉碎法,编译期将静态变量重新命名。C++函数重载也是如此,先粉碎函数名称再组合出新的名称。

内存中堆结构的每个节点都是使用双向链表的形式存储的。

 

第八章:

数组名本身就是常量地址,可直接对数组名所代替的地址值进行偏移计算,所以指针寻址效率比下标低

例:

指针:

mov eax,dword ptr [ebp-4]

movsx edx,byte ptr [eax]

数组下标:

movsx edx,byte ptr [ebp-0ch]

 

第九章:

对于C++类,对象的大小只包含数据成员,类成员函数属于执行代码不属于类对象数据。

空类实际长度为一个字节,因为需要实例化,如果不占空间,空类就无法获得实例对象地址,this指针就失效。

 

结构体对齐规则:

结构体中数据成员类型最大值为M,指定的对齐值为N,那么实际对齐值为min(M,N)。对于嵌套结构体,在对齐值计算中,嵌套结构体参加计算的是嵌套结构体所使用的对齐值。

 

类成员函数当使用其他调用约定时,this指针不再使用ecx传递,而是改用栈传递,相当于函数的第一个参数。

静态数据成员在反汇编中很难被识别,因为其展示形态与全局变量相同,很难被还原成对应的高级代码。酌情处理。

类对象中的数据成员传参顺序为:最先定义的数据成员最后压栈,最后定义的数据成员最先压栈。

事实上有的时候编译器并不会添加默认的构造函数。

参数为对象的函数传参时,汇编中在栈中为对象分配内存作为参数,就像普通的参数传递一样,只是对象的大小可能比较大

返回值为对象的函数时候,汇编中调用者为对象分配一个存储返回对象和一个存储临时对象的栈空间并将空间地址作为参数传递给调用的函数,进入函数后返回时,函数将要返回的对象复制到返回对象空间后返回,然后调用者将对象从返回对象空间复制到临时对象空间,一个临时对象就这样生成了,之后要对返回值进行何种操作也就是对这个临时对象数据进行操作。//当有拷贝构造函数时会省去一些步骤

 

在访问对象成员时,其寻址方式颇为特别,使用的是寄存器相对间接访问方式。这种访问方式可以作为识别对象的必要条件,但是还需要考察成员类型。

 

 

第十章:

所谓默认构造函数其实就是对原对象的直接数据拷贝。

 

全局对象初始化

mainCRTStartup->_initterm(_xc_a, _xc_z )//一个遍历指针数组并调用的程序(),2008有个initterm_e,功能差不多,应该就是本来的第一次调用换成这个函数了
->(跟踪后貌似第一次pre_cpp_init,可能是对数据初始化以及执行_onexitinit初始化终止数组,_onexit负责维护),第一次之后就是些对象的初始化)全局对象构造代理函数

全局对象构造代理函数:
...
...
call $ ;调用对象的构造函数初始化全局对象
call $ ;登记析构函数地址
{
 ...
 ...
 push offset 析构代理函数   //和构造代理一样,其中会调用析构函数
 call atexit //查找全局对象,构造析构函数虚函数表的关键断点
 ...
 ...
}

代理函数存在的意义:由于存在各种形形色色的参数和调用约定的构造函数,而代理类型统一指定为PVFV,因此能通过一个指针数组统一管理,代理函数可以保证各种参数问题,栈平衡等。

主函数退出时:
exit->doexit(倒序遍历终止函数数组并调用,类似前面初始化,析构函数调用析构全局对象)

编译器何时提供默认构造函数:

本类,成员对象或父类存在虚函数;

父类或本类定义的成员对象带有构造函数;

对象数组比较特殊,数组前4个字节存放对象计数,堆对象由专门的构造和析构代理函数

调用构造函数返回值其实为对象首地址,析构函数无返回值。

编译器为每个全局对象分别生成构造代理函数,由代理函数去调用各类形形色色的参数和约定的构造函数。

 

第十一章:

虚表指针被定义在对象首地址的前4字节处,因此虚函数必须作为成员函数使用。虚标为指向一个函数指针数组。

注意虚函数的实现

 

第十二章:

C++语法规定的访问控制权限仅限于编译层面,在编译过程中由编译器进行语法检查,因此访问控制不会影响对象的内存结构。

父类数据成员被排列在最前端的目的是为了在添加派生类后方便子类使用父类的成员数据,,并且可以将子类指针当父类指针使用。

由于如果有虚函数,编译器会在构造函数插入虚表指针赋值代码,所以在每个构造或析构函数中续表指针指向当前构造或析构函数,所以多态会失效。

以写入虚表的指令为界限,可以粗略划分出父类的构造函数和子类的构造函数的实现代码。

由于存在虚表,就算类中没有定义析构函数,编译器也会产生默认的析构函数。

有几个父类便会出现对应个数的虚表指针(虚基类除外),编译器会根据每个父类在对象中占用的空间位置,对应地传入各个父类部分的首地址作为this指针;

类在内存中的组织基本按照常识先定义在对象内存的前面,后定义在后面,基类在前面子类在后面顺序排列,只有棱形继承较特殊(虚基类在内存中排在最后,而且每个父类还添加了一个指向偏移记录的成员)。

棱形结构比较复杂,不想写了....反正书上有

 

第十三章:

对于C++的异常处理try/cathch/throw,抛出异常是调用CxxThrowException函数(其中有调用了api  RaiseException)来抛出异常。

每个catch块的返回值为当前try语句对应的所有catch的末尾地址

try和catch块分别对应了一个连接到许多结构体的复杂结构,最后都同样指向一个TypeDescriptor结构,即类型描述结构,其中国的字符串信息记录了抛出的异常类型信息,如.H表示int,.M表示float,.D表示char,.N表示double,对于结构体和类,这个字符串中包含了类名称。