如何绕过IsDebuggerPresent的反调试

看不見的法師
发布: 2025-08-28 10:23:38
原创
952人浏览过

在某爱论坛上看到一个师傅分享了一个关于如何绕过isdebuggerpresent的反调试技术的crackme教程。我闲来无事,决定复现并调试一下。

首先,这里是原文的链接:https://www.php.cn/link/e6a75be3243049a89e4cb0cfddc81082

反调试

什么是反调试技术

反调试技术,顾名思义,就是一种用来防止程序被调试的技术。简单的反调试往往是识别是否被调试,如果是则退出程序、封禁账号等(检测)。更复杂的反调试可以在反汇编代码中插入花指令,使调试器的反汇编引擎无法正确解析反汇编指令(干扰)。门槛较高的反调试则可以从驱动层将调试权限清零,使得调试器失效等(权限清零)。反调试的手段可以大致归纳为:检测、干扰、权限清零三种常见手段。

反调试手段层出不穷,可以分为两类:0环(内核级调试)和3环(用户应用层调试)。之前在写对抗沙盒的时候,判断父进程是否是explorer.exe,不是则退出,似乎也可以作为一种简单的反调试手段。之前没怎么了解过反调试,最多听海哥说过可以检查句柄表。今天就学习一下,先看看Windows的反调试API,0环反调试等以后知识储备够了再学习。

IsDebuggerPresent

IsDebuggerPresent API可以确定调用过程是否正在由用户模式调试器调试。

参考链接:https://www.php.cn/link/dc4a1c1e778909c03a41d2c672c2b962

CheckRemoteDebuggerPresent

CheckRemoteDebuggerPresent API可以确定是否正在调试指定的进程。

参考链接:https://www.php.cn/link/2da5a68c98274485adc266e224d77b0f

开始调试

打开Crackme程序,界面看起来人畜无害。

如何绕过IsDebuggerPresent的反调试

查壳结果显示,这是一个64位的MFC程序,用C++编写,没有壳。

如何绕过IsDebuggerPresent的反调试

ASLR

什么是ASLR

维基百科:在计算机科学中,地址空间配置随机加载(英语:Address space layout randomization,缩写ASLR,又称地址空间配置随机化、地址空间布局随机化)是一种防范内存损坏漏洞被利用的计算机安全技术。ASLR通过随机放置进程关键数据区域的地址空间来防止攻击者能可靠地跳转到内存的特定位置来利用函数。现代操作系统一般都加设这一机制,以防范恶意程序对已知地址进行Return-to-libc攻击。

总的来说,ASLR就是将内存地址虚拟化,我们看到的内存地址并不是真正的内存地址偏移。

ASLR的作用

地址空间配置随机加载利用随机方式配置数据地址空间,使某些敏感数据配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击。粗俗地说,就是使得每次调试工具(如OD、x64dbg等)加载程序后,地址是随机动态分配的,无法使用固定的地址进行定位。

ASLR的体现

用x64 debug打开程序。

如何绕过IsDebuggerPresent的反调试

到达系统断点,我们需要让他到达OEP,即程序入口点。

ALT+F9

如何绕过IsDebuggerPresent的反调试

这里地址是7FF6E.....

再看真实的入口点:

如何绕过IsDebuggerPresent的反调试

明显不一样。

用MFC编译出的64位程序默认是开启ASLR的。

关闭ASLR

找到可选PE头的DllCharacteristics属性,取消DYNAMIC_BASE。

如何绕过IsDebuggerPresent的反调试

如何绕过IsDebuggerPresent的反调试

回到真正的内存偏移。

如何绕过IsDebuggerPresent的反调试

关于DllCharacteristics可以参考:

https://www.php.cn/link/1b2ae1abc7405fb92168d400454c936c

x64反调试

F9让程序运行,但是一运行程序就会直接结束,不会弹出窗口。

如何绕过IsDebuggerPresent的反调试

做到这里不禁让我想到直接写反杀箱的时候一样,一运行就挂。

大概代码是这样:

if (explorer_id == parent_id) {
    CeatRemoThread(explorer_id);
} else {
    exit(1);
}
登录后复制

只不过他这里是其他判断,比如是否被调试,是就直接exit,不是则执行下面的。

于是对ExitProcess下断点。

bp ExitProcess

如何绕过IsDebuggerPresent的反调试

下断点后直接F9运行到断点处。

观察此时的堆栈。

如何绕过IsDebuggerPresent的反调试

这里又返回到crakeme,猜想是否是判断是否在调试之后又回到原本的函数。

选中这一行按回车,跟进反汇编。

如何绕过IsDebuggerPresent的反调试

看到使用了IsDebuggerPresent来反调试。

IDA Pro x64反调试

进入IDA后,按G,并输入刚刚反汇编开始的地址。

如何绕过IsDebuggerPresent的反调试

跳转后。

如何绕过IsDebuggerPresent的反调试

选择startaddress。

如何绕过IsDebuggerPresent的反调试

F5进入伪代码。

如何绕过IsDebuggerPresent的反调试

这里很明确了,就是这个在反调试。

IDA pro 反反调试处理

可以直接在函数头部就直接ret,让他不走IsDebuggerPresent。

这里要用到IDA Pro的KeyPatch功能:

Symanto Text Insights
Symanto Text Insights

基于心理语言学分析的数据分析和用户洞察

Symanto Text Insights 84
查看详情 Symanto Text Insights

选中函数的头部,然后右键 → Key Patch → Patch:

如何绕过IsDebuggerPresent的反调试

接下来要将Patch完的结果导出到文件:

Edit→ Patch Program → Apply patches to input file。

如何绕过IsDebuggerPresent的反调试

OK即可。

如何绕过IsDebuggerPresent的反调试

验证反反调试处理

如何绕过IsDebuggerPresent的反调试

正式Crack

先随便输入一个数看看。

如何绕过IsDebuggerPresent的反调试

本来这里可以搜索字符串,但发现定位有些问题。

换一种思路,定位API,以前写Win32程序的时候,要想在dialog中输出一段字符串,用SetWindowText,这里可以用这个API定位。

bp SetWindowTextW

如何绕过IsDebuggerPresent的反调试

回车,断点就设置好了。

如何绕过IsDebuggerPresent的反调试

然后再点确定。

如何绕过IsDebuggerPresent的反调试

观察此时堆栈,出现了100和密码错误,并且有个返回函数。

如何绕过IsDebuggerPresent的反调试

选中返回函数那一行,回车。

找到附近的"密码正确"。

如何绕过IsDebuggerPresent的反调试

IDA Pro分析

跳转到刚刚"密码正确的地址"。

如何绕过IsDebuggerPresent的反调试

选中函数头部F5,进入伪代码。

如何绕过IsDebuggerPresent的反调试

得到:

如何绕过IsDebuggerPresent的反调试

说实话,这个伪代码不是很能直接看得懂,看了下原作者的,他调试的是Debug版的,跟这个Release版的还是有差别的,感觉Release版IDA很多都识别不了了。

附上作者关于密码的源代码:

void encodeCString(CString str) {                   //简单的字符串加密函数
    for (int i=0;i<str.GetLength();i++) {
        str.SetAt(i, str[i] ^ 0x55);
    }
}
<p>void CMFCApplication2Dlg::OnBnClickedButton1()
{
CString correctStr = L"密码正确";
CString errorStr = L"密码错误";
CString debugStr = L"检测到被调试";
BOOL flag = TRUE;
CString str = GetDlgItem(IDC_EDIT1)->GetWindowText();
CString strList[5];
long t1 = GetTickCount64();                     //获取开始时间</p><pre class="brush:php;toolbar:false;"><pre class="brush:php;toolbar:false;">// TODO: 在此添加控件通知处理程序代码
if (str.GetLength() < 15 || str.GetLength() > 25) {
    flag = FALSE;
    encodeCString(errorStr);
    GetDlgItem(IDC_STATIC)->SetWindowTextW(errorStr);
} else {
    //password
    //610 - 520 - 666 - 233
    CString sToken = _T("");
    int i = 0; // substring index to extract
    while (AfxExtractSubString(sToken, str, i, '-')) {
        //以-进行分割
        //..//work with sToken
        //..
        strList[i] = sToken.Trim();                     //字符串去空格
        i++;
        if (i > 4) {
            //如果分割大于4,则不满足条件
            flag = FALSE;
            encodeCString(errorStr);
            GetDlgItem(IDC_STATIC)->SetWindowTextW(errorStr);
            break;
        }
    }
    if (i != 4) {
        //如果分割不等于4,不满足条件
        flag = FALSE;
        encodeCString(errorStr);
        GetDlgItem(IDC_STATIC)->SetWindowTextW(errorStr);
    } else {
        for (i = 0; i < 4; i++) {
            if (strList[i].CompareNoCase(_T("610")) != 0 &&
                strList[i].CompareNoCase(_T("520")) != 0 &&
                strList[i].CompareNoCase(_T("666")) != 0 &&
                strList[i].CompareNoCase(_T("233")) != 0) {
                flag = FALSE;
                encodeCString(errorStr);
                GetDlgItem(IDC_STATIC)->SetWindowTextW(errorStr);
                break;
            }
        }
        if (flag) {
            for (i = 0; i < 4; i++) {
                strList[i].MakeReverse();
            }
            if (strList[0].CompareNoCase(_T("016")) != 0 ||
                strList[1].CompareNoCase(_T("025")) != 0 ||
                strList[2].CompareNoCase(_T("666")) != 0 ||
                strList[3].CompareNoCase(_T("332")) != 0) {
                flag = FALSE;
                encodeCString(errorStr);
                GetDlgItem(IDC_STATIC)->SetWindowTextW(errorStr);
            }
        }
    }
}

//判断标记
if (flag) {
    encodeCString(correctStr);
    GetDlgItem(IDC_STATIC)->SetWindowTextW(correctStr);
}

Sleep(500);
long t2 = GetTickCount64();                     //获取结束时间
if (t2 - t1 >= 560) {                            //如果时间差大于等于560则超时,是被调试的情况
    encodeCString(debugStr);
    GetDlgItem(IDC_STATIC)->SetWindowTextW(debugStr);
}
登录后复制

}

可以看到跟IDA生成的伪代码差距还是比较大,但还是不影响用源码分析一波算法。

  1. 通过GetTickCount64获取自系统启动以来经过的毫秒数,变量t1。

    GetTickCount64:https://www.php.cn/link/10155a0cc6dde7d912d2ac796956876d

  2. 获取输入的密码长度,如果长度小于15,或大于25,就赋值flag=false,然后SetWindowText"密码错误",并且可以看到这个字符串是由encodeCString加密了的,所以如果一开始如果想直接找字符串,可能就无法准确定位。

  3. AfxExtractSubString:https://www.php.cn/link/91139f12ae3f102e07a6c8c7333b685d

    这个API可用于从给定的源字符串中提取子字符串,通过这个API的返回值可以判断有几个"-",如果是4段密码,且以“-”分割,就可以进入比较字符串环节。

  4. CompareNoCase:https://www.php.cn/link/3c39d9d70b662746b8ef049c9841643a

    该函数使用lstrcmpi函数对一个CString和另一个CString进行比较。

    返回值为:

    由参数lpsz指定这个用于比较的string。如果两个对象完全一致则返回0,如果小于lpsz,则返回-1,否则返回1。

    这里不等于-1就行,也就是不小于。

  5. MakeReverse:https://www.php.cn/link/ed8a8a99fd79417dd5ee86e141fc2233

    功能大概就是反转字符串,所以四个数为610,520,666,233。

  6. 最后有一个计算时间差。

所以总结一下就是:长度满足15-25之间,以“-”分割成4段,每段分别为610、520、666、233,反转后分别为016、025、666、332。

比如这样:

如何绕过IsDebuggerPresent的反调试

但是这个小程序我还是发现不少bug,比如:

如何绕过IsDebuggerPresent的反调试

还有这样写的话程序会直接崩掉:

如何绕过IsDebuggerPresent的反调试

后记

作为学习反反调试的初级阶段,重要的是使用x64 debug和IDA Pro分析的过程,这个还是很有帮助的。

脑海中又浮现了海哥的话:“没有好的正向基础就不会有好的逆向基础。”

如何绕过IsDebuggerPresent的反调试

以上就是如何绕过IsDebuggerPresent的反调试的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号