专栏名称: 黑白之道
黑白之道,普及网络安全知识!
目录
相关文章推荐
软件室  ·  阿里1400个福利码,速撸! ·  23 小时前  
风清扬大侠  ·  一图文秒懂延迟退休、退休利好概念股小表哥 ·  5 天前  
风清扬大侠  ·  一图文秒懂延迟退休、退休利好概念股小表哥 ·  5 天前  
一亩三分地Warald  ·  逆天wlb!还能远程办公的公司有哪些? ·  6 天前  
一亩三分地Warald  ·  逆天wlb!还能远程办公的公司有哪些? ·  6 天前  
安天集团  ·  活跃的RansomHub勒索攻击组织情况分析 ·  1 周前  
能源电力说  ·  因锂电池爆炸,阿里云机房火灾持续超30小时 ·  1 周前  
能源电力说  ·  因锂电池爆炸,阿里云机房火灾持续超30小时 ·  1 周前  
51好读  ›  专栏  ›  黑白之道

记一次实战中解密JVMTI加密过的jar包

黑白之道  · 公众号  · 互联网安全  · 2024-09-16 08:48

正文


原文首发在:先知社区

https://xz.aliyun.com/t/15423/3648

在审一套Java系统的时候,发现其核心代码都被加密了看不到,这篇文章来介绍总结一下解密jar包的思路。

分析

初步分析系统,发现此系统的 web 源码都放在 service\xxxsecurity 目录下。反编译 jar 包,同时发现这里所有 service 结尾的 jar 包还有其它部分的 jar 包都被加密了,反编译不出来源码。这里后面想办法解决。

此外,此系统的运行机制是在安装时将要执行的程序利用 service 目录下的 nssm.exe ( nssm工具 ) 注册为系统服务,每次开机就会自动以 system 的权限启动服务。

这里刚开始还不知道这个系统到底是怎么启动的,我们可以用这个工具来查询服务其对应的程序和参数。

可以得到 Core Service ( 此系统的 web 服务)对应的运行程序和参数如下:

korat.exe -java=../../jre8/jre/bin/java.exe -params=eyJwb3J0X3R5cGUiOiJhZG1zIiwicG9ydCI6IjgwOTgiLCJzZXJ2ZXJfc3NsX2VuYWJsZSI6ImZhbHNlIiwicHdkX2VuY3J5cHQiOiIxIiwiYWRtc19wb3J0IjoiODA4OCIsInJlZGlzX2hvc3QiOiIxMjcuMC4wLjEiLCJyZWRpc19wb3J0IjoiNjM5MCIsInJlZGlzX3B3ZCI6IklRUmU5R0RYNFJodTFPemhIQkwxdEE9PSIsImRiX3R5cGUiOiJwb3N0Z3JlIiwic3lzdGVtX2xhbmd1YWdlIjoiemhfQ04iLCJkYl9uYW1lIjoiYmlvc2VjdXJpdHktYm9vdCIsImRiX3VzZXJuYW1lIjoicm9vdCIsImRiX3B3ZCI6IjZDU2RUUmtKYXArV0N2Mi9jbC9pWnc9PSIsImRiX2hvc3QiOiIxMjcuMC4wLjEiLCJkYl9wb3J0IjoiNTQ0MiIsImluc3RhbGxfcGF0aCI6IiIsImJhY2t1cF9wYXRoIjoiRjpcXHRlc3QiLCJpbnN0YWxsX2RhdGUiOiJXdGVtOExIYm1ZV0hhQjlDaEw5TlRnPT0iLCJtb2R1bGVfZXhjbHVkZSI6IiJ9 -arch=64 -xms= -xmx= -xxm= -xxp=

这里参数经过了 base64 编码,解码后如下,发现应该是一些系统的启动参数,猜测这个系统的启动原理就是将 java 程序的启动封装到 korat.exe 中,然后将这个 exe 注册为系统服务。

{"port_type":"adms","port":"8098","server_ssl_enable":"false","pwd_encrypt":"1","adms_port":"8088","redis_host":"127.0.0.1","redis_port":"6390","redis_pwd":"IQRe9GDX4Rhu1OzhHBL1tA==","db_type":"postgre","system_language":"zh_CN","db_name":"biosecurity-boot","db_username":"root","db_pwd":"6CSdTRkJap+WCv2/cl/iZw==","db_host":"127.0.0.1","db_port":"5442","install_path":"","backup_path":"F:\\test","install_date":"Wtem8LHbmYWHaB9ChL9NTg==","module_exclude":""}

由于这个系统是以 system 权限启动的服务,因此后续我们操作这个服务的时候可能会因为权限问题而不方便,因此这里建议关闭服务,然后自己根据上面获取到的启动程序和参数来自己启动。

然后我们就会发现任务管理器中多了一个 java.exe 的程序。现在的关键就是怎么获取到 java 程序的启动参数。这里介绍下面几种方法:

查询java程序启动参数

使用WMI工具

# 使用管理员cmd打开,运行下面的目命令即可获取system权限的cmd,如果上面已经自己启动了korat.exe没有使用服务自启的exe,就没有权限限制了,也就不需要这一步了。不然会因为权限问题看不到system用户启动的程序的启动参数
PsExec -i -s -d cmd
# 查询java.exe的启动参数
wmic process get caption,commandline /value | findStr java.exe

使用jdk自带的工具

这里使用 jvisualvm 工具就可以看到参数。

修改jar包的逻辑(不一定可用)

不难发现这个系统的启动程序写在 xxx-startup.jar 中,我们可以修改这个包的 main 方法所在的 Class ,然后替换原 jar 包中对应的 Class ,让其运行时添加我们注入的代码,打印出其启动时的参数。

FileOutputStream fos = new FileOutputStream("test.log");
// 获取JVM启动参数
RuntimeMXBean bean = ManagementFactory.getRuntimeMXBean();
List<String> aList = bean.getInputArguments();

for(int i = 0; i < aList.size(); ++i) {
fos.write(((String)aList.get(i)).getBytes());
fos.write("\n".getBytes());
}
// 获取main方法参数
String[] var8 = args;
int var5 = args.length;
for(int var6 = 0; var6 < var5; ++var6) {
String arg = var8[var6];
fos.write(arg.getBytes());
fos.write("\n".getBytes());
}

注意这里这个系统其实是会对 jar 包的一致性做校验的,因此其实修改 jar 包不应该行的通,这个系统启动 jar 包前,会检测 jar 包是否被修改,修改过的话就不会启动。但是这里我发现在使用 Windows 服务启动这个系统的时候(也就是原生系统运行的方式,不是我们前面手动运行 korat.exe ),如果我们在服务运行的时候强制在任务管理器中关闭 java.exe ,服务就会自动重启程序,就可以绕过这个对 jar 包的检测,来成功注入命令。不过同时发现这里只适用一次,不能关闭两次 java.exe ,不然服务就会强制停止,除非重启服务了以后再次强制停止 java.exe 一次。我猜测是因为对 jar 包检验的逻辑是写在 korat.exe 中的(的确后面逆向 korat.exe发现其中确实检验了),然后我们强制停止 korat.exe 中启动的 java.exe 不会重新运行 korat.exe 来启动重新,从而再次触发对 jar 包的检验,而是直接运行 jar 包来恢复服务,从而可以成功注入代码。(只是猜测)

启动参数

最后获取到的启动参数如下:

D:/xxx/jre8/jre/bin/java.exe -Dspring.profiles.active=pro -Djava.library.path=lib/dll/64 -Djna.library.path=lib/dll/64 -Dloader.path=lib/jar/ -XX:+DisableAttachMechanism -Dcom.ibm.tools.attach.enable=no -agentpath:libdonskoy.dll=72a2800aeb36cc98cc35bd7074e49193 -Dspring.datasource.url=jdbc:postgresql://127.0.0.1:5442/biosecurity-boot -Dspring.datasource.username=root -Dspring.datasource.password=ZKTeco##123 -Dspring.datasource.driver-class-name=org.postgresql.Driver -Dspring.jpa.properties.hibernate.dialect=com.xxx.xxx.core.config.PostgreDialect -Dspring.redis.host=127.0.0.1 -Dspring.redis.port=6390 -Dspring.redis.password=xxx -Dadms.netty.https=adms -Dserver.port=8098 -Dsystem.language=zh_CN -Dsecurity.require-ssl=false -Dadms.push.port=8088 -Dsystem.installDate=Wtem8LHbmYWHaB9ChL9NTg== -Xms1024m -Xmx2048m -XX:MetaspaceSize=256m -Dorg.apache.catalina.connector.RECYCLE_FACADES=true -jar xxx-startup.jar

解决jar包被加密的问题和jar包无法启动的问题

经过分析可以发现这里 jar 包是使用了 JVMTI 来加密 jar 包,通过 -agentpath 参数来在 dll 中解密 jar 包。下面介绍几种解密的思路:

逆向agentpath参数的dll

方法入口

既然解密逻辑写在 libdonskoy.dll 中,那我们可以直接用 ida 分析这个 dll 来获取解密逻辑。

根据JVMTI加密jar包的基础知识 ,可以知道关键逻辑写在 Agent_OnLoad 方法中,直接先定位到这个方法。

经过分析,这里的关键逻辑在 JvmTIAgentL::ParseOptions() 和 JvmTIAgent::registerEvent() 方法中。

绕过options参数的检验

ParseOptions() 方法的作用是检验 -agentpath 参数后面的值( str )是否合法,不合法就终止程序不让运行。本来以为在前面获取启动参数的时候获取到了这个值就可以直接用,但是发现这个值居然是动态的,并且前一个能用的值之后就用不了了,一个静态的程序能有动态的参数就说明这个参数大概率是跟时间有关的。

分析密钥的生成逻辑

不难得出这里的检验逻辑很简单,简单来说就是检验这个参数和当前时间是否匹配,匹配才让运行,可以通过当前时间直接算出这个需要传入的参数。这里 t=time(0) 获取当前时间戳,拼接到 now 和 before 字符串中。然后把 now 和 before 都经过 md5 编码(这里 0x42 就是这两个字符串的长度),再 hex 编码。如果 JvmTIAgent::m_options 等于两者其中之一就通过检验。

我们可以使用下面的脚本来计算出未来某个时刻运行时需要的参数值,然后去掐点运行程序,就可以绕过这个无法启动 jar 包的限制了。

注意 :由于加密函数一般都不会自己实现,这里可以根据加密函数的特征来发现这个 dll 使用的是什么库来加密的。知道什么库来加密可以方便我们后面写脚本,免得要是库不一样,参数传入的格式不一样,需要处理参数,这样就麻烦很多。使用的 AES 库: https://github.com/kokke/tiny-AES-c使用的 MD5 库: https://github.com/pod32g/MD5

#include 
#include
#include
#include "md5.c"
#include "aes.c"
#include "time.h"

char *__cdecl ascii2hex(char *chs, int len) {
char hex[16]; // [rsp+20h] [rbp-30h] BYREF
int b; // [rsp+3Ch] [rbp-14h]
char *ascii; // [rsp+40h] [rbp-10h]
int i; // [rsp+4Ch] [rbp-4h]

memcpy(hex, "0123456789abcdef", sizeof(hex));
ascii = (char *) calloc(3 * len + 1, 1);
for (i = 0; i < len; ++i) {
b = (unsigned __int8) chs[i];
ascii[2 * i] = hex[b >> 4];
ascii[2 * i + 1] = hex[b % 16];
}
return ascii;
}

int main() {
time_t t = time(0);
char now[66];
unsigned char res[16];
// 获取15s后对应的参数值
sprintf(now, "[email protected]%ldtotoroisthemosthandsomemanintheworld", t + 15);
md5((const uint8_t *)now, 0x42, res);
printf_s("%s\n", now);
printf("%s", ascii2hex(res, 16));
return 0;
}

修改dll绕过

使用上面的方法需要我们每次启动的时候都跑一次脚本,然后去运行,比较麻烦,我们可以直接修改 dll 的逻辑来直接一劳永逸。

我们直接把这里 strcmp 的逻辑改了就行,把 if(exp) 改为 if(!exp) 。

也就是把这里的 jnz 改为 jz 即可。

获取解密逻辑

接着看怎么逆向得出解密逻辑,这里的解密逻辑通过 JvmTIAgent::RegisterEvent 方法来注册 hook  JVM加载类的方法( HandleClassFileLoadHook )。

  • HandleClassFileLoadHook

void __cdecl JvmTIAgent::HandleClassFileLoadHook(
jvmtiEnv *jvmti_env,
JNIEnv *jni_env,
jclass class_being_redefined,
jobject loader,
const char *name,
jobject protection_domain,
jint class_data_len,
const unsigned __int8 *class_data,
jint *new_class_data_len,
unsigned __int8 **new_class_data)
{
std::ostream *v10; // rcx
std::ostream *v11; // rax
std::ostream *v12; // rax
__int64 v13; // rax
std::ostream *v14; // rax
AgentException *exception; // rbx
std::ostream *v16; // rcx
std::ostream *v17; // rax
AgentException *v18; // rbx
unsigned __int8 *v19; // rax
size_t v20; // rcx
std::ostream *v21; // rax
AES_ctx ctx; // [rsp+20h] [rbp-60h] BYREF
unsigned __int8 tempIv[16]; // [rsp+E0h] [rbp+60h] BYREF
unsigned __int8 tempKey[16]; // [rsp+F0h] [rbp+70h] BYREF
unsigned __int8 *pNewClass_1; // [rsp+100h] [rbp+80h]
unsigned __int8 *pNewClass_0; // [rsp+108h] [rbp+88h]
jvmtiError error; // [rsp+114h] [rbp+94h]
uint8_t *data; // [rsp+118h] [rbp+98h]
size_t ivLen; // [rsp+120h] [rbp+A0h]
size_t keyLen; // [rsp+128h] [rbp+A8h]
char type; // [rsp+137h] [rbp+B7h]
int length; // [rsp+138h] [rbp+B8h]
char padding; // [rsp+13Fh] [rbp+BFh]
size_t data_len; // [rsp+140h] [rbp+C0h]
int index_0; // [rsp+14Ch] [rbp+CCh]
unsigned __int8 *pNewClass; // [rsp+150h] [rbp+D0h]
int index; // [rsp+15Ch] [rbp+DCh]

if ( name )
{
if ( isEncrypt(class_data) )
{
data_len = class_data_len - 2;
padding = class_data[data_len];
length = hexCharToInt(padding) + 1;
type = class_data[data_len + 1];
switch ( type )
{
case '1':
keyLen = strlen((const char *)g_fish);
ivLen = strlen((const char *)g_lion);
md5(g_fish, keyLen, tempKey);
md5(g_lion, ivLen, tempIv);
g_key = tempKey;
g_iv = tempIv;
break;
case '2':
keyLen = strlen((const char *)g_fly);
ivLen = strlen((const char *)g_bee);
md5(g_fly, keyLen, tempKey);
md5(g_bee, ivLen, tempIv);
g_key = tempKey;
g_iv = tempIv;
break;
case '0':
keyLen = strlen((const char *)g_cat);
ivLen = strlen((const char *)g_dog);
md5(g_cat, keyLen, tempKey);
md5(g_dog, ivLen, tempIv);
g_key = tempKey;
g_iv = tempIv;
break;
default:
v10 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "[donskoy] decrypt: ");
v11 = (std::ostream *)std::operator<<<std::char_traits<char>>(v10, (char *)name);
v12 = (std::ostream *)std::operator<<<std::char_traits<char>>(v11, "error!");
refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(v12);
v13 = std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "[donskoy] Error: unknown encrypt type: ");
v14 = (std::ostream *)std::operator<<<std::char_traits<char>>(v13, (unsigned int)type);
refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(v14);
exception = (AgentException *)_cxa_allocate_exception(4ui64);
AgentException::AgentException(exception, JVMTI_ERROR_INTERNAL);
_cxa_throw(exception, (struct type_info *)&`typeinfo for'AgentException, 0i64);
}
data = (uint8_t *)operator new[](data_len);
memset(data, 0, data_len);
for ( index = 0; index < data_len; ++index )
data[index] = class_data[index];
AES_init_ctx_iv((AES_ctx_0 *)&ctx, g_key, g_iv);
AES_CBC_decrypt_buffer((AES_ctx_0 *)&ctx, data, data_len);
if ( isEncrypt(data) )
{
v16 = (std::ostream *)std::operator<<<std::char_traits<char>>(refptr__ZSt4cout, "decrypt failed: ");
v17 = (std::ostream *)std::operator<<<std::char_traits<char>>(v16, (char *)name);
refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(v17);
v18 = (AgentException *)_cxa_allocate_exception(4ui64);
AgentException::AgentException(v18, JVMTI_ERROR_INTERNAL);
_cxa_throw(v18, (struct type_info *)&`typeinfo for'AgentException, 0i64);
}
error = _jvmtiEnv::Allocate(JvmTIAgent::m_jvmti, data_len - length, new_class_data);
JvmTIAgent::CheckException(error);
pNewClass = *new_class_data;
if ( new_class_data_len )
*new_class_data_len = data_len - length;
for ( index_0 = 0; index_0 < data_len - length; ++index_0 )
{
v19 = pNewClass++;
*v19 = data[index_0];
}
}
else
{
v20 = strlen(g_SelfJavaPackageName);
if ( !strncmp(name, g_SelfJavaPackageName, v20) )
{
v21 = (std::ostream *)std::operator<<<std::char_traits<char>>(
refptr__ZSt4cout,
"---------------------------------- using xxx asm -------------------------------------------");
refptr__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_(v21);
error = _jvmtiEnv::Allocate(JvmTIAgent::m_jvmti, 50123i64, new_class_data);
JvmTIAgent::CheckException(error);
pNewClass_0 = *new_class_data;
if ( new_class_data_len )
*new_class_data_len = 50123;
memcpy(pNewClass_0, _data_start__, 0xC3CBui64);
}
else
{
error = _jvmtiEnv::Allocate(JvmTIAgent::m_jvmti, class_data_len, new_class_data);
JvmTIAgent::CheckException(error);
pNewClass_1 = *new_class_data;
if ( new_class_data_len )
*new_class_data_len = class_data_len;
memcpy(pNewClass_1, class_data, class_data_len);
}
}
}
}

这里解密的过程是先取 class 字节码的前 data_len = class_data_len - 2 部分的字节为 data ,倒数第二个字符转为数字再加一作为 length ,最后一个字符作为 type 。根据后面的 switch 语句,可以发现这个 type 的作用是确定 AES 的 key 和 iv 。

然后对 data 进行 AES 解密,将解密得到的结果的前 data.length() - length 作为最后的字节码。

逆向不难推出其加密时的大致逻辑就是原 class 的字节码填充 length 长度的任意字节使之长度为 16 的倍数,满足 AES 加密的要求,然后随机三种 key 和 iv 进行加密,根据最后一个字节来判断 key 和 iv是哪个。

这里没想到解密的密钥直接写死为字符串常量在方法中,而且解密的逻辑也很简单,完全没有逆向难度,直接 CV 其解密的逻辑到本地来解密字节码就可以了。解密脚本放到了后面 解密class字节码脚本 。

拓展:使用两次agent

如果 dll 中的解密逻辑加了混淆,比较复杂,并且无法 CV 下来,这里可以使用两次 agent ,我们自己写一个 agent2 放在解密 agent1 的后面,此时 Agent1_OnLoad 获取到的字节码就是解密后的了。

java -agentpath:lib.dll -agentpath:my-lib.dll -jar .\app_encrypted.jar

写好的工具 agent 放到了后面 利用两次agent来dump字节码脚本 ,实测可以成功。

g++ -I %JAVA_HOME%\include -I %JAVA_HOME%\include\win32 -fPIC -shared library.cpp -o download_class.dll
# options指定要下载的类,这里用/分割,且开头不带/
java -agentpath:other.dll -agentpath:download_class.dll.dll=com/zkteco -jar .\app_encrypted.jar

拓展:使用HSDB

这个 jdk 自带的工具通过 JVM 中的 gHotSpotVMStructs 可以 dump 字节码,这个工具原理是 Java SA,因此不受 -XX:+DisableAttachMechanism 的限制。但是注意由于 SA 对 jdk 版本很敏感,必须运行 sa-jdi.jar 用的 jdk 和程序用的 jdk 版本一模一样,包括小版本号。

java -cp %JAVA_HOME%\lib\sa-jdi.jar sun.jvm.hotspot.HSDB

拓展:使用frida获取AES解密的key和IV

这个系统解密 jar 包比较容易,key 和 IV 写死在了变量中导致很容易暴露。这里可以加大难度,思考如果 key 和 IV 不好得到怎么办?

这里可以使用 frida 来 hook  AES 解密的方法,来获取到 key 和 IV 的结果,避免分析复杂的中间逻辑。脚本如下:

import sys
import frida

session = frida.attach("java.exe")
script = session.create_script("""
function dumpAddr(addr, size) {
if (addr.isNull())
return;
const buf = addr.readByteArray(size);
return Array.prototype.map.call(new Uint8Array(buf),
x => ('00' + x.toString(16)).slice(-2)).join(''); // 将ArrayBuffer转十六进制显示,对应C语言中的%2.2x显示
}
const baseAddr = Module.findBaseAddress('libdonskoy.dll');
console.log('libdonskoy.dll baseAddr: ' + baseAddr);
const AES_init_ctx_iv_addr = 0x65842AEC; // 从ida反编译dll获取到的地址
Interceptor.attach(ptr(AES_init_ctx_iv_addr), {
onEnter(args) {
console.log('[+] Called AES_init_ctx_iv');
console.log('[+] Key: ' + dumpAddr(args[1], 16));
console.log('[+] IV: ' + dumpAddr(args[2], 16));
},
});
""")
script.load()
sys.stdin.read()

agent注入程序来dump字节码

这里在启动参数中加了 -XX:+DisableAttachMechanism 和  -Dcom.ibm.tools.attach.enable=no 导致我们无法 agent 注入程序,并且这个程序会在运行前检测是否携带了这个参数,没带就不让运行,那怎么办呢?

修改jar包,注入代码(失败)

本来试着想用前面 修改jar包的逻辑(不一定可用) 的技巧来在启动的时候在注入代码来利用 javassist 工具 dump 字节码到本地,但是发现行不通。因为不知道为什么 javassist 获取到的是没解密前的字节码。这里以后再研究。

ClassPool pool = ClassPool.getDefault();
// 解决SpringBoot环境下JavaAssist找不到类的问题
pool.appendClassPath(new LoaderClassPath(Thread.currentThread().getContextClassLoader()));
String className = "xxx";
CtClass ctClass = pool.getCtClass(className);
fos.write(ctClass.toBytecode());

补充:这里有的 jar 包在 MANIFEST.MF 文件中做了签名校验(启动的 jar 包没有检验,是可以改成功的),直接改 jar 包会无法运行。但是发现可以直接删除 MANIFEST.MF 文件中的签名就可以绕过了。

使用调试执行代码绕过参数检验

这里这个程序有一个很大的奇怪点,就是这里检验的 agent 参数关键字是不能大于 2 ,但是这个程序自身的启动参数中只包含一个 agent ,也就是这里允许再加一个 agent 关键字,虽然通过 -XX:+DisableAttachMechanism 防止了 attach ,但是没防调试参数,虽然调试参数中包含了关键字 agent ,但是这里可以多一个,因此可以添加调试参数来调试此程序,从而在检验启动参数的时候打断点通过 idea 执行代码来绕过这里的检验。

这里检验 agent 参数的逻辑写在 guard.jar 中的 CheckAgentUtil 类中,每次断点断在 if (attach)的时候修改 attach 的值为 false 即可绕过。

dump字节码

这里可以用阿里的 arthas 工具来 dump 字节码,不过需要注意的是只有当触发类加载的时候才会调用 JavaAgent 的逻辑,也就是说我们 dump 字节码必须先遍历所有的类,然后手动去触发类加载。这点还是比较麻烦的。

也可以自己写一个 Agent 。脚本放到了后面 利用agent.jar来dump字节码脚本 。

解密class字节码脚本

C 版本只实现了解密单个 class 的功能(用于验证解密思路,解密逻辑有没有问题),Java 版本实现了批量解密 jar 包的功能。

Java版本

package com.just;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;

public class Main {
public static void main(String[] args) throws Exception {
String dirPath = "D:\\xxx\\service\\xxx\\lib\\jar\\";
File dirFile = new File(dirPath);
File[] fileList = dirFile.listFiles();
assert fileList != null;
for (File f : fileList) {
System.out.printf("===== %s =====\n", f.getAbsolutePath());
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
File srcFile = new File(f.getAbsolutePath());
File dstFile = new File(".\\decrypt_out\\" + f.getName());
FileOutputStream dstFos = new FileOutputStream(dstFile);
JarOutputStream dstJar = new JarOutputStream(dstFos);
JarFile srcJar = new JarFile(srcFile);
for (Enumeration<JarEntry> enumeration = srcJar.entries(); enumeration.hasMoreElements(); ) {
JarEntry entry = enumeration.nextElement();
InputStream is = srcJar.getInputStream(entry);
int len;
while ((len = is.read(buf, 0, buf.length)) != -1) {
baos.write(buf, 0, len);
}
byte[] bytes = baos.toByteArray();
String name = entry.getName();
System.out.println(name);
if (name.endsWith(".class")) {
if (Utils.isEncrypt(bytes)) {
bytes = Utils.decrypt(bytes);
assert bytes != null;
if (Utils.isEncrypt(bytes)) {
System.out.println("Error");
return;
}
}
}
JarEntry ne = new JarEntry(name);
dstJar.putNextEntry(ne);
dstJar.write(bytes);
baos.reset();
}
srcJar.close();
dstJar.close();
dstFos.close();
}
System.out.println("success");
}
}
package com.just;

import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.Arrays;
import javax.crypto.Cipher;

public class Utils {
public static String MAGIC = "cafebabe";
public static String fish = "ok, let me have a look. er ~~~ . Say something about pang zhi? Oh, OK OK that's all.";
public static String lion = "en ~~, abcdefg hijklmnop qrs tuv wx y and z, now I can say my abc, next time want's yon sing with me.";
public static String dog = "my name is san ye. I hate pang zhi, actually I hate everything fat";
public static String fly = "3ye!@#3ye~~ohohohohoh3ye~~2ye1yeyeyeyeyesoManyYe!!Hello three ye.";
public static String bee = "er ~~~, write something? en *_*. biu biu biu biu, bong bong bong. die....";
public static String cat = "[email protected]&there is a pang zhi neer by&^_^&it's funny to write something here~~ ha ha ha";
public static boolean isEncrypt(byte[] class_data) {
byte[] magic = Arrays.copyOfRange(class_data, 0, 4);
return !MAGIC.equals(toHexString(magic));
}
public static byte[] decrypt(byte[] class_data) throws Exception{
int data_len = class_data.length - 2;
byte[] data = Arrays.copyOfRange(class_data, 0, data_len);
char padding = (char) class_data[data_len];
int length = Integer.parseInt(String.valueOf(padding),16)+ 1;
char type = (char) class_data[data_len + 1];
byte[] key, iv;
switch (type) {
case '0':
key = md5(cat.getBytes());
iv = md5(dog.getBytes());
break;
case '1':
key = md5(fish.getBytes());
iv = md5(lion.getBytes());
break;
case '2':
key = md5(fly.getBytes());
iv = md5(bee.getBytes());
break;
default:
System.out.println("Error");
return null;
}
byte[] decrypt = aesDecrypt(data, key, iv);
return Arrays.copyOfRange(decrypt, 0, data_len - length);
}
public static String toHexString(byte[] byteArray) {
if (byteArray == null || byteArray.length < 1)
throw new IllegalArgumentException("this byteArray must not be null or empty");

final StringBuilder hexString = new StringBuilder();
for (int i = 0; i < byteArray.length; i++) {
if ((byteArray[i] & 0xff) < 0x10)
hexString.append("0");
hexString.append(Integer.toHexString(0xFF & byteArray[i]));
}
return hexString.toString().toLowerCase();
}

public static byte[] md5(byte[] b) {
byte[] digest = null;
try {
MessageDigest md5 = MessageDigest.getInstance("md5");
digest = md5.digest(b);
} catch (Exception e) {
e.printStackTrace();
}
return digest;
}

public static byte[] aesDecrypt(byte[] encryptedBytes, byte[] key, byte[] iv)
throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec);
return cipher.doFinal(encryptedBytes);
}
}

C版本

#include 
#include
#include
#include
#include "md5.c"
#include "aes.c"

unsigned char fish[] = "ok, let me have a look. er ~~~ . Say something about pang zhi? Oh, OK OK that's all.";
unsigned char lion[] = "en ~~, abcdefg hijklmnop qrs tuv wx y and z, now I can say my abc, next time want's yon sing with me.";
unsigned char dog[] = "my name is san ye. I hate pang zhi, actually I hate everything fat";
unsigned char fly[] = "3ye!@#3ye~~ohohohohoh3ye~~2ye1yeyeyeyeyesoManyYe!!Hello three ye.";
unsigned char bee[] = "er ~~~, write something? en *_*. biu biu biu biu, bong bong bong. die....";
unsigned char cat[] = "[email protected]&there is a pang zhi neer by&^_^&it's funny to write something here~~ ha ha ha";

char *class_data = NULL;
size_t class_data_len;

void readBinaryFile(const char* filename) {
FILE* file = fopen(filename, "rb");
if (!file) {
class_data = NULL;
return;
}
char* buffer = (char*)malloc(1024);
if (!buffer) {
fclose(file);
class_data = NULL;
return;
}
class_data_len = 0;
size_t len;
class_data = NULL;
while ((len = fread(buffer, 1, 1024, file)) > 0) {
char* temp = realloc(class_data, class_data_len + len + 1);
if (!temp) {
free(class_data);
fclose(file);
free(buffer);
class_data = NULL;
return;
}
class_data = temp;
memcpy(class_data + class_data_len, buffer, len);
class_data_len += len;
}
class_data[class_data_len] = '\0'; // 添加字符串结束符
fclose(file);
free(buffer);
printf("%s size = %lld\n", filename, class_data_len);
}

void writeBinaryFile(const char* filename, const char* content, size_t size) {
FILE* file = fopen(filename, "wb");
if (file) {
fwrite(content, 1, size, file);
fclose(file);
}
}

int __cdecl hexCharToInt(char c)
{
if ( c > 47 && c <= 57 )
return c - 48;
if ( c > 64 && c <= 70 )
return c - 55;
if ( c <= 96 || c > 102 )
return 0;
return c - 87;
}

int main() {
const char* inFilename = "D:\\Project\\cproject\\untitled4\\in\\BaseCerTypeServiceImpl.class";
const char* outFilename = "D:\\Project\\cproject\\untitled4\\out\\BaseCerTypeServiceImpl.class";
readBinaryFile(inFilename);
size_t data_len = class_data_len - 2;
char padding = class_data[data_len];
int length = hexCharToInt(padding) + 1;
unsigned char type = class_data[data_len + 1];
unsigned char key[16];
unsigned char iv[16];
printf("padding = %c, length = %d, type = %c\n", padding, length, type);
switch (type) {
case '1':
md5(fish, strlen((const char*) fish), key);
md5(lion, strlen((const char*) lion), iv);
break;
case '2':
md5(fly, strlen((const char*) fly), key);
md5(bee, strlen((const char*) bee), iv);
break;
case '0':
md5(cat, strlen((const char*) cat), key);
md5(dog, strlen((const char*) dog), iv);
break;
default:
printf("Error\n");
break;
}
printf("key = ");
for (int i = 0; i < 16; ++i) {
printf("%2.2x", key[i]);
}
printf("\n");
printf("iv = ");
for (int i = 0; i < 16; ++i) {
printf("%2.2x", iv[i]);
}
printf("\n");
unsigned char data[data_len];
memset(data, 0, data_len);
for (int i = 0; i < data_len; ++i) { // data是class_data的前data_len部分
data[i] = class_data[i];
}
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, key, iv); // data会经过AES解密,其key和IV是根据class的最后一个字节决定的
AES_CBC_decrypt_buffer(&ctx, (unsigned char*)data, data_len);
writeBinaryFile(outFilename, (char *)data, data_len - length); // class的最终内容就是data数组的前data_len-length部分,length是根据class的倒数第二个字节决定的
printf("%x%x%x%x\n", data[0], data[1], data[2], data[3]);
return 0;
}

利用两次agent.dll来dump字节码脚本

#include 
#include "library.h"
#include "jni.h"
#include
#include "jvmti.h"
#include "jni_md.h"
#include

char *target;

void mkdirs(char *dir) {
char *lastSlash;
lastSlash = strrchr(dir, '/');
if (lastSlash == nullptr) {
mkdir(dir);
printf("[*] mkdir %s\n", dir);
return;
}
struct stat info;
if (!stat(dir, &info)) {
return;
}
size_t length = lastSlash - dir;
char subDir[length + 1];
strncpy(subDir, dir, length);
subDir[length] = '\0';
mkdirs(subDir);
mkdir(dir);
printf("[*] mkdir %s\n", dir);
}

void writeBinaryFile(const char* filename, const char* content, size_t size) {
char *lastSlash;
lastSlash = strrchr(filename, '/');
if (lastSlash != nullptr) {
size_t length = lastSlash - filename;
char subString[length + 1];
strncpy(subString, filename, length);
subString[length] = '\0';
struct stat info;
if (stat(subString, &info)) {
mkdirs(subString);
}
}
FILE* file = fopen(filename, "wb");
if (file) {
fwrite(content, 1, size, file);
fclose(file);
} else {
printf("[*] Open %s error\n", filename);
}
}

void JNICALL ClassDecryptHook(
jvmtiEnv* jvmti_env,
JNIEnv* jni_env,
jclass class_being_redefined,
jobject loader,
const char* name,
jobject protection_domain,
jint class_data_len,
const unsigned char* class_data,
jint* new_class_data_len,
unsigned char** new_class_data
) {
*new_class_data_len = class_data_len;
jvmti_env->Allocate(class_data_len, new_class_data);
unsigned char* _data = *new_class_data;
for (int i = 0; i < class_data_len; i++) {
_data[i] = class_data[i];
}
if (name && strncmp(name, target, strlen(target)) == 0) {
char *path = new char[strlen(target) + strlen(name) + 6];
sprintf(path, "decrypt/%s.class", name);
writeBinaryFile(path, (const char *)(class_data), class_data_len);
printf("[*] write %s\n", path);
}
}

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) {
if (options == nullptr) {
target = new char [2];
strncpy(target, "", 1);
target[1] = '\0';
} else {
size_t len = strlen(options);
if (options[0] == '/') {
printf("[*] target can't start with '/'\n");
exit(0);
}
target = new char [len + 1];
for (int i = 0; i < len; ++i){
target[i] = options[i];
}
target[len] = '\0';
}
printf("[*] target class = %s\n", target);
jvmtiEnv* jvmti;
jint ret = vm->GetEnv((void**)&jvmti, JVMTI_VERSION);
if (JNI_OK != ret) {
printf("ERROR: Unable to access JVMTI!\n");
return ret;
}
jvmtiCapabilities capabilities;
(void)memset(&capabilities, 0, sizeof(capabilities));

capabilities.can_generate_all_class_hook_events = 1;
capabilities.can_tag_objects = 1;
capabilities.can_generate_object_free_events = 1;
capabilities.can_get_source_file_name = 1;
capabilities.can_get_line_numbers = 1;
capabilities.can_generate_vm_object_alloc_events = 1;

jvmtiError error = jvmti->AddCapabilities(&capabilities);
if (JVMTI_ERROR_NONE != error) {
printf("ERROR: Unable to AddCapabilities JVMTI!\n");
return error;
}

jvmtiEventCallbacks callbacks;
(void)memset(&callbacks, 0, sizeof(callbacks));

callbacks.ClassFileLoadHook = &ClassDecryptHook;
error = jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
if (JVMTI_ERROR_NONE != error) {
printf("ERROR: Unable to SetEventCallbacks JVMTI!\n");
return error;
}

error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL);
if (JVMTI_ERROR_NONE != error) {
printf("ERROR: Unable to SetEventNotificationMode JVMTI!\n");
return error;
}

return JNI_OK;
}

利用agent.jar来dump字节码脚本


package com.agent;

import com.sun.tools.attach.VirtualMachine;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.instrument.Instrumentation;
import java.net.URLDecoder;

public class Main {

public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.out.println("命令格式: java -jar attach-agent.jar ");
return;
}
String agentPath = getJarFileByClass(Main.class);
System.out.println("[*] AgentPath: " + agentPath);
Class.forName("sun.tools.attach.HotSpotAttachProvider");
System.out.println("[*] start inject pid " + args[0]);
VirtualMachine virtualMachine = VirtualMachine.attach(args[0]);
System.out.println("[*] " + args[0] + " inject success");
virtualMachine.loadAgent(agentPath, "xxx");
virtualMachine.detach();
}

public static void agentmain(String agentArgs, Instrumentation inst) throws Exception {
System.out.println("[*] =====agentmain=====");
com.agent.MyTransformer raspTransformer = new com.agent.MyTransformer();
inst.addTransformer(raspTransformer, true);
}

public static void premain(String agentArgs, Instrumentation inst) throws Exception {
System.out.println("[*] =====premain=====");
com.agent.MyTransformer raspTransformer = new MyTransformer();
inst.addTransformer(raspTransformer, true);
}

public static String getJarFileByClass(Class cs) {
String fileString = null;
if (cs != null) {
String tmpString = cs.getProtectionDomain().getCodeSource().getLocation().getFile();
if (tmpString.endsWith(".jar")) {
try {
fileString = URLDecoder.decode(tmpString, "utf-8");
} catch (UnsupportedEncodingException var4) {
fileString = URLDecoder.decode(tmpString);
}
}
}
return (new File(fileString)).toString();
}
}
package com.agent;

import java.io.FileOutputStream;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class MyTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className,
Class> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
if(className.startsWith("com/just/service/")){
System.out.println("[*] decode " + className);
try {
FileOutputStream fos = new FileOutputStream(className.substring(className.lastIndexOf("/") + 1) + ".class");
fos.write(classfileBuffer);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return classfileBuffer;
}

}




黑白之道发布、转载的文章中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!

如侵权请私聊我们删文


END