記一次逆向分析解密還原Class檔案

magic_zero發表於2023-12-05

前言

前陣子我的一位朋友發來一份程式碼讓我幫忙看看。具體就是所有的jsp檔案內容和大小都一樣,漏洞挖掘無從下手。經過分析發現所有的Class都使用了自定義的加密工具加密,經過逆向分析,順利解密,因而有了此文。

初步分析

檔案內容如下所示:

其他檔案亦如是:

接著在tomcat work目錄找到了編譯後的class檔案:

但是沒辦法直接反編譯,檢視頭資訊發現都一樣:

因此猜測一種可能性是Java層面實現的類載入器,類載入的時候進行動態解密操作。於是自己寫了一個jsp檔案上傳到目標環境,首先訪問一下這個jsp檔案,讓JVM載入至記憶體中。然後我們呼叫Class.forName再去載入該類,獲取到該類的java.lang.Class物件例項,然後呼叫getClassLoader獲取該類的載入器:

<%
try {
    out.println(Class.forName("org.apache.jsp.test_html").getClassLoader());
} catch(Throwable th) {
    th.printStackTrace(out);
}
%>

結果獲取到的內容為tomcat實現的WebAppClassLoader,回溯父類載入器也沒有發現自定義的實現。於是計劃取巧,使用Arthas之類的工具attach到目標的JVM,去記憶體dump載入過的Class。然後發現無法attach,估計是目標JVM版本過低,為JDK 1.5。因而繼續取巧,用動態除錯除錯,在ClassLoader#defineClass方法下斷點爭取將byte[]直接dump出來,可是也沒有成功。

峰迴路轉

過了幾天,後來回頭重新去做分析。在tomcat啟動指令碼中發現瞭如下的引數:

嗯,果然是自定義載入器,透過實現JVMTI介面,並未在應用層使用Java程式碼去實現,而是直接用C++實現介面。具體的實現就在這個dll中:

經過對java agent的簡單學習,瞭解相關引數和實現後,在ida中將相關的結構體還原始碼如上圖所示。這裡有個ida使用技巧,分析C/C++程式碼最重要就是要了解關鍵的結構體功能,這相當於瞭解Java中類的定義和相關方法的含義。而JVMTI的SDK在JDK的安裝目錄中是開源的,我們可以用ida匯入本地結構體的功能批次匯入。匯入的時候根據header檔案的載入順序依次複製到一個檔案中,否則會有很多依賴缺失導致的報錯。最終的標頭檔案結構如下:

jni_md.h -> jni.h -> jvmti.h

然後需要將前邊的include指令匯入操作刪除掉,匯入ida:

順利的話,將會提示如上圖所示:

相關的錯誤資訊也會在message視窗輸出。可以用來定位錯誤。

接著開始我們的逆向之旅,經過一些分析之後還原出來的虛擬碼如圖所示:

在第22行,這裡一定要把資料的顯示格式修改為hex,如上圖,我們可以看到這裡判斷了Class檔案的魔術頭,因而猜測這裡就是解密的操作了,我們跟進解密函式的具體實現(第29行 decryptClassBytes函式):

程式碼的實現很簡單,根據隨機數種子設定srand函式。然後迴圈呼叫rand()函式去和讀取到的byte進行異或操作。最終返回異或結果。這裡一度讓我十分困惑,因為根據我對隨機數的認識,至少這裡應該會把隨機數也存放在某個地方,這樣將來解密才能正確執行。因此我自己寫了一些程式碼來觀察隨機數的生成:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	srand(0x96F07);
	int i = rand();
	printf("rand number = %x\n", i);
	
	int c = rand();
	printf("rand number = %d\n", c);
	
	int n = rand();
	printf("rand number = %d\n", n);
	
	int k = rand();
	printf("rand number = %d\n", k);
   
   return 0;
}

找了好幾個線上執行程式碼的站點,發現最終生成的結果居然是完全一樣的!查詢該函式之後發現了這樣的一句話:

初始化隨機種子,會提供一個種子,這個種子會對應一個隨機數,如果使用相同的種子後面的 rand() 函式會出現一樣的隨機數。

結合前陣子JumpServer出現過的隨機數問題,讓我再次認識到這個問題的居然是這種方式!

因此我們的解密操作就很簡單了:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

int main() {
    const int BUFFER_SIZE = 1;
    srand(0x96F07);
    char *src_file = "D:\\cms\\tomcat\\work\\Catalina\\localhost\\oa\\org\\apache\\jsp\\test_html.class";
    char *dst_file = "C:\\Users\\Administrator\\CLionProjects\\decrypt\\decrypt.class";

    FILE *p_src = fopen(src_file, "rb");
    if (p_src == NULL) {
        printf("src_file open failed");
        return 0;
    }
    FILE *p_dst = fopen(dst_file, "wb");
    if (p_dst == NULL) {
        printf("dst_file open failed");
        return 0;
    }
    // 判斷檔案大小 , 該結構體接收檔案大小結果
    struct stat st = {0};
    stat(src_file, &st);
    // 計算緩衝區檔案大小
    int buffer_size = st.st_size;
    if ( buffer_size > BUFFER_SIZE ) {
        buffer_size = BUFFER_SIZE;
    }
    char *buffer = malloc(buffer_size);
    char output[1];

    while ( !feof(p_src) ) {
        int res = fread(buffer, 1, buffer_size, p_src);
        *output = *buffer ^ rand();
        fwrite(output, 1, res, p_dst);
    }
    // 釋放緩衝區記憶體
    free(buffer);
    fclose(p_src);
    fclose(p_dst);
    printf("Copy Success");
    return 0;
}

以上程式碼並沒有成功,後來我用這個程式碼加密一個未加密過的class檔案,發現和目標檔案差了1個位元組。也就是說解密操作是越過第一個位元組開始的,在如上程式碼基礎上,越過第一個位元組即可:

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>

int main() {
    const int BUFFER_SIZE = 1;
    srand(0x96F07);
    char *src_file = "D:\\cms\\tomcat\\work\\Catalina\\localhost\\oa\\org\\apache\\jsp\\test_html.class";
    char *dst_file = "C:\\Users\\Administrator\\CLionProjects\\decrypt\\decrypt.class";

    FILE *p_src = fopen(src_file, "rb");
    if (p_src == NULL) {
        printf("src_file open failed");
        return 0;
    }
    FILE *p_dst = fopen(dst_file, "wb");
    if (p_dst == NULL) {
        printf("dst_file open failed");
        return 0;
    }
    // 判斷檔案大小 , 該結構體接收檔案大小結果
    struct stat st = {0};
    stat(src_file, &st);
    // 計算緩衝區檔案大小
    int buffer_size = st.st_size;
    if ( buffer_size > BUFFER_SIZE ) {
        buffer_size = BUFFER_SIZE;
    }
    char *buffer = malloc(buffer_size);
    char output[1];

    // 跳過最初的1位元組
    if (fseek(p_src, 1, SEEK_SET) != 0) {
        printf("fseek error!\n");
        fclose(p_src);
        return 1;
    }

    while ( !feof(p_src) ) {
        int res = fread(buffer, 1, buffer_size, p_src);
        *output = *buffer ^ rand();
        fwrite(output, 1, res, p_dst);
    }
    // 釋放緩衝區記憶體
    free(buffer);
    fclose(p_src);
    fclose(p_dst);
    printf("Copy Success");
    return 0;
}

解密: