快捷搜索:  网络  渗透  后门  CVE  扫描  木马  黑客  as

x86环境病毒阐发的反汇编基础知识

本文是一篇对x86环境下进行病毒阐发所需的基础反汇编知识的简单总结,更深层的知识还需要实战经验。

x86系统结构

大部分当代计算机系统结构(包括x86)在内部完成上遵循冯·诺依曼结构。这类结构包含3种硬件组件:

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

 • 内存(RAM),负责存储所有的数据以及代码。

 • 输入/输出体系(IO),为硬盘、键盘、显示器等设备提供接口。

指针(instruction pointer)的寄存器(register)从内存取得要执行的指令,这个寄存器中存有指令的地址。寄存器是CPU中数据的基本存储单元,通过它,很多时辰CPU再也不需要走访内存,从而节省了时间。算术逻辑单元(arithmetic logic unit, ALU)执行从内存取来的指令,并将效果放到寄存器或内存中。一条条取指令、执行指令的过程不断重复,就形成了程序的运行。

冯诺依曼系统结构

Image

1.内存

一个程序的内存可分为以下四个首要的节

Image

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

**代码** 

代码节包含了在执行程序义务时CPU所取得的指令。这些代码决定了程序是做什么的,和程序中的义务怎么样和谐工作。

**堆** 

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

**栈** 

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

2. 指令

指令是汇编程序的构成块。在x8六、汇编说话中,一条指令由一个助记符,和零个或多个操作数组成。

3. 操作码以及字节序

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

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

数据的字节序(enclianness)是指在一个大数据项中,最高位(大端,big-endian)照样最低位(小端,little-enclian)被排在第一位 (即排在最低的地址上)。

4. 操作数

操作数说明指令要使用的数据。有下列三种类型:

 立即数(i妹妹ediate)操作数是一个固定的值

 寄存器(register)操作数指向寄存器

 内存地址(memory address)操作数指向感兴致的值地点的内存地址,一般由方括号内包含值、寄存器或方程式组成,如[eax]。

5. 寄存器

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

最经常使用的x86寄存器,可以将它们回为下列四类:

通用寄存器,CPU在执行期间使用。

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

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

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

x86寄存器

Image

所有通用寄存器的大小都是32位,可以在汇编代码中以32位或16位引用。

有4个寄存器(EAX、EBX、ECX、EDX)还可以8位值的方式引用,从而使用其最低的8位,或次低的8位。

**通用寄存器**  

通用寄存器一般用于存储数据或内存地址,而且经常交换着使用以实现程序。无非,虽然它们被称为通用寄存器,但它们并不完全通用。

一些x86指令只能使用特定的寄存器。例如,乘法以及除法指令就只能使用EAX以及EDX。

**标志寄存器** 

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

对恶意代码阐发来说,最重要的一些标志先容以下:

**ZF**  当一个运算的效果等于0时,ZF被置位,否则被清除。

**CF**  当一个运算的效果相对目标操作数太大或太小时,CF被置位,否则被清除。

**SF**  当一 个运算的效果为负数,SF被置位;若效果为正数,SF被清除。对算术运算,当运算效果的最高位值为l时,SF也会被置位。

 **TF**  TF用于调试。当它被置位时,x86处理器每一次只执行一条指令。

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

注意:当EIP被损坏(即指向了一个不包含合法程序代码的内存地址 )时,CPU 没法取得一条合法指令来执行,此时正在运行的程序就可能崩溃。当你控制了EIP,也就控制了CPU将要执行什么,这也就是为什么攻击者试图通过漏洞行使获得对EIP的控制。通常,攻击者先妥使攻击代码进入内存,然后改变EIP使其指向那段代码,工控黑客 ,从而攻击体系。

 6. 简单指令

 **mov**  

用于将数据从一个地位挪移到另一个地位

mov指令可以将数据挪移到寄存器或内存,其格式是:mov destination, source

由方括号括起来的操作数是对内存中数据的引用。例如,[ebx]指向内存中地址为EBX处的数据。

 **lea**

lea指令用来将一个内存地址赋给目的操作数。

 “load effective address“(加载有用地址)的缩写。它的格式是lea destination, source。

例如,lea eax, [ebx+8]就将EBX+8的值给EAX。

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

算术运算

加法以及减法是从目标操作数中加上或减去一个值。

加法指令的格式是add destination, value。

减法的指令是sub destination, value。

sub指令会修改两个重要的标志:ZF以及CF。

要是效果为零,ZF被置位;要是目标操作数比要减去的值小,则CF被置位。

inc以及dec指令将一个寄存器加一以及减一 。

Image

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

mul指令的格式是mul value; 

div指令的格式是div value。

mul或div指令要操作的寄存器一般会在之前许多条指令之处被赋值,因此你可能需要在程序的上下文中来寻觅。

Image

mul value指令老是将eax乘上value。因此,EAX寄存器必须在乘法指令出现前就赋值好。乘法的效果以64位的情势分开存储在两个寄存器中:EDX以及EAX。其中,EDX存储了高的32位,EAX存储低的32位。

div value指令将EDX以及EAX合起来存储的64位值除以value。因此,在做除法之前,EDX以及EAX这两个寄存器必须赋值好。除法的商将存储到EAX,余数则存储在EDX中。

**模(mod)**运算会被编译为在div指令后取EDX寄存器的值(因为除法保留了余数)

逻辑运算

x86架构还使用逻辑运算符,例如OR、AND以及XOR。其相应指令的用法与add以及sub类似,对源操作数以及目的操作数做相应的操作,并将效果保存在目的操作数中。

xor eax, eax就一种将EAX寄存器快速置0的要领。这么做是为了优化,因为这条指令只要要2个字节,而mov eax, 0需要5个字节。

shr以及shl指令用于对寄存器做移位操作。

shr指令的格式是 “shr destination, count” 

shl指令的格式是 “shl destination, count” 

shr以及shl指令对目的操作数右移或左移,由count决定移若干位。移出目的操作数边界的位则会先挪移到CF标志位中。在移位时,使用0加添新的位。移位运算整个实现后,CF标志位中就包含了最后移出目的操作数的那一位。

轮回移位指令ror以及rol与移位指令类似,但移出的那一位会被填到另一端空出来的位上,即右轮回移位(ror)会将最低位轮回移到最高位;左轮回移位Crol)则相反。

移位经常被用于对乘法运算的优化。由于不需要像乘法那样配置寄存器、挪移数据,移位会更简单、更快。

在阐发恶意代码时,要是遇到一个函数中只有xor、or、and、shl、ror、shr、rol如许的指令,并且它们反复出现,看起来随机排列的模样,就多是遇到了一个加密或者压缩函数。最佳是将其标记为一个加密函数,然后继续后面的阐发。

Image

**NOP指令**

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

这条指令的opcode是Ox90。在缓冲区溢出攻击中,当攻击者没法完善地控制行使代码,就常用NOP滑板。它起到了加添代码的作用,以降低shellcode可能在中间部分最先执行所酿成的危害。

7. 栈

用于函数的内存、局部变量、流控制结构等被存储在栈中。

栈是一种用压以及弹操作来刻画的数据结构,向战中压入一些器械,然后再把它们弹出来。

它是一种后入先出(LIFO)的结构。

与栈有关的指令包括push、pop、call、leave、enter、以及ret。在内存中,栈被调配成自顶向下的,最高的地址开始被使用。当一个值被压入战时,使用低一点的地址。

栈只能用于短期存储。它经经常使用于保存局部变量、参数以及返归地址。其首要用途是治理函数调用之间的数据交换。而不同的编译器对这类治理要领的详细完成有所不同,但大部分常见约定都使用相对于EBP的地址来引用局部变量与参数。

函数调用

许多函数包含一 段 “序言” (prologue),它是在函数最先处的少数几行代码,用于保存函数中要用到的栈以及寄存器。相应的,在函数结尾的 “结语” (epilogue)则将技以及这些寄存器恢复至函数被调用前的状态。

下面列举了函数调用至多见的完成流程

> 1. 使用push指令将参数压入栈中。

> 2. 使用call memory_location来调用函数。此时,当前指令地址(指EIP寄存器中的内容)被压入栈中。这个地址会在函数结束后,被用于返归到主代码。当函数最先执行时,EIP的值被设为memory_location (即函数的肇始地址)。

> 3. 通过函数的序言部分,调配栈中用于局部变量的空间,EBP (基址指针)也被压入栈中。如许就达到了为调用函数保存EBP的目的。

> 4. 函数最先做它的工作。

> 5. 通过函数的结语部分,恢复。调整E归来开释局部变量,恢复EBP,以使得调用函数可以准确地定位它的变量。leave指令可以用作结语,因为它的功能是使ESP等于EBP,然后从栈中弹出EBP。

> 6. 函数通过调用ret指令返归。这个指令会从栈中弹出返归地址给EIP,因此程序会从原来调用之处继续执行。

> 7. 调整栈,以移除此前压入的参数,除非它们在后面还要被使用。

x86架构还提供了其他弹出以及压入的指令,其中最经常使用的是pusha以及pushad。它们将所有的寄存器都压入战中,并且常与popa以及popad结合使用,后者从栈中弹出所有的寄存器。

pusha以及pushad的详细功能以下。

> •    pusha下列面的顺序将所有16位寄存器压入栈中:AX、EX、DX 、BX 、SP、BP、SI、DI

> • pushad下列面的顺序将所有32位寄存器压入枝中:EAX、ECX 、EDX 、EBX 、ESP、EBP、ESI、 EDI

在shellcode中,要是要将寄存器的当前状态整个保存在栈上,以便稍后恢复,就常使用这些指令。编译器很少使用它们,因此,看到它们,通常说明是某人手工写的汇编代码或者shellcode。

8. 前提指令

至多见的两个前提指令是test以及cmp。

test指令与and指令的功能同样,但它并不会修改其使用的操作数。test指令只配置标志位。

对某个器械与它自身的test经常被用于检查它是不是一个NULL值。

cmp指令与sub指令的功能同样,但它不影响其操作数。cmp指令也是只用于配置标志位,其执行效果是,ZF以及CF标志位可能发生变迁。

9. 分支指令

至多见的分支指令是跳转指令。程序中使用了大批的跳转指令,其中最简单的是jmp指令,它使得下一条要被执行的指令是其格式jmp location中指定地位的指令,又被称为无前提跳转,因为总会跳到目的地位去执行。这个简单的跳转没法餍足所有的跳转需求。

Image

10. 重复指令

重复指令是一组操作数据缓冲区的指令。数据缓冲区一般为一个字节数组的情势,也可以是单字或者双字。

常见的数据缓冲区操作指令是movsx, cmpsx、stosx以及scasx,其中x可以是b、w或者d,分别表示字节、字以及双字。这些指令对任何情势的数据都有用。

在这些操作中,使用ESI以及EDI寄存器。ESI是源索引寄存器,EDI是目的索引寄存器。还有ECX用作计数的变量。

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

Image

在x86下,使用重复前缀来做多字节操作。rep指令会增添ESI以及EDI这两个偏移,缩小ECX寄存器。rep前缀会不断重复,直至ECX=O。repe/repz以及repne/repnz前缀则不断重复,直至ECX=O或直至ZF= 1或0。

movsb指令用于将一串字节从一个地位挪移到另一 个地位。rep前缀经常与movsb一块儿使用,从而复制一串长度由ECX 决定的字节。从逻辑上说,rep movsb指令等价于C说话的memcpy函数。movsb指令从ESI指向地址取出一 个字节,将其存入ED I指向地址,然后根据方向标志(DF)的配置,将ESI以及EDI的值加1或者减1。要是DF=O,则加,否则减。

在由C代码编译后的效果中,很少能看到DF标志。然则在shellcode里,人们无意候会调换方向标志,如许就可以反方向存储数据。要是有rep前缀,就会检查ECX是否为0,要是不等于0,则指令继续从ESI挪移一个字节到EDI并将ECX 寄存器减1。这个过程会不断重复,直至ECX=0。

cmpsb指令用于比较两串字节,以确定其是不是相同的数据。cmpsb指令用ESI指向地址的字简略节略去EDI指向地址的字节,并更新相干的标志位。它经常与repe前缀一块儿使用。此时,cmpsb指令逐个比较两串字节,直至发现一处不同,或比较到头。cmpsb指令从地址ESI获得一 个字节,将其与ED I指向地位的字节进行比较,并配置标志位,然后对ESI以及EDI分别加1。要是有「epe前缀,就检查ECX的值以及标志位,要是ECX=O或者ZF=O,就休止重复。这至关于C说话中的memcmp函数。

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

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

Image

Image

————参考资料《恶意代码阐发实战》,《汇编说话》

*

您可能还会对下面的文章感兴趣: