首页
社区
课程
招聘
[旧帖] [分享]Anti-Debugging-A Developer's View学习笔记 0.00雪花
发表于: 2012-3-28 21:31 3323

[旧帖] [分享]Anti-Debugging-A Developer's View学习笔记 0.00雪花

2012-3-28 21:31
3323
Anti-Debugging - A Developer's View
这篇文章我是在这里下载到的http://tuts4you.com/download.php?list.66
我写的这个东西不是一个严格的翻译,主要是我最近几天读这篇这个的一点笔记。
作者提到了很多我一直没听过的东西。我感觉很不错。正好我的最近想把自己的看雪号升级成正式会员所以来分享一下。本人水平有限,仅供参考.
废话不多说了下面是我的收获。
原作者把反调试手段分为很多类下面分类介绍
1.基于API的反调试
这个基本上有强大的StrongOD插件存在我们已经可以不用自己处理的。因为原文很长这些简单API使用方法我就不在这详细介绍了很容易在MSDN或者网上搜索到。但是还是应该掌握的。BOOL WINAPI IsDebuggerPresent(void);

BOOL WINAPI CheckRemoteDebuggerPresent(
  __in          HANDLE hProcess,
  __in_out      PBOOL pbDebuggerPresent
);

这两个函数可以检测当前进程 和 任意进程(需要有进程句柄) 是否被调试.
HookApi或者简单的在OD中修改返回值即可绕过.

void WINAPI OutputDebugString(
  __in_opt      LPCTSTR lpOutputString
);
这个函数不是调试输出语句么?
没错就是它用法如下:
DWORD Val = 666;//可以是一个你自己定义任意错误号
SetLastError(Val);
OutputDebugString(L"anything");
if (GetLasrError() == Val)
{
    //调试器存在
else
{
  //没有调试器
}

原作者解释说这个函数在没有调试器存在(没有调试器就不知道像哪输出了啊!!!)的情况下是会产生一个错误的。GetLastError()可以读取到这个错误号(我在一个没有被检测到的OD中调试发现得到的错误号为0x2)。如果跟提前设置的错误号不想等就说明错误发生了既是没有调试器。如果相等的话必然是调试信息输出成功了。。。也就是调试器存在.
另外我的原版OD+SOD插件调试的情况下不会输出调试信息也不会被检测到。我刚刚发现这个。。。

FindWindow(L"OLLYDBG",0);
这个比较没有技术含量。。只是按照窗口标题判断特定的调试器窗口是否存在。。解决方法。。自己修改OD标题或者是插件

另外一个不太有用的:
寻找注册表的以下键值
HKEY_CLASSES_ROOT\dllfile\shell\open with Olly&Dbg\command
HKEY_CLASSES_ROOT\exefile\shell\open with Olly&Dbg\command

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AeDebug Debugger =
前两个是OD被添加到资源管理器右键菜单的时候会出现的注册表项。。。
后一个是系统当前实时调试器的注册表项目  当这个调试器为OD时这个值为OD的路径 -p %ld -e %ld
说这个不太有用最主要的原因是这个很容易被欺骗而且最多代表人家系统里装了OD,不能因为这样的原因就禁止人家用你的软件.

接下来是NtQueryProcessInformation()
这个函数可以通过改变参数查询非常多的信息。。。在这些非常多的信息里面还包含了很多代表进程被调试的信息。
NTSTATUS WINAPI NtQueryInformationProcess(
  __in          HANDLE ProcessHandle,
  __in          PROCESSINFOCLASS ProcessInformationClass,
  __out         PVOID ProcessInformation,
  __in          ULONG ProcessInformationLength,
  __out_opt     PULONG ReturnLength
);

先来说这个函数,这个函数是被ntdll.dll导出的用法是如下这种(这个是我自己的用法高手可以无视):
typedef NTSTATUS (WINAPI *__NtQueryInformationProcess)(DWORD, DWORD, DWORD, DWORD, DWORD);

__NtQueryInformationProcess  _NtQueryInformationProcess;
hmod = LoadLibrary(L"ntdll.dll");
_NtQueryInformationProcess = (__NtQueryInformationProcess)GetProcAddress(hmod, "NtQueryInformationProcess");

这个函数的第二个参数为ProcessDebugPort(0x7)时第三个参数返回的是是调试器的调试端口,如果调试器存在这个值应该是个非0值。代码如下
status = (_NtQueryInformationProcess)(-1, 0x07(ProcessDebugPort), (DWORD)&retVal, 4, NULL);
if (reVal != 0)
{
        //Debug Detected
}

当第二个参数为ProcessDebugFlags(0x1f)时第三个参数中返回的是进程是否被调试的信息返回值为0时进程被调试;返回值是1时进程没有被调试.
例子:
        DWORD debugFlag;
        status = (_NtQueryInformationProcess)(-1, 0x1f, (DWORD)&debugFlag, 4, NULL);
        if (debugFlag == 0)
        {
                MessageBox(NULL, L"Debugger Detected", NULL, MB_OK);
        }
               if (debugFlag == 1)
               {
                             MessageBox(NULL, L"No Debugger Detected!",L"No Debugger", MB_OK);
               }

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 0
支持
分享
最新回复 (15)
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
2
下面介绍另一个从ntdll.dll中导出的函数
NTSTATUS
WINAPI
NtSetInformationThread(

  IN HANDLE               ThreadHandle,
  IN THREAD_INFORMATION_CLASS ThreadInformationClass,
  IN PVOID                ThreadInformation,
  IN ULONG                ThreadInformationLength );

可以这样使用这个函数
typedef NTSTATUS(WINAPI *__NtSetInformationThread)(
                                           IN HANDLE               ThreadHandle,
                                           IN DWORD                                   ThreadInformationClass,
                                           IN PVOID                ThreadInformation,
                                           IN ULONG                ThreadInformationLength );

__NtSetInformationThread _NtSetInformationThread;
lib = LoadLibrary(L"ntdll.dll");
_NtSetInformationThread = GetProcAddress(lib, "NtSetInformationThread");
(_NtSetInformationThread )(GetCurrentThread(), 0x11, 0, 0);

原作者指出像例子中这样用自己的线程句柄作为第一个参数,用0x11作为第二个参数可以让线程与调试器断开。我自己实现时。OD表现的是直接运行程序后F12可以暂停下来再运行程序就不见了.不知道是否就是达到效果了。单步运行到这里的时候OD应该是是会直接跟程序断开的。

接下来介绍一个更加有意思的方法

创建子进程调试程序自身
一个进程只能关联一个调试程序,所以判断自身进程是否被调试的另一种方法是调试自身进程。为了达到这个目的。进程需要先创建一个子进程。通过参数把父进程id传递给子进程,子进程可以通过调用DebugActiveProcess()函数根据返回值判断是否已经有调试程序存在.
这里只贴上关键部分的代码:
DWORD dwPId;
wchar_t szPId[5];
_itow_s((int)dwPId, szPId, 8, 10);
wcsncat_s((wchar_t *)szCmdLine, 64, szPId, 4);
CreateProcess(path, szCmdLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);

...
success = DebugActiveProcess(pid);
if (success == 0)
{
     //'Debugger Detected
}
if (success == 1)
{
    //No Debugger Detected
}

可以在一个程序中利用不同的参数区分是检测进程(子进程)还是运行进程(主进程)
2012-3-28 22:02
0
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
。。好像有个地方没说清楚
//Debugger Detected 意识是 检测到了调试器
另一个
//No Debugger Detected
就是 没有检测到调试期了
不好意思。。。是从笔记里粘贴过来的。。忘了加上这个了。。
2012-3-28 22:05
0
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
4
还有挺多。。今天有点晚了。。有时间继续。。由于是一边整理笔记一边发帖可能比较慢(特别是代码有的代码是原作者的有些是我自己刚仿照着写的)。。。。第一次发帖。。一定坚持的发完。。
2012-3-28 22:13
0
雪    币: 246
活跃值: (14)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
这个好像我好像在某些PDF中见过,记不太清了,还是从看雪下载的,MS都不太起作用,OD一开插件,就都是浮云了。
2012-3-28 22:15
0
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
6
嗯。。。SOD比较强大的屏蔽了大多数。。不过在我的测试中还有几个稍微有点作用的比如OD不能调试文件名以%s%s开头的exe...还是很有意思的
2012-3-28 22:30
0
雪    币: 506
活跃值: (65)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
哦。

好贴。
2012-3-28 23:37
0
雪    币: 506
活跃值: (65)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
开插件始中不是办法,自己明白原理才成,
不然发个问题手忙脚乱,而且由于开了别人的插件,还更不容易定位问题出在哪了。

谢谢楼主。
2012-3-29 07:47
0
雪    币: 39
活跃值: (52)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
我个人认为这帖子很好,那个PDF没找到,谢了。。不过这篇和你的差不多吧??http://bbs.pediy.com/showthread.php?t=70470&highlight=%E8%B0%83%E8%AF%95+%E8%AF%95%E5%87%BD+%E5%87%BD%E6%95%B0
2012-3-29 09:55
0
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
10
休息了几天没来上网。。
粗略的浏览了一下楼上提到的。。。基本相同。。。怪我没有好好看大牛们的帖子。。。我去整理一下看看重点发不一样的地方。。。
另外楼上说没找到发一个更具体的链接。。上面那个是文章列表的
http://tuts4you.com/download.php?view.2896
2012-4-7 20:48
0
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
11
接下来介绍的是
SeDebugPrivilege()与
OpenProcess()
当一个进程是在调试器试器打开或者被调试器附加会被赋予SeDebugPrivilege权限.一些调试器适当的移除这个特权然后还原进程到原始特权等级,然而一些调试器没有完成这步。如果我们的进程正在没有还原这个特权等级的调试器调试的话。我们能从特权信息上检测我们的进程是否被调试。
检测这个的方法是用PROCESS_ALL_ACCESS权限打开一个像csrss.exe这样的进程。正常情况下我们的进程不会被允许用PROCESS_ALL_ACCESS权限打开这样的进程,但是如果特权信息被调试器修改的话我们就可以这样打开它。
下面是一个windowsXp下的一个可用的例子:
//第一步是得到csrss.exe的pID
typedef LONG (WINAPI *__CsrGetProcessId)();
__CsrGetProcessId _CsrGetProcessId;
HANDLE Csrss = 0;
DWORD dwPId;
HMODULE hMod;
hMod = LoadLibrary(L"ntdll.dll");
                        _CsrGetProcessId = (__CsrGetProcessId)GetProcAddress(hMod, (LPCSTR)"CsrGetProcessId");
                        dwPId = (_CsrGetProcessId)();
//然后尝试用这个pId和PROCESS_ALL_ACCESS权限打开csrss.exe 进程
                        Csrss = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPId);
                        if (Csrss != 0)
                        {
                                MessageBox(NULL, L"Detected!", NULL, MB_OK);
                        }
                        else
                        {
                                MessageBox(NULL, L"Not Detected!", NULL, MB_OK);
                        }

测试了自己的3个od,对比之下感觉应该是SOD这个插件屏蔽了这个检测方法.

下面介绍一个思路。。。在我的测试中发现作者的这个想法不是很成熟。。不过思路好像不错。

所有版本的OllyDbg会在的固定地址处存放一个固定 DWORD。可能是用于保证OllyDbg不会错误的尝试调试自身。我们可以用这个信息来检测运行的OllyDbg的实例.我们检查所有进程的0x4b064b地址处的DWORD值是否为0x594c4c4f  
作者的想法是枚举进程检测此每个进程此处内存的值。。先放开隐藏OD进程的插件。。这个位置为此值的进程在我现在的系统上就有很多。。。关闭了三个还提示检测到。。我就不继续尝试了。。我估计作者对于这个值作用的猜想是错误的。导致这个值是一类程序中都有的。。如果真能找到一个在某一调试器上的类似特征码的东西。。反调试不也成了杀毒试的。。查杀调试器了。。这个想想好像不可能 但是 如果程序运行效率越来越高这点损耗已经能被容忍了。。这也是一种办法。。
2012-4-7 22:09
0
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
12
OllyDbg 还有一个文件名bug,不能调试文件名以 %s%s 开头的文件。但是攻击者可以很容易的修改文件名。所以我们还需要在程序中检测文件名称是否被更改.
假设例子程序问件名为%s%s.exe
        GetModuleFileName(0, (LPWCH)&pathname, MAX_PATH);
        filename = wcsrchr(pathname, L'\\');
        if (wcsncmp(filename, L"\\%s%s.exe", 9) == 0)
        {
                //没有发现调试器
        }
        else
        {
                //文件名被修改
        }
当然这个东西还是很容易被pach掉的。。。配合花指令代码混淆一类的东西使用应该能稍微增加一点实用性...

调试Api类似于IsDebuggerPresent(); 其实是在进程的PEB中读取标志检测。Api可能会被hook返回值被修改等。我们可以自己读取标志位。
NtQueryInformationProcess 的第二个参数为ProcessBasicInformation时函数会返回PROCESS_BASIC_INFORMATION structure构的指针.
结构如下
typedef struct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PPEB PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
详细解释参见MSDN这样我们就可以得到PEB的指针了。。当然很多朋友喜欢mov eax,fs:[30h]的这种汇编方式。。这里只是提供另一个个方法。。其他的就是PEB结构的知识了。。
msdn里有一部分通用的
还有一些东西是未文档化的(也就是微软没有明确表名是这样)参见论坛里的精华贴或者是这个网站http://undocumented.ntinternals.net/UserMode/Undocumented%20Functions/NT%20Objects/Process/PEB.html

PEB中还有很多有用的结构可以用来检测调试器

NtGlobalFlag
进程在被调试的情况下运行会与不被调试有所不同。尤其是在创建内存堆时。告知内核怎样创建堆的信息是被存放在PEB结构中的0x68偏移处的。当一个进程被调试的情况下运行 该信息会被设置成标志***_HEAP_ENABLE_TAIL_CHECK(0x10)、 ***_HEAP_ENABLE_FREE_CHECK(0x20)、***_HEAP_VALIDATE_PARAMETERS(0x40)。我们可以通过检测此数据是否为0x70(这三个标志的和)利用这一个特点来判断我们的进程是否被调试
例子:
//很多东西在前面的例子中已经详细介绍了这里只列出主要部分
hmod = LoadLibrary(L"ntdll.dll");
_NtQueryInformationProcess = GetProcAddress(hmod, "NtQueryInformationProcess");
hnd = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, GetCUrrentProcessId());
status = (_NtQueryInformationProcess)(hnd, ProcessBasicInformation, &pPIB, sizeof(PROCESS_BASIC_INFORMATION), &bytesWritten);

value = (pPIB.PebBaseAddress);
value = value + 0x68;
printf("FLAG DWORD : %08x\n", *value);
if (*value == 0x70)
{
        //Detected
}
else
{
        //No Detected
}

//接下来是一个Vista下的技术 由于我只有Xp系统这个还并未测试是否有错或者是否已被插件屏蔽
Vista TEB System DLL Pointer
这个反调试技术是Vista操作系统特有的,当一个进程没有被调试的时候主线程的TEB包含一个指向系统dll名字的Unicode字符串。这个指针位于TEB0xBFC偏移。一般这个Unicode字符串在0xC00偏移。
当进程在被调试的情况下运行。这个系统dll的名称会被替换为"HookSwitchHookEnabledEvent"

使用这个技术,我们应该在函数中首先检查程序是否在Vista系统上运行。如果进程在Vista系统上运行我们在用下面的代码来取得TIB
void * getTib()
{
        void *pTib;
        _asm
        {
        mov        eax,fs:[18h] //fs:[18h]是TIB的地址
        mov        [pTib],eax
        }
        return pTib;
}
得到了TIB的指针之后,读取出0xBFC偏移的内容。如果这个值是0x00000c00我们就可以在0xC00偏移出读出字符串比较他是否是HookSwitchHookEnableEvent
wchar_t *hookStr = L"HookSwitchHookEnabledEvent";
strPtr = TIB + 0xBFC;
delta = (int)(*strPtr) - (int)strPtr;
if (delta == 0x4)
{
        if (wcscmp(*strPtr, hookStr) == 0)
        {
                //Detected
        }
        else
        {
                //No
        }
}
else
{
        //No Detect
}

PEB PRocessHeap Flag Debugger
在PEB中还有存有一个用来告知内核如何为堆分配内存附加结构的指针。这个标志也可以用来检测进程是否被调试。这个结构的一个指针,存放在PEB的 0x18偏移处。在这个结构的0x10偏移处会有一个特殊的标志告知内核是否有调试器存在。如果这个值非零说明调试器存在.

__asm
                        {
                                MOV EAX,DWORD PTR FS:[18h]
                                MOV EAX,DWORD PTR [EAX+30h]
                                MOV EAX,DWORD PTR[EAX+18h]
                                CMP DWORD PTR DS:[EAX+10h],0
                                JNE DebuggerDetecte
                        }
                        MessageBox(NULL, L"Not Detected!", NULL, MB_OK);
                        break;
DebuggerDetecte:
                                MessageBox(NULL, L"Detected!", NULL, MB_OK);
                               
                        break;

LDR_Module

PEB的0xc偏移处是Ldr_data的指针在被调试的情况下Ldr_Module会有特殊的FEEEFEEE这个DWORD值存在我们只要在Ldr_Module中查找这个值即可。
例子如下:
flag = 0;
walk = ldr_module;
__try
{
        while (*ldr_module != 0xEEFEEEFE)//这个是因为Little-Endian的原因
        {
                printf("Value at pointer : %08x\n", *ldr_module);
                walk = walk + 0x01;//walk 是一个字节
                ldr_module = walk;
        }
}
__except (EXCEPTION_EXUTE_HANDLER)
{
        flag = 1;
        //No Debugger Detected
}

if (flag == 0)
        //Debugger Detected

运行这段代码后有两种结果如果这个值被找到则不会触发异常。否则没有找到一直运行到触发内存访问异常。在异常处理中设置标志位则可以判断是否被调试
2012-4-7 22:45
0
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
13
基于硬件和寄存器的检测

1.硬件断点
        断点是一个告诉调试器在特定情况下停止进程运行标志。调试器可以用多种方法创建断点。
软件断点和硬件断点产生的结果是一样的。但是实现上有区别。软件断点是由调试器将指令操作码更改成断点操作码完成的。程序的原始操作码存放在一个表中当断点发生后调试器会将原始操作码写回原地址。这样程序可以继续运行。
硬件断点是由处理器的硬件自己实现的。这些硬件包括专门用来保存断点地址的寄存器。当总线上的地址与专用寄存器里的地址相同的时候,INT 1中断信号被激发。cpu会停止运行。x86系列cpu有8个调试寄存器Dr0~Dr7.Dr0~Dr3保存中断的地址。Dr7包含一些标志位用来启用或忽略Dr0~Dr3寄存器中的断点.Dr6是一个状态寄存器来允许调试器知道那个调试寄存器的断点被触发.
调用GetCurrentThreadContext可以读取调试寄存器的信息。然后我们比较调试寄存器和0x00可以得知当前处理器是否有硬件断点。

GetThreadContext(GetCurrentThread(),&ctx);
                        if ((ctx.Dr0 != 0x00) || (ctx.Dr1 != 0x00) || (ctx.Dr2 != 0x00) || (ctx.Dr3 != 0x00) || (ctx.Dr6 != 0x00) || (ctx.Dr7 != 0x00))
                        {
                                MessageBox(NULL, L"Detected!", NULL, MB_OK);
                        }
                        else
                        {
                                MessageBox(NULL, L"Not Detected!", NULL, MB_OK);
                        }

个人感觉这个思路没问题。。不过在我在看雪下载的汉化原版Od里面。。我都没有检测到。。。
还有常见的Anti思路是把Dr0 Dr1 Dr2 Dr3 都设置成0x00破环已经设置好的断点

近来对于虚拟机的研究尤其是对VMware虚拟机的研究使得程序可以检测出运行在真实系统还是处于虚拟机。
虽然这些方法不是专门用来进行反调试的。但是由于大多数的逆向工作出于安全的考虑是在仿真环境下完成的。特别是恶意软件的调试与逆向。反调试代码可以被设计成只能在真实的本地系统下运行。或者至少帮助
确定一个是启发式的值代表逆向或者调试会话。

虚拟机必须模拟一些硬件资源来使操作系统的像预期的那样启动和工作。这些资源包括寄存器和一些特定的处理器部件。对于反调试有用的是这些虚拟机程序模拟的寄存器可以被没有特权等级的读取。Interrupt Descriptor Table Register(IDTR),全局描述附表寄存器(GDTR),和本地描述附表寄存器(LDTR)都显示出这样的特殊行为。
这些寄存器中的数据指向一些特殊的数据。换一种说法 指向特定数据的指针保存在这些寄存器中因为虚拟机必须模拟这些寄存器,这些指针必须指向不同的区域。基于我们从寄存器中得到的interrupt,global and local Descriptor tables 我们可以判断操作系统是否在虚拟机下运行

GDTR 和 IDTR在多核的处理器情况下用于检测是否为虚拟机是不准确的。因为多处理器系统中每个不同的核心有不同的值.LDTR寄存器 在所有的核心和处理器之间都是可以用来检测虚拟机存在与否的。
LDTR寄存器保存的值高位两字节在实体机系统中一直为0x00

unsigned char ldt_info[6];
int ldt_flag = 0;
_asm
{
        sldt ldt_info;
}

if ((ldt_info[0] != 00) && (ldt_info[1] != 0x00))
        ldt_flag = 1;
if (ldt_flag == 1)
        //VMware Detected
else
        //No VMware

C STR 寄存器
STR寄存器的原理类似于LDT寄存器,我们可以用汇编指令 str 来储存STR寄存器的内容到内存中如果第一字节的内容为0x00第二字节的内容等于0x40我们可以确定程序在Vmware下运行

unsigned char mem[4] = {0, 0, 0, 0};
_asm str mem;
printf("\n[+] Test 4: STR\n");
printf("STR base: 0x%02x%02x%02x%02x\n", mem[0], mem[1], mem[2], mem[3]);
if ((mem[0] == 0x00) && (mem[1] == 0x40))
        //VMware Detected
else
        //No Debugger Detected

//由于我用的是VirtualBox虚拟机 而作者的这些都是根据Vm的虚拟机的检测方法。。我没有实验。。不过感觉思路很不错。。听起来像是真的。。
2012-4-8 13:16
0
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
14
VI 基于时间的检测

在调试时一般都会让程序处于单步运行的模式。在这个模式下同样的代码的运行时间会增长很多。我们可以在特定的代码之间调用一些基于时间的函数。取两次的差值与一个合理的范围进行比较来判断程序是否在单步模式下运行从而判断是否被调试

A RDTSC
x86系列cpu包括一个64位的时间戳计数器储存处理器从启动以来的时间戳计数(这个计数应该与处理器频率有关)。
RDTSC指令可以读取出这个计数。在C中我们可以调用__rdtsc()函数。
在例子中我们连续两次调用这个函数然后计算两次返回值的差值。将差值与0xff比较。从而判断程序是否单步运行。
i = __rdtsc();
j = __rdtsc();
if (j - i < 0xff)
        //No Debugger Detected
ese
        //Debugger Detected

B NtQUeryPerfoemanceCounter

现代处理器中还包含一些硬件性能计数器。这些计数器是一些储存cpu中的硬件相关的活动的计数。用QueryPerformanceCounter函数可以查询到这个寄存器中的值。看例子
QueryPerformanceCounter(&li);
QueryPerformanceCounter(&li2);

if ((li2.QuadPart - li.QuadPart) > 0xff)
{
        //Detected
}
else
{
        //No Detected       
}

C GetTickCount()

我们还可以调用由kernel32.dll导出的API函数GetTickCount()函数来检测。这个函数的返回一个以毫秒为单位的计数。这个数字是从开机启动以来经过的时间。跟之前例子差别最大的地方在于由于单位是毫秒我们用来检测是否为单步操作结果的时间值变成0x10了

li = GetTickCOunt();
li2 = GetTickCOunt();

if ((li2 - li) > 0x10)
        //Detected
else
        //No Detected

D timeGetTime
这个函数跟GetTickCount非常相似。
li = timeGetTIme();
li2 = timeGetTime();

if ((li2 - li) > 0x10)
        //Detected
else
        //No Detected
2012-4-8 13:19
0
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
15
VII  检测代码被修改

自校验可以被用来检测我们的代码是否被修改。调试器在设置软件断点时会把设置断点处的指令的首个字节替换成int 3的操作码0xcc所以通过检测我们的代码是否被修改可以检测到软件断点.

自校验的方法就很多了Hash,Crc等。。这里需要注意的就是:
    我们可以通过创建一个函数来完成校验。函数的输入参数 是 代码的起始地址(一般是一个函数指针)和范围一般通过需要校验的函数的下个函数的指针减去需要校验函数的指针得出.
一但我们有了一个有效的函数来计算目标代码区的CRC值 我们可以运行这个函数和预先硬编码的结果比较
或者我们的程序可以多次调用这个函数每次都与上次计算的结果比较。如果Crc校验结果变化了。很高的可能性是软件断点被设置了。一个需要注意的问题是你要确定你的VisualStudio项目设置中关闭了 增量链接 。
否则像例子中这样直接用函数指针可能不会正常工作(当增量链接使用jump thunks 时程序会增加一些跳转表来指示函数的位置,函数指针会指向跳转表而不是函数体)

在我们的例子中 CRCCCITT函数可以被其他的CRC函数或者哈希函数代替

void antidebug(int pass)
{
        printf("Location of runmycode = %08x and antidebug = %08x\n", &runmycode, &antidebug);
        if (pass == 1)
        {
                original_crc = CRCCITT((unsigned char*)&antidebug, (DWORD)&runmycode - (DWORD)&antidebug, 0xffff, 0);
        }
        else
        {
                the_CRC = CRCCITT((unsigned char*)&antidebug, (DWORD) &runmycode - (DWORD) &antidebug, 0xffff, 0);
        }
        retrurn;
}

void runmycode()
{
        if (the_crc != original_crc)
        {
                //Detected
        }
        else
        {
                //No debugger Detected
        }
}
2012-4-8 13:24
0
雪    币: 84
活跃值: (25)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
16
基于异常处理的检测 依赖于 调试器会内部消化异常不会适当的把他们交给被调试程序处理的事实。
一些调试器中可能会提供设置选择忽略异常还是把异常交给程序处理。但是多数情况下这个功能不是默认开启的。如果调试器不适当的将异常交回给程序,程序可以通过异常处理机制检测到。
M$ Windows用异常链设计来在异常导致程序或者操作系统崩溃之前处理异常。异常链由1个或者多个异常处理程序向量构成,后面紧跟着SEH链 最后是用来处理用其他方式处理失败的异常的未处理异常过滤器(Unhandled exception filter)。
A.INT 3 Exception(0xCC)
通用调试器最基本的操作是用INT 3指令触发一个软件断点。前面已经提到了的硬件断点用另一个中断来触发.Int 3的机器码是0xCC当进程被调试时0xCC会导致调试器捕捉到异常。如果调试器不存在异常会被交给一个异常处理结构来处理。利用这个原理可以检测调试器的存在。

int flag = 0;
__try
{
        __asm
        {
                int 3;
        }
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
        flag = 1;
        //No Debugger
}
if (flag == 0)
        //Detected Debugger;

B INT 2D
INT 2D 被用来访问核心调试系统。这个指令会创建一个异常记录然后激发一个被核心调试器处理的异常。这个指令最终会被内核或者R3层的调试器截获。我们可以把上个例子的int 3 替换为int 2dh来检测调试器。

C         ICE 断点
另一种思路是用ICE断点,这种思路用了一个undocumented操作码我们一般叫做ICE断点来导致一个和int 3一样的中断。我们可以用操作码0xF1来触发这个中断。
例如:
flag = 0;
                        __try
                        {
                                __asm
                                {
                                        __emit 0xf1
                                }
                        }
                        __except(EXCEPTION_EXECUTE_HANDLER)
                        {
                                flag = 1;
                                //No Debugger
                        }
                        if (flag == 0)
                        {       
                                MessageBox(NULL, L"Detected!", NULL, MB_OK);
                        }

D 单步检测

当程序运行时,我们可以告诉线程每运行一条指令后触发一个单步中断。修改EFLAGS寄存器的单步标志可以达到这个目的。我们可以通过pushfd指令把EFLAGS寄存器的值压栈。用or 操作改变标志位。最终用popfd指令存入EFLAGS寄存器。这些操作之后单步中断就被打开了。如果我们的进程正在被调试,调试器会拦截这个异常跳过我们的处理函数。

int flag = 0;
__try
{
        __asm
        {
                pushfd;
                or BYTE PTR [esp+1], 1;
                popfd;
                nop;
        }
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
        flag = 1;
        //No Debugger Detected
}

if (flag == 0)
{
        //Debugger Detected
}

E Unhandle Exception Filter
当在M$ windows操作系统上运行的程序发生异常的时候,会有一些预先定义的异常处理函数。如果如果没有自定义的异常处理函数可以处理发生的异常。最终处理会调用未处理异常过滤器。这个过滤器会接收所有其他异常处理机制不能处理的异常。运用M$提供的API函数我们可以改变未处理异常过滤器的回调函数。
当有调试器存在时。他会在未处理异常过滤器之前插入自己的函数在其之前运行。调试器会在我们的任务回调函数允许我们判断是否有调试器存在之前拦截异常。在例子中我们创建一个当调试器存在时会导致程序崩溃的异常。因为调试器不会处理异常而是允许其运行。当代码在没有调试的情况下运行的时候。未处理异常过滤器会捕获异常然后安全的运行程序。
利用这个反调试方法的第一个步是设置回调函数。这个回调函数在例子中用SetUnhandledExceptionFilter()函数来注册。紧接着这个函数调用的内嵌汇编会导致一个除0异常。

int flag = 0;
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER) exHandler);
_asm
{
        xor eax,eax;
        div eax;
}
//No Debugger Detected

我们的回调函数只是 简单的通过重置默认的未处理异常过滤器然后继续运行处理异常

LONG WINAPI exHandler(PEXCEPTION_POINTERS pExecpPointers)
{
        SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)pExecpPointers-ContextRecord->Eax);
        pExecpPointers->ContextRecord->Eip += 2;
        return EXCEPTION_CONTINUE_EXECUTION;
}

F CloseHandle
另一个琐碎的方法来创建会导致调试器拦截的异常。引发这个异常可以通过调用CloseHandle()关闭一个不合理的句柄。这个调用会直接运行系统调用ZwClose() 引发一个异常。像以上介绍的那些检测方法。我们用__try 和 __except 使得我们的程序可以安全的处理异常。这个特殊的异常机制在程序被调试的情况下会有稍微的不同。
如果调试器存在,这个调用会产生一个异常。
如果没有调试器存在,这个调用只是返回一个错误代码然后继续运行。
基于这样的原理。如果我们的except区的代码被执行过说明调试器存在

__try
{
        CloseHandle((HANDLE)0x12345678);
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
        flag = 1;
        //No Debugger Detected
}

if (flag == 0)
{
        //Detected
}

Control-C Vectored(向量?) 异常

Vectored 异常处理 是M$操作系统最近(这篇文章为2008年)增加的异常处理机制。Vectored 异常处理首先在异常链中被执行 任意数量的VEH处理可以被链在一起。Vectored异常处理部分可以直接加入到代码中不需要__try和__except 区块。当创建vectored异常处理时 会用到一个链接表结构 允许程序在SEH和最终的unhandled exception filter 之间安装无限制数量的异常处理程序.
        当一个命令行模式的应用程序在调试模式下运行键入Crtl-C可以创建一个可以被vectored 异常处理程序检测到并处理的异常。一般命令行模式的应用程序会使用一个处理程序来适当处理这个异常。
如果程序没有在调试器情况下运行,这个信号处理程序会被运行。
通过创建信号处理程序和异常处理程序。我们可以判断是信号处理还是异常处理被运行。
在Visual Studio debugger 异常会在我们的程序中被抛出和处理
AddVectoredExceptionHandler(1, (PVECTORED_EXCEPTION_HANDLER)exhandler);
SetConsoleCtrlHandler((PHANDLER_ROUTINE)sighandler, TRUE);
success = GenerateConsoleCrlEvent(CTRL_C_EVENT, 0);
以上的代码中有3个函数调用。第一个调用我们增加了一个vectored异常处理程序。然后增加了一个信号处理程序。最后我们触发了一个control-C信号.
在信号处理函数中我们只是简单的处理信号然后继续运行。在异常处理函数中我们假设调试器被发现。代码在被调试的情况下异常处理函数会首先被调用。如果VEH没有运行说明没有调试器存在我们的信号处理函数处理了ctrl-c信号。

具体的异常处理函数由于我以前没有接触过。。我就不实现了。。感兴趣的可以自己查资料实现。。原理和以前的异常检测机制是一样的

H Prefix 处理
当程序中内嵌汇编了 prefix 指令 一些有趣的情况会发生。在一些调试器中prefixes指令可能不会正常的执行。当rep指令紧跟着prefix指令时一些调试器可能只是简单的步过这些指令和接下来的一条指令,而不会真正运行指令refix 和 rep后面紧跟的指令。这会导致我们例子中导致中断发生的代码不会运行,进而导致异常处理函数不会运行。这种情况下我们可以检测到调试器的存在。在用这种方法达到反调试目的时候我们应该确定我们需要检测的调试器可以被这种方法检测到(OllyDbg会被检测到)。

int flag = 0;
__try
{
        __asm
        {
                __emit 0xf3; //0xf3 0x64 is REFIX REP:
                __emit 0x64;
                __emit 0xF1; //Break that gets skipped if debugged
        }
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
        flag = 1;
        // No Debugger Detected
}

if (flag == 0)
        // Detected

I CMPXCHG8B 和 LOCK
LOCK prefix指令
被用来设置一个特殊的标志 在运行接下来的指令。这个特殊的表示被用来确保只有一个处理器可以访问共享的内存区域。LOCK prefix 被用来在多核系统中的同步问题。

CMPXCHG8B指令
比较一个 EDX:EAX 中的64位值与目的操作数。如果相等则把ECX:EBX中的64位值存储到目的操作数中否则目的操作数被存入EDX:EAX中。目的操作数是一个8字节的内存区域

如果这两个指令不适当的组合会造成无效指令错误。这段代码在调试器中运行时调试器会捕获无效指令异常然后终止程序运行。如果没有调试器存在我们可以自己处理这个异常后让程序继续运行。
我们设置一个unhandled exception filter然后用内嵌汇编构造一个这样的异常来达到这个目的。

void error()
{
        //No Debugger Detected
        return;
}

...
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)error);
_asm
{
        __emit 0xf0;
        __emit 0xf0;
        __emit 0xc7;
        __emit 0xc8;
}

J OllyDbg 内存断点

OllyDbg 系统会自动处理一些特殊的异常。如果我们把一个内存页面设置成PAGE_GUARD然后尝试运行这个页面上的代码,OllyDbg会把这个当成自己的内存断点.如果在OllyDbg调试的情况下   我们的代码运行到被保护的内存区域     一个内存断点会发生,这样的话我们的异常处理函数不会被运行。如果没有被OllyDbg调试的话我们的异常处理函数会被运行。
构造这样的代码第一步是需要申请一块内存区域。然后用Return的机器指令填充这块区域。这个可以用VirtualAlloc()和RtlFillMemory 用参数0xc3(ret的机器码)来完成.用ret指令填充的话OllyDbg在处理好断点继续运行时代码会从被设置保护的目标内存区域返回原来的代码区域。

memRegion = VirtualAlloc(NULL, 0x10000, MEM_COMMIT, PAGE_READWRITE);
RtlFillMemory(memRegion, 0x10, 0xc3);

然后我们用VirtualProtect()为申请到的内存区域加上PAGE_GUARD限制。
success = VirtualProtect(memRegion, 0x10, PAGE_EXECUTE_READ | PAGE_GUARD, &oldProt);

然后建立我们的异常处理函数和一个指向申请区域的函数指针.我们在__try部分调用这个函数指针然后在异常处理函数部分放入代表我们的函数没有在OllyDbg中运行的代码

myproc = (FARPROC) memRegion;
success = 1;
__try
{
        myproc();
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
        success = 0;
        //No Olly&Dbg Detected
}

最后我们检测我们的异常处理函数是否被运行了
if (success == 1)
        //Olly&Dbg Detected

这些基于异常的调试既是有插件存在的情况下也不能完全忽视很可能会给调试带来麻烦。有时候需要手动处理这些Anti这个时候需要一些Windows Seh异常链的知识。了解了SEH链这些东西也没什么神奇的了只要多注意这些引发异常的代码。预先在异常发生之前就设置好异常处理函数起始处的断点然后让程序跑起来触发异常即可。
2012-4-8 13:28
0
游客
登录 | 注册 方可回帖
返回
//