1、抽象层次

  • 比如可以在各种类型的硬件上运行windows系统
  • 抽象层次越低,跨系统移植性越差
  • 硬件:电子电路组成,实现数字逻辑
  • 微指令:固件(firmware),只能在特定电路上执行,提供了访问硬件的接口
  • 机器码:十六进制数字。由高级语言编写的计算机程序编译而来
  • 低级语言:主要是汇编语言
  • 高级语言:C C++
  • 解释型语言:位于最高层,C#、Perl、.NET、Java等,不会被编译为机器吗,被翻译为字节码(bytecode),在解释器中执行。解释器是一个运行时将字节码实时翻译为可执行机器码的程序。

2、逆向工程


3、x86体系结构

  • 遵循冯诺依曼结构,包含CPU(执行代码)、内存(RAM 存储所有的数据和代码)、输入/输出系统(I/O 为硬盘、键盘、显示器等设备提供接口)
  • CPU:控制单元使用指令指针寄存器从内存中取指令,该寄存器中存放的是指令的地址
    算数逻辑单元(ALU)执行从内存取来的指令,并将结果放到寄存器或内存中
  • 内存
    • 数据:内存中的数据节,其中包含了一些值,这些值在程序初始加载时被放到这里,称为静态值(static value)或全局值(global value);程序运行时不会发生变化并且程序任何部分都可以使用它们。
    • 代码:代码节包含CPU所取得的指令
    • 堆:为程序执行期间需要的动态内存准备的,用于创建(分配)新的值,以及消除(释放)不再需要的值。将其称为动态内存,其内容在程序运行期间经常被改变。
    • 栈:用于函数的局部变量和参数,控制程序执行流
      2.jpg
  • 指令:助记符+操作数
  • 操作码和字节序: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)
  • :用于函数的内存、局部变量、流控制结构等被存储在栈中
    • 栈的内建支持: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,程序从原来调用的地方继续执行
        • 调整栈,移除此前压入的参数,除非后面还要被使用
    • 栈的布局
      • 栈被分配为自顶向下的,高内存地址先被使用。每一次函数调用就生成了一个新的栈帧
        3.jpg
      • 不用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标志位可能发生变化
      4.jpg
  • 分支指令:一串指令根据程序流有条件的执行
    • 跳转指令:
      jmp:无条件跳转
      条件跳转使用标志位来决定是跳转还是执行下一条指令
      5.jpg
  • 重复指令:操作数据缓冲区的指令。数据缓冲区通常是一个字节数组的形式,可以是单字或双字

    • 常见数据缓冲区操作指令: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:字符串数据指针,指向所有的命令行参数
      6.jpg
  • 更多信息:Intel x86 Architecture Manual