Capital Cjar格式逆向及分析
Capital Cjar格式逆向及分析
由于工作原因,需要对工业软件进行二次开发,除了文档外最好也能参考其中已有的组件。Capital中插件以JAR文件格式存在plugin/文件夹下,然而对外暴露的API功能有限,不能很好的满足需求。根据供应商提供的插件功能项来看,软件本身还存在一些更为核心的接口,如导入文件并解析,生成图纸对象等功能。这类插件存放于adaptor/,以.cjar作为后缀。尝试普通的反编译工具均失败,因此决定分析下这个独特的cjar格式。
常规文件格式分析
想先看看是不是只是什么文件格式的trick,使得普通的加载方式无法识别CJAR格式,上网搜索无相关信息,所以直接看看文件本体。根据JAVA的定义,JAR包本质是一个zip打包文件,所以直接解压打开看看内部的class是否与常规的class文件不同。简单对比即可发现,与常规的jar不同,cjar内的class似乎被加密过,看不到函数名。(左侧为cjar,右侧为jar)

到此基本上可以猜测是一种自定义的格式,那么就需要在加载cjar的过程中动手脚,比较可能的思路是通过java的classloader机制去自定义读取cjar的文件内容再行解析。所以我第一时间想到的是去JVM动态获取内存,定位加载cjar的逻辑,或是直接从JVM加载的类中找到解密后的class。
这时候我想起来,软件的本体是exe,它是怎么完成jar包插件的载入呢?可以想到的途径是这两种:
- exe中封装好命令,用子进程启动一个jvm
- 加载jvm.dll,用函数控制jvm行为
途径1的话可以直接尝试对jvm进程进行监控或者分析,如果是途径2的话就比较麻烦,除了分析exe外还可能要面对修改过的jvm。总之先看判别下是1或者2。直接用process explorer,检查capital进程下的子进程,没有jvm.exe,但是DLL中存在jvm.dll,可以推断是走的途径2,dll对象为CHS\MentorGraphic\jre\bin\server\jvm.dll。接下来的目标就是先定位主程序是何时载入这个dll的,从而进一步抓到载入cjar的逻辑
主程序Capital Modeler.exe分析
定位JVM.dll
capital软件本身由多个模块组成,采用启动器的设计,所以需要找到具体模块的exe进行分析。电气的每个模块基本都支持插件,挑一个熟悉的就行,这里选的CapitalModeler
加载jvm.dll的话,估计需要LoadLibirary、GetProcAddr函数,现在侧边找下这两个函数,然后看下参数有没有我们的目标:
- JNI_CreateJavaVM
很快定位到具体的代码,同时周边能看到许多设置JVMoption的字符串,暂时无关心,简单看下有没有特殊的防护参数即可。

直接页面内查找JNI_createJVM的调用位置,根据这个函数的原型还原一下参数名

定位LoadClass
往下追踪pJVM的调用处,看到一些比较关键的字符串,如ClassLoader、Main Class信息

FUN_140006d50和FUN_140006db0都是类似的,通过偏移量从Java Native接口调用JVM相关函数。
void FUN_140006d50(longlong *pJVM,undefined8 param_2,undefined8 param_3)
{
(**(code **)(*pJVM + 0x20))(pJVM,param_2,param_3);
return;
}
void FUN_140006db0(longlong *JNI_env,undefined8 param_2,undefined8 param_3,undefined8 param_4,
undefined4 param_5)
{
(**(code **)(*JNI_env + 0x28))(JNI_env,param_2,param_3,param_4,param_5);
return;
}
结合JAVA规范中规定的结构体信息,可以推出这两个函数分别是AttachCurrentThread和DefineClass
typedef struct JNIInvokeInterface_ {
void *reserved0;
void *reserved1;
void *reserved2;
jint (*DestroyJavaVM)(JavaVM *);
jint (*AttachCurrentThread)(JavaVM *, JNIEnv **, void *);
jint (*DetachCurrentThread)(JavaVM *);
jint (*GetEnv)(JavaVM *, void **, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM *, JNIEnv **, void *);
} JNIInvokeInterface;
struct JavaVM {
const struct JNIInvokeInterface_ *functions;
};
struct JNINativeInterface_ {
void *reserved0;
void *reserved1;
void *reserved2;
void *reserved3;
/* 方法调用相关 */
jint (*GetVersion)(JNIEnv *);
jclass (*DefineClass)(JNIEnv *, const char *, jobject, const jbyte *, jsize);
jclass (*FindClass)(JNIEnv *, const char *);
// ... ...
}
分析类加载过程
由于定位到defineClass,这个函数是类加载机制中用于读取class文件字节流的函数,大概率这段代码就包含我们想要分析的解密cjar过程

其中FUN_140003410较为可疑,点进去能快速推断出是xor解密。此外这个InstallAnywhere似乎是软件使用的打包工具

不细看了,先甩给AI进行分析还原代码。
#include <stdio.h>
#include <string.h>
void xor_encrypt_decrypt(unsigned char *data, size_t length)
{
const char *key = "2301031555InstallAnywhereTestKey";
size_t key_len = strlen(key);
size_t key_index = 0;
for (size_t i = 0; i < length; i++) {
data[i] ^= key[key_index];
key_index++;
if (key_index >= key_len) {
key_index = 0; // 密钥循环
}
}
}
根据xor_encrypt的参数,返回来看全局变量,大概是这么个结构:
struct capital_item {
pointer64 *xor_data;
uint64 length;
union record {
// 可能是纯数据如字符串,也可能是全局变量的索引
byte[256] data;
pointer64[32] addrs;
};
}
可以在ghidra里先造个Struct方便查看

循环 < 3,那就把起始地址+0x0、+0x110、+0x220的数据都看一遍,大差不差,符合上述结构。
这两个应该就是实际运行中加载Jar和解密class的代码,本身是一个经过xor加密的class字节流,直接静态存在xor_data中。
那直接从对应地址把字节流导出并解密即可。用ghidra copy special的功能拷贝为python list,然后让AI写一个xor还原并写入filename.class的脚本:
def xor_and_save(data, filename):
key = b"2301031555InstallAnywhereTestKey"
key_len = len(key)
# XOR
result = bytearray()
for i, b in enumerate(data):
result.append(b ^ key[i % key_len])
# write
with open(filename, "wb") as f:
f.write(result)
print(f"[+] 已写入解密结果到 {filename}")
if __name__ == "__main__":
data = [0x11, 0x02, 0x45, 0x99, 0xAA, 0xFE]
xor_and_save(data, "output.bin")
LoadClass与Decrypt分析
LoadClass
直接jadx反编译看看,加载过程比较简单,根据Manifest文件中的Encrypt字段判断Jar是否调用Decrypt。
public synchronized Object[] MGClassDecryptJarEntry(JarFile var1, JarEntry var2) {
return this.MGClassDecryptJarEntry(var1, var2, this.MGClassIsEncryptedJar(var1));
}
private Object[] MGClassDecryptJarEntry(JarFile jar_file, JarEntry entry, boolean is_encrypt) {
DataInputStream istream = null;
try {
istream = new DataInputStream(jar_file.getInputStream(entry));
int jar_size = (int)entry.getSize();
byte[] raw_buffer = (byte[])this.m_rawData.get();
if (raw_buffer == null || raw_buffer.length < jar_size) {
raw_buffer = new byte[jar_size];
this.m_rawData = new WeakReference(raw_buffer);
}
istream.readFully(raw_buffer, 0, jar_size);
istream.close();
Object java_obj = null;
int clazz_size;
Object[] java_clazzs;
byte[] clazz_data;
if (is_encrypt) {
java_clazzs = m_decrypter.decryptClass(raw_buffer, jar_size, (byte[])java_obj);
clazz_data = (byte[])java_clazzs[0];
clazz_size = (Integer)java_clazzs[1];
} else {
clazz_data = raw_buffer;
clazz_size = jar_size;
}
java_clazzs = new Object[]{new byte[clazz_size], clazz_size};
System.arraycopy(clazz_data, 0, (byte[])java_clazzs[0], 0, clazz_size);
Object[] var10 = java_clazzs;
return var10;
} catch (IOException var27) {
ignoreException(var27);
} catch (Exception var28) {
ignoreException(var28);
} finally {
if (istream != null) {
try {
istream.close();
} catch (IOException var25) {
ignoreException(var25);
} catch (Exception var26) {
ignoreException(var26);
}
}
}
return null;
}
Decrypt逻辑
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class DecryptClass {
private static final String CRYPT_ALGO = "AES";
private static final String CRYPT_MODE = "CBC";
private static final String CRYPT_PADDING = "PKCS5Padding";
public static final int INIT_VECTOR_SIZE = 16;
private String hexKey = "366966ccc3ee04998244b4dbb17e618f";
public DecryptClass() {
}
void setHexKey(String var1) {
this.hexKey = var1;
}
Cipher newCipher(byte[] var1) {
if (this.hexKey != null && !this.hexKey.startsWith("@")) {
byte[] var2 = this.decode(this.hexKey);
SecretKeySpec var3 = new SecretKeySpec(var2, "AES");
IvParameterSpec var4 = new IvParameterSpec(var1);
try {
String var5 = String.join("/", "AES", "CBC", "PKCS5Padding");
Cipher var6 = Cipher.getInstance(var5);
var6.init(2, var3, var4);
return var6;
} catch (Exception var7) {
var7.printStackTrace();
}
} else {
System.err.println("Invalid KEY used to create the binary. Please contact build team");
}
return null;
}
public Object[] decryptClass(byte[] var1, int var2, byte[] var3) {
byte[] var4 = new byte[16];
System.arraycopy(var1, 0, var4, 0, 16);
byte[] var5 = new byte[var2 - 16];
System.arraycopy(var1, 16, var5, 0, var5.length);
Cipher var6 = this.newCipher(var4);
return this.decryptClass(var5, var5.length, var3, var6);
}
private Object[] decryptClass(byte[] var1, int var2, byte[] var3, Cipher var4) {
if (var4 == null) {
return new Object[]{var1, var1};
} else {
Object[] var5 = new Object[2];
try {
int var6 = var4.getOutputSize(var2 - 4) + 4;
if (var3 != null && var3.length >= var6) {
var5[0] = var3;
} else {
var5[0] = new byte[var6];
}
System.arraycopy(var1, 0, (byte[])var5[0], 0, 4);
int var7 = var4.doFinal(var1, 4, var2 - 4, (byte[])var5[0], 4);
var5[1] = var7 + 4;
} catch (Exception var8) {
var8.printStackTrace();
}
return var5;
}
}
public byte[] decode(String var1) {
byte[] var2 = new byte[var1.length() / 2];
char[] var3 = var1.toCharArray();
for(int var4 = 0; var4 < var3.length; var4 += 2) {
int var5 = Character.digit(var3[var4], 16) << 4;
var5 |= Character.digit(var3[var4 + 1], 16);
var2[var4 / 2] = (byte)var5;
}
return var2;
}
}
这里建议直接给AI分析之后写解密代码,加密方法为AES-CBC模式-PKCS5填充,class大概是前16字节为AES加密中的IV,剩余字节为加密后的数据。取IV和硬编码的key一起可以还原所有加密的class
import os
import sys
import zipfile
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from io import BytesIO
def hex_to_key(hex_key):
"""Convert hex string to byte key"""
return bytes.fromhex(hex_key)
def decrypt_class_data(encrypted_data, key):
"""
Decrypt a single .class file data
:param encrypted_data: Encrypted .class content (with 16-byte IV at front)
:param key: AES key (16 bytes)
:return: Decrypted .class data
"""
if len(encrypted_data) < 16:
raise ValueError("Encrypted data too short (less than 16 bytes)")
iv = encrypted_data[:16] # First 16 bytes = IV
cipher_data = encrypted_data[16:] # Rest is encrypted content
if len(cipher_data) < 4:
raise ValueError("No payload after IV")
# Extract first 4 bytes (to be preserved)
header = cipher_data[:4]
body = cipher_data[4:]
# Decrypt body
cipher = AES.new(key, AES.MODE_CBC, iv)
try:
decrypted_body = cipher.decrypt(body)
# Remove padding only if needed; some payloads may not be padded properly
# We try unpad, but if it fails, use raw decrypted
try:
unpadded_body = unpad(decrypted_body, AES.block_size, style='pkcs7')
except ValueError:
# Padding error, use raw (might be intentional)
unpadded_body = decrypted_body
except Exception as e:
print(f"[Warning] Decryption error: {e}")
unpadded_body = decrypted_body # fallback
return header + unpadded_body
def decrypt_jar(encrypted_jar_path, output_jar_path, hex_key):
key = hex_to_key(hex_key)
with zipfile.ZipFile(encrypted_jar_path, 'r') as encrypted_zip:
with zipfile.ZipFile(output_jar_path, 'w', zipfile.ZIP_DEFLATED) as new_zip:
for item in encrypted_zip.infolist():
data = encrypted_zip.read(item.filename)
if item.filename.endswith('.class'):
print(f"Decrypting: {item.filename}")
try:
decrypted_data = decrypt_class_data(data, key)
new_zip.writestr(item, decrypted_data)
except Exception as e:
print(f"[ERROR] Failed to decrypt {item.filename}: {e}")
# Optionally write original (encrypted) for debugging
new_zip.writestr(item, data)
else:
# Not a .class file → copy as-is (resources, manifests, etc.)
print(f"Copying (not decrypted): {item.filename}")
new_zip.writestr(item, data)
print(f"\n✅ Decrypted JAR saved to: {output_jar_path}")
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python decrypt_cjar.py <encrypted.cjar>")
sys.exit(1)
encrypted_jar = sys.argv[1]
output_jar = encrypted_jar+".jar"
hex_key = "366966ccc3ee04998244b4dbb17e618f" # From your Java code
if not os.path.exists(encrypted_jar):
print(f"❌ File not found: {encrypted_jar}")
sys.exit(1)
decrypt_jar(encrypted_jar, output_jar, hex_key)
其他
JVM启动成功+LoadClass之后,主要调用法为 JNIEnv->offset 得到函数指针,传参然后call function
对第三个奇怪的内嵌数据,加载终止地址之前的所有jar包,把class存放在内部的classArray中
后续可以分析这两个jar包:
- capitalcaf
- capitallogic