mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
721 字
2 分钟
jnic转化代码分析
2026-02-18
统计加载中...

介绍#

NOTE

曾经也是jnic正版用户,但是由于jnic的更新太慢了,150英镑的昂贵价格让人望而却步。 jnic还有授权次数限制,次数限制一旦没有就无法再次授权新设备。

有个myj2c的混淆,通过魔改native-obfuscator的逻辑来实现和 jnic 一致是性能和效果

radioegor146
/
native-obfuscator
Waiting for api.github.com...
00K
0K
0K
Waiting...

这里面也包括 chacha20 和一模一样的缓存技术,但是却丢失了flow的功能。

NOTE

但是这个myj2c的混淆名声不是很好,因为其中藏有后门,利用LZMA将无混native的class一同打包进.dat文件中

由于太无聊,这次看看jnic的3.7.0生成的代码都有什么新变化,希望这篇文章不会被dmca

配置#

字符串混淆#

新版本将原来的哈希SHA-256升级为SHA-512,增加了数字签名Ed25519

但是似乎字符串加密并没有什么变化。

唯一区别就是JNI_ONLOAD的

jnic_buf = (*env)->GetDirectBufferAddress(env, buf);
struct chacha20_context c;
chacha20_init_context(&c, key, jnic_buf + 32, 0);
chacha20_write_stream(&c, jnic_buf, 1681);

变为

uint8_t *java_buf = (*env)->GetDirectBufferAddress(env, buf);
struct chacha20_context c;
chacha20_init_context(&c, key, java_buf + 32, 0);
jnic_buf = malloc(2564);
chacha20_write_stream(&c, jnic_buf, 2564);

依旧没法避免 jni hook 得到这个buf(

对应更新:

  • Improved memory efficiency of loader

新功能useIntrinsics#

内部优化,根据官网的文档的话,那就是

该选项允许JNIC将特定Java API方法的调用替换为手工编写的最优代码。

除了能够提升性能外,这种方式还能防止在JVM层面对这些方法调用进行插桩检测,

从而增加逆向工程的难度。支持优化的方法包括:

  • java.lang.Object.getClass()
  • java.lang.String.equals(java.lang.Object)
  • java.lang.String.isEmpty()
  • java.lang.String.length()

若未指定该选项,默认值为 false(禁用状态)。

String.equals#

实际上就是把 原来的CallBooleanMethod直接替换成一整块的代码块

  • 未优化实现
// c_27id_7 -> equals
stack0.i = (*env)->CallBooleanMethod(env,stack0.1, c_27id_7(env),stack1.l);
if ((*env)->ExceptionCheck(env)){
return;
}
  • 优化后实现
// stack0.i 就相当于返回equals的结果
if (stack1.l == NULL) stack0.i = 0;
else if ((*env)->IsSameObject(env, stack0.l, stack1.l)) stack0.i = 1;
else if (!(*env)->IsInstanceOf(env, stack1.l, c_27(env))) stack0.i = 0;
else {
jsize l = (*env)->GetStringLength(env, stack0.l);
if (l != (*env)->GetStringLength(env, stack1.l)) stack0.i = 0;
else {
const jchar *s1 = (*env)->GetStringCritical(env, stack0.l, NULL);
const jchar *s2 = (*env)->GetStringCritical(env, stack1.l, NULL);
jboolean r = 1;
for (jsize i = 0; i < l; ++i) {
if (s1[i] != s2[i]) { r = 0; break; }
}
(*env)->ReleaseStringCritical(env, stack1.l, s2);
(*env)->ReleaseStringCritical(env, stack0.l, s1);
stack0.i = r;
}
}

String.length#

  • 未优化实现
stack2.l = local1.l;
stack2.i = (*env)->CallIntMethod(env, stack2.l, c_4id_2(env));
if ((*env)->ExceptionCheck(env)) {
return;
}
  • 优化后实现
stack2.l = local1.l;
stack2.i = (*env)->GetStringLength(env, stack2.l);
if ((*env)->ExceptionCheck(env)) {
return;
}

实际上,我们依旧可以通过Hook GetStringLength 来得到这个字符串的长度以及内容

String.isEmpty#

  • 优化前实现
stack2.l = local3.l;
stack2.i = (*env)->CallBooleanMethod(env, stack2.l, c_4id_1(env));
if ((*env)->ExceptionCheck(env)) {
return;
}
  • 优化后实现
stack2.l = local3.l;
stack2.i = (*env)->GetStringLength(env, stack2.l) == 0;
if ((*env)->ExceptionCheck(env)) {
return;
}

实际上就是获取长度,判断它的长度是否为0,和前面的length依旧可以进行hook

Object.getClass#

  • 优化前实现
stack2.l = local1.l;
stack2.l = (*env)->CallObjectMethod(env, stack2.l, c_3id_0(env));
if ((*env)->ExceptionCheck(env)) {
return;
}
  • 优化后实现
stack2.l = local1.l;
stack2.l = (*env)->GetObjectClass(env, stack2.l);
if ((*env)->ExceptionCheck(env)) {
return;
}

实现的优化依旧还是在jni的体系中啊(,依旧可以轻松的Hook出来

fastCompile快速编译#

由于这个功能没人照做过,但是通过Hook可以复现同款操作(。

其输出的是:

zig cc -fno-sanitize=all -fno-sanitize-trap=all -Os -fno-optimize-sibling-calls -fno-slp-vectorize -target x86_64-windows -std=c11 -fPIC -shared -s -fvisibility=hidden -IC:\Users\xxxx\AppData\Local\Temp\jnic6249051816001900267 -oC:\Users\xxxx\AppData\Local\Temp\jnic6249051816001900267\WINDOWS_X86_64\jnic.jnilib C:\Users\xxxx\AppData\Local\Temp\jnic6249051816001900267\jnic.c

根据上面的hook之类输出则可以得到下面的代码

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ZigMultiCompiler {
private static final String BASE_DIR = "C:\\Users\\xxxxx\\AppData\\Local\\Temp\\jnic6249051816001900267";
private static final String SOURCE_FILE_NAME = "jnic.c";
static class Target {
String zigTarget; // Zig 的 target
String outDirName; // 输出文件夹名称
String ext; // 文件后缀 ( .jnilib, .so, .dll)
public Target(String zigTarget, String outDirName, String ext) {
this.zigTarget = zigTarget;
this.outDirName = outDirName;
this.ext = ext;
}
}
public static void main(String[] args) {
List<Target> targets = new ArrayList<>();
// --- Windows ---
targets.add(new Target("x86_64-windows", "WINDOWS_X86_64", ".jnilib"));
// targets.add(new Target("x86-windows", "WINDOWS_X86", ".jnilib"));
// --- Linux ---
targets.add(new Target("x86_64-linux-gnu", "LINUX_X86_64", ".so"));
// targets.add(new Target("aarch64-linux-gnu", "LINUX_ARM64", ".so"));
targets.add(new Target("x86_64-macos", "MACOS_X86_64", ".dylib"));
targets.add(new Target("aarch64-macos", "MACOS_ARM64", ".dylib"));
long startTime = System.currentTimeMillis();
int successCount = 0;
for (Target target : targets) {
boolean success = compileForTarget(target);
if (success) successCount++;
}
long endTime = System.currentTimeMillis();
System.out.println("\n========================================");
System.out.println(String.format("批量编译完成。成功: %d / %d,耗时: %d ms",
successCount, targets.size(), (endTime - startTime)));
}
private static boolean compileForTarget(Target target) {
String sourcePath = new File(BASE_DIR, SOURCE_FILE_NAME).getAbsolutePath();
File outputDir = new File(BASE_DIR, target.outDirName);
File outputFile = new File(outputDir, "jnic" + target.ext);
if (!outputDir.exists()) {
outputDir.mkdirs();
}
List<String> command = new ArrayList<>();
command.add("zig");
command.add("cc");
command.add("-fno-sanitize=all"); // 禁用所有运行时代码清洗/安全检查
command.add("-fno-sanitize-trap=all"); // 防止编译器为未定义行为生成陷阱指令。
command.add("-Os"); // 减小代码体积
command.add("-fno-optimize-sibling-calls"); // 禁用“兄弟调用”
command.add("-fno-slp-vectorize"); // 禁用 SLP
command.add("-std=c11");
command.add("-fPIC");
command.add("-shared");
command.add("-s"); // 删除符号表和调试信息
command.add("-fvisibility=hidden"); // 隐藏符号
command.add("-target");
command.add(target.zigTarget);
command.add("-I" + BASE_DIR);
command.add("-o" + outputFile.getAbsolutePath());
command.add(sourcePath);
try {
ProcessBuilder pb = new ProcessBuilder(command);
pb.directory(new File(BASE_DIR));
pb.inheritIO();
Process process = pb.start();
int exitCode = process.waitFor();
if (exitCode == 0) {
System.out.println(" [OK] 生成文件: " + target.outDirName + File.separator + outputFile.getName());
return true;
} else {
System.err.println(" [FAIL] 编译失败: " + exitCode);
return false;
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
return false;
}
}
}

果然这个编译还是太神秘了

缓存#

这个应该是对应的更新

  • 通过 CSE 在 JNI ID 缓存上生成的原生代码性能提升 (Improved performance of generated native code with CSE on JNI ID caches)
struct cached_c_5 {
jclass _Atomic clazz;
jmethodID id_3;
jmethodID id_1;
jmethodID id_2;
jmethodID id_0;
};
static struct cached_c_5* c_5_(JNIEnv *env) {
static struct cached_c_5 cache;
static atomic_flag lock;
if (atomic_load_explicit(&cache.clazz, memory_order_acquire)) return &cache;
jclass clazz = (*env)->FindClass(env, jnic_decrypt((char[]) {60, -104, -28, -42, -8, 73, 11, -111, 48, 79, -26, -75, -119, -90, -67, 94, 108, 31, 102, 6, 52, -120, 10, 0}, 893, 23));
while (atomic_flag_test_and_set(&lock)) {}
if (!cache.clazz) {
cache.id_2 = (*env)->GetMethodID(env, clazz, jnic_decrypt((char[]) {93, 106, -90, 50, 107, 117, 0}, 916, 6), jnic_decrypt((char[]) {-64, -61, 113, 40, 100, 52, -59, -33, 45, 113, 21, -97, 63, -58, 73, -10, -57, -87, 111, 72, -8, 7, -35, 56, 83, -63, -123, 49, 0}, 922, 28));
cache.id_0 = (*env)->GetMethodID(env, clazz, jnic_decrypt((char[]) {-25, 94, -108, 72, 7, 81, 0}, 950, 6), jnic_decrypt((char[]) {-40, -85, -45, 0}, 956, 3));
cache.id_1 = (*env)->GetMethodID(env, clazz, jnic_decrypt((char[]) {-118, -41, -21, 71, -5, -21, 0}, 959, 6), jnic_decrypt((char[]) {-97, 24, -54, -125, 58, -24, -117, 121, 3, -103, 88, 127, -81, -9, -79, 97, -14, 70, 12, -92, 89, -31, 0, -68, -107, -32, -61, 116, 109, -122, -83, -94, -113, -86, 35, 62, -70, -102, 98, -35, 32, 22, -53, -31, 65, 0}, 965, 45));
cache.id_3 = (*env)->GetMethodID(env, clazz, jnic_decrypt((char[]) {-3, 36, -91, 100, -66, -4, 64, -47, 0}, 1010, 8), jnic_decrypt((char[]) {-97, 83, -23, -37, -99, 70, 51, 72, 47, 30, -118, 56, 23, -69, 49, 60, -117, -12, -84, 6, 0}, 1018, 20));
clazz = (*env)->NewGlobalRef(env, clazz);
atomic_store_explicit(&cache.clazz, clazz, memory_order_release);
}
atomic_flag_clear(&lock);
return &cache;
}

升级为单独的一个函数

__attribute__((const))static jmethodID c_40id_0(JNIEnv *env) {
static _Atomic jmethodID id;
jmethodID cached = atomic_load_explicit(&id, memory_order_relaxed);
if (__builtin_expect(cached != 0, 1)) return cached;
jclass clazz = c_40(env);
cached = (*env)->GetMethodID(env, clazz, "<init>", "(Ljava/lang/String;ILjava/lang/String;)V");
atomic_store_explicit(&id, cached, memory_order_relaxed);
return cached;
}
__attribute__((const))static jclass c_19(JNIEnv *env) {
static _Atomic jclass clazz;
jclass cached = atomic_load_explicit(&clazz, memory_order_relaxed);
if (__builtin_expect(cached != 0, 1)) return cached;
jclass loaded = (*env)->FindClass(env, "java/lang/Integer");
loaded = (*env)->NewGlobalRef(env, loaded);
if (atomic_compare_exchange_strong_explicit(&clazz, &cached, loaded, memory_order_relaxed, memory_order_relaxed))
return loaded;
(*env)->DeleteGlobalRef(env, loaded);
return cached;
}

Flow#

太复杂了,分析不来

jnic机器码#

可以发现jnic的机器码是依据设备的mac地址

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class HWIDGenerator {
private static final MessageDigest SHA256;
static {
try {
SHA256 = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
public static byte[] getMachineId() throws Exception {
InetAddress localHost = InetAddress.getLocalHost();
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(localHost);
byte[] macAddress = networkInterface.getHardwareAddress();
byte[] machineId = SHA256.digest(macAddress);
return machineId;
}
public static void main(String[] args) throws Exception {
System.out.println(Base64.getEncoder().encodeToString(getMachineId()));
}
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

jnic转化代码分析
https://blog.cutebaka.cloud/posts/jnic/
作者
0x0AB8
发布于
2026-02-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时