代码还原技术
一丶简介代码还原
- 例子一:我们很多人都学习过汇编.但是汇编的核心知识就是我能看的懂.有人拿汇编去做外挂.比如我去追偏移.看着视频去做.然后换一个游戏依然这样.但是终有一天,你可能发现没意思了.因为这些知识都是死的.比如我们想看游戏中,这段代码做了什么事情.这个时候就需要将汇编转为高级代码查看了. IDA的F5插件.一般能做到.但是很多是做不到的. 比如游戏中.这段代码你找到一个对象+多少偏移是什么什么功能.但是会逆向的人.这段代码抠出来.转为高级代码.一看.原来这个意思.+多少是什么作用.另外还实现了什么功能.这个就是核心技术了.为什么别人的外挂功能比较多.你的比较少.其核心就在这里.
- 例子二:算法逆向,如一个软件.让你追出注册码.你可能就爆破.但是如果你能把它算法逆出来.那么是不是第一提升了自己,第二,自己可以写注册机专门为这个程序生成注册码了.
- 例子三: 如果你是为公司工作.可能某一天,公司需要你进行逆向.发现xx软件的一个功能比较好.此时你需要怎么办.完整的根据汇编去逆向出来这个功能.并且让公司去做出这个功能.这个也是一个很好的例子.
二丶代码还原中的数据类型表现形式
上面说了很多了,那么真正的开始篇幅讲解.
1.整数类型
C++中整数的基本数据类型有三种, int long short. 在 VC6.0中,int long所占内存都是4字节.
short两个字节. 以16进制为例 int long 分别就是4个字节. short两个字节. 一个字节是8位.
2.无符号整数
在内存中,无符号整数是用来表示数值的.如果32位下.那么取值范围是 0x00000000~0xFFFFFFF
10进制: 0~4294967295,因为无符号数,那么最高位就是0填充.所以表示数值比较大.
3.有符号整数
有符号整数跟上面无符号整数一样.只不过高位用来表示符号位,其余低位表示数值.这样有符号的整数.表示的数值就只有31位了.范围则是 0x80000000~0x7FFFFFFF 转为十进制: -2147483648~ 2147483647
因为最高位是符号位,可以表示 负数. 例如 -3
在内存中负数都是补码形式表示的
补码规则: 补码规则则是用0 - 去这个数的绝对值
例如: 0 - 3 的结果就是 -3在内存中的表现形式.
因为补码高位为1,要转为真值也是 0 - 补码的形式. 但是一般计算机计算的话,通常都是用补码取反+1进行获得真值. 前边带上符号即可.
为什么负数取值总比整数取值多一个值.
例如如上:
-2147483648~2147483647
原因:
对于四个字节补码 0x80000000 代表的是-0. 但是对于0来讲. 正负区分没必要.所以0x800000000规定了就是4字节补码最小值了.所以这也是负数比正数多一位的原因.
4.浮点数数据类型
关于浮点数存储.科学上有很多争议.有很多存储实数(小数)的方式.不过很少用了.所以我们也不再介绍了
现在是不管如何存储.都分为 定点实数存储 跟 浮点数实数存储 这两种方式
- 定点实数存储
定点实数存储,就是约定整数位和小数位的长度.比如4个字节为例,高2个字节存储整数.低两个字节存储实数.这样的好处是计算的效率高,缺点是存储不灵活.比如存储65536.5 整数部分已经存储不了65536了. - 浮点实数存储
浮点实数存储就是用一部分二进制位存放小数点的位置信息,我们可以称之为指数域其它的数据位用来存储没有小数点时的数据和符号,我们可以称之为数据域丶符号域
如:
67.625 我们可以使用浮点实数存储, 数据域 可以存放67625 小数位置可以记住位置为 10~-3次方 ,对这个数进行访问的时候.只需要计算一下即可.
优缺点: 优点缺点跟第一种是相反的. 80286CPU之前,程序员常常为实数的计算,伤脑筋.最后出来了浮点协处理器.可以协助CPU计算.程序员计算实数的效率就大大的提高了.于是现在 浮点存储的方式就推广了出来
注意: 现在都是第二种方法进行存储的.不是定点存储方式了 - C++中的浮点
在C++当中,有浮点数 float 以及 double用来存储浮点数. float 4个字节. double 8个字节.
由于double空间大,所以精度高. 两种数据类型在内存中同样的是16进制存储.但是与10进制的16进制不同. float dobule 的16进制比较大.
原因:浮点类型并不是将一个浮点小数直接转为二进制进行存储的.而是将浮点小数转换成二进制,重新编码.再进行存储.C/C++中的浮点数是有符号的.
值得注意 浮点数转为整数,并不是四舍五入.而是向0取整. 也就是说舍弃小数位.转为整数.
例如: a = 3.78; int b = (int) a; 此时b的值是3. 而不是传统意义上的 4; 因为不是四舍五入.
5.浮点编码
- 浮点编码转换.
我们上面说了,浮点数是重新进行编码进行存储的.所以我们只要搞明白了编码.那么就可以自己算出浮点数在内存中怎么表示.或者反转回来.16进制怎么转换为浮点数 - 浮点编码采用的是 IEEE规定的编码.
float double转换方式一样. 都是因为表示范围不一样.所以编码方式有些特别. - 1.浮点编码的编码方式
浮点编码,会将一个浮点数转为二进制数.以科学计数法进行区分.分为三部分
1.符号域
2.指数域
3.尾数域
如下图所示:
最高位是符号位,表示正负
去掉符号位往后数8位 是指数域.
最后的23位则表示尾数.
1.正数浮点转为十六进制表示
-
2.浮点数转为16进制存储
现在我们要把浮点数转为十六进制存储在内存里.转换步骤
1.将一个浮点数转化为二进制例如:12.25 转为2进制 = 1100.01
整数直接转为二进制即可. 小数不断 * 2 取整.
例如: 0.25
0.25 * 2 = 0.5 取整 = 0
0.5 * 2 = 1.0 取整就是1
所有12.25 转为二进制表示就是 1100.01
2.计算指数位
计算指数位首先移动小数点位置到符号位置除最高位为1的地方.
也就是符号位也好.不是符号位也好.移动到最高位为1的地方. 7.25 转换之后是
0111.01 移动到最高位则是 1.1101.
1100.01 移动 1.10001 总共移动了3位.每次移动一位,指数+1
因为指数为移动了三位.所以 3 + 127(8位) = 130 转为二进制 10000010 这个就是指数位.
也就是上图中所说的符号位后面数8位是指数位. 我们上边计算的就是指数位的值.
为什么 +127.因为可能会出现负数.十进制127可以表示二进制的01111111. IEEE浮点编码规定
当指数域< 0111111的时候,就是一个负数.如果大于01111111的时候就是一个正数. 所以01111111为0. IEEE浮点编码规定的.所以只要记住即可. 127即可. 也可以理解为指数域是8位,表示的数值是128.但IEE规定了.所以-1 指数最大值 - 1即可.
3.计算尾数位
经过上面计算我们符号是1,但是符号位基本不变.因为是正数浮点.所以符号位为0:
指数位为: 130 10000010
现在计算尾数位. 尾数位就是我们移动小数点之后的数值
1.10001 尾数位就是 10001,但是他不组23位.所以我们补0填充.
1000 1000 0000 0000 0000 000 补0之后,我们需要从左到右,按照4个字节分开
100 0100 0000 0000 0000 0000 分开之后.
此时加上我们之前的符号位以及指数位
0100 0001 0100 0100 0000 0000 0000 0000 这是拼接好的.我们转换为16进制进行存储
0x41440000 那么在内存中,我们的浮点数12.25 其实就是16进制 0x41 44 00 00 进行存储的.
2.负数浮点转为十六进制表示.
负数跟上边一样.一样计算指数位.也是分为以下步骤
1.转为科学计数法.
2.移动指数位.
3.计算指数位
4.尾数位补零到23位.
5.拼接进行二进制,并且二进制转为16进制.
1.转为科学计数法
-0.125 = 0.001
2.移动指数位
此时移动指数位是往小数点右边移动,移动到最高位为1的地方.
0.001 =>1.0 移动了三位,计算 -3. 往右边移动就是负数
1.0 则符号位是1代表负数. 指数位是负数.
3.计算指数位.
上面我们计算指数位是往小数点左边移动.所以指数位去相加.现在是往右边移动.所以相减
127-3 = 124 转为二进制 = 01111100
4.尾数位补零
0000 0000 0000 0000 0000 000
5.符号位 指数位 尾数位 进行拼接
1011 1110 0000 0000 0000 0000 0000 0000 (总共32位)
转为16进制
0xBE00 0000
所以-0.125 在内存中的16进制则是 0xBE000000
3.正数浮点16进制转为浮点数解析
我们会转换为16进制那么也要回转换回来
1.16进制拆分为2进制.
2.分出符号位 指数位 尾数位
3.求指数位是负数还是整数
4.移动指数位
比如我们的12.25f. 十六进制是x41440000
1.16进制转为2进制
0100 0001 0100 0100 0000 0000 0000 0000
2.分出符号位 指数位 尾数位
0 10000010 10001000000000000000000
3.求出指数位
指数位位 10000010 > 01111111 所以可以判断我们的小数是正数
10000010 - 01111111 = 130 - 127 = 3;
得出了我们要移动的位数
4.移动指数位
首先计算出的指数转为2进制 3 = 0011;
然后反过来.尾数位右移动三位.如下.
100010 >> 3 = 100.010 尾数的最高位不需要.所以补零
然后加上符号位.
符号位为0.代表是正数.所以+1
1100.010
再举个例子
7.25 在内存的16进制为0x40E8
1.16进制转为二进制
0100 0000 1110 1000 0000 0000 .....
2.计算指数位,尾数需要往右移动纪委
10000001 - 01111111 = 129 - 127 = 2;得出移动2位
3.移动尾数位
因为高位为0,所以代表我们转换的浮点数是正数.最后我们的高位要加上1才可以.
11010 ==>2位 = 11.010
符号位为0.所以高位补1
111.01 这个二进制在转换为10进制得出7.25
小数转为10进制:
.01是是两位. 分别记位 2的-1次方 2-2次方.
第一位计算: 0 * 1/2-1次方.
第二位计算: 1 * 1/2-2次方即可.
最后结果相加.
如果有三位.那么就是 用第三位数值 * 1/2-3次方即可.
0*1/2 + 1 * 1/4 = 0.25.所以我们可以推算出是0.25
4.Double类型解析.
double类型转换跟float一样.只不过指数位变成了11位. 剩余的42位表示尾数位.
2~11次方 - 1;就是用于计算的指数.也就是 1023.
三丶浮点汇编
1.浮点栈
因为有了浮点协处理器.所以浮点指令的操作有点不同.它是通过浮点寄存器来实现的.
浮点寄存器是通过栈结构来实现的.也称作浮点栈. 由 st(0) - st(7); 其中写st默认就是st(0)
操作任意浮点栈就需要加上序号 st(7);
值得注意的是浮点栈是循环栈. 也就是说st(0)出栈的数据.会放到st(7)中.这样依次使用.
2.浮点汇编
针对协处理器.也提供的相应的汇编进行操作.
分别是 fld类指令 fst 指令. 以及 fcom fadd等指令
都是大写
压栈指令
FLD IN 将浮点数IN 压入浮点栈
FILD IN 将整数压入浮点栈 mem32/64 80
FLDZ 默认压入0,浮点栈是0出栈指令
FST OUT 将浮点栈顶(st(0))的值给OUT存储. out可以是 mem32/64,但是不出栈
FSTP OUT 同FST out保存值,但是会出栈.
FISTP OUT 出栈,并且以整数的形式给OUT存储.栈比较
也可以进行栈中的值比较.用来更改标志位.
FCOM IN 将IN地址的内容.与浮点栈顶比较(st(0));
FTST 比较栈顶(st(0)); 是否为空.浮点加法
FADD IN 将St(0)的数据于in做加法. 值保存在 栈顶st(0);中.
FADDP st(N),st 将st(n)栈中的数据于st(0)中的数据进行运算.浮点栈有7个.那么N的取值就是0~7;
先执行一次出栈冬枣.然后相加结果放在 st(0)中存储.
3.使用内联浮点汇编实现加法
- 浮点做加法
int main(int argc, char* argv[])
{
float a = 11.25;
float c = 12.35;
float d = 13.25f;
float b = 0.0f;
__asm{
fld dword ptr[ebp - 0x4];
fld dword ptr[ebp - 0x8];
fld dword ptr[ebp - 0xc];
faddp st(1),st(0)
fstp dword ptr[ebp - 0x10];
}
printf("%f \r\n",b);
return 0;
}
实现结果:
- 浮点做返回值
float GetFloatValue()
{
return 12.25f;
}
int main(int argc, char* argv[])
{
int value = GetFloatValue();
return 0;
}
观看汇编,汇编分为两层.一层是调用内.一层是调用外.
调用内: 也就是GetFloatValue()函数内部.
push ebp
mov ebp,esp
sub esp,40h
push ebx
push esi
push edi
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
fld dword ptr [__real@4@4002c400000000000000 (00423fd0)]
主要看最后一样. fld 内存的值. 其实就是把我们的浮点数转为IEE编码.放到内存中.
其实就是放到内存中.
外层调用: 就是调用完毕之后.
0040EB1D call __ftol (004010ec)
0040EB22 mov dword ptr [ebp-4],eax
调用完毕之后,会使用 _ftol. 浮点数转为整数进行转化.下面的返回值放到我们的局部变量中
所以以后看到这样操作.我们就要明白. 返回值是float或者double类型.进行了转换.
_ftol内部
004010EC push ebp
004010ED mov ebp,esp
004010EF add esp,0F4h
;浮点异常检查
004010F2 wait
004010F3 fnstcw word ptr [ebp-2]
004010F6 wait
004010F7 mov ax,word ptr [ebp-2]
004010FB or ah,0Ch
004010FE mov word ptr [ebp-4],ax
00401102 fldcw word ptr [ebp-4]
;从str(0)中取出八个字节放到局部变量 ebp -och中. 所以后面是qword ptr代表8个字节.
;将st(0);从栈中弹出.
00401105 fistp qword ptr [ebp-0Ch]
00401108 fldcw word ptr [ebp-2]
;下方 eax edx同用,eax保存4字节的整数部分. edx则保存小数部分.
0040110B mov eax,dword ptr [ebp-0Ch]
0040110E mov edx,dword ptr [ebp-8]
平展返回.
00401111 leave
00401112 ret
内部则是进行浮点转化.比较等等.
四丶布尔类型
布尔类型就是0 跟 1 表示.在内存中就是这样的表示形式. 0就是 false 1就是true
地址丶指针丶引用表达形式
- 地址
在C++中,使用地址需要使用&取地址符号. 取一个变量所在的内存地址.
int main(int argc, char* argv[])
{
int a = 10;
int &nType = a;
printf("Value = %d\r\n",nType);
system("pause");
return 0;
}
对应汇编代码
.text:00401158 mov [ebp+var_4], 0Ah
.text:0040115F lea eax, [ebp+var_4]
.text:00401162 mov [ebp+var_8], eax
.text:00401165 mov ecx, [ebp+var_8]
.text:00401168 mov edx, [ecx]
.text:0040116A push edx
.text:0040116B push offset aValueD ; "Value = %d\r\n"
.text:00401170 call _printf
可以看到引用其实就是隐藏了细节.
1.赋值变量为10
2.获得这个变量的地址
3.变量地址给引用保存(ntype)
4.获取引用变量地址.
5.从地址取值给edx
6.下方进行打印.
跟指针一样.指针也是保存地址.只不过引用就是对外隐藏了细节.
指针跟引用一样,产生的汇编代码也是一样的.不过使用的时候我们可以直接传引用进行打印.而使用指针
需要加上符号. 例如 printf(nType,type); 前边是引用,后边是指针取值. 两种取值方式不同
- 指针
指针的本质就是存储地址的.只不过有类型一说.表示我已什么方式存储这个地址. 比如
char szBuff[10] = {1,2,3...};
char *sz = szBuffer; 那么sz保存的是szBuffer的地址.只不过a按照1个字节解释.
比如 sz++,因为是char类型.所以地址就是+1, 如果是int类型解释+1就是+4个字节.
如果对其去内容 *sz那么此时的值是2.因为是char *类型解释的地址 sz++ *sz就是3 - 引用
在C++中,创建引用 TYPE & a = szBuffer; 创建引用的时候必须给变量给初始化.
本质就是一个变量的别名.在内存中其实就是对地址 取内容的操作.
1.指针的寻址方式
关于指针.我们说过有不同的表达形式. 例如 BYTE * short *...
因为指针有不同的表达形式.所以自增自减都会产生偏移计算.
例如:
mov eax,byte ptr[ebp - 0xc];
mov ebx,byte ptr[ebp - 0xb];
mov ecx,byte ptr[ebp - 0xa];
....
所以我们可以总结一条寻址公式
目的地址 = 首地址 + sizeof(type) * n的值.
目的地址就是我们要进行寻址目的.
sizeof(type) 就是你的数据类型大小
n的值就是你的偏移量.
例如一个数组:
char szBuf[10] = {1,2...7,8,9,10};
我们想要得到下标为8的位置的的值怎么获得.
高级代码: int a = szbuf[8];完了.
因为有公式,我们可以不用这样写.
写成如下:
目的地址 = 首地址 + sizeof(type) *n; 套公式
szbuffer = szbuffer + sizeof(char) * 8;
此时szBuffer的地址就是指向数组下表为8的位置.我们对其取内容即可获取其值.
int main(int argc, char* argv[])
{
char szBuf[10] = {1,2,3,4,5,6,7,8,9,10};
char *dst = szBuf + sizeof(char) * 8;
printf("Value = %d\r\n",*dst);
system("pause");
return 0;
}
常量 #define与const定义
- define定义的常量.是真正的常量.在预处理的时候就已经替换成值了.是没有内存地址的.
- const定义的常量是假常量.是一个变量.只不过在编译之间不能修改,在汇编层次就是一个变量.可以进行修改的.
总结:
逆向工作者看完本博客,应该思考一下如何逆向. 归根结底,计算机就是输入 - 处理 - 输出的过程
学完各种数据类型表达形式.以及转换等等.
那么在逆向的第一步,就是对于内存中的数据.做一个简单的猜想.
1.首先确定数据的处理存储位置
2.确定位置.得到内存中的值,要确定内存属性
3.内存的属性有 可读 可写 可执行. 基于这个,我们可以知道内存是否是变量.
可读写(常量) 可读可执行(代码)
4.确定内存布局. 一般栈所占的内存跟代码区是不一样的. 堆也是.这是经验总结.可以多看看内存得出结论.
举一个例子,比如数字0x12 34 56 78在内存中的表示形式。
5.熟悉内存中存储数据的大端模式小端模式
大端模式: 低地址放数据的高位.高地址方数据的低位
小端模式: 低地址放低位,高地址放高位.
低地址 --------------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
2)小端模式:Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。
低地址 --------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12