fix: decoding APK with many compact entries and unknown uses-sdk attrs (#3705)

* fix: decoding APK with many compact entries and unknown uses-sdk attrs

This fixes 2 new issues with a stock APK sourced from an Android 15 ROM.

https://drive.google.com/file/d/1x9udLN4W5I7chyGp1ZY8Cyfhu1vXezU9/view

1) mIn.readShort() for size in readEntryData is incorrect and the size < 0 check is not possible.
   Entry size is stored by AAPT2 as an unsigned short and thus will never be negative.
   Reading it as a signed short will cause negative entry sizes in compactly packed entries in
   very large string pools and will result in a lot of "APKTOOL_DUMMYVAL_" values.

2) sdkInfo isn't stored properly for APKs with unexpected properties in uses-sdk tag.
   As far as I can tell, these attributes serve no purpose and can be ignored.
   In the given APK, additional "android:versionCode" and "android:versionName" attributes appear
   in the uses-sdk tag, purpose unknown and they don't represent the actual version of the app.

   E: uses-sdk (line=26)
     A: http://schemas.android.com/apk/res/android:minSdkVersion(0x0101020c)=35
     A: http://schemas.android.com/apk/res/android:versionCode(0x0101021b)=31
     A: http://schemas.android.com/apk/res/android:versionName(0x0101021c)="3.1"
     A: http://schemas.android.com/apk/res/android:targetSdkVersion(0x01010270)=35

* test: add assertion for issue 3705

---------

Co-authored-by: Connor Tumbleson <connor.tumbleson@gmail.com>
Co-authored-by: Connor Tumbleson <iBotPeaches@users.noreply.github.com>
This commit is contained in:
Igor Eisberg 2024-10-04 15:58:32 +03:00 committed by GitHub
parent 5c99919d94
commit 24541c3943
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 136 additions and 67 deletions

View File

@ -41,17 +41,12 @@ public class ResResSpec {
public ResResSpec(ResID id, String name, ResPackage pkg, ResTypeSpec type) {
this.mId = id;
String cleanName;
name = EMPTY_RESOURCE_NAMES.contains(name) ? null : name;
ResResSpec resResSpec = type.getResSpecUnsafe(name);
if (resResSpec != null) {
cleanName = String.format("APKTOOL_DUPLICATE_%s_%s", type, id.toString());
} else {
cleanName = ((name == null || name.isEmpty()) ? ("APKTOOL_DUMMYVAL_" + id.toString()) : name);
if (name == null || name.isEmpty() || EMPTY_RESOURCE_NAMES.contains(name)) {
name = "APKTOOL_DUMMYVAL_" + id.toString();
} else if (type.getResSpecUnsafe(name) != null) {
name = String.format("APKTOOL_DUPLICATE_%s_%s", type, id.toString());
}
this.mName = cleanName;
this.mName = name;
this.mPackage = pkg;
this.mType = type;
}

View File

@ -266,10 +266,6 @@ public class ResTable {
mApkInfo.sparseResources = flag;
}
public void clearSdkInfo() {
mApkInfo.sdkInfo.clear();
}
public void addSdkInfo(String key, String value) {
mApkInfo.sdkInfo.put(key, value);
}

View File

@ -355,16 +355,12 @@ public class ARSCDecoder {
}
private EntryData readEntryData() throws IOException, AndrolibException {
short size = mIn.readShort();
int size = mIn.readUnsignedShort();
short flags = mIn.readShort();
boolean isComplex = (flags & ENTRY_FLAG_COMPLEX) != 0;
boolean isCompact = (flags & ENTRY_FLAG_COMPACT) != 0;
if (size < 0 && !isCompact) {
throw new AndrolibException("Entry size is under 0 bytes and not compactly packed.");
}
int specNamesId = mIn.readInt();
if (specNamesId == NO_ENTRY && !isCompact) {
return null;

View File

@ -863,5 +863,5 @@ public class AXmlResourceParser implements XmlResourceParser {
private static final int PRIVATE_PKG_ID = 0x7F;
private static final String ANDROID_RES_NS_AUTO = "http://schemas.android.com/apk/res-auto";
private static final String ANDROID_RES_NS = "http://schemas.android.com/apk/res/android";
public static final String ANDROID_RES_NS = "http://schemas.android.com/apk/res/android";
}

View File

@ -46,8 +46,7 @@ public class AndroidManifestPullStreamDecoder implements ResStreamDecoder {
final ResTable resTable = mParser.getResTable();
XmlSerializerWrapper ser = new StaticXmlSerializerWrapper(mSerial, factory) {
boolean hideSdkInfo = false;
boolean hidePackageInfo = false;
final boolean hideSdkInfo = !resTable.getAnalysisMode();
@Override
public void event(XmlPullParser pp)
@ -57,76 +56,76 @@ public class AndroidManifestPullStreamDecoder implements ResStreamDecoder {
if (type == XmlPullParser.START_TAG) {
if ("manifest".equals(pp.getName())) {
try {
hidePackageInfo = parseManifest(pp);
parseManifest(pp);
} catch (AndrolibException ignored) {}
} else if ("uses-sdk".equals(pp.getName())) {
try {
hideSdkInfo = parseAttr(pp);
if (hideSdkInfo) {
return;
}
parseUsesSdk(pp);
} catch (AndrolibException ignored) {}
if (hideSdkInfo) {
return;
}
}
} else if (hideSdkInfo && type == XmlPullParser.END_TAG
} else if (type == XmlPullParser.END_TAG
&& "uses-sdk".equals(pp.getName())) {
return;
} else if (hidePackageInfo && type == XmlPullParser.END_TAG
&& "manifest".equals(pp.getName())) {
super.event(pp);
return;
if (hideSdkInfo) {
return;
}
}
super.event(pp);
}
private boolean parseManifest(XmlPullParser pp)
private void parseManifest(XmlPullParser pp)
throws AndrolibException {
String attr_name;
// read <manifest> for package:
for (int i = 0; i < pp.getAttributeCount(); i++) {
attr_name = pp.getAttributeName(i);
String ns = pp.getAttributeNamespace(i);
String name = pp.getAttributeName(i);
String value = pp.getAttributeValue(i);
if (attr_name.equals(("package"))) {
resTable.setPackageRenamed(pp.getAttributeValue(i));
} else if (attr_name.equals("versionCode")) {
resTable.setVersionCode(pp.getAttributeValue(i));
} else if (attr_name.equals("versionName")) {
resTable.setVersionName(pp.getAttributeValue(i));
if (value.isEmpty()) {
continue;
}
if (ns.isEmpty()) {
if (name.equals("package")) {
resTable.setPackageRenamed(value);
}
} else if (ns.equals(AXmlResourceParser.ANDROID_RES_NS)) {
switch (name) {
case "versionCode":
resTable.setVersionCode(value);
break;
case "versionName":
resTable.setVersionName(value);
break;
}
}
}
return true;
}
private boolean parseAttr(XmlPullParser pp)
private void parseUsesSdk(XmlPullParser pp)
throws AndrolibException {
for (int i = 0; i < pp.getAttributeCount(); i++) {
final String a_ns = "http://schemas.android.com/apk/res/android";
String ns = pp.getAttributeNamespace(i);
String name = pp.getAttributeName(i);
String value = pp.getAttributeValue(i);
if (a_ns.equals(ns)) {
String name = pp.getAttributeName(i);
String value = pp.getAttributeValue(i);
if (name != null && value != null) {
if (name.equals("minSdkVersion")
|| name.equals("targetSdkVersion")
|| name.equals("maxSdkVersion")
|| name.equals("compileSdkVersion")) {
if (value.isEmpty()) {
continue;
}
if (ns.equals(AXmlResourceParser.ANDROID_RES_NS)) {
switch (name) {
case "minSdkVersion":
case "targetSdkVersion":
case "maxSdkVersion":
case "compileSdkVersion":
resTable.addSdkInfo(name, value);
} else {
resTable.clearSdkInfo();
return false; // Found unknown flags
}
}
} else {
resTable.clearSdkInfo();
if (i >= pp.getAttributeCount()) {
return false; // Found unknown flags
break;
}
}
}
return ! resTable.getAnalysisMode();
}
};

View File

@ -23,6 +23,8 @@ import brut.directory.FileDirectory;
import org.custommonkey.xmlunit.*;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
@ -155,6 +157,20 @@ public class BaseTest {
return count;
}
protected static boolean resourceNameContains(Element element, String name) {
if (element.hasAttribute("name") && element.getAttribute("name").contains(name)) {
return true;
}
NodeList children = element.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE && resourceNameContains((Element) child, name)) {
return true;
}
}
return false;
}
protected static ExtFile sTmpDir;
protected static ExtFile sTestOrigDir;
protected static ExtFile sTestNewDir;

View File

@ -0,0 +1,67 @@
/*
* Copyright (C) 2010 Ryszard Wiśniewski <brut.alll@gmail.com>
* Copyright (C) 2010 Connor Tumbleson <connor.tumbleson@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package brut.androlib.decode;
import brut.androlib.*;
import brut.directory.ExtFile;
import brut.common.BrutException;
import brut.util.OS;
import java.io.File;
import java.io.IOException;
import org.junit.*;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import static org.junit.Assert.*;
public class LargeCompactResourceTest extends BaseTest {
@BeforeClass
public static void beforeClass() throws Exception {
TestUtils.cleanFrameworkFile();
sTmpDir = new ExtFile(OS.createTempDirectory());
TestUtils.copyResourceDir(CompactResourceTest.class, "decode/issue3705/", sTmpDir);
}
@AfterClass
public static void afterClass() throws BrutException {
OS.rmdir(sTmpDir);
}
@Test
public void checkIfDecodeSucceeds() throws BrutException, IOException, ParserConfigurationException, SAXException {
String apk = "issue3705.apk";
ExtFile testApk = new ExtFile(sTmpDir, apk);
// decode issue3705.apk
ApkDecoder apkDecoder = new ApkDecoder(testApk);
sTestOrigDir = new ExtFile(sTmpDir + File.separator + apk + ".out");
File outDir = new File(sTmpDir + File.separator + apk + ".out");
apkDecoder.decode(outDir);
Document doc = loadDocument(new File(sTestOrigDir + "/res/values/strings.xml"));
assertFalse(resourceNameContains(doc.getDocumentElement(), "APKTOOL"));
Config config = Config.getDefaultConfig();
LOGGER.info("Building issue3705.apk...");
new ApkBuilder(sTestOrigDir, config).build(testApk);
}
}