手把手教你找回加密程序的密码
前不久一朋友说忘了加密程序的密码,里面有很多重要信息,希望我能帮TA找回密码。心想不就是点一下“忘记密码”么,所以爽快答应了,然后就发生了接下来故事。
0×01 前言
当拿到加密文件后,瞬间傻眼。不是联网程序,就是一个孤零零的exe,压根没有“忘记密码”这个选项,双击运行后,弹出那冷冰冰的对话框“please enter password”,于是习惯性地进行了“人工智能弱密码破解”(手动穷举输入密码),一番折腾后,果断放弃了尝试。后背一阵冷汗后,还是硬着头皮上了,谁让咱爽快的答应别人了哩。同时为了挑战一下自己,于是决定将这个程序进行逆向解析,彻底 ”爆”出里面的秘密,谁让咱是屌丝学僧哩,还要指望着修炼技术找工作呢。。。
0×02 猜想
程序在用户输入密码后,会立刻判断出密码的对错,所以文件中存在”对比密钥”用于判断密码的正确性。
对比密钥的几种形式:
1.密码的明文;
2.密码的散列值;
3.使用密码和某一特征值生成对比密钥;
4.使用密码和用户待加密的原数据生成对比密钥;
5.………………
0×03 信息收集
第一步:样本设置
第二步:文件静态分析
选取样本1使用UE的检索功能搜索密码“123456”,未找到结果,可证明密码不是以明文形式存储。
选取样本1为标准样本,使用UE的二进制对比功能,对比样本2、3、4与样本1的差异。
将内容为空的加密程序用UE打开,最后一行行号为c9f0h。对比上述样本,可判断加密程序采用文件末尾追加数据的方式存储密文数据,进一步分析后得到数据存储格式。
第三步:IDA静态分析
使用IDA加载样本1,弹出提示框。
点击Ok,程序成功载入,但是函数窗口中只有一个函数,可见程序加了某种壳对IDA逆向分析产生了干扰。
第四步:脱壳
为减少调试中的干扰,进一步理清程序流程,需要进行脱壳处理。使用壳检测神器PEiD判断壳类型,结果如下:
PEiD成功检测出壳名称为PECompact 2.x -> Jeremy Collake ,如果是未知壳, 则需要进行手动脱壳。这里根据壳信息下载对应的脱壳程序对之前设置的四个样本程序进行脱壳处理。
将脱壳后程序再进行IDA静态分析,函数窗口可获取到所有的函数信息,主程序流程图如下所示
这密密麻麻的分支,让我再次一身冷汗
第五步:动态调试
使用OD进行动态调试分析,主要分析程序的密码比对流程。
1、将样本1载入OD中,F9直接运行。此时,奇怪的事发生了,程序在弹出密码输入框的同时,OD左下角提示进程已经结束,这意味着程序已经运行结束,怎么密码框还在呢?!!!
由此判断程序在运行时,创建了其他工作进程后结束了自身进程。打开任务管理器,可以看到如下疑似进程在运行。Kill掉这个进程后,密码框消失,可证明该线程为密码框工作线程。
2、使用文件夹的搜索功能,对全盘进行了搜索,寻找该进程对应的程序存放目录。
打开其对应的文件夹,可以看到有很多类似程序,这些都是测试时记事本生成的中间程序。
3、运行这些程序,均为空白记事本,没有任何内容。经过UE比对确认,这些程序均为笔记本的原始程序,不包含任何数据。
推测:记事本在运行时,先将原始程序释放在temp目录下,然后创建新进程加参数运行释放的程序。
证明:使用OD查看程序调用的函数列表,找到创建进程的相关函数。
这里确定kernel32库函数CreateProcessA,右键选择“查看引用”。
407BAE处调用了该函数创建新进程,在407BAE下断点,运行程序
程序确实是通过加参数的形式运行的,打开cmd,输入程序路径并且加参数运行
程序弹出错误窗口,并不能正常运行弹出密码输入框。
0×04深度分析
获取上述基本信息后,确定了加密程序的数据存储格式和运行加载方式。下面采用OD附加进程的方式直接对运行后新创建的进程进行调试,来梳理密码判断流程。
1、附加程序
双击运行程序,打开OD->文件->附加,双击新进程名称,将OD附加上去,对其进行调试。
由于此时新进程处于密码框输入状态,所以OD会停留在系统函数领空,此时密码框为不可用状态。为了跟踪密码输入后的流程,需从密码输入后跟踪调试,使用Alt+F9程序会自动运行并停留在用户代码段。此时密码输入框处于激活状态,输入正确的密码,点击确定,程序停留在用户代码段。
2、IDA辅助查看程序流程
在获取到密码输入后的关键地址后,使用IDA加载程序,使用F5反编译功能,查看程序的伪代码。
可以看到While循环中第33行为密码输入框,37行调用函数404648进行了密码正确性判断,39行为“Invalid passphrase”密码错误信息。将OD定位到404648函数的调用处,可以看到函数的返回值eax决定了后续分支走向,这个值便是密码正确性判断后产生的结果。
找到密码判断的关键后,进入404648函数,查看返回值的生成过程,确定关键代码。
repe cmps byte ptr [esi],byte ptr [edi]
ESI为12FE78,EDI为3E3D99
程序对两处0×20字节的数据进行比对,而这两个数据正是样本1中key中的前0×20字节的数据。可确定程序在获得输入密码后,经过一系列加密变换后生成0×20字节的key与文件中的密钥进行对比,来判断输入的密码是否正确。
3、加密流程
确定密钥判断关键位置后,继续向上追溯,寻找对比密钥生成过程。经过一番跟踪后,确定函数407481为对比密钥生成函数。
size_t __usercall sub_407481@(int a1@, void *a2, size_t a3)
{
int v3; // edi@1
size_t result; // eax@1
int v5; // ebx@1
size_t v6; // ebx@7
v3 = *(_DWORD *)a1 & 0x3F;
result = a3 + *(_DWORD *)a1;
v5 = 64 - v3;
*(_DWORD *)a1 = result;
if ( result < a3 )
++*(_DWORD *)(a1 + 4);
if ( v3 && a3 >= v5 )
{
memcpy_0((void *)(v3 + a1 + 40), a2, 64 - v3);
result = sub_404B4C(a1 + 40, a1);
a3 -= v5;
a2 = (char *)a2 + v5;
v3 = 0;
}
if ( a3 >= 0x40 )
{
v6 = a3 >> 6;
do
{
result = sub_404B4C((int)a2, a1);
a3 -= 64;
a2 = (char *)a2 + 64;
--v6;
}
while ( v6 );
}
if ( a3 )
result = (size_t)memcpy_0((void *)(v3 + a1 + 40), a2, a3);
return result;
}
用OD在407481函数处下断点
什么!!!!函数在执行时,参数1是明文内容,参数2是明文长度。可见在此之前,程序利用输入的密码对密文进行了解密,然后又将解密出的明文送入函数407481生成比对密钥。
明文是如何解出来的,稍后再分析。先继续分析407481函数如何利用明文生成对比密钥。经调试后,确定函数404B4C为关键的加密函数。
由于该函数非常复杂,所以并不打算对该加密算法进行深入分析,直接将该函数的汇编代码抠出来作为c程序的内嵌代码使用。
404B4C函数的输入分别为eax(待加密的内容,长度为0×40字节),ecx(生成的密钥存放位置),ecx所指向的密钥存放位置为0×28字节,前8个字节存放着原始明文的总长度,后面0×20字节存储着生成密钥,且这0×20字节密钥设有初始值。
407481函数
输入:参数Arg1:原始明文地址
参数Arg2:原始明文长度
输出: 蓝色框中为原始明文长度
红色框中为密钥变换后的结果
绿色框为明文长度除以0×40后剩余的明文内容
407481过程表示
蓝色框中写入参数Arg2的值
count = Arg2 / 0x40; //明文长度除以0x40
data=Arg1;
While(count--)
{
Call 404B4C(data); //每次讲明文的0x40字节进行加密计算
data=data+0x40;
}
Call 40B240(Arg2 %0x40 ,data); //将明文的剩余部分写入绿色框中
上述过程结束后,程序再次调用了407481函数,参数为原始加密文件中key2密钥,长度为0×10字节。
407481函数运行后将0×10字节的密钥追加在了剩余明文尾部。
随后的call 00407508函数会计算出0×20字节的对比密钥。
经过分析,程序主要利用如下区域的数据进行对比密钥的生成。
蓝色框:原始明文长度+末尾附加的数据长度
红色框:密钥
绿色框:剩余数据
总结对比密钥生成过程:
1.将红色区域初始化,将初始密钥写入
2.每次读取0×40字节的原始数据,使用红色区域的密钥进行加密变换,生成的密钥输出到红色区域;
3.将剩余的原始数据进行填充处理,使其达到0×40长度,然后再进行一次密钥变换,此时生成的密钥便是比对密钥,用于和正确的密钥进行比对。
分析到这里我们发现,对比密钥的生成条件都是可以从文件数据中获取,但是有一个条件现在还不知道,那就是明文数据!!!我们似乎陷入了一个死循环中。。
0×05密文解密
1、猜想:程序在获取到输入密码后,利用输入密码对密文进行解密,用解密后的密文生成对比密钥。
证明:继续回溯跟踪,确定404A6F地址处调用的call 0040854C函数是解密函数,参数1为密文内容,参数2为密文长度,参数3为文件中的key2密钥。
40854C函数在解密过程中还调用了一些未知区域的数据进行解密变换
2、猜想:程序在初始化运行时,生成了这些未知区域的数据。
证明:重新运行程序,断在程序入口点处,查看数据区域41E340处,可见该区域均为0×00。
在此数据区域设置内存写入断点,F9运行。
确定位置后用IDA反编译,可以清晰观察到程序通过调用408FFB函数向41B300,41BB00,41E340,41EB40,41C700,41CB00六个区域写入数据,每个区域长度为0×100。
int sub_408FFB()
{
int v0; // eax@1
int v1; // ecx@1
…………………………
v0 = 1;
v1 = 0;
do
{
v2 = 283 * (((unsigned int)v0 >> 7) & 1);
*(int *)((char *)&dword_41CF00 + v1) = v0;
v1 += 4;
v0 = v2 ^ 2 * v0;
}while ( (unsigned int)v1 < 0x28 );
v28 = 0;
do
{
v3 = v28;
LOBYTE(v2) = v28;
v4 = sub_408F52(v1, v2);
v5 = 2 * (v4 ^ 2 * (v4 ^ 2 * (v4 ^ 2 * v4))) ^ v4;
v6 = (unsigned __int8)(v5 ^ BYTE1(v5) ^ 0x63);
v7 = 2 * v6 ^ 283 * (v6 >> 7) | ((v6 | (v6 << 8)) << 8) | 452984832 * (v6 >> 7) ^ ((v6 ^ 2 * v6) << 24);
……………………
v20 = 72448 * v17 ^ 72448 * v18 ^ 72448 * v19 ^ ((v14 ^ 8 * v14) << 8) | 18546688 * v17 ^ 18546688 * v15 ^ 18546688 * (((unsigned int)v14 >> 6) & 1) ^ 18546688 * v18 ^ 18546688 * v19 ^ ((v14 ^ 4 * v16) << 16) | 452984832 * ((unsigned int)v14 >> 7) ^ 452984832 * v17 ^ 452984832 * v18 ^ 452984832 * v19 ^ ((v14 ^ 2 * (v14 ^ 4 * v14)) << 24) | 283 * ((unsigned int)v14 >> 7) ^ 283 * v17 ^ 283 * v15 ^ 283 * (((unsigned int)v14 >> 6) & 1) ^ 283 * v18 ^ 283 * v19 ^ 2 * (v14 ^ 2 * v16);
……………………
dword_41E340[v8] = v21;
dword_41EB40[v8] = v24;
dword_41C700[v8] = v1;
dword_41CB00[v8] = v26;
}
while ( v28 < 0x100 );
dword_41FF58 = 1;
return 0;
}
0×06.解密流程
0×07.后记
根据上述分析后,可确定加密程序并非将密码存储在文件中,所以不能根据加密数据逆向推导出原密码,而只能根据上述分析的密码验证流程采用字典攻击进行暴力破解。而这也正是很多加密程序需要使用字典进行暴力破解的原因。程序分析到这里,就该写程序结合字典来“爆”出里面的秘密了。。。。。。
原创作者:追影人,本文属FreeBuf原创奖励计划文章,未经许可禁止转载