Nook in the Lunar Mare

Capital Cjar格式逆向及分析

Jan 10, 2026

Capital Cjar格式逆向及分析

由于工作原因,需要对工业软件进行二次开发,除了文档外最好也能参考其中已有的组件。Capital中插件以JAR文件格式存在plugin/文件夹下,然而对外暴露的API功能有限,不能很好的满足需求。根据供应商提供的插件功能项来看,软件本身还存在一些更为核心的接口,如导入文件并解析,生成图纸对象等功能。这类插件存放于adaptor/,以.cjar作为后缀。尝试普通的反编译工具均失败,因此决定分析下这个独特的cjar格式。

常规文件格式分析

想先看看是不是只是什么文件格式的trick,使得普通的加载方式无法识别CJAR格式,上网搜索无相关信息,所以直接看看文件本体。根据JAVA的定义,JAR包本质是一个zip打包文件,所以直接解压打开看看内部的class是否与常规的class文件不同。简单对比即可发现,与常规的jar不同,cjar内的class似乎被加密过,看不到函数名。(左侧为cjar,右侧为jar) image(2).png

到此基本上可以猜测是一种自定义的格式,那么就需要在加载cjar的过程中动手脚,比较可能的思路是通过java的classloader机制去自定义读取cjar的文件内容再行解析。所以我第一时间想到的是去JVM动态获取内存,定位加载cjar的逻辑,或是直接从JVM加载的类中找到解密后的class。

这时候我想起来,软件的本体是exe,它是怎么完成jar包插件的载入呢?可以想到的途径是这两种:

  1. exe中封装好命令,用子进程启动一个jvm
  2. 加载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的话,估计需要LoadLibiraryGetProcAddr函数,现在侧边找下这两个函数,然后看下参数有没有我们的目标:

  • JNI_CreateJavaVM

很快定位到具体的代码,同时周边能看到许多设置JVMoption的字符串,暂时无关心,简单看下有没有特殊的防护参数即可。

image(3).png

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

image(4).png

定位LoadClass

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

image(5).png

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规范中规定的结构体信息,可以推出这两个函数分别是AttachCurrentThreadDefineClass

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过程

image(6).png

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

不细看了,先甩给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方便查看

image(8).png

循环 < 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