记一次逆向分析解密还原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;
}

解密:

相关推荐
阿伟*rui1 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel
XiaoLeisj3 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
paopaokaka_luck3 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
dayouziei3 小时前
java的类加载机制的学习
java·学习
Yaml45 小时前
Spring Boot 与 Vue 共筑二手书籍交易卓越平台
java·spring boot·后端·mysql·spring·vue·二手书籍
小小小妮子~5 小时前
Spring Boot详解:从入门到精通
java·spring boot·后端
hong1616885 小时前
Spring Boot中实现多数据源连接和切换的方案
java·spring boot·后端
aloha_7896 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
记录成长java6 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
睡觉谁叫~~~6 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust