Beansjian's Develop Blog

C/C++ 反汇编与逆向分析

字数统计: 3.4k阅读时长: 12 min
2019/04/19 Share

这篇文章是我学习《C++反汇编与逆向技术》这本书第二章内容的自我总结。

前言

什么是逆向工程

先阐述一下什么是逆向工程吧!想必很多同学甚至学信息技术的大学生对此也不太了解。

逆向工程,其实就是看成品,反推出制作过程的过程,并让其变成自己的技术。比如最近闹得火的芯片。如果你拿到一个集成的芯片,那么这个芯片就是一个黑箱。你输入,他有输出。但他黑箱内部具体是什么,你很难知道。这就需要逆向工程的帮助来还原芯片内部结构。

那么在计算机软件领域,逆向工程就是将程序反编译成源代码。比如,大家都用过C/C++,都知道在程序编译完成后会生成exe文件。将这个exe文件反编译回C语言的代码就是逆向工程。

新的旅程

那么,我是怎么接触到这个东西的呢?在初中的时候,我记得有一款游戏叫Kingdom Rush,他里面的英雄需要付费解锁。当时我用的是破解的iphone4,我就自己摸索着找到了一个hero所在的参数地点。将False 改成Ture后,我就可以免费用英雄了!这第一次激起了我对逆向的兴趣,虽然当时我还不知道什么是逆向。

大学里,当我看到有一篇文章叫一个非酋的逆向旅程(对!就是这个垃圾游戏!!)再次激起了我对这个领域的兴趣,我开始自学逆向工程。在此过程中,我找到了一个叫CTF(capture the flag)的比赛。其中就有逆向的题目。我很兴奋,终于找到组织了!!然而我参加了几次比赛都是0输出,0贡献(尴尬。。。)。所以我想静下心来,潜心修炼。

自我感想

不像ACM,入门的门槛较低(我认为的,大佬们嘴下留情啊。。),CTF比赛中的逆向,你没学到一定境界,就是0输出。这个领域,需要的知识杂而广,深且难。但我还是想在这个领域走下去,因为逆向给我带来的快乐是无与伦比的。

在此,既然学习了,我就想在博客里做一个记录。虽然远不及那些大佬写的文章,但我希望我的博客会对读者有帮助,也希望我的这个博客可以记录我20岁开始还没掉头发的青春。

整数类型的表现形式

在C中,整形的数据类型有三种:int,long,short。int,long占用4个字节(32 bits),short占用两个
由于二进制不方便阅读和显示,所以内存里的数据采用十六进制数显示。一个字节由两个十六进制数组成。
整数类型可分有符号型和无符号型。其区别在于是否有符号位。负数在内存中以补码形式存在。
正数区间0x00000000~0x7FFFFFFF
负数区间0x80000000~0xFFFFFFFF
以上内容大学课堂里即使你没认真上过也应该知道,所以在这里不做赘述。

浮点数的表现形式

浮点数的编码

在C中,浮点数有两种数据类型:

float(单精度),占4字节。二进制中,最高为为符号位,接下来8位表示指数部分,剩下23位为尾数部分

avatar

double(双精度),占8字节。最高位为符号位,指数占11位,52位为尾数位。

这种编码方式叫IEEE编码。

遇到四舍五入的情况时0舍1入。

浮点数汇编指令

浮点数的操作指令与普通数据类型不同,浮点数操作通过浮点寄存器来实现,而普通数据类型使用的是通用寄存器,它们分别使用两套不同的指令。

浮点寄存器是通过栈结构来实现的,由ST(0)~ST(7)共8个栈空间组成,每个寄存器占8个字节。每次使用浮点寄存器都是率先使用ST(0),而不能越过ST(0)直接使用ST(1)。浮点寄存器的使用就是压栈,出栈的过程。当ST(0)存在数据时,执行压栈操作后,ST(0)的数据将装入ST(1)中,若无出栈操作,将顺序地向下压栈,直到浮点寄存器占满。

这里要讲一下常用浮点数汇编指令,看到知道是在操作浮点数就行。

指令名称 使用格式 指令功能
FLD FLD IN 将浮点数IN压入ST(0)中
FILD FILD IN 将整数数IN压入ST(0)中
FLDZ FLDZ 将0.0压入ST(0)中
FLD1 FLD1 将1.0压入ST(0)中
FST FST OUT ST(0)中的数据以浮点形式存入OUT地址中
FSTP FSTP OUT 和FST指令一样,但会执行一次出栈操作
FIST FIST OUT ST(0)中的数据以整数形式存入OUT地址中
FISTP FISTP OUT 和FIST指令一样,但会执行一次出栈操作
FCOM FCOM IN 将IN地址数据与ST(0)进行实数比较,影响对应标记位
FIST FIST 比较ST(0)是否为0.0,影响对应标记位
FADD FADD IN 将IN地址内数据与ST(0)做加法运算,结果放入ST(0)中
FADDP FADDP ST(N) ST 将ST(N)中的数据与ST(0)中的数据做加法运算,N为0~7中的任意一个,先执行一次出栈操作,然后将相加结果放入ST(0)中保存

字符和字符串的表现形式

字符编码

分ASCLL和Unicode。Unicode是ASCLL的升级编码格式。

ASCLL占一个字节,范围是0~255。

由于ASCLL表示范围过小,因此占双字节,表示范围为0~65535的Unicode编码就产生了。

但不同语言解码的Unicode会有重复。比如你在txt文件里输入日文。若用日文解码字符表那么出来的就是正常的日文,但如果用中文解码字符表那么出来的就是混乱的中文。

字符串的存储方式

在程序中,只要知道字符串首地址和结束地址就可以确定字符串的位置和长度大小。首地址很简单,确定尾地址有两种方式:

1.在开始的时候保存总长度

取得首地址前n字节就可以得到字符串长度。缺点是在通信邻域要多开销n字节空间,而且双方要事先知道通信字符串的长度。

2.使用结束符

ASCLL用一个字节的’\0’,Unicode用两个字节的’\0’。他们两个编码不能混用,不然就会发生解释错误。

字符串的识别相对简单,在OllyDBG和IDA Pro里都会自动识别出程序的字符串,所以这里不作更多讨论。

布尔类型

C++中0为假,非0为真。布尔类型在内存中占一字节。基本上都是存储在al里。

关于eax,al,ah寄存器的关系:

|===============EAX===============|–32个0,4个字节,2个字,1个双字

|======AX=======|–16个0,2个字节,1个字

|==AH===|———–8个0,1个字节

|===AL==|—8个0,1个字节

地址,指针和引用

说到地址,这里就需要来区分一下物理地址,虚拟地址,偏移量等概念。

  • 物理地址:

就是在内存中的真实存在地址

  • 虚拟地址:

这就要牵扯到Windows的历史了。当时的内存比较小,很容易就内存满而不能继续操作。这时候有人就说,内存不够可以用硬盘呀!所以现在一些程序在预加载的时候是先在硬盘里加载出来的(为了减轻内存的压力)。那么这个虚拟出来的内存就是虚拟地址。在一定条件下,虚拟地址会变回物理地址。因为内存速度还是比硬盘速度快很多。

*偏移量:

就是距离首地址的偏移量。对于程序来说,偏移量可以直接定位程序的功能,不会因为其他原因随意变化。物理地址和虚拟地址是会随意变动的。

只有变量才存在内存地址,常量没有地址。

指针的定义是使用”TYPE*”,TYPE是各类的数据类型。这意味着不同种类的指针有不同用法。

各类指针的工作方式

可能大家在前面的学习中有些疑惑,电脑咋知道你这个是ASCLL还是int还是double呢?

数据的存储很直接暴力,之前怎么讲他就怎么存。读取这数据的时候只要用他对应的指针,那么电脑就知道你这个范围内地址存储的数据是什么类型的了。

  • 这里拿几个C++源码来对比
定义int初始化为0x12345678
1
int nVar = 0x12345678;
1
mov dword ptr [ebp-10h],12345678h
定义char类型指针变量,初始化为变量nVar地址
1
char *pcVar=(char*)&nVar;
1
2
lea edx,[ebp-10h]
mov dword ptr [ebp-18h],edx
定义short类型的指针变量,初始化为变量nVar地址
1
short *psnVar=(short*)&nVar;
1
2
lea eax,[ebp-10h]
mov dword ptr [ebp-1Ch],eax

等等

所有指针类型只支持加法和减法。加减法用于地址偏移。但只指针的加减法不像数学中加减法那么简单。指针加一后,指针内保存的地址值不一定是加一,具体取决于指针类型。如int型,地址值会加4,因为int是32位的共4字节。所以加的类型长度,而非数字1。

引用

引用其实就是一种传参数的类型。可以大大减少内存的占用量。C++为了简化指针操作,对指针的操作进行了封装,产生了引用类型,而实际上引用类型就是指针类型

常量

字面意思,常量是一个恒定不变的值,他在内存中也是不可修改的。

常量数据在程序运行前就已经存在,他们被编译到可执行文件中。当程序启动后,他们便会被加载进来。这些数据通常会在常量数据区中保存,该节区的属性中是没有可改写权限的,所以在对常量进行修改时,程序就会报错。试图修改他们的数据都将引发异常,导致程序崩溃。

常量的定义与define和const的区别

#define是一个真常量,而const却是由编译器判断实现的常量,是一个假的常量。在实际中,使用const定义的变量,最终还是一个变量,只是在编译器内进行了检查,发现有修改就报错。

因此const所对应的地址数值是可以修改的。但const的值无法修改。

1
2
3
4
5
const in nConst=5;        //定义常量
int *pConst=(int*)&nConst;//将nConst指针给pConst
*pConst=6; //改变nConst指针所指的值
int nVar=nConst;
printf("%d %d %d",nConst,nVar,*pConst);

这里不会报错,但结果会是 5 5 6。即nConst指针所指的指已经变成6了,但nConst还是等于5。

为什么呢?我们来看汇编代码

1
2
3
4
5
mov dword ptr [ebp-4],5;   给常量赋值5
lea eax,[ebp-4]; 把地址给pConst
mov dword ptr [ebp-8],eax
mov dword ptr [ecx],6; 变6
mov dword ptr [ebp-0Ch],5; 给nVar赋值

可以看出来,nConst的5不仅存在在其地址上,更是作为一个常数。因为给nVar赋值的时候不是把指针里的值给他,而是直接给常量。

因此,#define和const还是很有区别的。

#define const
编译期间查找替换 编译期间检测其修饰变量是否被修改
由系统判断是否被修改 由编译器限制修改
字符串定义在只读区,常量变成二进制代码的一部分 根据作用域决定所在内存位置和属性

小结

计算机的工作流程,归根到底是 输入,处理,输出的过程。数据是处理的对象。对数据的考察有两点

数据在何处?

数据是代码加工处理的对象,而代码本身也是以二进制存放的,对于处理器而言,代码的本质也是数据。在分析时,我们会看到不同指令对数据的处理,这时首先要确定数据的存储位置,对于内存中的数据,这时候要查看地址。有内存地址才能得到内存属性。

数据如何解释?

得到内存地址,还是无法得到数据的正确内容,因为缺少解释方式。因此,这第二章中对各类数据的解释方式和特点很重要,是后面内容的基础。

后记

写这篇博客花了我一周的时间学习,也逼我完全搞懂了第二章中的每一个知识点,挺好的。就是中间运动会期间吃坏了肚子,现在在床上一边码博客一边休息,很难受。。。
其实我做这个博客也只因为周围同学都做了博客所以我也就尝试了一下,感觉超级不错。以后我会不定期的更新我的博客的。总觉得学了些东西不总结不分享将来就忘记了。以此来谨记我这青春岁月吧!

CATALOG
  1. 1. 前言
    1. 1.1. 什么是逆向工程
    2. 1.2. 新的旅程
    3. 1.3. 自我感想
  2. 2. 整数类型的表现形式
  3. 3. 浮点数的表现形式
    1. 3.1. 浮点数的编码
    2. 3.2. 浮点数汇编指令
  4. 4. 字符和字符串的表现形式
    1. 4.1. 字符编码
    2. 4.2. 字符串的存储方式
  5. 5. 布尔类型
  6. 6. 地址,指针和引用
    1. 6.1. 各类指针的工作方式
      1. 6.1.0.1. 定义int初始化为0x12345678
      2. 6.1.0.2. 定义char类型指针变量,初始化为变量nVar地址
      3. 6.1.0.3. 定义short类型的指针变量,初始化为变量nVar地址
  • 7. 引用
  • 8. 常量
    1. 8.1. 常量的定义与define和const的区别
  • 9. 小结
    1. 9.1. 数据在何处?
    2. 9.2. 数据如何解释?
  • 10. 后记