在某爱论坛上看到一个师傅分享了一个关于如何绕过isdebuggerpresent的反调试技术的crackme教程。我闲来无事,决定复现并调试一下。
首先,这里是原文的链接:https://www.php.cn/link/e6a75be3243049a89e4cb0cfddc81082
反调试技术,顾名思义,就是一种用来防止程序被调试的技术。简单的反调试往往是识别是否被调试,如果是则退出程序、封禁账号等(检测)。更复杂的反调试可以在反汇编代码中插入花指令,使调试器的反汇编引擎无法正确解析反汇编指令(干扰)。门槛较高的反调试则可以从驱动层将调试权限清零,使得调试器失效等(权限清零)。反调试的手段可以大致归纳为:检测、干扰、权限清零三种常见手段。
反调试手段层出不穷,可以分为两类:0环(内核级调试)和3环(用户应用层调试)。之前在写对抗沙盒的时候,判断父进程是否是explorer.exe,不是则退出,似乎也可以作为一种简单的反调试手段。之前没怎么了解过反调试,最多听海哥说过可以检查句柄表。今天就学习一下,先看看Windows的反调试API,0环反调试等以后知识储备够了再学习。
IsDebuggerPresent API可以确定调用过程是否正在由用户模式调试器调试。
参考链接:https://www.php.cn/link/dc4a1c1e778909c03a41d2c672c2b962
CheckRemoteDebuggerPresent API可以确定是否正在调试指定的进程。
参考链接:https://www.php.cn/link/2da5a68c98274485adc266e224d77b0f
打开Crackme程序,界面看起来人畜无害。

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

维基百科:在计算机科学中,地址空间配置随机加载(英语:Address space layout randomization,缩写ASLR,又称地址空间配置随机化、地址空间布局随机化)是一种防范内存损坏漏洞被利用的计算机安全技术。ASLR通过随机放置进程关键数据区域的地址空间来防止攻击者能可靠地跳转到内存的特定位置来利用函数。现代操作系统一般都加设这一机制,以防范恶意程序对已知地址进行Return-to-libc攻击。
总的来说,ASLR就是将内存地址虚拟化,我们看到的内存地址并不是真正的内存地址偏移。
地址空间配置随机加载利用随机方式配置数据地址空间,使某些敏感数据配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击。粗俗地说,就是使得每次调试工具(如OD、x64dbg等)加载程序后,地址是随机动态分配的,无法使用固定的地址进行定位。
用x64 debug打开程序。

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

这里地址是7FF6E.....
再看真实的入口点:

明显不一样。
用MFC编译出的64位程序默认是开启ASLR的。
找到可选PE头的DllCharacteristics属性,取消DYNAMIC_BASE。


回到真正的内存偏移。

关于DllCharacteristics可以参考:
https://www.php.cn/link/1b2ae1abc7405fb92168d400454c936c
F9让程序运行,但是一运行程序就会直接结束,不会弹出窗口。

做到这里不禁让我想到直接写反杀箱的时候一样,一运行就挂。
大概代码是这样:
if (explorer_id == parent_id) {
CeatRemoThread(explorer_id);
} else {
exit(1);
}只不过他这里是其他判断,比如是否被调试,是就直接exit,不是则执行下面的。
于是对ExitProcess下断点。
bp ExitProcess

下断点后直接F9运行到断点处。
观察此时的堆栈。

这里又返回到crakeme,猜想是否是判断是否在调试之后又回到原本的函数。
选中这一行按回车,跟进反汇编。

看到使用了IsDebuggerPresent来反调试。
进入IDA后,按G,并输入刚刚反汇编开始的地址。

跳转后。

选择startaddress。

F5进入伪代码。

这里很明确了,就是这个在反调试。
可以直接在函数头部就直接ret,让他不走IsDebuggerPresent。
这里要用到IDA Pro的KeyPatch功能:
选中函数的头部,然后右键 → Key Patch → Patch:

接下来要将Patch完的结果导出到文件:
Edit→ Patch Program → Apply patches to input file。

OK即可。


先随便输入一个数看看。

本来这里可以搜索字符串,但发现定位有些问题。
换一种思路,定位API,以前写Win32程序的时候,要想在dialog中输出一段字符串,用SetWindowText,这里可以用这个API定位。
bp SetWindowTextW

回车,断点就设置好了。

然后再点确定。

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

选中返回函数那一行,回车。
找到附近的"密码正确"。

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

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

得到:

说实话,这个伪代码不是很能直接看得懂,看了下原作者的,他调试的是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生成的伪代码差距还是比较大,但还是不影响用源码分析一波算法。
通过GetTickCount64获取自系统启动以来经过的毫秒数,变量t1。
GetTickCount64:https://www.php.cn/link/10155a0cc6dde7d912d2ac796956876d
获取输入的密码长度,如果长度小于15,或大于25,就赋值flag=false,然后SetWindowText"密码错误",并且可以看到这个字符串是由encodeCString加密了的,所以如果一开始如果想直接找字符串,可能就无法准确定位。
AfxExtractSubString:https://www.php.cn/link/91139f12ae3f102e07a6c8c7333b685d
这个API可用于从给定的源字符串中提取子字符串,通过这个API的返回值可以判断有几个"-",如果是4段密码,且以“-”分割,就可以进入比较字符串环节。
CompareNoCase:https://www.php.cn/link/3c39d9d70b662746b8ef049c9841643a
该函数使用lstrcmpi函数对一个CString和另一个CString进行比较。
返回值为:
由参数lpsz指定这个用于比较的string。如果两个对象完全一致则返回0,如果小于lpsz,则返回-1,否则返回1。
这里不等于-1就行,也就是不小于。
MakeReverse:https://www.php.cn/link/ed8a8a99fd79417dd5ee86e141fc2233
功能大概就是反转字符串,所以四个数为610,520,666,233。
最后有一个计算时间差。
所以总结一下就是:长度满足15-25之间,以“-”分割成4段,每段分别为610、520、666、233,反转后分别为016、025、666、332。
比如这样:

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

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

作为学习反反调试的初级阶段,重要的是使用x64 debug和IDA Pro分析的过程,这个还是很有帮助的。
脑海中又浮现了海哥的话:“没有好的正向基础就不会有好的逆向基础。”

以上就是如何绕过IsDebuggerPresent的反调试的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号