Skip to content

Commit bb437dc

Browse files
authored
fix: rename injected entry spec if the name already exists (#4043)
Obfuscators can exploit the new missing entry injection to prevent rebuilding by causing entry specs with identical names to be injected. Example scenario: res/values/public.xml: <resources> <public type="attr" name="existingAttr" id="0x7f010000" /> </resources> res/values/attrs.xml: <resources> <attr name="existingAttr" format="string" /> </resources> res/layout/issue.xml: N: android=http://schemas.android.com/apk/res/android (line=2) N: app=http://schemas.android.com/apk/res-auto (line=2) E: merge (line=2) E: TextView (line=4) A: http://schemas.android.com/apk/res/android:layout_width(0x010100f4)=-2 A: http://schemas.android.com/apk/res/android:layout_height(0x010100f5)=-2 A: http://schemas.android.com/apk/res-auto:existingAttr(0x7f010001)=1 The spec 0x7f010001 is missing and the obfuscator renamed the raw attribute name to "existingAttr", which is already assigned to 0x7f010000. Apktool will inject a generic spec with the ID 0x7f010001 and the name "existingAttr" - that's a duplicate, and the APK can't be rebuilt. We patch this hole before obfuscators exploit it.
1 parent bdb7ece commit bb437dc

File tree

6 files changed

+46
-28
lines changed

6 files changed

+46
-28
lines changed

brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -457,18 +457,18 @@ private static void cmdDecode(String[] args) throws AndrolibException {
457457
} else {
458458
String mode = cli.getOptionValue(decodeResResolveModeOption);
459459
switch (mode) {
460-
case "remove":
461-
config.setDecodeResolve(Config.DecodeResolve.REMOVE);
462-
break;
463460
case "keep":
464461
config.setDecodeResolve(Config.DecodeResolve.KEEP);
465462
break;
463+
case "remove":
464+
config.setDecodeResolve(Config.DecodeResolve.REMOVE);
465+
break;
466466
case "dummy":
467467
config.setDecodeResolve(Config.DecodeResolve.DUMMY);
468468
break;
469469
default:
470470
System.err.println("Unknown resolve resources mode: " + mode);
471-
System.err.println("Expect: 'remove', 'keep' or 'dummy'.");
471+
System.err.println("Expect: 'keep', 'remove' or 'dummy'.");
472472
System.exit(1);
473473
return;
474474
}

brut.apktool/apktool-lib/src/main/java/brut/androlib/res/decoder/BinaryXmlResourceParser.java

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -382,18 +382,19 @@ public String getAttributeName(int index) {
382382
// Are improperly decoded when trusting the string pool.
383383
// Leveraging the resource map allows us to get the proper value.
384384
// <item android:state_enabled="true" app:d2="false" app:d3="true">
385+
String nameStr;
385386
try {
386-
String resourceMapValue = decodeFromResourceId(nameId);
387-
if (resourceMapValue != null) {
388-
return resourceMapValue;
387+
nameStr = decodeFromResourceId(nameId);
388+
if (nameStr != null) {
389+
return nameStr;
389390
}
390391
} catch (AndrolibException ignored) {
391392
}
392393

393394
// Couldn't decode from resource map, fall back to string pool.
394-
String stringPoolValue = mStringPool.getString(name);
395-
if (stringPoolValue == null) {
396-
stringPoolValue = "";
395+
nameStr = mStringPool.getString(name);
396+
if (nameStr == null) {
397+
nameStr = "";
397398
}
398399

399400
// In certain optimized apps, some attributes's specs are removed despite being used.
@@ -408,24 +409,24 @@ public String getAttributeName(int index) {
408409
if (removeUnresolved || nameId.getPackageId() != pkg.getId()) {
409410
LOGGER.warning(String.format(
410411
"null attr reference: ns=%s, name=%s, id=%s",
411-
getAttributePrefix(index), stringPoolValue, nameId));
412-
return stringPoolValue;
412+
getAttributePrefix(index), nameStr, nameId));
413+
return nameStr;
413414
}
414415

415-
if (stringPoolValue.isEmpty()) {
416-
stringPoolValue = ResEntrySpec.MISSING_PREFIX + nameId;
416+
if (nameStr.isEmpty()) {
417+
nameStr = ResEntrySpec.MISSING_PREFIX + nameId;
417418
}
418-
pkg.addEntrySpec(nameId, stringPoolValue);
419+
nameStr = pkg.addEntrySpec(nameId, nameStr).getName();
419420
pkg.addEntry(nameId, ResConfig.DEFAULT, ResAttribute.DEFAULT);
420421
} catch (AndrolibException ex) {
421422
setFirstError(ex);
422423
LOGGER.warning(String.format(
423424
"Could not add missing attr: ns=%s, name=%s, id=%s",
424-
getAttributePrefix(index), stringPoolValue, nameId));
425+
getAttributePrefix(index), nameStr, nameId));
425426
}
426427
}
427428

428-
return stringPoolValue;
429+
return nameStr;
429430
}
430431

431432
private String decodeFromResourceId(ResId resId) throws AndrolibException {

brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResEntrySpec.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ public ResEntrySpec(ResTypeSpec typeSpec, ResId id, String name) {
3232
assert typeSpec.getId() == id.getTypeId();
3333
mTypeSpec = typeSpec;
3434
mId = id;
35-
// Some apps had their entry names obfuscated or collapsed to
36-
// a single value in the key string pool.
35+
// Some apps had their entry names obfuscated or collapsed to a single
36+
// value in the key string pool.
3737
mName = isValidEntryName(name) ? name : RENAMED_PREFIX + id;
3838
}
3939

@@ -47,14 +47,11 @@ private static boolean isValidEntryName(String name) {
4747
if (!Character.isJavaIdentifierStart(name.charAt(0))) {
4848
return false;
4949
}
50-
// The rest must be valid Java identifier part characters.
50+
// The rest must be valid Java identifier part characters or any of the
51+
// whitelisted special characters.
5152
for (int i = 1; i < len; i++) {
5253
char ch = name.charAt(i);
53-
// Whitelisted special characters.
54-
if (ch == '.' || ch == '-') {
55-
continue;
56-
}
57-
if (!Character.isJavaIdentifierPart(ch)) {
54+
if (!Character.isJavaIdentifierPart(ch) && ch != '.' && ch != '-') {
5855
return false;
5956
}
6057
}

brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResPackage.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323

2424
import java.util.Collection;
2525
import java.util.HashMap;
26+
import java.util.HashSet;
2627
import java.util.Map;
2728
import java.util.Objects;
29+
import java.util.Set;
2830
import java.util.logging.Logger;
2931

3032
public class ResPackage {
@@ -36,6 +38,7 @@ public class ResPackage {
3638
private final Map<Integer, ResTypeSpec> mTypeSpecs;
3739
private final Map<Pair<Integer, ResConfig>, ResType> mTypes;
3840
private final Map<ResId, ResEntrySpec> mEntrySpecs;
41+
private final Map<Integer, Set<String>> mEntryNames;
3942
private final Map<Pair<ResId, ResConfig>, ResEntry> mEntries;
4043
private final Map<String, ResOverlayable> mOverlayables;
4144

@@ -46,6 +49,7 @@ public ResPackage(ResTable table, int id, String name) {
4649
mTypeSpecs = new HashMap<>();
4750
mTypes = new HashMap<>();
4851
mEntrySpecs = new HashMap<>();
52+
mEntryNames = new HashMap<>();
4953
mEntries = new HashMap<>();
5054
mOverlayables = new HashMap<>();
5155
}
@@ -152,8 +156,24 @@ public ResEntrySpec addEntrySpec(ResId id, String name) throws AndrolibException
152156

153157
int typeId = id.getTypeId();
154158
ResTypeSpec typeSpec = getTypeSpec(typeId);
159+
160+
// Obfuscation can cause specs to be generated using existing names.
161+
// Enforce uniqueness by renaming the spec when that happens.
162+
Set<String> entryNames = mEntryNames.get(typeId);
163+
if (entryNames == null) {
164+
entryNames = new HashSet<>();
165+
mEntryNames.put(typeId, entryNames);
166+
} else if (entryNames.contains(name)) {
167+
// Clear the name to force a rename.
168+
name = "";
169+
}
170+
155171
entrySpec = new ResEntrySpec(typeSpec, id, name);
156172
mEntrySpecs.put(id, entrySpec);
173+
174+
// Record the name to enforce uniqueness.
175+
entryNames.add(entrySpec.getName());
176+
157177
return entrySpec;
158178
}
159179

brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/value/ResItem.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ public static ResItem parse(ResPackage pkg, int type, int data, String rawValue)
4747
return data == TypedValue.DATA_NULL_EMPTY ? ResPrimitive.EMPTY : ResReference.NULL;
4848
case TypedValue.TYPE_REFERENCE:
4949
case TypedValue.TYPE_DYNAMIC_REFERENCE:
50-
return data != 0 || rawValue != null
50+
return (data != 0 || rawValue != null)
5151
? new ResReference(pkg, ResId.of(data), rawValue)
5252
: ResReference.NULL;
5353
case TypedValue.TYPE_ATTRIBUTE:
5454
case TypedValue.TYPE_DYNAMIC_ATTRIBUTE:
55-
return data != 0 || rawValue != null
55+
return (data != 0 || rawValue != null)
5656
? new ResReference(pkg, ResId.of(data), rawValue, ResReference.Type.ATTRIBUTE)
5757
: ResReference.NULL;
5858
case TypedValue.TYPE_STRING:

brut.j.util/src/main/java/brut/util/ZipUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public static void zipDir(File baseDir, String dirName, ZipOutputStream out, Col
6161
if (file.isDirectory()) {
6262
zipDir(baseDir, fileName, out, doNotCompress);
6363
} else if (file.isFile()) {
64-
zipFile(baseDir, fileName, out, doNotCompress != null && !doNotCompress.isEmpty()
64+
zipFile(baseDir, fileName, out, (doNotCompress != null && !doNotCompress.isEmpty())
6565
? entryName -> doNotCompress.contains(entryName)
6666
|| doNotCompress.contains(FilenameUtils.getExtension(entryName))
6767
: entryName -> false);

0 commit comments

Comments
 (0)