掌握黑客技术一大难点就在于你要非常深入计算机技术的底层。绝大多数程序员只愿意在上层应用上花点时间,毕竟他们只想”混饭吃“,任何有志于不断提升技术能力的工程师都必须跨过几个高门槛,一个是算法,一个是系统设计,还有就是掌握计算机体系结构,与底层,与硬件打交道,这些知识点难度大,有些甚至很枯燥,因此愿意专研的人不多,我们本节所要描述的汇编语言就属于计算机体系结构的一部分。
一名真正的黑客,不是那些只会使用工具搞些歪门邪道的门外汉,掌握汇编语言在关键时刻使用反汇编技术进行分析不可避免。在这里我们对汇编语言做初步掌握,真正要学会,我强烈推荐王爽老师的《汇编语言》。
信息计算最显著的特点就是抽象,特别是针对编程语言,在最顶层是高级语言例如java,python等,它们会被编译器先编译成汇编语言,然后再由汇编编译器将其编译成CPU能执行的字节码,如下图所示:
不管是PC平台还是手机等移动平台,其对应的汇编语言会有所不同但指令的作用都差不多,要不就是mov,将数据从一个地方挪到一个地方,要不就是jump,将程序控制流从一个地方转移到另一个地方,因此掌握一种汇编语言,对于其他类型的汇编也能很容易搞懂,因此这里我们集中精力与掌握x86汇编。
我们先看程序加载到内存后其布局方式:
对32位系统而言,内存总共4G,前2G用于操作系统内核,后2G才是用户程序被加载位置。操作系统分配内存时使用分页技术,于是可以将0-2G的内存固定对应到具体的物理内存页,而用户空间也就是3-4G的内存可以通过映射不同物理内存页的方式让不同用户程序都加载到3-4G内的虚拟空间。
程序二进制文件有两部分在加载时特别重要,分别是代码段和数据段,这两部分在前面几节讨论linux ELF文件结构时有描述。同时程序运行时需要使用两种不同内存,一种叫栈,它是预先分配给程序使用的内存,例如在调用函数时,函数的输入参数就必须存放在栈上;第二种叫堆,它是动态分配的内存,在C 中经常使用new来获取堆上的内存,同时堆的内存在使用完毕后必须释放。
汇编语言对应的指令就存储在代码段。指令通常由操作符 操作数的方式组成。例如mov ecx 0x10,意思是将数值0x10存放到寄存器ecx。这条指令会编译成数字指令以便CPU执行,对应数字指令为B9 10 00 00 00,当CPU执行单元被输入数值B9时,它就知道要把给定数值放置到寄存器ecx中。这里需要注意的是,X86结构使用小端数据模式,也就是4字节数据中,位置低的内存存放低数值,例如0x1234,那么数值0x34就会存放在内存的低位,而0x12就会存放在内存的高位。
汇编语言要直接跟机器打交道,因此编写汇编语言需要底层硬件思维,这也是它难以像高阶语言那样容易掌握的原因。例如在写汇编时,你必须关心数据如何传递给CPU,通常有三种方式,一种是数据直接跟着操作指令后面,一种是数据必须提前放置到指定寄存器中,一种是数据放置在指定的内存地址,然后将内存地址存放在某个寄存器中。
寄存器是一种能存储数据的硬件,容量很小通常只有几个字节,但是由于它跟CPU运算核心组装在一起,因此CPU从寄存器获取数据的速度会很快。寄存器分为四种,一种是通用寄存器,他们通常用来存储各种数据;第二种是段寄存器,他们用来帮助CPU访问特定内存,第三种是状态寄存器,其中的数值会影响CPU的运行流程,第四种叫指令寄存器,它专门用来指向CPU要指向的指令,在X86平台上所有寄存器分类如下:
寄存器内部又可以分割成两部分,例如EAX寄存器存储32位数据,也就是4字节数据。AX对应它的第2字节数据,同时AX又可以分成两部分,一部分AL对应低16位数据,另一部分AH对应高16位数据,其他EBX,ECX同理。举个例子,EAX寄存器可以存储数值0xA9DC81F5,那么访问AX寄存器就能得到数据0x81F5,访问AH寄存器得到数据0x81,访问寄存器AL得到数据0xF5,对应关系如下图所示:
通常情况下,不同寄存器的使用要根据指令而定,如果做乘法或除法,那么指令会自动使用32位寄存器。
EFLAGS寄存器也叫状态寄存器,它的每一位都用于做标志位使用,将特定位设置为1或0都会影响CPU的执行状态。它对应的几个比特位特别设计到黑客静态分析技术: ZF 如果某个指令执行后结果为0,它就设置为1,要不然就设置为0,由此可见该位设计到类似if…else这样的代码
CF, 它叫进位标志,如果指令执行后结果的数据过大导致指定内存无法存储,那么该标志位就会被设置,例如执行乘法指令后,结果是32位数值,但是指令原来要求将结果存放到16位的内存,那么产生数据溢出就会导致该标志位被设置。
SF,它是符号标志位,如果指令执行后所得结果为负数,那么该标志位设置为1
TF,它是陷阱标志位,它通常用于调试目的,如果该标志位被设置,那么X86CPU一次只执行一条指令然后就停下来。
最后我们看EIP寄存器,它也叫指令寄存器,用于指向下一条CPU要指向的指令所在地址,对于汇编语言的指令使用方法,我们在下节介绍。