术→技巧, 研发

Android应用:apk文件反编译

钱魏Way · · 1,085 次浏览
!文章内容如有错误或排版问题,请提交反馈,非常感谢!

APK文件简介

每个需要安装到 android 平台的应用都要被编译打包为一个单独的文件,后缀名为.apk(Android application package),其中包含了应用的二进制代码、资源、配置文件等。

apk 文件实际是一个 zip 压缩包,可以通过解压缩工具解开(将后缀名改为.zip 后再用解压缩文件解压)。APK 文件的大致目录结构如下:

|– AndroidManifest.xml
|– META-INF
||– CERT.RSA
||– CERT.SF
|`– MANIFEST.MF
|– classes.dex
|– res
||– drawable
||`– icon.png
|`– layout
|`– main.xml
`– resources.arsc

AndroidManifest.xml

AndroidManifest.xml 官方解释是应用清单,每个应用的根目录中都必须包含一个,并且文件名必须一模一样。这个文件中包含了 APP 的配置信息,系统需要根据里面的内容运行 APP 的代码,显示界面。

上述的功能是非常笼统的解释,具体到细节就是:

  • 为应用的 Java 软件包命名。软件包名称充当应用的唯一标识符。
  • 描述应用的各个组件,包括构成应用的 Activity、服务、广播接收器和内容提供程序。它还为实现每个组件的类命名并发布其功能,例如它们可以处理的Intent消息。这些声明向 Android 系统告知有关组件以及可以启动这些组件的条件的信息。
  • 确定托管应用组件的进程。
  • 声明应用必须具备哪些权限才能访问 API 中受保护的部分并与其他应用交互。还声明其他应用与该应用组件交互所需具备的权限
  • 列出Instrumentation类,这些类可在应用运行时提供分析和其他信息。这些声明只会在应用处于开发阶段时出现在清单中,在应用发布之前将移除。
  • 声明应用所需的最低 Android API 级别
  • 列出应用必须链接到的库

上面是官方的解释。很多东西现在还不能理解,也没有用到,先挑能理解的进行解释。

  • 第一条:提供软件包名。这就是我们的 apk 的名字,通常我们的名字都是类似”com.android.helloworld”这种,和 Java 类名类似,目的是确定使其成为一个唯一值。
  • 第二条:描述应用的各个组件。这是用来定义四大组件用的。我们最常用的就是 Activity 组件。它需要定义组件的表现形式(组件名、主题、启动类型),组件可以响应的操作(例如某个启动意图)等。
  • 第三条、第四条和第五条:还没用到,不做解释。
  • 第五条:声明最低 API 级别。这个级别在 gradle 文件中也能定义,字段是 minSdkVersion。在 AndroidManifest.xml 文件中定义的情况比较少。
  • 第六条:列出必要的 lib 库。这东西在 0 以后的 Android Studio 似乎也没什么功能,因为在 3.0 以后编译用的是 CMakeLists.txt 文件,以及 build.gradle 文件来指定库。

AndroidManifest.xml 文件示例:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sample.teapot"
android:versionCode="1"
android:versionName="1.0.0.1">

<uses-feature android:glEsVersion="0x00020000"></uses-feature>

<application
android:allowBackup="false"
android:fullBackupContent="false"
android:supportsRtl="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:name="com.sample.teapot.TeapotApplication"
>

<!-- Our activity is the built-in NativeActivity framework class.
This will take care of integrating with our NDK code. -->
<activity android:name="com.sample.teapot.TeapotNativeActivity"
android:label="@string/app_name"
android:configChanges="orientation|keyboardHidden">
<!-- Tell NativeActivity the name of our .so -->
<meta-data android:name="android.app.lib_name"
android:value="TeapotNativeActivity"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>

这是 Google 官方示例中的 teapots 项目中的一个文件,我们就针对这份文件来分析字段的意义。字段的意义参考的是官方文档

<manifest>元素:所有的 xml 都必须包含 <manifest> 元素。这是文件的根节点。它必须要包含 <application> 元素,并且指明 xmlns:android 和 package 属性。

<manifest>元素中的属性

  • xmlns:android 这个属性定义了 Android 命名空间。必须设置成”http://schemas.android.com/apk/res/android”。不要手动修改。
  • package 这是一个完整的 Java 语言风格包名。包名由英文字母(大小写均可)、数字和下划线组成。每个独立的名字必须以字母开头。包名也代表着唯一的 application ID,用来发布应用。但是,要注意的一点是:在 APK 构建过程的最后一步,package 名会被 gradle 文件中的 applicationId 属性取代。如果这两个属性值一样,那么万事大吉,如果不一样,那就要小心了。构建 APK 的时候,构建系统使用这个属性来做两件事:
    • 生成 java 类时用这个名字作为命名空间(用于访问 APP 的资源)。比如:package 被设置成 com.sample.teapot,那么生成的 R 类就是:com.sample.teapot.R
    • 用来生成在 manifest 文件中定义的类的完整类名。比如 package 被设置成 sample.teapot,并且 activity 元素被声明成 <activity android:name=”.MainActivity”>,完整的类名就是 com.sample.teapot.MainActivity。
  • android:versionCode:内部的版本号。用来表明哪个版本更新。这个数字不会显示给用户。显示给用户的是 versionName。这个数字必须是整数。不能用 16 进制,也就是说不接受 “0x1” 这种参数
  • android:versionName:显示给用户看的版本号。

<manifest>元素中的元素

  • <uses-feature>元素:Google Play 利用这个元素的值从不符合应用需要的设备上将应用过滤。这东西的作用是将 APP 所依赖的硬件或者软件条件告诉别人。它说明了 APP 的哪些功能可以随设备的变化而变化。使用的时候要注意,必须在单独的 <uses-feature> 元素中指定每个功能,如果要多个功能,需要多个 <uses-feture> 元素。比如要求设备同时具有蓝牙和相机功能:
<uses-feature android:name="android.hardware.bluetooth"/>
<uses-feature android:name="android.hardware.camera"/>
  • <uses-feature>的属性:
    • android:name:该属性以字符串形式指定了 APP 要用的硬件或软件功能。
    • android:required:这项属性如果值为 true 表示需要这项功能否则应用无法工作,如果为 false 表示应用在必要时会使用该功能,但是如果没有此功能应用也能工作。
    • android:glEsVersion:指明应用需要的 Opengl ES 版本。高 16 位表示主版本号,低 16 位表示次版本号。例如,如果是要 2 的版本,就是 0x00030002。如果定义多个 glEsVersion,应用会自动启用最高的设置。
  • <application>元素:此元素描述了应用的配置。这是一个必备的元素,它包含了很多子元素来描述应用的组件,它的属性影响到所有的子组件。许多属性(例如 icon、label、permission、process、taskAffinity 和 allowTaskReparenting)都可以设置成默认值。
    • <application>的属性
      • android:allowBackup:表示是否允许 APP 加入到备份还原的结构中。如果设置成 false,那么应用就不会备份还原。默认值为 true。
      • android:fullBackupContent:这个属性指向了一个 xml 文件,该文件中包含了在进行自动备份时的完全备份规则。这些规则定义了哪些文件需要备份。此属性是一个可选属性。默认情况下,自动备份包含了大部分 app 文件。
      • android:supportsRtl:声明你的 APP 是否支持 RTL(Right To Left)布局。如果设置成 true,并且 targetSdkVersion 被设置成 17 或更高。很多 RTL API 会被集火,这样你的应用就可以显示 RTL 布局了。如果设置成 false 或者 targetSdkVersion 被设置成 16 或更低。哪些 RTL API 就不起作用了。该属性的默认的值是 false。
      • android:icon:APP 的图标,以及每个组件的默认图标。可以在组价中自定义图标。这个属性必须设置成一个引用,指向一个可绘制的资源,这个资源必须包含图片。系统不设置默认图标。例如 mipmap/ic_launcher 引用的就是下面的资源
      • android:label:一个用户可读的标签,以及所有组件的默认标签。子组件可以用他们的 label 属性定义自己的标签,如果没有定义,那么就用这个标签。标签必须设置成一个字符串资源的引用。这样它们就能和其他东西一样被定位,比如 @string/app_name。当然,为了开发方便,你也可以定义一个原始字符串。
      • android:theme:该属性定义了应用使用的主题的,它是一个指向 style 资源的引用。各个 activity 也可以用自己的 theme 属性设置自己的主题。
      • android:name:Application 子类的全名。包括前面的路径。例如 sample.teapot.TeapotApplication。当应用启动时,这个类的实例被第一个创建。这个属性是可选的,大多数 APP 都不需要这个属性。在没有这个属性的时候,Android 会启动一个 Application 类的实例。
    • <activity>元素:该元素声明一个实现应用可视化界面的 Activity(Activity 类子类)。这是 <application> 元素中必要的子元素。所有 Activity 都必须由清单文件中的 <activity> 元素表示。任何未在该处声明的 Activity 对系统都不可见,并且永远不会被执行。
      • android:name:Activity 类的名称,是 Activity 类的子类。该属性值为完全限定类名称,例如 sample.teapot.TeapotNativeActivity。为了方便起见,如果第一个字符是点(’.’),就需要加上 <manifest> 元素中的包名。应用一旦发布,不应更改该名称。没有默认值,必须指定该名称。
      • android:label:Activity 标签,可以被用户读取。该标签会在 Activity 激活时显示在屏幕上。如果未设置,用 <application> 中的 label 属性。对属性的设置要求和 <application> 中一样。
      • android:configChanges:列出 Activity 将自行处理的配置更改消息。在运行时发生配置更改时,默认情况下会关闭 Activity 然后将其重新启动,但使用该属性声明配置将阻止 Activity 重新启动。Activity 反而会保持运行状态,并且系统会调用其onConfigurationChanged() 方法。注:应避免使用该属性,并且只应在万不得已的情况下使用。如需了解有关如何正确处理配置更改所致重新启动的详细信息,请阅读处理运行时变更。这属性可以设置的项很多,这里列出常用的项:
        • orientation:屏幕放心啊发生了变化,比如用户旋转了设备
        • keyboardHidden:键盘无障碍功能发生了变化,比如用户显示了硬件键盘
        • android:launchMode:关于如何启动 Activity 的指令。一共有四种指令:”standard”、”singleTop”、”singleTask”、”singleInstance” 默认情况下是 standard。这些模式被分为两大类:”standard” 和 “singleTop” 是一类。该模式的 Activity 可以多次实例化。实例可属于任何任务,并且可以位于 Activity 堆栈中的任何位置。”singleTask” 和 “singleInstance” 是一类。该模式只能启动任务,它们始终位于 Activity 堆栈的根位置。此外,设备一次只能保留一个 Activity 实例。设置成 singleTask 后,系统在新任务的根位置创建 Activity 并向其传送 Intent。如果已经存在一个 Activity 实例,则系统会通过调用该实例的 onNewIntent() 方法向其传送 Intent 而不是创建一个新的 Activity 实例。
      • android:theme:设定主题格式,与 <application> 中的 theme 类似。
    • <meta-data>元素:指定额外的数据项,该数据项是一个 name-value 对,提供给其父组件。这些数据会组成一个 Bundle 对象,可以由 metaData 字段使用。虽然可以使用多个 <meta-data> 元素标签,但是不推荐这么使用。如果有多个数据项要指定,推荐做法是:将多个数据项合并成一个资源,然后使用一个 <meta-data> 包含进去。该元素有三个属性:

      • android:name:数据项名称,这是一个唯一值。
      • android:resource:一个资源的引用。
      • android:value:数据项的值。
    • <intent-filter> 元素:指明这个 activity 可以以什么样的意图(intent)启动。该元素有几个子元素可以包含。我们先介绍遇到的这两个:
      • <action> 元素:表示 activity 作为一个什么动作启动,intent.action.MAIN 表示作为主 activity 启动。
      • <category> 元素:这是 action 元素的额外类别信息,intent.category.LAUNCHER 表示这个 activity 为当前应用程序优先级最高的 Activity。

需要注意的是,直接通过 zip 解压出来的 AndroidManifest.xml 文件是经过压缩过的。如果直接用记事本打开是乱码。可以通过 AXMLPrinter2 工具解开。具体流程后面会详细介绍。

Res 文件夹

在 Android 项目文件夹里面,主要的资源文件 是放在 res 文件夹,res 文件夹下为所有的资源文件。

  • res/assets:assets 文件夹是存放不进行编译加工的原生文件,即该文件夹里面的文件不会像 xml,java 文件被预编译,可以存放一些图片,html,js,css 等文件。assets 目录不会被映射到 R 中,因此,资源无法通过 id 方式获取,必须要通过 AssetManager 进行操作与获取,assets 下可以有多级目录(只有 /assets 目录可以包含任意子目录列表。每个其他目录只能有该目录级别的文件)。注意:Gradle 默认不创建 asserts 文件夹,但他的路径已经存在于 main 文件夹下面了。
  • res/animator:用于定义属性动画的 XML 文件。
  • res/anim:用于定义动画对象。存放定义了补间动画(tweened animation)或逐帧动画(frame by frame animation)的 XML 文件。
  • res/drawable:存放各种图片类型,不能纯数字定义文件名,另可以新建 .xml 文件类型通常自定义控件样式时会在此文件夹中新建个 .xml 格式文件作为背景图。
  • res/color:用于定义颜色状态列表的 XML 文件。请参阅颜色状态列表资源
  • res/layout:用于定义用户界面布局的 XML 文件。
  • res/menu:用于定义应用菜单(如选项菜单、上下文菜单或子菜单)的 XML 文件。
  • res/raw:需以原始形式保存的任意文件。如要使用原始 InputStream 打开这些资源,请使用资源 ID(即 raw.filename)调用 Resources.openRawResource()。但是,如需访问原始文件名和文件层次结构,则可以考虑将某些资源保存在 assets/ 目录(而非 res/raw/)下。assets/ 中的文件没有资源 ID,因此您只能使用 AssetManager 读取这些文件。
  • res/values:包含字符串、整型数和颜色等简单值的 XML 文件。其他 res/ 子目录中的 XML 资源文件会根据 XML 文件名定义单个资源,而 values/ 目录中的文件可描述多个资源。对于此目录中的文件,<resources> 元素的每个子元素均会定义一个资源。例如,<string> 元素会创建 string 资源,<color> 元素会创建 R.color 资源。由于每个资源均使用自己的 XML 元素进行定义,因此您可以随意命名文件,并在某个文件中放入不同的资源类型。但是,您可能需要将独特的资源类型放在不同的文件中,使其一目了然。例如,对于可在此目录中创建的资源,下面给出了相应的文件名约定:
    • xml:资源数组(类型数组)。
    • xml:颜色值。
    • xml:尺寸值。
    • xml:字符串值。
    • xml:样式。
  • res/xml:可在运行时通过调用 getXML() 读取的任意 XML 文件。各种 XML 配置文件(如可搜索配置)都必须保存在此处。
  • res/xml:带有扩展名的字体文件(如 .ttf、.otf 或 .ttc),或包含 <font-family> 元素的 XML 文件。

resources.arsc 文件

resource.arsc 文件是 Apk 打包过程中的产生的一个资源索引文件。在对 apk 进行解压或者使用 Android Studio 对 apk 进行分析时便可以看到 resource.arsc 文件。通过学习 resource.arsc 文件结构,可以帮助我们深入了解 apk 包体积优化中使用到的重复资源删除、资源文件名混淆技术。

arsc 文件作用

在 Java 中访问一个文件是需要提供文件路径,如:

new File("./res/drawable-xxhdpi/img.png");

而在 Android 中,却可以通过 drawableId 获得资源文件:

getDrawable(R.drawable.img);

这里凭一个 id 就能获取资源文件内容,省去了文件路径的手动输入,其背后就是通过读取 arsc 文件实现的。这些 R.drawable.xxx、R.layout.xxx、R.string.xxx 等的值(存储在 R.jar 或者 R.java 文件中)称之为资源索引,通过这些资源索引可以在 arsc 文件中查取实际的资源路径或资源值。例如:getDrawable(R.drawable.img) 在编译后成了 getDrawable(2131099964),再将 id 转为十六进制:2131099964=0x7f06013c。这时的资源索引为 0x7f06013c。

资源索引具有固定的格式:0xPPTTEEEE,PackageId(2 位)+TypeId(2 位)+EntryId(4 位):

  • PP: PackageID,包的命名空间,取值范围为 [0x01,0x7f],第三方应用均为 7f。
  • TT: 资源类型,有 anim、layout、mipmap、string、style 等资源类型。
  • EEEE: 代表某一类资源在偏移数组中的值

所以 0x7f06013c 中 PackageId=0x7f、TypeId=0x06、EntryId=0x013c

最简单的我们可以将 arsc 函数想象成一个含有多个 Pair 数组的文件,且每个资源类型(TypeId)对应一个 Pair[](或多个,为了便于理解先只认为是一个)。因此在 arsc 中查找 0x7f06013c 元素的值,就是去设法找到 TypeId=0x06 所对应的数组 Pair[],然后找到其中的第 0X013c 号元素 Pair[0X013c]。这个元素恰好就是 Pair(“img”,”./res/drawable-xxhdpi/img.png”),左边是资源名称 img,右边是资源的文件路径 “./res/drawable-xxhdpi/img.png”,有了文件路径,程序便可以访问到对应的资源文件了。

当然实际的 arsc 文件在结构上要稍微复杂一点,下面开始分析 arsc 文件结构。

chunk为了便于理解,在正式介绍resource.arsc(以下简称arsc)文件前,需要对chunk进行解释一下,在其他文章中也多次使用了“chunk”这个词。chunk翻译为中文就是“块、部分(尤指大部分,一大块)”的意思,例如:一棵树,可以分为三个chunk(部分):树冠、树茎、树根。也可以将一棵树视为一个chunk,这个chunk就是这棵树。

arsc文件结构

resources.arsc是一个二进制文件,其内部结构的定义在ResourceTypes.h,arsc文件结构:

图片整体描述了arsc文件中各个chunk的关系(注意结合图片左右两侧内容):

  • 整个arsc文件是一个RES_TABLE_TYPE类型的chunk;
  • RES_TABLE_TYPE可分为三个部分:文件头部和两个子chunk(RES_STRING_POOL_TYPE、RES_TABLE_PACKAGE_TYPE);
  • RES_TABLE_PACKAGE_TYPE中包含了:头部、资源类型字符串常量池、资源项名称字符串常量池、多个子chunk(RES_TABLE_TYPE_SPEC_TYPE和RES_TABLE_TYPE_TYPE);
  • 每种类型的chunk都含有一个头结构

META-INF目录

META-INF目录下存放的是签名信息,用来保证apk包的完整性和系统的安全。Android开发环境对每一个需要Release的APK都会进行签名,在APK文件被安装时,Android系统会对APK的签名信息进行比对,以此来判断程序的完整性,最终确定APK是否可以正常安装使用,一定程度上达到安全的目的。

META-INF目录下看到四个文件:MANIFEST.MF、CERT.SF、CERT.RSA

  • MF(摘要文件):程序遍历APK包中的所有文件,对非文件夹非签名文件的文件,逐个用SHA1生成摘要信息,再用Base64进行编码。如果APK包的文件被修改,在APK安装校验时,被修改的文件与MANIFEST.MF的校验信息不同,程序将无法正常安装。
  • SF(对摘要文件的签名文件):对于生成的MANIFEST.MF文件利用SHA1-RSA算法对开发者的私钥进行签名。在安装时只有公共密钥才能对其解密。解密之后将其与未加密的摘要信息进行比对,如果相符则文件没有被修改。
  • RSA保存公钥、加密算法等信息。

在APK进行安装时,可以通过MANIFEST.MF文件开始的环环相扣来保证APK的安全性。比如拿到一个apk包后,如果想要替换里面的一幅图片,一段代码,或一段版权信息,想直接解压缩、替换再重新打包,基本是不可能的。如此一来就给病毒感染和恶意修改增加了难度,有助于保护系统的安全。但这些文件或者密钥如果被攻击者得到或者被攻击者通过某些技术手段攻破,则Android操作系统无法验证其安全性。

classes.dex文件

dex文件是Android系统中的一种文件,是一种特殊的数据格式,和APK、jar等格式文件类似。能够被DVM识别,加载并执行的文件格式。相对于PC上的java虚拟机能运行.class;android上的Davlik虚拟机能运行.dex。当Java程序编译成class后,还需要使用dx工具将所有的class文件整合到一个dex文件,目的是其中各个类能够共享数据,在一定程度上降低了冗余,同时也是文件结构更加经凑,实验表明,dex文件是传统jar文件大小的50%左右。

在明白什么是Dex文件之前,要先了解一下JVM,Dalvik和ART。JVM是JAVA虚拟机,用来运行JAVA字节码程序。Dalvik是Google设计的用于Android平台的运行时环境,适合移动环境下内存和处理器速度有限的系统。ART即Android Runtime,是Google为了替换Dalvik设计的新Android运行时环境,在Android4.4推出。ART比Dalvik的性能更好。Android程序一般使用Java语言开发,但是Dalvik虚拟机并不支持直接执行JAVA字节码,所以会对编译生成的.class文件进行翻译、重构、解释、压缩等处理,这个处理过程是由dx进行处理,处理完成后生成的产物会以.dex结尾,称为Dex文件。Dex文件格式是专为Dalvik设计的一种压缩格式。所以可以简单的理解为:Dex文件是很多.class文件处理后的产物,最终可以在Android运行时环境执行。

Java代码转化为dex文件的流程如图所示,当然真的处理流程不会这么简单,这里只是一个形象的显示:

lib文件夹

引用的第三方sdk的so文件,有C/C++编译而成。

Apk的打包流程

我们先对安卓的打包流程进行一个简单的了解,从而明白.java文件是一步步成为apk中的一部分的,在生成apk的过程中主要包含以下流程,括号中代表使用的工具:

  • 打包资源文件,生成java文件(aapt)
  • 处理aidl文件,生成相应的.java文件(aidl)
  • 编译项目源代码,生成class文件(javac)
  • 转换所有的class文件,生成dex文件(dx)
  • 编译过的资源和.dex文件都会被apkbuilder工具打包到最终的.apk文件中。(apkbuilder)
  • 对APK文件进行签名(jarsigner)
  • 对签名后的APK文件进行对齐处理(zipalign)

APK文件的反编译

资源文件获取

apktool主要用于资源文件的获取,Apktool的主要作用:

  • 将资源文件还原成原始形式(9.png、xml)
  • 将Android的dex文件反编译为smali源码
  • 将反编译的资源重新编译成APK/JAR

所以Apktool不光能够拆解apk,还能加已经拆解的apk资源重新组装成apk。

使用就比较简单了,直接执行如下命令就可以进行反编译:

apktool d bar.apk //直接解码
apktool d bar.apk -o baz //解码到baz的文件夹中

Apktool既然能够进行反编译,那也能重新编译成Apk文件:

apktool b bar //在父目录执行building
apktool b . //在当前bar目录执行building
apktool b bar -o new_bar.apk // 在父目录执行building并生成名为new_bar的apk文件

使用apktool大概有如下作用:

  • 查看AndroidManifest文件内容,知道每一个Activity的绝对路径,方便后面查看代码快速定位
  • 拿到完整的资源文件(assest、drawable、resouces…目录下的文件)
  • 如果我们熟悉smali语法的话,还可以将别人的代码进行修改然后重新打包成apk(比如破解会员验证)

解压后获得AndroidManifest.xml文件、assets文件夹、res文件夹、smali文件夹等。original文件夹是原始的AndroidManifest.xml文件,res文件夹是反编译出来的所有资源,smali文件夹是反编译出来的代码。注意,smali文件夹下的结构和我们的源代码的package一模一样,只不过换成了smali语言。它有点类似于汇编的语法,是Android虚拟机所使用的寄存器语言。

使用apktool版本时如果版本过低会报如下错误:

.........apktool..........
I: Baksmaling...
testI: Loading resource table...
Exception in thread "main" brut.androlib.AndrolibException: Could not decode arsc file
at brut.androlib.res.decoder.ARSCDecoder.decode(ARSCDecoder.java:55)
at brut.androlib.res.AndrolibResources.getResPackagesFromApk(AndrolibResources.java:315)
at brut.androlib.res.AndrolibResources.loadMainPkg(AndrolibResources.java:50)
at brut.androlib.res.AndrolibResources.getResTable(AndrolibResources.java:43)
at brut.androlib.Androlib.getResTable(Androlib.java:44)
at brut.androlib.ApkDecoder.getResTable(ApkDecoder.java:148)
at brut.androlib.ApkDecoder.decode(ApkDecoder.java:98)
at brut.apktool.Main.cmdDecode(Main.java:120)
at brut.apktool.Main.main(Main.java:57)
Caused by: java.io.IOException: Expected: 0x001c0001, got: 0x00000000
at brut.util.ExtDataInput.skipCheckInt(ExtDataInput.java:48)
at brut.androlib.res.decoder.StringBlock.read(StringBlock.java:45)
at brut.androlib.res.decoder.ARSCDecoder.readPackage(ARSCDecoder.java:97)
at brut.androlib.res.decoder.ARSCDecoder.readTable(ARSCDecoder.java:82)
at brut.androlib.res.decoder.ARSCDecoder.decode(ARSCDecoder.java:48)
...8 more

解决方案是去官网下载最新版本,替换后如果发现如下错误:

.........apktool..........
Smali Debugging has been removed in 2.1.0 onward. Please see: https://github.com/
iBotPeaches/Apktool/issues/1061

原因是最新版的apktool已将SmaliDebugging移除,我的解决方案是下载2.0.9版本。

XML文件的反编译

在apk中的xml文件是经过压缩的,可以通过AXMLPrinter2工具解开,具体命令类似:

java -jar .\AXMLPrinter2.jar .\AndroidManifest.xml > .\AndroidManifest.txt

apktool已经自带AndroidManifest.xml文件解压,所以这个工具价值不大。

classes.dex文件反编译

classes.dex是java源码编译后生成的java字节码文件。但由于Android使用的dalvik虚拟机与标准的java虚拟机是不兼容的,dex文件与class文件相比,不论是文件结构还是opcode都不一样。目前常见的java反编译工具都不能处理dex文件。Android模拟器中提供了一个dex文件的反编译工具,dexdump。用法为首先启动Android模拟器,把要查看的dex文件用adb push上传的模拟器中,然后通过adb shell登录,找到要查看的dex文件,执行dexdump xxx.dex。但是这样得到的结果,其可读性是极差的。下面介绍一个可读性比较好的工具。

  • dex2jar:把dex文件反编译为jar文件的工具。
  • JD-GUI:把jar反编译为java的工具。

反编译的步骤:

  • 从APK中提取dex文件,对APK文件解压即可得到。将其放到dex2jar的目录下,打开cmd,运行bat classes.dex,生成classes.dex.dex2jar.jar。
  • 运行JD-GUI工具,打开上面的jar文件,即可看到源代码。

dex2jar软件包中包含的工具:

  • d2j-jar2dex:调用dx将jar转换为dex
  • d2j-jar-remap:重命名jar文件中的包(package)/类(class)/方法(method)/域(field)
  • d2j-dex2jar:将dex转换为jar
  • dex2jar:此工具已被弃用,若可能请使用d2j-dex2jar
  • d2j-jasmin2jar:将.j文件编译为.class文件
  • d2j-jar-access:增加或移除jar文件中对类(class)/方法(method)/域(field)的访问
  • d2j-asm-verify:校验jar文件中的.class文件
  • d2j-dex-dump:将.dex或.apk文件中的数据dump至dump.jar文件中
  • d2j-init-deobf:为反混淆jar文件生成初始化配置文件
  • d2j-apk-sign:用测试证书对apk文件进行数字签名
  • d2j-jar2jasmin:反汇编jar文件中的.class文件至jasmin文件

Enjarify是由Google推出的一款基于Python3开发,类似dex2jar的反编译工具,它可以将Dalvik字节码转换成相对应的Java字节码,有比dex2jar更优秀的兼容性,准确性及更高的效率。

有时使用JD-GUI反编译的时候会有部分文件(Constants类即常量类)打开是// INTERNAL ERROR //,解决办法,使用其他Luytenjadx代替。

自动化工具汇总(一键反编译Apk)

onekey-decompile-apk

onekey-decompile-apk是先前我使用的 apk 反编译工具,功能比较简单,主要是 apktool/dex2jar/jd-gui 只需执行一步即可反编译出 apk 所有文件(资源文件和 jar 等等)。使用方法非常简单:

  • 解压缩下载的 onekey-decompile-apk.zip
  • 将 apk 文件放到 onekey-decompile-apk 目录下
  • 将 apk 文件拖拽到 _onekey-decompile-apk.bat 上

由于作者长时间没有更新,其实自己可升级 _tool 目录下的工具,并在 _onekey-decompile-apk.bat 中修改成最新版软件的路径。

TTDeDroid

TTDeDroid 也是一键反编译工具,支持反编译 decompile apk/aar/dex/jar,主要集成了 jadx/dex2jar/enjarify(不需要手动安装 Python)

Google 官方:android-classysh

android-classysh 是 Google 退出的一键反编译工具,直接打开 Apk 文件,就可以看到 Apk 中所有的文件结构,甚至还集成了 dex 文件查看,java 代码查看,方法数分析、导入混淆 mapping 文件等一系列工具。谷歌推出这个工具的目的是为了让我们开发者更清楚的了解自己的 Apk 中都有什么文件、混淆前后有什么变化,并方便我们进一步优化自己的 Apk 打包实现。比较不好的体验是无法进行导出。

Python 实现的工具:Androguard

Androguard 集成了反编译资源、代码等各种文件的工具包。需要安装 Python 环境来运行这个工具,这个工具按照不同的反编译需求,分别写成了不同的 py 功能模块,还有静态分析的功能。所以如果想要用 Python 开发一个解析 Apk 文件并进行静态扫描分析的服务,可以引用这个工具来实现。

AndroidKiller

AndroidKiller 集 Apk 反编译、Apk 打包、Apk 签名,编码互转,ADB 通信(应用安装-卸载-运行-设备文件管理)等特色功能于一身,支持 logcat 日志输出,语法高亮,基于关键字(支持单行代码或多行代码段)项目内搜索,可自定义外部工具;吸收融汇多种工具功能与特点,打造一站式逆向工具操作体验,大大简化了用户在安卓应用/游戏修改过程中的各类繁琐工作。

GDA

GDA 除了反编译外,还支持包过滤的分析功能,提供有算法工具,文件转换工具等等Bytecode Viewer

Bytecode Viewer 是一个强大的反编译工具,其集成了 6 个 Java 反编译库(包含 Fernflower 和 CFR),Andorid 反编译类库和字节码类库。

这个工具提供 GUI 界面,可以提升使用 CFR 的用户体验。

APK 反编译高级篇

Android APK 中的 Java 代码可以被反编译到什么程度主要看 APK 的加密程度。

  • 第一种情况:无混淆无加密无加壳。直接利用 Dex2jar 和 JD-GUI 可把源码从 APK 里抠出来,代码逻辑清晰,基本上做到可复用,只是资源文件的引用需要计算一下。
  • 第二种情况:混淆。通常是利用 ProGuard 做的防护。因为是对 jar 做的不可逆混淆(除非有 mapping),因此不能还原成原来的代码。但是代码结构,代码逻辑一致,只要花长时间对代码进行梳理一样可找准核心代码,解密方法跟第一种一致。
  • 第三种情况:加密。以 DexGuard 为例。对于这种代码加密的方法,在程序运行中必定会进行解密,只要抽出它解密的逻辑便可。
  • 第四种情况:加壳。这种情况跟第三种类似。无论你怎么加壳,运行的时候必定是 Dalvik 可识别的 Odex 代码,建议直接在内存里 dump 出来。

Android 混淆处理:ProGuard

Android SDK 自带了混淆工具 Proguard。它位于 SDK 根目录\tools\proguard 下面。ProGuard 是一个免费的 Java 类文件收缩,优化,混淆和预校验器。它可以检测并删除未使用的类,字段,方法和属性。它可以优化字节码,并删除未使用的指令。它可以将类、字段和方法使用短无意义的名称进行重命名。最后,预校验的 Java 6 或针对 Java Micro Edition 的所述处理后的码。如果开启了混淆,Proguard 默认情况下会对所有代码,包括第三方包都进行混淆,可是有些代码或者第三方包是不能混淆的,这就需要我们手动编写混淆规则来保持不能被混淆的部分。

Android 中的“混淆”可以分为两部分,一部分是 Java 代码的优化与混淆,依靠 proguard 混淆器来实现;另一部分是资源压缩,将移除项目及依赖的库中未被使用的资源。

代码混淆

  • 压缩(Shrinking):默认开启,用以减小应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员)。
  • 优化(Optimization):默认开启,在字节码级别执行优化,让应用运行的更快。
  • 混淆(Obfuscation):默认开启,增大反编译难度,类、函数、变量名会被随机命名成无意义的代号形如:a,b,c…之类的,除非用 keep 保护。

上面这几个功能都是默认打开的,要关闭他们只需配置对应的规则即可。混淆后默认会在工程目录 app/build/outputs/mapping/release 下生成一个 mapping.txt 文件,这就是混淆规则,我们可以根据这个文件把混淆后的代码反推回源本的代码。原则上,代码混淆后越乱越无规律越好,但有些地方我们是要避免混淆的,否则程序运行就会出错。

资源压缩

资源压缩将移除项目及依赖的库中未被使用的资源,这在减少 apk 包体积上会有不错的效果,一般建议开启。具体做法是在 build.grade 文件中,将 shrinkResources 属性设置为 true。需要注意的是,只有在用 minifyEnabled true 开启了代码压缩后,资源压缩才会生效。资源压缩包含了“合并资源”和“移除资源”两个流程。“合并资源”流程中,名称相同的资源被视为重复资源会被合并。需要注意的是,这一流程不受 shrinkResources 属性控制,也无法被禁止,gradle 必然会做这项工作,因为假如不同项目中存在相同名称的资源将导致错误。gradle 在四处地方寻找重复资源:

  • src/main/res/ 路径
  • 不同的构建类型(debug、release 等等)
  • 不同的构建渠道
  • 项目依赖的第三方库合并资源时按照如下优先级顺序:

依赖->main->渠道->构建类型

举个例子,假如重复资源同时存在于 main 文件夹和不同渠道中,gradle 会选择保留渠道中的资源。同时,如果重复资源在同一层次出现,比如 src/main/res/ 和 src/main/res2/,则 gradle 无法完成资源合并,这时会报资源合并错误。“移除资源”流程则见名知意,需要注意的是,类似代码,混淆资源移除也可以定义哪些资源需要被保留,这点在下文给出。

DexGuard 混淆(防二次打包)

DexGuard 与 Android 上主流的混淆工具 ProGuard 同属一家公司开发,但相比免费的 ProGuard 功能更多,混淆力度也更大。

  • Progurad 是免费的,而且已经集成到 Android ADT 中了,使用起来很方便。Proguard 只能保护代码,却不能保护我们的 apk 文件。任何人都可以使用 apktool 工具,反编译我们开发的 apk 文件,进而更改其中各种资源,或者更改部分代码,甚至是注入代码,然后再打包回 apk,二次发布后,达到自己的目的。或者是加入了广告,或者是增加了恶意木马病毒等。不需要 multi-dex。
  • DexGuard 是收费的,DexGuard 是在 Proguard 基础上,加入了更多的保护措施。使用 DexGuard 混淆后,生成的 apk 文件,就无法正常使用 apktool 反编译了。尽管还是能够反编译出部分资源文件,但是由于反编译过程不完全,就无法再打包成 apk 了。这样就保护了我们的 apk 文件,不会被二次打包发布了。代码混淆力度更大+资源混淆+so 加壳等。自带 multi-dex 扫描。

Apk 文件加壳

所谓 Apk 加壳,就是给目标 Apk 加一层保护程序,把重要数据信息隐藏起来。加壳程序可以有效阻止对程序的反编译和逆向分析。Apk 壳本质的功能就是实现类加载器。系统先执行壳代码,然后将加了密的 dex 进行解密操作,再加载到系统内存中运行。

安卓 dex 加壳原理

加壳过程中主要有三个程序:

  • 需要加壳的源 Apk1(未加壳的原始应用)
  • 壳程序 APK2(用来解密并运行 apk1 程序)
  • 加密工具(将源 APK1 进行加密,并和壳程序 APK2 的 dex 合并成新的 dex)

加壳过程:

  • 得到需要加密的 apk 和自己的脱壳程序 apk1。
  • 利用加壳工具对源 apk 进行加密。把加密后的 apk 数据写入脱壳程序的 Dex 末尾,并在文件尾部添加加密数据的大小;修改脱壳程序 Dex 头中的 signature 和 file_size 头信息;合并得到新的 Dex1 文件。
  • 然后将新的 Dex1 文件替换原脱壳程序 apk1 中 dex 文件。生成新的 apk,叫做脱壳程序 apk。

脱壳过程:

  • 读取 Dex 文件末尾数据获取待脱壳加密数据长度。
  • 从 Dex 文件读取脱壳数据,解密加密数据。以文件形式保存解密的数据到 *.apk 文件.
  • 通过 DexClassLoader 动态加载 *.apk。

壳史

第一代壳:DEX 加密(混淆技术)

  • Dex 字符串加密
  • 资源加密
  • 对抗反编译
  • 反调试
  • 自定义 DexClassLoader

第二代壳:Dex 抽取与 So 加固(加壳技术)

  • 对抗第一代壳常见的脱壳法
  • DexMethod 代码抽取到外部
  • Dex 动态加载
  • So 加密

类抽取常规的有隐藏 dex 文件和修改 dex 结构。隐藏 dex 文件是通过对目标 dex 文件进行整体加密或压缩方式把整个 dex 转换为另外一个文件存放在 assert 文件夹中或者其它地方,然后利用类加载器技术进行内存解密并加载运行。而修改 dex 结构则是抽取 DexCode 中的字节码指令后用零去填充,或者修改方法属性等操作,运行时在内存中做修正,修复等处理工作。

关于 Dex 动态加载,就要提到 dalvik 虚拟机了。它和 java 虚拟机一样,在运行程序时首先需要将对应的类加载到内存中。在标准虚拟机中,类加载可以从 class 文件中读取,也可以是其他二进制流,这样就可以在程序运行时手动加载 class,从而达到代码动态执行的目的。常用的有两个类:DexClassLoader 和 PathClassLoader,PathClassLoader 是安卓应用中的默认加载器。区别:

  • DexClassLoader 可以加载任何路径的 dex/dex/jar
  • PathClassLoader 只能加载 data/app 中的 apk,也就是已经安装到手机中的这个也是 PathClassLoader 作为默认的类加载器的原因,因为一般程序都是安装了,再打开,这时 PathClassLoader 就去加载指定的 apk.

第三代壳:Dex 动态解密与 So 混淆(指令抽离)

  • DexMethod 代码动态解密
  • So 代码膨胀混淆
  • 对抗之前出现的所有脱壳法

第四代壳:armvmp(指令转换)

vmp:用 vmp 加固后的还原过程比较复杂和困难,需要用大量的时间作分析。

壳的识别

国内提供 apk 加固的第三方技术公司有:娜迦、爱加密、梆梆加固、360 加固保、百度加固、腾讯加固等。

经过加固后的 apk,通过 dex2jar 反编译:

腾讯乐固:

360 加固:

通过查看资料发现大多数加密后都会生成相应的特征 so 文件。这样就可以根据 so 来查壳。

特征 So 文件 所属加固公司
libchaosvmp.so 娜迦
libddog.so 娜迦
libfdog.so 娜迦
libedog.so 娜迦企业版
libexec.so 爱加密
libexecmain.so 爱加密
ijiami.dat 爱加密
ijiami.ajm 爱加密企业版
libsecexe.so 梆梆免费版
libsecmain.so 梆梆免费版
libSecShell.so 梆梆免费版
libDexHelper.so 梆梆企业版
libDexHelper-x86.so 梆梆企业版
libprotectClass.so 360
libjiagu.so 360
libjiagu_art.so 360
libjiagu_x86.so 360
libegis.so 通付盾
libNSaferOnly.so 通付盾
libnqshield.so 网秦
libbaiduprotect.so 百度
aliprotect.dat 阿里聚安全
libsgmain.so 阿里聚安全
libsgsecuritybody.so 阿里聚安全
libmobisec.so 阿里聚安全
libtup.so 腾讯
libexec.so 腾讯
libshell.so 腾讯
mix.dex 腾讯
lib/armeabi/mix.dex 腾讯
lib/armeabi/mixz.dex 腾讯
libtosprotection.armeabi.so 腾讯御安全
libtosprotection.armeabi-v7a.so 腾讯御安全
libtosprotection.x86.so 腾讯御安全
libnesec.so 网易易盾
libAPKProtect.so APKProtect
libkwscmm.so 几维安全
libkwscr.so 几维安全
libkwslinker.so 几维安全
libx3g.so 顶像科技
libapssec.so 盛大
librsprotect.so 瑞星

这个特点可以作为加壳厂商的特征。这样就可以得到厂商加壳的套路,脱壳也就有了破解之法。
这里提一下网上的一款apk查壳工具:ApkScan-PKID.其工作原理就是根据apk加壳后生成的特征文件来匹配加壳厂商的。

常用脱壳软件

VirtualXposed

VirtualXposed:无需root手机即可使用Xposed框架。

ZjDroid

ZjDroid是基于Xposed Framewrok的动态逆向分析模块,逆向分析者可以通过ZjDroid完成以下工作:1、DEX文件的内存dump 2、基于Dalvik关键指针的内存BackSmali,有效破解主流加固方案 3、敏感API的动态监控 4、指定内存区域数据dump 5、获取应用加载DEX信息。6、获取指定DEX文件加载类信息。7、dump Dalvik java堆信息。8、在目标进程动态运行lua脚本。

FDex2

FDex2,可以从安卓app中dump导出有用的dex文件,供后续再从dex导出jar包,jar包导出java源码,功能非常的强大。

frida

frida的原理是,通过在PC上安装Frida,手机上运行frida-server,实现PC对手机的控制,同时通过js注入的方式,将dex从“壳”里“钩”出来。它是一款基于Python的hook(钩子)工具,因此在安装它之前我们需要先配置Py环境,现在的frida仅支持3.7以下的环境,3.8以上的暂不支持。FRIDA-DEXDump

dumpDex

dumpDex是一款Android脱壳工具,需要xposed支持drizzleDumper

drizzleDumper是一款基于内存搜索的Android脱壳工具,可以从运行中的安卓app中,利用ptrace机制,导出dex文件

参考链接:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注