记一次对实战中js的解密下
【摘要】 拓展:使用HSDB这个 jdk 自带的工具通过 JVM 中的 gHotSpotVMStructs 可以 dump 字节码,这个工具原理是 Java SA,因此不受 -XX:+DisableAttachMechanism 的限制。但是注意由于 SA 对 jdk 版本很敏感,必须运行 sa-jdi.jar 用的 jdk 和程序用的 jdk 版本一模一样,包括小版本号。java -cp %JAVA...
拓展:使用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 = "totoro.ou@zkteco.com&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[] = "totoro.ou@xxx.com&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 <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 %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 <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;
}
}
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)