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

View File

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

View File

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

View File

@ -282,8 +282,8 @@ public class AXmlResourceParser implements XmlResourceParser {
String value = m_strings.getString(namespace); String value = m_strings.getString(namespace);
if (value == null || value.length() == 0) { if (value == null || value.length() == 0) {
ResID resourceId = new ResID(getAttributeNameResource(index)); ResID resId = new ResID(getAttributeNameResource(index));
if (resourceId.package_ == PRIVATE_PKG_ID) { if (resId.pkgId == PRIVATE_PKG_ID) {
value = getNonDefaultNamespaceUri(offset); value = getNonDefaultNamespaceUri(offset);
} else { } else {
value = android_ns; value = android_ns;
@ -327,29 +327,32 @@ public class AXmlResourceParser implements XmlResourceParser {
return ""; return "";
} }
String value = m_strings.getString(name); String resourceMapValue;
String namespace = getAttributeNamespace(index); String stringBlockValue = m_strings.getString(name);
int resourceId = getAttributeNameResource(index);
// If attribute name is lacking or a private namespace emerges, try {
// retrieve the exact attribute name by its id. resourceMapValue = mAttrDecoder.decodeFromResourceId(resourceId);
if (value == null || value.length() == 0) { } catch (AndrolibException ignored) {
try { resourceMapValue = null;
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) {}
} }
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 @Override
@ -383,18 +386,22 @@ public class AXmlResourceParser implements XmlResourceParser {
if (mAttrDecoder != null) { if (mAttrDecoder != null) {
try { try {
String value = valueRaw == -1 ? null : ResXmlEncoders.escapeXmlChars(m_strings.getString(valueRaw)); String stringBlockValue = valueRaw == -1 ? null : ResXmlEncoders.escapeXmlChars(m_strings.getString(valueRaw));
String obfuscatedValue = mAttrDecoder.decodeManifestAttr(valueData); String resourceMapValue = mAttrDecoder.decodeFromResourceId(valueData);
String value = stringBlockValue;
if (! (value == null || obfuscatedValue == null)) { if (stringBlockValue != null && resourceMapValue != null) {
int slashPos = value.lastIndexOf("/"); 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) { if (slashPos != -1) {
// Handle a value with a format of "@yyy/xxx" if (colonPos == -1) {
String dir = value.substring(0, slashPos); String type = stringBlockValue.substring(0, slashPos);
value = dir + "/"+ obfuscatedValue; value = type + "/" + resourceMapValue;
} else if (! value.equals(obfuscatedValue)) { }
value = obfuscatedValue; } else if (! stringBlockValue.equals(resourceMapValue)) {
value = resourceMapValue;
} }
} }

View File

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

View File

@ -45,7 +45,7 @@ public class XmlPullStreamDecoder implements ResStreamDecoder {
try { try {
XmlPullWrapperFactory factory = XmlPullWrapperFactory.newInstance(); XmlPullWrapperFactory factory = XmlPullWrapperFactory.newInstance();
XmlPullParserWrapper par = factory.newPullParserWrapper(mParser); 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) { XmlSerializerWrapper ser = new StaticXmlSerializerWrapper(mSerial, factory) {
boolean hideSdkInfo = false; boolean hideSdkInfo = false;