前言
年前曾写过一篇文章《CTF逆向选手入坑指南》https://www.freebuf.com/articles/web/225999.html当时只是大概的介绍了一下入坑CTF逆向所需要知道的一些基本概念、术语、还有需要学习的知识等,并没有对提到的知识点进行详细的讲解和案例讲解。这段时间买了刚出的新书《CTF特训营》和《从0到1:CTFer成长之路》这两本CTF教学书看,里面的逆向章节(CTF特训营第二篇、CTFer成长之路第5章)也提到了很多CTF逆向的技术和解题方法。但是因为篇幅原因书中都只是对提到的知识点进行了简略的介绍没有进行详细讲解。而且两本书中重复的内容和知识点还是比较多的,所以打算在读这两本书的时候综合吸收两本书的精华并融合补充一下两本书中的知识点,并把每个知识点找案例动手做一遍,然后进行详细的记录当作读书笔记,也方便和我一样刚入门的小伙伴们学到更多的知识。
因为自己平时工作是渗透加上最近工作经常出差,而且参加攻防演练和CTF也比较多,所以拿出大块的时间来写逆向的技术文章也不太现实,况且每个知识点找合适的案例做实验再把知识点讲解明白也比较麻烦,所以此系列文章可能会更新的比较慢,但是时间就像海绵里的水挤一挤总会有的,一定会坚持写完的(先立下flag,万一到时候打脸还请各位大佬轻喷)。在此声明此系列文章面向初学者和刚入门的新手,大佬勿喷,不过还是强烈建议高手大佬们阅读指导提出宝贵建议一起交流和补充,小弟一定虚心听取,不胜感激。
写作计划
不太了解CTF逆向基本概念的小伙伴可以先去看看我的那篇《CTF逆向选手入坑指南》大概了解一些基本概念和常识。
综合了一下《CTF特训营》和《从0到1:CTFer成长之路》两本书中的逆向章节(CTF特训营第二篇、CTFer成长之路第5章),我打算按照以下计划去写此系列文章:
1.汇编语言学习(主要讲解一下逆向分析的基础——汇编语言)
2.常用逆向分析工具讲解(讲解一下静态分析和动态调试的常用的反汇编调试工具(IDA\OD\GDB......))
3.常规逆向分析流程及常见算法识别(讲解拿到一个程序后应该怎么入手进行分析包括常见算法的识别)
4.自动化逆向工具讲解(讲解一些提高效率解决重复性工作的工具和脚本功能的使用和它们的原理)
5.干扰分析及代码保护技术的破解(介绍一下常见的二进制代码保护技术以及突破方法)
6.高级语言的逆向(讲解一下.Net、Python、Java、Rust、Go、C#、C++MFC等程序的逆向手法)
7.逆向中的技巧(讲解一些逆向中的技巧,包括HOOK、dump内存....等)
8.最终测验(挑选一些不同类型且比较常见的实战题目进行练习,实战演练一下自己的逆向分析和解题脚本编写能力)
Ps:移动端apk逆向相关的知识因能力和精力有限,以后再写吧。~~~
《CTF特训营》电子书链接:https://pan.baidu.com/s/1-ktFwsYZ-HPutW5tZZir9A提取码:37f2
《从0到1:CTFer成长之路》购买链接:https://item.jd.com/12950900.html
x86体系汇编语言讲解
0x00 汇编语言是什么
我们知道,CPU 只负责计算,本身不具备智能。你输入一条指令它就运行一次,然后停下来,等待下一条指令。这些指令都是二进制的,称为操作码(opcode)也就是机器指令,比如加法指令就是00000011。编译器的作用,就是将高级语言写好的程序,翻译成一条条CPU能够读懂的机器指令然后传给CPU执行。对于人类来说,二进制程序是不可读的,根本看不出来机器干了什么。为了解决可读性的问题,以及偶尔的编辑需求,就诞生了汇编语言。
0x01 汇编语言的应用领域
汇编语言的优点是能够直接访问计算机硬件,所以执行起来要比那些高级语言效率高,而且占用资源极少,一般用于:系统内核、工业控制、驱动程序、实时系统、核心算法等对运行效率有极高要求的领域,或者说为了突破系统性能的瓶颈而将频繁使用的子程序或程序段用汇编来编写。
因为汇编语言是一种面向机器的语言,其汇编指令与机器指令基本上一一对应,所以汇编指令就和机器指令一样功能单一且具体,例如想要机算(A+B+C)那么我们就必须细化的安排CPU要进行运算的每一步(先计算A+B,然后把C与前者运算的结果相加),而且编写汇编语言程序时要考虑资源的限制,所以使用汇编语言开发程序对程序员的编程水平要求极高,而且开发程序的周期特别特别长,为了实现一个简单的功能都得非常的麻烦,所以现在已经很少有人编写纯汇编的应用程序了,但是我们学习CTF逆向不需要把汇编学到能开发大型应用程序的境界,我们的目的是读懂汇编,理解它的特性。因为汇编语言本身就是机器指令的一种符号表示,不同类型的CPU有不同的机器指令系统,所以汇编语言也有不同的类型,本文主要介绍的是目前最常见的也就是Intel CPU使用的x86汇编语言。
0x02 机器语言、汇编语言、高级语言的关系
最早的计算机采用机器语言,这种语言直接用二进制数表示,通过直接输入二进制数,插拔电路板等实现,也就是传说中的只用0和1编程,这种“编程”很容易出错,每个命令都是通过查命令表实现,既然是通过“查表”实现的,那当然也可以让计算机来代替人查表实现了。于是就产生了汇编语言,所以我们可以认为机器语言和汇编语言二者就是等价的。后来人们发现,用汇编语言编某一功能的时候,连续一段代码都是相同或相似,于是就考虑用一句语言来代替这一段汇编语言,于是就产生了高级语言(C\C++等)。因此,所有高级语言都能转化成汇编语言,而所有汇编语言又可转化成机器语言。反之,所有机器语言也可以转成汇编语言(因为二者等价)。但并不是所有汇编语言都能转成高级语言。
0x03 认识寄存器
我们首先要搞清楚寄存器和内存不是一种东西,寄存器是在CPU中,内存在内存条中。寄存器的存取速度相当于内存的100倍左右。CPU本身只负责运算,不负责储存数据。数据一般都储存在内存之中,CPU 要用的时候就去内存读写数据。但是,CPU 的运算速度远高于内存的读写速度,为了避免被内存的读写速度拖慢,CPU 都自带一级缓存和二级缓存。CPU 缓存可以看作是读写速度较快的内存。但是,CPU 缓存还是不够快,另外数据在缓存里面的地址是不固定的,CPU 每次读写都要先寻址再读写,这样会拖慢速度。因此,除了缓存之外,CPU还自带了寄存器(Register),用来储存最常用的数据。也就是说,那些最频繁读写的数据(例如循环变量),都会放在寄存器里面,CPU优先读写寄存器,再由寄存器跟内存交换数据。寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称,我们告诉 CPU 去具体的哪一个寄存器拿数据,这样的速度是最快的,所以寄存器被喻为CPU的零级缓存。
通用寄存器
先讲一讲通用寄存器,顾名思义,通用寄存器是一种通用型的寄存器,用于传送和暂存数据,也可以参与算数逻辑运算,并保存运算结果。早期x86 CPU只有8个寄存器,并且每个的用途都不相同。
我们所说的32位CPU、64位CPU的“位”,就是指的寄存器的大小,32位CPU的寄存器大小就是4个字节,也就是4x8=32位
通用寄存器的作用(32位):
EAX:累加器,在乘法和除法指令中被自动使用;在Win32中,一般用在函数的返回值中。
ECX:计数器,CPU自动使用ECX作为循环计数器,在字符串和循环操作中常用,在循环指令(LOOP)或字符串操作中,ECX用来进行循环计数,每执行一次循环,ECX都会被CPU自动减一。
EDX:数据寄存器,常被用来放整数除法产生的余数。
EBX:基地址寄存器,在内存寻址时存放基地址。
ESP:指向最上面一个栈帧的栈顶,ESP用来寻址堆栈上的数据,ESP寄存器一般不参与算数运算,通常称为堆栈指针寄存器。
EBP:指向最上面一个栈帧的底部,EBP由高级语言用来引用参数和局部变量。
ESI:一般在字符串操作时指向源串。
EDI:一般在字符串操作时指向目标串。
再来说一说指令寄存器EIP:存放的是下一条要执行的指令地址,几乎不可以作为他用。
16位的通用寄存器是:AX、BX、CX、DX、SI、DI、BP、SP
其中前四个(AX、BX、CX、DX)每个还可以分成高8位(8到15)和低8位(0到7)两个独立的寄存器
所以说8位的通用寄存器是:高8位:AH BH CH DH 低8位:AL BL CL DL
对其中某8位的操作,并不影响另外对应的8位的数据。
标志寄存器
CPU内部的寄存器中,有一种特殊的寄存器具有以下三种作用。
1.用来存储相关指令的某些执行结果;
2.用来为CPU执行相关指令提供行为依据;
3.用来控制CPU的相关工作方式;
这种特殊的寄存器被称为标志寄存器(EFLAG),x86的标志寄存器有32位。我们了解一下都是什么作用。
标志寄存器
它们大致分为两类:状态标志和控制标志。
状态标志(CF\ZF\SF\PF\OF\AF):用来记录程序运行结果得状态信息,许多指令得执行都将相应地设置它。
控制标志(DF\IF\TF):可由程序根据需要用指令设置,用于控制处理器执行指令的方式。
下面详细的说一下各个标志位:
进位标志CF(Carry Flag):当运算结果的最高有效位有进位(加法)或借位(减法)时,进位标志置1,即CF=1,否则CF=0。
例如(十六进制算数运算):3A+7C=B6,没有进位:CF=0;AA+7C=(1)26,有进位,CF=1。
零标志ZF(Zero Flag):若运算结果为0,则ZF=1;否则ZF=0;(注意:ZF为1表示的结果是0)。
例如:3A+7C=B6,结果不是零:ZF=0;84+7C=(1)00,结果是零:ZF=1。
符号标志SF(Sign Flag):运算结果最高位为1,则SF=1;否则SF=0(有符号数据用最高有效位表示数据的符号)。
例如:3A+7C=B6,最高位为1,SF=1,84+7C=(1)00,最高位为0,SF=0;
奇偶标志PF(Parity Flag):奇偶标志PF用于反映运算结果中“1”的个数的奇偶性。如果“1”的个数为偶数,则PF的值为1,否则其值为0。
溢出标志OF(Overflow Flag):若算数运算的结果有溢出,则OF=1,否则OF=0。
什么是溢出:例如16位的范围是(+32767~-32768),如果运算结果超出这个范围,就产生了溢出,有溢出,说明有符号数的运算结果不正确。
溢出和进位:溢出标志OF和进位标志CF是两个意义不同的标志,进位标志表示无符号数运算结果是否超出范围,运算结果仍然正确;溢出标志表示有符号数运算结果是否超出范围,运算结果已经不正确。
什么是有符号数和无符号数:有符号数就是用最高位表示符号(正或负),其余位表示数值大小,无符号数则所有位都用于表示数的大小有符号数和无符号数是针对二进制来讲的。有符号数用最高位作为符号位,“0”代表“+”,“1”代表“-”;其余数位用作数值位,代表数值。比如:0011 表示 +3;1011 表示 -3。无符号数全部二进制均代表数值,没有符号位。即第一个"0"或"1"不表示正负。比如:0011 表示 3;1011 表示 11。
辅助进位标志AF(Auxiliary Carry Flag):在发生下列情况时,辅助进位标志AF的值被置为1,否则其值为0:(1)、在字操作时,发生低字节向高字节进位或借位时;(2)、在字节操作时,发生低4位向高4位进位或借位时。(这个标志主要由处理器内部使用,用于十进制算数运算调整指令中,用户一般不必关心)
方向标志DF(Direction Flag):用于串操作指令中,控制地址的变化方向:设置DF=0,存储器地址自动增加。DF=1,存储器地址自动减少。
例如:CLD指令用于复位方向标志,执行后DF=0;STD指令用于置位方向标志,执行后DF=1;
中断允许标志IF(Interrupt-enable Flag):中断允许标志IF是用来决定CPU是否响应CPU外部的可屏蔽中断发出的中断请求。但不管该标志为何值,CPU都必须响应CPU外部的不可屏蔽中断所发出的中断请求,以及CPU内部产生的中断请求。具体规定如下:(1)、当IF=1时,CPU可以响应CPU外部的可屏蔽中断发出的中断请求;(2)、当IF=0时,CPU不响应CPU外部的可屏蔽中断发出的中断请求。CPU的指令系统中也有专门的指令来改变标志位IF的值。
例如:CLI指令用于复位中断标志,执行后IF=0;STI指令用于置位中断标志,执行后IF=1;
陷阱标志TF(Trap Flag):当TF被置为1时,CPU进入单步执行方式,即每执行一条指令,产生一个单步中断请求。这种方式主要用于程序的调试。指令系统中没有专门的指令来改变标志位TF的值,但程序员可用其它办法来改变其值。
段寄存器
段寄存器用来存放段的基地址,段是一块预分配的内存区域。有些段存放有程序的指令,有些则存放有程序的变量,另外还有其他的段,如堆栈段存放着函数变量和函数参数等。在16位CPU中,段寄存器只有4个,分别时CS(代码段)、DS(数据段)、SS(堆栈段)、ES(附加数据段)。
CS:指明代码段的起始地址,代码段用来存放程序的指令序列,处理器利用CS:IP取得下一条要执行的指令。
DS:指明数据段的起始地址,数据段存放运行程序所用的数据,处理器利用DS:EA(有效地址)存取数据段中的数据
SS:指明堆栈段的起始地址,堆栈段确定堆栈所在的主存区域,处理器利用SS:SP操作堆栈顶的数据
ES:指明附加段的起始地址,附加段是附加的数据段,也用于数据的保存,处理器利用ES:EA存取附加段中的数据,串操作指令将附加段作为其目的操作数的存放区域。
在32位CPU中,段寄存器从4个扩展为6个,分别时CS、DS、SS、ES、FS、GS。FS和GS段寄存器也属于附加的段寄存器,每个段寄存器用来确定一个逻辑段的其实地址,每种逻辑段均有各自的用途。
段寄存器的相关知识对于新手学习来说是比较难的,所以我们开始并不需要完全掌握,只做个大概了解,随着代码逆向分析技术水平提高,需要学习段寄存器时再深入学习亦可。
0x04 寻址方式
在学习寻址方式之前我们先来学习一下指令是什么,指令由操作码和操作数两部分组成,例如:mov eax,12 就是一条指令(把12赋值给eax),其中的mov就是操作码,它用来说明计算机要执行哪种操作,比如:传送、运算、移位、跳转等操作,它是指令中不可缺少的组成部分。ax(目的操作数)和12(源操作数)被称作是操作数,操作数是指指令执行的参与者,即各种操作的对象,有些指令不需要操作数,通常的指令都有一个或两个操作数,也有个别指令有三个甚至四个操作数。
每种指令的操作码用一个唯一的助记符表示(指令功能的英文缩写),对应着机器指令的一个二进制编码。
指令中的操作数可以数一个具体的数值,可以是存放数据的寄存器,或指明数据在储存位置的存储器地址。
下面我们来学习一下寻址方式,我们知道在程序的运行过程中,CPU会不断的处理数据。CPU处理的数据通常来自三个地方:
1.数据在指令中直接给出
2.数据在寄存器中保存
3.数据在内存中保存
我们在使用高级语言进行开发的时候,CPU如何对数据处理对于程序员来说是不需要关心的,编译器会在代码编译的时候进行这些处理。而在使用汇编语言编写程序时,指令操作的数据来自何处,CPU应该从哪里取出数据,则是汇编程序员需要自己解决的问题。CPU寻找最终要操作数据的过程,称为寻址。
寻址方式介绍:
1.指令中给出的数据:例如:MOV ESI,00403010 操作数直接放在指令中,作为指令的一部分存放在代码里,这种方式称为立即数寻址。执行完上面的指令后ESI寄存器的值是指令中给出的值00403010
2.数据在寄存器中:例如:MOV EAX,00403000 MOV ESI,EAX 第一条指令是立即数寻址,将00403000放入EAX寄存器中;第二条指令是把EAX寄存器中的值传递给ESI寄存器。因此,ESI寄存器中的值是00403000。这种寻址方式称为寄存器寻址。
3.数据在内存中:数据在内存中存放可以有多种方式,主要有直接寻址、寄存器间接寻址、变址寻址和基址变址寻址。
直接寻址:在指令中直接给出操作数所在的内存地址称为直接寻址,例如:
MOV DWORD PTR [005A6110],12345678
MOV EAX,DWORD PTR [005A6110]
我们在OD中调试一下这两条指令
寄存器间接寻址:操作数的地址由寄存器给出,这里的地址指的是内存地址,而实际的操作数存储在内存中。例如:MOV DWORD PTR[403000],12345678
MOV EAX,00403000 MOV EDX,[EAX] 上面3条指令执行完成后,EDX寄存器取到了内存地址为00403000处的值,即12345678
其他寻址方式:其他还有寄存器相对寻址、变址寻址、基址变址寻址、比例因子寻址这里就不再一一介绍了,大家可自己搜索了解。
0x05 认识堆栈
堆栈是一个“后进先出LIFO”(或者说是“先进后出FILO”)的内存区域,位于堆栈段中,它的本质还是一块内存,堆栈的内存分配是由高地址向低地址延伸的;SS段寄存器记录其段地址。
堆栈只有一个出口,即当前栈顶,用堆栈指针寄存器ESP指定,栈顶是地址较小的一端(低端),栈底不变。
堆栈的用途:
1.用于储存临时的数据
2.高级语言中参数的传递
栈的基本操作有2种:进栈和出栈,分别对应指令PUSH和POP,这两个指令一般会成对出现,前面有push,后面就一定会有POP,执行入栈和出栈指令时总是在ESP寄存器的一端。
PUSH(压栈):功能是向堆栈中(栈顶)压入数据,修改栈顶指针ESP寄存器的值(减4)
例如原先的栈顶指针ESP的值为下图的0019C6EC,我们执行完push 0x666之后数据666就存入到了堆栈种,栈顶指针也减了4,ESP变为了0019C6E8(因为程序在使用堆栈的时候,是从顶部往底部存储,空间也就会越存越小,直到存不下的时候,它就崩溃了)。
POP(出栈):功能是将堆栈中(栈顶)数据存储到寄存器或内存,修改栈顶指针ESP的值(加4)
在逆向分析的时候我们一定要清楚堆栈的变化,熟练的画堆栈图,大家可以参考这些文章,试着自己练习一下。
https://www.cnblogs.com/Tkitn/p/12354131.html
https://blog.csdn.net/qq_41904330/article/details/101840311
0x06 常见汇编指令介绍
我们在实际的逆向工程中,大多数情况都是面对的一行一行的汇编指令,在逆向工程中需要用到的汇编语言知识并没有那么多,正如WIndows程序员没必要记住所有的Windows API函数一样,做逆向工程我们也没必要记住所有的汇编指令,我们只需要知道常见的汇编指令(也就大概20到50条左右),了解其大致的作用,其余的不常用或者生僻的指令可以通过查手册或文档进行学习,我们查询指令格式的时候,会看到以下的样式。
这里面的mov是汇编指令,r指的是寄存器,m指内存,imm指立即数,8、16、32指的是数据的宽度,分别是8位、16位和32位。下面给大家列举一下常见的汇编指令。
数据传递指令
1.mov指令
mov指令是最常见的数据传输指令,相当于赋值操作,该指令的操作数有2个,mov 目的操作数,源操作数
mov指令可以实现寄存器与寄存器之间、寄存器与内存之间、寄存器与立即数之间、内存与立即数之间的数据传递。
需要注意,内存与内存之间是无法直接传递数据的,目的操作数不能为立即数,两个操作数的宽度必须一致。
用法示例:
mov eax,12345678h (12345678h是16进制数,在OD中数值默认是16进制数,所以省略显示后面的h)
mov eax,dword ptr [00401000h] ([00401000h]是一个内存地址,因为用[]括起来了,前面的dword 是数据宽度)
mov eax,ebx
mov word ptr [00401000h],1234h
2.xchg指令
xchg指令的功能是交换两个操作数的数据。该指令有两个参数,分别是源操作数和目的操作数。
xchg指令允许寄存器和寄存器之间交换数据,也允许寄存器和内存之间交换数据,但是内存和内存之间是不能进行数据交换的。
用法示例:
xchg eax,ebx
xchg dword ptr ds:[403020],ebx
3.lea指令
lea指令,即装入有效地址指令,它将内存单元的地址送至指定的寄存器。和mov的用法不同,它的操作数虽然也是内存单元,但是它获取到的是内存单元的地址,而不是内存单元中的数据。
例如:
MOV EAX,DWORD PTR DS:[403000]
LEA EBX,DWORD PTR DS:[403000]
我们将上面两条指令输入到OD反汇编窗口中运行观察
我们可以看到运行完第一条指令后,eax中存放的是地址403000中的内容 因为是小端存储,所以内存中显示为FFFF8D50,存入eax变为508DFFFF
我们可以看到运行完第二条指令,ebx中存放的是内存单元的地址编号,也就是00403000
逻辑运算指令
1.and指令
and指令是逻辑按位“与”,用于将目的操作数中的每个数据位与原操作数中的对应位进行逻辑与操作。
汇编语言中 AND 运算符运算的时候是按位操作的。如下例所示,X 中的每一位都与 Y 中的相应位进行 AND 运算:
对应的位在进行“与”操作的时,对应的位同为1时结果是1,否则相“与”的结果为0
and指令影响的标志位有OF\SF\ZF\PF\CF。因为汇编中的逻辑运算是按“位”进行运算的,所以在进行数值运算的时候切记要转化为二进制后进行运算。
2.or指令
or指令是逻辑按位“或”运算指令,用于将目的操作数中的每个数据位与源操作数中的对应位进行逻辑或操作。
对应的位在进行“或”操作时,对应的位同为0时,结果是0,否则相“或”的结果为1
or指令影响的标志位有OF\SF\ZF\PF\CF
3.not指令
not指令指令触发(翻转)操作数中的所有位。其结果被称为反码。
例如:取F0h 的反码 0Fh
mov al,11110000b
not al
此时的AL = 00001111b
not指令不影响标志位
4.xor指令
XOR 指令在两个操作数的对应位之间进行(按位)逻辑异或(XOR)操作,并将结果存放在目标操作数中。
异或是可逆运算。1 xor 1=0,0 xor 0=0,1 xor 0=1,0 xor 1=1。
a xor b的运算方法:将a,b 转化为2进制数,再进行对比,每个数位上的0或1如果相同,那么结果就取0,如果不同就取1,将得到的结果转化为原来进制的数,就是结果。
如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。
算数运算指令
1.add指令
add指令是加法指令,将源操作数和目的操作数相加,相加的结果储存在目的操作数中,操作数的长度必须相同。例如:add AL,BL
2.sub指令
sub指令是减法指令,将源操作数和目的操作数相减,相减的结果储存在目的操作数中。
3.adc指令
adc指令是带进位的加法,类似于add指令,区别在于将目的操作数与源操作数相加后,需要再加上标志寄存器CF位的值,执行adc指令后的结果为目的操作数=目的操作数+源操作数+CF位的值
例如:
此时右下角CF位为1 eax的值为0 运行 adc eax,0x5 之后就 eax的值就变为了0+5+1=6
4.sbb指令
sbb和上面的adc指令相反,sbb指令是带借位的减法,类似于sub指令,区别在于将目的操作数与源操作数相减后,需要再减去标志寄存器CF位的值,执行sbb指令后的结果为目的操作数=目的操作数-源操作数-CF位的值
例如:
此时eax的值为6 ,右下角CF的值为1,运行完sbb eax,0x2 指令后 eax的值就为 6-2-1=3
5.inc指令
inc指令是加一指令,用于对目的操作数进行加一操作。
例如:inc eax inc dword ptr ds:[403000]
其实从功能上来说 inc eax指令与 add eax,1 是相同的,但是转换成机器码执行的话 inc eax的机器码更短,执行速度会更快。
6.dec指令
inc指令是减一指令,用于对目的操作数进行减一操作。
例如:dec eax dec dword ptr ds:[403000]
堆栈操作指令
在前面我们了解堆栈的时候已经知道了push和pop指令的作用我们打开OD练习一下堆栈的操作,输入以下指令顺序执行,然后观察堆栈和相关寄存器的变化。
没有运行指令之前堆栈情况如下图所示。
我们输入以上指令开始按F8一条一条执行,然后观察每一步的变化
全部执行完毕后eax\ebx\ecx应该是如下值
还有2个指令是pushad和popad,它们的作用是保存/恢复通用寄存器现场,pushad指令在堆栈上按顺序压入所有的32位通用寄存器,顺序依次是eax、ecx、edx、ebx、esp、ebp、esi、edi。popad指令以相反的顺序从堆栈中弹出这些通用寄存器。
例如下图,执行完pushad后,被修改的寄存器只有esp寄存器,因为8个通用寄存器入栈以后,esp寄存器会指向新得栈顶,pushad指令压入的esp寄存器的值是在esp寄存器被改变之前的值。
虽然中间进行了eax,ebx的mov赋值运算,但是执行完popad指令之后,寄存器又全部恢复了原样。
还有两个指令分别是pushfd和popfd,pushfd指令在堆栈上压入32位的EFLAGS标志寄存器的值,popfd指令将堆栈顶部的值弹出并送至EFLAGS标志寄存器。
下图所示执行完pushad指令后,堆栈中压入了eflags的值0x246,因为前面在认识寄存器的时候讲过,eflags标志寄存器也是一个32位的寄存器,这点千万不要忘记,并不是分别压入 0,1,0,1,0,1,0,1这种值。
常用的保存eflags标志寄存器不被修改的方法是:
pusdfd
其他可能修改eflags标志寄存器的语句
popfd
转移指令
我们前面介绍的那些指令可以对通用寄存器进行操作,但是并不能改变程序的执行流程,也就是我们没办法改变EIP寄存器的值。因为EIP是在指令执行过程中自动修改变化的,那么现在我们就讲一讲如何改变EIP寄存器的值,从而使得程序可以跳跃执行,而不是顺序执行,转移指令用于实现分支、循环、过程(函数)等程序结构。
1.无条件转移指令
jmp指令是一条无条件转移指令。只要遇到jmp指令,即跳转到相应的地址进行执行。
jmp后面可以跟寄存器,内存地址,立即数
例如:
jmp 00401022
mov eax,0040102A
jmp eax
jmp dword ptr ds:[403000]
例如:下图中我们没执行 jmp edx 指令之前我们关注一下edx的值为5273d4,eip的值为53e746(也就是我们下一步要执行的指令的位置)
我们下一步执行 jmp edx 然后我们再看一下 EIP,就跳到了edx的位置进行执行了
2.条件转移指令
条件转移指令有多条,通常称条件转移指令为jcc指令集:jcc指令决定它跳不跳转跟别的没关系。只跟EFLAG标志寄存器有关系,会根据eflag标志寄存器中不同的标志位决定如何进行跳转。
下面介绍两个经常与jcc指令配合使用的指令,分别是测试指令(test)和比较指令(cmp)
1.test指令
test对两个操作数进行逻辑与运算,结果不送入目的操作数,但影响标志位OF\SF\ZF\PF和CF
test指令通常用于测试一些条件是否满足
2.cmp指令
比较指令cmp对两个操作数进行比较,比较的方式相当于用目的操作数减源操作数的减法操作,但是cmp只影响相应的标志寄存器,不会将减法的结果送入目的操作数中。
该指令影响的标志位有OF\SF\ZF\AF\PF\CF
大家可以在OD中练习一下下面的指令:
上面指令先使用了mov对eax,ebx分别赋值,然后使用cmp比较了eax和ebx的值是否相等,相等的话就跳转到0040102B(也就是mov ecx,2)这条指令处,使得ecx赋值为2,不想等则直接ecx的值为1,然后jmp跳走(相当于C语言中的if/else结构)。
上面运算的结果肯定是JE不会跳转,因为EAX和EBX不相等,那么我们如何使得JE指令跳转呢,我们可以将EAX和EBX修改为相同的值,这样是在代码上进行修改。我们还可以使用另一种方式,就是在执行完cmp指令时,在OD的寄存器窗口通过双击ZF标志位后面的值来改变ZF标志位的方式使得JE指令跳转,因为JE指令是否跳转主要依赖于ZF标志位(该方法在分析软件流程的时候非常有用)。
在一般的编程语言中,都是通过if、switch等保留字来实现条件分支的,而在汇编中则是通过控制标志的cmp、test指令,以及根据标志完成分支的跳转类指令来实现的。
3.循环指令
loop指令是循环控制指令,需要使用ecx寄存器来进行循环计数,当执行到loop指令时,先将ecx寄存器中的值减1,如果ecx寄存器中的值大于0,则转移到loop指令后的地址处,如果ecx寄存器中的值等于0,则执行loop指令的下一条指令。
大家可以在OD中练习一下如下代码:
在代码中LOOP后面的00401020是add eax,ecx指令的地址,大家可以根据情况修改为自己OD中显示的地址。进行F8单步执行的时候注意观察寄存器ecx的变化和循环的次数。
4.调用过程(函数)指令和返回指令
call指令是调用过程(函数)的指令,它的作用类似于jmp指令。可以修改EIP寄存器的值,从而使指令转移到其他地址继续执行。与jmp指令不同的地方是,call指令在修改EIP寄存器的值之前,会将call指令的下一条指令的地址保存至堆栈,以便在调用过程(函数)执行完之后再返回到原来call指令处执行下一条指令。
call指令调用函数时,函数的参数是通过push指令存放在栈中的。
例如:fun(1,2,3);
汇编语言中传参就是
push 3
push 2
push 1
call fun
在汇编语言中,参数是按照从后往前的顺序入栈的(这方面的规则会根据CPU和编译器的不同而存在一些差异,所以大家只需要记住“参数是通过栈来传递的”就可以了)。
call指令是调用函数的,且函数的返回值一般都被存放在eax中(大多数情况)。
ret指令用于过程(函数)的返回,该指令从堆栈的栈顶中弹出4个字节(32位系统)送入EIP寄存器中。一般该指令在过程(函数)需要返回的位置或者是过程(函数)的结尾处。call指令调用过程(函数)时会将call指令的下一条指令压入栈顶,当过程(函数)执行中遇到ret指令时,会将call指令压入的指令弹出送入EIP寄存器中,这样代码的流程就会接着call指令的下一条指令继续执行。
ret指令不需要修正堆栈栈顶的位置直接返回,retn指令则需要修正堆栈栈顶的位置后再进行返回。
我们在OD中输入以下指令调试练习:
首先在执行call之前需要观察堆栈,然后使用F7单步运行步入call,然后再次观察EIP和堆栈。
步入前后的变化如下图所示
我们可以看到F7键单步步入call指令后,堆栈栈顶由原来的0012FF8C变为0012FF88,并且将00401005这个地址保存在了堆栈中。并且此时EIP寄存器的值位00401024,也就是当前要执行的指令就是在RETN处。
我们在此处按下F8后,会将栈顶的值00401005送入EIP寄存器中,并且堆栈栈顶又变回到了原来的0012FF8C处。查看反汇编窗口,当前要执行的代码停留在了地址为00401005处的mov eax,00401024处。
串操作指令
串操作指令主要操作在内存中连续区域的数据,此处讨论movs、stos和rep三个常用的指令。
1.串传送指令
串传送指令MOVS是借助ESI寄存器和EDI寄存器,把内存中源地址(ESI指向源地址)的数据送入内存的目的地址(EDI指向目的地址)中。MOVS指令有MOVSB、MOVSW、MOVSD三种宽度。
在默认情况下,MOVS相当于MOVSD(32位操作系统下)
在执行了MOVS指令后,ESI寄存器和EDI寄存器指向的地址会自动增加1个单位(根据指令增加1个字节、2个字节或4个字节)或者自动减少1个单位(根据指令减少1个字节、2个字节或4个字节)。两个寄存器指向的地址是增加还是减少,需要依赖EFLAGS标志寄存器的DF标志位进行控制。当DF标志位为0时,执行MOVS指令后ESI寄存器和EDI寄存器指向的地址会自增;当DF标志位为1时,执行MOVS指令后ESI寄存器和EDI寄存器指向的地址会自减。
我们在OD调试器中练习如下代码:
上面代码中,CLD指令是对DF标志位进行复位,也就是设置DF标志位为0;STD指令是对DF标志位进行置位,也就是设置DF标志位为1。在执行MOVS后我们注意观察ESI寄存器和EDI寄存器的值的变化,以及其指向的地址中的值的变化。
2.串存储指令
串存储指令STOS是将AL/AX/EAX的值存储到EDI寄存器指向的内存单元。STOS指令有STOSB、STOSW和STOSD三种宽度。
在默认情况下,STOS相当于STOSD(32位操作系统下)
在执行了STOS指令后,EDI寄存器指向的地址会自动增加1个单位(根据指令增加1个字节、2个字节或4个字节)或者自动减少1个单位(根据指令减少1个字节、2个字节或4个字节)。EDI寄存器指向的地址是增加还是减少,需要依赖EFLAGS标志寄存器的DF标志位进行控制。当DF标志位为0时,执行STOS指令后EDI寄存器指向的地址会自增;当DF标志位为1时,执行STOS指令后EDI寄存器指向的地址会自减。
大家在OD中可以练习调试以下代码:
在执行MOVS指令后,注意观察 EDI寄存器的值的变化,以及其指向的地址中的值的变化。有时候在初始化某块缓冲区时会用到STOS指令。
3.重复前缀指令
REP(REPeat)指令就是“重复”的意思,因为既然是传递字符串,则不可能一个字节一个字节地传送,所以需要有一个寄存器来控制串长度,这个寄存器就是ECX,
指令每次执行前都会判断ECX的值是否为0(为0结束重复,不为0,ECX的值减1),从此来设定重复执行的次数。
下面我们练习一下,例如:
把当前数据段中偏移1000H开始的100个字节数据传送到偏移2000H开始的单元中
第一种写法:
第二种写法:使用loop
第三种写法:使用rep
0x07 程序实例演示
接下来通过实验破解一个很简单的小程序来巩固一下我们上面所学的汇编语言。
我已经将实验用的小程序和讲解视频放到了网盘中有需要的小伙伴可以下载跟着练习:
链接:https://pan.baidu.com/s/1bJNSYQ6hJiut61rBD6CC0Q
提取码:wty0
小结
汇编的知识就先基本介绍到这里,以上的学完基本上可以满足大家阅读简单的汇编代码的需要,大家如果想进一步学习汇编语言的话给大家分享一个滴水的汇编课程:网盘链接:https://pan.baidu.com/s/1qGcR7CxbR-HnsStqVELfvQ提取码:jobe
学习逆向要打牢基础,先过了语言关,其实语言关不止汇编语言,还包括C语言,Windows编程等这些在我《CTF逆向选手入坑指南》中也有提到这些都是必备的基础知识。
学习汇编语言推荐大家:《汇编语言基于x86处理器》这本书,只需要看完前十章就可以了。
学习C、C语言的话就推荐大家:《C Primer Plus》和《C Primer Plus》学习C和C++不错的入门书。
学习Windows编程的话推荐大家:《Windows核心编程(第五版)》掌握常用API。
其实在CTF逆向中你可能会遇到各种形式的汇编语言,也可能是x64、mips等等,所谓一法通则万法通,我们要先学会最基础的再去了解其他形式。学习了汇编语言大家还需要在动态调试和静态分析多练习多阅读,下一篇文章会给大家讲解一下OD、IDA等动态调试和静态分析工具的使用。
参考资料
冀云《逆向分析实战》
李承远《逆向工程核心原理》
https://www.cnblogs.com/chuijingjing/p/9318880.html
http://www.ruanyifeng.com/blog/2018/01/assembly-language-primer.html
https://www.jb51.net/article/178846.htm
https://zhuanlan.zhihu.com/p/78478567
https://blog.csdn.net/qq_43573676/article/details/104313857