本文代码中的inlinehook代码来自海风月影的hook库,DrxHook部分代码来自搜索引擎,特此声明,望周知。
调试器是一个非常好的东西,方便快捷给力(多快好省什么什么的),总而言之,在日常调戏生活里,这是个好东西,可以方便山寨(应该叫“微创新”才对吧)别人的产品功能,破解那些需要钱的注册码等等,但是当自己的产品被别人调戏分析,被山寨+破解的时候,就会非常不爽了。
作为一个调戏师(人...调教师?不要乱想啊,混蛋),对自我产品的Anti调戏,必须要做一些自己独有的手法(私壳?No,no,绝对不是私壳这种稀有宝具啊)。
常见的调试器(OD),都是基于系统API提供的调试接口完成,因此从接口的地方下手,还是比较高效而且简单的。
首先就windows下的调试器而言,必须注入一个调试起始的远程线程,这点毋庸置疑是一个很好的攻击点。传统无特殊情况下,调试器注射的远程线程起始的地址,都是固定的ntdll内的函数:
DbgUiRemoteBreakin
很显然很多人直接简单的想到了最容易实现的一种方式直接hook ntdll内的这个函数。我不得不承认这个注意很好,しかし 残酷的现实说明,仅仅这样是不行的(由于OD这个调试器具有强大的各种插件可以无视这种hook方式的anti)。
DWORD __stdcall MyDbgUiRemoteBreakin(LPVOID lparam)
{
OutputDebugStringA("attach\r\n");
TerminateProcess(GetCurrentProcess(),-1);
return 0;
}
DWORD __stdcall MyDbgBreakPoint()
{
OutputDebugStringA("attach 2\r\n");
//m_Attach2Count++;
//if (m_Attach2Count>1)
{
TerminateProcess(GetCurrentProcess(),-1);
}
return 0;
}
DWORD WINAPI AntiDbgAttach(LPVOID lparam)
{
//挂钩一些特殊函数!
BYTE myHookCodeDbgUiRemoteBreakin[5]={0};
BYTE myHookCodeDbgBreakPoint[5]={0};
VOID *ptrDbgUiRemoteBreakin = (VOID *)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")),"DbgUiRemoteBreakin");
InlineHook(ptrDbgUiRemoteBreakin,(void *)MyDbgUiRemoteBreakin,(void **)&OldDbgUiRemoteBreakin);
void *ptrDbgBreakPoint = (void *)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")),"DbgBreakPoint");
InlineHook(ptrDbgBreakPoint,(void *)MyDbgBreakPoint,(void **)&OldDbgBreakPoint);
RtlCopyMemory(myHookCodeDbgBreakPoint,ptrDbgBreakPoint,5);
RtlCopyMemory(myHookCodeDbgUiRemoteBreakin,ptrDbgUiRemoteBreakin,5);
while(1)
{
if (memcmp(ptrDbgUiRemoteBreakin,myHookCodeDbgUiRemoteBreakin,5)!=0)
{
OutputDebugStringA("find patch DbgUiRemoteBreakin");
TerminateProcess(GetCurrentProcess(),-1);
}
if (memcmp(ptrDbgBreakPoint,myHookCodeDbgBreakPoint,5)!=0)
{
OutputDebugStringA("find patch DbgBreakPoint");
WriteReadOnlyMemory((LPBYTE)ptrDbgBreakPoint,myHookCodeDbgBreakPoint,5);
}
Sleep(3000);
}
return 0;
}
虽然hook的anti不是很成功,但是也对某些没有插件的调试器产生了致命anti(比如od2)。不过作为一个伟大的调教师(说漏嘴了吧,更正一下是调戏师),是不会满足于简单的这样的anti的,于是一个新的想法出现了,为什么不从线程启动的方面入手呢?
经过翻阅WRK这种万金油级别的代码后,发现原来用户态线程不论如何都会是以ntdll中的LdrInitializeThunk作为命运中的那个开始啊。
那么只要简单的hook一下,是不是就可以过滤到了某些特殊的东西,呵呵。But 只是hook这里的话,可能是会引发一些冲突,比如跟某些壳,某些第三方XX,于是再深入一点,直接hook中间深层次的东西(LdrpInitialize),那样就好了(更深的方式可以hook ntdll里的KiUserApcDispatcher来搞,不过那样子更加的麻烦)。
DWORD GetLdrpInitializeAddress()
{
DWORD dwAddress =0;
DWORD dwIndex=0;
dwAddress = (DWORD)GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")),"LdrInitializeThunk");
for (dwIndex=0;dwIndex<0x1000;)
{
unsigned char *pOpCode=NULL;
DWORD dwFunc = dwIndex+dwAddress;
DWORD Len = SizeOfCode((void *)(dwFunc),&pOpCode);
if (*pOpCode==0xE8||*pOpCode==0xE9)
{
return *(DWORD *)(dwFunc+1)+dwFunc+5;
}
dwIndex+=Len;
}
return 0;
}
VOID
CDECL
LdrpInitialize(
PCONTEXT ThreadContext,
PVOID NtdllBase
)
{
MEMORY_BASIC_INFORMATION mbi={0};
DWORD ThreadStartAddr = ThreadContext->Eax;
//AfxMessageBox(TEXT("nima"));
//("come on\r\n");
if(VirtualQueryEx(GetCurrentProcess(),(LPCVOID)ThreadStartAddr,&mbi,sizeof(MEMORY_BASIC_INFORMATION))==sizeof(MEMORY_BASIC_INFORMATION))
{
//OutputDebugStringA("in hooked\r\n");
char szString[100];
sprintf(szString,"%08x type start %08x\r\n",mbi.Type,ThreadStartAddr);
//::MessageBoxA(NULL,szString,"123",MB_OK);
if (!(mbi.Type&MEM_IMAGE))
{
OutputDebugStringA("find remote thread\r\n");
TerminateThread(GetCurrentThread(),-1);
}
}
__asm mov ebx,esp
OldLdrpInitialize(ThreadContext,NtdllBase);
__asm mov esp,ebx
}
DWORD WINAPI AntiRemoteThread(LPVOID lparam)
{
BYTE myHookCodeLdrp[5]={0};
DWORD LdrpInitializeAddress = GetLdrpInitializeAddress();
if (LdrpInitializeAddress)
{
OutputDebugStringA("hook\r\n");
InlineHook((void *)LdrpInitializeAddress,(void *)LdrpInitialize,(void **)&OldLdrpInitialize);
RtlCopyMemory(myHookCodeLdrp,(PVOID)LdrpInitializeAddress,5);
while(1)
{
if (memcmp((PVOID)LdrpInitializeAddress,myHookCodeLdrp,5)!=0)
{
OutputDebugStringA("find patch Ldrpinit");
TerminateProcess(GetCurrentProcess(),-1);
}
Sleep(3000);
}
}
return 0;
}
看起来是解决了大问题了,拿上OD+SOD(这里的SOD不是日本的SOD影视公司)测试一下吧,好吧,再次未遂。纳尼~~~(这里要不要配上,我还会回来的!),虽然如果在驱动层次处理起来非常简单,但是考虑一下,本文的主题所以现在还是继续走常规应用层的方式。
对于SOD而言前面的那些方式不太给力,而且没有阻止世界的崩坏,那么还是继续施展更加强大的禁术吧。由于SOD的Anti Anti Attach的强大功能,所以在附加调试器上应用层的Anti手法略微有些无力,其实也有一些取巧的方式(比如添加线程标记,然后循环找没有标记的线程啊这种方式..)。
其实调戏中比较重要的是断点的使用,对于F2这类虚弱的软断,相信很多人都知道怎么抹杀(经常听过CRC吧,内存代码完整性校对检测啊——其实SHA1,MD5,都是可以用来做这个检测的,CRC太容易产生碰撞了)。大部分的调戏者都比较喜欢使用硬断来完成一些工作,比如寻找做完整性检测的代码并和谐。
对硬断,每一个调戏师自然要有一套自己的Anti见解。最常看到的那些壳的手法就是不断循环取出调试寄存器的值,进行研究其中是否被设置了断点,但是这种虚弱的方式对于现在的调试器的高端大气上档次的插件自然而然的不好用啊(themida壳和vmp壳的方式完全就是在送经验....有木有一种:Timor队长正在送命的感觉)。
既然调试器可以设置硬断,喂毛自己不能设置,自己设置后,你懂得,你懂得....呵呵(为了避免VEH冲突,这里使用了Hook RtlDispatchException 的方式)
VOID
WINAPI
MyExitProcess(
__in UINT uExitCode
)
{
return ;
}
// CTestAntiODDlg 对话框
DWORD WINAPI ThreadAntiHBP(LPVOID lparam)
{
VOID * ptrExitProcess = NULL;
ptrExitProcess = (VOID *)GetProcAddress(GetModuleHandle(TEXT("kernel32.dll")),"ExitProcess");
InitDrxHook();
DrxHookEx((void *)ptrExitProcess,(void *)MyExitProcess,(void **)&OldExitProcess,GetCurrentThreadId());
Sleep(5000);
//MessageBox(NULL,TEXT("Job Done"),TEXT("ok"),MB_OK);
while(1)
{
//OutputDebugStringA("begin kill me\r\n");
//DrxHookEx((void *)ptrExitProcess,(void *)MyExitProcess,(void **)&OldExitProcess,GetCurrentThreadId());
Sleep(2000);
//OutputDebugStringA("kill me");
CONTEXT ctx={0};
ctx.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(GetCurrentThread(),&ctx);
if( ctx.Dr0!=(DWORD)ptrExitProcess &&
ctx.Dr1!=(DWORD)ptrExitProcess &&
ctx.Dr2!=(DWORD)ptrExitProcess &&
ctx.Dr3!=(DWORD)ptrExitProcess)
{
AfxMessageBox(TEXT("发现调试器"));
ExitProcess(-1);
}
}
return 0;
}
做了这么操作,万一被识破了怎么办啊,这个很简单加个VMProtect把邪恶的一面保护起来(其实SafeEngine更好一些)。但是仅仅这样是不能毁灭整个调试世界的,不过既然有调试器附加,那么根据海森堡测不准的原则,那么神奇的调试器会导致Timer变得不准时发生(貌似很久以前就有人提过吧,难道是微创新?),很容易可以通过Timer+稳定计数器模式识别是否自己被人为的干扰了。
带上timer部分的整体工程完整代码: TestAntiOD.zipIGS游戏安全技术培训
QQ群:48715131
欢迎有兴趣研究各类游戏安全相关技术的人员加入。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课