前不久一朋友说忘了加密程序的密码,里面有很多重要信息,希望我能帮TA找回密码。心想不就是点一下“忘记密码”么,所以爽快答应了,然后就发生了接下来故事。
当拿到加密文件后,瞬间傻眼。不是联网程序,就是一个孤零零的exe,压根没有“忘记密码”这个选项,双击运行后,弹出那冷冰冰的对话框“please enter password”,于是习惯性地进行了“人工智能弱密码破解”(手动穷举输入密码),一番折腾后,果断放弃了尝试。后背一阵冷汗后,还是硬着头皮上了,谁让咱爽快的答应别人了哩。同时为了挑战一下自己,于是决定将这个程序进行逆向解析,彻底 ”爆”出里面的秘密,谁让咱是屌丝学僧哩,还要指望着修炼技术找工作呢。。。
程序在用户输入密码后,会立刻判断出密码的对错,所以文件中存在”对比密钥”用于判断密码的正确性。
对比密钥的几种形式:
1.密码的明文;
2.密码的散列值;
3.使用密码和某一特征值生成对比密钥;
4.使用密码和用户待加密的原数据生成对比密钥;
5.………………
选取样本 1 使用 UE 的检索功能搜索密码“ 123456 ”,未找到结果,可证明密码不是以明文形式存储。
选取样本 1 为标准样本,使用 UE 的二进制对比功能,对比样本 2 、 3 、 4 与样本 1 的差异。
将内容为空的加密程序用UE打开,最后一行行号为c9f0h。对比上述样本,可判断加密程序采用文件末尾追加数据的方式存储密文数据,进一步分析后得到数据存储格式。
使用 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 ,输入程序路径并且加参数运行
程序弹出错误窗口,并不能正常运行弹出密码输入框。
获取上述基本信息后,确定了加密程序的数据存储格式和运行加载方式。下面采用OD附加进程的方式直接对运行后新创建的进程进行调试,来梳理密码判断流程。
双击运行程序,打开 OD-> 文件 -> 附加,双击新进程名称,将 OD 附加上去,对其进行调试。
由于此时新进程处于密码框输入状态,所以 OD 会停留在系统函数领空,此时密码框为不可用状态。为了跟踪密码输入后的流程,需从密码输入后跟踪调试,使用 Alt+F9 程序会自动运行并停留在用户代码段。此时密码输入框处于激活状态,输入正确的密码,点击确定,程序停留在用户代码段。
在获取到密码输入后的关键地址后,使用 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 与文件中的密钥进行对比,来判断输入的密码是否正确。
确定密钥判断关键位置后,继续向上追溯,寻找对比密钥生成过程。经过一番跟踪后,确定函数 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 长度,然后再进行一次密钥变换,此时生成的密钥便是比对密钥,用于和正确的密钥进行比对。
分析到这里我们发现,对比密钥的生成条件都是可以从文件数据中获取,但是有一个条件现在还不知道,那就是明文数据!!!我们似乎陷入了一个死循环中。。
1、猜想:程序在获取到输入密码后,利用输入密码对密文进行解密,用解密后的密文生成对比密钥。
证明:继续回溯跟踪,确定 404A6F 地址处调用的 call 0040854C 函数是解密函数,参数 1 为密文内容,参数 2 为密文长度,参数 3 为文件中的 key2 密钥。
40854C函数在解密过程中还调用了一些未知区域的数据进行解密变换
证明:重新运行程序,断在程序入口点处,查看数据区域 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; }
根据上述分析后,可确定加密程序并非将密码存储在文件中,所以不能根据加密数据逆向推导出原密码,而只能根据上述分析的密码验证流程采用字典攻击进行暴力破解。而这也正是很多加密程序需要使用字典进行暴力破解的原因。程序分析到这里,就该写程序结合字典来“爆”出里面的秘密了。。。。。。
原创作者:追影人 ,本文属FreeBuf原创奖励计划文章,未经许可禁止转载