feat: custom zip reader implementation to fight tampering
fix(zip): use size info from CD if LFH entry is incorrect refactor: move custom zip implementation into new module feat: move ZipSecurity into jadx-zip module
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
package jadx.cli;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.security.JadxSecurityFlag;
|
||||
import jadx.api.security.impl.JadxSecurity;
|
||||
import jadx.commons.app.JadxCommonEnv;
|
||||
import jadx.zip.security.DisabledZipSecurity;
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
import jadx.zip.security.JadxZipSecurity;
|
||||
|
||||
public class JadxAppCommon {
|
||||
|
||||
public static void applyEnvVars(JadxArgs jadxArgs) {
|
||||
Set<JadxSecurityFlag> flags = JadxSecurityFlag.all();
|
||||
IJadxZipSecurity zipSecurity;
|
||||
|
||||
boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false);
|
||||
if (disableXmlSecurity) {
|
||||
flags.remove(JadxSecurityFlag.SECURE_XML_PARSER);
|
||||
// TODO: not related to 'xml security', but kept for compatibility
|
||||
flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE);
|
||||
}
|
||||
|
||||
boolean disableZipSecurity = JadxCommonEnv.getBool("JADX_DISABLE_ZIP_SECURITY", false);
|
||||
if (disableZipSecurity) {
|
||||
flags.remove(JadxSecurityFlag.SECURE_ZIP_READER);
|
||||
zipSecurity = DisabledZipSecurity.INSTANCE;
|
||||
} else {
|
||||
JadxZipSecurity jadxZipSecurity = new JadxZipSecurity();
|
||||
int maxZipEntriesCount = JadxCommonEnv.getInt("JADX_ZIP_MAX_ENTRIES_COUNT", -2);
|
||||
if (maxZipEntriesCount != -2) {
|
||||
jadxZipSecurity.setMaxEntriesCount(maxZipEntriesCount);
|
||||
}
|
||||
int zipBombMinUncompressedSize = JadxCommonEnv.getInt("JADX_ZIP_BOMB_MIN_UNCOMPRESSED_SIZE", -2);
|
||||
if (zipBombMinUncompressedSize != -2) {
|
||||
jadxZipSecurity.setZipBombMinUncompressedSize(zipBombMinUncompressedSize);
|
||||
}
|
||||
int setZipBombDetectionFactor = JadxCommonEnv.getInt("JADX_ZIP_BOMB_DETECTION_FACTOR", -2);
|
||||
if (setZipBombDetectionFactor != -2) {
|
||||
jadxZipSecurity.setZipBombDetectionFactor(setZipBombDetectionFactor);
|
||||
}
|
||||
zipSecurity = jadxZipSecurity;
|
||||
}
|
||||
jadxArgs.setSecurity(new JadxSecurity(flags, zipSecurity));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package jadx.cli;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -10,11 +8,8 @@ import jadx.api.JadxDecompiler;
|
||||
import jadx.api.impl.AnnotatedCodeWriter;
|
||||
import jadx.api.impl.NoOpCodeCache;
|
||||
import jadx.api.impl.SimpleCodeWriter;
|
||||
import jadx.api.security.JadxSecurityFlag;
|
||||
import jadx.api.security.impl.JadxSecurity;
|
||||
import jadx.cli.LogHelper.LogLevelEnum;
|
||||
import jadx.cli.plugins.JadxFilesGetter;
|
||||
import jadx.commons.app.JadxCommonEnv;
|
||||
import jadx.core.utils.exceptions.JadxArgsValidateException;
|
||||
import jadx.plugins.tools.JadxExternalPluginsLoader;
|
||||
|
||||
@@ -54,7 +49,7 @@ public class JadxCLI {
|
||||
jadxArgs.setPluginLoader(new JadxExternalPluginsLoader());
|
||||
jadxArgs.setFilesGetter(JadxFilesGetter.INSTANCE);
|
||||
initCodeWriterProvider(jadxArgs);
|
||||
applyEnvVars(jadxArgs);
|
||||
JadxAppCommon.applyEnvVars(jadxArgs);
|
||||
try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) {
|
||||
jadx.load();
|
||||
if (checkForErrors(jadx)) {
|
||||
@@ -87,22 +82,6 @@ public class JadxCLI {
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyEnvVars(JadxArgs jadxArgs) {
|
||||
Set<JadxSecurityFlag> flags = JadxSecurityFlag.all();
|
||||
boolean modified = false;
|
||||
boolean disableXmlSecurity = JadxCommonEnv.getBool("JADX_DISABLE_XML_SECURITY", false);
|
||||
if (disableXmlSecurity) {
|
||||
flags.remove(JadxSecurityFlag.SECURE_XML_PARSER);
|
||||
// TODO: not related to 'xml security', but kept for compatibility
|
||||
flags.remove(JadxSecurityFlag.VERIFY_APP_PACKAGE);
|
||||
modified = true;
|
||||
}
|
||||
// TODO: migrate 'ZipSecurity'
|
||||
if (modified) {
|
||||
jadxArgs.setSecurity(new JadxSecurity(flags));
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean checkForErrors(JadxDecompiler jadx) {
|
||||
if (jadx.getRoot().getClasses().isEmpty()) {
|
||||
if (jadx.getArgs().isSkipResources()) {
|
||||
|
||||
@@ -10,7 +10,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -18,8 +17,10 @@ import org.slf4j.LoggerFactory;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.android.TextResMapFile;
|
||||
import jadx.core.utils.files.ZipFile;
|
||||
import jadx.core.xmlgen.ResTableBinaryParser;
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.ZipContent;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
import static jadx.core.utils.files.FileUtils.expandDirs;
|
||||
|
||||
@@ -53,18 +54,19 @@ public class ConvertArscFile {
|
||||
LOG.info("Input entries count: {}", resMap.size());
|
||||
|
||||
RootNode root = new RootNode(new JadxArgs()); // not really needed
|
||||
ZipReader zipReader = new ZipReader();
|
||||
rewritesCount = 0;
|
||||
for (Path resFile : inputResFiles) {
|
||||
ResTableBinaryParser resTableParser = new ResTableBinaryParser(root, true);
|
||||
if (resFile.getFileName().toString().endsWith(".jar")) {
|
||||
// Load resources.arsc from android.jar
|
||||
try (ZipFile zip = new ZipFile(resFile.toFile())) {
|
||||
ZipEntry entry = zip.getEntry("resources.arsc");
|
||||
try (ZipContent zip = zipReader.open(resFile.toFile())) {
|
||||
IZipEntry entry = zip.searchEntry("resources.arsc");
|
||||
if (entry == null) {
|
||||
LOG.error("Failed to load \"resources.arsc\" from {}", resFile);
|
||||
continue;
|
||||
}
|
||||
try (InputStream inputStream = zip.getInputStream(entry)) {
|
||||
try (InputStream inputStream = entry.getInputStream()) {
|
||||
resTableParser.decode(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## jadx zip
|
||||
|
||||
Custom zip reader implementation to fight tampering and provide additional security checks
|
||||
@@ -0,0 +1,3 @@
|
||||
plugins {
|
||||
id("jadx-library")
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package jadx.zip;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface IZipEntry {
|
||||
|
||||
/**
|
||||
* Zip entry name
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Uncompressed bytes
|
||||
*/
|
||||
byte[] getBytes();
|
||||
|
||||
/**
|
||||
* Stream of uncompressed bytes.
|
||||
*/
|
||||
InputStream getInputStream();
|
||||
|
||||
long getCompressedSize();
|
||||
|
||||
long getUncompressedSize();
|
||||
|
||||
boolean isDirectory();
|
||||
|
||||
File getZipFile();
|
||||
|
||||
/**
|
||||
* Return true if {@link #getBytes()} method is more optimal to use other than
|
||||
* {@link #getInputStream()}
|
||||
*/
|
||||
boolean preferBytes();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package jadx.zip;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
|
||||
public interface IZipParser extends Closeable {
|
||||
|
||||
ZipContent open() throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package jadx.zip;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class ZipContent implements Closeable {
|
||||
private final IZipParser zipParser;
|
||||
private final List<IZipEntry> entries;
|
||||
private final Map<String, IZipEntry> entriesMap;
|
||||
|
||||
public ZipContent(IZipParser zipParser, List<IZipEntry> entries) {
|
||||
this.zipParser = zipParser;
|
||||
this.entries = entries;
|
||||
this.entriesMap = entries.stream().collect(Collectors.toMap(IZipEntry::getName, Function.identity()));
|
||||
}
|
||||
|
||||
public List<IZipEntry> getEntries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
public @Nullable IZipEntry searchEntry(String fileName) {
|
||||
return entriesMap.get(fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
zipParser.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package jadx.zip;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import jadx.zip.fallback.FallbackZipParser;
|
||||
import jadx.zip.parser.JadxZipParser;
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
import jadx.zip.security.JadxZipSecurity;
|
||||
|
||||
/**
|
||||
* Jadx wrapper to provide custom zip parser ({@link JadxZipParser})
|
||||
* with fallback to default Java implementation.
|
||||
*/
|
||||
public class ZipReader {
|
||||
private final ZipReaderOptions options;
|
||||
|
||||
public ZipReader() {
|
||||
this(ZipReaderOptions.getDefault());
|
||||
}
|
||||
|
||||
public ZipReader(Set<ZipReaderFlags> flags) {
|
||||
this(new ZipReaderOptions(new JadxZipSecurity(), flags));
|
||||
}
|
||||
|
||||
public ZipReader(IJadxZipSecurity security) {
|
||||
this(new ZipReaderOptions(security, ZipReaderFlags.none()));
|
||||
}
|
||||
|
||||
public ZipReader(ZipReaderOptions options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
public ZipContent open(File zipFile) throws IOException {
|
||||
try {
|
||||
JadxZipParser jadxParser = new JadxZipParser(zipFile, options);
|
||||
IZipParser detectedParser = detectParser(zipFile, jadxParser);
|
||||
if (detectedParser != jadxParser) {
|
||||
jadxParser.close();
|
||||
}
|
||||
return detectedParser.open();
|
||||
} catch (Exception e) {
|
||||
if (options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
|
||||
throw new IOException("Failed to open zip: " + zipFile, e);
|
||||
}
|
||||
// switch to fallback parser
|
||||
return buildFallbackParser(zipFile).open();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit valid entries in a zip file.
|
||||
* Return not null value from visitor to stop iteration.
|
||||
*/
|
||||
public <R> @Nullable R visitEntries(File file, Function<IZipEntry, R> visitor) {
|
||||
try (ZipContent content = open(file)) {
|
||||
for (IZipEntry entry : content.getEntries()) {
|
||||
R result = visitor.apply(entry);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process zip file: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void readEntries(File file, BiConsumer<IZipEntry, InputStream> visitor) {
|
||||
visitEntries(file, entry -> {
|
||||
if (!entry.isDirectory()) {
|
||||
try (InputStream in = entry.getInputStream()) {
|
||||
visitor.accept(entry, in);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to process zip entry: " + entry, e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public ZipReaderOptions getOptions() {
|
||||
return options;
|
||||
}
|
||||
|
||||
private IZipParser detectParser(File zipFile, JadxZipParser jadxParser) {
|
||||
if (zipFile.getName().endsWith(".apk")
|
||||
|| options.getFlags().contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
|
||||
return jadxParser;
|
||||
}
|
||||
if (!jadxParser.canOpen()) {
|
||||
return buildFallbackParser(zipFile);
|
||||
}
|
||||
// default
|
||||
if (options.getFlags().contains(ZipReaderFlags.FALLBACK_AS_DEFAULT)) {
|
||||
return buildFallbackParser(zipFile);
|
||||
}
|
||||
return jadxParser;
|
||||
}
|
||||
|
||||
private FallbackZipParser buildFallbackParser(File zipFile) {
|
||||
return new FallbackZipParser(zipFile, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package jadx.zip;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
public enum ZipReaderFlags {
|
||||
/**
|
||||
* Search all local file headers by signature without reading
|
||||
* 'central directory' and 'end of central directory' entries
|
||||
*/
|
||||
IGNORE_CENTRAL_DIR_ENTRIES,
|
||||
|
||||
/**
|
||||
* Enable additional checks to verify zip data and report possible tampering
|
||||
*/
|
||||
REPORT_TAMPERING,
|
||||
|
||||
/**
|
||||
* Use fallback (java built-in implementation) parser as default.
|
||||
* Custom implementation will be used for '*.apk' files only.
|
||||
*/
|
||||
FALLBACK_AS_DEFAULT,
|
||||
|
||||
/**
|
||||
* Use only jadx custom parser and do not switch to fallback on errors.
|
||||
*/
|
||||
DONT_USE_FALLBACK;
|
||||
|
||||
public static Set<ZipReaderFlags> none() {
|
||||
return EnumSet.noneOf(ZipReaderFlags.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package jadx.zip;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
import jadx.zip.security.JadxZipSecurity;
|
||||
|
||||
public class ZipReaderOptions {
|
||||
|
||||
public static ZipReaderOptions getDefault() {
|
||||
return new ZipReaderOptions(new JadxZipSecurity(), ZipReaderFlags.none());
|
||||
}
|
||||
|
||||
private final IJadxZipSecurity zipSecurity;
|
||||
private final Set<ZipReaderFlags> flags;
|
||||
|
||||
public ZipReaderOptions(IJadxZipSecurity zipSecurity, Set<ZipReaderFlags> flags) {
|
||||
this.zipSecurity = zipSecurity;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
public IJadxZipSecurity getZipSecurity() {
|
||||
return zipSecurity;
|
||||
}
|
||||
|
||||
public Set<ZipReaderFlags> getFlags() {
|
||||
return flags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package jadx.zip.fallback;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import jadx.zip.IZipEntry;
|
||||
|
||||
public class FallbackZipEntry implements IZipEntry {
|
||||
private final FallbackZipParser parser;
|
||||
private final ZipEntry zipEntry;
|
||||
|
||||
public FallbackZipEntry(FallbackZipParser parser, ZipEntry zipEntry) {
|
||||
this.parser = parser;
|
||||
this.zipEntry = zipEntry;
|
||||
}
|
||||
|
||||
public ZipEntry getZipEntry() {
|
||||
return zipEntry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return zipEntry.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preferBytes() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytes() {
|
||||
return parser.getBytes(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return parser.getInputStream(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCompressedSize() {
|
||||
return zipEntry.getCompressedSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUncompressedSize() {
|
||||
return zipEntry.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return zipEntry.isDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getZipFile() {
|
||||
return parser.getZipFile();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package jadx.zip.fallback;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.IZipParser;
|
||||
import jadx.zip.ZipContent;
|
||||
import jadx.zip.ZipReaderOptions;
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
import jadx.zip.security.LimitedInputStream;
|
||||
|
||||
public class FallbackZipParser implements IZipParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(FallbackZipParser.class);
|
||||
private final File file;
|
||||
private final IJadxZipSecurity zipSecurity;
|
||||
private final boolean useLimitedDataStream;
|
||||
|
||||
private ZipFile zipFile;
|
||||
|
||||
public FallbackZipParser(File file, ZipReaderOptions options) {
|
||||
this.file = file;
|
||||
this.zipSecurity = options.getZipSecurity();
|
||||
this.useLimitedDataStream = zipSecurity.useLimitedDataStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipContent open() throws IOException {
|
||||
zipFile = new ZipFile(file);
|
||||
|
||||
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
|
||||
if (maxEntriesCount == -1) {
|
||||
maxEntriesCount = Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
List<IZipEntry> list = new ArrayList<>();
|
||||
Enumeration<? extends ZipEntry> entries = zipFile.entries();
|
||||
while (entries.hasMoreElements()) {
|
||||
FallbackZipEntry zipEntry = new FallbackZipEntry(this, entries.nextElement());
|
||||
if (isValidEntry(zipEntry)) {
|
||||
list.add(zipEntry);
|
||||
if (list.size() > maxEntriesCount) {
|
||||
throw new IllegalStateException("Max entries count limit exceeded: " + list.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ZipContent(this, list);
|
||||
}
|
||||
|
||||
private boolean isValidEntry(IZipEntry zipEntry) {
|
||||
boolean validEntry = zipSecurity.isValidEntry(zipEntry);
|
||||
if (!validEntry) {
|
||||
LOG.warn("Zip entry '{}' is invalid and excluded from processing", zipEntry);
|
||||
}
|
||||
return validEntry;
|
||||
}
|
||||
|
||||
public byte[] getBytes(FallbackZipEntry entry) {
|
||||
try (InputStream is = getEntryStream(entry)) {
|
||||
return is.readAllBytes();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to read bytes for entry: " + entry.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream getInputStream(FallbackZipEntry entry) {
|
||||
try {
|
||||
return getEntryStream(entry);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to open input stream for entry: " + entry.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream getEntryStream(FallbackZipEntry entry) throws IOException {
|
||||
InputStream entryStream = zipFile.getInputStream(entry.getZipEntry());
|
||||
InputStream stream;
|
||||
if (useLimitedDataStream) {
|
||||
stream = new LimitedInputStream(entryStream, entry.getUncompressedSize());
|
||||
} else {
|
||||
stream = entryStream;
|
||||
}
|
||||
return new BufferedInputStream(stream);
|
||||
}
|
||||
|
||||
public File getZipFile() {
|
||||
return file;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
if (zipFile != null) {
|
||||
zipFile.close();
|
||||
}
|
||||
} finally {
|
||||
zipFile = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package jadx.zip.parser;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
import jadx.zip.IZipEntry;
|
||||
|
||||
public final class JadxZipEntry implements IZipEntry {
|
||||
private final JadxZipParser parser;
|
||||
private final String fileName;
|
||||
private final int compressMethod;
|
||||
private final int entryStart;
|
||||
private final int dataStart;
|
||||
private final long compressedSize;
|
||||
private final long uncompressedSize;
|
||||
|
||||
JadxZipEntry(JadxZipParser parser, String fileName, int entryStart, int dataStart,
|
||||
int compressMethod, long compressedSize, long uncompressedSize) {
|
||||
this.parser = parser;
|
||||
this.fileName = fileName;
|
||||
this.entryStart = entryStart;
|
||||
this.dataStart = dataStart;
|
||||
this.compressMethod = compressMethod;
|
||||
this.compressedSize = compressedSize;
|
||||
this.uncompressedSize = uncompressedSize;
|
||||
}
|
||||
|
||||
public boolean isSizesValid() {
|
||||
if (compressedSize <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (uncompressedSize <= 0) {
|
||||
return false;
|
||||
}
|
||||
return compressedSize <= uncompressedSize;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getCompressedSize() {
|
||||
return compressedSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUncompressedSize() {
|
||||
return uncompressedSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDirectory() {
|
||||
return fileName.endsWith("/");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preferBytes() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getBytes() {
|
||||
return parser.getBytes(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return parser.getInputStream(this);
|
||||
}
|
||||
|
||||
public int getEntryStart() {
|
||||
return entryStart;
|
||||
}
|
||||
|
||||
public int getDataStart() {
|
||||
return dataStart;
|
||||
}
|
||||
|
||||
public int getCompressMethod() {
|
||||
return compressMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getZipFile() {
|
||||
return parser.getZipFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return parser.getZipFile().getName() + ':' + fileName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,403 @@
|
||||
package jadx.zip.parser;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.IZipParser;
|
||||
import jadx.zip.ZipContent;
|
||||
import jadx.zip.ZipReaderFlags;
|
||||
import jadx.zip.ZipReaderOptions;
|
||||
import jadx.zip.fallback.FallbackZipParser;
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
|
||||
/**
|
||||
* Custom and simple zip parser to fight tampering.
|
||||
* Many zip features aren't supported:
|
||||
* - Compression methods other than STORE or DEFLATE
|
||||
* - Zip64
|
||||
* - Checksum verification
|
||||
* - Multi file archives
|
||||
*/
|
||||
public final class JadxZipParser implements IZipParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JadxZipParser.class);
|
||||
|
||||
private static final byte LOCAL_FILE_HEADER_START = 0x50;
|
||||
private static final int LOCAL_FILE_HEADER_SIGN = 0x04034b50;
|
||||
private static final int CD_SIGN = 0x02014b50;
|
||||
private static final int END_OF_CD_SIGN = 0x06054b50;
|
||||
|
||||
private final File zipFile;
|
||||
private final ZipReaderOptions options;
|
||||
private final IJadxZipSecurity zipSecurity;
|
||||
private final Set<ZipReaderFlags> flags;
|
||||
private final boolean verify;
|
||||
|
||||
private RandomAccessFile file;
|
||||
private FileChannel fileChannel;
|
||||
private ByteBuffer byteBuffer;
|
||||
|
||||
private int endOfCDStart = -2;
|
||||
|
||||
private @Nullable ZipContent fallbackZipContent;
|
||||
|
||||
public JadxZipParser(File zipFile, ZipReaderOptions options) {
|
||||
this.zipFile = zipFile;
|
||||
this.options = options;
|
||||
this.zipSecurity = options.getZipSecurity();
|
||||
this.flags = options.getFlags();
|
||||
this.verify = options.getFlags().contains(ZipReaderFlags.REPORT_TAMPERING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipContent open() throws IOException {
|
||||
load();
|
||||
try {
|
||||
int maxEntriesCount = zipSecurity.getMaxEntriesCount();
|
||||
if (maxEntriesCount == -1) {
|
||||
maxEntriesCount = Integer.MAX_VALUE;
|
||||
}
|
||||
List<IZipEntry> entries;
|
||||
if (flags.contains(ZipReaderFlags.IGNORE_CENTRAL_DIR_ENTRIES)) {
|
||||
entries = searchLocalFileHeaders(maxEntriesCount);
|
||||
} else {
|
||||
entries = loadFromCentralDirs(maxEntriesCount);
|
||||
}
|
||||
return new ZipContent(this, entries);
|
||||
} catch (Exception e) {
|
||||
if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
|
||||
throw new IOException("Failed to open zip: " + zipFile + ", error: " + e.getMessage(), e);
|
||||
}
|
||||
LOG.warn("Zip open failed, switching to fallback parser, zip: {}", zipFile, e);
|
||||
return initFallbackParser();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("RedundantIfStatement")
|
||||
public boolean canOpen() {
|
||||
try {
|
||||
load();
|
||||
int eocdStart = searchEndOfCDStart();
|
||||
ByteBuffer buf = byteBuffer;
|
||||
buf.position(eocdStart + 4);
|
||||
int diskNum = readU2(buf);
|
||||
if (diskNum == 0xFFFF) {
|
||||
// Zip64
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Jadx parser can't open zip file: {}", zipFile, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidEntry(JadxZipEntry zipEntry) {
|
||||
boolean validEntry = zipSecurity.isValidEntry(zipEntry);
|
||||
if (!validEntry) {
|
||||
LOG.warn("Zip entry '{}' is invalid and excluded from processing", zipEntry);
|
||||
}
|
||||
return validEntry;
|
||||
}
|
||||
|
||||
private void load() throws IOException {
|
||||
if (byteBuffer != null) {
|
||||
// already loaded
|
||||
return;
|
||||
}
|
||||
file = new RandomAccessFile(zipFile, "r");
|
||||
long size = file.length();
|
||||
if (size >= Integer.MAX_VALUE) {
|
||||
throw new IOException("Zip file is too big");
|
||||
}
|
||||
int fileLen = (int) size;
|
||||
if (fileLen < 100 * 1024 * 1024) {
|
||||
// load files smaller than 100MB directly into memory
|
||||
byte[] bytes = new byte[fileLen];
|
||||
file.readFully(bytes);
|
||||
byteBuffer = ByteBuffer.wrap(bytes).asReadOnlyBuffer();
|
||||
file.close();
|
||||
file = null;
|
||||
} else {
|
||||
// for big files - use a memory mapped file
|
||||
fileChannel = file.getChannel();
|
||||
byteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
|
||||
}
|
||||
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
private List<IZipEntry> searchLocalFileHeaders(int maxEntriesCount) {
|
||||
List<IZipEntry> entries = new ArrayList<>();
|
||||
while (true) {
|
||||
int start = searchEntryStart();
|
||||
if (start == -1) {
|
||||
return entries;
|
||||
}
|
||||
JadxZipEntry zipEntry = loadFileEntry(start);
|
||||
if (isValidEntry(zipEntry)) {
|
||||
entries.add(zipEntry);
|
||||
if (entries.size() > maxEntriesCount) {
|
||||
throw new IllegalStateException("Max entries count limit exceeded: " + entries.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<IZipEntry> loadFromCentralDirs(int maxEntriesCount) throws IOException {
|
||||
int eocdStart = searchEndOfCDStart();
|
||||
if (eocdStart < 0) {
|
||||
throw new RuntimeException("End of central directory not found");
|
||||
}
|
||||
ByteBuffer buf = byteBuffer;
|
||||
buf.position(eocdStart + 10);
|
||||
int entriesCount = readU2(buf);
|
||||
buf.position(eocdStart + 16);
|
||||
int cdOffset = buf.getInt();
|
||||
|
||||
if (entriesCount > maxEntriesCount) {
|
||||
throw new IllegalStateException("Max entries count limit exceeded: " + entriesCount);
|
||||
}
|
||||
List<IZipEntry> entries = new ArrayList<>(entriesCount);
|
||||
buf.position(cdOffset);
|
||||
for (int i = 0; i < entriesCount; i++) {
|
||||
JadxZipEntry zipEntry = loadCDEntry();
|
||||
if (isValidEntry(zipEntry)) {
|
||||
entries.add(zipEntry);
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private JadxZipEntry loadCDEntry() {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
int start = buf.position();
|
||||
buf.position(start + 28);
|
||||
int fileNameLen = readU2(buf);
|
||||
int extraFieldLen = readU2(buf);
|
||||
int commentLen = readU2(buf);
|
||||
buf.position(start + 42);
|
||||
int fileEntryStart = buf.getInt();
|
||||
int entryEnd = start + 46 + fileNameLen + extraFieldLen + commentLen;
|
||||
JadxZipEntry entry = loadFileEntry(fileEntryStart);
|
||||
if (verify) {
|
||||
compareCDAndLFH(buf, start, entry);
|
||||
}
|
||||
if (!entry.isSizesValid()) {
|
||||
entry = fixEntryFromCD(entry, start);
|
||||
}
|
||||
buf.position(entryEnd);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private JadxZipEntry fixEntryFromCD(JadxZipEntry entry, int start) {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
buf.position(start + 10);
|
||||
int comprMethod = readU2(buf);
|
||||
buf.position(start + 20);
|
||||
int comprSize = buf.getInt();
|
||||
int unComprSize = buf.getInt();
|
||||
return new JadxZipEntry(this, entry.getName(), start, entry.getDataStart(), comprMethod, comprSize, unComprSize);
|
||||
}
|
||||
|
||||
private static void compareCDAndLFH(ByteBuffer buf, int start, JadxZipEntry entry) {
|
||||
buf.position(start + 10);
|
||||
int comprMethod = readU2(buf);
|
||||
if (comprMethod != entry.getCompressMethod()) {
|
||||
LOG.warn("Compression method differ in CD {} and LFH {} for {}",
|
||||
comprMethod, entry.getCompressMethod(), entry);
|
||||
}
|
||||
buf.position(start + 20);
|
||||
int comprSize = buf.getInt();
|
||||
int unComprSize = buf.getInt();
|
||||
if (comprSize != entry.getCompressedSize()) {
|
||||
LOG.warn("Compressed size differ in CD {} and LFH {} for {}",
|
||||
comprSize, entry.getCompressedSize(), entry);
|
||||
}
|
||||
if (unComprSize != entry.getUncompressedSize()) {
|
||||
LOG.warn("Uncompressed size differ in CD {} and LFH {} for {}",
|
||||
unComprSize, entry.getUncompressedSize(), entry);
|
||||
}
|
||||
}
|
||||
|
||||
private JadxZipEntry loadFileEntry(int start) {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
buf.position(start + 8);
|
||||
int comprMethod = readU2(buf);
|
||||
buf.position(start + 18);
|
||||
int comprSize = buf.getInt();
|
||||
int unComprSize = buf.getInt();
|
||||
int fileNameLen = readU2(buf);
|
||||
int extraFieldLen = readU2(buf);
|
||||
String fileName = readString(buf, fileNameLen);
|
||||
int dataStart = start + 30 + fileNameLen + extraFieldLen;
|
||||
buf.position(dataStart + comprSize);
|
||||
return new JadxZipEntry(this, fileName, start, dataStart, comprMethod, comprSize, unComprSize);
|
||||
}
|
||||
|
||||
private int searchEndOfCDStart() throws IOException {
|
||||
if (endOfCDStart != -2) {
|
||||
return endOfCDStart;
|
||||
}
|
||||
ByteBuffer buf = byteBuffer;
|
||||
int pos = buf.limit() - 22;
|
||||
int minPos = Math.max(0, pos - 0xffff);
|
||||
while (true) {
|
||||
buf.position(pos);
|
||||
int sign = buf.getInt();
|
||||
if (sign == END_OF_CD_SIGN) {
|
||||
endOfCDStart = pos;
|
||||
return pos;
|
||||
}
|
||||
pos--;
|
||||
if (pos < minPos) {
|
||||
throw new IOException("End of central directory record not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int searchEntryStart() {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
while (true) {
|
||||
int start = buf.position();
|
||||
if (start + 4 > buf.limit()) {
|
||||
return -1;
|
||||
}
|
||||
byte b = buf.get();
|
||||
if (b == LOCAL_FILE_HEADER_START) {
|
||||
buf.position(start);
|
||||
int sign = buf.getInt();
|
||||
if (sign == LOCAL_FILE_HEADER_SIGN) {
|
||||
return start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
InputStream getInputStream(JadxZipEntry entry) {
|
||||
return new ByteArrayInputStream(getBytes(entry));
|
||||
}
|
||||
|
||||
synchronized byte[] getBytes(JadxZipEntry entry) {
|
||||
int compressMethod = entry.getCompressMethod();
|
||||
if (verify) {
|
||||
if (compressMethod == 0) {
|
||||
if (entry.getCompressedSize() != entry.getUncompressedSize()) {
|
||||
LOG.warn("Not equal sizes for STORE method: compressed: {}, uncompressed: {}, entry: {}",
|
||||
entry.getCompressedSize(), entry.getUncompressedSize(), entry);
|
||||
}
|
||||
} else if (compressMethod != 8) {
|
||||
LOG.warn("Unknown compress method: {} in entry: {}", compressMethod, entry);
|
||||
}
|
||||
}
|
||||
if (compressMethod == 8) {
|
||||
try {
|
||||
return ZipDeflate.decompressEntryToBytes(byteBuffer, entry);
|
||||
} catch (Exception e) {
|
||||
if (isEncrypted(entry)) {
|
||||
throw new RuntimeException("Entry is encrypted, failed to decompress: " + entry, e);
|
||||
}
|
||||
if (flags.contains(ZipReaderFlags.DONT_USE_FALLBACK)) {
|
||||
throw new RuntimeException("Failed to decompress zip entry: " + entry + ", error: " + e.getMessage(), e);
|
||||
}
|
||||
LOG.warn("Entry '{}' parse failed, switching to fallback parser", entry, e);
|
||||
return useFallbackParser(entry);
|
||||
}
|
||||
}
|
||||
// treat any other compression methods values as UNCOMPRESSED
|
||||
return bufferToBytes(entry.getDataStart(), (int) entry.getUncompressedSize());
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
private byte[] useFallbackParser(JadxZipEntry entry) {
|
||||
IZipEntry zipEntry = initFallbackParser().searchEntry(entry.getName());
|
||||
if (zipEntry == null) {
|
||||
throw new RuntimeException("Fallback parser can't find entry: " + entry);
|
||||
}
|
||||
return zipEntry.getBytes();
|
||||
}
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
private ZipContent initFallbackParser() {
|
||||
if (fallbackZipContent == null) {
|
||||
try {
|
||||
fallbackZipContent = new FallbackZipParser(zipFile, options).open();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Fallback parser failed to open file: " + zipFile, e);
|
||||
}
|
||||
}
|
||||
return fallbackZipContent;
|
||||
}
|
||||
|
||||
private boolean isEncrypted(JadxZipEntry entry) {
|
||||
int flags = readFlags(entry);
|
||||
return (flags & 1) != 0;
|
||||
}
|
||||
|
||||
private int readFlags(JadxZipEntry entry) {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
buf.position(entry.getEntryStart() + 6);
|
||||
return readU2(buf);
|
||||
}
|
||||
|
||||
byte[] bufferToBytes(int start, int size) {
|
||||
ByteBuffer buf = byteBuffer;
|
||||
byte[] data = new byte[size];
|
||||
buf.position(start);
|
||||
buf.get(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
private static int readU2(ByteBuffer buf) {
|
||||
return buf.getShort() & 0xFFFF;
|
||||
}
|
||||
|
||||
private static String readString(ByteBuffer buf, int fileNameLen) {
|
||||
byte[] bytes = new byte[fileNameLen];
|
||||
buf.get(bytes);
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
try {
|
||||
if (fileChannel != null) {
|
||||
fileChannel.close();
|
||||
}
|
||||
if (file != null) {
|
||||
file.close();
|
||||
}
|
||||
if (fallbackZipContent != null) {
|
||||
fallbackZipContent.close();
|
||||
}
|
||||
} finally {
|
||||
fileChannel = null;
|
||||
file = null;
|
||||
byteBuffer = null;
|
||||
endOfCDStart = -2;
|
||||
fallbackZipContent = null;
|
||||
}
|
||||
}
|
||||
|
||||
public File getZipFile() {
|
||||
return zipFile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JadxZipParser{" + zipFile + '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package jadx.zip.parser;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.zip.DataFormatException;
|
||||
import java.util.zip.Inflater;
|
||||
|
||||
final class ZipDeflate {
|
||||
|
||||
static byte[] decompressEntryToBytes(ByteBuffer buf, JadxZipEntry entry) throws DataFormatException {
|
||||
buf.position(entry.getDataStart());
|
||||
ByteBuffer entryBuf = buf.slice();
|
||||
entryBuf.limit((int) entry.getCompressedSize());
|
||||
byte[] out = new byte[(int) entry.getUncompressedSize()];
|
||||
Inflater inflater = new Inflater(true);
|
||||
inflater.setInput(entryBuf);
|
||||
int written = inflater.inflate(out);
|
||||
if (written != out.length) {
|
||||
throw new DataFormatException("Unexpected size of decompressed entry: " + entry
|
||||
+ ", got: " + written + ", expected: " + out.length);
|
||||
}
|
||||
inflater.end();
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package jadx.zip.security;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import jadx.zip.IZipEntry;
|
||||
|
||||
public class DisabledZipSecurity implements IJadxZipSecurity {
|
||||
|
||||
public static final DisabledZipSecurity INSTANCE = new DisabledZipSecurity();
|
||||
|
||||
@Override
|
||||
public boolean isValidEntry(IZipEntry entry) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidEntryName(String entryName) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInSubDirectory(File baseDir, File file) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useLimitedDataStream() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxEntriesCount() {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package jadx.zip.security;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import jadx.zip.IZipEntry;
|
||||
|
||||
public interface IJadxZipSecurity {
|
||||
|
||||
/**
|
||||
* Check if zip entry is valid and safe to process
|
||||
*/
|
||||
boolean isValidEntry(IZipEntry entry);
|
||||
|
||||
/**
|
||||
* Check if the zip entry name is valid.
|
||||
* This check should be part of {@link #isValidEntry(IZipEntry)} method.
|
||||
*/
|
||||
boolean isValidEntryName(String entryName);
|
||||
|
||||
/**
|
||||
* Use limited InputStream for entry uncompressed data
|
||||
*/
|
||||
boolean useLimitedDataStream();
|
||||
|
||||
/**
|
||||
* Max entries count expected in a zip file, fail zip open if the limit exceeds.
|
||||
* Return -1 to disable entries count check.
|
||||
*/
|
||||
int getMaxEntriesCount();
|
||||
|
||||
/**
|
||||
* Check if a file will be inside baseDir after a system resolves its path
|
||||
*/
|
||||
boolean isInSubDirectory(File baseDir, File file);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package jadx.zip.security;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.zip.IZipEntry;
|
||||
|
||||
public class JadxZipSecurity implements IJadxZipSecurity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JadxZipSecurity.class);
|
||||
|
||||
private static final File CWD = getCWD();
|
||||
|
||||
/**
|
||||
* The size of uncompressed zip entry shouldn't be bigger of compressed in zipBombDetectionFactor
|
||||
* times
|
||||
*/
|
||||
private int zipBombDetectionFactor = 100;
|
||||
|
||||
/**
|
||||
* Zip entries that have an uncompressed size of less than zipBombMinUncompressedSize are considered
|
||||
* safe
|
||||
*/
|
||||
private int zipBombMinUncompressedSize = 25 * 1024 * 1024;
|
||||
|
||||
private int maxEntriesCount = 100_000;
|
||||
|
||||
@Override
|
||||
public boolean isValidEntry(IZipEntry entry) {
|
||||
return isValidEntryName(entry.getName()) && !isZipBomb(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useLimitedDataStream() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxEntriesCount() {
|
||||
return maxEntriesCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that entry name contains no any traversals and prevents cases like "../classes.dex",
|
||||
* to limit output only to the specified directory
|
||||
*/
|
||||
@Override
|
||||
public boolean isValidEntryName(String entryName) {
|
||||
if (entryName.contains("..")) { // quick pre-check
|
||||
if (entryName.contains("../") || entryName.contains("..\\")) {
|
||||
LOG.error("Path traversal attack detected in entry: '{}'", entryName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
File currentPath = CWD;
|
||||
File canonical = new File(currentPath, entryName).getCanonicalFile();
|
||||
if (isInSubDirectoryInternal(currentPath, canonical)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// check failed
|
||||
}
|
||||
LOG.error("Invalid file name or path traversal attack detected: {}", entryName);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInSubDirectory(File baseDir, File file) {
|
||||
try {
|
||||
return isInSubDirectoryInternal(baseDir.getCanonicalFile(), file.getCanonicalFile());
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isZipBomb(IZipEntry entry) {
|
||||
long compressedSize = entry.getCompressedSize();
|
||||
long uncompressedSize = entry.getUncompressedSize();
|
||||
boolean invalidSize = compressedSize < 0 || uncompressedSize < 0;
|
||||
boolean possibleZipBomb = uncompressedSize >= zipBombMinUncompressedSize
|
||||
&& compressedSize * zipBombDetectionFactor < uncompressedSize;
|
||||
if (invalidSize || possibleZipBomb) {
|
||||
LOG.error("Potential zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}",
|
||||
compressedSize, uncompressedSize, entry.getName());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isInSubDirectoryInternal(File baseDir, File file) {
|
||||
File current = file;
|
||||
while (true) {
|
||||
if (current == null) {
|
||||
return false;
|
||||
}
|
||||
if (current.equals(baseDir)) {
|
||||
return true;
|
||||
}
|
||||
current = current.getParentFile();
|
||||
}
|
||||
}
|
||||
|
||||
public void setMaxEntriesCount(int maxEntriesCount) {
|
||||
this.maxEntriesCount = maxEntriesCount;
|
||||
}
|
||||
|
||||
public void setZipBombDetectionFactor(int zipBombDetectionFactor) {
|
||||
this.zipBombDetectionFactor = zipBombDetectionFactor;
|
||||
}
|
||||
|
||||
public void setZipBombMinUncompressedSize(int zipBombMinUncompressedSize) {
|
||||
this.zipBombMinUncompressedSize = zipBombMinUncompressedSize;
|
||||
}
|
||||
|
||||
private static File getCWD() {
|
||||
try {
|
||||
return new File(".").getCanonicalFile();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to init current working dir constant", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+14
-11
@@ -1,21 +1,21 @@
|
||||
package jadx.api.plugins.utils;
|
||||
package jadx.zip.security;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class LimitedInputStream extends FilterInputStream {
|
||||
|
||||
private final long maxSize;
|
||||
|
||||
private long currentPos;
|
||||
|
||||
protected LimitedInputStream(InputStream in, long maxSize) {
|
||||
public LimitedInputStream(InputStream in, long maxSize) {
|
||||
super(in);
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
private void checkPos() {
|
||||
private void addAndCheckPos(long count) {
|
||||
currentPos += count;
|
||||
if (currentPos > maxSize) {
|
||||
throw new IllegalStateException("Read limit exceeded");
|
||||
}
|
||||
@@ -25,18 +25,17 @@ public class LimitedInputStream extends FilterInputStream {
|
||||
public int read() throws IOException {
|
||||
int data = super.read();
|
||||
if (data != -1) {
|
||||
currentPos++;
|
||||
checkPos();
|
||||
addAndCheckPos(1);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
@SuppressWarnings("NullableProblems")
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int count = super.read(b, off, len);
|
||||
if (count > 0) {
|
||||
currentPos += count;
|
||||
checkPos();
|
||||
addAndCheckPos(count);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
@@ -44,10 +43,14 @@ public class LimitedInputStream extends FilterInputStream {
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long skipped = super.skip(n);
|
||||
if (skipped != 0) {
|
||||
currentPos += skipped;
|
||||
checkPos();
|
||||
if (skipped > 0) {
|
||||
addAndCheckPos(skipped);
|
||||
}
|
||||
return skipped;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
api(project(":jadx-plugins:jadx-input-api"))
|
||||
api(project(":jadx-commons:jadx-zip"))
|
||||
|
||||
implementation("com.google.code.gson:gson:2.11.0")
|
||||
|
||||
|
||||
@@ -50,9 +50,9 @@ import jadx.core.utils.DecompilerScheduler;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.core.utils.files.ZipPatch;
|
||||
import jadx.core.utils.tasks.TaskExecutor;
|
||||
import jadx.core.xmlgen.ResourcesSaver;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
/**
|
||||
* Jadx API usage example:
|
||||
@@ -87,6 +87,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
private final JadxArgs args;
|
||||
private final JadxPluginManager pluginManager;
|
||||
private final List<ICodeLoader> loadedInputs = new ArrayList<>();
|
||||
private final ZipReader zipReader;
|
||||
|
||||
private RootNode root;
|
||||
private List<JavaClass> classes;
|
||||
@@ -109,6 +110,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
this.args = Objects.requireNonNull(args);
|
||||
this.pluginManager = new JadxPluginManager(this);
|
||||
this.resourcesLoader = new ResourcesLoader(this);
|
||||
this.zipReader = new ZipReader(args.getSecurity());
|
||||
}
|
||||
|
||||
public void load() {
|
||||
@@ -145,9 +147,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
|
||||
private void loadInputFiles() {
|
||||
loadedInputs.clear();
|
||||
List<File> inputs = ZipPatch.patchZipFiles(args.getInputFiles());
|
||||
args.setInputFiles(inputs);
|
||||
List<Path> inputPaths = Utils.collectionMap(inputs, File::toPath);
|
||||
List<Path> inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath);
|
||||
List<Path> inputFiles = FileUtils.expandDirs(inputPaths);
|
||||
long start = System.currentTimeMillis();
|
||||
for (PluginContext plugin : pluginManager.getResolvedPluginContexts()) {
|
||||
@@ -333,7 +333,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
// process AndroidManifest.xml first to load complete resource ids table
|
||||
for (ResourceFile resourceFile : getResources()) {
|
||||
if (resourceFile.getType() == ResourceType.MANIFEST) {
|
||||
new ResourcesSaver(outDir, resourceFile).run();
|
||||
new ResourcesSaver(this, outDir, resourceFile).run();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -352,7 +352,7 @@ public final class JadxDecompiler implements Closeable {
|
||||
// ignore resource made from input file
|
||||
continue;
|
||||
}
|
||||
tasks.add(new ResourcesSaver(outDir, resourceFile));
|
||||
tasks.add(new ResourcesSaver(this, outDir, resourceFile));
|
||||
}
|
||||
executor.addParallelTasks(tasks);
|
||||
}
|
||||
@@ -702,6 +702,10 @@ public final class JadxDecompiler implements Closeable {
|
||||
return resourcesLoader;
|
||||
}
|
||||
|
||||
public ZipReader getZipReader() {
|
||||
return zipReader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "jadx decompiler " + getVersion();
|
||||
|
||||
@@ -2,7 +2,6 @@ package jadx.api;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
import jadx.core.xmlgen.entry.ResourceEntry;
|
||||
|
||||
@@ -42,7 +41,7 @@ public class ResourceFile {
|
||||
}
|
||||
|
||||
public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
|
||||
if (!ZipSecurity.isValidZipEntryName(name)) {
|
||||
if (!decompiler.getArgs().getSecurity().isValidEntryName(name)) {
|
||||
return null;
|
||||
}
|
||||
return new ResourceFile(decompiler, name, type);
|
||||
@@ -98,6 +97,10 @@ public class ResourceFile {
|
||||
return zipRef;
|
||||
}
|
||||
|
||||
public JadxDecompiler getDecompiler() {
|
||||
return decompiler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ResourceFile{name='" + name + '\'' + ", type=" + type + '}';
|
||||
|
||||
@@ -6,9 +6,9 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -19,18 +19,19 @@ import jadx.api.plugins.CustomResourcesLoader;
|
||||
import jadx.api.plugins.resources.IResContainerFactory;
|
||||
import jadx.api.plugins.resources.IResTableParserProvider;
|
||||
import jadx.api.plugins.resources.IResourcesLoader;
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.android.Res9patchStreamDecoder;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.core.utils.files.ZipFile;
|
||||
import jadx.core.xmlgen.BinaryXMLParser;
|
||||
import jadx.core.xmlgen.IResTableParser;
|
||||
import jadx.core.xmlgen.ResContainer;
|
||||
import jadx.core.xmlgen.ResTableBinaryParserProvider;
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.ZipContent;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
import static jadx.core.utils.files.FileUtils.READ_BUFFER_SIZE;
|
||||
import static jadx.core.utils.files.FileUtils.copyStream;
|
||||
@@ -101,21 +102,19 @@ public final class ResourcesLoader implements IResourcesLoader {
|
||||
return decoder.decode(file.length(), inputStream);
|
||||
}
|
||||
} else {
|
||||
try (ZipFile zipFile = new ZipFile(zipRef.getZipFile())) {
|
||||
ZipEntry entry = zipFile.getEntry(zipRef.getEntryName());
|
||||
ZipReader zipReader = rf.getDecompiler().getZipReader();
|
||||
try (ZipContent content = zipReader.open(zipRef.getZipFile())) {
|
||||
IZipEntry entry = content.searchEntry(zipRef.getEntryName());
|
||||
if (entry == null) {
|
||||
throw new IOException("Zip entry not found: " + zipRef);
|
||||
}
|
||||
if (!ZipSecurity.isValidZipEntry(entry)) {
|
||||
return null;
|
||||
}
|
||||
try (InputStream inputStream = ZipSecurity.getInputStreamForEntry(zipFile, entry)) {
|
||||
return decoder.decode(entry.getSize(), inputStream);
|
||||
try (InputStream inputStream = entry.getInputStream()) {
|
||||
return decoder.decode(entry.getUncompressedSize(), inputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new JadxException("Error decode: " + rf.getDeobfName(), e);
|
||||
throw new JadxException("Error decode: " + rf.getOriginalName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +207,7 @@ public final class ResourcesLoader implements IResourcesLoader {
|
||||
|
||||
public void defaultLoadFile(List<ResourceFile> list, File file, String subDir) {
|
||||
if (FileUtils.isZipFile(file)) {
|
||||
ZipSecurity.visitZipEntries(file, (zipFile, entry) -> {
|
||||
jadxRef.getZipReader().visitEntries(file, entry -> {
|
||||
addEntry(list, file, entry, subDir);
|
||||
return null;
|
||||
});
|
||||
@@ -218,7 +217,7 @@ public final class ResourcesLoader implements IResourcesLoader {
|
||||
}
|
||||
}
|
||||
|
||||
public void addEntry(List<ResourceFile> list, File zipFile, ZipEntry entry, String subDir) {
|
||||
public void addEntry(List<ResourceFile> list, File zipFile, IZipEntry entry, String subDir) {
|
||||
if (entry.isDirectory()) {
|
||||
return;
|
||||
}
|
||||
@@ -234,7 +233,7 @@ public final class ResourcesLoader implements IResourcesLoader {
|
||||
public static ICodeInfo loadToCodeWriter(InputStream is) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(READ_BUFFER_SIZE);
|
||||
copyStream(is, baos);
|
||||
return new SimpleCodeInfo(baos.toString("UTF-8"));
|
||||
return new SimpleCodeInfo(baos.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private synchronized BinaryXMLParser loadBinaryXmlParser() {
|
||||
|
||||
@@ -14,6 +14,7 @@ import jadx.api.plugins.input.JadxCodeInput;
|
||||
import jadx.api.plugins.options.JadxPluginOptions;
|
||||
import jadx.api.plugins.pass.JadxPass;
|
||||
import jadx.api.plugins.resources.IResourcesLoader;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
public interface JadxPluginContext {
|
||||
|
||||
@@ -59,4 +60,9 @@ public interface JadxPluginContext {
|
||||
* Access to plugin specific files and directories
|
||||
*/
|
||||
IJadxFiles files();
|
||||
|
||||
/**
|
||||
* Custom jadx zip reader to fight tampering and provide additional security checks
|
||||
*/
|
||||
ZipReader getZipReader();
|
||||
}
|
||||
|
||||
@@ -4,63 +4,52 @@ import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Enumeration;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.plugins.JadxPluginContext;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.files.ZipFile;
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.ZipReader;
|
||||
import jadx.zip.security.DisabledZipSecurity;
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
import jadx.zip.security.JadxZipSecurity;
|
||||
import jadx.zip.security.LimitedInputStream;
|
||||
|
||||
/**
|
||||
* Deprecated, migrate to {@link ZipReader}. <br>
|
||||
* Prefer already configured instance from {@link JadxDecompiler#getZipReader()} or
|
||||
* {@link JadxPluginContext#getZipReader()}.
|
||||
*/
|
||||
@Deprecated
|
||||
public class ZipSecurity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZipSecurity.class);
|
||||
|
||||
private static final boolean DISABLE_CHECKS = Utils.getEnvVarBool("JADX_DISABLE_ZIP_SECURITY", false);
|
||||
|
||||
/**
|
||||
* size of uncompressed zip entry shouldn't be bigger of compressed in
|
||||
* {@link #ZIP_BOMB_DETECTION_FACTOR} times
|
||||
*/
|
||||
private static final int ZIP_BOMB_DETECTION_FACTOR = 100;
|
||||
|
||||
/**
|
||||
* Zip entries that have an uncompressed size of less than {@link #ZIP_BOMB_MIN_UNCOMPRESSED_SIZE}
|
||||
* are considered safe
|
||||
*/
|
||||
private static final int ZIP_BOMB_MIN_UNCOMPRESSED_SIZE = 25 * 1024 * 1024;
|
||||
|
||||
private static final int MAX_ENTRIES_COUNT = Utils.getEnvVarInt("JADX_ZIP_MAX_ENTRIES_COUNT", 100_000);
|
||||
|
||||
private static final IJadxZipSecurity ZIP_SECURITY = buildZipSecurity();
|
||||
|
||||
private static final ZipReader ZIP_READER = new ZipReader(ZIP_SECURITY);
|
||||
|
||||
private static IJadxZipSecurity buildZipSecurity() {
|
||||
if (DISABLE_CHECKS) {
|
||||
return DisabledZipSecurity.INSTANCE;
|
||||
}
|
||||
JadxZipSecurity jadxZipSecurity = new JadxZipSecurity();
|
||||
jadxZipSecurity.setMaxEntriesCount(MAX_ENTRIES_COUNT);
|
||||
return jadxZipSecurity;
|
||||
}
|
||||
|
||||
private ZipSecurity() {
|
||||
}
|
||||
|
||||
private static boolean isInSubDirectoryInternal(File baseDir, File file) {
|
||||
File current = file;
|
||||
while (true) {
|
||||
if (current == null) {
|
||||
return false;
|
||||
}
|
||||
if (current.equals(baseDir)) {
|
||||
return true;
|
||||
}
|
||||
current = current.getParentFile();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isInSubDirectory(File baseDir, File file) {
|
||||
if (DISABLE_CHECKS) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
return isInSubDirectoryInternal(baseDir.getCanonicalFile(), file.getCanonicalFile());
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
return ZIP_SECURITY.isInSubDirectory(baseDir, file);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,48 +57,15 @@ public class ZipSecurity {
|
||||
* to limit output only to the specified directory
|
||||
*/
|
||||
public static boolean isValidZipEntryName(String entryName) {
|
||||
if (DISABLE_CHECKS) {
|
||||
return true;
|
||||
}
|
||||
if (entryName.contains("..")) { // quick pre-check
|
||||
if (entryName.contains("../") || entryName.contains("..\\")) {
|
||||
LOG.error("Path traversal attack detected in entry: '{}'", entryName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try {
|
||||
File currentPath = CommonFileUtils.CWD;
|
||||
File canonical = new File(currentPath, entryName).getCanonicalFile();
|
||||
if (isInSubDirectoryInternal(currentPath, canonical)) {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// check failed
|
||||
}
|
||||
LOG.error("Invalid file name or path traversal attack detected: {}", entryName);
|
||||
return false;
|
||||
return ZIP_SECURITY.isValidEntryName(entryName);
|
||||
}
|
||||
|
||||
public static boolean isZipBomb(ZipEntry entry) {
|
||||
if (DISABLE_CHECKS) {
|
||||
return false;
|
||||
}
|
||||
long compressedSize = entry.getCompressedSize();
|
||||
long uncompressedSize = entry.getSize();
|
||||
boolean invalidSize = (compressedSize < 0) || (uncompressedSize < 0);
|
||||
boolean possibleZipBomb = (uncompressedSize >= ZIP_BOMB_MIN_UNCOMPRESSED_SIZE)
|
||||
&& (compressedSize * ZIP_BOMB_DETECTION_FACTOR < uncompressedSize);
|
||||
if (invalidSize || possibleZipBomb) {
|
||||
LOG.error("Potential zip bomb attack detected, invalid sizes: compressed {}, uncompressed {}, name {}",
|
||||
compressedSize, uncompressedSize, entry.getName());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
public static boolean isZipBomb(IZipEntry entry) {
|
||||
return !ZIP_SECURITY.isValidEntry(entry);
|
||||
}
|
||||
|
||||
public static boolean isValidZipEntry(ZipEntry entry) {
|
||||
return isValidZipEntryName(entry.getName())
|
||||
&& !isZipBomb(entry);
|
||||
public static boolean isValidZipEntry(IZipEntry entry) {
|
||||
return ZIP_SECURITY.isValidEntry(entry);
|
||||
}
|
||||
|
||||
public static InputStream getInputStreamForEntry(ZipFile zipFile, ZipEntry entry) throws IOException {
|
||||
@@ -122,44 +78,15 @@ public class ZipSecurity {
|
||||
}
|
||||
|
||||
/**
|
||||
* Visit valid entries in zip file.
|
||||
* Visit valid entries in a zip file.
|
||||
* Return not null value from visitor to stop iteration.
|
||||
*/
|
||||
@Nullable
|
||||
public static <R> R visitZipEntries(File file, BiFunction<ZipFile, ZipEntry, R> visitor) {
|
||||
try (ZipFile zip = new ZipFile(file)) {
|
||||
Enumeration<? extends ZipEntry> entries = zip.entries();
|
||||
int entriesProcessed = 0;
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
if (isValidZipEntry(entry)) {
|
||||
R result = visitor.apply(zip, entry);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
entriesProcessed++;
|
||||
if (!DISABLE_CHECKS && entriesProcessed > MAX_ENTRIES_COUNT) {
|
||||
throw new JadxRuntimeException("Zip entries count limit exceeded: " + MAX_ENTRIES_COUNT
|
||||
+ ", last entry: " + entry.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to process zip file: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
return null;
|
||||
public static <R> R visitZipEntries(File file, Function<IZipEntry, R> visitor) {
|
||||
return ZIP_READER.visitEntries(file, visitor);
|
||||
}
|
||||
|
||||
public static void readZipEntries(File file, BiConsumer<ZipEntry, InputStream> visitor) {
|
||||
visitZipEntries(file, (zip, entry) -> {
|
||||
if (!entry.isDirectory()) {
|
||||
try (InputStream in = getInputStreamForEntry(zip, entry)) {
|
||||
visitor.accept(entry, in);
|
||||
} catch (Exception e) {
|
||||
throw new JadxRuntimeException("Failed to process zip entry: " + entry.getName());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
public static void readZipEntries(File file, BiConsumer<IZipEntry, InputStream> visitor) {
|
||||
ZIP_READER.readEntries(file, visitor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ import java.io.InputStream;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
public interface IJadxSecurity {
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
|
||||
public interface IJadxSecurity extends IJadxZipSecurity {
|
||||
|
||||
/**
|
||||
* Check if application package is safe
|
||||
|
||||
@@ -6,7 +6,8 @@ import java.util.Set;
|
||||
public enum JadxSecurityFlag {
|
||||
|
||||
VERIFY_APP_PACKAGE,
|
||||
SECURE_XML_PARSER;
|
||||
SECURE_XML_PARSER,
|
||||
SECURE_ZIP_READER;
|
||||
|
||||
public static Set<JadxSecurityFlag> all() {
|
||||
return EnumSet.allOf(JadxSecurityFlag.class);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package jadx.api.security.impl;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -12,14 +13,52 @@ import org.w3c.dom.Document;
|
||||
import jadx.api.security.IJadxSecurity;
|
||||
import jadx.api.security.JadxSecurityFlag;
|
||||
import jadx.core.deobf.NameMapper;
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.security.DisabledZipSecurity;
|
||||
import jadx.zip.security.IJadxZipSecurity;
|
||||
import jadx.zip.security.JadxZipSecurity;
|
||||
|
||||
import static jadx.api.security.JadxSecurityFlag.SECURE_ZIP_READER;
|
||||
|
||||
public class JadxSecurity implements IJadxSecurity {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JadxSecurity.class);
|
||||
|
||||
private final Set<JadxSecurityFlag> flags;
|
||||
private final IJadxZipSecurity zipSecurity;
|
||||
|
||||
public JadxSecurity(Set<JadxSecurityFlag> flags) {
|
||||
this.flags = flags;
|
||||
this.zipSecurity = flags.contains(SECURE_ZIP_READER) ? new JadxZipSecurity() : DisabledZipSecurity.INSTANCE;
|
||||
}
|
||||
|
||||
public JadxSecurity(Set<JadxSecurityFlag> flags, IJadxZipSecurity zipSecurity) {
|
||||
this.flags = flags;
|
||||
this.zipSecurity = zipSecurity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidEntry(IZipEntry entry) {
|
||||
return zipSecurity.isValidEntry(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValidEntryName(String entryName) {
|
||||
return zipSecurity.isValidEntryName(entryName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInSubDirectory(File baseDir, File file) {
|
||||
return zipSecurity.isInSubDirectory(baseDir, file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useLimitedDataStream() {
|
||||
return zipSecurity.useLimitedDataStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxEntriesCount() {
|
||||
return zipSecurity.getMaxEntriesCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,13 +2,13 @@ package jadx.core.dex.visitors;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.JadxArgs;
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.RootNode;
|
||||
@@ -35,18 +35,15 @@ public class SaveCode {
|
||||
if (codeStr.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (cls.root().getArgs().isSkipFilesSave()) {
|
||||
JadxArgs args = cls.root().getArgs();
|
||||
if (args.isSkipFilesSave()) {
|
||||
return;
|
||||
}
|
||||
String fileName = cls.getClassInfo().getAliasFullPath() + getFileExtension(cls.root());
|
||||
save(codeStr, dir, fileName);
|
||||
}
|
||||
|
||||
public static void save(String code, File dir, String fileName) {
|
||||
if (!ZipSecurity.isValidZipEntryName(fileName)) {
|
||||
if (!args.getSecurity().isValidEntryName(fileName)) {
|
||||
return;
|
||||
}
|
||||
save(code, new File(dir, fileName));
|
||||
save(codeStr, new File(dir, fileName));
|
||||
}
|
||||
|
||||
public static void save(ICodeInfo codeInfo, File file) {
|
||||
@@ -55,7 +52,7 @@ public class SaveCode {
|
||||
|
||||
public static void save(String code, File file) {
|
||||
File outFile = FileUtils.prepareFile(file);
|
||||
try (PrintWriter out = new PrintWriter(outFile, "UTF-8")) {
|
||||
try (PrintWriter out = new PrintWriter(outFile, StandardCharsets.UTF_8)) {
|
||||
out.println(code);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Save file error", e);
|
||||
|
||||
@@ -32,6 +32,7 @@ import jadx.core.plugins.files.JadxFilesData;
|
||||
import jadx.core.utils.Utils;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData, Comparable<PluginContext> {
|
||||
private final JadxDecompiler decompiler;
|
||||
@@ -190,6 +191,11 @@ public class PluginContext implements JadxPluginContext, JadxPluginRuntimeData,
|
||||
closeable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ZipReader getZipReader() {
|
||||
return decompiler.getZipReader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
|
||||
@@ -24,9 +24,9 @@ import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -276,6 +276,11 @@ public class FileUtils {
|
||||
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
|
||||
public static void writeFile(Path file, byte[] data) throws IOException {
|
||||
FileUtils.makeDirsForFile(file);
|
||||
Files.write(file, data, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
|
||||
}
|
||||
|
||||
public static void writeFile(Path file, InputStream is) throws IOException {
|
||||
FileUtils.makeDirsForFile(file);
|
||||
Files.copy(is, file, StandardCopyOption.REPLACE_EXISTING);
|
||||
@@ -358,20 +363,18 @@ public class FileUtils {
|
||||
return new String(hexChars, StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private static final byte[] ZIP_FILE_MAGIC = { 0x50, 0x4B, 0x03, 0x04 };
|
||||
|
||||
public static boolean isZipFile(File file) {
|
||||
try (InputStream is = new FileInputStream(file)) {
|
||||
byte[] headers = new byte[4];
|
||||
int read = is.read(headers, 0, 4);
|
||||
if (read == headers.length) {
|
||||
String headerString = bytesToHex(headers);
|
||||
if (Objects.equals(headerString, "504b0304")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
int len = ZIP_FILE_MAGIC.length;
|
||||
byte[] headers = new byte[len];
|
||||
int read = is.read(headers);
|
||||
return read == len && Arrays.equals(headers, ZIP_FILE_MAGIC);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed read zip file: {}", file.getAbsolutePath(), e);
|
||||
LOG.error("Failed to read zip file: {}", file.getAbsolutePath(), e);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String getPathBaseName(Path file) {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package jadx.core.utils.files;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Deprecated zip file wrapper
|
||||
*/
|
||||
public class ZipFile extends java.util.zip.ZipFile {
|
||||
|
||||
public ZipFile(File file) throws IOException {
|
||||
this(file, OPEN_READ);
|
||||
}
|
||||
|
||||
public ZipFile(File file, int mode) throws IOException {
|
||||
this(file, mode, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public ZipFile(String name, Charset charset) throws IOException {
|
||||
this(new File(name), OPEN_READ, charset);
|
||||
}
|
||||
|
||||
public ZipFile(String name) throws IOException {
|
||||
this(name, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public ZipFile(File file, int mode, Charset charset) throws IOException {
|
||||
super(file, mode, charset);
|
||||
}
|
||||
}
|
||||
@@ -1,199 +0,0 @@
|
||||
package jadx.core.utils.files;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class ZipPatch {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ZipPatch.class);
|
||||
|
||||
public static List<File> patchZipFiles(List<File> inputs) {
|
||||
List<File> result = new ArrayList<>(inputs.size());
|
||||
for (File input : inputs) {
|
||||
try {
|
||||
result.add(patchZipFile(input));
|
||||
} catch (Throwable e) {
|
||||
LOG.warn("Failed to patch zip file: {}", input.getAbsolutePath(), e);
|
||||
result.add(input);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static File patchZipFile(File file) throws IOException {
|
||||
String fileName = file.getPath().toLowerCase();
|
||||
if (!fileName.endsWith(".apk") && !fileName.endsWith(".zip")) {
|
||||
return file;
|
||||
}
|
||||
|
||||
var cDirEntriesToFix = new ArrayList<Long>();
|
||||
var localHeaders = new ArrayList<Long>();
|
||||
List<Long> localHeaderToFix;
|
||||
|
||||
try (var raFile = new RandomAccessFile(file, "r")) {
|
||||
var endOfCDirOffset = findEndOfCentralDir(raFile);
|
||||
|
||||
raFile.seek(endOfCDirOffset + 0x10);
|
||||
var cDirOffset = Integer.toUnsignedLong(Integer.reverseBytes(raFile.readInt()));
|
||||
raFile.seek(endOfCDirOffset + 0x0a);
|
||||
var cDirNumEntries = Short.toUnsignedLong(Short.reverseBytes(raFile.readShort()));
|
||||
|
||||
for (long i = 0, off = cDirOffset; i < cDirNumEntries; i++) {
|
||||
var info = readHeader(raFile, off);
|
||||
|
||||
if (!info.validCompression()) {
|
||||
cDirEntriesToFix.add(off);
|
||||
}
|
||||
|
||||
raFile.seek(off + 0x2a);
|
||||
localHeaders.add(Integer.toUnsignedLong(Integer.reverseBytes(raFile.readInt())));
|
||||
|
||||
off += info.dataOffset;
|
||||
}
|
||||
|
||||
localHeaderToFix = localHeaders
|
||||
.stream()
|
||||
.filter(off -> !readHeaderVexxed(raFile, off).validCompression())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (cDirEntriesToFix.isEmpty() && localHeaderToFix.isEmpty()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
var newFile = copyFile(file);
|
||||
|
||||
try (var newRaFile = new RandomAccessFile(newFile, "rwd")) {
|
||||
|
||||
for (var off : cDirEntriesToFix) {
|
||||
var info = readHeader(newRaFile, off);
|
||||
|
||||
newRaFile.seek(off + 0x0a);
|
||||
newRaFile.writeShort(0);
|
||||
|
||||
newRaFile.seek(off + 0x14);
|
||||
newRaFile.writeInt(Integer.reverseBytes((int) info.uncompressedSize));
|
||||
|
||||
}
|
||||
|
||||
for (var off : localHeaderToFix) {
|
||||
var info = readHeader(newRaFile, off);
|
||||
|
||||
newRaFile.seek(off + 0x08);
|
||||
newRaFile.writeShort(0);
|
||||
|
||||
newRaFile.seek(off + 0x12);
|
||||
newRaFile.writeInt(Integer.reverseBytes((int) info.uncompressedSize));
|
||||
|
||||
newRaFile.seek(off + 0x1c);
|
||||
newRaFile.writeShort(0);
|
||||
|
||||
moveBlockBack(newRaFile, off + info.dataOffset, info.uncompressedSize, info.extraLen);
|
||||
}
|
||||
}
|
||||
LOG.info("Input zip file patched: {}", file.getAbsolutePath());
|
||||
return newFile;
|
||||
}
|
||||
|
||||
private static void moveBlockBack(RandomAccessFile file, long offset, long size, long delta) throws IOException {
|
||||
var buffer = new byte[1024 * 1024];
|
||||
|
||||
while (size > 0) {
|
||||
var len = (int) Math.min(buffer.length, size);
|
||||
|
||||
file.seek(offset);
|
||||
file.read(buffer, 0, len);
|
||||
file.seek(offset - delta);
|
||||
file.write(buffer, 0, len);
|
||||
|
||||
size -= len;
|
||||
offset += len;
|
||||
}
|
||||
}
|
||||
|
||||
private static File copyFile(File file) throws IOException {
|
||||
var newFile = FileUtils.createTempFile(file.getName()).toFile();
|
||||
try (var in = new FileInputStream(file)) {
|
||||
try (var out = new FileOutputStream(newFile)) {
|
||||
in.transferTo(out);
|
||||
}
|
||||
}
|
||||
return newFile;
|
||||
}
|
||||
|
||||
private static long findEndOfCentralDir(RandomAccessFile file) throws IOException {
|
||||
var offset = file.length() - 0x15L + 1;
|
||||
|
||||
do {
|
||||
if (offset <= 0) {
|
||||
throw new IllegalArgumentException("File is not a valid ZIP: End of central directory record not found");
|
||||
}
|
||||
file.seek(--offset);
|
||||
} while (Integer.reverseBytes(file.readInt()) != 0x06054b50);
|
||||
|
||||
return offset;
|
||||
}
|
||||
|
||||
private static class HeaderInfo {
|
||||
short compression;
|
||||
long uncompressedSize;
|
||||
long dataOffset;
|
||||
long extraLen;
|
||||
|
||||
boolean validCompression() {
|
||||
return compression == 0x0 || compression == 0x8;
|
||||
}
|
||||
}
|
||||
|
||||
private static HeaderInfo readHeaderVexxed(RandomAccessFile file, long offset) {
|
||||
try {
|
||||
return readHeader(file, offset);
|
||||
} catch (IOException e) {
|
||||
throw new UndeclaredThrowableException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static HeaderInfo readHeader(RandomAccessFile file, long offset) throws IOException {
|
||||
var info = new HeaderInfo();
|
||||
|
||||
file.seek(offset);
|
||||
var signature = Integer.reverseBytes(file.readInt());
|
||||
|
||||
if (signature != 0x02014b50 && signature != 0x04034b50) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Invalid ZIP header signature %x at offset %x",
|
||||
signature, offset));
|
||||
}
|
||||
|
||||
var isCentralHeader = signature == 0x02014b50;
|
||||
var delta = isCentralHeader ? 0 : -2;
|
||||
|
||||
file.seek(offset + 0x0a + delta);
|
||||
info.compression = Short.reverseBytes(file.readShort());
|
||||
|
||||
file.seek(offset + 0x18 + delta);
|
||||
info.uncompressedSize = Integer.toUnsignedLong(Integer.reverseBytes(file.readInt()));
|
||||
|
||||
file.seek(offset + 0x1c + delta);
|
||||
var nameLen = Short.toUnsignedLong(Short.reverseBytes(file.readShort()));
|
||||
info.extraLen = Short.toUnsignedLong(Short.reverseBytes(file.readShort()));
|
||||
var commentLen = 0L;
|
||||
|
||||
if (isCentralHeader) {
|
||||
commentLen = Short.toUnsignedLong(Short.reverseBytes(file.readShort()));
|
||||
}
|
||||
|
||||
info.dataOffset = (isCentralHeader ? 0x2e : 0x1e) + nameLen + info.extraLen + commentLen;
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.args.ResourceNameSource;
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.IFieldInfoRef;
|
||||
@@ -392,7 +391,7 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl
|
||||
private static final ResourceEntry STUB_ENTRY = new ResourceEntry(-1, "stub", "stub", "stub", "");
|
||||
|
||||
private ResourceEntry buildResourceEntry(PackageChunk pkg, String config, int resRef, String typeName, String origKeyName) {
|
||||
if (!ZipSecurity.isValidZipEntryName(origKeyName)) {
|
||||
if (!root.getArgs().getSecurity().isValidEntryName(origKeyName)) {
|
||||
// malicious entry, ignore it
|
||||
// can't return null here, return stub without adding it to storage
|
||||
return STUB_ENTRY;
|
||||
|
||||
@@ -8,9 +8,10 @@ import java.nio.file.StandardCopyOption;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.JadxDecompiler;
|
||||
import jadx.api.ResourceFile;
|
||||
import jadx.api.ResourcesLoader;
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
import jadx.api.security.IJadxSecurity;
|
||||
import jadx.core.dex.visitors.SaveCode;
|
||||
import jadx.core.utils.exceptions.JadxException;
|
||||
import jadx.core.utils.exceptions.JadxRuntimeException;
|
||||
@@ -21,10 +22,12 @@ public class ResourcesSaver implements Runnable {
|
||||
|
||||
private final ResourceFile resourceFile;
|
||||
private final File outDir;
|
||||
private final IJadxSecurity security;
|
||||
|
||||
public ResourcesSaver(File outDir, ResourceFile resourceFile) {
|
||||
public ResourcesSaver(JadxDecompiler decompiler, File outDir, ResourceFile resourceFile) {
|
||||
this.resourceFile = resourceFile;
|
||||
this.outDir = outDir;
|
||||
this.security = decompiler.getArgs().getSecurity();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -52,7 +55,7 @@ public class ResourcesSaver implements Runnable {
|
||||
|
||||
private void save(ResContainer rc, File outDir) {
|
||||
File outFile = new File(outDir, rc.getFileName());
|
||||
if (!ZipSecurity.isInSubDirectory(outDir, outFile)) {
|
||||
if (!security.isInSubDirectory(outDir, outFile)) {
|
||||
LOG.error("Invalid resource name or path traversal attack detected: {}", outFile.getPath());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import jadx.api.impl.InMemoryCodeCache;
|
||||
import jadx.api.metadata.ICodeNodeRef;
|
||||
import jadx.api.usage.impl.EmptyUsageInfoCache;
|
||||
import jadx.api.usage.impl.InMemoryUsageInfoCache;
|
||||
import jadx.cli.JadxAppCommon;
|
||||
import jadx.cli.plugins.JadxFilesGetter;
|
||||
import jadx.core.dex.nodes.ClassNode;
|
||||
import jadx.core.dex.nodes.ProcessState;
|
||||
@@ -66,6 +67,7 @@ public class JadxWrapper {
|
||||
JadxArgs jadxArgs = getSettings().toJadxArgs();
|
||||
jadxArgs.setPluginLoader(new JadxExternalPluginsLoader());
|
||||
project.fillJadxArgs(jadxArgs);
|
||||
JadxAppCommon.applyEnvVars(jadxArgs);
|
||||
|
||||
decompiler = new JadxDecompiler(jadxArgs);
|
||||
guiPluginsContext = initGuiPluginsContext(decompiler, mainWindow);
|
||||
|
||||
@@ -53,7 +53,7 @@ public class HexArea extends AbstractCodeArea {
|
||||
public void load() {
|
||||
byte[] bytes = null;
|
||||
if (binaryNode instanceof JResource) {
|
||||
JResource jResource = ((JResource) binaryNode);
|
||||
JResource jResource = (JResource) binaryNode;
|
||||
try {
|
||||
bytes = ResourcesLoader.decodeStream(jResource.getResFile(), (size, is) -> is.readAllBytes());
|
||||
} catch (JadxException e) {
|
||||
|
||||
@@ -15,7 +15,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.plugins.tools.data.JadxPluginListCache;
|
||||
import jadx.plugins.tools.data.JadxPluginMetadata;
|
||||
@@ -24,6 +23,7 @@ import jadx.plugins.tools.resolvers.github.LocationInfo;
|
||||
import jadx.plugins.tools.resolvers.github.data.Asset;
|
||||
import jadx.plugins.tools.resolvers.github.data.Release;
|
||||
import jadx.plugins.tools.utils.PluginUtils;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
import static jadx.core.utils.GsonUtils.buildGson;
|
||||
import static jadx.plugins.tools.utils.PluginFiles.PLUGINS_LIST_CACHE;
|
||||
@@ -125,14 +125,15 @@ public class JadxPluginsList {
|
||||
private static List<JadxPluginMetadata> loadListBundle(Path tmpListFile) {
|
||||
Gson gson = buildGson();
|
||||
List<JadxPluginMetadata> entries = new ArrayList<>();
|
||||
ZipSecurity.readZipEntries(tmpListFile.toFile(), (entry, in) -> {
|
||||
new ZipReader().visitEntries(tmpListFile.toFile(), entry -> {
|
||||
if (entry.getName().endsWith(".json")) {
|
||||
try (Reader reader = new InputStreamReader(in)) {
|
||||
try (Reader reader = new InputStreamReader(entry.getInputStream())) {
|
||||
entries.addAll(gson.fromJson(reader, LIST_TYPE));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to read plugins list entry: " + entry.getName());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return entries;
|
||||
}
|
||||
|
||||
+11
-6
@@ -3,24 +3,29 @@ package jadx.plugins.input.apkm
|
||||
import jadx.api.plugins.input.ICodeLoader
|
||||
import jadx.api.plugins.input.JadxCodeInput
|
||||
import jadx.api.plugins.utils.CommonFileUtils
|
||||
import jadx.api.plugins.utils.ZipSecurity
|
||||
import jadx.plugins.input.dex.DexInputPlugin
|
||||
import jadx.zip.ZipReader
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
class ApkmCustomCodeInput(
|
||||
private val plugin: ApkmInputPlugin,
|
||||
private val dexInputPlugin: DexInputPlugin,
|
||||
private val zipReader: ZipReader,
|
||||
) : JadxCodeInput {
|
||||
|
||||
override fun loadFiles(input: List<Path>): ICodeLoader {
|
||||
val apkFiles = mutableListOf<File>()
|
||||
for (file in input.map { it.toFile() }) {
|
||||
if (!file.name.endsWith(".apkm")) continue
|
||||
|
||||
// Check if this is a valid APKM file
|
||||
val manifest = ApkmUtils.getManifest(file) ?: continue
|
||||
val manifest = ApkmUtils.getManifest(file, zipReader) ?: continue
|
||||
if (!ApkmUtils.isSupported(manifest)) continue
|
||||
|
||||
// Load all files ending with .apk
|
||||
ZipSecurity.visitZipEntries<Any>(file) { zip, entry ->
|
||||
zipReader.visitEntries<Any>(file) { entry ->
|
||||
if (entry.name.endsWith(".apk")) {
|
||||
val tmpFile = ZipSecurity.getInputStreamForEntry(zip, entry).use {
|
||||
val tmpFile = entry.inputStream.use {
|
||||
CommonFileUtils.saveToTempFile(it, ".apk").toFile()
|
||||
}
|
||||
apkFiles.add(tmpFile)
|
||||
@@ -29,7 +34,7 @@ class ApkmCustomCodeInput(
|
||||
}
|
||||
}
|
||||
|
||||
val codeLoader = plugin.dexInputPlugin.loadFiles(apkFiles.map { it.toPath() })
|
||||
val codeLoader = dexInputPlugin.loadFiles(apkFiles.map { it.toPath() })
|
||||
|
||||
apkFiles.forEach { CommonFileUtils.safeDeleteFile(it) }
|
||||
|
||||
|
||||
+9
-5
@@ -4,21 +4,25 @@ import jadx.api.ResourceFile
|
||||
import jadx.api.ResourcesLoader
|
||||
import jadx.api.plugins.CustomResourcesLoader
|
||||
import jadx.api.plugins.utils.CommonFileUtils
|
||||
import jadx.api.plugins.utils.ZipSecurity
|
||||
import jadx.zip.ZipReader
|
||||
import java.io.File
|
||||
|
||||
class ApkmCustomResourcesLoader : CustomResourcesLoader {
|
||||
class ApkmCustomResourcesLoader(
|
||||
private val zipReader: ZipReader,
|
||||
) : CustomResourcesLoader {
|
||||
private val tmpFiles = mutableListOf<File>()
|
||||
|
||||
override fun load(loader: ResourcesLoader, list: MutableList<ResourceFile>, file: File): Boolean {
|
||||
if (!file.name.endsWith(".apkm")) return false
|
||||
|
||||
// Check if this is a valid APKM file
|
||||
val manifest = ApkmUtils.getManifest(file) ?: return false
|
||||
val manifest = ApkmUtils.getManifest(file, zipReader) ?: return false
|
||||
if (!ApkmUtils.isSupported(manifest)) return false
|
||||
|
||||
// Load all files ending with .apk
|
||||
ZipSecurity.visitZipEntries<Any>(file) { zip, entry ->
|
||||
zipReader.visitEntries<Any>(file) { entry ->
|
||||
if (entry.name.endsWith(".apk")) {
|
||||
val tmpFile = ZipSecurity.getInputStreamForEntry(zip, entry).use {
|
||||
val tmpFile = entry.inputStream.use {
|
||||
CommonFileUtils.saveToTempFile(it, ".apk").toFile()
|
||||
}
|
||||
loader.defaultLoadFile(list, tmpFile, entry.name + "/")
|
||||
|
||||
+3
-6
@@ -6,9 +6,6 @@ import jadx.api.plugins.JadxPluginInfo
|
||||
import jadx.plugins.input.dex.DexInputPlugin
|
||||
|
||||
class ApkmInputPlugin : JadxPlugin {
|
||||
private val codeInput = ApkmCustomCodeInput(this)
|
||||
private val resourcesLoader = ApkmCustomResourcesLoader()
|
||||
internal lateinit var dexInputPlugin: DexInputPlugin
|
||||
|
||||
override fun getPluginInfo() = JadxPluginInfo(
|
||||
"apkm-input",
|
||||
@@ -17,8 +14,8 @@ class ApkmInputPlugin : JadxPlugin {
|
||||
)
|
||||
|
||||
override fun init(context: JadxPluginContext) {
|
||||
dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java)
|
||||
context.addCodeInput(codeInput)
|
||||
context.decompiler.addCustomResourcesLoader(resourcesLoader)
|
||||
val dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java)
|
||||
context.addCodeInput(ApkmCustomCodeInput(dexInputPlugin, context.zipReader))
|
||||
context.decompiler.addCustomResourcesLoader(ApkmCustomResourcesLoader(context.zipReader))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
package jadx.plugins.input.apkm
|
||||
|
||||
import jadx.api.plugins.utils.ZipSecurity
|
||||
import jadx.core.utils.GsonUtils.buildGson
|
||||
import jadx.core.utils.files.FileUtils
|
||||
import jadx.core.utils.files.ZipFile
|
||||
import jadx.zip.ZipReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
|
||||
object ApkmUtils {
|
||||
fun getManifest(file: File): ApkmManifest? {
|
||||
fun getManifest(file: File, zipReader: ZipReader): ApkmManifest? {
|
||||
if (!FileUtils.isZipFile(file)) return null
|
||||
try {
|
||||
ZipFile(file).use { zip ->
|
||||
val manifestEntry = zip.getEntry("info.json") ?: return null
|
||||
return InputStreamReader(ZipSecurity.getInputStreamForEntry(zip, manifestEntry)).use {
|
||||
zipReader.open(file).use { zip ->
|
||||
val manifestEntry = zip.searchEntry("info.json") ?: return null
|
||||
return InputStreamReader(manifestEntry.inputStream).use {
|
||||
buildGson().fromJson(it, ApkmManifest::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
+31
-5
@@ -18,10 +18,12 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.plugins.utils.CommonFileUtils;
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
import jadx.plugins.input.dex.sections.DexConsts;
|
||||
import jadx.plugins.input.dex.sections.DexHeaderV41;
|
||||
import jadx.plugins.input.dex.utils.DexCheckSum;
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.ZipContent;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
public class DexFileLoader {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(DexFileLoader.class);
|
||||
@@ -31,10 +33,16 @@ public class DexFileLoader {
|
||||
|
||||
private final DexInputOptions options;
|
||||
|
||||
private ZipReader zipReader = new ZipReader();
|
||||
|
||||
public DexFileLoader(DexInputOptions options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public void setZipReader(ZipReader zipReader) {
|
||||
this.zipReader = zipReader;
|
||||
}
|
||||
|
||||
public List<DexReader> collectDexFiles(List<Path> pathsList) {
|
||||
return pathsList.stream()
|
||||
.map(Path::toFile)
|
||||
@@ -76,6 +84,13 @@ public class DexFileLoader {
|
||||
}
|
||||
}
|
||||
|
||||
private List<DexReader> loadFromZipEntry(byte[] content, String fileName) {
|
||||
if (isStartWithBytes(content, DexConsts.DEX_FILE_MAGIC) || fileName.endsWith(".dex")) {
|
||||
return loadDexReaders(fileName, content);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public List<DexReader> loadDexReaders(String fileName, byte[] content) {
|
||||
DexHeaderV41 dexHeaderV41 = DexHeaderV41.readIfPresent(content);
|
||||
if (dexHeaderV41 != null) {
|
||||
@@ -106,14 +121,25 @@ public class DexFileLoader {
|
||||
|
||||
private List<DexReader> collectDexFromZip(File file) {
|
||||
List<DexReader> result = new ArrayList<>();
|
||||
try {
|
||||
ZipSecurity.readZipEntries(file, (entry, in) -> {
|
||||
try (ZipContent zip = zipReader.open(file)) {
|
||||
for (IZipEntry entry : zip.getEntries()) {
|
||||
if (entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
result.addAll(load(null, in, entry.getName()));
|
||||
List<DexReader> readers;
|
||||
if (entry.preferBytes()) {
|
||||
readers = loadFromZipEntry(entry.getBytes(), entry.getName());
|
||||
} else {
|
||||
readers = load(null, entry.getInputStream(), entry.getName());
|
||||
}
|
||||
if (!readers.isEmpty()) {
|
||||
result.addAll(readers);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to read zip entry: {}", entry, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to process zip file: {}", file.getAbsolutePath(), e);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ public class DexInputPlugin implements JadxPlugin {
|
||||
public void init(JadxPluginContext context) {
|
||||
context.registerOptions(options);
|
||||
context.addCodeInput(this::loadFiles);
|
||||
loader.setZipReader(context.getZipReader());
|
||||
}
|
||||
|
||||
public ICodeLoader loadFiles(List<Path> input) {
|
||||
|
||||
+23
-9
@@ -17,16 +17,22 @@ import java.util.stream.Stream;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.plugins.JadxPluginContext;
|
||||
import jadx.api.plugins.utils.CommonFileUtils;
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
import jadx.api.security.IJadxSecurity;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
public class JavaConvertLoader {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JavaConvertLoader.class);
|
||||
|
||||
private final JavaConvertOptions options;
|
||||
private final ZipReader zipReader;
|
||||
private final IJadxSecurity security;
|
||||
|
||||
public JavaConvertLoader(JavaConvertOptions options) {
|
||||
public JavaConvertLoader(JavaConvertOptions options, JadxPluginContext context) {
|
||||
this.options = options;
|
||||
this.zipReader = context.getZipReader();
|
||||
this.security = context.getArgs().getSecurity();
|
||||
}
|
||||
|
||||
public ConvertResult process(List<Path> input) {
|
||||
@@ -64,9 +70,13 @@ public class JavaConvertLoader {
|
||||
try (JarOutputStream jo = new JarOutputStream(Files.newOutputStream(jarFile))) {
|
||||
for (Path file : clsFiles) {
|
||||
String clsName = AsmUtils.getNameFromClassFile(file);
|
||||
if (clsName == null || !ZipSecurity.isValidZipEntryName(clsName)) {
|
||||
if (clsName == null) {
|
||||
throw new IOException("Can't read class name from file: " + file);
|
||||
}
|
||||
if (!security.isValidEntryName(clsName)) {
|
||||
LOG.warn("Skip class with invalid name: {}", clsName);
|
||||
continue;
|
||||
}
|
||||
addFileToJar(jo, file, clsName + ".class");
|
||||
}
|
||||
}
|
||||
@@ -82,7 +92,7 @@ public class JavaConvertLoader {
|
||||
PathMatcher aarMatcher = FileSystems.getDefault().getPathMatcher("glob:**.aar");
|
||||
input.stream()
|
||||
.filter(aarMatcher::matches)
|
||||
.forEach(path -> ZipSecurity.readZipEntries(path.toFile(), (entry, in) -> {
|
||||
.forEach(path -> zipReader.readEntries(path.toFile(), (entry, in) -> {
|
||||
try {
|
||||
String entryName = entry.getName();
|
||||
if (entryName.endsWith(".jar")) {
|
||||
@@ -105,8 +115,8 @@ public class JavaConvertLoader {
|
||||
}
|
||||
|
||||
private boolean repackAndConvertJar(ConvertResult result, Path path) throws Exception {
|
||||
// check if jar need a full repackage
|
||||
Boolean repackNeeded = ZipSecurity.visitZipEntries(path.toFile(), (zipFile, zipEntry) -> {
|
||||
// check if jar needs a full repackaging
|
||||
Boolean repackNeeded = zipReader.visitEntries(path.toFile(), zipEntry -> {
|
||||
String entryName = zipEntry.getName();
|
||||
if (zipEntry.isDirectory()) {
|
||||
if (entryName.equals("BOOT-INF/")) {
|
||||
@@ -131,7 +141,7 @@ public class JavaConvertLoader {
|
||||
Path jarFile = Files.createTempFile("jadx-classes-", ".jar");
|
||||
result.addTempPath(jarFile);
|
||||
try (JarOutputStream jo = new JarOutputStream(Files.newOutputStream(jarFile))) {
|
||||
ZipSecurity.readZipEntries(path.toFile(), (entry, in) -> {
|
||||
zipReader.readEntries(path.toFile(), (entry, in) -> {
|
||||
try {
|
||||
String entryName = entry.getName();
|
||||
if (entryName.endsWith(".class")) {
|
||||
@@ -142,10 +152,14 @@ public class JavaConvertLoader {
|
||||
}
|
||||
byte[] clsFileContent = CommonFileUtils.loadBytes(in);
|
||||
String clsName = AsmUtils.getNameFromClassFile(clsFileContent);
|
||||
if (clsName == null || !ZipSecurity.isValidZipEntryName(clsName)) {
|
||||
if (clsName == null) {
|
||||
throw new IOException("Can't read class name from file: " + entryName);
|
||||
}
|
||||
addJarEntry(jo, clsName + ".class", clsFileContent, entry.getLastModifiedTime());
|
||||
if (!security.isValidEntryName(clsName)) {
|
||||
LOG.warn("Ignore class with invalid name: {} from {}", clsName, entry);
|
||||
} else {
|
||||
addJarEntry(jo, clsName + ".class", clsFileContent, null);
|
||||
}
|
||||
} else if (entryName.endsWith(".jar")) {
|
||||
Path tempJar = CommonFileUtils.saveToTempFile(in, ".jar");
|
||||
result.addTempPath(tempJar);
|
||||
|
||||
+3
-2
@@ -17,9 +17,9 @@ public class JavaConvertPlugin implements JadxPlugin, JadxCodeInput {
|
||||
public static final String PLUGIN_ID = "java-convert";
|
||||
|
||||
private final JavaConvertOptions options = new JavaConvertOptions();
|
||||
private final JavaConvertLoader loader = new JavaConvertLoader(options);
|
||||
|
||||
private JadxPluginRuntimeData dexInput;
|
||||
private JavaConvertLoader loader;
|
||||
|
||||
@Override
|
||||
public JadxPluginInfo getPluginInfo() {
|
||||
@@ -32,8 +32,9 @@ public class JavaConvertPlugin implements JadxPlugin, JadxCodeInput {
|
||||
|
||||
@Override
|
||||
public void init(JadxPluginContext context) {
|
||||
dexInput = context.plugins().getById(DexInputPlugin.PLUGIN_ID);
|
||||
context.registerOptions(options);
|
||||
dexInput = context.plugins().getById(DexInputPlugin.PLUGIN_ID);
|
||||
loader = new JavaConvertLoader(options, context);
|
||||
context.addCodeInput(this);
|
||||
}
|
||||
|
||||
|
||||
+56
-9
@@ -5,6 +5,7 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
@@ -17,7 +18,11 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import jadx.api.plugins.utils.CommonFileUtils;
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
import jadx.core.plugins.files.TempFilesGetter;
|
||||
import jadx.core.utils.files.FileUtils;
|
||||
import jadx.zip.IZipEntry;
|
||||
import jadx.zip.ZipContent;
|
||||
import jadx.zip.ZipReader;
|
||||
|
||||
public class JavaInputLoader {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(JavaInputLoader.class);
|
||||
@@ -26,8 +31,24 @@ public class JavaInputLoader {
|
||||
private static final byte[] JAVA_CLASS_FILE_MAGIC = { (byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE };
|
||||
private static final byte[] ZIP_FILE_MAGIC = { 0x50, 0x4B, 0x03, 0x04 };
|
||||
|
||||
private final ZipReader zipReader;
|
||||
private final Path tempPath;
|
||||
|
||||
private int classUniqId = 1;
|
||||
|
||||
public JavaInputLoader(ZipReader zipReader, Path tempPath) {
|
||||
this.zipReader = zipReader;
|
||||
this.tempPath = tempPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will use zip reader with default options and ignore provided in jadx args
|
||||
*/
|
||||
@Deprecated
|
||||
public JavaInputLoader() {
|
||||
this(new ZipReader(), TempFilesGetter.INSTANCE.getTempDir());
|
||||
}
|
||||
|
||||
public List<JavaClassReader> collectFiles(List<Path> inputFiles) {
|
||||
return inputFiles.stream()
|
||||
.map(Path::toFile)
|
||||
@@ -78,6 +99,23 @@ public class JavaInputLoader {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private List<JavaClassReader> loadReaderFromZipEntry(byte[] content, String name, String parentFileName) throws IOException {
|
||||
if (isStartWithBytes(content, JAVA_CLASS_FILE_MAGIC) || name.endsWith(".class")) {
|
||||
String source = concatSource(parentFileName, name);
|
||||
JavaClassReader reader = new JavaClassReader(getNextUniqId(), source, content);
|
||||
return Collections.singletonList(reader);
|
||||
}
|
||||
if (isStartWithBytes(content, ZIP_FILE_MAGIC) || CommonFileUtils.isZipFileExt(name)) {
|
||||
Path tempZip = Files.createTempFile(tempPath, "temp", ".zip");
|
||||
FileUtils.writeFile(tempZip, content);
|
||||
File zipFile = tempZip.toFile();
|
||||
List<JavaClassReader> readers = collectFromZip(zipFile, concatSource(parentFileName, name));
|
||||
CommonFileUtils.safeDeleteFile(zipFile);
|
||||
return readers;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private static String concatSource(@Nullable String parentFileName, String name) {
|
||||
if (parentFileName == null) {
|
||||
return name;
|
||||
@@ -87,19 +125,28 @@ public class JavaInputLoader {
|
||||
|
||||
private List<JavaClassReader> collectFromZip(File file, String name) {
|
||||
List<JavaClassReader> result = new ArrayList<>();
|
||||
try {
|
||||
ZipSecurity.readZipEntries(file, (entry, in) -> {
|
||||
try (ZipContent zip = zipReader.open(file)) {
|
||||
for (IZipEntry entry : zip.getEntries()) {
|
||||
if (entry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
String entryName = entry.getName();
|
||||
if (entryName.startsWith("META-INF/versions/")) {
|
||||
// skip classes for different java versions
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
String entryName = entry.getName();
|
||||
if (entryName.startsWith("META-INF/versions/")) {
|
||||
// skip classes for different java versions
|
||||
return;
|
||||
List<JavaClassReader> readers;
|
||||
if (entry.preferBytes()) {
|
||||
readers = loadReaderFromZipEntry(entry.getBytes(), entryName, name);
|
||||
} else {
|
||||
readers = loadReader(entry.getInputStream(), entryName, null, name);
|
||||
}
|
||||
result.addAll(loadReader(in, entryName, null, name));
|
||||
result.addAll(readers);
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to read zip entry: {}", entry, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.error("Failed to process zip file: {}", name, e);
|
||||
}
|
||||
|
||||
+8
-1
@@ -25,7 +25,14 @@ public class JavaInputPlugin implements JadxPlugin {
|
||||
|
||||
@Override
|
||||
public void init(JadxPluginContext context) {
|
||||
context.addCodeInput(JavaInputPlugin::loadClassFiles);
|
||||
context.addCodeInput(inputFiles -> {
|
||||
JavaInputLoader loader = new JavaInputLoader(context.getZipReader(), context.files().getPluginTempDir());
|
||||
List<JavaClassReader> readers = loader.collectFiles(inputFiles);
|
||||
if (readers.isEmpty()) {
|
||||
return EmptyCodeLoader.INSTANCE;
|
||||
}
|
||||
return new JavaLoadResult(readers, null);
|
||||
});
|
||||
}
|
||||
|
||||
public static ICodeLoader loadClassFiles(List<Path> inputFiles) {
|
||||
|
||||
+12
-8
@@ -3,25 +3,29 @@ package jadx.plugins.input.xapk
|
||||
import jadx.api.plugins.input.ICodeLoader
|
||||
import jadx.api.plugins.input.JadxCodeInput
|
||||
import jadx.api.plugins.utils.CommonFileUtils
|
||||
import jadx.api.plugins.utils.ZipSecurity
|
||||
import jadx.core.utils.files.ZipFile
|
||||
import jadx.plugins.input.dex.DexInputPlugin
|
||||
import jadx.zip.ZipReader
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
class XapkCustomCodeInput(
|
||||
private val plugin: XapkInputPlugin,
|
||||
private val dexInputPlugin: DexInputPlugin,
|
||||
private val zipReader: ZipReader,
|
||||
) : JadxCodeInput {
|
||||
|
||||
override fun loadFiles(input: List<Path>): ICodeLoader {
|
||||
val apkFiles = mutableListOf<File>()
|
||||
for (file in input.map { it.toFile() }) {
|
||||
val manifest = XapkUtils.getManifest(file) ?: continue
|
||||
if (!file.name.endsWith(".xapk")) continue
|
||||
|
||||
val manifest = XapkUtils.getManifest(file, zipReader) ?: continue
|
||||
if (!XapkUtils.isSupported(manifest)) continue
|
||||
|
||||
ZipFile(file).use { zip ->
|
||||
zipReader.open(file).use { zip ->
|
||||
for (splitApk in manifest.splitApks) {
|
||||
val splitApkEntry = zip.getEntry(splitApk.file)
|
||||
val splitApkEntry = zip.searchEntry(splitApk.file)
|
||||
if (splitApkEntry != null) {
|
||||
val tmpFile = ZipSecurity.getInputStreamForEntry(zip, splitApkEntry).use {
|
||||
val tmpFile = splitApkEntry.inputStream.use {
|
||||
CommonFileUtils.saveToTempFile(it, ".apk").toFile()
|
||||
}
|
||||
apkFiles.add(tmpFile)
|
||||
@@ -30,7 +34,7 @@ class XapkCustomCodeInput(
|
||||
}
|
||||
}
|
||||
|
||||
val codeLoader = plugin.dexInputPlugin.loadFiles(apkFiles.map { it.toPath() })
|
||||
val codeLoader = dexInputPlugin.loadFiles(apkFiles.map { it.toPath() })
|
||||
|
||||
apkFiles.forEach { CommonFileUtils.safeDeleteFile(it) }
|
||||
|
||||
|
||||
+7
-5
@@ -4,20 +4,22 @@ import jadx.api.ResourceFile
|
||||
import jadx.api.ResourcesLoader
|
||||
import jadx.api.plugins.CustomResourcesLoader
|
||||
import jadx.api.plugins.utils.CommonFileUtils
|
||||
import jadx.api.plugins.utils.ZipSecurity
|
||||
import jadx.zip.ZipReader
|
||||
import java.io.File
|
||||
|
||||
class XapkCustomResourcesLoader : CustomResourcesLoader {
|
||||
class XapkCustomResourcesLoader(private val zipReader: ZipReader) : CustomResourcesLoader {
|
||||
private val tmpFiles = mutableListOf<File>()
|
||||
|
||||
override fun load(loader: ResourcesLoader, list: MutableList<ResourceFile>, file: File): Boolean {
|
||||
val manifest = XapkUtils.getManifest(file) ?: return false
|
||||
if (!file.name.endsWith(".xapk")) return false
|
||||
|
||||
val manifest = XapkUtils.getManifest(file, zipReader) ?: return false
|
||||
if (!XapkUtils.isSupported(manifest)) return false
|
||||
|
||||
val apkEntries = manifest.splitApks.map { it.file }.toHashSet()
|
||||
ZipSecurity.visitZipEntries<Any>(file) { zip, entry ->
|
||||
zipReader.visitEntries(file) { entry ->
|
||||
if (apkEntries.contains(entry.name)) {
|
||||
val tmpFile = ZipSecurity.getInputStreamForEntry(zip, entry).use {
|
||||
val tmpFile = entry.inputStream.use {
|
||||
CommonFileUtils.saveToTempFile(it, ".apk").toFile()
|
||||
}
|
||||
loader.defaultLoadFile(list, tmpFile, entry.name + "/")
|
||||
|
||||
+9
-11
@@ -3,22 +3,20 @@ package jadx.plugins.input.xapk
|
||||
import jadx.api.plugins.JadxPlugin
|
||||
import jadx.api.plugins.JadxPluginContext
|
||||
import jadx.api.plugins.JadxPluginInfo
|
||||
import jadx.api.plugins.JadxPluginInfoBuilder
|
||||
import jadx.plugins.input.dex.DexInputPlugin
|
||||
|
||||
class XapkInputPlugin : JadxPlugin {
|
||||
private val codeInput = XapkCustomCodeInput(this)
|
||||
private val resourcesLoader = XapkCustomResourcesLoader()
|
||||
internal lateinit var dexInputPlugin: DexInputPlugin
|
||||
|
||||
override fun getPluginInfo() = JadxPluginInfo(
|
||||
"xapk-input",
|
||||
"XAPK Input",
|
||||
"Load .xapk files",
|
||||
)
|
||||
override fun getPluginInfo(): JadxPluginInfo =
|
||||
JadxPluginInfoBuilder.pluginId("xapk-input")
|
||||
.name("XAPK Input")
|
||||
.description("Load .xapk files")
|
||||
.build()
|
||||
|
||||
override fun init(context: JadxPluginContext) {
|
||||
dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java)
|
||||
context.addCodeInput(codeInput)
|
||||
context.decompiler.addCustomResourcesLoader(resourcesLoader)
|
||||
val dexInputPlugin = context.plugins().getInstance(DexInputPlugin::class.java)
|
||||
context.addCodeInput(XapkCustomCodeInput(dexInputPlugin, context.zipReader))
|
||||
context.decompiler.addCustomResourcesLoader(XapkCustomResourcesLoader(context.zipReader))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
package jadx.plugins.input.xapk
|
||||
|
||||
import jadx.api.plugins.utils.ZipSecurity
|
||||
import jadx.core.utils.GsonUtils.buildGson
|
||||
import jadx.core.utils.files.FileUtils
|
||||
import jadx.core.utils.files.ZipFile
|
||||
import jadx.zip.ZipReader
|
||||
import java.io.File
|
||||
import java.io.InputStreamReader
|
||||
|
||||
object XapkUtils {
|
||||
fun getManifest(file: File): XapkManifest? {
|
||||
fun getManifest(file: File, zipReader: ZipReader): XapkManifest? {
|
||||
if (!FileUtils.isZipFile(file)) return null
|
||||
try {
|
||||
ZipFile(file).use { zip ->
|
||||
val manifestEntry = zip.getEntry("manifest.json") ?: return null
|
||||
return InputStreamReader(ZipSecurity.getInputStreamForEntry(zip, manifestEntry)).use {
|
||||
zipReader.open(file).use { zip ->
|
||||
val manifestEntry = zip.searchEntry("manifest.json") ?: return null
|
||||
return InputStreamReader(manifestEntry.inputStream).use {
|
||||
buildGson().fromJson(it, XapkManifest::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ include("jadx-gui")
|
||||
include("jadx-plugins-tools")
|
||||
|
||||
include("jadx-commons:jadx-app-commons")
|
||||
include("jadx-commons:jadx-zip")
|
||||
|
||||
include("jadx-plugins:jadx-input-api")
|
||||
include("jadx-plugins:jadx-dex-input")
|
||||
|
||||
Reference in New Issue
Block a user