当前位置: 首页 > news >正文

逆向分析基础总结

一、了解计算机部件

CPU:
中央处理器。有三个重要的部件:
逻辑部件:负责算数运算,包括定点运算、浮点运算等。
寄存器部件:负责临时数据存储,一个CPU包含多个寄存器。
控制部件:负责发出指令所要执行操作的控制信号。

内存:
用于存储系统运行的临时数据,是CPU与外部存储器进行沟通的桥梁。
CPU如果想准确地对内存进行读写,必须进行以下三类信息的交互。
地址信息:存储单元的地址。
控制信息:读或写的命令。
数据信息:读或写的数据。

存储单元一般以字节为单位,每个字节通过他们各自的唯一编号(也叫地址)进行访问,例如,一个存储器包含128个存储单元,这个存储器能够访问的地址范围为0~127。CPU读取数据的大致过程可参考下图:
在这里插入图片描述
对于大容量存储器(如硬盘和内存),可以使用以下不同单位来表示:
1Byte =8 Bit
1KB = 1024Byte
1MB = 1024KB
1GB = 1024MB
1TB = 1024GB
CPU的寻址能力是由CPU的地址总线宽度决定的。如果一个CPU的地址总线宽度为10,那么它的寻址能力就是1024字节(2的10次方)。此时,内存的大小如果大于1024字节,那么地址编号超过1023的内存单元都是没有意义的,因此CPU无法通过地址总线访问到它们。

内存分配机制:
内存的空间分为两类,分别是物理地址和虚拟地址空间。
物理地址就是物理内存的实际大小,
虚拟地址是由操作系统为每个进程分配的虚拟内存,虚拟内存由虚拟内存管理器进行管理。

但也出现了几个弊端:
(1)进程间不隔离。进程之间的数据可以相互修改。
(2)效率低。有些数据用不到,存满的情况下,想启动一个新的进程,需要把另一个进程的数据暂时复制到硬盘等外部存储中。
(3)地址不固定。程序在加载到物理内存时无法每次都分配在一块固定的地址。

后来,为了解决这个问题。提出了“分段”和“分页”两种措施。
分段:操作系统将内存中不同属性的内存分为一个个段,用户在访问不同的段内存时,拥有不同的访问权限,以此达到内存权限控制的目的。
分页:将物理内存按照一定量的大小进行分割,每个分割出来的内存块称为一个“页”。当程序向操作系统申请内存时,如果申请的内存大小小于一个“页”的大小,则默认分配一个页给程序使用,直到程序将这个页面用满为止,才会为程序分配下一块内存,以此减少内存分配的时间成本。

内存映射:
当一个进程启动时,程序的数据和依赖库的数据会被加载到系统分配给它的虚拟内存中,而系统不会马上将这些数据同步映射到物理内存中,否则仍然无法解决进程启动数量有限的问题,而只有当虚拟内存中的数据在程序执行过程中被真正访问(读、写、执行)时,系统才会将该数据与物理内存的某个内存页进行绑定,以此来节省物理内存的使用。
在这里插入图片描述

二、可执行文件

即,能够被操作系统加载并运行的文件。

PE:
是Windows平台下可执行文件遵守的数据格式代码,常见的PE格式文件扩展名有“.exe”“.dll”“.sys”。

PE文件加载:
当使用十六进制编辑器打开一个PE文件时,以字节为单位,每个字节相对于文件头的距离称作文件偏移。
在这里插入图片描述
双击运行程序时,程序的数据会被加载到进程的虚拟内存中,一般情况下,不是从0地址开始加载的,系统根据程序提供的相对虚拟地址将每个数据从文件中复制至虚拟内存中,分为两个部分。
(1)PE文件头:大小一般是固定的,复制到基址开始的位置。
(2)每个区段的数据。复制到节表指定的位置。
数据在虚拟内存中的地址叫作虚拟地址,即基址加数据的相对虚拟地址。

ELF:
ELF是Linux平台使用的数据格式。ELF头的位置是固定的,其余各部分的位置、大小等信息则是由ELF头中的各项值来决定的。ELF格式文件在运行时,只有ELF头和程序头表会被系统引用(程序头表的功能相当于PE格式文件中的节表),节和节头表主要是包含一些符号信息和调试信息,在静态分析时能够发挥作用,对程序运行帮助不大。

三、寄存器

是CPU的重要组成部分之一。不同架构、不同数位的CPU拥有的寄存器类型和数量也各有不同。在调试软件时,观察寄存器的变化也是重要的分析步骤。

寄存器的分类:
Intel x86 架构包含以下几种类型的寄存器,代号分别如下:
通用寄存器:EAX、EBX、ECX、EDX、ESP、EBP、ESI、EDI
段寄存器:CS、SS、DS、ES、FS、GS
标志寄存器:EFLAGS
指令指针寄存器:EIP
系统表寄存器:GDTR、IDTR、LDTR、TR
调试寄存器:DR0、DR1、DR2、DR3、DR4、DR5、DR6、DR7
控制寄存器:CR0、CR1、CR2、CR3、CR4
测试寄存器:TR6、TR7
具体模型寄存器:MSR

通用寄存器:
EAX(累加寄存器)、EBX(基址寄存器)、ECX(计数寄存器)、EDX(数据寄存器)、ESP(堆栈指针寄存器)、EBP(基址指针寄存器)、ESI(源变址寄存器)、EDI(目的变址寄存器)。
为了兼容早期CPU架构的程序,一些通用寄存器又可以拆分为几个低位寄存器。以EAX为例,它的低16位可以通过代号AX进行访问,AX的高8位又叫做AH,AX的低8位又叫AL,在编写程序时,可以根据具体需要选择相应宽度的寄存器进行使用。
在这里插入图片描述
在一般情况下,会将EAX、EBX、ECX、EDX、ESI、EDI这六个寄存器用于数据运算或寻址,只有在特定情况下,才会发挥他们原本的作用(如部分指令默认使用ECX作为循环变量)。

ESI与EDI:
ESI中的S是英文单词Source的缩写,EDI中的D则指的是Destination。一般情况下这两个寄存器也能用于暂存数据,而对于部分指令来说,ESI负责存放源数据的地址,而EDI则负责指向数据处理后需要存储的目的地址。
例:REP MOVS DWORD PTR DS:[EDI],DWORD PTR DS:[ESI]
在这条指令中,REP MOVS 指令的意思是循环复制内存数据,复制的数据默认从ESI指向的内存中读取,保存到EDI指向的内存中,用户可以指定每次复制的数据宽度(DWORD,表示四个字节),每执行一次后,令ESI和EDI指向下一块需复制的内存,并默认使用ECX表示剩余次数。
除了CPU内部设定好的一些默认规则,其他像使用那条指令、哪些寄存器去完成某一个动作,通常是由编译器决定的。

ESP与EBP:
ESP与EBP是与栈相关的两个寄存器,其中ESP通常表示栈顶,EBP表示栈底。
栈是位于内存中的一块线性的数据结构,比寄存器的内存大,从程序开始到结束的所有临时数据都能放得下。
栈的基地址是在程序启动时,由系统指定的。遵守几条基本规则:
1.栈从高地址向低地址分配内存。
2.当发生入栈操作时,入栈的元素被置入栈顶。
3.当发生出栈操作时,从当前栈顶中取出元素。
4.栈底是栈的边界,当栈顶和栈底指向同一块内存时表示栈为空。

段寄存器:
段寄存器比较特殊,常见有6个:
在这里插入图片描述
早期CPU中的每个寄存器宽度只有16位,寻址范围为0~0xFFFF,无法满足当时128KB内存的寻址要求,因此CPU厂商针对这种情况采用了“段寄存器*16+偏移量”的方式进行寻址。
如今单个寄存器的寻址能力已经能够满足寻址要求,因此现在已经不使用段寄存器寻址和类似的方式,段寄存器也被分配到了其他岗位。

标志寄存器:
在进行编程开发时,经常会使用类似“if……else……”这样的条件分支语句,根据条件是否成立来决定最终执行哪个分支的代码。对于CPU来说,标志寄存器(EFLAGS)起到了不可或缺的作用。
标志寄存器具有以下三个作用:
1、存储部分指令的执行状态。
2、为部分指令的执行提供行为依据
3、控制CPU的相关工作方式

需要重点关注的标志位见下表:
在这里插入图片描述
指令指针寄存器:
EIP寄存器叫做指令指针寄存器,它用于告诉CPU下一条指令的位置。如果没有EIP,CPU就不知道该去哪里读取指令,也就无法产生控制信息。
大部分汇编指令并不会自己修改EIP的值,这项工作由CPU自己完成,只有少部分汇编指令能为用户提供修改EIP的权限,控制程序的运行逻辑。
高级语言的判断语句、循环语句及函数调用本质上都是通过修改EIP的值来实现的。

四、汇编语言

汇编指令:汇编语言的主体,也叫机器码的助记符。
伪指令:提供编译过程中的相关信息,由编译器识别。
其他符号:如‘+’,‘-’,‘*’,‘/’等,由编译器识别。
汇编语言示例代码如下:

STACK SEGMENT PARA STACK
DW 20H DUP(0)
STACK ENDSDATA SEGMENT
STRING DB’Hello World’,’$’;
DATA ENDSCODES SEGMENT
ASSUME CS:CODES;DS:DATAS
START:
MOV AX,DATA
MOV DS,AX
LEA  DX,STRING
MOV AH,09H
INT 21H
MOV AH,4CH
INT 21H
CODES ENDS
END START

以上是一段简单的汇编源代码,功能是在控制台输出字符串“Hello World”。在这段汇编代码中,START部分包含具体的汇编指令。
汇编指令是用来代替机器指令的一种便于记忆的符号,因此其准确性和可靠性能够得到保证,并且在分析一个软件时,通过将程序中的机器指令反汇编成汇编指令进行阅读,是除了直接分析机器指令外最准确的一种方法。

汇编指令格式:
以MOV指令为例,InTER X86 汇编指令的格式为:MOV DEST,SRC。在这句汇编指令当中,MOV叫作操作码,表示指令的具体功能。
不同的汇编指令能够携带的参数个数不同,每个参数使用逗号进行分割。
在下文中,对于带两个参数的汇编指令,以DEST表示目的操作数,它既参与指令的执行也负责保存结果,以SRC表示源操作数,它通常只参与运算。
实际上,一条机器指令的构成是非常复杂的,Inter 将其大致分为6个部分,分别为地址前缀、操作码、操作数、辅助操作码、辅助操作数、内存修饰符和标号,如下图所示。且最大长度能达到14字节之多。
在这里插入图片描述
字段说明:
ModR/M:指定操作数的寻址方式(如寄存器、内存)。
SIB:当寻址需要基址 + 索引 + 比例因子时使用(如 [EBX*2+ESI])。
位移量:内存地址的偏移量(如 [EBP+0x8] 中的 0x8)。
立即数:指令中直接携带的数据(如 MOV EAX, 0xFFFFFFFF 中的 0xFFFFFFFF)。

指令执行流程与字段关联图如下图所示:
在这里插入图片描述
数据传送指令:
MOV指令叫作数据传送指令。它的作用是将数据从一个地方(数据源)传送到另一个地方(目的地)。指令执行后,数据源的值不会发生改变。
指令格式:MOV DEST,SRC
MOV 指令支持以下几种具体的用法:
(1)MOV寄存器,立即数,如mov eax,0x10
(2)MOV寄存器,寄存器,如mov ebx,ecx
(3)MOV寄存器,内存,如mov eax,dword ptr ds:[0x402100]
(4)MOV 内存,立即数,如mov byte ptr ds : [0x403400],0x2200
只有两种情况不被允许,具体如下。
(1)目的地是立即数,如mov 5,ebx
(2)两个内存之间进行数据传送,如mov dword ptr ds:[0x401200],dword ptr ds:[0x403400]
32位处理器支持一次性处理的常见数据宽度如下表:
在这里插入图片描述
对于一条正确的MOV指令,需要保证给出的两个参数的数据宽度相同。

算数运算指令:
在这里插入图片描述
在使用DIV指令时有两个需要注意的地方:
(1)除数不能为0,否则会触发除0异常。
(2)执行前需要确保 AH/DX/EDX的值为0。

逻辑运算指令:
在这里插入图片描述
移位指令:
在这里插入图片描述
条件转移指令:

JCC指令也叫条件转移指令,是功能相近的一系列汇编指令的统称,J为英文单词jump的缩写,CC指的是条件码。作用是根据条件码决定是否修改EIP,如果EIP被修改,可以认为程序的执行逻辑发生了“跳转”,因此条件转移指令口语化也称“条件跳转指令”。
指令格式:JCC寄存器/立即数/内存
条件检查码检查一个或多个标志寄存器中标志位的组合,由于标志位有许多个,因此标志位的组合也有许多种。
汇编语言中JCC指令包含的具体指令:
在这里插入图片描述
此外,汇编语言还提供了一个无条件转移指令:JMP指令。例如,JMP 0x401000
这条指令在执行后,EIP寄存器的值将被无条件地修改为0x401000,即CPU下次执行时将从地址0x401000读取指令。

栈操作指令:
PUSH指令和POP指令对应栈的两个基本操作:入栈和出栈。
PUSH指令表示入栈,意为将一个元素送入栈顶,如PUSH EAX
POP表示出栈,意为从栈顶取出一个元素,如POP EAX
其中,PUSH指令可以拆解为两个步骤来理解。

(1)ESP = ESP -4
(2)MOV[ESP],SRC

上面的代码,大家可以问问Deepseek,让它生成HTML格式的动画,方便大家理解。以下是我问豆包得到的图示:
PUSH指令执行前的栈状态见下图:
在这里插入图片描述
PUSH指令执行时的栈变化见下图:
在这里插入图片描述

函数调用:
函数是用于完成一段特定功能的代码片段,通常使用函数名进行调用,使用return退出函数,且能够根据需要附带返回值。
对于CPU来说,调用函数的本质是通过修改EIP实现的,进入函数即修改EIP到目标地址执行代码,退出函数即修改EIP到下一条指令的位置。
在汇编语言中,CALL指令和RET指令是与函数调用相关的两条基本指令。CALL指令表示调用函数,它与JMP指令很像,都能够无条件地修改EIP到目标地址,但是相比JMP指令而言,它在修改EIP之前还会进一步额外的操作,即在栈中存入下一条指令的地址,这个地址被称为“返回地址”。
RET指令表示退出函数,具体实现是从栈顶取出一个元素给EIP,相当于执行了 POP EIP指令,但汇编语言没有后者写法。
在了解CALL指令和RET指令的基本功能后,可以想象一下再汇编语言层面函数调用的大致过程。
(1)CALL指令向栈中存入一个“返回地址”,然后修改EIP到目的地址。
(2)CPU执行函数的主体代码,并在结尾处遇到RET指令。
(3)RET指令从栈中取出返回地址给EIP,程序回到父函数继续运行。
这三个步骤所描绘的函数调用过程可以参考:

除此之外,一个正常且完整的函数通常需要解决以下三个问题。
(1)函数的参数和局部变量在哪里?
(2)寄存器的值被修改了怎么办?
(3)如何保证退出函数时,能取出正确的返回地址?

在编译可执行时,全局变量和静态变量的具体值会被编译到文件中,(如果在代码中有初步定义),且在程序运行后拥有固定的地址;而局部变量只有当函数被调用时才会在栈中为它们分配内存,在函数退出时释放内存。
x86程序在调用函数时默认通过栈传递参数。也就是说,在调用函数前,若该函数需要参数,会将参数依次放入栈中。
在函数执行过程中,难免会用到一部分寄存器参与数据的运算和存储,这些寄存器的值在进入该函数前对于父函数是比较重要的,如果这些值在函数执行过程中被修改,在退出函数后,可能会影响父函数的正常执行,最终产生逻辑错误或崩溃。大多编译器为了避免这种情况的出现,在函数的主体代码执行前,会先将一些重要寄存器的值保存在栈里,然后便可放心地执行函数。只要在退出函数前,将这些关键数据再取出还给对应的寄存器即可。
另外,在执行汇编指令时,难免会遇到与栈相关的操作,这些操作通常会令ESP发生变化,而函数在退出时需要使用RET指令从栈中取出一个“返回地址”给EIP,此时如果栈顶指针并没有指向正确的返回地址,取出的数据将是不确定的,这将破坏整个程序的正常执行,甚至很可能导致程序崩溃。因此,在使用汇编指令对栈进行操作时需要十分小心。
(1)每个函数都拥有一块独立的栈空间,在退出前进行释放。
(2)函数的参数和局部变量位于栈或寄存器中。
(3)函数会通过“保存现场”保护一些关键的数据,在退出函数前“还原现场”,以确保回到父函数后,程序依然能够正常运作。
(4)在函数中对栈进行操作时,要十分小心,确保在函数退出时栈顶能够指向正确的返回地址。
了内存使用的痕迹,对于main函数而言,栈的状态完全没有发生变化。

中断指令:
操作系统在内部设置了一些子程序,用于完成某些用户难以完成的特定功能,如异常处理、I/O通信、屏幕输出等,用户可使用INT指令进行调用。
格式:INT N
INT指令也叫作软件中断指令,N表示中断号。当CPU在程序内部执行时,一旦遇到INT指令,就会暂停执行当前程序,到系统内核中执行相应的子程序,也可以将INT指令当作一种特殊的CALL指令来理解。
中断号占一个字节,其中,中断号3的作用是发出调试信号,是调试器设置断点最常用的方法之一。
当执行INT 3时,若当前进程中存在调试信息,则会将软件控制权转交给调试器,由调试器来决定下一步操作。INT 3对应的字节码为0xCC,这也是为什么编译器喜欢将栈中的局部变量初始化为0xCC的原因(若栈中未初始化的数据被意外执行,则会产生INT 3中断,便于调试分析)

http://www.xdnf.cn/news/729955.html

相关文章:

  • HTML 文件反编译指南:优化与学习网页代码
  • 【容器docker】启动容器kibana报错:“message“:“Error: Cannot find module ‘./logs‘
  • STUSB4500 PPS(PD3.0)快充SINK模块——应用 解析
  • [学习] C语言的回调函数(代码示例)
  • 数据基座觉醒!大数据+AI如何重构企业智能决策金字塔(下)
  • 【Linux 学习计划】-- 命令行参数 | 环境变量
  • 【目标检测】【AAAI-2022】Anchor DETR
  • 【Golang进阶】第八章:并发编程基础——从Goroutine调度到Channel通信实战
  • Redis持久化机制
  • MPC5744P——eTimer简介
  • Github 2025-05-30Java开源项目日报Top10
  • 《深入解析Go语言结构:简洁高效的工程化设计》
  • 基于 KubeKey 3.1.9,快速部署 K8s 1.33.0 高可用集群
  • Java复习Day23
  • haproxy 搭建web群集
  • EMQX社区版5.8.5集群搭建踩坑记
  • vscode命令行debug
  • 中国外卖包装废弃物高精度网格图谱(Tif/Excel/Shp)
  • 128、STM32H723ZGT6实现串口IAP
  • 贪心算法实战3
  • 6年“豹变”,vivo S30系列引领手机进入场景“体验定义”时代
  • 交叉编译tcpdump工具
  • File—IO流
  • Vue 3.0 中的路由导航守卫详解
  • [yolov11改进系列]基于yolov11引入轻量级注意力机制模块ECA的python源码+训练源码
  • CVPR 2025论文分享|MGGTalk:一个更加通用的说话头像动画生成框架
  • 60天python训练计划----day40
  • 训练和测试的规范写法
  • Z-AnyLabeling1.0.1
  • Glide NoResultEncoderAvailableException异常解决