mirror of
https://github.com/ispointer/RePairip.git
synced 2026-09-16 04:03:57 +02:00
#r_mod
This commit is contained in:
@@ -129,7 +129,7 @@ public class MethodMaker {
|
||||
|
||||
public static ImmutableMethod createClinit() {
|
||||
MethodImplementationBuilder builder = new MethodImplementationBuilder(1);
|
||||
builder.addInstruction(new BuilderInstruction21c(Opcode.CONST_STRING, 0, new ImmutableStringReference("RePatcher v1.4.14")));
|
||||
builder.addInstruction(new BuilderInstruction21c(Opcode.CONST_STRING, 0, new ImmutableStringReference("RePatcher v1.5.20")));
|
||||
builder.addInstruction(new BuilderInstruction35c(Opcode.INVOKE_STATIC, 0, 0, 0, 0, 0, 0, new ImmutableMethodReference(PairipClass.STARTUP_LAUNCHER.type, "launch", null, "V")));
|
||||
builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
|
||||
return new ImmutableMethod(PairipClass.APPLICATION.type, "<clinit>", null, "V", AccessFlags.STATIC.getValue() | AccessFlags.CONSTRUCTOR.getValue(), null, null, builder.getMethodImplementation());
|
||||
|
||||
@@ -1,343 +1,343 @@
|
||||
package com.antik.DexPatcher.Translation;
|
||||
|
||||
import com.antik.DexPatcher.Translation.antik.PairipClass;
|
||||
import com.antik.Main;
|
||||
import com.reandroid.apk.ApkModule;
|
||||
import com.reandroid.archive.ByteInputSource;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import org.jf.dexlib2.DexFileFactory;
|
||||
import org.jf.dexlib2.Opcodes;
|
||||
import org.jf.dexlib2.iface.ClassDef;
|
||||
import org.jf.dexlib2.iface.DexFile;
|
||||
import org.jf.dexlib2.writer.io.MemoryDataStore;
|
||||
import org.jf.dexlib2.writer.pool.DexPool;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class TranslationPatcher {
|
||||
|
||||
private static final byte[] DEX_MAGIC = new byte[]{0x64, 0x65, 0x78, 0x0A};
|
||||
private static final byte[] PAIRIP_ASSET_HEADER = new byte[]{0x00, 0x49, 0x41, 0x50, 0x02};
|
||||
|
||||
public static void patch(ApkModule m, File jsonFile) throws Exception {
|
||||
System.out.println("[INFO] Loading translation file: " + jsonFile.getName());
|
||||
TranslationData data = new TranslationData(jsonFile);
|
||||
TranslationRewriter rewriter = new TranslationRewriter(data);
|
||||
|
||||
List<String> dexNames = new ArrayList<>();
|
||||
for (InputSource s : m.getInputSources()) {
|
||||
if (s.getName().endsWith(".dex")) {
|
||||
dexNames.add(s.getName());
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> existingDexNames = new HashSet<>(dexNames);
|
||||
Set<String> existingClassTypes = new HashSet<>();
|
||||
for (String dn : dexNames) {
|
||||
InputSource s = m.getInputSource(dn);
|
||||
if (s == null) continue;
|
||||
try (InputStream input = s.openStream()) {
|
||||
DexFile dexFile = loadDexFile(readAllBytes(input), "trans_scan");
|
||||
for (ClassDef classDef : dexFile.getClasses()) {
|
||||
existingClassTypes.add(classDef.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<PendingDex> extraDexes = new ArrayList<>();
|
||||
if (data.hasMethods) {
|
||||
extraDexes.addAll(collectEmbeddedAssetDexes(m, existingClassTypes));
|
||||
PendingDex restoreMethodDex = loadRestoreMethodDex(existingClassTypes);
|
||||
if (restoreMethodDex != null) {
|
||||
extraDexes.add(restoreMethodDex);
|
||||
}
|
||||
}
|
||||
|
||||
for (String dn : dexNames) {
|
||||
InputSource s = m.getInputSource(dn);
|
||||
if (s == null) continue;
|
||||
|
||||
byte[] d_bs;
|
||||
try (InputStream i = s.openStream()) {
|
||||
d_bs = readAllBytes(i);
|
||||
}
|
||||
|
||||
DexFile d_f = loadDexFile(d_bs, "trans");
|
||||
|
||||
List<ClassDef> cds = new ArrayList<>();
|
||||
boolean mod = false;
|
||||
|
||||
for (ClassDef cd : d_f.getClasses()) {
|
||||
String type = cd.getType();
|
||||
if (type.startsWith("Lcom/pairip/")) {
|
||||
if (PairipClass.contains(type)) {
|
||||
System.out.println("[INFO] Patching " + type + " in " + dn);
|
||||
cds.add(rewriter.rewrite(cd));
|
||||
mod = true;
|
||||
} else if (!type.equals("Lcom/pairip/PairipLog;") && !type.equals("Lcom/pairip/RestoreMethod;")) {
|
||||
System.out.println("[INFO] Removing class: " + type);
|
||||
mod = true;
|
||||
} else {
|
||||
cds.add(cd);
|
||||
}
|
||||
} else {
|
||||
cds.add(cd);
|
||||
}
|
||||
}
|
||||
|
||||
if (mod) {
|
||||
MemoryDataStore ds = new MemoryDataStore();
|
||||
DexPool dp = new DexPool(Opcodes.getDefault());
|
||||
for (ClassDef c : cds) {
|
||||
dp.internClass(c);
|
||||
}
|
||||
dp.writeTo(ds);
|
||||
byte[] r_bs = Arrays.copyOf(ds.getData(), ds.getSize());
|
||||
m.add(new ByteInputSource(r_bs, dn));
|
||||
System.out.println("[BUILD] Patched translation in " + dn);
|
||||
}
|
||||
}
|
||||
|
||||
int nextDexNumber = nextDexNumber(existingDexNames);
|
||||
for (PendingDex pendingDex : extraDexes) {
|
||||
if (!pendingDex.canAddAgainst(existingClassTypes)) {
|
||||
if (!pendingDex.isFullyPresentIn(existingClassTypes)) {
|
||||
System.err.println("[WARN] Skipping " + pendingDex.sourceName + " due to partial class overlap");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
String dexEntryName = nextDexEntryName(existingDexNames, nextDexNumber);
|
||||
nextDexNumber = dexNumberOf(dexEntryName) + 1;
|
||||
m.add(new ByteInputSource(pendingDex.dexBytes, dexEntryName));
|
||||
existingDexNames.add(dexEntryName);
|
||||
existingClassTypes.addAll(pendingDex.classTypes);
|
||||
System.out.println("[INFO] Added " + dexEntryName + " from " + pendingDex.sourceName);
|
||||
}
|
||||
|
||||
List<String> libFiles = new ArrayList<>();
|
||||
for (InputSource source : m.getInputSources()) {
|
||||
if (source.getName().contains("libpairipcore.so")) {
|
||||
libFiles.add(source.getName());
|
||||
}
|
||||
}
|
||||
for (String libFile : libFiles) {
|
||||
try {
|
||||
m.getZipEntryMap().remove(libFile);
|
||||
System.out.println("[INFO] Removing lib: " + libFile);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[WARN] Could not remove lib: " + libFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream input) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int len;
|
||||
while ((len = input.read(buf)) != -1) {
|
||||
baos.write(buf, 0, len);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private static DexFile loadDexFile(byte[] dexBytes, String prefix) throws Exception {
|
||||
File tempDex = File.createTempFile(prefix, ".dex");
|
||||
try {
|
||||
Files.write(tempDex.toPath(), dexBytes);
|
||||
return DexFileFactory.loadDexFile(tempDex, Opcodes.getDefault());
|
||||
} finally {
|
||||
tempDex.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<PendingDex> collectEmbeddedAssetDexes(ApkModule module, Set<String> existingClassTypes) throws Exception {
|
||||
List<PendingDex> pendingDexes = new ArrayList<>();
|
||||
for (InputSource source : module.getInputSources()) {
|
||||
if (!isTopLevelAsset(source.getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] assetBytes;
|
||||
try (InputStream input = source.openStream()) {
|
||||
assetBytes = readAllBytes(input);
|
||||
}
|
||||
|
||||
if (!startsWith(assetBytes, PAIRIP_ASSET_HEADER)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int dexStart = findEmbeddedDexStart(assetBytes);
|
||||
if (dexStart < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] dexBytes = extractEmbeddedDex(assetBytes, dexStart);
|
||||
PendingDex pendingDex = pendingDexFromBytes(source.getName(), dexBytes, "asset_dex");
|
||||
if (!pendingDex.isFullyPresentIn(existingClassTypes)) {
|
||||
pendingDexes.add(pendingDex);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
pendingDexes.sort((left, right) -> {
|
||||
int bySize = Integer.compare(right.classTypes.size(), left.classTypes.size());
|
||||
if (bySize != 0) {
|
||||
return bySize;
|
||||
}
|
||||
return left.sourceName.compareTo(right.sourceName);
|
||||
});
|
||||
return pendingDexes;
|
||||
}
|
||||
|
||||
private static PendingDex loadRestoreMethodDex(Set<String> existingClassTypes) throws Exception {
|
||||
try (InputStream input = Main.class.getResourceAsStream("/restoreMethod.dex")) {
|
||||
if (input == null) {
|
||||
throw new FileNotFoundException("Missing resource: /restoreMethod.dex");
|
||||
}
|
||||
PendingDex pendingDex = pendingDexFromBytes("restoreMethod.dex", readAllBytes(input), "restore_method");
|
||||
System.out.println("[INFO] Loaded classes from restoreMethod.dex");
|
||||
return pendingDex.isFullyPresentIn(existingClassTypes) ? null : pendingDex;
|
||||
}
|
||||
}
|
||||
|
||||
private static PendingDex pendingDexFromBytes(String sourceName, byte[] dexBytes, String prefix) throws Exception {
|
||||
DexFile dexFile = loadDexFile(dexBytes, prefix);
|
||||
Set<String> classTypes = new HashSet<>();
|
||||
for (ClassDef classDef : dexFile.getClasses()) {
|
||||
classTypes.add(classDef.getType());
|
||||
}
|
||||
return new PendingDex(sourceName, dexBytes, classTypes);
|
||||
}
|
||||
|
||||
private static boolean isTopLevelAsset(String entryName) {
|
||||
if (!entryName.startsWith("assets/")) {
|
||||
return false;
|
||||
}
|
||||
return entryName.indexOf('/', "assets/".length()) < 0;
|
||||
}
|
||||
|
||||
private static boolean startsWith(byte[] bytes, byte[] prefix) {
|
||||
if (bytes.length < prefix.length) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < prefix.length; i++) {
|
||||
if (bytes[i] != prefix[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int findEmbeddedDexStart(byte[] bytes) {
|
||||
for (int i = 0; i <= bytes.length - DEX_MAGIC.length; i++) {
|
||||
boolean matches = true;
|
||||
for (int j = 0; j < DEX_MAGIC.length; j++) {
|
||||
if (bytes[i + j] != DEX_MAGIC[j]) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matches) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static byte[] extractEmbeddedDex(byte[] assetBytes, int dexStart) {
|
||||
int headerOffset = dexStart + 0x20;
|
||||
if (headerOffset + 4 > assetBytes.length) {
|
||||
throw new IllegalArgumentException("Embedded dex header truncated");
|
||||
}
|
||||
|
||||
int declaredFileSize = readLittleEndianInt(assetBytes, headerOffset);
|
||||
if (declaredFileSize <= 0) {
|
||||
throw new IllegalArgumentException("Embedded dex declared invalid file size: " + declaredFileSize);
|
||||
}
|
||||
|
||||
int dexEnd = dexStart + declaredFileSize;
|
||||
if (dexEnd > assetBytes.length) {
|
||||
throw new IllegalArgumentException("Embedded dex declared size exceeds asset bounds: " + declaredFileSize);
|
||||
}
|
||||
|
||||
return Arrays.copyOfRange(assetBytes, dexStart, dexEnd);
|
||||
}
|
||||
|
||||
private static int readLittleEndianInt(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xFF) | ((bytes[offset + 1] & 0xFF) << 8) | ((bytes[offset + 2] & 0xFF) << 16) | ((bytes[offset + 3] & 0xFF) << 24);
|
||||
}
|
||||
|
||||
private static int nextDexNumber(Set<String> dexEntryNames) {
|
||||
int next = 1;
|
||||
for (String name : dexEntryNames) {
|
||||
next = Math.max(next, dexNumberOf(name) + 1);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
private static int dexNumberOf(String dexEntryName) {
|
||||
if ("classes.dex".equals(dexEntryName)) {
|
||||
return 1;
|
||||
}
|
||||
if (!dexEntryName.startsWith("classes") || !dexEntryName.endsWith(".dex")) {
|
||||
return -1;
|
||||
}
|
||||
String middle = dexEntryName.substring("classes".length(), dexEntryName.length() - ".dex".length());
|
||||
if (middle.isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(middle);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static String nextDexEntryName(Set<String> dexEntryNames, int startNumber) {
|
||||
int number = Math.max(startNumber, 1);
|
||||
while (true) {
|
||||
String candidate = number == 1 ? "classes.dex" : "classes" + number + ".dex";
|
||||
if (!dexEntryNames.contains(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
number++;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PendingDex {
|
||||
private final String sourceName;
|
||||
private final byte[] dexBytes;
|
||||
private final Set<String> classTypes;
|
||||
|
||||
private PendingDex(String sourceName, byte[] dexBytes, Set<String> classTypes) {
|
||||
this.sourceName = sourceName;
|
||||
this.dexBytes = dexBytes;
|
||||
this.classTypes = classTypes;
|
||||
}
|
||||
|
||||
private boolean isFullyPresentIn(Set<String> existingClassTypes) {
|
||||
return existingClassTypes.containsAll(classTypes);
|
||||
}
|
||||
|
||||
private boolean canAddAgainst(Set<String> existingClassTypes) {
|
||||
for (String classType : classTypes) {
|
||||
if (existingClassTypes.contains(classType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.antik.DexPatcher.Translation;
|
||||
|
||||
import com.antik.DexPatcher.Translation.antik.PairipClass;
|
||||
import com.antik.Main;
|
||||
import com.reandroid.apk.ApkModule;
|
||||
import com.reandroid.archive.ByteInputSource;
|
||||
import com.reandroid.archive.InputSource;
|
||||
import org.jf.dexlib2.DexFileFactory;
|
||||
import org.jf.dexlib2.Opcodes;
|
||||
import org.jf.dexlib2.iface.ClassDef;
|
||||
import org.jf.dexlib2.iface.DexFile;
|
||||
import org.jf.dexlib2.writer.io.MemoryDataStore;
|
||||
import org.jf.dexlib2.writer.pool.DexPool;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class TranslationPatcher {
|
||||
|
||||
private static final byte[] DEX_MAGIC = new byte[]{0x64, 0x65, 0x78, 0x0A};
|
||||
private static final byte[] PAIRIP_ASSET_HEADER = new byte[]{0x00, 0x49, 0x41, 0x50, 0x02};
|
||||
|
||||
public static void patch(ApkModule m, File jsonFile) throws Exception {
|
||||
System.out.println("[INFO] Loading translation file: " + jsonFile.getName());
|
||||
TranslationData data = new TranslationData(jsonFile);
|
||||
TranslationRewriter rewriter = new TranslationRewriter(data);
|
||||
|
||||
List<String> dexNames = new ArrayList<>();
|
||||
for (InputSource s : m.getInputSources()) {
|
||||
if (s.getName().endsWith(".dex")) {
|
||||
dexNames.add(s.getName());
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> existingDexNames = new HashSet<>(dexNames);
|
||||
Set<String> existingClassTypes = new HashSet<>();
|
||||
for (String dn : dexNames) {
|
||||
InputSource s = m.getInputSource(dn);
|
||||
if (s == null) continue;
|
||||
try (InputStream input = s.openStream()) {
|
||||
DexFile dexFile = loadDexFile(readAllBytes(input), "trans_scan");
|
||||
for (ClassDef classDef : dexFile.getClasses()) {
|
||||
existingClassTypes.add(classDef.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<PendingDex> extraDexes = new ArrayList<>();
|
||||
if (data.hasMethods) {
|
||||
extraDexes.addAll(collectEmbeddedAssetDexes(m, existingClassTypes));
|
||||
PendingDex restoreMethodDex = loadRestoreMethodDex(existingClassTypes);
|
||||
if (restoreMethodDex != null) {
|
||||
extraDexes.add(restoreMethodDex);
|
||||
}
|
||||
}
|
||||
|
||||
for (String dn : dexNames) {
|
||||
InputSource s = m.getInputSource(dn);
|
||||
if (s == null) continue;
|
||||
|
||||
byte[] d_bs;
|
||||
try (InputStream i = s.openStream()) {
|
||||
d_bs = readAllBytes(i);
|
||||
}
|
||||
|
||||
DexFile d_f = loadDexFile(d_bs, "trans");
|
||||
|
||||
List<ClassDef> cds = new ArrayList<>();
|
||||
boolean mod = false;
|
||||
|
||||
for (ClassDef cd : d_f.getClasses()) {
|
||||
String type = cd.getType();
|
||||
if (type.startsWith("Lcom/pairip/")) {
|
||||
if (PairipClass.contains(type)) {
|
||||
System.out.println("[INFO] Patching " + type + " in " + dn);
|
||||
cds.add(rewriter.rewrite(cd));
|
||||
mod = true;
|
||||
} else if (!type.equals("Lcom/pairip/PairipLog;") && !type.equals("Lcom/pairip/RestoreMethod;")) {
|
||||
System.out.println("[INFO] Removing class: " + type);
|
||||
mod = true;
|
||||
} else {
|
||||
cds.add(cd);
|
||||
}
|
||||
} else {
|
||||
cds.add(cd);
|
||||
}
|
||||
}
|
||||
|
||||
if (mod) {
|
||||
MemoryDataStore ds = new MemoryDataStore();
|
||||
DexPool dp = new DexPool(Opcodes.getDefault());
|
||||
for (ClassDef c : cds) {
|
||||
dp.internClass(c);
|
||||
}
|
||||
dp.writeTo(ds);
|
||||
byte[] r_bs = Arrays.copyOf(ds.getData(), ds.getSize());
|
||||
m.add(new ByteInputSource(r_bs, dn));
|
||||
System.out.println("[BUILD] Patched translation in " + dn);
|
||||
}
|
||||
}
|
||||
|
||||
int nextDexNumber = nextDexNumber(existingDexNames);
|
||||
for (PendingDex pendingDex : extraDexes) {
|
||||
if (!pendingDex.canAddAgainst(existingClassTypes)) {
|
||||
if (!pendingDex.isFullyPresentIn(existingClassTypes)) {
|
||||
System.err.println("[WARN] Skipping " + pendingDex.sourceName + " due to partial class overlap");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
String dexEntryName = nextDexEntryName(existingDexNames, nextDexNumber);
|
||||
nextDexNumber = dexNumberOf(dexEntryName) + 1;
|
||||
m.add(new ByteInputSource(pendingDex.dexBytes, dexEntryName));
|
||||
existingDexNames.add(dexEntryName);
|
||||
existingClassTypes.addAll(pendingDex.classTypes);
|
||||
System.out.println("[INFO] Added " + dexEntryName + " from " + pendingDex.sourceName);
|
||||
}
|
||||
|
||||
List<String> libFiles = new ArrayList<>();
|
||||
for (InputSource source : m.getInputSources()) {
|
||||
if (source.getName().contains("libpairipcore.so")) {
|
||||
libFiles.add(source.getName());
|
||||
}
|
||||
}
|
||||
for (String libFile : libFiles) {
|
||||
try {
|
||||
m.getZipEntryMap().remove(libFile);
|
||||
System.out.println("[INFO] Removing lib: " + libFile);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[WARN] Could not remove lib: " + libFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readAllBytes(InputStream input) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int len;
|
||||
while ((len = input.read(buf)) != -1) {
|
||||
baos.write(buf, 0, len);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private static DexFile loadDexFile(byte[] dexBytes, String prefix) throws Exception {
|
||||
File tempDex = File.createTempFile(prefix, ".dex");
|
||||
try {
|
||||
Files.write(tempDex.toPath(), dexBytes);
|
||||
return DexFileFactory.loadDexFile(tempDex, Opcodes.getDefault());
|
||||
} finally {
|
||||
tempDex.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static List<PendingDex> collectEmbeddedAssetDexes(ApkModule module, Set<String> existingClassTypes) throws Exception {
|
||||
List<PendingDex> pendingDexes = new ArrayList<>();
|
||||
for (InputSource source : module.getInputSources()) {
|
||||
if (!isTopLevelAsset(source.getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] assetBytes;
|
||||
try (InputStream input = source.openStream()) {
|
||||
assetBytes = readAllBytes(input);
|
||||
}
|
||||
|
||||
if (!startsWith(assetBytes, PAIRIP_ASSET_HEADER)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int dexStart = findEmbeddedDexStart(assetBytes);
|
||||
if (dexStart < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] dexBytes = extractEmbeddedDex(assetBytes, dexStart);
|
||||
PendingDex pendingDex = pendingDexFromBytes(source.getName(), dexBytes, "asset_dex");
|
||||
if (!pendingDex.isFullyPresentIn(existingClassTypes)) {
|
||||
pendingDexes.add(pendingDex);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
pendingDexes.sort((left, right) -> {
|
||||
int bySize = Integer.compare(right.classTypes.size(), left.classTypes.size());
|
||||
if (bySize != 0) {
|
||||
return bySize;
|
||||
}
|
||||
return left.sourceName.compareTo(right.sourceName);
|
||||
});
|
||||
return pendingDexes;
|
||||
}
|
||||
|
||||
private static PendingDex loadRestoreMethodDex(Set<String> existingClassTypes) throws Exception {
|
||||
try (InputStream input = Main.class.getResourceAsStream("/restoreMethod.dex")) {
|
||||
if (input == null) {
|
||||
throw new FileNotFoundException("Missing resource: /restoreMethod.dex");
|
||||
}
|
||||
PendingDex pendingDex = pendingDexFromBytes("restoreMethod.dex", readAllBytes(input), "restore_method");
|
||||
System.out.println("[INFO] Loaded classes from restoreMethod.dex");
|
||||
return pendingDex.isFullyPresentIn(existingClassTypes) ? null : pendingDex;
|
||||
}
|
||||
}
|
||||
|
||||
private static PendingDex pendingDexFromBytes(String sourceName, byte[] dexBytes, String prefix) throws Exception {
|
||||
DexFile dexFile = loadDexFile(dexBytes, prefix);
|
||||
Set<String> classTypes = new HashSet<>();
|
||||
for (ClassDef classDef : dexFile.getClasses()) {
|
||||
classTypes.add(classDef.getType());
|
||||
}
|
||||
return new PendingDex(sourceName, dexBytes, classTypes);
|
||||
}
|
||||
|
||||
private static boolean isTopLevelAsset(String entryName) {
|
||||
if (!entryName.startsWith("assets/")) {
|
||||
return false;
|
||||
}
|
||||
return entryName.indexOf('/', "assets/".length()) < 0;
|
||||
}
|
||||
|
||||
private static boolean startsWith(byte[] bytes, byte[] prefix) {
|
||||
if (bytes.length < prefix.length) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < prefix.length; i++) {
|
||||
if (bytes[i] != prefix[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int findEmbeddedDexStart(byte[] bytes) {
|
||||
for (int i = 0; i <= bytes.length - DEX_MAGIC.length; i++) {
|
||||
boolean matches = true;
|
||||
for (int j = 0; j < DEX_MAGIC.length; j++) {
|
||||
if (bytes[i + j] != DEX_MAGIC[j]) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matches) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static byte[] extractEmbeddedDex(byte[] assetBytes, int dexStart) {
|
||||
int headerOffset = dexStart + 0x20;
|
||||
if (headerOffset + 4 > assetBytes.length) {
|
||||
throw new IllegalArgumentException("Embedded dex header truncated");
|
||||
}
|
||||
|
||||
int declaredFileSize = readLittleEndianInt(assetBytes, headerOffset);
|
||||
if (declaredFileSize <= 0) {
|
||||
throw new IllegalArgumentException("Embedded dex declared invalid file size: " + declaredFileSize);
|
||||
}
|
||||
|
||||
int dexEnd = dexStart + declaredFileSize;
|
||||
if (dexEnd > assetBytes.length) {
|
||||
throw new IllegalArgumentException("Embedded dex declared size exceeds asset bounds: " + declaredFileSize);
|
||||
}
|
||||
|
||||
return Arrays.copyOfRange(assetBytes, dexStart, dexEnd);
|
||||
}
|
||||
|
||||
private static int readLittleEndianInt(byte[] bytes, int offset) {
|
||||
return (bytes[offset] & 0xFF) | ((bytes[offset + 1] & 0xFF) << 8) | ((bytes[offset + 2] & 0xFF) << 16) | ((bytes[offset + 3] & 0xFF) << 24);
|
||||
}
|
||||
|
||||
private static int nextDexNumber(Set<String> dexEntryNames) {
|
||||
int next = 1;
|
||||
for (String name : dexEntryNames) {
|
||||
next = Math.max(next, dexNumberOf(name) + 1);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
private static int dexNumberOf(String dexEntryName) {
|
||||
if ("classes.dex".equals(dexEntryName)) {
|
||||
return 1;
|
||||
}
|
||||
if (!dexEntryName.startsWith("classes") || !dexEntryName.endsWith(".dex")) {
|
||||
return -1;
|
||||
}
|
||||
String middle = dexEntryName.substring("classes".length(), dexEntryName.length() - ".dex".length());
|
||||
if (middle.isEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(middle);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private static String nextDexEntryName(Set<String> dexEntryNames, int startNumber) {
|
||||
int number = Math.max(startNumber, 1);
|
||||
while (true) {
|
||||
String candidate = number == 1 ? "classes.dex" : "classes" + number + ".dex";
|
||||
if (!dexEntryNames.contains(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
number++;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class PendingDex {
|
||||
private final String sourceName;
|
||||
private final byte[] dexBytes;
|
||||
private final Set<String> classTypes;
|
||||
|
||||
private PendingDex(String sourceName, byte[] dexBytes, Set<String> classTypes) {
|
||||
this.sourceName = sourceName;
|
||||
this.dexBytes = dexBytes;
|
||||
this.classTypes = classTypes;
|
||||
}
|
||||
|
||||
private boolean isFullyPresentIn(Set<String> existingClassTypes) {
|
||||
return existingClassTypes.containsAll(classTypes);
|
||||
}
|
||||
|
||||
private boolean canAddAgainst(Set<String> existingClassTypes) {
|
||||
for (String classType : classTypes) {
|
||||
if (existingClassTypes.contains(classType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import com.antik.DexPatcher.DexPatcher;
|
||||
import com.antik.DexPatcher.Translation.TranslationPatcher;
|
||||
import com.antik.crc32.crc32;
|
||||
import com.antik.manifest.manifestP;
|
||||
import com.antik.root.*;
|
||||
import com.antik.root.PackageM.Installer;
|
||||
import com.antik.root.PackageM.uninstaller;
|
||||
import com.antik.ui.*;
|
||||
import com.antik.utils.*;
|
||||
import com.reandroid.apk.ApkBundle;
|
||||
import com.reandroid.apk.ApkModule;
|
||||
import com.reandroid.archive.WriteProgress;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
|
||||
@@ -22,12 +25,15 @@ public class Main {
|
||||
|
||||
String inputPath = null;
|
||||
String translatePath = null;
|
||||
boolean R_mod = false;
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if ("-i".equals(args[i]) && i + 1 < args.length) {
|
||||
inputPath = args[i + 1];
|
||||
} else if ("-t".equals(args[i]) && i + 1 < args.length) {
|
||||
translatePath = args[i + 1];
|
||||
} else if ("-r".equals(args[i])) {
|
||||
R_mod = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,37 +45,37 @@ public class Main {
|
||||
File inputApk = new File(inputPath);
|
||||
|
||||
if (!inputApk.exists()) {
|
||||
System.err.println("Input file not found: " + inputPath);
|
||||
System.err.println("Input file not found : " + inputPath);
|
||||
return;
|
||||
}
|
||||
|
||||
banner.banner();
|
||||
|
||||
File tempDir = null;
|
||||
File T_dIR = null;
|
||||
|
||||
try {
|
||||
ApkModule module;
|
||||
File mergedApkFile;
|
||||
|
||||
if (inputPath.endsWith(".apk")) {
|
||||
System.out.println("[INFO] Loading APK...");
|
||||
System.out.println("[INFO] Loading APK ");
|
||||
module = ApkModule.loadApkFile(inputApk);
|
||||
mergedApkFile = inputApk;
|
||||
} else {
|
||||
tempDir = Files.createTempDirectory("antik_merge").toFile();
|
||||
System.out.println("[MERGE] Extracting APKS...");
|
||||
AntikUtils.ex_apks(inputApk, tempDir);
|
||||
T_dIR = Files.createTempDirectory("antik_merge").toFile();
|
||||
System.out.println("[MERGE] Extracting APKS ");
|
||||
AntikUtils.ex_apks(inputApk, T_dIR);
|
||||
|
||||
System.out.println("[MERGE] Merging APK...");
|
||||
System.out.println("[MERGE] Merging APK ");
|
||||
ApkBundle bundle = new ApkBundle();
|
||||
bundle.loadApkDirectory(tempDir);
|
||||
bundle.loadApkDirectory(T_dIR);
|
||||
module = bundle.mergeModules();
|
||||
|
||||
System.out.println("[INFO] Patching AndroidManifest.xml");
|
||||
try {
|
||||
manifestP.patch(module);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[ERROR] Manifest patching failed: " + e.getMessage());
|
||||
System.err.println("[ERROR] Manifest patching failed : " + e.getMessage());
|
||||
}
|
||||
|
||||
String name = inputApk.getName();
|
||||
@@ -77,7 +83,7 @@ public class Main {
|
||||
name = (dot > 0 ? name.substring(0, dot) : name) + "_merged.apk";
|
||||
mergedApkFile = new File(output.get_out(inputApk, name));
|
||||
|
||||
System.out.println("[BUILD] Writing merged APK...");
|
||||
System.out.println("[BUILD] Writing merged APK ");
|
||||
loading.progress(module, mergedApkFile);
|
||||
System.out.println("[MERGE] APK merged successfully: " + mergedApkFile.getAbsolutePath());
|
||||
}
|
||||
@@ -85,7 +91,7 @@ public class Main {
|
||||
if (translatePath != null) {
|
||||
File jsonFile = new File(translatePath);
|
||||
if (jsonFile.exists()) {
|
||||
System.out.println("[INFO] Starting Translation Patch...");
|
||||
System.out.println("[INFO] Starting Translation Patch ");
|
||||
TranslationPatcher.patch(module, jsonFile);
|
||||
|
||||
String tn = mergedApkFile.getName();
|
||||
@@ -93,19 +99,18 @@ public class Main {
|
||||
tn = (td > 0 ? tn.substring(0, td) : tn) + "_translated.apk";
|
||||
File transFile = new File(output.get_out(inputApk, tn));
|
||||
|
||||
System.out.println("[BUILD] Building Translated APK...");
|
||||
System.out.println("[BUILD] Building Translated APK ");
|
||||
output.write(module, transFile);
|
||||
System.out.println("[BUILD] Translated APK built at: " + transFile.getAbsolutePath());
|
||||
System.out.println("[BUILD] Translated APK built at : " + transFile.getAbsolutePath());
|
||||
} else {
|
||||
System.err.println("[ERROR] Translation file not found: " + translatePath);
|
||||
System.err.println("[ERROR] Translation file not found : " + translatePath);
|
||||
}
|
||||
} else if (!inputPath.endsWith(".apk")) {
|
||||
// Default logging patch only if merging and no translation requested
|
||||
System.out.println("[INFO] Patching classes.dex for logging...");
|
||||
System.out.println("[INFO] Patching classes.dex for logging ");
|
||||
try {
|
||||
DexPatcher.patch(module);
|
||||
} catch (Exception e) {
|
||||
System.err.println("[ERROR] Patching failed: " + e.getMessage());
|
||||
System.err.println("[ERROR] Patching failed : " + e.getMessage());
|
||||
}
|
||||
|
||||
String pn = mergedApkFile.getName();
|
||||
@@ -113,20 +118,61 @@ public class Main {
|
||||
pn = (pd > 0 ? pn.substring(0, pd) : pn) + "_pairip.apk";
|
||||
File paiFile = new File(output.get_out(inputApk, pn));
|
||||
|
||||
System.out.println("[BUILD] Building Logging APK...");
|
||||
System.out.println("[BUILD] Building Logging APK ");
|
||||
output.write(module, paiFile);
|
||||
crc32.patch(mergedApkFile, paiFile);
|
||||
System.out.println("[BUILD] Logging APK built at: " + paiFile.getAbsolutePath());
|
||||
System.out.println("[BUILD] Logging APK built at : " + paiFile.getAbsolutePath());
|
||||
|
||||
if (R_mod) {
|
||||
String pkg = module.getPackageName();
|
||||
System.out.println("[ROOT] Starting Root Mode for package : " + pkg);
|
||||
uninstaller.uninstall(pkg);
|
||||
Installer.install(paiFile);
|
||||
|
||||
if (T_dIR != null) {
|
||||
File originalBase = new File(T_dIR, "base.apk");
|
||||
if (originalBase.exists()) {
|
||||
Base.replaceBase(pkg, originalBase);
|
||||
launchApp.launchApp(pkg);
|
||||
|
||||
File pulledJson = new File(output.get_out(inputApk, "pairip.json"));
|
||||
if (Pull.waitForFileAndPull(pkg, "dictionary/pairip.json", pulledJson)) {
|
||||
System.out.println("[ROOT] pairip.json captured!");
|
||||
Runtime.getRuntime().exec(new String[]{"su", "-c", "am force-stop " + pkg}).waitFor();
|
||||
uninstaller.uninstall(pkg);
|
||||
|
||||
System.out.println("[INFO] Reloading clean merged APK for final patching ");
|
||||
module = ApkModule.loadApkFile(mergedApkFile);
|
||||
|
||||
System.out.println("[INFO] Starting Translation Patch with captured JSON ");
|
||||
TranslationPatcher.patch(module, pulledJson);
|
||||
|
||||
String tn = mergedApkFile.getName();
|
||||
int td = tn.lastIndexOf('.');
|
||||
tn = (td > 0 ? tn.substring(0, td) : tn) + "_translated.apk";
|
||||
File transFile = new File(output.get_out(inputApk, tn));
|
||||
|
||||
System.out.println("[BUILD] Building Final Translated APK ");
|
||||
output.write(module, transFile);
|
||||
System.out.println("[BUILD] Final APK built at : " + transFile.getAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
System.err.println("[ERROR] Original base.apk not found in APKS for root replacement");
|
||||
}
|
||||
} else {
|
||||
System.out.println("[ROOT] Root mode replacement skipped (Input was not an APKS file)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("[BUILD] Process completed");
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("[ERROR] Process failed: " + e.getMessage());
|
||||
System.err.println("[ERROR] Process failed : " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (tempDir != null) {
|
||||
deleteDir.del_dir(tempDir);
|
||||
if (T_dIR != null) {
|
||||
deleteDir.del_dir(T_dIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.antik.root;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Base {
|
||||
public static void replaceBase(String _Pname, File Org) {
|
||||
try {
|
||||
System.out.println("[ROOT] Finding base.apk path {/}");
|
||||
|
||||
Process P_Proc = Runtime.getRuntime().exec( new String[]{
|
||||
"su",
|
||||
"-c",
|
||||
"pm path " + _Pname
|
||||
}
|
||||
);
|
||||
|
||||
BufferedReader ___Read = new BufferedReader(new InputStreamReader(P_Proc.getInputStream()));
|
||||
|
||||
String line = ___Read.readLine();
|
||||
P_Proc.waitFor();
|
||||
|
||||
if (line == null || !line.startsWith("package:")) {
|
||||
System.err.println("[ERROR] Package path not found : " + _Pname);
|
||||
return;
|
||||
}
|
||||
|
||||
String installedPath = line.substring(8).trim();
|
||||
|
||||
System.out.println("[ROOT] Replacing : " + installedPath);
|
||||
|
||||
Runtime.getRuntime().exec(new String[]{
|
||||
"su",
|
||||
"-c",
|
||||
"am force-stop " + _Pname
|
||||
}
|
||||
).waitFor();
|
||||
|
||||
String cmd = "cp " + Org.getAbsolutePath() + " " + installedPath + " && chmod 644 " + installedPath + " && chown system:system " + installedPath;
|
||||
|
||||
Process replaceProcess = Runtime.getRuntime().exec( new String[]{
|
||||
"su",
|
||||
"-c",
|
||||
cmd
|
||||
}
|
||||
);
|
||||
|
||||
replaceProcess.waitFor();
|
||||
|
||||
System.out.println("[ROOT] Finished ");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.antik.root.PackageM;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Installer {
|
||||
public static void install(File D_TOY) {
|
||||
try {
|
||||
System.out.println("[ROOT] Installing : " + D_TOY.getName());
|
||||
|
||||
Process p = Runtime.getRuntime().exec(new String[]{
|
||||
"su", "-c", "pm install -r -t -g " + D_TOY.getAbsolutePath()
|
||||
});
|
||||
|
||||
BufferedReader _TRead = new BufferedReader(new InputStreamReader(p.getInputStream()));
|
||||
|
||||
String LN;
|
||||
|
||||
while ((LN = _TRead.readLine()) != null) {
|
||||
System.out.println("[PM] " + LN);
|
||||
}
|
||||
|
||||
p.waitFor();
|
||||
|
||||
System.out.println("[ROOT] Finished");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.antik.root.PackageM;
|
||||
|
||||
public class uninstaller {
|
||||
|
||||
public static void uninstall(String _Pname) {
|
||||
try {
|
||||
System.out.println("[ROOT] Uninstalling: " + _Pname);
|
||||
Process p = Runtime.getRuntime().exec(new String[]{"su", "-c", "pm uninstall " + _Pname});
|
||||
p.waitFor();
|
||||
System.out.println("[ROOT] Finished");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.antik.root;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class Pull {
|
||||
public static boolean waitForFileAndPull(String packageName, String internalPath, File destination) {
|
||||
String fullPath = "/data/data/" + packageName + "/" + internalPath;
|
||||
|
||||
try {
|
||||
System.out.println("[ROOT] Waiting for: " + fullPath);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
long timeout = 60000;
|
||||
|
||||
while (System.currentTimeMillis() - startTime < timeout) {
|
||||
Process checkProcess = Runtime.getRuntime().exec(
|
||||
new String[]{
|
||||
"su",
|
||||
"-c",
|
||||
"ls " + fullPath
|
||||
}
|
||||
);
|
||||
|
||||
if (checkProcess.waitFor() == 0) {
|
||||
System.out.println("[ROOT] File found.");
|
||||
|
||||
String cmd = "cp " + fullPath + " " + destination.getAbsolutePath() + " && chmod 666 " + destination.getAbsolutePath();
|
||||
|
||||
Process C_pad = Runtime.getRuntime().exec(
|
||||
new String[]{
|
||||
"su",
|
||||
"-c",
|
||||
cmd
|
||||
}
|
||||
);
|
||||
|
||||
C_pad.waitFor();
|
||||
|
||||
System.out.println("[ROOT] Pulled successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
Thread.sleep(2000);
|
||||
}
|
||||
|
||||
System.err.println("[ERROR] Timeout ");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.antik.root;
|
||||
|
||||
public class launchApp {
|
||||
public static void launchApp(String _Pname) {
|
||||
try {
|
||||
System.out.println("[ROOT] Launching " + _Pname);
|
||||
Runtime.getRuntime().exec(new String[]{
|
||||
"su",
|
||||
"-c",
|
||||
"monkey -p " + _Pname + " -c android.intent.category.LAUNCHER 1"
|
||||
}
|
||||
).waitFor();
|
||||
|
||||
System.out.println("[ROOT] Finished ");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ public class banner {
|
||||
System.out.println("░█▀▄░█▀▀░█▀█░█▀█░▀█▀░█▀▄░▀█▀░█▀█░");
|
||||
System.out.println("░█▀▄░█▀▀░█▀▀░█▀█░░█░░█▀▄░░█░░█▀▀░");
|
||||
System.out.println("░▀░▀░▀▀▀░▀░░░▀░▀░▀▀▀░▀░▀░▀▀▀░▀░░░");
|
||||
System.out.println("Version : 1.3.10");
|
||||
System.out.println("Version : 1.5.20");
|
||||
System.out.println("--------------------------------------");
|
||||
System.out.println("Dev : Antik");
|
||||
System.out.println("Channel : https://t.me/RevDex");
|
||||
|
||||
Reference in New Issue
Block a user