目录
代码还原技术
一丶简介代码还原
二丶代码还原中的数据类型表现形式
1. 整数类型
2. 无符号整数
3. 有符号整数
4. 浮点数数据类型
5. 浮点编码
4.Double 类型解析.
三丶浮点汇编
1. 浮点栈
2. 浮点汇编
3. 使用内联浮点汇编实现加法
四丶布尔类型
地址丶指针丶引用表达形式
代码还原技术
一丶简介代码还原
例子一: 我们很多人都学习过汇编. 但是汇编的核心知识就是我能看的懂. 有人拿汇编去做外挂. 比如我去追偏移. 看着视频去做. 然后换一个游戏依然这样. 但是终有一天, 你可能发现没意思了. 因为这些知识都是死的. 比如我们想看游戏中, 这段代码做了什么事情. 这个时候就需要将汇编转为高级代码查看了. 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 次方即可.
01/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++ 中, 使用地址需要使用 & 取地址符号. 取一个变量所在的内存地址.
指针
指针的本质就是存储地址的. 只不过有类型一说. 表示我已什么方式存储这个地址. 比如
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;
- }
来源: https://www.cnblogs.com/iBinary/p/9847574.html