记一次逆向分析解密还原Class文件

前言

前阵子我的一位朋友发来一份代码让我帮忙看看。具体就是所有的jsp文件内容和大小都一样,漏洞挖掘无从下手。经过分析发现所有的Class都使用了自定义的加密工具加密,经过逆向分析,顺利解密,因而有了此文。

初步分析

文件内容如下所示:

其他文件亦如是:

接着在tomcat work目录找到了编译后的class文件:

但是没办法直接反编译,查看头信息发现都一样:

因此猜测一种可能性是Java层面实现的类加载器,类加载的时候进行动态解密操作。于是自己写了一个jsp文件上传到目标环境,首先访问一下这个jsp文件,让JVM加载至内存中。然后我们调用Class.forName再去加载该类,获取到该类的java.lang.Class对象实例,然后调用getClassLoader获取该类的加载器:

java 复制代码
<%
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进行异或操作。最终返回异或结果。这里一度让我十分困惑,因为根据我对随机数的认识,至少这里应该会把随机数也存放在某个地方,这样将来解密才能正确执行。因此我自己写了一些代码来观察随机数的生成:

C 复制代码
#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出现过的随机数问题,让我再次认识到这个问题的居然是这种方式!

因此我们的解密操作就很简单了:

C 复制代码
#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个字节。也就是说解密操作是越过第一个字节开始的,在如上代码基础上,越过第一个字节即可:

C 复制代码
#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;
}

解密:

相关推荐
代码雕刻家3 分钟前
编程语法细节
java·c语言·开发语言
SimonKing6 分钟前
Agnes AI出桌面版了,可图可视频,免费用
java·后端·程序员
左左右右左右摇晃25 分钟前
手写Tomcat原理整理
java·开发语言·笔记·tomcat
孫治AllenSun29 分钟前
【JVM】四大引用类型分析
java·开发语言·jvm
于慨30 分钟前
安装java
java
夕除32 分钟前
sign 是什么
java·前端
Penina10181 小时前
代码如何导入idea
java·intellij idea
.Hypocritical.1 小时前
Maven笔记——2
java·笔记·maven
huohuopro1 小时前
手写 Tomcat 步骤教程
java·网络
snow@li1 小时前
Java:Lombok 完整讲解
java