原文首发在:先知社区
https://xz.aliyun.com/t/15423/3648
在审一套Java系统的时候,发现其核心代码都被加密了看不到,这篇文章来介绍总结一下解密jar包的思路。
分析
初步分析系统,发现此系统的 web
源码都放在 servicexxxsecurity
目录下。反编译 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 <stdio.h> #include <stdlib.h> #include <string.h> #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("%sn", 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%includewin32 -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%libsa-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 <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #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] = '�'; // 添加字符串结束符
fclose(file);
free(buffer);
printf("%s size = %lldn", 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 = %cn", 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("Errorn");
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%xn", data[0], data[1], data[2], data[3]);
return 0;
}
利用两次agent.dll来dump字节码脚本
#include <iostream> #include "library.h" #include "jni.h" #include <cstring> #include "jvmti.h" #include "jni_md.h" #include <sys/stat.h>
char *target;
void mkdirs(char *dir) {
char *lastSlash;
lastSlash = strrchr(dir, '/');
if (lastSlash == nullptr) {
mkdir(dir);
printf("[*] mkdir %sn", 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] = '�';
mkdirs(subDir);
mkdir(dir);
printf("[*] mkdir %sn", 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] = '�';
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 errorn", 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 %sn", path);
}
}
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM* vm, char* options, void* reserved) {
if (options == nullptr) {
target = new char [2];
strncpy(target, "", 1);
target[1] = '�';
} 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] = '�';
}
printf("[*] target class = %sn", 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 <process pid>");
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;
}
}
原文始发于微信公众号(黑白之道):记一次实战中解密JVMTI加密过的jar包
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论