Correct Attribute Resolution (#3123)

* refactor: rename package_ to pkgId

* refactor: ResAttrDecoder takes ResTable

* fix: fallback on attr decoding to proper prefix

* fix: reverse order of processing to trust string pool 2nd

Android prefers the resource map value over what the String block has.
This can be seen quite often in obfuscated apps where values such as:
 <item android:state_enabled="true" app:state_collapsed="false" app:state_collapsible="true">
Are improperly decoded when trusting the String block.
Leveraging the resource map allows us to get the proper value.
 <item android:state_enabled="true" app:d2="false" app:d3="true">

* refactor: set default value if no string/res value
This commit is contained in:
Connor Tumbleson 2023-07-04 12:06:53 -04:00 committed by GitHub
parent f42ce82f0d
commit 79cfdd179c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 67 additions and 80 deletions

View File

@ -136,9 +136,7 @@ public class ResourcesDecoder {
// Set ResAttrDecoder
duo.m2.setAttrDecoder(new ResAttrDecoder());
ResAttrDecoder attrDecoder = duo.m2.getAttrDecoder();
// Fake ResPackage
attrDecoder.setCurrentPackage(new ResPackage(resTable, 0, null));
attrDecoder.setResTable(resTable);
Directory inApk, out;
try {
@ -159,8 +157,7 @@ public class ResourcesDecoder {
Duo<ResFileDecoder, AXmlResourceParser> duo = getManifestFileDecoder(true);
ResFileDecoder fileDecoder = duo.m1;
ResAttrDecoder attrDecoder = duo.m2.getAttrDecoder();
attrDecoder.setCurrentPackage(resTable.listMainPackages().iterator().next());
attrDecoder.setResTable(resTable);
Directory inApk, out;
try {
@ -259,8 +256,7 @@ public class ResourcesDecoder {
Duo<ResFileDecoder, AXmlResourceParser> duo = getResFileDecoder();
ResFileDecoder fileDecoder = duo.m1;
ResAttrDecoder attrDecoder = duo.m2.getAttrDecoder();
attrDecoder.setCurrentPackage(resTable.listMainPackages().iterator().next());
attrDecoder.setResTable(resTable);
Directory in, out;
try {
@ -273,7 +269,6 @@ public class ResourcesDecoder {
ExtMXSerializer xmlSerializer = getResXmlSerializer();
for (ResPackage pkg : resTable.listMainPackages()) {
attrDecoder.setCurrentPackage(pkg);
LOGGER.info("Decoding file-resources...");
for (ResResource res : pkg.listFiles()) {

View File

@ -17,22 +17,21 @@
package brut.androlib.res.data;
public class ResID {
public final int package_;
public final int pkgId;
public final int type;
public final int entry;
public final int id;
public ResID(int package_, int type, int entry) {
this(package_, type, entry, (package_ << 24) + (type << 16) + entry);
public ResID(int pkgId, int type, int entry) {
this(pkgId, type, entry, (pkgId << 24) + (type << 16) + entry);
}
public ResID(int id) {
this((id >> 24) & 0xff, (id >> 16) & 0x000000ff, id & 0x0000ffff, id);
}
public ResID(int package_, int type, int entry, int id) {
this.package_ = (package_ == 0) ? 2 : package_;
public ResID(int pkgId, int type, int entry, int id) {
this.pkgId = (pkgId == 0) ? 2 : pkgId;
this.type = type;
this.entry = entry;
this.id = id;

View File

@ -82,7 +82,7 @@ public class ResTable {
}
public ResResSpec getResSpec(ResID resID) throws AndrolibException {
return getPackage(resID.package_).getResSpec(resID);
return getPackage(resID.pkgId).getResSpec(resID);
}
public Set<ResPackage> listMainPackages() {

View File

@ -282,8 +282,8 @@ public class AXmlResourceParser implements XmlResourceParser {
String value = m_strings.getString(namespace);
if (value == null || value.length() == 0) {
ResID resourceId = new ResID(getAttributeNameResource(index));
if (resourceId.package_ == PRIVATE_PKG_ID) {
ResID resId = new ResID(getAttributeNameResource(index));
if (resId.pkgId == PRIVATE_PKG_ID) {
value = getNonDefaultNamespaceUri(offset);
} else {
value = android_ns;
@ -327,29 +327,32 @@ public class AXmlResourceParser implements XmlResourceParser {
return "";
}
String value = m_strings.getString(name);
String namespace = getAttributeNamespace(index);
String resourceMapValue;
String stringBlockValue = m_strings.getString(name);
int resourceId = getAttributeNameResource(index);
// If attribute name is lacking or a private namespace emerges,
// retrieve the exact attribute name by its id.
if (value == null || value.length() == 0) {
try {
value = mAttrDecoder.decodeManifestAttr(getAttributeNameResource(index));
if (value == null) {
value = "";
}
} catch (AndrolibException e) {
value = "";
}
} else if (! namespace.equals(android_ns)) {
try {
String obfuscatedName = mAttrDecoder.decodeManifestAttr(getAttributeNameResource(index));
if (! (obfuscatedName == null || obfuscatedName.equals(value))) {
value = obfuscatedName;
}
} catch (AndrolibException ignored) {}
try {
resourceMapValue = mAttrDecoder.decodeFromResourceId(resourceId);
} catch (AndrolibException ignored) {
resourceMapValue = null;
}
return value;
// Android prefers the resource map value over what the String block has.
// This can be seen quite often in obfuscated apps where values such as:
// <item android:state_enabled="true" app:state_collapsed="false" app:state_collapsible="true">
// Are improperly decoded when trusting the String block.
// Leveraging the resource map allows us to get the proper value.
// <item android:state_enabled="true" app:d2="false" app:d3="true">
if (resourceMapValue != null) {
return resourceMapValue;
}
if (stringBlockValue != null) {
return stringBlockValue;
}
// In this case we have a bogus resource. If it was not found in either.
return "APKTOOL_MISSING_" + Integer.toHexString(resourceId);
}
@Override
@ -383,18 +386,22 @@ public class AXmlResourceParser implements XmlResourceParser {
if (mAttrDecoder != null) {
try {
String value = valueRaw == -1 ? null : ResXmlEncoders.escapeXmlChars(m_strings.getString(valueRaw));
String obfuscatedValue = mAttrDecoder.decodeManifestAttr(valueData);
String stringBlockValue = valueRaw == -1 ? null : ResXmlEncoders.escapeXmlChars(m_strings.getString(valueRaw));
String resourceMapValue = mAttrDecoder.decodeFromResourceId(valueData);
String value = stringBlockValue;
if (! (value == null || obfuscatedValue == null)) {
int slashPos = value.lastIndexOf("/");
if (stringBlockValue != null && resourceMapValue != null) {
int slashPos = stringBlockValue.lastIndexOf("/");
int colonPos = stringBlockValue.lastIndexOf(":");
// Handle a value with a format of "@yyy/xxx", but avoid "@yyy/zzz:xxx"
if (slashPos != -1) {
// Handle a value with a format of "@yyy/xxx"
String dir = value.substring(0, slashPos);
value = dir + "/"+ obfuscatedValue;
} else if (! value.equals(obfuscatedValue)) {
value = obfuscatedValue;
if (colonPos == -1) {
String type = stringBlockValue.substring(0, slashPos);
value = type + "/" + resourceMapValue;
}
} else if (! stringBlockValue.equals(resourceMapValue)) {
value = resourceMapValue;
}
}

View File

@ -17,72 +17,58 @@
package brut.androlib.res.decoder;
import brut.androlib.exceptions.AndrolibException;
import brut.androlib.exceptions.CantFindFrameworkResException;
import brut.androlib.exceptions.UndefinedResObjectException;
import brut.androlib.res.data.ResID;
import brut.androlib.res.data.ResPackage;
import brut.androlib.res.data.ResResSpec;
import brut.androlib.res.data.ResTable;
import brut.androlib.res.data.value.ResAttr;
import brut.androlib.res.data.value.ResScalarValue;
public class ResAttrDecoder {
public String decode(int type, int value, String rawValue, int attrResId)
throws AndrolibException {
ResScalarValue resValue = mCurrentPackage.getValueFactory().factory(
type, value, rawValue);
throws AndrolibException {
ResScalarValue resValue = mResTable.getCurrentResPackage().getValueFactory().factory(type, value, rawValue);
String decoded = null;
if (attrResId > 0) {
try {
ResAttr attr = (ResAttr) getCurrentPackage().getResTable()
.getResSpec(attrResId).getDefaultResource().getValue();
ResAttr attr = (ResAttr) mResTable.getResSpec(attrResId).getDefaultResource().getValue();
decoded = attr.convertToResXmlFormat(resValue);
} catch (UndefinedResObjectException | ClassCastException ex) {
// ignored
}
} catch (UndefinedResObjectException | ClassCastException ignored) {}
}
return decoded != null ? decoded : resValue.encodeAsResXmlAttr();
}
public String decodeManifestAttr(int attrResId)
public String decodeFromResourceId(int attrResId)
throws AndrolibException {
if (attrResId != 0) {
int attrId = attrResId;
ResID resId = new ResID(attrResId);
// See also: brut.androlib.res.data.ResTable.getResSpec
if (attrId >> 24 == 0) {
ResPackage pkg = getCurrentPackage();
int packageId = pkg.getId();
int pkgId = (packageId == 0 ? 2 : packageId);
attrId = (0xFF000000 & (pkgId << 24)) | attrId;
}
// Retrieve the ResSpec in a package by id
ResID resId = new ResID(attrId);
ResPackage pkg = getCurrentPackage();
if (pkg.hasResSpec(resId)) {
ResResSpec resResSpec = pkg.getResSpec(resId);
try {
ResResSpec resResSpec = mResTable.getResSpec(resId);
if (resResSpec != null) {
return resResSpec.getName();
}
}
} catch (UndefinedResObjectException | CantFindFrameworkResException ignored) {}
}
return null;
}
public ResPackage getCurrentPackage() throws AndrolibException {
if (mCurrentPackage == null) {
throw new AndrolibException("Current package not set");
public ResTable getResTable() throws AndrolibException {
if (mResTable == null) {
throw new AndrolibException("Res Table not set");
}
return mCurrentPackage;
return mResTable;
}
public void setCurrentPackage(ResPackage currentPackage) {
mCurrentPackage = currentPackage;
public void setResTable(ResTable resTable) {
mResTable = resTable;
}
private ResPackage mCurrentPackage;
private ResTable mResTable;
}

View File

@ -45,7 +45,7 @@ public class XmlPullStreamDecoder implements ResStreamDecoder {
try {
XmlPullWrapperFactory factory = XmlPullWrapperFactory.newInstance();
XmlPullParserWrapper par = factory.newPullParserWrapper(mParser);
final ResTable resTable = ((AXmlResourceParser) mParser).getAttrDecoder().getCurrentPackage().getResTable();
final ResTable resTable = ((AXmlResourceParser) mParser).getAttrDecoder().getResTable();
XmlSerializerWrapper ser = new StaticXmlSerializerWrapper(mSerial, factory) {
boolean hideSdkInfo = false;