平时经常用 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.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(" "" ,""" ));            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 吧。