某相安全平台加密,除Main类外,其他class文件均被加密,熵值非常高

1696662684475-29457599-b862-4054-a8fd-1b906f7dfba1.png

除jar外,还有一个.so文件。在启动时,需要指定-agentpath:./.so命令行参数。由于未确定其加固方案(比如xjar),于是手动逆向其流程。

基本启动流程

jvm在指定-agentpath:./.so后,会先加载此so文件,然后运行此入口函数:

1
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm,char *options,void *reserved)

参考:Java Agent机制 · 攻击Java Web应用-[Java Web安全]
容易推测,jvm会先通过此入口函数进行初始化,包括实现一个Loader,其在运行时会对Class文件进行解密。

so脱壳

拖入IDAPro对so文件进行分析,从导出表进入查看其代码,发现被加密,其他函数也是加密状态:

1696601688515-70f3972f-4de1-4261-85a4-a2a9ac06b428.png

很明显so本身也加壳,并且未能确定so壳类型。由于so在加载时,操作系统会调用其_init_proc函数。于是查看此函数,发现未被加密:

1696665874923-211a4e0f-d2e1-46d3-ab9c-4cd6c3eb5232.png

可推测此函数进行了so的解密流程。于是手动编写加载程序test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <dlfcn.h>

int main(int argc, char* argv[]) {
void * handle;
char * error;

handle = dlopen(argv[1], RTLD_NOW);
if (handle == NULL) {
printf("Open library *s error: *s\n", argv[1], dlerror());
return -1;
}
printf("Open success! %x %s\n", handle, argv[1]);
while (1) {}

return 0;
}

编译后通过ida进行远程调试,待so被加载后查看其内存段:

1696671163044-77a5ea40-9f25-45f4-a1c7-76b2091ae858.png

进入可执行段,修复ida的函数声明。其中解密后的函数相对段基址的偏移不变,因此可以对照原so对虚拟基址的偏移确定解密后段中函数的位置。
由于dump(参考Android so层怎么去脱壳)后需要修复虚拟地址等,于是直接在此进程中进行分析。为方便分析应使用ctrl+F9导入jni.h头文件(参考ida导入jni.h

so分析

分析Agent_OnLoad函数:

1696667154559-0aa98bf9-92ce-4b19-b369-4a0ff5bcad6e.png

通过分析发现so会先注册两个Native函数:

1696667440714-8bc9c79a-6d25-481c-b41e-02c62787bf63.png

其中参数class_objAgent_OnLoad通过使用MethodID获取后传递。后面进行分析发现就是org.springframework.asm.ClassReader

然后会使用RegisterNatives进行注册:

1
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

1696668326422-903ebc9d-912c-4f44-92d6-a804dc3fd603.png

其中JNINativeMethod结构体在jni.h中有声明,需要在IDA中Structures窗口中添加:

image.png

查看两个结构体地址,发现一个是isCipher函数,一个是decrypt函数

1696671311028-8cf7a93a-02b4-49d5-9ccd-f3f3a7b95fc7.png

查看方法签名发现接受一个字节数组作为参数,并返回一个布尔值。后者则接受一个字节数组,返回一个字节数组。

进一步分析isCipher函数:

1696668661789-406f2e92-ba18-4db2-96e1-f2a51ca837aa.png

发现会检查输入的字节数组前四个字节是否为0BAh, 0BEh, 0CAh, 0FEh,可以发现加密的class文件头就是如此:

1696668757861-6db9c9ac-2c52-4c14-9788-db2e1ddc3a24.png

另外正常的class文件头是CAFEBABE。由此可知此函数用于判断是否为一个加密的class文件
再分析decrypt函数。发现解密时会使用一个全局16长度字节数组加密class生成aes keyvi

1696669745580-6f918e19-6e3a-4b7a-a054-37504aec17aa.png

通过交叉引用发现global_bytes_16Agent_OnLoad结尾处进行了初始化,通过一个初始keyvi对一个长度0xA150的密文进行解密并转换提取16个字节生成。

1696669838583-0098985d-1aa0-4955-8d87-91c406ddba5a.png

1696671370919-6864f6d7-a80d-46f3-9026-73c676634574.png

使用AES CBC NoPadding对密文进行解密,发现就是ClassReader.class

1696671411422-e6e5be7b-dce5-4a4a-b929-2d522f344e91.png

1696670045911-6e231b0a-cffb-4db1-a7cf-54ac99c0f82f.png

回到正常class解密流程,class密文前21字节global_bytes_16生成aes keyvi,再解密21偏移后的内容即可解密class文件。

此文章仅作技术交流,严禁利用文中技术进行非法行为,否则后果自负!

⬆︎TOP