1、抽象层次
- 比如可以在各种类型的硬件上运行windows系统
- 抽象层次越低,跨系统移植性越差
- 硬件:电子电路组成,实现数字逻辑
- 微指令:固件(firmware),只能在特定电路上执行,提供了访问硬件的接口
- 机器码:十六进制数字。由高级语言编写的计算机程序编译而来
- 低级语言:主要是汇编语言
- 高级语言:C C++
- 解释型语言:位于最高层,C#、Perl、.NET、Java等,不会被编译为机器吗,被翻译为字节码(bytecode),在解释器中执行。解释器是一个运行时将字节码实时翻译为可执行机器码的程序。
2、逆向工程
3、x86体系结构
- 遵循冯诺依曼结构,包含CPU(执行代码)、内存(RAM 存储所有的数据和代码)、输入/输出系统(I/O 为硬盘、键盘、显示器等设备提供接口)
- CPU:控制单元使用指令指针寄存器从内存中取指令,该寄存器中存放的是指令的地址
算数逻辑单元(ALU)执行从内存取来的指令,并将结果放到寄存器或内存中 - 内存:
- 数据:内存中的数据节,其中包含了一些值,这些值在程序初始加载时被放到这里,称为静态值(static value)或全局值(global value);程序运行时不会发生变化并且程序任何部分都可以使用它们。
- 代码:代码节包含CPU所取得的指令
- 堆:为程序执行期间需要的动态内存准备的,用于创建(分配)新的值,以及消除(释放)不再需要的值。将其称为动态内存,其内容在程序运行期间经常被改变。
- 栈:用于函数的局部变量和参数,控制程序执行流
- 指令:助记符+操作数
- 操作码和字节序:x86架构使用小端字节序,最低位被排在最低地址上(例:127.0.0.1 -> 0x0200007F)
在网络通信时使用大端字节序(127.0.0.1->0x7F000002) - 操作数:
- 立即数:操作数是固定值
- 寄存器:操作数指向寄存器
- 内存地址:操作数指向感兴趣的值所在的内存地址,一般由[]包含值、寄存器或方程式组成
- 寄存器:可以被CPU使用的少量数据寄存器
- 通用寄存器:CPU在执行期间使用
- 一般用于存储数据或内存地址
- 被程序的一致特性使用(约定),比如EAX通常存储一个函数调用的返回值
- 段寄存器:用于定位内存节
- 状态标志寄存器:用于作出决定
- EFLAGS是x86架构中32位的标志寄存器,每一位是一个标志
- 执行期间每一位是置位或清零
- 控制CPU的运算
- ZF:当一个运算结果是0时,ZF被置位
- CF:当运算结果相对于目标操作数太大或太小时,CF被置位
- SF:当运算结果为负数,SF被置位;对算术运算,点那个算数结果最高位为1时,SF会被置位
- TF:用于调试,当它被置位时,x86处理器每次只执行一条指令
- 指令指针(程序计数器):定位要执行的下一条指令
- EIP:保存了下一条指令在内存中的地址,攻击者可以改变EIP的值使其执行特定恶意代码
- 所有通用寄存器的大小都是32位【4个字节】,可以在汇编代码中以32位或16位引用(例:EDX32位,DX指向低16位)
- EAX、EBX、ECX、EDX还可以以8位引用(AL最低8位,AH次低八位)
- 简单指令:
- mov:用于读写内存,将数据移动到寄存器或内存
- lea:将一个内存地址赋给目的操作数(lea eax,ebx+8:将EBX+8的值给EAX),用于计算
- 算数指令:
- add dest,value
- sub dest,value:结果为0,ZF被置位;目标操作数比要减去的值小,CF被置位
- inc:将一个寄存器+1
- dec:将一个寄存器-1
- mul value:乘法使用预先规定的寄存器,指令码加上寄存器要乘的值,总是将eax*value,eax寄存器必须在乘法指令出现前就赋值好,乘法的结果以64位形式分开存储在两个寄存器中(EDX、EAX,EDX存储高32位、EAX存储低32位)
- div value:同乘法(将EDX和EAX合起来存储的64位值除以value,商存储在EAX中,余数存储在EDX中
- imul、idiv为mul和div的有符号版本
- or、and、xor:源操作数和目的操作数做相应操作,并将结果保存在目的操作数中(xor eax,eax:将EAX寄存器快速置0
- shr/shl dest,count:对目的操作数右移或左移,移出目的操作数边界的位会先移动到CF标志位中,移位时使用0填充新的位(shl eax,1 == mul eax,2)
- ror/rol:循环移位指令,移出的会被填到另一端空出的位上
- 函数中出现大量shr、shl、ror、rol意味着这可能是一个加密或压缩函数
- 移位都是在二进制的基础上操作的,要先转化为二进制
- 1010左移两位:101000(00001010-》00101000=101000)
- 1010循环右移两位:10000010
- NOP:nop出现时直接执行下一条指令(xchg eax,eax)
- 通用寄存器:CPU在执行期间使用
- 栈:用于函数的内存、局部变量、流控制结构等被存储在栈中
- 栈的内建支持:ESP和EBP,ESP是栈指针,包含了指向栈顶的内存地址,一些东西被压入或弹出栈时,这个寄存器的值相应改变。EBP是栈基址寄存器,在一个函数中保持不变,用来确定局部变量和参数的位置
- 与栈有关的指令:push、pop、call、leave、enter、ret
- 内存中栈被分配为自顶向下的,最高的地址最先被使用(高——>低)
- 用于短期存储,保存局部变量、参数和返回值,主要用途是管理函数调用之间的数据交换。
- 使用相对EBP的地址引用局部变量和参数
- 函数调用
- 程序如何使用栈,最常见的约定为cdecl
- 函数调用最常见的实现流程:
- 使用push指令将参数压入栈中
- 使用call memory_location来调用函数,当前指令地址(EIP)被压入栈中,用于函数结束后返回到主代码。函数开始执行时EIP的值被设为memory_location(函数起始地址)
- 通过序言部分,分配栈中用于局部变量的空间,EBP(基址指针)被压入栈中
- 函数工作
- 通过结语部分,恢复栈。调整ESP来释放局部变量,恢复EBP。leave指令可以用作结语,使ESP等于EBP然后从栈中弹出EBP
- 函数通过调用ret指令返回,这个指令从栈中弹出返回地址给EIP,程序从原来调用的地方继续执行
- 调整栈,移除此前压入的参数,除非后面还要被使用
- 栈的布局
- 栈被分配为自顶向下的,高内存地址先被使用。每一次函数调用就生成了一个新的栈帧
- 不用push、pop也可以从栈中读取数据:mov eax,ss:[esp]指令直接访问栈顶
- x86架构提供其他弹入弹出指令:
- pusha:按AX、CX、DX、BX、SP、BP、SI、DI的顺序将所有16位寄存器压入栈中
- pushad:按EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI的顺序将32位寄存器压入栈中
- popa、popad
- 这些指令一般是手工写的汇编代码或shellcode,编译器很少使用
- 栈被分配为自顶向下的,高内存地址先被使用。每一次函数调用就生成了一个新的栈帧
- 条件指令:用来做比较的指令
- test:与and功能相同,但是不会修改其使用的操作数,只设置标识位。ZF
- cmp:与sub指令功能相同,但不影响操作数,只设置标志位。ZF和CF标志位可能发生变化
- 分支指令:一串指令根据程序流有条件的执行
- 跳转指令:
jmp:无条件跳转
条件跳转使用标志位来决定是跳转还是执行下一条指令
- 跳转指令:
重复指令:操作数据缓冲区的指令。数据缓冲区通常是一个字节数组的形式,可以是单字或双字
- 常见数据缓冲区操作指令:movsx、cmpsx、stosx、scasx(x可以是b字节、w字或d双字)
- 使用ESI、EDI寄存器:ESI-源索引寄存器,EDI-目的索引寄存器,ECI-计数变量
- 指令需要前缀,用于对长度超过1的数据操作。movsb指令本身只会移动一个字节而不实用ECX寄存器
- rep指令:增加ESI和EDI这两个偏移,减少ECX寄存器。rep前缀不断重复直至ECX=0。repe/repz和repne/repnz前缀不断重复,直至ECX=0或直至ZF=1或0
- movsb指令:将一串字节从一个位置移动到另一个位置。rep前缀通常与movsb一起使用,从而复制一串长度由ECX决定的字节。(rep movsb:从ESI指向地址取出一个字节,将其存入EDI指向的地址,根据方向标志(DF)的设置,将ESI或EDI的值加1(DF=0)或减1(DF=1)。
- cmpsb指令:比较两串字节,确定是否是相同的数据。该指令用ESI指向地址的字节减去EDI指向地址的字节,并更新相关的标志位,然后对ESI和EDI分别加1,如果有repe前缀,就检查ECX的值和标志位,ECX=0或ZF=0就停止重复
- scasb指令:从一串字节中搜索一个值,这个值由AL寄存器给出,repe操作使比较不断继续,直到找到该字节或ECX=0,找到了其位置会被存储在ESI中
- stosb指令:将指定的字节存储到EDI指向的地址
C语言主函数和偏移
- 标准C程序的主函数有两个参数:
int main(int argc, char ** argv[])
argc:整数,说明了命令行中参数的个数,包括程序名字本身
argv:字符串数据指针,指向所有的命令行参数
- 标准C程序的主函数有两个参数:
- 更多信息:Intel x86 Architecture Manual