WeChatAppEx.exe 版本:2.0.6609.4

以融智云考學生端為例。

網上已經有關于微信小程序解密的非常優秀的文章,本著學習的目的便不參考相關內容。

筆者水平實在有限,如發現紕漏,還請讀者不吝賜教。

一
行為監控

工具:火絨劍

首先看看打開一個小程序微信做了點什么,對微信進行火絨行為監控。因為小程序最初在PC端運行,必然會相關文件在客戶機上釋放,所以我們主要關注微信的文件讀寫行為。

注意到這里有類似文件釋放的行為,在監控上訪其實同樣有讀取此文件夾的行為,根據經驗這其實就是一個簡單的讀取相關目錄,發現沒有相關程序邏輯文件后,主動請求服務器下載相關文件。

那我們的關注點來到\__APP__.wxapkg,根據前人的經驗,這就是小程序的主要邏輯所在的地方。

其中可以監控到很多關于調用堆棧的信息,不過這些堆棧附近大概是文件釋放相關邏輯,這并不是我們關注的重點。

二
分析文件特征

我們用010Editor打開文件,任意一個二進制編輯器都可以。

可以看到明顯程序邏輯被加密那么我們關注點就來到了這個文件的解密操作。

三
wxapkg解密思路

那么有兩種思路

1.很經典的思路,既然文件被加密,那么微信客戶端裝載此程序的時候必然要進行解密,那么必然要進行打開文件的操作,我們對CreateFile下斷點,應該是可以找到打開文件的操作,記錄打開的句柄,同時微信也需要對文件進行讀取的操作。我們在ReadFile下條件斷點,當傳入的的句柄是我們獲得的小程序文件的句柄時斷下,調用堆棧附近應該就有相關解密的操作,這種操作可行性很大,但是相對比較麻煩,微信打開的文件很多,在CreateFile下斷點可能比較麻煩。

2.如果微信使用的是比較常規的加密 算法,那么可以通過IDA的插件Findcrypt看看有沒有比較明顯的特征。

需要注意到的是,在WeChat中并沒用打開這個wxapkg的相關操作。那么斗膽猜測可能是微信重新啟動一個加載器對wxapkg進行裝載。**那么我們進行火絨劍全局監控,看看有沒對wxapkg進行讀取的操作。

四
重新監控

可以觀察到名為WeChatAppEx.exe對文件做了兩次讀取的操作,根據經驗我們關注第二次讀取,雙擊File_read操作,查看調用棧。

五
解密分析

跟隨堆棧,我們在IDA和X64dbg中分別定位此位置。RVA:0x678353d。

可以看到此位置對文件進行了讀取,我們在dbg中看看有沒有文件的相關數據。附加到WeChatAppEx.exe,在對應位置下斷點,并運行一個小程序。

輕而易舉的斷下,并且我們觀察到文件大小非常接近打開的加密文件大小,并且在堆棧中出現了相關文件信息。

我們步過查看readBuf內的數據。

可以看到WeChatAppEx.exe讀取了加密后的文件。那么我們不難理解WeChatAppEx.exe類似一個加載器,在運行時對文件進行解密裝載。利用程序要對這一片內存進行讀寫的特征,我們對這篇內存區域下硬件訪問斷點(Xdbg的內存斷點是針對內存頁的,可能會斷在奇怪的地方,可能是筆者不太會使用這種特性)。

可以看到在RVA:2784F7A處斷下,比較奇怪的是此處對文件的前8字節賦值為0,不太能理解,索性我們對沒有修改的內存再次下硬件訪問斷點(不要忘記卸載之前的斷點)。

再次斷下時對之后的8個字節賦值為-1,同樣比較難解,我們再次對剩余區域下硬件斷點。

再次在RVA:676C91E處斷下,可以看到這里有對字符串操作的相關指令,根據movsb指令的功能。

指令:                                          
     MOVSB, MOVSW, MOVSD
                        
     描述:
     移動字符串數據,復制由ESI寄存器尋址的內存地址處的數據至EDI尋址的內存地址處。

拓展的,我們分別觀察RSI與RDI指向的內存區域。

尾部部分解密

解密例程分析


可以看到RSI指向的內存中有非完整的加密文件的十六進制形式(前8字節,即V1MMWX?被忽略),有意思的是,復制操作的指針并沒有指向文件頭部,而是指向了距離除去V1MMWX?之后的1024字節之后,這里有分塊加密的特征,但是為什么程序沒有在解密前1024字節處停下呢,可能是筆者疏忽或者程序先解密尾部部分在解密首部,既然已經到這里,我們不妨順藤摸瓜。

可以看到正在向RDI中賦值數據,步過至字符串操作完成,我們再對這片內存區域下硬件訪問斷點。

之后再次在RVA:676C91E出斷下,類似的,字符串操作完之后,我們再次對RDI下硬件訪問斷點。

再次運行之后再RVA:302772F處斷下。

觀察到我們下斷點的內存區域已經出現了../image字樣,這無疑是非常讓人興奮的,可能這就是文件解密的位置。事實確實如此,我們取消硬件斷點,在上一步斷下出的循環中循環幾次簡單分析就可以確定,這確實是一個解密點,而且是非常簡單的異或解密。事實上,這一部分解密過程與微信圖片解密相同。

現在,我們找到了密文的1024字節之后部分的解密例程。我們默認忽略前8字節的處理,讀者可自行分析,裝載器并沒有對前8字節做過多處理,在解密過程中只是簡單的忽略。

找到xorKey

那么我們rbp所對應的秘鑰0x34從何而來呢,追蹤異或Key使我們接下來要做的事情。

有請尊敬的IDA先生,我們在IDA轉到RVA:302772A,即異或解密位置追蹤Key從何而來。

根據分析,a3即是xorKey,至于a3*0x1010101010…目的是用int8類型的值a3填滿rbp至8個字節,進行8個字節分組異或。我們對a3進行簡單的重命名—>xorKey_。

可以看到xorKey作為參數被傳入(為提高辨識度,筆者對此函數進行簡單的重命名)。

查看對此函數的引用,有兩個運行時調用,還有一個直接調用,處于防止跟飛情況,我們對此函數頭下斷來找到調用點。

文件再次斷下,并在堆棧中回溯,我們來到調用點RVA:302759B

在IDA中我們了解到xorKey在異或解密函數ContextDecode中作為第三個參數傳遞,且類型為int8,根據fastcall調用約定,我們關注寄存器R8:000000000014EE34,最后一個字節,即0x34。他是怎么來的呢,我們向上分析。

VA:00007FF632C97589處對r8最后一個字節進行了賦值,我們看[rax+rcx-0x2]是什么。

可以看到rcx指向一個字符串,實際上這是微信小程序的ID,即AppID,進行簡單的分析,rax是AppID的長度,而減去0x2后,[rax+rcx-0x2]指向的是AppID字符串的倒數第二個字符,將字符所對應的ascii碼賦值給r8,這樣,我們xorKey就拿到了。我們在everything找搜索對應AppId:wxf2a0156c0235fc4c

可以證實上面的說法。至此,尾部部分解密告一段落。

首部1024字節解密

解密例程分析

我們再次來到RVA:0x678353d上方的ReadFile處下斷,因為根據之前分析,此處有全部密文出現,同樣我們忽視前8字節,對剩余內存內容下硬件訪問斷點,嘗試尋找前1024字節解密位置。

重新在此處斷下(RVA:676C91E),同之前分析,補過字符串操作之靈后,我們跟隨目的地(RDI)內存區域,對其下硬件訪問斷點。

再次在此處斷下(RVA:676C91E)斷下,重復上述步驟,在目的地地址下斷。

運行之后再RVA:40DFE處斷下。

這里就有比較令人興奮的字段:the iv:16bytes,部分加密需要一個向量,我們不妨猜測,這里就是加密函數,我們在IDA中來到對應位置。

之前筆者已經對一些變量進行分析并且重命名,所以看起來似乎一目了然,這些變量的命名,我們之后逐步分析但不是現在的關注的重點。

引起我們注意的是類似的匯編指令aesdeclast xmm2, xmm1,注意到字樣“aes”就可以懷疑密文首部采用的是aes加密,事實上確實采用的是這用加密,從學習的角度,我們假設并不知情相關特征。

既然到了這一步,不妨運行看看相關內存區域有沒有明文信息。

運行若干步之后,我們在RSI所指內存區域中發現明文特征,這與之前分析尾部解密中得出的明文十分類似,至此可以確定,這一部分邏輯即是對前1024字節進行解密的邏輯。

那么我們下一步要解決的問題是:”這是什么加密“,以便我們能找出秘鑰,自行寫出解密腳本。

我們百度aesdeclast xmm2, xmm1,看看能不能收獲一些有用的信息。

下面是來自于互聯網的一些資料:

AESDECLAST — Perform Last Round of an AES Decryption Flow

Description?

This instruction performs the last round of the AES decryption flow using the Equivalent Inverse Cipher, with the round key from the second source operand, operating on a 128-bit data (state) from the first source operand,and store the result in the destination operand.

128-bit Legacy SSE version: The first source operand and the destination operand are the same and must be an XMM register. The second source operand can be an XMM register or a 128-bit memory location. Bits (MAXVL-1:128) of the corresponding YMM destination register remain unchanged.

VEX.128 encoded version: The first source operand and the destination operand are XMM registers. The second source operand can be an XMM register or a 128-bit memory location. Bits (MAXVL-1:128) of the destination YMM register are zeroed.

請注意描述中的加粗部分,其大概意思是aesdeclast xmm2, xmm1執行的是反向解密的最后一輪解密過程,xmm1是round Key(拓展秘鑰,aes將用戶設置的秘鑰進行拓展以便于運算),而xmm2即是最后一輪解密的數據。

現在我們可以確定這一部分加密使用的是AES加密,我們正在分析的是其對應的解密部分。根據AES加密的對應的解密過程,最后一輪解密使用的round Key正是用戶設定的秘鑰,關于AES使用類似指令的介紹以及加解密的細節問題,筆者收集到一篇優質文章:

Intel AES-NI使用入門 - 被遺忘的海灘 | Nagi's Blog (x-nagi.com)(https://x-nagi.com/post/aesni.html)

AES_Key生成例程分析

我們再次來到RVA:40DFE處

結合引用文章的介紹,我們大概可以得出:

那么我們接下來的關注點放到了拓展秘鑰緩沖區,我們跟隨秘鑰緩沖區的生成會進入到秘鑰拓展例程,在那里,我們大概率可以拿到Key,我們在IDA中追蹤秘鑰緩沖區。

v33對應的是Rcx,而拓展秘鑰緩沖區即keyArry來自于函數外。我們對此函數頭下斷進行棧回溯。

函數頭:

再次斷下后(前幾次斷下并不能得到我們要的調用棧,因為相關參數中找不到密文緩沖區等特征),根據fastcall約定,秘鑰應該是r9所指緩沖區,實際上,秘鑰緩沖區最后十六個字節作為原始Key的一部分(16字節),即未拓展的Key,秘鑰拓展例程通過原始Key進行秘鑰拓展,不過不注意這個細節也沒有關系,這在之后的分析中將會體現。

我們回溯到RVA:2811EB5

在此函數中,keyArry已經生成,那么我們繼續棧回溯,來到VA:00007FF6444C1137

[rcx+0x10]即是keyArry,同樣分析此函數,發現keyArry同樣作為參數傳入此函數,那么我們繼續進行調用棧回溯。

回退到第二次調用棧,我們來到VA:00007FF6444C1398,如下圖:

同樣的,[rcx+0x10]即是keyArry,同樣是作為參數傳遞進來的,再次進行堆棧回溯,來到RVA:00000000027F15AB,如下圖,這里再次進行了簡單的轉發,再次進行棧回溯。

來到RVA:285B20C,如下圖:

我們所說的keyArry是aes實例化的一個對象,里面存儲有拓展秘鑰。再此函數進行簡單分析后發現,key同樣來自函數外通過參數傳遞進來。

繼續堆棧回溯-_-||,來到RVA:000000000285AE26

繼續回溯,來到RVA:000000000285AED8,同樣是一個簡單的轉發,繼續回溯,來到RVA:0000000003026BF2

如下圖:

如圖,[[v18]+0x8]指向秘鑰,終于要計算秘鑰了-_-||,本函數上方有對v18的相關操作,如下圖:

其實看到‘salt’這幾個字符,對秘鑰拓展熟悉的朋友應該能馬上反應過來這里應該就是拓展秘鑰的地方了。我們進到函數里。

如圖:

這就非常明確了,我們百度Pbkdf2

PBKDF的全稱是Password-Based Key Derivation Function,簡單的說,PBKDF就是一個密碼衍生的工具。既然有PBKDF2那么就肯定有PBKDF1,那么他們兩個的區別是什么呢?PBKDF2是PKCS系列的標準之一,具體來說他是PKCS#5的2.0版本,同樣被作為RFC 2898發布。它是PBKDF1的替代品,為什么會替代PBKDF1呢?那是因為PBKDF1只能生成160bits長度的key,在計算機性能快速發展的今天,已經不能夠滿足我們的加密需要了。所以被PBKDF2替換了。在2017年發布的RFC 8018(PKCS #5 v2.1)中,是建議是用PBKDF2作為密碼hashing的標準。PBKDF2和PBKDF1主要是用來防止密碼暴力破解的,所以在設計中加入了對算力的自動調整,從而抵御暴力破解的可能性。
PBKDF2的工作流程
PBKDF2實際上就是將偽散列函數PRF(pseudorandom function)應用到輸入的密碼、salt中,生成一個散列值,然后將這個散列值作為一個加密key,應用到后續的加密過程中,以此類推,將這個過程重復很多次,從而增加了密碼破解的難度,這個過程也被稱為是密碼加強。

稍微閱讀以上引用內容 ,對比此函數參數不難得出:

1.鹽值:saltiest

2.秘鑰:小程序ID

3.秘鑰拓展算法:PBKDF2

4.迭代次數:1000

5.秘鑰長度:256位

既然秘鑰長度是256位,那么可以推測出加密算法是AES_256。

注意到的是,它的偽散列算法是可以替換的,那么我們下一步要找出的是它使用的是哪種散列算法。

這里筆者簡單使用js選幾種常見的散列算法試一試。

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 èYMéμj.= 
00001D5A002CD190     71122C7B6FFE09D6 ?.to{,.q 
00001D5A002CD198     C3981F7A8828924E N.(.z..? 
00001D5A002CD1A0     BD14C8D6E69FEA98 .ê.??è.? 
00001D5A002CD1A8     3336977C3609F094 .e.6|.63 
00001D5A002CD1B0     1969D6DC01305252 RR0.ü?i.


至此,我們找到了秘鑰的生成算法。

分組模式以及iv


分組模式以及iv的尋找相對簡單,只要對AES加密流程以及幾種加密模式的區別熟悉就可以在加密函數(RVA:0000000000040C30)中分析出加密模式以及向量。這里筆者不再贅述。

經過簡單分析,總結之前分析成果,有如下清單:

除去文件頭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;
}

WeChatAppEx_2.0.6609.4下載:

鏈接:https://pan.baidu.com/s/1N1f6DwOiIoElt9m1aN4uSw?pwd=vp03

提取碼:vp03