学习过计算机的读者都知道,计算机中的各种数据都是以二进制形式进行存储的,无论是文本文件、图片文件,还是音频文件、视频文件、可执行文件等,统统都是由二进制文件存储的。学习过计算机的读者在学习计算机基础的时候一定学习过进制转换,也一定学习过数据的表示方式等,大部分人在学习这部分知识时会觉得枯燥、无用,但是对于学习逆向知识和使用逆向工具,数据的存储及表示形式是必须要掌握的。
本文借助OllyDbg这款调试工具来一起讨论数据的存储及表示形式,让读者对于学习计算机的数据存储及表示可以更加的感性,从而脱离纯粹理论性的学习。
本文内容较为枯燥,但是着实是学习逆向的基础知识,对于从来没有接触过逆向或者是刚开始接触逆向的读者,本文内容还是有一定帮助的。
本文关键字:进制 数据表示 数据转换 数据存储
1.1 进制及进制的转换
了解进制的概念及进制的转换是学习逆向的基础,因为计算机使用的进制是二进制,它又不同于我们现实生活中使用的十进制,因此我们必须学习不同的进制及进制之间的转换。
1.1.1 现实生活中的进制与计算机的二进制
我们在现实生活中会接触到多种多样的进制,通常见到的有十进制、十二进制和二十四进制等。下面分别对这几种进制进行举例说明。
十进制是每个人从上学就开始接触和学习的进制表示方法。所谓的十进制,就是逢十进一,最简单的例子就是9+1=10。这个无需过多解释。
十二进制也是我们日常生活中常见的表示方法。所谓的十二进制,就是逢十二进一,例如12个月为1年,13个月就是1年1个月。
二十四进制也是我们日常生活中常见的表示方法。所谓的二十四进制,就是逢二十四进一,例如24小时为1天,25小时就是1天1小时。
介绍了以上现实生活中的例子后,我们再来说说计算机中的二进制。根据前面各种进制的解释,我们可以想到,二进制就是逢二进一。这里举个不太恰当的例子,例如2斤就是1公斤。
在计算机中为什么使用二进制呢?简单说就是计算机用高电平和低电平来表示1和0最为方便和稳定,高电平被认为是1,低电平被认为是0,这就是所谓的二进制的来源。
由于二进制在阅读上不方便,计算机又引入了十六进制来直观地表示二进制。所谓的十六进制,就是逢十六进一。
因此在计算机中,我们常见的数据表示方法有二进制、十进制和十六进制。
1.1.2 进制的定义
在学习小学数学的时候我们就学习了十进制,十进制一共有十个数字,从0一直到9,9再往后数一个的时候要产生进位,也就是逢十进一。总结十进制的定义则是,由0到9十个数字组成,并且逢十进一。
举一反一地来说,二进制的定义是,由0到1两个数字组成,逢二进一。十六进制的定义是由0到9十个数字和A到F六个字母组成,逢十六进一。
由此,我们衍生出N进制的定义是,由N个符号组成,逢N进一。
表1-1所列为这三种进制的数字表。
表1-1 二进制、十进制和十六进制数字表
数 制 | 基 数 | 数 字 |
二进制 | 2 | 0 1 |
十进制 | 10 | 0 1 2 3 4 5 6 7 8 9 |
十六 进制 | 16 | 0 1 2 3 4 5 6 7 8 9 A B C D E F |
1.1.3 进制的转换
在逆向当中,我们直接面对的通常是十六进制,而由于很多原因,我们需要将其当作十进制或二进制来查看,当然也有可能需要根据二进制转换成十六进制或十进制。所以,我们就需要掌握进制之间的转换。
1.二进制转十进制
二进制整数的每个位都是2的幂次方,最低位是2的0次方,最高为是2的(N-1)次方,我们通过一个例子进行说明。我们把二进制数10010011转换成十进制数,计算方式如下:
10010011 = 1 × 27 + 0 × 26 + 0 × 25 + 1 × 24 + 0 × 23 + 0 × 22 + 1 × 21 + 1 × 20 = 128 + 0 + 0 +16 + 0 + 0 + 2 + 1 = 147
我们得出的结果是,把二进制10010011转换成十进制后是147。我们用计算机进行验算,如图1-1和图1-2所示。
从图1-1和图1-2中可以看出,我们的计算结果是正确的,由此读者在计算二进制时按照上面转换的例子进行转换即可。
图1-1 验算二进制(一)
图1-2 验算二进制(二)
2.十六进制与二进制的转换
由于一个简单的数值用二进制表示需要很长的位数,这样对于阅读很不方便,因此汇编和调试器常用十六进制表示二进制。十六进制的每个位可以代表4个二进制位,因为2的4次方刚好是16。这样,在二进制与十六进制之间就产生了一个很好的对应关系,如表1-2所列。
表1-2 二进制对应的十六进制与十进制数
二进制 | 十进制 | 十六进制 | 二进制 | 十进制 | 十六进制 |
0000 | 0 | 0 | 0110 | 6 | 6 |
0001 | 1 | 1 | 0111 | 7 | 7 |
0010 | 2 | 2 | 1000 | 8 | 8 |
0011 | 3 | 3 | 1001 | 9 | 9 |
0100 | 4 | 4 | 1010 | 10 | A |
0101 | 5 | 5 | 1011 | 11 | B |
1100 | 12 | C | 1110 | 14 | E |
1101 | 13 | D | 1111 | 15 | F |
根据此表,我们可以很快地把二进制和十六进制进行转换,把上例的二进制10010011转换成十六进制,转换过程如下:
第一步,把10010011从最低开始按每四位分为一组,不足四位前面补0,划分结果为1001 0011;
第二步,把划分好的组进行查表,1001对应十六进制是9,0011对应的十六进制是3。
那么,二进制10010011转换成十六进制后的值是93。读者可以通过计算器自行进行验算。
在逆向中常用的就是二进制与十进制的转换,或者是二进制与十六进制的转换,其他的转换方式读者可以自行查找资料进行学习。关于十六进制和二进制需要记住的重要一点就是,一位十六进制数可以表示四位二进制数。
1.2 数据宽度、字节序和ASCII码
前面介绍了计算机中常用的进制表示方法和转换,现在读者知道了计算机存储的都是二进制的数据,那么接下来要讨论的是在计算机中数据存储的单位以及数据是如何存储在存储空间的。
1.2.1 数据的宽度
数据的宽度是指数据在存储器中存储的尺寸。在计算机中,所有数据的基本存储单位都是字节(byte),每个字节占8个位(位是计算机存储的最小单位,而不是基本单位,因为在存储数据时几乎没有按位进行存储的)。其他的存储单位还有字(word)、双字(dword)和八字节(qword)。
图1-3 给出各个存储单位所包含的位数。
在计算机编程中,常用的几个重要数据存储单位分别就是byte、word和dword,这几个存储单位稍后我们会使用到。
1.2.2 数值的表示范围
在计算机中存储数值时,也是要依据前面介绍过的数据宽度进行存储的,那么在存储数据时由于存储数据的宽度限制,数值的表示也是有范围限制的。那么byte、word和dword能存储多少数据呢?我们先来计算一下,如果按位存储的话,能存储多少个数据,再分别来计算以上三种单位能够存储的数值的范围。
计算机使用二进制进行数据存储时,一位二进制最多能表示几个数呢?因为是二进制数,只存在0和1两个数,所以一位二进制数最多能表示两个数,分别是0和1。那么,两位二进制最多能表示几个数呢?因为一位二进制数能表示两个数,所以两位二进制数则能表示2的2次方个数,即4个数,分别是0、1、10、11。进一步地,三位二进制数能表示的就是2的3次方个数,即8个数,分别是0、1、10、11、100、101、110、111。
上面的过程可以整理成表1-3。
表1-3 N位二进制位能够表示的数
二进制位数 | 表示数的个数 | 表示的数 | 2的N次方 |
1 | 2 | 0、1 | 2的1次方 |
2 | 4 | 0、 1、 10、 11 | 2的2次方 |
3 | 8 | 0、1、10、11、100、101、110、111 | 2的3次方 |
根据表1-3计算的byte、word和dword三种数据存储宽度能表示的数据的范围如表1-4所列。
表1-4 无符号整数的表示范围
存储 单位 | 十进制范围 | 十六进 制范围 | 2的N次方 |
byte | 0~255 | 0~FF | 2的8次方 |
word | 0~65535 | 0~FFFF | 2的16次方 |
dword | 0~4294967295 | 0~FFFFFFFF | 2的32次方 |
2的8次方是256,为什么数值只有0~255个呢?因为计算机计数是从0开始,从0到255同样是256个数,这里的2的8次方表示能够表示数值的个数,而不是能够表示数值的最大的数。
1.2.3 字节序
字节序也称为字节顺序,在计算机中对数值的存储有一定的标准,而该标准随着系统架构的不同而不同。了解字节存储顺序对于逆向工程是一项基础知识,在动态分析程序的时候,往往需要观察内存数据的变化情况,这就需要我们在掌握数据的存储宽度、范围之后,进一步了解字节顺序。
通常情况下,数值在内存中存储的方式有两种,一种是大尾方式,另一种是小尾方式。关于字节序的知识,通过一个简单的例子就可以掌握。
比如有0x01020304(C语言中对十六进制数的表示方式)这样一个数值,如果用大尾方式存储,其存储方式为01 02 03 04,而用小尾方式进行存储则是04 03 02 01,用更直观的方式展示其区别,如表1-5所列。
表1-5 字节顺序对比表
大尾方式 | 小尾方式 | ||
数据 | 地址值 | 数据 | 地址值 |
01 | 00000000H | 04 | 00000000H |
02 | 00000001H | 03 | 00000001H |
03 | 00000002H | 02 | 00000002H |
04 | 00000003H | 01 | 00000003H |
从两个地址列可以看出,地址的值都是一定的,没有变化,而数据的存储顺序却是不相同的。从表中可以得到如下结论。
大尾存储方式:内存高位地址存放数据低位字节数据,内存低位地址存放数据高位字节数据;
小尾存储方式:内存高位地址存放数据高位字节数据,内存低位地址存放数据低位字节数据。
通常情况下,Windows操作系统兼容的CPU为小尾存储方式,而Unix操作系统兼容的CPU多为大尾存储方式。在网络中传输的数据的字节顺序使用的是大尾存储方式。
1.2.4 ASCII码
计算机智能存储二进制数据,那么计算机是如何存储字符的呢?为了存储字符,计算机必须支持特定的字符集,字符集的作用是将字符映射为整数。早期字符集仅仅使用8个二进制数据位进行存储,即ASCII码。后来,由于全世界语言的种类繁多,又产生了新的字符集Unicode字符编码。
ASCII码是美国标准信息交换码的字母缩写,在ASCII字符集中,每个字符由唯一的7位整数表示。ASCII码仅使用了每个字节的低7位,最高位被不同计算机用来创建私有字符集。由于标准ASCII码仅使用7位,因此十进制表示范围是0~127共128个字符。
在编程与逆向中都会用到ASCII码,因此有必要记住常用的ASCII字符对应的十六进制和十进制数。常用的ASCII字符如表1-6所列。
表1-6 常用ASCII码表
字 符 | 十进制 | 十六进制 | 说 明 |
LF | 10 | 0AH | 换行 |
CR | 13 | 0DH | 回车 |
SP | 32 | 20H | 空格 |
0~9 | 48~57 | 30H~39H | 数字 |
A~Z | 65~90 | 41H~5AH | 大写 字母 |
a~z | 97~122 | 61H~7AH | 小写 字母 |
表1-6是经常使用到的ASCII字符,这些字符是经常会见到和用到的,希望读者能将其保存,以便使用之时可以快速查阅。
Unicode编码是为了使字符编码更进一步符合国际化而进行的扩展,Unicode使用一个字(也就是两个字节,即16位)来表示一个字符。这里不做过多的介绍。
1.3 在OD中查看数据
在逆向分析中,调试工具可以说是非常重要的。调试器能够跟踪一个进程的运行时状态,在逆向分析中称为动态分析工具。动态调试会用在很多方面,比如漏洞的挖掘、游戏外挂的分析、软件加密解密等方面。本节介绍应用层下最流行的调试工具OllyDbg。
OllyDbg简称OD,是一款具有可视化界面的运行在应用层的32位的反汇编逆向调试分析工具。OD是所有进行逆向分析人员都离不开的工具。它的流行,主要原因是操作简单、参考文档丰富、支持插件功能等。
熟悉OD
OD的操作非常简单,但是由于逆向是一门实战性和综合性非常强的技术,因此要真正熟练掌握OD的使用却并不是容易的事,单凭操作而言看似没有太多的技术含量,但是其真正的精髓在于配合逆向的思路来达到逆向者的目的。
1.OD的选型
为什么先介绍OD的选型,而不直接开始介绍OD的使用呢?OD的主流版本是1.10和待崛起的2.0。虽然它的主流版本是1.10,但是它仍然存在很多修改版。所谓修改版,就是由用户自己对OD进行修改而产生的,类似于病毒的免杀。OD虽然是动态调试工具,但是由于其强大的功能经常被很多人用在软件破解等方面,导致很多作者的心血付诸东流。软件的作者为了防止软件被OD调试,加入了很多专门针对OD进行调试的反调试功能来保护自
己的软件不被调试,从而不被破解;而破解者为了能够继续使用OD来破解软件,则不得不对OD进行修改,从而达到反反调试的效果。
调试、反调试、反反调试,对于新接触调试的爱好者来说容易混淆。简单来说,反调试是阻止使用OD进行调试,而反反调试是突破反调试继续进行调试。OD的修改版本之所以很多,目的就是为了能够更好地突破软件的反调试功能。
因此,如果从学习的角度来讲,建议选择原版的OD进行使用。在使用的过程中,除了会掌握很多调试技巧外,还会学到很多反调试的技巧,从而掌握反反调试的技巧。如果在实际的应用中,则可以直接使用修改版的OD,避免OD被软件反调试,从而提高逆向调试分析的速度。
2.熟悉OD主界面
OD的发行是一个压缩包,解压即可运行使用,运行OD解压目录总的ollydbg.exe程序,就会出现一个分布恰当、有菜单有面板和能输入命令的看着很强大的软件窗口,如图1-4所示。
在图1-4的OD调试主窗口中的工作区大致可以分为6个部分,按照从左往右、从上往下,这6部分分别是反汇编窗口、信息提示窗口、数据窗口、寄存器窗口、栈窗口和命令窗口。下面分别介绍各个窗口的用法。
反汇编窗口:该窗口用于显示反汇编代码,调试分析程序主要在这个窗口中进行,这也是进行调试分析的主要工作窗口。
信息提示窗口:该窗口用于显示与反汇编窗口中上下文环境相关的内存、寄存器或跳转来源、调用来源等信息。
数据窗口:该窗口用于以多种格式显示内存中的内容,可使用的格式有Hex、文本、短型、长型、浮点、地址和反汇编等。
寄存器窗口:该窗口用于显示各个寄存器的内容,包括前面介绍的通用寄存器、段寄存器、标志寄存器、浮点寄存器。另外,还可以在寄存器窗口中的右键菜单选择显示MMX寄存器、3DNow!寄存器和调试寄存器等。
栈窗口:该窗口用于显示栈内容、栈帧,即ESP或EBP寄存器指向的地址部分。
命令窗口:该窗口用于输入命令来简化调试分析的工作,该窗口并非基本窗口,而是由OD的插件提供的功能,由于几乎所有的OD使用者都会使用该插件,因此有必要把它也列入主窗口中。
图1-4 OD调试主窗口
3.在数据窗口中查看数据
前面已经介绍,OD是一款应用层下的调试工具,它除了可以进行软件的调试以外,还可以帮助我们学习前面介绍的数据宽度、进制转换等知识,而且能够帮助我们学习汇编语言。本节主要介绍通过OD的数据窗口来观察数据宽度。
为了能够直观地观察内存中的数据,我们通过RadAsm创建一个没有资源的汇编工程,然后编写一段自己的汇编代码,代码如下:
在上面的代码中,定义了10个全局变量。首先,var1、var2和var3分别定义了dword类型的3个变量,其中var1的值是十六进制的12h,var2的值是十进制的12,var3的值是2进制的11b。b1到b4四个变量是字节类型的,w1和w2两个变量是字类型的,d变量是dword类型的。
这10个全局变量就是我们要考察的关键。在RadAsm中进行编译连接后,直接按下Ctrl + D这个快捷键,即可在RadAsm安装时自带的OD中打开。在OD调试器中打开该程序后,观察它的数据窗口(如图1-5所示)。
图1-5 数据窗口中查看变量
在图1-5中,数据窗口一共有3列,分别是地址列、HEX数据列和ASCII列。这3个列,可以通过单击鼠标右键来改变现实方式和显示的列数。在地址00403000处开始的4个字节12 00 00 00是十六进制的12,也就是在汇编代码中定义的var1;在地址00403004处的4个字节0C 00 00 00是十六进制0C,也就是在汇编代码中定义的var2,var2变量定义的值是十进制的12,也就是十六进制的0C;在地址00403008处的4个字节03 00 00 00是十六进制的03,也就是在汇编代码中定义的var3,var3变量定义的值是2进制的11,也就是十六进制的03。
这3个变量在我们定义的时候都是以dd进行的,都是dword类型的变量,分别各占用4字节,因此在内存中,前3个变量分别是12 00 00 00、0C 00 00 00和03 00 00 00。
在地址0040300C处的值是11 22 33 44,这4个值分别是我们定义b1、b2、b3和b4 4个字节型的变量,这4变量按照内存由低到高的顺序显示分别是11、22、33、44。
在地址00403010处显示的值是66 55 88 77,这4个值分别对应我们定义的w1和w2两个字型变量,但是我们定义的变量w1的值是5566h,w2的值是7788h,在内存中为何显示的是6655和8877呢?这就是我们提到过的字节顺序的问题。我们的主机采用的是小尾方式存储的数据,也就是数据的低位存放在内存的低地址中,数据的高位存放在内存的高地址中,因此在地址00403020中存放的是5566H的低位数据66,在地址00403021中存放的是5566H的高位数据55,在内存看时,顺序是相反的。
在地址00403014处存放的是78 56 34 12,这是我们定义的最后一个变量d,它也是按照小尾方式存储在内存中的。因此,在查看内存时顺序也是反的。
OD提供了多种查看内存数据的方式,通过在数据窗口中单击鼠标右键,会弹出如图1-6所示菜单。
当在数据窗口中选择数据时,右键的菜单提供编辑、赋值、查找、断点功能,如图1-7所示。
图1-6 查看数据方式的菜单选项
图1-7 OD中对数据操作的菜单
4.通过命令窗口改变数据窗口显示方式
在图1-4中的最下方可以看到有一个输入命令的编辑框,在此处可以输入OD的相关命令以提高调试的速度。本小节就介绍如果通过命令窗口来改变数据窗口的显示方式。
在上面代码中定义变量时,使用了db、dw和dd三种类型,在OD的命令窗口中也同样可以使用者3个命令,其格式分别如表1-7所列。
表1-7 命令窗口改变数据显示命令格式
命 令 | 格 式 | 说 明 | 举 例 |
db | db address | 按字节的方式查看 | db 403000 |
dw | dw address | 按字的方式查看 | dw 403000 |
dd | dd address | 按双字的方式查看 | dd 403000 |
将表1-7中的命令在命令窗口中进行输入,数据窗口的变化和数值显示的变化分别如图1-8、图1-9和图1-10所示。
图1-8 dd命令显示的数据窗口
图1-9 dw命令显示的数据窗口
图1-10 db命令显示的数据窗口
从图中可以看出不同方式下数据窗口显示的样式,但是无论使用哪种方式显示数据,地址列总是会显示在最前面的,只要我们知道数据的地址,就
可以直接在命令窗口中输入显示数据的格式来查看指定内存中的数据。
1.4 编程判断主机字符序
编程判断主机字节序是更进一步掌握字节序的方式,本小节给出两种对主机的字节序进行判断的方式。
1.4.1 字节序相关函数
在TCP/IP网络编程中会涉及关于字节序的函数,TCP/IP协议中传递数据是以网络字节序进行传输的,网络字节序是指网络传输相关协议所规定的字节传输的顺序,TCP/IP协议所使用的网络字节序与大尾方式相同。而主机字节序包含大尾方式与小尾方式,因此在进行网络传输时会进行相应的判断,如果主机字节序是大尾方式则无需进行转换即可传输,如果主机字节序是小尾方式则需要转换成网络字节序(也就是转换成大尾方式)然后进行传输。
常用的字节序涉及的函数有如下几个:
在这4个函数中,前两个是将主机字节序转换成网络字节序,后两个是将网络字节序转换为主机字节序。关于更多的字节序的函数可参考MSDN。
1.4.2 编程判断主机字节序
“编程判断主机字节序”是很多杀毒软件公司或者安全开发职位的一道面试题,因为这个题目比较基础。通过前面的知识,相信读者能够很容易地实现该程序。这里给出笔者自己对于该题目的实现方法。笔者认为,完成该题目有两种方法,第一种方法是“取值比较法”,第二种方法是“直接转换比较法”。
1.取值比较法
所谓取值比较法,是首先定义一个4字节的十六进制数。因为使用调试器查看内存最直观的就是十六进制,所以定义十六进制数是一个操作起来比较直观的方法。而后通过指针方式取出这个十六进制数在“内存”中的某一个字节,最后与实际数值中相对应的数进行比较。
由于字节序的原因,内存中的某字节与实际数值中对应的字节可能不相同,这样就可以确定字节序了。
代码如下:
以上代码中,定义了0x01020304这个十六进制数,其在小尾方式内存中的存储顺序为04 03 02 01。取(BYTE )&dwSmallNum内存中的低地址位的值,如果是小尾方式的话,那么低地址存储的值为0x04;如果是大尾方式的话,则低地址存储的值为0x01。
2.直接转换比较法
所谓直接转换比较法,是利用字节序转换函数将所定义的值进行转换,然后用转换后的值与原值进行比较。如果原值与转换后的值相同,说明是大尾方式,否则为小尾方式。
代码如下:
这种方式比较直接,其前提是网络字节序是固定的,就是大尾方式。因为是比较,所以就要有一个参照物。如果原值转换后的结果与原值相同,就说明该主机是大尾方式存储,反之则是小尾方式。
1.5 总结
本文对内存中存储基础数据的方式进行了阐述,并且在最后部分介绍了如何使用OD调试器来查看内存中的数据。在学习编程时,都会从数据类型开始介绍,不同的数据类型都是以二进制的方式存储在内存中的,只是它们存储的方式不同,或者是存储的宽度不同。在我们学习逆向时,也首先讲解了数据的基础及数据的存储方式。