package com.spineview.dec; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.lang.reflect.Method; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; /** * 3.8.99 字符串批量解密。 * * 原理全部来自字节码,不是猜的: * 所有解密函数(PrefsPanes$Prefs$ToolbarPosition.f、aaQ$Wm.x 以及若干内联副本) * 第一步都算同一个 key: * key = (cpSize + 调用类名 + 调用方法名 + cpSize).hashCode() * cpSize 取的是**调用解密函数的那个类**的常量池大小(两次都用 st[2])。 * * 所以只要造一个合成类,让它 * - 类名 == 原始调用方类名 * - 方法名 == 原始调用方法名 * 由它去调解密函数,key 就自然对上;cpSize 由解密函数内部 Class.forName 从真实类读到。 * * 用法: * java -cp "asm.jar:本工具:已全 public 化的载荷.zip" com.spineview.dec.DecAll * <载荷> <映射.tsv> [可读副本.zip] */ public final class DecAll { private static final String DECRYPT_DESC = "(Ljava/lang/Object;)Ljava/lang/String;"; private static final String HOOK_OWNER = "sun/misc/SharedSecrets"; private static final String HOOK_NAME = "getJavaLangAccess"; public static void main(String[] args) throws Exception { Path payload = Paths.get(args[0]); Path mapOut = Paths.get(args[1]); Path readableOut = args.length > 2 ? Paths.get(args[2]) : null; // 0) 整个载荷读进内存(约 15MB,无压力) Map entries = new LinkedHashMap(); List order = new ArrayList(); try (ZipFile zip = new ZipFile(payload.toFile())) { Enumeration it = zip.entries(); while (it.hasMoreElements()) { ZipEntry entry = it.nextElement(); if (entry.isDirectory()) continue; entries.put(entry.getName(), readAll(zip.getInputStream(entry))); order.add(entry.getName()); } } System.err.println("载荷条目 " + entries.size()); // 1) 找出所有解密函数:方法体里出现 SharedSecrets.getJavaLangAccess 的 (Object)String 方法 Set decryptors = new HashSet(); Map parsed = new LinkedHashMap(); for (Map.Entry entry : entries.entrySet()) { if (!entry.getKey().endsWith(".class")) continue; ClassNode node; try { node = parse(entry.getValue()); } catch (Throwable error) { System.err.println(" 跳过 " + entry.getKey() + " : " + error); continue; } parsed.put(entry.getKey(), node); for (MethodNode method : node.methods) { if (!DECRYPT_DESC.equals(method.desc)) continue; if (callsCryptHook(method)) decryptors.add(node.name + "." + method.name); } } System.err.println("找到解密函数 " + decryptors.size() + " 个"); for (String id : decryptors) System.err.println(" " + id); // 2) 找所有 "ldc 密文; invokestatic 解密函数" 位置,按 (调用类, 调用方法, 解密函数) 分组 Map groups = new LinkedHashMap(); Map> sitesByClass = new LinkedHashMap>(); for (Map.Entry entry : parsed.entrySet()) { ClassNode node = entry.getValue(); for (MethodNode method : node.methods) { for (AbstractInsnNode insn = method.instructions.getFirst(); insn != null; insn = insn.getNext()) { if (!(insn instanceof LdcInsnNode) || !(((LdcInsnNode) insn).cst instanceof String)) continue; AbstractInsnNode after = insn.getNext(); while (after != null && (after.getOpcode() == Opcodes.NOP || after.getOpcode() == Opcodes.CHECKCAST)) { after = after.getNext(); } if (!(after instanceof MethodInsnNode)) continue; MethodInsnNode call = (MethodInsnNode) after; if (call.getOpcode() != Opcodes.INVOKESTATIC) continue; String id = call.owner + "." + call.name; if (!decryptors.contains(id)) continue; String key = node.name + '\u0000' + method.name + '\u0000' + id; Group group = groups.get(key); if (group == null) { group = new Group(node.name.replace('/', '.'), method.name, call.owner, call.name); groups.put(key, group); } group.ciphers.add((String) ((LdcInsnNode) insn).cst); List list = sitesByClass.get(entry.getKey()); if (list == null) { list = new ArrayList(); sitesByClass.put(entry.getKey(), list); } list.add(new Site((LdcInsnNode) insn, (MethodInsnNode) after, method, group, group.ciphers.size() - 1)); } } } int total = 0; for (Group group : groups.values()) total += group.ciphers.size(); System.err.println("(调用类,调用方法,解密函数) 组合 " + groups.size() + ",待解密字符串 " + total + " 个"); // 3) 逐组解密 int done = 0, ok = 0, failed = 0; try (BufferedWriter writer = Files.newBufferedWriter(mapOut, StandardCharsets.UTF_8)) { for (Group group : groups.values()) { String[] plain = group.decrypt(); group.plain = plain; for (int i = 0; i < group.ciphers.size(); i++) { String value = plain == null ? null : plain[i]; if (value == null) failed++; else ok++; writer.write(group.owner); writer.write('\t'); writer.write(group.method); writer.write('\t'); writer.write(escape(value == null ? "<失败>" : value)); writer.write('\n'); } if (++done % 300 == 0) System.err.println(" 进度 " + done + "/" + groups.size()); } } System.err.println("解密完成:成功 " + ok + ",失败 " + failed + " -> " + mapOut); // 4) 产出"字符串已换成明文"的可读副本:ldc 换成明文,解密调用换成 NOP。 // 解密调用是 (Object)->String,净栈效果为 0,换成 NOP 后栈完全平衡, // 栈帧表和 max_stack 都不用重算。 if (readableOut != null) { int rewritten = 0; try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(readableOut))) { for (String name : order) { byte[] raw = entries.get(name); List sites = sitesByClass.get(name); if (sites != null && !sites.isEmpty()) { ClassNode node = parse(raw); int replaced = 0; // 和第二步同样的遍历顺序,所以用计数器就能把密文和实际调用点对上 Map counter = new LinkedHashMap(); for (MethodNode method : node.methods) { for (AbstractInsnNode insn = method.instructions.getFirst(); insn != null; insn = insn.getNext()) { if (!(insn instanceof LdcInsnNode) || !(((LdcInsnNode) insn).cst instanceof String)) continue; AbstractInsnNode after = insn.getNext(); while (after != null && (after.getOpcode() == Opcodes.NOP || after.getOpcode() == Opcodes.CHECKCAST)) { after = after.getNext(); } if (!(after instanceof MethodInsnNode)) continue; MethodInsnNode call = (MethodInsnNode) after; if (call.getOpcode() != Opcodes.INVOKESTATIC) continue; String key = node.name + '\u0000' + method.name + '\u0000' + call.owner + "." + call.name; Group group = groups.get(key); if (group == null || group.plain == null) continue; Integer seen = counter.get(group); int index = seen == null ? 0 : seen.intValue(); if (index >= group.ciphers.size()) continue; counter.put(group, Integer.valueOf(index + 1)); ((LdcInsnNode) insn).cst = group.plain[index]; method.instructions.set(call, new InsnNode(Opcodes.NOP)); replaced++; } } rewritten += replaced; ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); node.accept(writer); raw = writer.toByteArray(); } out.putNextEntry(new ZipEntry(name)); out.write(raw); out.closeEntry(); } } System.err.println("可读副本已写出(替换 " + rewritten + " 处) -> " + readableOut); } } private static boolean callsCryptHook(MethodNode method) { for (AbstractInsnNode insn = method.instructions.getFirst(); insn != null; insn = insn.getNext()) { if (insn instanceof MethodInsnNode) { MethodInsnNode call = (MethodInsnNode) insn; if (HOOK_OWNER.equals(call.owner) && HOOK_NAME.equals(call.name)) return true; } } return false; } private static ClassNode parse(byte[] raw) { ClassNode node = new ClassNode(); new ClassReader(raw).accept(node, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); return node; } private static String escape(String text) { // 解出来的可能是任意 char(有些"字符串"其实是二进制数据),一律转成纯 ASCII 转义, // 免得写 UTF-8 时因为落单代理对炸掉。 StringBuilder builder = new StringBuilder(text.length() + 8); for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); if (c == '\t') builder.append("\\t"); else if (c == '\n') builder.append("\\n"); else if (c == '\r') builder.append("\\r"); else if (c == '\\') builder.append("\\\\"); else if (c >= 0x20 && c <= 0x7E) builder.append(c); else builder.append(String.format("\\u%04x", Integer.valueOf(c))); } return builder.toString(); } private static byte[] readAll(InputStream in) throws java.io.IOException { ByteArrayOutputStream buffer = new ByteArrayOutputStream(); byte[] chunk = new byte[65536]; int n; while ((n = in.read(chunk)) > 0) buffer.write(chunk, 0, n); return buffer.toByteArray(); } private static final class Site { final LdcInsnNode ldc; final MethodInsnNode call; final MethodNode method; final Group group; final int index; Site(LdcInsnNode ldc, MethodInsnNode call, MethodNode method, Group group, int index) { this.ldc = ldc; this.call = call; this.method = method; this.group = group; this.index = index; } } private static final class Group { final String owner; final String method; final String decryptorOwner; final String decryptorName; final List ciphers = new ArrayList(); String[] plain; Group(String owner, String method, String decryptorOwner, String decryptorName) { this.owner = owner; this.method = method; this.decryptorOwner = decryptorOwner; this.decryptorName = decryptorName; } String[] decrypt() { try { byte[] bytes = build(owner, method, decryptorOwner, decryptorName, ciphers); ShadowLoader loader = new ShadowLoader(owner, bytes, DecAll.class.getClassLoader()); Class cls = Class.forName(owner, false, loader); if ("".equals(method)) { Class.forName(owner, true, loader); } else if ("".equals(method)) { cls.getDeclaredConstructor().newInstance(); } else { for (Method candidate : cls.getDeclaredMethods()) { if (candidate.getName().equals(method)) { candidate.setAccessible(true); candidate.invoke(null); break; } } } return (String[]) cls.getField("R").get(null); } catch (Throwable error) { Throwable cause = error; while (cause.getCause() != null && cause.getCause() != cause) cause = cause.getCause(); System.err.println(" [失败] " + owner + "." + method + " via " + decryptorOwner + "." + decryptorName + " : " + error + (cause == error ? "" : " <- " + cause)); return null; } } } private static byte[] build(String owner, String method, String decryptorOwner, String decryptorName, List ciphers) { String internal = owner.replace('.', '/'); ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); writer.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC | Opcodes.ACC_SUPER, internal, null, "java/lang/Object", null); writer.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "R", "[Ljava/lang/String;", null, null).visitEnd(); int access = Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC; if ("".equals(method)) access = Opcodes.ACC_STATIC; else if ("".equals(method)) access = Opcodes.ACC_PUBLIC; MethodVisitor code = writer.visitMethod(access, method, "()V", null, null); code.visitCode(); if ("".equals(method)) { code.visitVarInsn(Opcodes.ALOAD, 0); code.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "", "()V", false); } code.visitLdcInsn(Integer.valueOf(ciphers.size())); code.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String"); code.visitFieldInsn(Opcodes.PUTSTATIC, internal, "R", "[Ljava/lang/String;"); for (int i = 0; i < ciphers.size(); i++) { code.visitFieldInsn(Opcodes.GETSTATIC, internal, "R", "[Ljava/lang/String;"); code.visitLdcInsn(Integer.valueOf(i)); code.visitLdcInsn(ciphers.get(i)); code.visitMethodInsn(Opcodes.INVOKESTATIC, decryptorOwner, decryptorName, DECRYPT_DESC, false); code.visitInsn(Opcodes.AASTORE); } code.visitInsn(Opcodes.RETURN); code.visitMaxs(0, 0); code.visitEnd(); writer.visitEnd(); return writer.toByteArray(); } private static final class ShadowLoader extends ClassLoader { private final String shadowed; private final byte[] bytes; ShadowLoader(String shadowed, byte[] bytes, ClassLoader parent) { super(parent); this.shadowed = shadowed; this.bytes = bytes; } @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.equals(shadowed)) { Class found = findLoadedClass(name); if (found == null) found = defineClass(name, bytes, 0, bytes.length); if (resolve) resolveClass(found); return found; } return super.loadClass(name, resolve); } } }