X86反汇编速成(恶意代码篇)

x86反汇编

抽象层面

在这里插入图片描述

​ 通常,计算机系统被描述为六个抽象层次。

硬件 硬件层是唯一的一个物理层,由电子电路组成。这些电路实现了XOR门、AND门、OR门和NOT门等逻辑运算器的复杂组合,成为数字逻辑。硬件很难被软件所操作。

微指令 微指令层又成为固件(firmware)。微指令只能在为它设计的特定电路上执行。这层由一些微指令构成,它们从更高的机器码层翻译而来,提供了访问硬件的接口。当分析恶意代码时,我们通常不关心微指令,因为它们通常是为特定的计算机硬件设计的。

机器码 机器码层由操作码(opcode)组成,操作码是一些十六进制形式的数字,用于告诉处理器你想它做什么。机器码一般由多条微指令实现,这样底层硬件就能实际执行代码了。而机器码本身又由高级语言编写的计算机程序编译而来。

低级语言 低级语言杀死计算机体系结构指令集的人类易读版本,主要是汇编语言,恶意代码分析师利用这一层,因为对人来说,机器码太难理解了。我们使用反汇编器来生成低级语言的文本,这些文本由一些简单的助记符组成,如mov和jmp。

高级语言 大部分程序员使用高级语言。高级语言对机器层做了很强的抽象,从而可以很轻松地使用程序逻辑和流控制机制。高级语言包括C,C++等。它们被一个编译器经过成为编译地过程转为机器码。

解释性语言 解释性语言位于最高层。很多程序员使用诸如c#、Perl、NET、Java等解释语言。这一层的代码不会被编译成机器码,而是被翻译为字节码。字节码是特定于该语言的一种中间表示,它在解释器中执行。解释器(interpreter)是一个在运行时将字节码实时翻译成可执行机器码的程序。相比于传统被编译的代码,解释器提供了一种自动的抽象层次,因为它可以独立于操作系统,自己处理错误和管理内存。

逆向工程

​ 恶意代码存储在磁盘上时,通常是机器码层的二进制形式。机器码是一种计算机可以快速高效执行的代码形式。而我们反汇编恶意代码,就是使用反汇编器,将恶意代码二进制文件作为输入,输出汇编语言代码。

x86体系结构

在这里插入图片描述

​ 大部分现代计算机体系结构(包括x86)在内部实现上遵循冯诺依曼结构。这种结构包括3种硬件组件:

​ 中央处理单元(CPU),负责执行代码

​ 内存(RAM),负责存储所有的数据和代码。

​ 输入/输出系统(I/O),为硬盘、磁盘、显示器等设备提供接口。

CPU又包含一些组件。其中:控制单元(control unit)使用某一个又成为指令指针(instruction pointer)的寄存器(register)从内存取得要执行的指令,这个寄存器中存有指令的地址。寄存器是CPU中数据的基本存储单元,通过它,很多时候CPU不在需要访问内存,从而节省了事件。算术逻辑单元(arithmetic logic unit,ALU)执行从内存取来的指令,并将结果放到存储器或内存中。一条条取指令、执行指令的过程不断重复,就形成了程序的运行。

内存

在这里插入图片描述

数据 这个词指的是内存中一个特定的节,名为数据节(data section),其中包含了一些值。这些值在程序初始加载时被放到这里,称为静态值(static value),因为程序运行时他们可能并不发生变化,还可以成为全局值(global value),因为程序的任何部分都可以使用它们。

代码 代码节包含了在执行程序任务时CPU所取得的指令。这些代码决定了程序是做什么的,以及程序中的任务如何协调工作。

堆是为程序执行期间需要的动态内存准备的,用于创建(分配)新的值,以及消除(释放)不在需要的值。将其称为动态内存(dynamic memory),是因为其内容在程序运行期间经常被改变。

栈用于函数的局部变量和参数,以及控制程序执行流。

指令

指令是汇编程序的构成块。在x86汇编语言中,一条指令由一个助记符,以及零个或多个操作数组成。

在这里插入图片描述

操作码(opcode)和字节序

​ 每条指令使用操作码告诉CPU程序要执行什么样的操作。

​ 反汇编器将操作码翻译为人类易读的指令。

在这里插入图片描述

用0x42000000表示值0x42,是因为x86架构使用小端字节序。

数据的字节序(endianness)是指在一个大数据项中,最高位(大端,big-endian)还是最小位(小端,little-endian)被排在第一位(即排在最低的地址上)。一些恶意代码在网络通信时必须改变字节序,因为网络数据使用大端字节序,而x86程序使用小端字节序。因此在大端字节序下,IP地址127.0.0.1会被表示为0x7F000001,而在小端字节序下,表示为0x0100007F.

操作数

​ 操作数说明指令要使用的数据。

​ 立即数(immediate)操作数是一个固定的值,比如4-1中的0x42

​ 寄存器(register)操作数指向寄存器,如ecx

​ 内存地址(memory address)操作数指向感兴趣的值所在的内存地址,一般由方括号内包含值、寄存器或方程式组成,如【eax】。

寄存器

​ 寄存器是可以被CPU使用的少数数据存储器,访问其中内容的速度会比访问其他存储器要快。x86处理器中有一组寄存器,可以用于临时存储或者作为工作区。

在这里插入图片描述

​ 通用寄存器,CPU在执行时使用。

​ 段寄存器,用于定位内存节。

​ 状态标志,用于做出决定。

​ 指令指针,用于定位要执行的下一条语句。

​ 所有通用寄存器的大小都是32位,可以在汇编代码中以32位或16位引用。比如,EDX指向这个完整的32位寄存器,而DX指向EDX寄存器的低16位。

通用寄存器

​ 通用寄存器一般用于存储数据或内存地址,而且经常交换着使用以完成程序。

在这里插入图片描述

标志寄存器

​ EFLAGS寄存器是一个标志寄存器。在x86架构中,它是32位的,每一位是一个标志。在执行期间,每一位表示要么是置位要么是清除,并由这些值来控制CPU的运算,或者给出某些CPU运算的值。具体的标志见笔记标志寄存器。

EIP、指令指针

​ 在X86架构中,EIP寄存器,又称为指令指针或程序计数器,保存了程序将要执行的下一条指令在内存中的地址。EIP的唯一作用就是告诉处理器接下来做什么。

在这里插入图片描述

简单指令

​ 由方括号括起来的操作数是对内存中数据的引用。不允许在不加方括号的情况下,在指令里面做一个运算。例如:mov eax,ebx+esi*4是一条非法指令

指令描述
MOV EAX,EBX将EBX中的内容复制到EAX寄存器
MOV EAX,0X42将立即数0x42复制到EAX寄存器
MOV EAX,[0X4037C4]将内存地址0x4037C4的四个字节复制到EAX寄存器
MOV EAX,[EBX]将EBX寄存器指向的内存地址四个字节复制到EAX寄存器
MOV EAX,[EBX+ESI*4]将ebx+esi*4等式结果指向的内存地址处四个字节复制到EAX

​ 另一条类似于MOV的指令是lea(load effective address 加载有效地址)。

​ lea destination,source。lea指令用来将一个内存地址赋给目的操作数。例如,lea eax,[ebx+8]就将EBX+8的值给了EAX。相反的,mov则加载内存中地址为EBX+8处的数据。因此,lea eax,[ebx+8]就和mov eax,ebx+8指令实际上是等价的,然而,采用这种寻址方式的mov指令是无效的。

在这里插入图片描述

​ mov eax,[ebx+8]将值0x20赋给EAX。

​ 而指令lea eax,[ebx+8]则将0xB30048这个值给EAX。

​ lea指令并非专门用于计算内存地址。它还被用来计算普通的值,因为它所需的指令更少。

算术运算
加减法
指令描述
sub eax,0x10eax寄存器值减去0x10
add eax,ebx将ebx值加入eax并将结构保存至eax
inc edxedx值递增1
dec ecxecx值递减1
乘除法

​ 乘法和除法都是用了一个预先预定的寄存器,因此其指令很简单,就是指令码加上寄存器要去乘或除的值。

​ mul指令的格式是mul value;div指令的格式是div value。

​ mul value指令总是将eax乘上value。因此,eax寄存器必须在乘法指令出现前就赋值好。乘法的结果以64位的形式分开存储在edx和eax中。其中edx存储了高32位,eax存储了低32位。

​ div value指令与mul运算方向相反:它将EDX和EAX合起来存储的64位值除以value。因此,在做除法之前,EDX和EAX这两个寄存器必须赋值好。商存储到EAX,余数存储到EDX中。

mul 0x50:将EAX的值乘以0x50,并将结果存入EDX:EAX寄存器中

div 0x75:将EDX:EAX值除以0x75,并将结果存入EAX,将余数存入EDX

​ shr和shl指令用于寄存器做移位操作。

​ shr指令的格式是“shr destination(目的数),count” shl指令的格式也是一样。

​ shr和shl指令对目的操作数右移或左移,由count决定移多少位。移出目的操作数(看宽度)边界的位则会先移动到CF标志位(进位标志)中。在移位时,使用0填充新的位。

eg:shr 0B1000,1 那么其结果位0100 CF标志位中就包含了最后移出目的操作数的哪一位。

*注:标志寄存器CF:进位标志 如果运算结果的最高位产生了一个进位或错位,那么其值位1

​ 循环移位指令ror(右循环)和rol(左循环)与移位指令类似,但移出的哪一位会被填到另一端(注意寄存器的宽度)空出来的位上,即右循环移位会将最低位循环移到最高位;左循环则相反;

​ 移位经常被用于对乘法运算的优化,因为乘法运算前需要准备寄存器、移动数据,这时移位就会显得更简单、更快。shl eax,1的计算结果和将eax乘以2一样。

​ 在分析恶意代码时,如果出现一个函数中只有xor、or、and、shl、ror、shr、rol这样的指令,并且反复出现,看起来随机排列,就可能遇到了一个加密或者压缩函数。

指令描述
xor eax,eax将EAX寄存器清零
or eax,0x7575将EAX的值与0x7575进行或操作
mov eax,0xA将eax寄存器左移两位,eax=0x28
shl eax,21010 001010->101000
mov bl,0xA将BL(8位)寄存器循环移位移两位
ror bl,21010 00001010—>10000010

注:三四指令一起 五六指令一起

NOP指令 当它出现时,直接执行下一条指令。

这条指令的opcode是0x90。在缓冲区溢出攻击中,当攻击者无法完美地控制利用代码,就经常使用NOP滑板。它起到了填充代码的作用,以降低shellcode可能在中间部分开始执行所造成的风险。

​ 用于函数的内存、局部变量、流控制结构等被存储在栈中。栈是一种用压和弹操作来刻画的数据结构,向栈中压入一些东西,然后再弹出来。是一种先进后出(LIFO)的结构。

​ x86架构有对栈的内建支持。用于这种支持的寄存器包括ESP(栈顶)和EBP(栈底)。其中,ESP时栈指针,包含了指向栈顶的内存地址。EBP是栈基址寄存器,在一个函数中会保持不变。

​ 栈只能用于短期存储。它经常用来保存局部变量、参数和返回地址。规格如下:

局部变量是从ebp-4开始 参数是ebp+8 返回地址(eip)是ebp+4

局部变量(ebp-4)
ebp
返回地址(ebp+4)
参数(ebp+8)
函数调用

​ 函数是程序中的一段代码,执行一个特定的任务,并于其他代码相对独立。主代码调用函数,并在其返回到主代码前,临时将执行权交给函数。程序如何使用栈,这对一个二进制文件是贯穿始终的问题。现在,我们将关注最常见的约定,称之为cdecl。

注:函数调用的三种方式cdecl、stdcall、__fastcall

cdecl是C/C++默认的调用方式

stdcall是Windows API函数的调用方式,只不过我们在头文件里查看这些API的声明的时候是用了winap的宏进行代替了,而这个宏其实就是stdcall了。

函数的调用,设计参数传递,返回值传递,调用后返回,这都是通过栈的变化来实现的

cdecl

*C/C++默认方式,参数从右向左入栈,主调函数(call指令所属的函数)负责栈平衡。

add esp,8 即是栈平衡操作
在这里插入图片描述
在这里插入图片描述

stdcall

Windows API默认方式,参数从右向左入栈,被调函数(函数内部)负责栈平衡。

ret 8为栈平衡操作
在这里插入图片描述
在这里插入图片描述

fastcall

快速调用方式。所谓快速,这种方式选择将参数优先从寄存器传入(ecx和edx),剩下的参数再从右向左从栈传入。因为栈是位于内存的区域,而寄存器位于CPU内,故存取方式快于内存,故其名曰fast

​ 许多函数包含一段“序言”(prologue),它是函数开始处的少数几行代码,用于保存函数中要用到的栈和寄存器。相应的,在函数末尾的“结语”(epilougue)则将栈和这些寄存器恢复至函数被调用前的状态(说白了就是平衡堆栈操作)

条件指令

​ 所有的汇编语言都能做比较,并根据比较结果做出决定。条件指令就是用来做比较的指令。

​ 最常见的两个条件指令是test和cmp。test指令与and(与操作:相同操作数返回值为1)指令的功能一样,但它并不会修改其使用的操作数。test指令只设置标志位。test指令执行后,我门感兴趣的是ZF标志位。

注:ZF:零标志 用于判断结果是否为0,运算结果为0,ZF置1,否则置0

对某个东西与它自身的test经常被用于检查它是否是一个NULL值.

eg:test eax,eax 其实也可以直接将eax与0比较,但是其字节更少,花费的CPU周期也少

​ cmp指令与sub指令的功能一样,但是它不影响其操作数。cmp指令也是只用于设置标志位,其执行结果是,ZF和CF标志可能会发生变化。

在这里插入图片描述

注:这里可以去看看某站的滴水逆向汇编部分 用一个圆来解释如何判断是否溢出

分支指令

​ 分支指令是一串指令根据程序流有条件地执行。

​ 最常见的分支指令是跳转指令。程序中使用了大量的跳转指令,最简单的是jmp指令,

注:jmp指令控制程序直接跳转到目标地址执行程序,程序总是顺序执行,指令本身无堆栈操作过程。

它使得下一条要被执行得指令是其格式jmp location中指定位置的指令,又称为无条件跳转,因为总会跳到目的位置去执行。这个简单的跳转无法满足所有的跳转需求。

​ 条件跳转使用标志位来决定是跳转,还是继续执行下一条指令。

下面表格是一些跳转指令

指令描述
jz loc如果ZF=1,跳转至指定位置
jnz loc如果ZF=0,跳转到指定位置
je loc与jz类似,但通常在一条cmp指令后使用。如果操作数与目的操作数相等,则跳转
jne loc与jnz类似,但通常在一条cmp指令后使用。如果操作数与目的操作数不相等,则跳转
jg loc在一条cmp指令做有符号比较之后,如果目的操作数大于源操作数,跳转
jge loc在一条cmp指令做有符号比较之后,如果目的操作数大于等于源操作数,跳转
ja loc与jg类似,但使用无符号比较
jae loc与jge类似,但是用无符号比较
jl loc在一条cmp指令做有符号比较之后,如果目的操作数小于源操作数,则跳转
jle loc在一条cmp指令做有符号比较之后,如果目的操作数小于等于源操作数,则跳转
jb loc与jl类似,但使用无符号比较
jbe loc与jle类似,但使用无符号比较
jo loc如果前一条指令置位了溢出标志位(OF=1),则跳转
js loc如果符号标志位被置位(SF=1),则跳转
jecxz loc如果ECX=0,则跳转

重复指令

​ 重复指令是一组操作数据缓冲区的指令。数据缓冲区通常是一个字节数组的形式,也可以是单字或者双字。

​ 最常见的数据缓冲区操作指令是movsx、cmpsx、stosx和scasx,其中x可以是b、

w或者d。分别表示字节、字和双字。

比如函数请求堆栈,填充数据缓冲区时,就利用了这个指令

mov eax,0xCCCCCCCC

mov ecx,0x10

lea edi,dword ptr ds:[ebp-0x40](内存寻址方式,将ebp-40的值赋给edi)

rep stosd

​ 在上述重复指令操作中,使用了ESI和EDI,ESI是原索引寄存器,EDI是目的索引寄存器。还有ECX用作计数的变量

​ 这些指令还需要一个前缀,用于对长度超过1的数据做操作。movsb指令本身只会移动一个字节,而不使用ECX寄存器。

指令描述
rep循环终止条件ECX=0
repe,repz循环终止条件ECX=0 or ZF=0
repne,repnz循环终止条件ECX=0 or ZF=1

​ 在x86下,使用重复前缀来做字节操作。rep指令会增加ESI和EDI这两个偏移,减少ECX寄存器。rep前缀会不断重复,直至ECX=0.repe/repz和repne/repnz前缀则不断重复,直至ECX=0或直至ZF=0或1.因此,在大部分数据缓冲区操作指令中,ESI、EDI和ECX必须为rep指令的生效,而进行适当的初始化。

​ movsb指令用于将一串字节从一个位置移动到另一个位置。rep前缀经常与movsb一起使用,从而复制一串长度由ECX决定的字节。从逻辑上说,rep movsb指令等价于C语言的memcpy函数(用于内存拷贝)。movsb指令从ESI指向地址取出一个字节,将其存入EDI指向地址,然后根据方向标志(DF)的设置,将ESI和EDI的值加1或者减1。如果DF=0,则加,否则减。
​ 在由C代码编译后的结果中,很少能看到DF标志。但是在shellcode里,人们有时候会调换方向标志,这样就可以反方向存储数据。

​ cmpsb指令用于比较两串字节,以确定其是否是相同的数据。cmpsb指令用ESI指向地址的字节减去EDI指向地址的字节,并更细相关的标志位。它经常与repe前缀一起使用。此时,cmpsb指令逐一比较两串字节,直至发现一处不同,或比较到头。cmpsb指令从地址ESI获得一个字节,将其与EDI指向位置的字节进行比较,并设置标志位,然后对ESI和EDI分别加1。如果repe前缀,就检查ECX的值和标志位,如果ECX=0或者ZF=0,就停止重复。这相当于C语言的memcmp函数。

​ scasb指令用于从一串字节中搜索一个值。这个值由AL寄存器给出。它的工作方式与cmpsb一样,但是它是将ESI指向地址的字节与AL进行比较,而不是与EDI指向地址的字节比较。repe操作会使得这个比较不断继续,直到找到该字节,或者ECX=0.如果在这串字节中找到了这个值,则其位置会被存储在ESi中。

​ stosb指令用于将值存储到EDI指向的地址。它与scasb一样,但不是去搜索,而是将指定的字节存入EDI指向的地址。rep前缀与scasb一起使用后,就初始化了一段内存缓冲区,其中的每个字节都是相同的值。这等价于C语言的memset函数。

rep指令实例:

指令描述
repe cmpsb用于比较两块数据缓冲区。EDI和ESI必须被设为两段缓冲区的地址,ECX必须被设为缓冲区长度。当ECX=0或者发现缓冲区不一致的时候,停止比较
rep stosb用于用一个给定的值初始化一块缓冲区中所有字节。EDI包含了缓冲区地址,AL则包含初始值。这个指令通常与xor eax,eax一起使用。
rep movsb一般用于复制缓冲区中的字节。ESI需要被设为原缓冲区地址,EDI被设为目的缓冲区地址,ECX则必须为要复制的长度。会逐字节复制,直至ECX=0.
repne scasb用于在一段数据缓冲区中搜索一个字节。EDI徐指向缓冲区地址,AL则包含要找的字节,ECX设为缓冲区长度。当ECX=0或找到该字节时,比较停止

参考书文:《恶意代码分析实战》

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值