drozer app.package.manifest 有时候完全错误的 bug

平时经常用 drozer dump manifest,感觉稍微有点难用,直到某次批量 dump,发现很多 manifest 连 hash 都一样,发现 drozer 抽风了,本文记录一下。

一、触发方式

在小米手机上,运行 run app.package.manifest com.miui.notes ,发现返回的是 com.xiaomi.micloud.sdk 的 manifest。 当然在其他手机上也有类似的现象,我就不列举了,批量 dump 就会发现的。

二、代码溯源

app.package.manifest 功能本质上是一个 drozer module,它位于 drozer/modules/app/package.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Manifest(Module, common.Assets):

name = "Get AndroidManifest.xml of package"
description = "Retrieves AndroidManifest.xml from an installed package."
examples = """Getting the manifest for drozer

dz> run app.package.manifest com.mwr.dz

<manifest versionCode="2" versionName="1.1" package="com.mwr.dz">
<uses-sdk minSdkVersion="8" targetSdkVersion="4">
</uses-sdk>
<uses-permission name="android.permission.INTERNET">
</uses-permission>

...
</manifest>"""
author = "MWR InfoSecurity (@mwrlabs)"
date = "2012-11-06"
license = "BSD (3 clause)"
path = ["app", "package"]
permissions = ["com.mwr.dz.permissions.GET_CONTEXT"]

def add_arguments(self, parser):
parser.add_argument("package", help="the identifier of the package")

def execute(self, arguments):
if arguments.package == None or arguments.package == "":
self.stderr.write("No package provided.n")
else:
self.__write_manifest(self.getAndroidManifest(arguments.package))

这个 getAndroidManifest 在 drozer/modules/common/assets.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Assets(loader.ClassLoader):
"""
Utility methods for interacting with the Android Asset Manager.
"""

def getAndroidManifest(self, package):
"""
Extract the AndroidManifest.xml file from a package on the device, and
recover it as an XML representation.
"""

XmlAssetReader = self.loadClass("common/XmlAssetReader.apk", "XmlAssetReader")

asset_manager = self.getAssetManager(package)
xml = asset_manager.openXmlResourceParser("AndroidManifest.xml")

xml_string = str(XmlAssetReader.read(xml))

# self.reflector comes from drozer.modules.base
self.reflector.delete(asset_manager)
self.reflector.delete(xml)

return xml_string

加载一个 java 文件,在手机上运行后获取返回的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.io.IOException;

import android.content.res.XmlResourceParser;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

public class XmlAssetReader {

public static String read(XmlResourceParser xml) {
StringBuilder output = new StringBuilder();

try {
while (xml.next() != XmlPullParser.END_DOCUMENT) {
switch (xml.getEventType()) {
case XmlPullParser.START_TAG:
output.append("<");
output.append(xml.getName());
for(int i=0; i<xml.getAttributeCount(); i++) {
output.append(" ");
output.append(xml.getAttributeName(i));
output.append("="");
output.append(xml.getAttributeValue(i).replace(""","&quot;"));
output.append(""");
}
output.append(">n");
break;

case XmlPullParser.END_TAG:
output.append("</");
output.append(xml.getName());
output.append(">n");
break;

case XmlPullParser.TEXT:
output.append(xml.getText());
output.append("n");
break;

default:
break;
}
}
}
catch(IOException e) {
return null;
}
catch(XmlPullParserException e) {
return null;
}

return output.toString();
}

}

因此,我们将这个过程移植到手机上,即可进行测试。

三、代码测试

编写如下的 java 代码进行模拟:

1
2
XmlResourceParser io = createPackageContext("com.miui.notes",0 ).getAssets().openXmlResourceParser("AndroidManifest.xml");
Log.e("tutu", "" + XmlAssetReader.read(io));

打印输出仍然是错误的,说明是openXmlResourceParser 的问题。

跟进一步,发现openXmlResourceParser(String)会调用openXmlResourceParser(0, String),第一个参数零表示 cookie,最终调用到 native 方法。 在AssetManager 里搜cookie 的代码,大致猜测出这个 cookie 表示下标,为零时表示随意选择一个 ApkAsset,而 AssetManager 里有 ApkAssets[] 结构体,很可能是相关的,编写如下代码进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
String pkgName = "com.miui.notes";
pkgName = getPackageName();
AssetManager assets = createPackageContext(pkgName, 0).getAssets();
Method m = assets.getClass().getDeclaredMethod("findCookieForPath", String.class);
m.setAccessible(true);
Field f = assets.getClass().getDeclaredField("mApkAssets");
f.setAccessible(true);
ApkAssets[] as = (ApkAssets[]) f.get(assets);
for (ApkAssets a : as) {
Log.e("tutu", a.getAssetPath());
}
int cookieIdx = (int) m.invoke(assets, getPackageManager().getApplicationInfo(pkgName, 0).publicSourceDir);
Log.e("tutu", "cookieIdx " + cookieIdx);

当包名是 com.miui.notes 和 自己时,日志分别为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
E/tutu: /system/framework/framework-res.apk
E/tutu: /system/framework/framework-ext-res/framework-ext-res.apk
E/tutu: /system/app/miuisystem/miuisystem.apk
E/tutu: /system/app/miui/miui.apk
E/tutu: /vendor/overlay/FrameworksResCommon.apk
E/tutu: /vendor/overlay/DevicesAndroidOverlay.apk
E/tutu: /data/app/com.miui.notes-l07QybyO3roqn-okroQsSQ==/base.apk
E/tutu: /system/priv-app/RtMiCloudSDK/RtMiCloudSDK.apk
E/tutu: cookieIdx 7

E/tutu: /system/framework/framework-res.apk
E/tutu: /system/framework/framework-ext-res/framework-ext-res.apk
E/tutu: /system/app/miuisystem/miuisystem.apk
E/tutu: /system/app/miui/miui.apk
E/tutu: /vendor/overlay/FrameworksResCommon.apk
E/tutu: /vendor/overlay/DevicesAndroidOverlay.apk
E/tutu: /data/app/com.leadroyal.helloandroid-ml5tuR__VGsg6o3Far2g7Q==/base.apk
E/tutu: cookieIdx 7

差别仅在于最后那一项,com.miui.notes 会依赖 RtMiCloudSDK.apk,也就是出错的那一项,而我们的 apk 是没有它的。 将正确的值传递给 openXmlResourceParser ,发现返回是正确的,至此,破案了,是 openXmlResourceParser 发生了非预期的现象。

四、解决方案

由于相关 API 都是 hide 的,写代码不方便,在此我还是建议别用这个工具了,还是用我写的 ShrinkApkAnalyzer 吧。