diff --git a/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java b/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java index bf640fed..ae509349 100644 --- a/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java +++ b/brut.apktool/apktool-cli/src/main/java/brut/apktool/Main.java @@ -124,6 +124,9 @@ public class Main { if (cli.hasOption("r") || cli.hasOption("no-res")) { decoder.setDecodeResources(ApkDecoder.DECODE_RESOURCES_NONE); } + if (cli.hasOption("force-manifest")) { + decoder.setForceDecodeManifest(ApkDecoder.FORCE_DECODE_MANIFEST_FULL); + } if (cli.hasOption("no-assets")) { decoder.setDecodeAssets(ApkDecoder.DECODE_ASSETS_NONE); } @@ -286,6 +289,11 @@ public class Main { .desc("Do not decode resources.") .build(); + Option forceManOption = Option.builder() + .longOpt("force-manifest") + .desc("Decode the APK's compiled manifest, even if decoding of resources is set to \"false\".") + .build(); + Option noAssetOption = Option.builder() .longOpt("no-assets") .desc("Do not decode assets.") @@ -405,6 +413,7 @@ public class Main { DecodeOptions.addOption(analysisOption); DecodeOptions.addOption(apiLevelOption); DecodeOptions.addOption(noAssetOption); + DecodeOptions.addOption(forceManOption); BuildOptions.addOption(debugBuiOption); BuildOptions.addOption(aaptOption); @@ -453,6 +462,7 @@ public class Main { allOptions.addOption(analysisOption); allOptions.addOption(debugDecOption); allOptions.addOption(noDbgOption); + allOptions.addOption(forceManOption); allOptions.addOption(noAssetOption); allOptions.addOption(keepResOption); allOptions.addOption(debugBuiOption); diff --git a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java index 23f9d3b3..eb03780d 100644 --- a/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java +++ b/brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java @@ -104,6 +104,15 @@ public class ApkDecoder { switch (mDecodeResources) { case DECODE_RESOURCES_NONE: mAndrolib.decodeResourcesRaw(mApkFile, outDir); + if (mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) { + setTargetSdkVersion(); + setAnalysisMode(mAnalysisMode, true); + + // done after raw decoding of resources because copyToDir overwrites dest files + if (hasManifest()) { + mAndrolib.decodeManifestWithResources(mApkFile, outDir, getResTable()); + } + } break; case DECODE_RESOURCES_FULL: setTargetSdkVersion(); @@ -119,14 +128,12 @@ public class ApkDecoder { // if there's no resources.asrc, decode the manifest without looking // up attribute references if (hasManifest()) { - switch (mDecodeResources) { - case DECODE_RESOURCES_NONE: - mAndrolib.decodeManifestRaw(mApkFile, outDir); - break; - case DECODE_RESOURCES_FULL: - mAndrolib.decodeManifestFull(mApkFile, outDir, - getResTable()); - break; + if (mDecodeResources == DECODE_RESOURCES_FULL + || mForceDecodeManifest == FORCE_DECODE_MANIFEST_FULL) { + mAndrolib.decodeManifestFull(mApkFile, outDir, getResTable()); + } + else { + mAndrolib.decodeManifestRaw(mApkFile, outDir); } } } @@ -190,6 +197,13 @@ public class ApkDecoder { mDecodeResources = mode; } + public void setForceDecodeManifest(short mode) throws AndrolibException { + if (mode != FORCE_DECODE_MANIFEST_NONE && mode != FORCE_DECODE_MANIFEST_FULL) { + throw new AndrolibException("Invalid force decode manifest mode"); + } + mForceDecodeManifest = mode; + } + public void setDecodeAssets(short mode) throws AndrolibException { if (mode != DECODE_ASSETS_NONE && mode != DECODE_ASSETS_FULL) { throw new AndrolibException("Invalid decode asset mode"); @@ -306,6 +320,9 @@ public class ApkDecoder { public final static short DECODE_RESOURCES_NONE = 0x0100; public final static short DECODE_RESOURCES_FULL = 0x0101; + public final static short FORCE_DECODE_MANIFEST_NONE = 0x0000; + public final static short FORCE_DECODE_MANIFEST_FULL = 0x0001; + public final static short DECODE_ASSETS_NONE = 0x0000; public final static short DECODE_ASSETS_FULL = 0x0001; @@ -417,6 +434,7 @@ public class ApkDecoder { private ResTable mResTable; private short mDecodeSources = DECODE_SOURCES_SMALI; private short mDecodeResources = DECODE_RESOURCES_FULL; + private short mForceDecodeManifest = FORCE_DECODE_MANIFEST_NONE; private short mDecodeAssets = DECODE_ASSETS_FULL; private boolean mForceDelete = false; private boolean mKeepBrokenResources = false; diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/ForceManifestDecodeNoResourcesTest.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/ForceManifestDecodeNoResourcesTest.java new file mode 100644 index 00000000..1d051925 --- /dev/null +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/ForceManifestDecodeNoResourcesTest.java @@ -0,0 +1,144 @@ +/** + * Copyright (C) 2017 Ryszard Wiśniewski + * Copyright (C) 2017 Connor Tumbleson + * + * 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 + * + * http://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; + +import brut.common.BrutException; +import brut.directory.ExtFile; +import brut.util.OS; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ForceManifestDecodeNoResourcesTest { + + private byte[] xmlHeader = new byte[] { + 0x3C, // < + 0x3F, // ? + 0x78, // x + 0x6D, // m + 0x6C, // l + 0x20, // (empty) + }; + + @BeforeClass + public static void beforeClass() throws Exception { + TestUtils.cleanFrameworkFile(); + sTmpDir = new ExtFile(OS.createTempDirectory()); + TestUtils.copyResourceDir(ForceManifestDecodeNoResourcesTest.class, "brut/apktool/issue1680/", sTmpDir); + } + + @AfterClass + public static void afterClass() throws BrutException { + OS.rmdir(sTmpDir); + } + + @Test + public void checkIfForceManifestWithNoResourcesWorks() throws BrutException, IOException { + String apk = "issue1680.apk"; + String output = sTmpDir + File.separator + apk + ".out"; + + // decode issue1680.apk + decodeFile(sTmpDir + File.separator + apk, ApkDecoder.DECODE_RESOURCES_NONE, + ApkDecoder.FORCE_DECODE_MANIFEST_FULL, output); + + // lets probe filetype of manifest, we should detect XML + File manifestFile = new File(output + File.separator + "AndroidManifest.xml"); + byte[] magic = TestUtils.readHeaderOfFile(manifestFile, 6); + assertTrue(Arrays.equals(this.xmlHeader, magic)); + + // confirm resources.arsc still exists, as its raw + File resourcesArsc = new File(output + File.separator + "resources.arsc"); + assertTrue(resourcesArsc.isFile()); + } + + @Test + public void checkIfForceManifestWorksWithNoChangeToResources() throws BrutException, IOException { + String apk = "issue1680.apk"; + String output = sTmpDir + File.separator + apk + ".out"; + + // decode issue1680.apk + decodeFile(sTmpDir + File.separator + apk, ApkDecoder.DECODE_RESOURCES_FULL, + ApkDecoder.FORCE_DECODE_MANIFEST_FULL, output); + + // lets probe filetype of manifest, we should detect XML + File manifestFile = new File(output + File.separator + "AndroidManifest.xml"); + byte[] magic = TestUtils.readHeaderOfFile(manifestFile, 6); + assertTrue(Arrays.equals(this.xmlHeader, magic)); + + // confirm resources.arsc does not exist + File resourcesArsc = new File(output + File.separator + "resources.arsc"); + assertFalse(resourcesArsc.isFile()); + } + + @Test + public void checkForceManifestToFalseWithResourcesEnabledIsIgnored() throws BrutException, IOException { + String apk = "issue1680.apk"; + String output = sTmpDir + File.separator + apk + ".out"; + + // decode issue1680.apk + decodeFile(sTmpDir + File.separator + apk, ApkDecoder.DECODE_RESOURCES_FULL, + ApkDecoder.FORCE_DECODE_MANIFEST_NONE, output); + + // lets probe filetype of manifest, we should detect XML + File manifestFile = new File(output + File.separator + "AndroidManifest.xml"); + byte[] magic = TestUtils.readHeaderOfFile(manifestFile, 6); + assertTrue(Arrays.equals(this.xmlHeader, magic)); + + // confirm resources.arsc does not exist + File resourcesArsc = new File(output + File.separator + "resources.arsc"); + assertFalse(resourcesArsc.isFile()); + } + + @Test + public void checkBothManifestAndResourcesSetToNone() throws BrutException, IOException { + String apk = "issue1680.apk"; + String output = sTmpDir + File.separator + apk + ".out"; + + // decode issue1680.apk + decodeFile(sTmpDir + File.separator + apk, ApkDecoder.DECODE_RESOURCES_NONE, + ApkDecoder.FORCE_DECODE_MANIFEST_NONE, output); + + // lets probe filetype of manifest, we should not detect XML + File manifestFile = new File(output + File.separator + "AndroidManifest.xml"); + byte[] magic = TestUtils.readHeaderOfFile(manifestFile, 6); + assertFalse(Arrays.equals(this.xmlHeader, magic)); + + // confirm resources.arsc exists + File resourcesArsc = new File(output + File.separator + "resources.arsc"); + assertTrue(resourcesArsc.isFile()); + } + + private void decodeFile(String apk, short decodeResources, short decodeManifest, String output) + throws BrutException, IOException { + ApkDecoder apkDecoder = new ApkDecoder(new File(apk)); + apkDecoder.setDecodeResources(decodeResources); + apkDecoder.setForceDecodeManifest(decodeManifest); + apkDecoder.setForceDelete(true); // delete directory due to multiple tests. + + apkDecoder.setOutDir(new File(output)); + apkDecoder.decode(); + } + + private static ExtFile sTmpDir; +} diff --git a/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java b/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java index 39334d32..4fe06898 100644 --- a/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java +++ b/brut.apktool/apktool-lib/src/test/java/brut/androlib/TestUtils.java @@ -135,6 +135,23 @@ public abstract class TestUtils { } } + /** + * + * @return byte[] + * @throws FileNotFoundException + * @throws IOException + */ + public static byte[] readHeaderOfFile(File file, int size) throws IOException { + byte[] buffer = new byte[size]; + InputStream inputStream = new FileInputStream(file); + if (inputStream.read(buffer) != buffer.length) { + throw new IOException("File size too small for buffer length: " + size); + } + inputStream.close(); + + return buffer; + } + /** * * @return File diff --git a/brut.apktool/apktool-lib/src/test/resources/brut/apktool/issue1680/issue1680.apk b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/issue1680/issue1680.apk new file mode 100644 index 00000000..fdb46603 Binary files /dev/null and b/brut.apktool/apktool-lib/src/test/resources/brut/apktool/issue1680/issue1680.apk differ