windows运算符和数据类型重载反CE查询搜索
1、windows下搞逆向,一般都是从数据搜索开始的。数据搜索最常见的工具就是CE了;由于CE自己重写了关键的系统函数,想从驱动层完全杜绝CE读内存数据很难,怎么才能在应用层防止或干扰CE读取自己的内存了?
要反CE搜索,就要先学习CE搜索的原理和数据在内存存放的原理;这里以32位CPU为例:总所周知,理论上讲,内存的读写速度只有cpu寄存器的1/100左右,所以cpu的厂家设计了各种缓存来存储cpu从内存读取的数据,同时也会尽量减少从内存读数据;为了高效从内存读数据,cpu一般都是按照32/8=4字节对齐来写内存数据的,char、byte、short等不满4字节的也占用4自己的空间,比如:
struct{ char a; short b; int c; }
char占用1字节,short占用2字节,int占用4字节,整个结构体占用7字节??? 哈哈,哪有这么简单啊! 为了4字节对齐,char和short一共占用4字节,int占用4字节,那么这个结构体一共占用8字节。cpu读内存的时候按照4字节的颗粒度读取(地址线是32位的),也就是每次读连续的4字节,这也刚好是int的长度; CE搜索的类型展示如下:
CE搜索也要通过CPU去读内存的数据,所以也是按照4字节的宽度从内存读数据的!然后根据用户搜索不同的数据长度做拼接或裁剪;这里以int为例,CE会按照4字节的对齐方式和颗粒度在内存搜索,所以这里反CE的方式也就很明显了:
- 对数据加密:在屏幕显示的数据并不直接存放在内存,而是简单加密,比如异或某个数字。需要在屏幕显示的时候再异或解密
- 或则不加密,把int整型的4个字节分别存放在4个不同的、不连续的内存空间,然后自己重载所需的算术运算;
- 或则把int的4个字节存在4个连续的byte空间,但顺序打乱,读取的时候自己恢复
这里顺便扩展一下:我们用椭圆曲线做非对称加密的时候,密钥一般不低于128位,远超了普通int 32位的长度。为了表达128位的私钥,专门定制了bigNum类型来保存私钥!其原理和这里是一样的,没有本质区别!
下面代码演示通过重载int类型、算术运算来反CE搜索!先写段简单的代码模拟被攻击少血的场景:角色有1500滴血,每被攻击一次就减少50滴血;
#include <iostream> int main() { int hp = 1500; while (true) { hp = hp - 50; printf("剩余血量:%d\n",hp); system("pause"); } }
由于是int类型,在CE里面选择4字节的精准搜索;这里很容易就搜到了,然后改成任意数字;
通过CE的”find out what access this address“还能进一步定位到读写这个内存地址的代码,如下:
这里甚至可以把sub改成add,导致血量越来越多:
血量越来越多:
2、先在怎么通过重载反CE搜索了?
(1)重写int类型
上面说了,CE是从4字节对齐的地址一次性读取4字节来搜索和比对的。这里有两种思路:4字节还是挨着,但是顺序打乱;或4字节顺序不打乱,但是地址随机分配,隔开了!这里为了节约内存、便于管理,我们采用第一种思路,即4字节还是挨着,但是人为把字节存储的顺序打乱!核心代码如下:
myInt::myInt(int val) { mem[0] = new char; mem[1] = new char; mem[2] = new char; mem[3] = new char; *this = val;//只有一个成员变量,所以*this就是取这个成员变量的值 } void myInt::operator=(int val) { /* 1、把原int顺序打散 2、mem这4个byte还是顺序相接的 */ char* read = (char*)&val; mem[0][0] = read[3]; mem[1][0] = read[1]; mem[2][0] = read[0]^0xa5; mem[3][0] = read[2]; }
这里重载了赋值号,方便给成员变量赋值;同时也打乱了原有int中4字节的顺序来存储;为了显示打印,还有做四则运算,还需要重载int类型,核心代码如下:
myInt::operator int() { int val; char* read = (char*)&val; read[3] = mem[0][0]; read[1] = mem[1][0]; /*255以内的数,1个字节和4个字节的效果是一样的,所以这里用个随机数加密一下,避免被CE搜索到*/ read[0] = mem[2][0]^0xa5; read[2] = mem[3][0]; return val; }
有一点需要注意:如果数字在255以内,也就是只用了1字节,那么打乱顺序也是没用的,所以这里随机选一个数给最后一个自己的数加密;
完整的代码如下:
(1)自定义myInt类型来重载int类型的数据变量。头文件如下:
#pragma once class myInt { char* mem[4]; public: myInt(int val = 0); ~myInt(); operator int(); void operator=(int val); };
myInt.cpp实现如下:
#include "myInt.h" //内存分配io效率低,建议一次性分配足够空间,每次按需取用就行 myInt::myInt(int val) { mem[0] = new char; mem[1] = new char; mem[2] = new char; mem[3] = new char; *this = val;//只有一个成员变量,所以*this就是取这个成员变量的值 } myInt::~myInt() { delete mem[0]; delete mem[1]; delete mem[2]; delete mem[3]; } myInt::operator int() { int val; char* read = (char*)&val; read[3] = mem[0][0]; read[1] = mem[1][0]; /*255以内的数,1个字节和4个字节的效果是一样的,所以这里用个随机数加密一下,避免被CE搜索到*/ read[0] = mem[2][0]^0xa5; read[2] = mem[3][0]; return val; } void myInt::operator=(int val) { /* 1、把原int顺序打散 2、mem这4个byte还是顺序相接的 */ char* read = (char*)&val; mem[0][0] = read[3]; mem[1][0] = read[1]; mem[2][0] = read[0]^0xa5; mem[3][0] = read[2]; }
(2)使用myInt类型的代码如下:
#include <iostream> #include "myInt.h" myInt hp = 1500; int main() { //myInt hp = 1500; while (true) { hp = hp - 50; //printf("剩余血量:%d\n",hp); std::cout << "剩余血量:" << hp; system("pause"); } }
效果:这里用CE已经搜不到了!
这种方式可以有效反CE搜索,但是由于每次调用生成myInt类型数据都要分配、“逆序”读写内存,大量的IO操作会导致效率很低,所以只适用于关键数据的保护,比如key之类的!
扩展:这里使用了随机数组来操控内存地址。数组是一种非常简单的数据存储结构或方式,最大的特点就是数据是连续存储的;举个例子:char a[5],内存中的存储方式如下:
这个很简单,大家都能理解。因为本文重载int类型用了二维数组,所里这里有必要探究二维数组的存储方式,比如char a[5][5],大部分同学理解的存储方式是如下的:
可能是为了方便开发人员理解,上图这里实际上是抽象出来的逻辑存储方式。在实际的物理内存中也是这样存储的么?我们做个小测试打印出来看看:
看到了么,实际上是挨着存储的,并不是我们想象的在物理内存中也是“二维”存储的!数组元素的下标仅仅是索引,不代表实际的物理存储方式!所以代码中的mem[0][0]、mem[1][0]、mem[2][0]、mem[3][0]就是通过这种方式隔开存储的!