记一次实战中对JS的解密(上)
原文首发在:先知社区
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 <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, "xxx.xxx@xxx.com%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
- 点赞
- 收藏
- 关注作者
评论(0)