未验证 提交 71617a1c 编写于 作者: S sergey-wowwow 提交者: GitHub

feat(res): fix duplicate entries and deobfuscate file names in XML resources (PR #995)

* Fixes dublicates entries in XML resources
* can't use binary search on this list
* add entry config to name comparator, preserve renames by id, improve performance
* Deobf resource files
* Add break
* Changes ResourceFile
Co-authored-by: Nsergey-wowwow <bugi@MacBook-Pro.local>
Co-authored-by: NSkylot <skylot@gmail.com>
上级 9f684937
......@@ -4,6 +4,7 @@ import java.io.File;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.entry.ResourceEntry;
public class ResourceFile {
......@@ -34,6 +35,7 @@ public class ResourceFile {
private final String name;
private final ResourceType type;
private ZipRef zipRef;
private String deobfName;
public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
if (!ZipSecurity.isValidZipEntryName(name)) {
......@@ -48,10 +50,14 @@ public class ResourceFile {
this.type = type;
}
public String getName() {
public String getOriginalName() {
return name;
}
public String getDeobfName() {
return deobfName != null ? deobfName : name;
}
public ResourceType getType() {
return type;
}
......@@ -64,6 +70,15 @@ public class ResourceFile {
this.zipRef = zipRef;
}
public void setAlias(ResourceEntry ri) {
int index = name.lastIndexOf('.');
deobfName = String.format("%s%s/%s%s",
ri.getTypeName(),
ri.getConfig(),
ri.getKeyName(),
index == -1 ? "" : name.substring(index));
}
public ZipRef getZipRef() {
return zipRef;
}
......
......@@ -12,6 +12,6 @@ public class ResourceFileContent extends ResourceFile {
@Override
public ResContainer loadContent() {
return ResContainer.textResource(getName(), content);
return ResContainer.textResource(getDeobfName(), content);
}
}
......@@ -55,7 +55,7 @@ public final class ResourcesLoader {
try {
ZipRef zipRef = rf.getZipRef();
if (zipRef == null) {
File file = new File(rf.getName());
File file = new File(rf.getOriginalName());
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
return decoder.decode(file.length(), inputStream);
}
......@@ -74,7 +74,7 @@ public final class ResourcesLoader {
}
}
} catch (Exception e) {
throw new JadxException("Error decode: " + rf.getName(), e);
throw new JadxException("Error decode: " + rf.getDeobfName(), e);
}
}
......@@ -86,7 +86,7 @@ public final class ResourcesLoader {
CodeWriter cw = new CodeWriter();
cw.add("Error decode ").add(rf.getType().toString().toLowerCase());
Utils.appendStackTrace(cw, e.getCause());
return ResContainer.textResource(rf.getName(), cw.finish());
return ResContainer.textResource(rf.getDeobfName(), cw.finish());
}
}
......@@ -96,7 +96,7 @@ public final class ResourcesLoader {
case MANIFEST:
case XML:
ICodeInfo content = jadxRef.getXmlParser().parse(inputStream);
return ResContainer.textResource(rf.getName(), content);
return ResContainer.textResource(rf.getOriginalName(), content);
case ARSC:
return new ResTableParser(jadxRef.getRoot()).decodeFiles(inputStream);
......@@ -110,12 +110,12 @@ public final class ResourcesLoader {
}
private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) {
String name = rf.getName();
String name = rf.getOriginalName();
if (name.endsWith(".9.png")) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Res9patchStreamDecoder decoder = new Res9patchStreamDecoder();
decoder.decode(inputStream, os);
return ResContainer.decodedData(rf.getName(), os.toByteArray());
return ResContainer.decodedData(rf.getDeobfName(), os.toByteArray());
} catch (Exception e) {
LOG.error("Failed to decode 9-patch png image, path: {}", name, e);
}
......
......@@ -39,6 +39,8 @@ import jadx.core.utils.android.AndroidResourcesUtils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ResTableParser;
import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;
public class RootNode {
private static final Logger LOG = LoggerFactory.getLogger(RootNode.class);
......@@ -131,13 +133,14 @@ public class RootNode {
return;
}
try {
ResourceStorage resStorage = ResourcesLoader.decodeStream(arsc, (size, is) -> {
ResTableParser parser = new ResTableParser(this);
parser.decode(is);
return parser.getResStorage();
ResTableParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> {
ResTableParser tableParser = new ResTableParser(this);
tableParser.decode(is);
return tableParser;
});
if (resStorage != null) {
processResources(resStorage);
if (parser != null) {
processResources(parser.getResStorage());
updateObfuscatedFiles(parser, resources);
}
} catch (Exception e) {
LOG.error("Failed to parse '.arsc' file", e);
......@@ -163,6 +166,20 @@ public class RootNode {
}
}
private void updateObfuscatedFiles(ResTableParser parser, List<ResourceFile> resources) {
ResourceStorage resStorage = parser.getResStorage();
ValuesParser valuesParser = new ValuesParser(this, parser.getStrings(), resStorage.getResourcesNames());
for (int i = 0; i < resources.size(); i++) {
ResourceFile resource = resources.get(i);
for (ResourceEntry ri : parser.getResStorage().getResources()) {
if (resource.getOriginalName().equals(valuesParser.getValueString(ri))) {
resource.setAlias(ri);
break;
}
}
}
}
private void initInnerClasses() {
// move inner classes
List<ClassNode> inner = new ArrayList<>();
......
......@@ -30,7 +30,7 @@ public class ResContainer implements Comparable<ResContainer> {
}
public static ResContainer resourceFileLink(ResourceFile resFile) {
return new ResContainer(resFile.getName(), Collections.emptyList(), resFile, DataType.RES_LINK);
return new ResContainer(resFile.getDeobfName(), Collections.emptyList(), resFile, DataType.RES_LINK);
}
public static ResContainer resourceTable(String name, List<ResContainer> subFiles, ICodeInfo rootContent) {
......
......@@ -5,6 +5,7 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.slf4j.Logger;
......@@ -241,12 +242,12 @@ public class ResTableParser extends CommonBinaryParser {
is.checkPos(entriesStart, "Expected entry start");
for (int i = 0; i < entryCount; i++) {
if (entryIndexes[i] != NO_ENTRY) {
parseEntry(pkg, id, i, config);
parseEntry(pkg, id, i, config.getQualifiers());
}
}
}
private void parseEntry(PackageChunk pkg, int typeId, int entryId, EntryConfig config) throws IOException {
private void parseEntry(PackageChunk pkg, int typeId, int entryId, String config) throws IOException {
int size = is.readInt16();
int flags = is.readInt16();
int key = is.readInt32();
......@@ -256,32 +257,50 @@ public class ResTableParser extends CommonBinaryParser {
int resRef = pkg.getId() << 24 | typeId << 16 | entryId;
String typeName = pkg.getTypeStrings()[typeId - 1];
String keyName = pkg.getKeyStrings()[key];
if (keyName.isEmpty()) {
FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
if (constField != null) {
keyName = constField.getName();
constField.add(AFlag.DONT_RENAME);
} else {
keyName = "RES_" + resRef; // autogenerate key name
}
String origKeyName = pkg.getKeyStrings()[key];
ResourceEntry newResEntry = new ResourceEntry(resRef, pkg.getName(), typeName, getResName(resRef, origKeyName), config);
ResourceEntry prevResEntry = resStorage.searchEntryWithSameName(newResEntry);
if (prevResEntry != null) {
newResEntry = newResEntry.copyWithId();
// rename also previous entry for consistency
ResourceEntry replaceForPrevEntry = prevResEntry.copyWithId();
resStorage.replace(prevResEntry, replaceForPrevEntry);
resStorage.addRename(replaceForPrevEntry);
}
if (!Objects.equals(origKeyName, newResEntry.getKeyName())) {
resStorage.addRename(newResEntry);
}
ResourceEntry ri = new ResourceEntry(resRef, pkg.getName(), typeName, keyName);
ri.setConfig(config);
if ((flags & FLAG_COMPLEX) != 0 || size == 16) {
int parentRef = is.readInt32();
int count = is.readInt32();
ri.setParentRef(parentRef);
newResEntry.setParentRef(parentRef);
List<RawNamedValue> values = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
values.add(parseValueMap());
}
ri.setNamedValues(values);
newResEntry.setNamedValues(values);
} else {
ri.setSimpleValue(parseValue());
newResEntry.setSimpleValue(parseValue());
}
resStorage.add(newResEntry);
}
private String getResName(int resRef, String origKeyName) {
String renamedKey = resStorage.getRename(resRef);
if (renamedKey != null) {
return renamedKey;
}
if (!origKeyName.isEmpty()) {
return origKeyName;
}
FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
if (constField != null) {
constField.add(AFlag.DONT_RENAME);
return constField.getName();
}
resStorage.add(ri);
return "RES_" + resRef; // autogenerate key name
}
private RawNamedValue parseValueMap() throws IOException {
......
......@@ -211,7 +211,7 @@ public class ResXmlGen {
private String getFileName(ResourceEntry ri) {
StringBuilder sb = new StringBuilder();
String qualifiers = ri.getConfig().getQualifiers();
String qualifiers = ri.getConfig();
sb.append("res/values");
if (!qualifiers.isEmpty()) {
sb.append(qualifiers);
......
package jadx.core.xmlgen;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import jadx.core.xmlgen.entry.ResourceEntry;
public class ResourceStorage {
private static final Comparator<ResourceEntry> RES_ENTRY_NAME_COMPARATOR = Comparator
.comparing(ResourceEntry::getConfig)
.thenComparing(ResourceEntry::getTypeName)
.thenComparing(ResourceEntry::getKeyName);
private final List<ResourceEntry> list = new ArrayList<>();
private String appPackage;
public Collection<ResourceEntry> getResources() {
return list;
/**
* Names in one config and type must be unique
*/
private final Map<ResourceEntry, ResourceEntry> uniqNameEntries = new TreeMap<>(RES_ENTRY_NAME_COMPARATOR);
/**
* Preserve same name for same id across different configs
*/
private final Map<Integer, String> renames = new HashMap<>();
public void add(ResourceEntry resEntry) {
list.add(resEntry);
uniqNameEntries.put(resEntry, resEntry);
}
public void replace(ResourceEntry prevResEntry, ResourceEntry newResEntry) {
int idx = list.indexOf(prevResEntry);
if (idx != -1) {
list.set(idx, newResEntry);
}
// don't remove from unique names so old name stays occupied
}
public void addRename(ResourceEntry entry) {
addRename(entry.getId(), entry.getKeyName());
}
public void addRename(int id, String keyName) {
renames.put(id, keyName);
}
public void add(ResourceEntry ri) {
list.add(ri);
public String getRename(int id) {
return renames.get(id);
}
public ResourceEntry searchEntryWithSameName(ResourceEntry resourceEntry) {
return uniqNameEntries.get(resourceEntry);
}
public void finish() {
list.sort(Comparator.comparingInt(ResourceEntry::getId));
uniqNameEntries.clear();
renames.clear();
}
public ResourceEntry getByRef(int refId) {
ResourceEntry key = new ResourceEntry(refId);
int index = Collections.binarySearch(list, key, Comparator.comparingInt(ResourceEntry::getId));
if (index < 0) {
return null;
}
return list.get(index);
public Iterable<ResourceEntry> getResources() {
return list;
}
public String getAppPackage() {
......
......@@ -8,21 +8,30 @@ public final class ResourceEntry {
private final String pkgName;
private final String typeName;
private final String keyName;
private final String config;
private int parentRef;
private RawValue simpleValue;
private List<RawNamedValue> namedValues;
private EntryConfig config;
public ResourceEntry(int id, String pkgName, String typeName, String keyName) {
public ResourceEntry(int id, String pkgName, String typeName, String keyName, String config) {
this.id = id;
this.pkgName = pkgName;
this.typeName = typeName;
this.keyName = keyName;
this.config = config;
}
public ResourceEntry copy(String newKeyName) {
ResourceEntry copy = new ResourceEntry(id, pkgName, typeName, newKeyName, config);
copy.parentRef = this.parentRef;
copy.simpleValue = this.simpleValue;
copy.namedValues = this.namedValues;
return copy;
}
public ResourceEntry(int id) {
this(id, "", "", "");
public ResourceEntry copyWithId() {
return copy(keyName + "_RES_" + id);
}
public int getId() {
......@@ -41,6 +50,10 @@ public final class ResourceEntry {
return keyName;
}
public String getConfig() {
return config;
}
public void setParentRef(int parentRef) {
this.parentRef = parentRef;
}
......@@ -65,14 +78,6 @@ public final class ResourceEntry {
return namedValues;
}
public void setConfig(EntryConfig config) {
this.config = config;
}
public EntryConfig getConfig() {
return config;
}
@Override
public String toString() {
return " 0x" + Integer.toHexString(id) + " (" + id + ')' + config + " = " + typeName + '.' + keyName;
......
......@@ -243,7 +243,7 @@ public abstract class IntegrationTest extends TestUtils {
Integer id = entry.getKey();
String name = entry.getValue();
String[] parts = name.split("\\.");
resStorage.add(new ResourceEntry(id, "", parts[0], parts[1]));
resStorage.add(new ResourceEntry(id, "", parts[0], parts[1], ""));
}
root.processResources(resStorage);
}
......
......@@ -33,7 +33,7 @@ public class ApkSignature extends JNode {
public static ApkSignature getApkSignature(JadxWrapper wrapper) {
// Only show the ApkSignature node if an AndroidManifest.xml is present.
// Without a manifest the Google ApkVerifier refuses to work.
if (wrapper.getResources().stream().noneMatch(r -> "AndroidManifest.xml".equals(r.getName()))) {
if (wrapper.getResources().stream().noneMatch(r -> "AndroidManifest.xml".equals(r.getOriginalName()))) {
return null;
}
File openFile = wrapper.getOpenFile();
......
......@@ -218,7 +218,7 @@ public class JResource extends JLoadableNode implements Comparable<JResource> {
return SyntaxConstants.SYNTAX_STYLE_XML;
default:
String syntax = getSyntaxByExtension(resFile.getName());
String syntax = getSyntaxByExtension(resFile.getDeobfName());
if (syntax != null) {
return syntax;
}
......
......@@ -52,9 +52,9 @@ public class JRoot extends JNode {
for (ResourceFile rf : resources) {
String rfName;
if (rf.getZipRef() != null) {
rfName = rf.getName();
rfName = rf.getDeobfName();
} else {
rfName = new File(rf.getName()).getName();
rfName = new File(rf.getDeobfName()).getName();
}
String[] parts = new File(rfName).getPath().split(splitPathStr);
JResource curRf = root;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册