.exe版本:2.0.6609.4
以容智云考试学员终端为例。
网上已经有一些关于微信小程序解密的优秀文章,出于学习目的,我们就不去参考相关内容了。
作者水平实在有限,如果发现有错误,还请大家指出。
一
行为监控
工具:火绒剑
首先我们来看看当小程序打开时微信做了什么,监控微信的活容行为。因为小程序初始是在PC上运行的,相关的文件必然会在客户端释放,所以我们主要关注微信的文件读写行为。
注意这里有一个类似文件发布的行为,其实监控访问量的时候也有一个读取这个文件夹的行为,根据经验来看,这其实就是简单的读取相关目录,发现没有相关的程序逻辑文件之后,才主动请求服务器下载相关文件。
那么我们的重点就来到了\。根据之前的经验,这里是小程序的主要逻辑所在。
可以监控到很多关于调用堆栈的信息,但是围绕这些堆栈的逻辑可能和文件发布有关,这不是我们的重点。
二
分析文件特征
我们使用任何二进制编辑器打开该文件。
可以看出,程序逻辑明显是经过加密的,所以我们的重点就在于这个文件的解密操作。
三
wxapkg解密思路
因此有两种想法
1、这个是个经典的想法,由于文件是加密的,微信客户端在加载程序的时候必须解密,所以必须打开这个文件。我们设断点的时候应该能找到打开文件的操作,并记录下打开的句柄。微信也需要读取这个文件,我们设一个条件断点,当传递的句柄是我们获取到的小程序文件句柄的时候就断点。在调用栈附近应该有相关的解密操作。这个操作非常可行,但是相对麻烦,微信打开的文件非常多,设断点可能会比较麻烦。
2、如果微信采用了比较常规的加密算法,可以使用IDA插件看看是否有明显的特征。
需要注意的是,在里面并没有打开这个的相关操作。所以我大胆猜测,微信可能重启了一个来加载它。**然后我们对火绒健进行全局监控,看看有没有读操作。
四
重新监控
可以观察到名为.exe的文件对文件进行了两次读取,根据经验,我们重点关注第二次读取操作,双击该操作,查看调用堆栈。
五
解密分析
顺着堆栈,我们在 IDA 和 RVA 中找到这个位置:。
我们可以看到在这个位置读取了文件,我们去dbg里检查一下有没有这个文件的相关数据,附加到.exe中,在相应位置设个断点,运行一下小程序。
破解起来很容易,我们观察到该文件大小与打开的加密文件大小非常接近,并且堆栈中出现了相关的文件信息。
我们来看看里面的数据。
我们可以看到.exe读取了加密文件,不难理解.exe就像一个加载器,在运行时对文件进行解密和加载。利用程序需要读写这块内存的特点,我们针对这块内存区域设置硬件访问断点(Xdbg的内存断点是针对内存页的,可能会断在奇怪的地方,可能是作者不太会利用这个特性)。
我们可以看到断点位于 RVA:。奇怪的是,文件的前 8 个字节被赋值为 0,这让人无法理解。所以我们干脆再次对未修改的内存设置一个硬件访问断点(别忘了卸载之前的断点)。
当我们再次断点时,后面的8个字节被赋值为-1,这个问题也比较难解决,我们再次对剩余区域设置硬件断点。
再次打断在RVA:处,可以看到里面有字符串操作的相关指令,按照指令的功能进行操作即可。
指令:
MOVSB, MOVSW, MOVSD
描述:
移动字符串数据,复制由ESI寄存器寻址的内存地址处的数据至EDI寻址的内存地址处。
此外,我们观察RSI和RDI分别指向的内存区域。
尾部解密
解密程序分析
可以看出,RSI 指向的内存中保存着一个不完整加密文件的十六进制形式(忽略了前 8 个字节,也就是 β)。有意思的是,复制操作的指针并不是指向文件头,而是指向去掉 β 后的 1024 字节。这体现了分组加密的特点,但为什么解密前程序没有停在 1024 字节呢?可能是作者的疏忽,也可能是程序先解密了尾部,再解密了头部。既然已经到了这里,不妨顺着线索一探究竟。
我们可以看到数据正在被赋值给 RDI。我们逐步执行直到字符串操作完成,然后为该内存区域设置一个硬件访问断点。
然后我们再次在RVA上断点。同样,在字符串操作完成后,我们再次在RDI上设置硬件访问断点。
再次运行后,在 RVA: 处中断。
很兴奋的看到我们设置断点的内存区域出现了../,这有可能就是文件解密的位置,事实也确实如此,我们取消硬件断点,再循环上一步的循环几次,确认这确实是解密点,而且是非常简单的异或解密,其实这部分解密过程和微信图片解密是一样的。
至此,我们找到了密文1024字节之后的解密例程,我们默认忽略前8个字节的处理,读者可以自行分析,解密过程中对前8个字节并没有做太多处理,直接忽略即可。
到场
那么我们的rbp对应的密钥0x34从哪里来呢?追踪XOR Key使我们能够做下一步。
亲爱的IDA先生,我们来从IDA中的RVA:,也就是XOR解密的位置来追踪一下Key是从哪里来的。
分析一下,a3,至于a3*…,目的是将int8类型的a3值填充到rbp中8个字节,并进行8字节分组异或,我们简单改名为a3—>即可。
可以看到它是作为参数传入的(为了提高识别度,作者干脆把这个函数改名了)。
查看对该函数的引用,有两个运行时调用和一个直接调用。为了防止飞出,我们在这个函数头上断点以找到调用点。
文件再次分解并通过堆栈回溯,我们来到调用点 RVA:
在IDA中,我们知道它在XOR解密函数中作为第三个参数传递,类型为int8。根据调用约定,我们重点关注寄存器R8:,最后一个字节,也就是0x34。它是怎么来的呢?我们继续往上分析。
VA:r8的最后一个字节被赋值了,我们看看[rax+rcx-0x2]是多少。
我们可以看到rcx指向一个字符串,其实就是微信小程序的ID,简单分析一下,rax是长度,减去0x2之后,[rax+rcx-0x2]指向的是字符串的倒数第二个字符,把该字符对应的编码赋值给r8,就得到了,我们接下来进行对应的查找:
这证实了上面的说法。至此,尾部解密工作告一段落。
解密前1024个字节
解密程序分析
我们再次来到上面的RVA:处并断点,因为根据前面的分析,所有的密文都出现在这里。同样,我们忽略前8个字节,对剩余的内存内容设置硬件访问断点,尝试找到前1024个字节的解密位置。
再次在这里断点(RVA: ),与之前相同,修补字符串操作后,我们跟随目标(RDI)内存区域并命中硬件访问断点。
再次在这里断点(RVA:)断点,重复上面的步骤,在目标地址处断点。
运行后,在 RVA: 处中断。
这里有一个令人兴奋的字段:iv:。加密的一部分需要一个向量,所以我们可以猜测这是加密函数。我们在IDA中转到相应的位置。
前面已经分析过一些变量并重新命名过,所以一眼看上去就很清晰了。后面我们会一步步分析这些变量的命名,但不是现在关注的重点。
引起我们注意的是相似的汇编指令 xmm2 和 xmm1。注意到“aes”这个词,我们可以怀疑密文头是用 aes 加密的。事实上,确实用了这种加密。从学习的角度,我们假设我们不知道相关特性。
既然已经到了这一步,那我们就运行一下,看看相关内存区域里是否有明文信息。
经过几步运行,我们发现RSI指向的内存区域中存在明文特征,与前面分析尾部解密时得到的明文十分相似,至此可以确定这部分逻辑就是解密前1024个字节的逻辑。
所以接下来我们需要解决的问题是:“这是什么加密?”,这样我们才能找到密钥,自己编写解密脚本。
我们先在百度上搜索一下xmm2,xmm1,看看能不能得到一些有用的信息。
以下是来自网上的一些信息:
— 最后的 AES 流程
¶
这是 AES 流的最后一个,其中的密钥来自,位于的 128 位数据()上,并且位于中。
128 位 SSE : 和 相同,且必须是 XMM 。 可以是 XMM 或 128 位 。 YMM 的位 (-1:128)。
VEX.128 : 和 是 XMM 。 可以是 XMM 或 128 位 。 YMM 的位 (-1:128) 是 。
请注意说明中的加粗部分,大致意思是xmm2,xmm1进行逆向解密的最后一轮解密过程,xmm1是Key(扩展密钥,aes将用户设置的密钥进行了扩展,方便计算),xmm2是最后一轮解密的数据。
现在我们可以确认这部分加密是AES加密,接下来分析其对应的解密部分。根据AES加密对应的解密流程,最后一轮解密使用的密钥是用户设置的秘钥。关于AES使用的类似指令的介绍以及加密解密的细节,笔者收集了一篇很优秀的文章:
开始使用 AES-NI - 被遗忘的海滩 | Nagi 的博客 () ()
生成常规分析
我们回到了 RVA:
结合所引文章的介绍,我们大概可以得出这样的结论:
然后我们接下来的重点是扩展密钥缓冲区。生成密钥缓冲区后,我们将进入密钥扩展例程,在这里我们最有可能获得密钥。我们在 IDA 中跟踪密钥缓冲区。
v33 对应 Rcx,扩展的密钥缓冲区来自函数外部。我们在这个函数头上断点,然后进行堆栈回溯。
函数头:
再次破译之后(前面几次破译都无法得到我们想要的调用堆栈,因为在相关参数中没有找到密文缓冲区等特征),按照约定,秘钥应该是r9指向的缓冲区。其实秘钥缓冲区的后十六个字节是原始密钥(16个字节)的一部分,也就是未展开的密钥。密钥展开例程就是通过原始密钥来展开密钥,但是不注意这个细节也没关系,在后面的分析中会体现出来。
我们追溯到RVA:
在这个函数里,已经生成了,所以我们继续堆栈回溯,来到VA:
也就是[rcx+0x10],我们按照同样的方法分析这个函数,发现它也作为参数传递到了这个函数中,因此继续回溯调用堆栈。
回到第二个调用堆栈,我们来到VA:,如下所示:
同样的,[rcx+0x10]也作为参数传入,堆栈再次回溯到RVA:,如下图所示,这里又进行了一次简单的前转,再次回溯堆栈。
来到RVA:,如下图:
我们说的是一个aes实例化的对象,里面存放的是扩展的key,简单分析一下这个函数,我们发现key也是从函数外部通过参数过来的。
继续堆栈回溯-_-||,我们来到RVA:
继续回溯来到RVA:,同样是简单的转发,继续回溯来到RVA:
如下所示:
如图,[[v18]+0x8]指向秘钥,最后计算出秘钥-_-||,这个函数上面还有对v18的相关操作,如下图所示:
其实看到’salt’这几个字,熟悉按键扩展的朋友应该就能立刻反应过来,这里应该是按键扩展的地方。我们进入函数里面。
如图所示:
这个很清楚,我们百度一下
的全称是-Key。简单来说就是一个密码衍生工具。既然有,就一定有,那么这两者有什么区别呢?它是PKCS系列中的标准之一。具体来说是PKCS#5的2.0版本,也是以RFC 2898的形式发布的。它是的替代品,为什么被取代呢?那是因为它只能生成长度的密钥,在计算机性能飞速发展的今天,已经不能满足我们的加密需求。所以就被取代了。在2017年发布的RFC 8018(PKCS #5 v2.1)中,它被推荐作为密码标准使用。并且主要用于防止密码暴力破解,所以设计中加入了自动调节算力的功能,以抵抗暴力破解的可能。
工作流
其实就是将输入的密码和盐应用伪哈希函数PRF()生成哈希值,然后将这个哈希值作为加密密钥,应用到后面的加密过程中。如此反复,多次重复这个过程,从而增加密码破解的难度。这个过程也叫密码强化。
快速看完上面的引文,通过比较函数参数不难推断出:
1.盐值:
2.密钥:小程序ID
3.密钥扩展算法:
4.迭代次数:1000
5.密钥长度:256位
由于密钥长度为256位,我们可以推断加密算法是。
注意它的伪哈希算法是可替换的,所以我们的下一步是找出它使用了哪种哈希算法。
这里我简单用js选取了几种常见的hash算法来尝试一下。
const crypto = require('crypto');
let appid = "wxf2a0156c0235fc4c";
crypto.pbkdf2(appid,"saltiest",1000,32,'sha1',(err, derivedKey) =>
{
if (err) throw err;
console.log("The password is ",derivedKey.toString('hex'));
});
比较扩展键函数运行后的返回值[[rax]+0x8]:
可以验证哈希算法为‘sha1’,对应我们分析的扩展密钥的最后一个字节。
00001D5A002CD188 3D9D6AB5E94DDDE8 èÝMéµj.=
00001D5A002CD190 71122C7B6FFE09D6 Ö.þo{,.q
00001D5A002CD198 C3981F7A8828924E N.(.z..Ã
00001D5A002CD1A0 BD14C8D6E69FEA98 .ê.æÖÈ.½
00001D5A002CD1A8 3336977C3609F094 .ð.6|.63
00001D5A002CD1B0 1969D6DC01305252 RR0.ÜÖi.
至此,我们找到了生成密钥的算法。
分组模式和 iv
分组模式和iv的查找比较简单,只要熟悉AES加密流程和几种加密模式的区别,就可以在加密函数(RVA:)中分析加密模式和向量,这里就不多说了。
经过简单的分析,我们总结前面的分析结果,有以下列表:
除去文件头8个字节,剩余1024解密算法:
秘钥算法:PBKDF2
盐值:saltiest
秘钥:小程序ID,wxf2a0156c0235fc4c
摘要算法:sha1
秘钥长度:32字节
解密算法:aes-256-cbc模式
初始化向量iv:74 68 65 20 69 76 3A 20 31 36 20 62 79 74 65 73 对应字符串:“the iv: 16 bytes” -_-||
1024字节之后的数据处理方式:
解密方式:异或解密
异或Key:微信appid字符串的第二个字符对应的ASCII码形式。
拿到解密后的文件之后就可以使用对应的解压脚本进行解压了,网上不乏解压脚本,很遗憾笔者没有找到一个能够完全解压并恢复出微信开发者工具能够识别的相应文件的(微信开发者工具已经封装好了js等文件的样式,如果要调试源码的话需要将解压后的文件恢复成它能够识别的格式,网上确实有相关脚本,但大多比较老旧,微信开发者工具已经更新了样式,格式有些问题,笔者水平有限就不一一修复了)
以下是不成熟的C++解密脚本。
#include "PKCS7.h"
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
unsigned char iv[] = { 0x74,0x68,0x65,0x20,0x69,0x76,0x3A,0x20,0x31,0x36,0x20,0x62,0x79,0x74,0x65,0x73 };//iv
unsigned char recursive_keys[32] = { 0 };//计算AES秘钥
const unsigned char salt[] = "saltiest";//盐值
int main()
{
string app_id("wxf2a0156c0235fc4c");
/*cout << "Plz enter the AppID:" << endl;
cin >> app_id;*/
//计算递归秘钥
PKCS5_PBKDF2_HMAC_SHA1(app_id.c_str(),app_id.length(),salt,strlen((const char*)salt),1000,32, recursive_keys);
//cout << recursive_keys << endl;
string file_name("__APP__.wxapkg");
/*cout << "Plz enter the name of the file you want to decrypt :" << endl;
cin >> file_name;*/
//读取文件
int file_size = std::filesystem::file_size(file_name);
char* file_buf = new char[file_size] {0};
fstream fp(file_name.c_str(),std::ios::in|ios::binary);
if (!fp.is_open())
{
cout << "Sorry,please check that you entered the correct file name" << endl;
delete[] file_buf;
file_buf = nullptr;
return 0;
}
fp.read(file_buf, file_size);
fp.close();
//AES解密前1024字节内容(忽略文件头6个字节)
AES_KEY aes_key;
AES_set_decrypt_key((const unsigned char*)recursive_keys, 256, &aes_key);
AES_cbc_encrypt((const unsigned char*)file_buf+0x6, (unsigned char*)file_buf+0x6, 1024, &aes_key, iv, AES_DECRYPT);
PKCS7_unPadding* padding_result = removePadding(file_buf + 0x6, 1024);//解除填充
size_t diff = 1024 - padding_result->dataLengthWithoutPadding;//得到解除填充后与保持填充时明文的差值
//1024字节之后的密文解密
//找到异或Key
char xor_key = app_id.c_str()[app_id.length() - 2];
for (int i = 0; i < file_size - 0x6 - 0x400-diff; ++i)
{
file_buf[0x406 + i - diff] = file_buf[0x406 + i] ^ xor_key;
}
//将解密后的数据写入
fstream fp_out(file_name+"_plaintext",ios::out | ios::binary);
if (!fp_out.is_open())
{
cout << "File create failed." << endl;
fp_out.close();
delete[] file_buf;
file_buf = nullptr;
freeUnPaddingResult(padding_result);
return 0;
}
fp_out.write(file_buf+0x6,file_size-0x6-diff);
fp_out.close();
cout << "The file decryption is successful." << endl;
delete[] file_buf;
file_buf = nullptr;
freeUnPaddingResult(padding_result);
return 0;
}
.0.6609.4 下载:
提取码:vp03
仅用于学习目的。
雪编号:
*本文为看雪论坛精彩文章,原创,转载请注明来自看雪社区。
#之前的推荐
1、
2、
3.
4.
5.
6.