mirror of
https://github.com/ispointer/RePairip.git
synced 2026-09-14 12:03:39 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19a8b07717 | ||
|
|
978a4b12ce | ||
|
|
5f9c833473 | ||
|
|
e80e6193ed | ||
|
|
2983f474ed | ||
|
|
200a33e250 | ||
|
|
2c0edca6be | ||
|
|
75a2f7d0c1 | ||
|
|
920c19047a | ||
|
|
e3eab2393a | ||
|
|
bb4f41abd4 | ||
|
|
bb20f079a0 | ||
|
|
2cc06244a7 | ||
|
|
0de666f273 | ||
|
|
6844956c06 | ||
|
|
c855539ef7 | ||
|
|
3004c4543b | ||
|
|
dc38d1dabf | ||
|
|
433ad5e6fb | ||
|
|
05dc8da620 | ||
|
|
343394df95 | ||
|
|
aa16c29c6f | ||
|
|
0b6ca5b3db | ||
|
|
f28c927a63 | ||
|
|
7c7387d032 | ||
|
|
2856eca66b |
Generated
+3
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="CHOOSE_PER_TEST" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/antik" />
|
||||
</set>
|
||||
</option>
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
|
||||
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
|
||||
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -1 +1,40 @@
|
||||
Pairip Reverse Tools
|
||||
<p align="center">
|
||||
<img src="preview/re.png" width="100" height="100" alt="RePairip logo"/>
|
||||
</p>
|
||||
|
||||
<h1 align="center">RePairip</h1>
|
||||
|
||||
<p align="center">
|
||||
RePairip is a Google加固 reverse engineering tool that can dump all VMRunner snapshots and fully repack Android applications, removing protection layers completely
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<b>Current version:</b> 1.4.14 |
|
||||
<b>Language:</b> Java 17 |
|
||||
<b>Build:</b> Gradle + Shadow Jar
|
||||
</p>
|
||||
|
||||
|
||||
## Tutorial
|
||||
|
||||
[Watch Video on RevDex.Re](https://revdex.re/input.mp4)
|
||||
|
||||
|
||||
[Watch Video on Telegram](https://t.me/RevDex/557)
|
||||
|
||||
| App UI | Dump Style |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This project is distributed under the Apache License 2.0.
|
||||
|
||||
```text
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
```
|
||||
|
||||
Copyright (C) 2026 HighCapable
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
This is a resource file for the antik project.
|
||||
Project: RePairip
|
||||
Subproject: antik
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
||||
Manifest-Version: 1.0
|
||||
Main-Class: com.antik.Main
|
||||
|
||||
@@ -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("Pairip Patcher v1.3.10")));
|
||||
builder.addInstruction(new BuilderInstruction21c(Opcode.CONST_STRING, 0, new ImmutableStringReference("RePatcher v1.6.30")));
|
||||
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,17 +4,27 @@ 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;
|
||||
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
|
||||
for (String arg : args) {
|
||||
if ("-p".equals(arg) || "--package".equals(arg)) {
|
||||
com.antik.librarySurgery.LibSurgery.main(args);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (args.length < 2) {
|
||||
help.help();
|
||||
return;
|
||||
@@ -22,12 +32,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 +52,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,82 +90,97 @@ 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...");
|
||||
writeWithProgress(module, mergedApkFile);
|
||||
System.out.println("\n[MERGE] APK merged successfully: " + mergedApkFile.getAbsolutePath());
|
||||
System.out.println("[BUILD] Writing merged APK ");
|
||||
loading.progress(module, mergedApkFile);
|
||||
System.out.println("[MERGE] APK merged successfully: " + mergedApkFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
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();
|
||||
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 Translated APK...");
|
||||
writeSilently(module, transFile);
|
||||
System.out.println("[BUILD] Translated APK built at: " + transFile.getAbsolutePath());
|
||||
} else {
|
||||
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...");
|
||||
File transFile = new File(output.get_out(inputApk, tn));
|
||||
|
||||
System.out.println("[BUILD] Building Translated APK ");
|
||||
output.write(module, transFile);
|
||||
System.out.println("[BUILD] Translated APK built at : " + transFile.getAbsolutePath());
|
||||
} else {
|
||||
System.err.println("[ERROR] Translation file not found : " + translatePath);
|
||||
}
|
||||
} else if (!inputPath.endsWith(".apk")) {
|
||||
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();
|
||||
int pd = pn.lastIndexOf('.');
|
||||
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...");
|
||||
writeSilently(module, paiFile);
|
||||
crc32.patch(mergedApkFile, paiFile);
|
||||
System.out.println("[BUILD] Logging APK built at: " + paiFile.getAbsolutePath());
|
||||
}
|
||||
File paiFile = new File(output.get_out(inputApk, pn));
|
||||
|
||||
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());
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeWithProgress(ApkModule module, File file) throws IOException {
|
||||
int total = module.getZipEntryMap().size();
|
||||
if (module.hasTableBlock()) {
|
||||
total--;
|
||||
}
|
||||
int[] count = {0};
|
||||
final int finalTotal = total;
|
||||
module.writeApk(file, new WriteProgress() {
|
||||
@Override
|
||||
public void onCompressFile(String path, int method, long length) {
|
||||
count[0]++;
|
||||
loading.progress(count[0], finalTotal);
|
||||
}
|
||||
});
|
||||
loading.progress(finalTotal, finalTotal);
|
||||
}
|
||||
|
||||
private static void writeSilently(ApkModule module, File file) throws IOException {
|
||||
module.writeApk(file, new WriteProgress() {
|
||||
@Override
|
||||
public void onCompressFile(String path, int method, long length) {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.antik.librarySurgery;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
public static void deleteDirectory(File dir) {
|
||||
if (dir == null || !dir.exists()) return;
|
||||
File[] files = dir.listFiles();
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
if (f.isDirectory()) {
|
||||
deleteDirectory(f);
|
||||
} else {
|
||||
if (!f.delete()) {
|
||||
f.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean deleted = dir.delete();
|
||||
if (!deleted) {
|
||||
dir.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.antik.librarySurgery;
|
||||
|
||||
import com.antik.librarySurgery.memory.Memory;
|
||||
import com.antik.librarySurgery.memory.MemoryRegion;
|
||||
import com.antik.librarySurgery.process.ProcessFinder;
|
||||
import com.antik.ui.banner;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class LibSurgery {
|
||||
|
||||
public static void process(String packageName, String outDirStr) {
|
||||
banner.banner();
|
||||
|
||||
File outDir = new File(outDirStr);
|
||||
if (!outDir.exists()) {
|
||||
boolean created = outDir.mkdirs();
|
||||
if (!created && !outDir.exists()) {
|
||||
System.err.println("[ERROR] Could not create output directory : " + outDirStr);
|
||||
}
|
||||
}
|
||||
|
||||
File tmpDir = null;
|
||||
try {
|
||||
tmpDir = Files.createTempDirectory("pairip_surgery_").toFile();
|
||||
|
||||
List<File> apks = LibraryExtractor.getInstalledApks(packageName);
|
||||
Map<String, File> extractedLibs = LibraryExtractor.extractArm64Libs(apks, tmpDir);
|
||||
|
||||
String pid = ProcessFinder.waitForProcess(packageName);
|
||||
|
||||
MemoryRegion<Long> rxRegion = Memory.findRxRegion(pid);
|
||||
String rxAddr = rxRegion != null ? Long.toHexString(rxRegion.getStartAddress()) : null;
|
||||
String dumpedPath = rxRegion != null ? rxRegion.getPathname() : "";
|
||||
String dumpedLibName = !dumpedPath.isEmpty() ? new File(dumpedPath).getName() : "";
|
||||
|
||||
byte[] memDumpData = Memory.dumpMemory(pid, rxAddr);
|
||||
|
||||
int patchedCount = 0;
|
||||
for (Map.Entry<String, File> entry : extractedLibs.entrySet()) {
|
||||
String libName = entry.getKey();
|
||||
File libPath = entry.getValue();
|
||||
|
||||
if ("libpairipcore.so".equals(libName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] data = Files.readAllBytes(libPath.toPath());
|
||||
byte[] dumpToUse = (dumpedLibName.isEmpty() || libName.equalsIgnoreCase(dumpedLibName) || dumpedLibName.contains(libName))
|
||||
? memDumpData : null;
|
||||
|
||||
PatchResult res = LibraryPatcher.patchSingleLib(data, dumpToUse);
|
||||
if (res.success()) {
|
||||
String outName = libName.replace(".so", "") + "_patched.so";
|
||||
File outPath = new File(outDir, outName);
|
||||
Files.write(outPath.toPath(), data);
|
||||
System.out.println("[BUILD] " + libName + " -> Saved " + outName + " (" + data.length + " bytes)");
|
||||
patchedCount++;
|
||||
} else {
|
||||
System.out.println("[SKIP] " + libName + " : " + res.message());
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("[BUILD] Process completed (" + patchedCount + " libraries patched)");
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("[ERROR] Process failed : " + e.getMessage());
|
||||
} finally {
|
||||
if (tmpDir != null) {
|
||||
FileUtils.deleteDirectory(tmpDir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
String packageName = null;
|
||||
String outDir = null;
|
||||
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if (("-p".equals(args[i]) || "--package".equals(args[i])) && i + 1 < args.length) {
|
||||
packageName = args[++i];
|
||||
} else if (("-o".equals(args[i]) || "--outdir".equals(args[i])) && i + 1 < args.length) {
|
||||
outDir = args[++i];
|
||||
}
|
||||
}
|
||||
|
||||
if (packageName == null || outDir == null) {
|
||||
System.out.println("Usage: java -jar RePairip.jar -p <package_name> -o <out_dir>");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
process(packageName, outDir);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.antik.librarySurgery;
|
||||
|
||||
import com.antik.librarySurgery.shell.Shell;
|
||||
import com.antik.librarySurgery.shell.ShellResult;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public class LibraryExtractor {
|
||||
|
||||
public static List<File> getInstalledApks(String packageName) {
|
||||
ShellResult result = Shell.runRootCmd("pm path " + packageName);
|
||||
List<File> apks = new ArrayList<>();
|
||||
if (result.isSuccess() && !result.getStdout().isEmpty()) {
|
||||
for (String line : result.getStdout().split("\r?\n")) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.startsWith("package:")) {
|
||||
String apkPath = trimmed.replace("package:", "").trim();
|
||||
File f = new File(apkPath);
|
||||
if (f.exists()) {
|
||||
apks.add(f);
|
||||
} else {
|
||||
File tmpCopy = new File("/data/local/tmp/temp_base.apk");
|
||||
Shell.runRootCmd("cp " + apkPath + " " + tmpCopy.getAbsolutePath() + " && chmod 666 " + tmpCopy.getAbsolutePath());
|
||||
if (tmpCopy.exists()) {
|
||||
apks.add(tmpCopy);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (apks.isEmpty()) {
|
||||
System.err.println("[ERROR] Package not installed or APK paths unavailable : " + packageName);
|
||||
System.exit(1);
|
||||
}
|
||||
System.out.println("[INFO] Found " + apks.size() + " APK path(s) for package : " + packageName);
|
||||
return apks;
|
||||
}
|
||||
|
||||
public static Map<String, File> extractArm64Libs(List<File> apks, File tmpDir) {
|
||||
Map<String, File> extractedLibs = new HashMap<>();
|
||||
for (File apkFile : apks) {
|
||||
try (ZipFile zip = new ZipFile(apkFile)) {
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
String fname = entry.getName();
|
||||
if (fname.endsWith(".so") && fname.contains("arm64")) {
|
||||
String libName = new File(fname).getName();
|
||||
if (!extractedLibs.containsKey(libName)) {
|
||||
File extPath = new File(tmpDir, libName);
|
||||
try (InputStream is = zip.getInputStream(entry);
|
||||
OutputStream os = new FileOutputStream(extPath)) {
|
||||
is.transferTo(os);
|
||||
}
|
||||
extractedLibs.put(libName, extPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("[ERROR] Extracting native libs failed : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
System.out.println("[INFO] Extracted " + extractedLibs.size() + " ARM64 native library/libraries");
|
||||
return extractedLibs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.antik.librarySurgery;
|
||||
|
||||
import com.antik.librarySurgery.elf.DynamicEntry;
|
||||
import com.antik.librarySurgery.elf.Elf;
|
||||
import com.antik.librarySurgery.elf.ElfConstants;
|
||||
import com.antik.librarySurgery.elf.ProgramHeader;
|
||||
import com.antik.librarySurgery.io.ByteReader;
|
||||
import com.antik.librarySurgery.io.ByteWriter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class LibraryPatcher {
|
||||
|
||||
public static PatchResult patchSingleLib(byte[] data, byte[] memDump) {
|
||||
Elf elf = new Elf(data);
|
||||
if (!elf.getHeader().isValid()) {
|
||||
return new PatchResult(false, "Invalid ELF header");
|
||||
}
|
||||
|
||||
ProgramHeader dynamicHeader = elf.getDynamicHeader();
|
||||
if (dynamicHeader == null) {
|
||||
return new PatchResult(false, "No PT_DYNAMIC header found");
|
||||
}
|
||||
|
||||
ByteReader reader = elf.getReader();
|
||||
ByteWriter writer = new ByteWriter(data);
|
||||
|
||||
int dynOff = (int) dynamicHeader.getPOffset();
|
||||
int dynSz = (int) dynamicHeader.getPFileSz();
|
||||
|
||||
Long initVa = null;
|
||||
Long initArrayVa = null;
|
||||
Long initArraySz = null;
|
||||
List<DynamicEntry> newEntries = new ArrayList<>();
|
||||
boolean hasPairip = false;
|
||||
|
||||
for (int j = 0; j < dynSz; j += 16) {
|
||||
int entryPos = dynOff + j;
|
||||
if (entryPos + 16 > reader.length()) break;
|
||||
long tag = reader.readUInt64(entryPos);
|
||||
long val = reader.readUInt64(entryPos + 8);
|
||||
|
||||
if (tag == ElfConstants.DT_NULL) {
|
||||
break;
|
||||
}
|
||||
if (tag == ElfConstants.DT_INIT) {
|
||||
initVa = val;
|
||||
continue;
|
||||
}
|
||||
if (tag == ElfConstants.DT_INIT_ARRAY) {
|
||||
initArrayVa = val;
|
||||
continue;
|
||||
}
|
||||
if (tag == ElfConstants.DT_INIT_ARRAYSZ) {
|
||||
initArraySz = val;
|
||||
continue;
|
||||
}
|
||||
if (tag == ElfConstants.DT_NEEDED) {
|
||||
String name = elf.readStringFromStrtab(val);
|
||||
if (name.toLowerCase().contains("pairip")) {
|
||||
hasPairip = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
newEntries.add(new DynamicEntry(tag, val));
|
||||
}
|
||||
|
||||
int posStr = reader.indexOf(ElfConstants.OLD_EXECUTE_PROGRAM);
|
||||
boolean isPairipProtected = hasPairip || posStr != -1;
|
||||
|
||||
if (!isPairipProtected) {
|
||||
return new PatchResult(false, "Not protected by PairIP");
|
||||
}
|
||||
|
||||
newEntries.add(new DynamicEntry(ElfConstants.DT_NULL, 0L));
|
||||
|
||||
for (int k = 0; k < newEntries.size(); k++) {
|
||||
int pos = dynOff + k * 16;
|
||||
if (pos + 16 <= reader.length()) {
|
||||
writer.writeUInt64(pos, newEntries.get(k).getTag());
|
||||
writer.writeUInt64(pos + 8, newEntries.get(k).getVal());
|
||||
}
|
||||
}
|
||||
|
||||
writer.zeroRange(dynOff + newEntries.size() * 16, dynOff + dynSz);
|
||||
|
||||
if (initVa != null) {
|
||||
for (ProgramHeader ph : elf.getProgramHeaders()) {
|
||||
if (ph.getPType() == ElfConstants.PT_LOAD && ph.getPVaddr() <= initVa && initVa < ph.getPVaddr() + ph.getPMemSz()) {
|
||||
writer.writeUInt32(ph.getHeaderOffset(), ElfConstants.PT_NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (initArrayVa != null && initArraySz != null) {
|
||||
Long initArrayOff = elf.vaddrToOffset(initArrayVa);
|
||||
if (initArrayOff != null) {
|
||||
int arrayPos = initArrayOff.intValue();
|
||||
int arraySize = initArraySz.intValue();
|
||||
if (arrayPos >= 0 && arrayPos + arraySize <= reader.length()) {
|
||||
writer.zeroRange(arrayPos, arrayPos + arraySize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (posStr != -1) {
|
||||
writer.writeBytes(posStr, ElfConstants.NEW_MEMSET);
|
||||
}
|
||||
|
||||
Long jniOnLoadOffset = elf.findSymbolAddress("JNI_OnLoad");
|
||||
if (jniOnLoadOffset != null) {
|
||||
int stubPos = jniOnLoadOffset.intValue();
|
||||
if (stubPos >= 0 && stubPos + ElfConstants.ARM64_STUB_BYTES.length <= reader.length()) {
|
||||
writer.writeBytes(stubPos, ElfConstants.ARM64_STUB_BYTES);
|
||||
}
|
||||
} else {
|
||||
System.out.println("[INFO] JNI_OnLoad symbol not found in ELF, skipping stub patch safely.");
|
||||
}
|
||||
|
||||
if (memDump != null && memDump.length > 0) {
|
||||
int copyLen = Math.min(reader.length(), memDump.length);
|
||||
writer.writeBytes(0, Arrays.copyOf(memDump, copyLen));
|
||||
}
|
||||
|
||||
return new PatchResult(true, "Successfully patched");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.antik.librarySurgery;
|
||||
|
||||
public record PatchResult(boolean success, String message) {}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.antik.librarySurgery.elf;
|
||||
|
||||
public class DynamicEntry {
|
||||
|
||||
private final long tag;
|
||||
private final long val;
|
||||
|
||||
public DynamicEntry(long tag, long val) {
|
||||
this.tag = tag;
|
||||
this.val = val;
|
||||
}
|
||||
|
||||
public long getTag() {
|
||||
return tag;
|
||||
}
|
||||
|
||||
public long getVal() {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.antik.librarySurgery.elf;
|
||||
|
||||
import com.antik.librarySurgery.io.ByteReader;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Elf {
|
||||
|
||||
private final ByteReader reader;
|
||||
private final ElfHeader header;
|
||||
private final List<ProgramHeader> programHeaders = new ArrayList<>();
|
||||
private ProgramHeader dynamicHeader = null;
|
||||
private Long strtabVa = null;
|
||||
private Long strtabOff = null;
|
||||
private Long symtabVa = null;
|
||||
private Long symtabOff = null;
|
||||
|
||||
public Elf(byte[] data) {
|
||||
this.reader = new ByteReader(data);
|
||||
this.header = new ElfHeader(reader);
|
||||
|
||||
if (header.isValid()) {
|
||||
parseProgramHeaders();
|
||||
parseDynamicSection();
|
||||
}
|
||||
}
|
||||
|
||||
private void parseProgramHeaders() {
|
||||
long phOff = header.getPhOff();
|
||||
int phNum = header.getPhNum();
|
||||
for (int i = 0; i < phNum; i++) {
|
||||
int offset = (int) (phOff + i * 56L);
|
||||
if (offset + 56 > reader.length()) break;
|
||||
ProgramHeader ph = new ProgramHeader(reader, offset);
|
||||
programHeaders.add(ph);
|
||||
if (ph.getPType() == ElfConstants.PT_DYNAMIC) {
|
||||
this.dynamicHeader = ph;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseDynamicSection() {
|
||||
if (dynamicHeader == null) return;
|
||||
int dynOff = (int) dynamicHeader.getPOffset();
|
||||
int dynSz = (int) dynamicHeader.getPFileSz();
|
||||
|
||||
for (int j = 0; j < dynSz; j += 16) {
|
||||
int pos = dynOff + j;
|
||||
if (pos + 16 > reader.length()) break;
|
||||
long tag = reader.readUInt64(pos);
|
||||
long val = reader.readUInt64(pos + 8);
|
||||
if (tag == ElfConstants.DT_STRTAB) {
|
||||
this.strtabVa = val;
|
||||
} else if (tag == ElfConstants.DT_SYMTAB) {
|
||||
this.symtabVa = val;
|
||||
}
|
||||
}
|
||||
|
||||
if (strtabVa != null) {
|
||||
this.strtabOff = vaddrToOffset(strtabVa);
|
||||
}
|
||||
if (symtabVa != null) {
|
||||
this.symtabOff = vaddrToOffset(symtabVa);
|
||||
}
|
||||
}
|
||||
|
||||
public Long vaddrToOffset(long vaddr) {
|
||||
for (ProgramHeader ph : programHeaders) {
|
||||
if (ph.getPType() == ElfConstants.PT_LOAD && ph.getPVaddr() <= vaddr && vaddr < ph.getPVaddr() + ph.getPMemSz()) {
|
||||
return ph.getPOffset() + (vaddr - ph.getPVaddr());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Long findSymbolAddress(String symbolName) {
|
||||
if (symtabOff == null || strtabOff == null) return null;
|
||||
int symOff = symtabOff.intValue();
|
||||
int maxSyms = 3000;
|
||||
for (int i = 0; i < maxSyms; i++) {
|
||||
int entryPos = symOff + i * 24;
|
||||
if (entryPos + 24 > reader.length()) break;
|
||||
long nameIdx = reader.readUInt32(entryPos);
|
||||
long value = reader.readUInt64(entryPos + 8);
|
||||
if (nameIdx > 0) {
|
||||
String name = reader.readNullTerminatedString((int) (strtabOff + nameIdx));
|
||||
if (symbolName.equals(name)) {
|
||||
return vaddrToOffset(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ByteReader getReader() {
|
||||
return reader;
|
||||
}
|
||||
|
||||
public ElfHeader getHeader() {
|
||||
return header;
|
||||
}
|
||||
|
||||
public List<ProgramHeader> getProgramHeaders() {
|
||||
return programHeaders;
|
||||
}
|
||||
|
||||
public ProgramHeader getDynamicHeader() {
|
||||
return dynamicHeader;
|
||||
}
|
||||
|
||||
public Long getStrtabOff() {
|
||||
return strtabOff;
|
||||
}
|
||||
|
||||
public String readStringFromStrtab(long offsetVal) {
|
||||
if (strtabOff == null) return "";
|
||||
return reader.readNullTerminatedString((int) (strtabOff + offsetVal));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.antik.librarySurgery.elf;
|
||||
|
||||
public class ElfConstants {
|
||||
|
||||
public static final byte[] ELF_MAGIC = new byte[]{(byte) 0x7f, 'E', 'L', 'F'};
|
||||
|
||||
public static final int PT_NULL = 0;
|
||||
public static final int PT_LOAD = 1;
|
||||
public static final int PT_DYNAMIC = 2;
|
||||
|
||||
public static final long DT_NULL = 0L;
|
||||
public static final long DT_NEEDED = 1L;
|
||||
public static final long DT_STRTAB = 5L;
|
||||
public static final long DT_SYMTAB = 6L;
|
||||
public static final long DT_INIT = 12L;
|
||||
public static final long DT_INIT_ARRAY = 25L;
|
||||
public static final long DT_INIT_ARRAYSZ = 27L;
|
||||
|
||||
public static final byte[] OLD_EXECUTE_PROGRAM = "ExecuteProgram\0".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
|
||||
public static final byte[] NEW_MEMSET = "memset\0\0\0\0\0\0\0\0\0".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
|
||||
|
||||
public static final byte[] ARM64_STUB_BYTES = new byte[]{
|
||||
(byte) 0xc0, 0x00, (byte) 0x80, 0x52, // mov w0, #0
|
||||
0x20, 0x00, (byte) 0xa0, 0x72, // movk w0, #0x10, lsl #16 (JNI_VERSION_1_6 = 0x00010000)
|
||||
(byte) 0xc0, 0x03, 0x5f, (byte) 0xd6 // ret
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.antik.librarySurgery.elf;
|
||||
|
||||
import com.antik.librarySurgery.io.ByteReader;
|
||||
|
||||
public class ElfHeader {
|
||||
|
||||
private final long phOff;
|
||||
private final int phNum;
|
||||
private final boolean valid;
|
||||
|
||||
public ElfHeader(ByteReader reader) {
|
||||
if (!reader.checkMagic(ElfConstants.ELF_MAGIC)) {
|
||||
this.valid = false;
|
||||
this.phOff = 0L;
|
||||
this.phNum = 0;
|
||||
return;
|
||||
}
|
||||
this.phOff = reader.readUInt64(0x20);
|
||||
this.phNum = reader.readUInt16(0x38);
|
||||
this.valid = true;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
public long getPhOff() {
|
||||
return phOff;
|
||||
}
|
||||
|
||||
public int getPhNum() {
|
||||
return phNum;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.antik.librarySurgery.elf;
|
||||
|
||||
import com.antik.librarySurgery.io.ByteReader;
|
||||
|
||||
public class ProgramHeader {
|
||||
|
||||
private final int headerOffset;
|
||||
private final int pType;
|
||||
private final long pOffset;
|
||||
private final long pVaddr;
|
||||
private final long pFileSz;
|
||||
private final long pMemSz;
|
||||
|
||||
public ProgramHeader(ByteReader reader, int headerOffset) {
|
||||
this.headerOffset = headerOffset;
|
||||
this.pType = (int) reader.readUInt32(headerOffset);
|
||||
this.pOffset = reader.readUInt64(headerOffset + 8);
|
||||
this.pVaddr = reader.readUInt64(headerOffset + 16);
|
||||
this.pFileSz = reader.readUInt64(headerOffset + 32);
|
||||
this.pMemSz = reader.readUInt64(headerOffset + 40);
|
||||
}
|
||||
|
||||
public int getHeaderOffset() {
|
||||
return headerOffset;
|
||||
}
|
||||
|
||||
public int getPType() {
|
||||
return pType;
|
||||
}
|
||||
|
||||
public long getPOffset() {
|
||||
return pOffset;
|
||||
}
|
||||
|
||||
public long getPVaddr() {
|
||||
return pVaddr;
|
||||
}
|
||||
|
||||
public long getPFileSz() {
|
||||
return pFileSz;
|
||||
}
|
||||
|
||||
public long getPMemSz() {
|
||||
return pMemSz;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.antik.librarySurgery.io;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ByteReader {
|
||||
|
||||
private final byte[] buffer;
|
||||
private final ByteBuffer byteBuffer;
|
||||
|
||||
public ByteReader(byte[] buffer) {
|
||||
this.buffer = buffer;
|
||||
this.byteBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
public byte[] getBuffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return buffer.length;
|
||||
}
|
||||
|
||||
public int readUInt16(int offset) {
|
||||
if (offset < 0 || offset + 2 > buffer.length) return 0;
|
||||
return byteBuffer.getShort(offset) & 0xFFFF;
|
||||
}
|
||||
|
||||
public long readUInt32(int offset) {
|
||||
if (offset < 0 || offset + 4 > buffer.length) return 0L;
|
||||
return byteBuffer.getInt(offset) & 0xFFFFFFFFL;
|
||||
}
|
||||
|
||||
public long readUInt64(int offset) {
|
||||
if (offset < 0 || offset + 8 > buffer.length) return 0L;
|
||||
return byteBuffer.getLong(offset);
|
||||
}
|
||||
|
||||
public String readNullTerminatedString(int offset) {
|
||||
if (offset < 0 || offset >= buffer.length) return "";
|
||||
int end = offset;
|
||||
while (end < buffer.length && buffer[end] != 0) {
|
||||
end++;
|
||||
}
|
||||
return new String(buffer, offset, end - offset, StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
public int indexOf(byte[] target) {
|
||||
if (target == null || target.length == 0) return -1;
|
||||
outer:
|
||||
for (int i = 0; i <= buffer.length - target.length; i++) {
|
||||
for (int j = 0; j < target.length; j++) {
|
||||
if (buffer[i + j] != target[j]) {
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public boolean checkMagic(byte[] magic) {
|
||||
if (magic == null || buffer.length < magic.length) return false;
|
||||
for (int i = 0; i < magic.length; i++) {
|
||||
if (buffer[i] != magic[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.antik.librarySurgery.io;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
public class ByteWriter {
|
||||
|
||||
private final byte[] buffer;
|
||||
private final ByteBuffer byteBuffer;
|
||||
|
||||
public ByteWriter(byte[] buffer) {
|
||||
this.buffer = buffer;
|
||||
this.byteBuffer = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
public byte[] getBuffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void writeUInt16(int offset, int value) {
|
||||
if (offset >= 0 && offset + 2 <= buffer.length) {
|
||||
byteBuffer.putShort(offset, (short) value);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeUInt32(int offset, long value) {
|
||||
if (offset >= 0 && offset + 4 <= buffer.length) {
|
||||
byteBuffer.putInt(offset, (int) value);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeUInt64(int offset, long value) {
|
||||
if (offset >= 0 && offset + 8 <= buffer.length) {
|
||||
byteBuffer.putLong(offset, value);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeBytes(int offset, byte[] data) {
|
||||
if (data != null && offset >= 0 && offset + data.length <= buffer.length) {
|
||||
System.arraycopy(data, 0, buffer, offset, data.length);
|
||||
}
|
||||
}
|
||||
|
||||
public void zeroRange(int startOffset, int endOffset) {
|
||||
if (startOffset >= 0 && endOffset <= buffer.length && startOffset < endOffset) {
|
||||
for (int i = startOffset; i < endOffset; i++) {
|
||||
buffer[i] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.antik.librarySurgery.memory;
|
||||
|
||||
import com.antik.librarySurgery.shell.Shell;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
|
||||
public class Memory {
|
||||
|
||||
public static MemoryRegion<Long> findRxRegion(String pid) {
|
||||
MemoryMap map = MemoryMap.parseFromProcess(pid);
|
||||
return map.findRxRegion();
|
||||
}
|
||||
|
||||
public static byte[] dumpMemory(String pid, String rxAddr) {
|
||||
if (rxAddr == null || rxAddr.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String dumpPath = "/data/local/tmp/pairip_dump.bin";
|
||||
System.out.println("[ROOT] Dumping decrypted .text memory from RAM (Address 0x" + rxAddr + ")");
|
||||
Shell.runRootCmd("dd if=/proc/" + pid + "/mem of=" + dumpPath + " bs=1024 count=576 skip=$((0x" + rxAddr + "/1024))");
|
||||
|
||||
File dumpFile = new File(dumpPath);
|
||||
byte[] data = null;
|
||||
if (dumpFile.exists() && dumpFile.length() > 0) {
|
||||
try {
|
||||
data = Files.readAllBytes(dumpFile.toPath());
|
||||
System.out.println("[ROOT] Captured " + data.length + " bytes decrypted memory dump");
|
||||
} catch (Exception e) {
|
||||
System.err.println("[ERROR] Reading memory dump failed : " + e.getMessage());
|
||||
}
|
||||
}
|
||||
Shell.runRootCmd("rm -f " + dumpPath);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.antik.librarySurgery.memory;
|
||||
|
||||
import com.antik.librarySurgery.shell.Shell;
|
||||
import com.antik.librarySurgery.shell.ShellResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MemoryMap {
|
||||
|
||||
private final List<MemoryRegion<Long>> regions = new ArrayList<>();
|
||||
|
||||
public static MemoryMap parseFromProcess(String pid) {
|
||||
MemoryMap map = new MemoryMap();
|
||||
ShellResult result = Shell.runRootCmd("grep 'r-xp' /proc/" + pid + "/maps");
|
||||
if (!result.getStdout().isEmpty()) {
|
||||
for (String line : result.getStdout().split("\r?\n")) {
|
||||
String trimmed = line.trim();
|
||||
String[] parts = trimmed.split("\\s+");
|
||||
if (parts.length >= 1) {
|
||||
String[] addrs = parts[0].split("-");
|
||||
if (addrs.length == 2) {
|
||||
try {
|
||||
long start = Long.parseUnsignedLong(addrs[0], 16);
|
||||
long end = Long.parseUnsignedLong(addrs[1], 16);
|
||||
String perms = parts.length > 1 ? parts[1] : "";
|
||||
String path = parts.length > 5 ? parts[5] : "";
|
||||
map.regions.add(new MemoryRegion<>(start, end, perms, path));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
public List<MemoryRegion<Long>> getRegions() {
|
||||
return regions;
|
||||
}
|
||||
|
||||
public MemoryRegion<Long> findRxRegion() {
|
||||
for (MemoryRegion<Long> region : regions) {
|
||||
String path = region.getPathname();
|
||||
if (region.isExecutable() && (path.contains("apk") || path.contains("lib"))) {
|
||||
return region;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public String findRxAddressString() {
|
||||
MemoryRegion<Long> region = findRxRegion();
|
||||
if (region != null) {
|
||||
return Long.toHexString(region.getStartAddress());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.antik.librarySurgery.memory;
|
||||
|
||||
public class MemoryRegion<T extends Number> {
|
||||
|
||||
private final T startAddress;
|
||||
private final T endAddress;
|
||||
private final String permissions;
|
||||
private final String pathname;
|
||||
|
||||
public MemoryRegion(T startAddress, T endAddress, String permissions, String pathname) {
|
||||
this.startAddress = startAddress;
|
||||
this.endAddress = endAddress;
|
||||
this.permissions = permissions;
|
||||
this.pathname = pathname;
|
||||
}
|
||||
|
||||
public T getStartAddress() {
|
||||
return startAddress;
|
||||
}
|
||||
|
||||
public T getEndAddress() {
|
||||
return endAddress;
|
||||
}
|
||||
|
||||
public String getPermissions() {
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public String getPathname() {
|
||||
return pathname;
|
||||
}
|
||||
|
||||
public boolean isExecutable() {
|
||||
return permissions != null && permissions.contains("x");
|
||||
}
|
||||
|
||||
public boolean isReadable() {
|
||||
return permissions != null && permissions.contains("r");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.antik.librarySurgery.process;
|
||||
|
||||
import com.antik.librarySurgery.shell.Shell;
|
||||
import com.antik.librarySurgery.shell.ShellResult;
|
||||
|
||||
public class ProcessFinder {
|
||||
|
||||
public static String getPidForPackage(String packageName) {
|
||||
ShellResult result = Shell.runRootCmd("ps -A | grep " + packageName);
|
||||
if (!result.isSuccess() || result.getStdout().isEmpty()) {
|
||||
result = Shell.runRootCmd("pidof " + packageName);
|
||||
if (result.isSuccess() && !result.getStdout().isEmpty()) {
|
||||
String[] pids = result.getStdout().trim().split("\\s+");
|
||||
return pids[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
for (String line : result.getStdout().split("\r?\n")) {
|
||||
String trimmed = line.trim();
|
||||
if (trimmed.contains(packageName)) {
|
||||
String[] parts = trimmed.split("\\s+");
|
||||
if (parts.length >= 2) {
|
||||
return parts[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String waitForProcess(String packageName) {
|
||||
System.out.println("[INFO] Waiting for package : " + packageName);
|
||||
System.out.println("[INFO] Please launch the app on your device ");
|
||||
|
||||
while (true) {
|
||||
String pid = getPidForPackage(packageName);
|
||||
if (pid != null && !pid.isEmpty()) {
|
||||
System.out.println("[ROOT] App process detected with PID : " + pid);
|
||||
return pid;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(1500);
|
||||
} catch (InterruptedException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.antik.librarySurgery.shell;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public class Shell {
|
||||
|
||||
public static ShellResult runRootCmd(String command) {
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{"su", "-c", command});
|
||||
BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
|
||||
|
||||
StringBuilder outSb = new StringBuilder();
|
||||
for (String line = outReader.readLine(); line != null; line = outReader.readLine()) {
|
||||
outSb.append(line).append("\n");
|
||||
}
|
||||
|
||||
StringBuilder errSb = new StringBuilder();
|
||||
for (String line = errReader.readLine(); line != null; line = errReader.readLine()) {
|
||||
errSb.append(line).append("\n");
|
||||
}
|
||||
|
||||
int exitCode = process.waitFor();
|
||||
return new ShellResult(exitCode, outSb.toString(), errSb.toString());
|
||||
} catch (Exception e) {
|
||||
return new ShellResult(-1, "", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static ShellResult runCmd(String command) {
|
||||
try {
|
||||
Process process = Runtime.getRuntime().exec(new String[]{"sh", "-c", command});
|
||||
BufferedReader outReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
|
||||
StringBuilder outSb = new StringBuilder();
|
||||
for (String line = outReader.readLine(); line != null; line = outReader.readLine()) {
|
||||
outSb.append(line).append("\n");
|
||||
}
|
||||
int exitCode = process.waitFor();
|
||||
return new ShellResult(exitCode, outSb.toString(), "");
|
||||
} catch (Exception e) {
|
||||
return runRootCmd(command);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.antik.librarySurgery.shell;
|
||||
|
||||
public class ShellResult {
|
||||
|
||||
private final int exitCode;
|
||||
private final String stdout;
|
||||
private final String stderr;
|
||||
|
||||
public ShellResult(int exitCode, String stdout, String stderr) {
|
||||
this.exitCode = exitCode;
|
||||
this.stdout = stdout != null ? stdout.trim() : "";
|
||||
this.stderr = stderr != null ? stderr.trim() : "";
|
||||
}
|
||||
|
||||
public int getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public String getStdout() {
|
||||
return stdout;
|
||||
}
|
||||
|
||||
public String getStderr() {
|
||||
return stderr;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
return exitCode == 0;
|
||||
}
|
||||
}
|
||||
@@ -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,11 +8,11 @@ public class banner {
|
||||
System.out.println("░█▀▄░█▀▀░█▀█░█▀█░▀█▀░█▀▄░▀█▀░█▀█░");
|
||||
System.out.println("░█▀▄░█▀▀░█▀▀░█▀█░░█░░█▀▄░░█░░█▀▀░");
|
||||
System.out.println("░▀░▀░▀▀▀░▀░░░▀░▀░▀▀▀░▀░▀░▀▀▀░▀░░░");
|
||||
System.out.println("Version : 1.3.10");
|
||||
System.out.println("Version : 1.6.30");
|
||||
System.out.println("--------------------------------------");
|
||||
System.out.println("Dev : Antik (Fox)");
|
||||
System.out.println("Dev : Antik");
|
||||
System.out.println("Channel : https://t.me/RevDex");
|
||||
System.out.println("GitHub : https://github.com/ispointer/RePairip");
|
||||
System.out.println("GitHub : https://github.com/ispointer/RePairip");
|
||||
System.out.println("--------------------------------------");
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user