加密与解密
1.win32API函数
API函数提供应用程序运行所需要的窗口管理、图形设备接口、内存管理等服务功能。这些功能以函数库的形式组织在一起,形成了Windows应用程序编程接口,简称“Win API”。API函数的下面是Windows操作系统核心(开发人员),上面是WIndows应用程序(用户)。
Windows运转的核心是动态链接。Windows提供了应用程序可利用的函数调用,这些函数采用动态链接库(DLL)实现。
早期重要DLL:
-
Kernel:操作系统核心功能服务,包括进程与线程控制、内存管理、文件访问等。
-
Uesr:负责处理用户接口,包括键盘和鼠标输入、窗口和菜单管理等。
-
GDI:图形设备接口,允许程序在屏幕和打印机上显示文本和图形。
3.WOW64
是64位操作系统的子系统,可以使大多数32位应用程序在不进行修改的情况下运行在64位操作系统上。
4.Windows消息机制
Windows有两种消息队列:一种是系统消息队列;另一种是应用程序消息队列。当一个事件发生时,WIndows先将输入的消息放入系统消息队列,再讲输入的消息复制到对应的应用程序队列,应用程序检索消息并将消息发送给相应的窗口函数。
消息具有非抢先性,即不论事件的急与缓,总是按顺序先后排队(一些系统消息除外)。
常用Windows消息函数:
(1)SendMessage函数:
调用一个窗口的窗口函数,将一个消息发给那个窗口。只有消息处理完毕,函数才会返回。
返回值:由具体的消息决定。如果消息投递成功,则返回“TRUE”
(2)WM_COMMAND消息
当用户从菜单或按钮选择一个命令或者一个控键时该消息被发送给它的父窗口,或当一个快捷键被释放时发送。
返回值:如果应用程序处理这条消息,则返回值为零。
(3)WM_DESTROY消息
当窗口被销毁时发送该消息。
返回值:如果应用程序处理这条消息,则返回值为零。
5.虚拟内存
电脑中所运行的程序均需经由内存执行,若执行的程序占用内存很大或很多,则会导致内存消耗殆尽。为了解决这个问题,Windows运用了虚拟内存技术,即拿出一部分硬盘空间来充当内存使用,这部分空间即称为虚拟内存。
重点:
-
应用程序不会直接访问物理地址。
-
虚拟内存管理器通过虚拟地址的访问请求来控制所有的物理地址访问。
-
每个应用程序都有独立的4GB寻址空间,不同应用程序的地址空间是彼此隔离的。
-
DLL程序没有“私有”空间,它们总是被映射到其他应用程序的地址空间中,作为其他应用程序的一部分运行。原因是DLL不与其他应用程序处于同一个地址空间就无法调用它。
二.32位软件逆向技术
1.启动函数
编写Win32应用程序时,必须在源码中实现一个WinMain函数,但Windows程序执行不是从winMain函数开始的。对Visual C++来说,首先调用C/C++运行时启动函数,该函数负责对C/C++运行库进行初始化。
2.函数
程序都是由具有不同功能的函数组成的,因此在逆向分析中我们可以将分析重点放在函数的识别和参数的传递 。一个函数包括函数名、入口参数、返回值、函数功能等部分。
函数的识别
程序通过调用程序来调用函数,在函数执行后又返回调用程序继续执行。而函数返回的地址,在调用函数的代码中给出。在大多数情况下,编译器都使用call和ret指令实现函数的调用及返回调用位置。因此,可以通过定位call指令或ret指令来识别函数。
函数的参数
函数传递参数有三种方式,分别是栈方式、寄存器方式和通过全局变量进行隐含参数传递的方式。如果通过栈传递,需要定义参数在栈中的顺序,并约定函数被调用后谁来平衡栈。如果参数通过寄存器传递,要确定参数存放哪个寄存器中。
(1)利用栈传递参数
栈是一种后进先出的存储区。调用函数时,调用者依次把参数压栈,然后调用函数。参数传递有两个很重要的问题:当参数个数多于1个时,按照什么顺序把参数压入栈?函数结束后,由谁来平衡栈?这些都必须有约定。这种未来实现函数调用而建立的协议称为调用约定。不同的语言定义了不同的调用约定,常用的调用约定如下表
(2)利用寄存器传递参数
寄存器传递参数的方式没有标准,但绝大多数编译器都在不对兼容性进行声明的情况下遵循相应的规范,即Fastcall。
(3)名称修饰约定
为了允许使用操作符和函数重载,C++编译器往往会按照某种规则改写每一个入口点的符号名,从而允许同一个名字(具有不同的参数类型或者不同的作用域)有多个用法且不会破坏现有的基于C的链接器。此技术通常称为名称改编或者名称修饰。
3.函数的返回值
函数被调用执行后,将向调用者返回一个或多个执行结果,称为函数返回值。
(1)用return操作符返回值
一般情况下,函数的返回值放在eax寄存器中返回,如果处理结果的大小超过eax寄存器容量,其高32位就会放到edx寄存器中。
(2)通过参数按传引用方式返回值
给函数传递参数的方式有两种,分别是传值和传引用。进行传值调用时,会建立参数的一份复本,并把它传给调用函数,在调用函数中修改参数值的复本不会影响原始的变量值。传引用调用允许调用函数修改原始变量的值。调用某个函数,当把变量的地址传递给函数时,可以在函数中用间接引用运算符修改调用函数内存单元中该变量的值。
4.数据结构
数据结构是计算机存储、组织数据的方式。常见的数据结构有栈、队列、数组、链表等等。
(1)局部变量
局部变量是函数内部定义的一个变量,其作用域和生命周期局限于所在函数内。局部变量分配空间时经常使用栈和寄存器。
①利用栈存放局部变量
②利用寄存器存放局部变量
(2)全局变量
全局变量作用于整个程序,它一直存在,放在全局变量的内存区中。全局变量可以被同一文件当中的所有函数修改,如果某个函数修改了全局变量的值就能影响其他函数。
(3)数组
数组是相同数据类型的元素的集合,它们在内存中按顺序连续放在一起,可存在于栈、数据段及动态内存中。其寻址在汇编状态下用“基址+偏移量”实现,称为间接寻址。
5.虚函数
虚函数是在程序运行时定义的函数,虚函数的地址不能在编译时确定,只能在调用即将进行时确定。所有对虚函数的引用通常都放在一个专用数组虚函数表(VTBL)中。调用虚函数时,程序先取出虚函数表指针(VPTR),得到虚函数表的地址,再根据这个地址到虚函数表中取出该函数的地址,最后调用该函数。
6.控制语句
在高级语言中,用IF—THEN—ELSE、SWITCH—CASE等语句,来构建程序的判断流程,但其汇编代码比较复杂,我们会看到cmp等指令后跟各类跳转指令,例如jz、jnz。
(1)IF—THEN—ELSE语句
此语句编译成汇编代码后,整数用cmp指令进行比较,浮点值用fcom、fcomp等指令进行比较。IF—THEN—ELSE编译后,汇编代码通常为
(2)SWITCH—CASE语句
SWITCH语句是多分支语句。编译后的SWITCH语句,实质就是多个IF—THEN的嵌套组合。编译器会将SWITCH语句编译成一组由不同的关系运算组成的语句。
7.转移指令机器码的计算
-
短转移(short jump):无条件转移和条件转移的机器码均为2字节,转移的范围-128~127字节。
-
长转移(long jump):无条件转移的机器码为5字节,条件转移的机器码为6字节。因为条件转移要用2字节表示其转移类型(例如je、jg、jns),其他4字节表示转移偏移量,而无条件转移只需1字节表示转移类型(jmp),4字节表示转移偏移量。
-
子程序调用指令(call):call调用有两类。一类类似于长转移;另一类调用的参数涉及寄存器、栈等值。
8.文本字符串
(1)字符串存储格式
在程序中,一般将字符串作为字符数组来处理。但不同的编程语言,字符存储格式是不同的。
①C字符串
C字符串也称“ASCIIZ字符串”,应用于Windows和UNIX操作系统中。“Z”表示以“\0”为结束标志。“\0”代表ASCII码为0的字符,因为ASCII码为0的字符不是可以显示的字符,而是“空操作符”。
②DOS字符串
在DOS中,输出行函数以“$"字符作为终止字符。由于DOS早已淘汰,目前很少见到这类字符串了。
③PASCAL字符串
PASCAL字符串没有终止符,但在字符串头部定义了1字节,用于指示当前字符串的长度。由于只用了1字节来表示字符串的长度,所以字符串不能超过255个字符。字符串中的每个字符都属于AnsiChar(标准字符类型)。
④Delphi字符串
为克服传统PASCAL字符串的局限性,32位Delphi
增长了对长字符串的支持。
-
双字节Delphi字符串:表示长度的字段扩展为2字节,使字符串的最大长度达到65535,
-
四字节Delphi字符串:表示长度的字段扩展为4字节,使字符串长度达到4GB。
(2)字符寻址指令
8086系统支持寄存器直接寻址与寄存器间接寻址等。与字符指针处理相关的指令有mov、lea等。
mov指令将当前指令所在内存复制并放到目的寄存器中,其操作数可以是常量,也可以是指针。
lea的意思是“装入有效地址”,它的操作数就是地址,所以“lea eax [addr]”就是将表达式addr的值放入eax寄存器。
(3)字母大小写转换
大写字母的ASCII码范围是41h~5Ah,小写字母的ASCII码范围是61h~7AH,大小写的转换就是将原ASCII码的值加/减20。
(4)计算字符串长度
高级语言中会有特定函数计算字符串长度。如C语言中Strlen()。汇编代码如下
三.64位软件逆向技术
1.寄存器
2.函数
(1)栈平衡
栈中存储的数据主要包括局部变量、函数参数、函数返回地址等。每调用一个函数,会根据函数的需要申请相应的栈空间。当函数调用完成,需要释放刚才申请的栈空间,保证栈顶与函数调用前位置一致。此过程称为栈平衡。
在程序运行时,栈内存空间被各函数重复利用,如果函数调用只申请栈空间而不释放它,随着函数调用次数的增加,栈内存会很快耗光,程序因此无法正常运行。平衡栈的操作,目的是保证函数调用后的栈顶位置和函数调用前的位置一致,过多或过少地释放栈空间都会影响其他函数对栈空间数据的操作。
(2)启动函数
(3)调用约定
x64应用程序只有1种寄存器快速调用约定。前四个参数使用寄存器传递,超过四个,多余的参数就放在栈里,入栈顺序从右到左。前四个参数存放的寄存器是固定的,分别是RCX、RDX、R8、R9。
(4)函数返回值
64位环境下,使用RAX寄存器来保存函数返回值。返回值类型为浮点型使用MMX0寄存器返回。RAX寄存器可以保存8字节的数据。当返回值大于8字节时,可以将栈空间的地址作为参数间接访问,进而达到目的。
3.数据结构
(1)局部变量
局部变量是函数内部定义的变量,存放的内存区域为栈区,生命周期为进入函数时分配、函数返回时释放。函数在入口处申请了预留栈空间和局部变量空间,预留栈空间在低地址,局部变量空间在高地址。
(2)全局变量
全局变量的地址在编译时就会固定下来,因为一般会用固定的地址去访问全局变量。全局变量的地址也是先定义的在低地址,后定义的在高地址。
(3)数组
数组中的数据在内存中的存储是线性连续,数组中的数据是从低地址到高地址顺序排列的。
①数组寻址公式
编译器访问数组元素,先定位数组元素的地址,再访问数组元素的内容。
一维数组的寻址公式:数组元素的地址 = 数组首地址 + sizeof(数组类型)×下标
e.g. 数组 int ary[3] = {1,2,3} 假设数组首地址为0x1000,访问ary[2],由寻址公式得,0x1000+sizeof(int)×2=1000+4×2=0x1000+8=0×1008.
多维数组也可以看成两个一维数组。
多维数组的寻址公式:数组首地址+sizeof(数组类型)×下标1+sizeof(数组类型)×下标2
②一维数组
当访问的数组下标为常量时,编译器会根据一维数组寻址公式直接计算出数组相对于数组首地址的偏移。二维数组同样。 若数组下标未知,会用一维数组寻址公式定位数组元素。
4.控制语句
(1)if语句
if语句的功能是对表达式的结果进行判定,根据表达式结果的真假跳转到对应语句执行。
-
特征识别:首先会有一个jxx指令向下跳转,且跳转目的if_end中没有jmp指令。
-
图形识别:在逆向工具中,为了方便表示跳转的位置使用虚线箭头表示条件跳转jxx,使用实线箭头表示无条件跳转jmp。若有向下的虚线箭头,据此图形可判断为if语句。
(2)if……else语句
-
特征识别:首先会有一个jxx指令用于向下跳转,且跳转的目的else中有jmp指令。else代码的结尾没有jmp指令,else的代码也会执行if_else_end 的代码。
-
图形识别:if语句中有jxx向下跳转,所以有向下的虚线箭头,else语句中有jmp跳转,所以虚线箭头会有一个向下的实线箭头,此图形可判定if……else语句。
(3)switch—case语句
switch是常用的多分支结构,通常比if有更高的效率。
当case≥6,且case间隔比较小时,编译器会采用case表的方式实线switch语句,也就是把所有要跳转的case位置偏移放在一个一维数组的表中,把case的值当成数组下标进行跳转。
当case较多时,编译器直接用if来实现switch。
5.转移指令机器码的计算
(1)call/jmp direct
位移量 = 目的地址-起始地址-跳转指令长度

浙公网安备 33010602011771号