算法难度:⭐⭐⭐(这个有意思)
爆破难度:⭐⭐
反调试:⭐⭐
信息收集
运行情况:
查壳与脱壳:
无壳,但是自带了一个dll文件
调试分析
是个C/C++程序,直接打开可以运行,但是用x86dbg打开则会发生异常,因为跳转到的0地址执行
因为调试器会接管异常的缘故,不管怎么点运行,程序都停留在了这里
首先推测是这里存在反调试!有两个可能:
- 在这之前进行反调试检测,检测到调试器,则不往这个地址里填值,使得这里读取到的内容是0
- 这里就是通过故意触发异常来进行反调试的
接下来先对抗一下反调试
处理反调试
对于猜想1,我先启动了程序,然后再附加查看内存,这个地址的内容依然是0,所以要么是猜想1不成立,要么是跳转完成之后又把值改为了0,经过IDA静态分析里查找这个变量,通过交叉引用查看,发现并没有修改这个变量的迹象,故认为猜想1不成立
对于猜想2,由于这个函数在跳转0地址之前,注册了一个SEH处理函数,所以这里就让SEH来处理异常,我们去SEH里下断点(4024D0),同时设置x86dbg忽略0xc0000005异常
程序成功在异常处理里断下了,如果只是简单的通过让调试器接管异常使得程序不能进行下去来反调试,那么到此程序应该就能运行下去了,点击运行之后,又再次执行到了异常处理里的断点处:
这不对劲,有猫腻!经过不停的步过,发现异常处理函数下面的这个call造成了异常:
这个call的地址取决于edi和ecx,edi是个基址,ecx是索引
这里ecx的值不同,就会执行不同的函数,一定有一个是程序正常执行需要执行的,然后有很多干扰项
不停的运行,到这里ecx的值依次增加3:0,3,6,9…当运行到某一个函数的时候(ecx=C):手动生成了一个奇怪的异常(之前都是jmp 0地址)
再往下,第7次进入这个异常处理的时候,call了另一个不一样的函数:
这个函数调用了函数SetUnhandledExceptionFilter
经过查阅资料:这个函数的功能注册一个终极异常处理函数
通常来说,当程序发生异常之后,如果没有异常处理器能处理,就会执行终极异常处理:UnhandledExceptionFilter
,而这个函数的作用就是自定义一个终极异常处理函数
而这个终极异常处理函数在调试器下不会被调用,在调试器下,处理不了的异常就交给调试器了,只有在无调试器情况下才会交给终极异常处理让程序正常执行,从而达到反调试的效果
也正因为处理不了的异常会交给调试器,所以调试的时候能见到同一个异常出现了很多次程序都不退出
这里因为在调试器下没法进入这个终极异常处理,所以这里对程序进行一些修改,这个call之下有一个push F
,然后再下面的那个call会产生异常,所以这里干脆直接让下面的程序直接变成跳转到异常处理里,修改如下:
运行,程序能跑起来了,接下来打补丁保存这个修改,右键,patch,patch file
调试新保存的程序,结果依然跑不起来,发生了异常:又在jmp 0地址,但是右下角给出了提示,IsDebuggerPresent,看来是另一个反调试
这个反调试好处理,直接修改fs:[30]地址处的第二个字节为0即可,或者用x86dbg插件ScallyHide来隐藏PEB调试痕迹
然后再次重新运行,程序可以调试了,反调试都处理完了(应该)
定位校验函数
输入Name 和Serial,点击按钮,发现没反应,没有错误提示
那就用老办法去找C++界面程序的按钮事件,学习Win32编程的时候,我们知道窗口程序有一个窗口过程函数,当有消息了,就传递给窗口过程来处理
使用xspy工具查看控件ID:按钮id=3ee
接下来,用IDA分析,在导入表中找到分发消息的程序段一定会用的函数:DispatchMessage,然后通过交叉引用定位找到窗口创建函数,在上面不远处看到窗口过程函数:
这个函数很简单:就处理一个事件
查阅资料[3]可知,按钮按下的事件是一个WM_COMMAND消息,这个按钮ID刚好是e33,那这个处理就是按钮按下的过程了(实际测试,在动态调试器下,按下按钮确实会断在这里)
这里的功能是给一个变量赋值为1
接下来看看这个变量是干嘛用的:这里如果按钮按下了,就会进入一大段程序里,否则就跳出
这一大段程序除了不知道干啥的初始化和赋值,值得关注的就是这个call了(名字是逆向完之后重命名的)
交叉引用查看这个call的地址,找到赋值的地方:是来自dll的导出函数
上面那个call:fp_CopyDll,的功能是复制dll到某个地方,然后将文件名保存到全局变量里:
然后对dll文件进行一个解密循环操作:
(难怪之前直接dll拖IDA识别不出来),等执行完这个循环去临时目录拿到解密后的dll来继续分析导出函数,这里按钮的功能就是调用这个导出函数
分析校验函数
随便输入点啥,调用导出函数之前下个断点,看看参数是啥:参数1是用户名,参数2是序列号
这个校验函数的内容就很简单:
上面的初始化过程就跳过不管了,这里分为两部分:
- 生成真码
- 序列号对比
后者就是strcmp,没啥好说的,这里主要就是真码的生成:
- 遍历Name,取一个字节
- 除以0x62得到余数
- 余数作为一个超长随机字符串的索引,取2个值,拼接在一起
- 依次循环,直到拼接完成每个字节索引到的两个字符
注册机
注册码生成算法:
#include <iostream>
auto arr = "fytugjhkuijonlbpvqmcnxbvzdaeqrwtryetdgfkgphonuivmdbxfanqydexzwztqnkcfkvcpvlbmhotyiufdkdnjxuzyqh";
int main()
{
char name[100] = { 0 };
char serial[100] = {0};
std::cin >> name;
int len = strlen(name);
for (int i = 0,j=0; i < len; i++,j+=2)
{
int tmp = name[i];
serial[j] = arr[tmp % 0x62];
serial[j + 1] = arr[tmp % 0x62 + 1];
}
std::cout << serial;
}
效果:
总结
这个有意思,基于异常的反调试和基于PEB的反调试,解密dll文件使用导出函数进行密码校验
这个反调试对于新手来说,确实不好搞,很有价值的一次逆向学习!