系列
【原创】反调试实战系列一 x64dbg+IDA 过IsDebuggerPresent
【原创】反调试实战系列二 TLS反调试+CheckRemoteDebuggerPresent原理
前言
挺久之前就想开一个反调试系列的坑,而且正好可以用这个实战系列来巩固所学
这次分析的程序为64位,用到的调试工具有:IDA Pro (64-bit)
和 x64dbg
这次的CrackMe并不难,有兴趣的可以尝试先自己动手破解看看
反调试介绍
在实战之前,先大致介绍一下反调试
什么是反调试
反调试技术,顾名思义就是用来防止被调试的一种技术
简单的反调试往往是识别是否被调试,如果是则退出程序,封禁账号等等 (检测)
再复杂些可以在反汇编代码中插入花指令,使调试器的反汇编引擎无法正确解析反汇编指令(干扰)
门槛较高的反调试则可以是从驱动层将调试权限清零,使得调试器失效等等 (权限清零)
反调试的手段可以大致归纳为:检测、干扰、权限清零 三种
反调试的常见手段
反调试手段层出不穷,可以分为两类:
- Ring0(内核层反调试)
- Ring3(应用层反调试)
Ring3
Windows API中提供了两个用于检测是否被调试的函数:
- IsDebuggerPresent
- CheckRemoteDebuggerPresent
Windows API |
作用 |
IsDebuggerPresent |
确定调用进程是否由用户模式调试器进行调试 |
CheckRemoteDebuggerPresent |
确定是否正在调试指定的进程 |
除了使用上述两个Windows API外 还有不少在Ring3下的反调试手段:
如检测 PEB(Process Environment Block)进程环境块 中的BeingDebugged、ProcessHeap等
除了检测PEB外,还可以检测 软件断点、硬件断点,代码校验等等
除了检测,之前提到的插入花指令进行干扰也属于Ring3中的反调试手段
在Ring3下的反调试手段 五花八门,这里仅列举出了一小部分
Ring0
在Ring0下的反调试保护,TenProtect不可谓不强,在先前的【原创】TP驱动保护分析系列一 定位TenProtect保护中已经提及
这里限于篇幅原因不再赘述
反调试实战
前面稍微补充了一点关于反调试的知识,接下来正式进入实战环节
要调试的程序
为了更好地起到学习作用,这次要调试的程序是我自己写的一个小demo,是一个MFC程序(练习了一波MFC)
界面比较简陋,毕竟是个小demo,不要介意
关于这个demo的代码之后也会放在后面的附件里,有需要的可以自行取用(* ̄3 ̄)╭
实战流程
查壳
首先使用PE工具:DIE(Detect It Easy) 查壳
可以看到,程序并没有加壳,并且是64位程序、用MFC编写
关闭ASLR
使用MFC编译出的64位程序默认是开启ASLR的,不利于调试,需要先关闭
什么是ASLR
ASLR全称Address Space Layout Randomization,又称地址空间配置随机化、地址空间布局随机化
ASLR的作用
地址空间配置随机加载利用随机方式配置数据地址空间,使某些敏感数据配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击
粗俗地说,就是使得每次调试工具(如OD、x64dbg等)加载程序后,地址是随机动态分配的,无法使用固定的地址进行定位
ASLR的体现
上面纯粹的说明可能不是很直观,接下来使用x64dbg载入程序
可以看到,x64dbg默认是中断在了系统断点,我们需要它运行到OEP(程序入口点)
使用快捷键:ALT+F9 运行到OEP(程序没有加壳,所以可以运行到OEP),或者 调试→运行到用户代码
得到了:
可以看到此时的EntryPoint为:
00007FF6950D14F1 | E9 CADE0000 | jmp <crackme.wWinMainCRTStartup> |
记录下此时的地址为:00007FF6950D14F1
如果学习过PE就会知道 EntryPoint的地址 = EntryPoint + ImageBase
(不了解这个知识点的可以回顾:PE文件笔记五 PE文件头之扩展PE头)
从前面的DIE工具的查看中可以得到:
正常的 EntryPoint的地址 = EntryPoint + ImageBase = 0x114F1 + 0x140000000 = 0x1400114F1
但是此时的地址很明显不等于0x1400114F1,这就是ASLR的体现
使用PE工具关闭ASLR
知道了ASLR会干扰调试,于是要使用PE工具关闭ASLR
ASLR由 扩展PE头中的DllCharacteristics决定,关于DllCharacteristics可参考:DllCharacteristics
验证ASLR的关闭
关闭完ASLR,再使用x64dbg载入程序,查看此时的OEP:
可以发现,此时的EntryPoint地址就和前面计算出来的地址一致,为:0x1400114F1
x64dbg定位反调试
载入程序以后,要先让程序跑起来再设置相关的API断点,于是先运行
使用快捷键:F9 使得程序运行起来
但是当运行F9后,会发现程序直接退出了(不使用调试工具时程序是可以正常运行的);这也就是本帖的关键了:反调试
通过上面的操作,可以推测出:程序检测当前是否正在被调试,如是是则直接退出程序
推断出大致的流程后,可以写出伪代码:
void AntiDebug(){ //反调试函数
bool IsBeingDebugged=checkIsDebug();//通过某种方式判断当前程序是否正在被调试
if (IsBeingDebugged){ //如果正在被调试
exit(); //退出程序
}
}
可以得出调用情况为:AntiDebug→exit
于是可以从exit(退出程序)入手,开始定位
退出程序一般会使用到ExitProcess()这个Windows API,于是对这个函数下断点
在底下的命令行输入:
bp ExitProcess
然后可以得到:
确定设置完断点后,按F9让程序运行起来,然后断点断下:
注意堆栈中调用情况:
找到调用的该程序的函数,可以怀疑这个函数相当于AntiDebug函数(用来反调试)
选中这一行,然后回车,查看其对应的反汇编
得到:
不难发现在调用exit函数的前面调用了IsDebuggerPresent来检测是否被调试
IDA Pro分析反调试
为了更清晰地分析代码,使用x64dbg定位到了关键函数后,可以搭配IDA Pro进行分析
使用IDA Pro载入程序后,按G键弹出:
这里要跳转的地址为前面得到的地址:这里填14001A1D8
跳转后得到:
选中函数的头部,按快捷键:F5查看其对应的伪代码:
得到:
即:
__int64 StartAddress_0()
{
__int64 *v0; // rdi
__int64 i; // rcx
__int64 v3; // [rsp+0h] [rbp-30h] BYREF
v0 = &v3;
for ( i = 70i64; i; --i )
{
*(_DWORD *)v0 = -858993460;
v0 = (__int64 *)((char *)v0 + 4);
}
sub_140011C12(&unk_14004201F);
if ( IsDebuggerPresent() ) //通过IsDebuggerPresent判断是否被调试
exit(0); //如果检测到被调试则退出程序
Sleep(0x64u); //为防止线程占用过高,使用Sleep
beginthreadex(0i64, 0, StartAddress, 0i64, 0, 0i64); //启动检测线程
return 0i64;
}
到这里其实就已经十分清晰了,接下来开始处理反调试
IDA Pro处理反调试
分析出了该函数就是个反调试函数,于是可以直接在函数头部使其返回,让反调试函数无效
这里要用到IDA Pro的KeyPatch功能:
选中函数的头部,然后右键 → Key Patch → Patch:
Patch完结果如下:
接下来要将Patch完的结果导出到文件:
Edit→ Patch Program → Apply patches to input file
确定导出即可(导出的时候,记得要先关闭x64dbg,不然程序被占用会无法导出)
验证反调试的处理
此时再使用x64dbg载入程序 并让程序运行起来,可以发现此时就可以正常运行了:
x64定位Crack相关函数
摆脱了反调试的干扰后,终于可以正式开始Crack了
一般来说,对于没有加壳的程序直接搜索字符串即可,但这里字符串是被加密的,于是不能通过字符串直接定位
换个角度,可以通过对相关的API函数下断进行定位
这里可以发现当输入了错误的密码后,等待Crack中变成了密码错误
这很显然是对标签文本的修改,可以尝试对SetWindowTextW下断点:
bp SetWindowTextW
设置完断点后,再点击确定来触发断点:
观察此时的堆栈情况:
可以看到,堆栈中有输入的密码:610 和 要设置的文本:密码错误
于是可以以此为突破口,继续分析
选中 L"密码错误"上面的函数,按回车查看其对应的反汇编
翻看附近的代码,可以看到在代码下方不远处可以找到:
这里就算定位到了关键的函数处,接下来使用IDA Pro进行分析
IDA Pro分析Crack相关函数
在IDA Pro中 按G弹出跳转地址窗口,然后填入要跳转的地址:140019272
得到:
接下来故技重施,选中函数的头部,然后按F5查看伪代码:
得到:
到了这里其实基本上就已经水落石出了,已经有人解出了密码,详见置顶楼
总结
该篇是反调试实战这个系列的开篇之作,此次实战只涉及了一个最简单的反调试:IsDebuggerPresent
后续还会不断更新其它反调试实战内容(应该会吧?咕咕咕(づ ̄ 3 ̄)づ),希望大家多多支持(。・∀・)ノ゙
稍微总结一下此篇的内容:
- 反调试的手段可以大致归纳为:检测、干扰、权限清零 三种
- 分析程序时,可以采取x64dbg搭配IDA Pro 动静结合的方式进行分析
- ASLR可以使用PE工具关闭,之后调试起来更方便
- 反调试的手段五花八门,重点在于如何定位,一般都是以某个相关函数作为突破口进行分析
此次的实战确实很简单,重点在于分享反调试相关的思路和x64dbg配合IDA Pro分析的操作,大佬勿喷
帖子中可能会有不妥之处,欢迎大家指出
附件
CrackMe下载地址
CrackMe.zip
CrackMe源代码
(因为MFC项目还挺大的,这里截取出关键的代码)
反调试代码
unsigned int __stdcall ThreadFun(PVOID pM) //反调试线程
{
if (IsDebuggerPresent()) { //使用IsDebuggerPresent判断是否被调试
exit(0); //被调试则退出
return 0;
}
Sleep(100); //休眠,防止线程创建过多,导致资源占用
HANDLE handle = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL); //继续创建反调试线程
return 0;
}
HANDLE handle = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL); //创建线程
密码验证相关代码
void encodeCString(CString str) { //简单的字符串加密函数
for (int i=0;i<str.GetLength();i++)
{
str.SetAt(i, -str[i]); //简单的加密
}
}
CString correctStr = L"密码正确";
CString errorStr = L"密码错误";
CString debugStr = L"检测到被调试";
void CMFCApplication2Dlg::OnBnClickedButton1() //按钮"确定"的响应事件
{
// TODO: 在此添加控件通知处理程序代码
//获取到edit1的内容 然后给edit2赋值
CString str;
edit1.GetWindowTextW(str); //获取输入的密码
WCHAR out[1024];
CString strList[4];
CString correctList[4]; //用来存放正确的密码,后面拿来比较
BOOL flag = TRUE; //标志,用来标记密码是否正确
correctList[0] = "016";
correctList[1] = "025";
correctList[2] = "666";
correctList[3] = "332";
encodeCString(correctStr); //简单的加密
encodeCString(errorStr); //简单的加密
encodeCString(debugStr); //简单的加密
long t1 = GetTickCount64(); //获取开始时间
if (str.GetLength() > 25 || str.GetLength() < 15) { //字符串长度判断
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(correctList[i].MakeReverse())==-1){ //注意这里的MakeReverse()
flag = FALSE;
encodeCString(errorStr);
GetDlgItem(IDC_STATIC)->SetWindowTextW(errorStr);
break;
}
}
}
}
//判断标记
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);
}
}
留一份副本在论坛里,土豪专享( •̀ .̫ •́ )✧