目录
0x00 前言
0x01 前情回顾
0x02 Call的基础知识
0x03 Call常见防护手段 {
标志位检测
call链
堆栈检测
线程环境检测
其他防护和检测
0x04 总结
相信大家很多人做逆向都是从“游戏外挂”这种东西开始的,这个技术一旦被恶意利用,便会带来极大的危害。因此,一提及该技术便会遭来异样的眼光
基于此,我们要做的便是“做好自己”,去抛开利益,回归技术的本质,最重要的东西是逆向这个领域给我们带来的乐趣,对事物的理解,人生成长的经历,这个过程才是我们最为宝贵且不可估量的财富。
在这之前,写了一篇关于游戏攻防的文章:
《网络游戏安全之实战某游戏厂商FPS游戏CRC检测的对抗与防护》https://bbs.pediy.com/thread-253552.htm
这篇文章便有提及到在游戏安全的对抗中,诞生的许多对抗游戏外挂作弊的方法,其中便有Call检测被提及到根据自身总结的一些经验结合编程知识,我们通过这篇文章来系统的讲解一下常见的游戏外挂Call检测攻防对抗。
首先,我们今天要讲的是游戏的Call检测,所以为了能让下面的内容让大家理解,我们先来准备一下Call检测的基础知识吧:
1. Call是什么:
call是汇编指令,该指令是计算机转移到调用的子程序
一般来说,执行一条CALL指令相当于执行一条PUSH指令加一条JMP指令
call指令是调用子程序,后面紧跟的应该是子程序名或者过程名
2. Call的格式:
3. Call在反汇编逆向中的体现:
通过查阅资料,发现对于Call的资料比较松散且言语晦涩遮掩,仅仅存在于汇编方面,在此我们将通过C语言编程让大家对Call有一个清晰的认识。
①编写如下C语言代码:
代码需要注意的地方:我们编写了一个add函数,但是并没有在main函数中调用,只是在main函数中进行了赋值变量的操作。
②去除优化编译生成: 去除优化是为了方便调试
③编译运行程序,附加到OD定位调试分析:
0042F170 55 push ebp ; void add()
0042F171 8BEC mov ebp, esp
0042F173 81EC C0000000 sub esp, 0C0
0042F179 53 push ebx
标志位检测
#include <stdio.h>
#include <Windows.h>
int a, b;
void add(){
//利用变量a进行校验
a = a + b;
printf("这里是函数add\n");
}
//游戏攻击Call内层-实现攻击
void Attack_2(){
if (a == 1)
printf("检测到非法调用!\n");
}
//游戏攻击Call外层-实现攻击
void Attack_1(){
add();
Attack_2();
printf("这里是函数Attack_1\n");
}
int main(){
a = 1, b = 2;
getchar();
return 0;
}
1.可以直接更改关键的跳转
2.也可以更改标志位寄存器
3.还可以更改标志位的内存地址
4.如果有CRC校验需要过掉该处代码的CRC检测
5.还可以直接调用最外层的Call,通过C代码可看出:最外层的Call为最安全无检测的Call
6.以及多种技术的交合使用....
Call链
由于该方法没有做代码的混淆,仅仅是通过复杂的调用使其产生Call链来增加IDA等静态分析的能力,因此,在此基础之上,如果时间充裕,可以尝试一下或者采用动态分析。
堆栈检测
#include <stdio.h>
#include <Windows.h>
int a, b;
void add(){
//利用变量a进行校验
a = a + b;
//printf("这里是函数add\n");
}
//游戏攻击Call内层-实现攻击
void Attack_2(){
MessageBox(NULL, "我是Attack2", "cap", MB_OK);
}
//游戏攻击Call外层-实现攻击
void Attack_1(){
add();
Attack_2();
MessageBox(NULL, "合法调用MessageBox", "cap", MB_OK);
//每次更新校验值
a = 1;
}
int main(){
a = 1, b = 2;
Attack_1();
getchar();
return 0;
}
0042F080 /> \55 push ebp ; attack1
0042F110 55 push ebp ; attack2
#include <stdio.h>
#include <Windows.h>
int ret1, ret2;
DWORD addr_Module, size_Module;
//游戏攻击Call内层-实现攻击
void Attack_2(){
__asm
{
mov ret1, ebp
mov eax,[ebp+4]
mov ret2,eax
}
printf("ebp:0x%x [ebp+4]: 0x%x\n 模块起始地址:0x%x 模块大小范围:%x \n", ret1,ret2,addr_Module,size_Module);
if (ret2 < addr_Module || ret2 > (addr_Module + size_Module))
MessageBox(NULL, "非法调用", "cap", MB_OK);
else
MessageBox(NULL, "合法调用", "cap", MB_OK);
}
//游戏攻击Call外层-实现攻击
void Attack_1(){
Attack_2();
MessageBox(NULL, "合法调用MessageBox", "cap", MB_OK);
}
int main(){
DWORD hModule = GetModuleHandle(NULL);
IMAGE_DOS_HEADER *dosHeader = (IMAGE_DOS_HEADER *)hModule;
IMAGE_NT_HEADERS *ntHeaders = addr_Module = (IMAGE_NT_HEADERS *)((DWORD)hModule + dosHeader->e_lfanew);
DWORD dwImageSize = size_Module = ntHeaders->OptionalHeader.SizeOfImage;
printf("模块基地址:0x%x 模块地址取值大小:%x", ntHeaders,dwImageSize);
getchar();
return 0;
}
既然堆栈中的数据作了校验,那我们就选择伪造堆栈数据即可,其方法就是观察寄存器,hook数据,然后写入合法的数据
当然了,如果大家能够找到关键的跳转,也可以更改跳转,甚至nop,不过一般后面的代码会存在心跳数据包,改了小心追封哦
线程环境检测
766E2B18 > 64:A1 18000000 mov eax,dword ptr fs:[0x18] ; GetCurrentThreadId
766E2B1E 8B40 24 mov eax,dword ptr ds:[eax+0x24]
766E2B21 C3 retn
PEB(Process Environment Block,进程环境块)存放进程信息,每个进程都有自己的PEB信息。位于用户地址空间。在Win 2000下,进程环境块的地址对于每个进程来说是固定的,在0x7FFDF000处,这是用户地址空间,所以程序能够直接访问。准确的PEB地址应从系统 的EPROCESS结构的0x1b0偏移处获得,但由于EPROCESS在系统地址空间,访问这个结构需要有ring0的权限。还可以通过TEB结构的偏 移0x30处获得PEB的位置,FS段寄存器指向当前的TEB结构:
//获取线程ID
ULONG CallFlt_GetCurrentThreadId()
{
ULONG ThreadId;
__asm mov eax,fs:[0x24]
__asm mov ThreadId,eax
return ThreadId;
}
//定义结构体保存线程信息
typedef struct Call_ThreadInfo{
DLIST_ENTRY ListEntry;
ULONG ThreadId;//线程ID
}CALL_FLT_ENTRY,*PCALL_FLT_ENTRY;
typedef struct Call_TABLE {
DLIST_ENTRY ListEntryHead; //链表头
ULONG EntryCount; //链表元素个数
CRITICAL_SECTION TableLock;//保存上次的搜索结果
PCALL_FLT_ENTRY LastHit; //保存上次搜索的结果
}CALL_FLT_TABLE,*PCALL_FLT_TABLE;
//获取线程ID
ULONG CallFlt_GetCurrentThreadId()
{
ULONG ThreadId;
__asm mov eax, fs:[0x24]
__asm mov ThreadId, eax
return ThreadId;
}
PCALL_FLT_ENTRY CallFlt_SearchCallFltTable(PCALL_FLT_TABLE CallFltTable, ULONG ThreadId)
{
PCALL_FLT_ENTRY CallFltEntry;
assert(CallFltTable != NULL);
if (CallFltTable->LastHit != NULL && //判断上次查找结构是否匹配
CallFltTable->LastHit->ThreadId == ThreadId)
return CallFltTable->LastHit;
//遍历双向链表
CallFltEntry = (PCALL_FLT_ENTRY)CallFltTable->ListEntryHead->next;
while (CallFltEntry != (PCALL_FLT_ENTRY)(&CallFltTable->ListEntryHead)){
if (CallFltEntry->ThreadId == ThreadId)
return CallFltEntry;
CallFltEntry = (PCALL_FLT_ENTRY)(CallFltEntry->ListEntry->next);
}
return NULL;
}
BOOL CallFlt_IsMyThread(PCALL_FLT_TABLE CallFltTable)
{
ULONG Thread;
PCALL_FLT_ENTRY CallFltEntry;
assert(CallFltTable != NULL);
Thread = CallFlt_GetCurrentThreadId();//获取当前线程ID
CallFltEntry = CallFlt_SearchCallFltTable(CallFltTable, Thread);
if (CallFltEntry != NULL)
//合法线程,返回1,说明是正常调用
return 1;
else
//非法线程,返回0,说明是非法调用
return 0;
}
1.GetCurrentThreadId函数位置下断,观察eax,eax中即为调用的线程id
2.当远程调用时hook此处位置,设置线程id为正常id,或者直接ret
1.依然是GetCurrentThreadId位置下断
2.回溯找到关键跳转,hook伪造返回值或者直接暴力nop
其他防护
看雪ID:小迪xiaodi
https://bbs.pediy.com/user-680946.htm
推荐文章++++
好书推荐