Bootstrap

安卓逆向基础知识之apk文件结构

最近在看正己大佬的安卓逆向教程,我顺便了解了些相关的基础知识,我就想把这些基础知识做个汇总,若有错误请大佬指正!

一、apk文件结构

APK是Android Package的缩写,即Android安装包。而apk文件其实就是一个压缩包,我们可以将apk文件的后缀改为.zip来观察apk中的文件。

我们来了解当中一些常见的文件和文件夹:

assets文件夹

assets 这里存放的是静态资源文件(图片,视频等),这个文件夹下的资源文件不会被编译。不被编译的资源文件是指在编译过程中不会被转换成二进制代码的文件,而是直接被打包到最终的程序中。这些文件通常是一些静态资源,如图片、音频、文本文件等。

lib文件夹

lib:.so库(c或c++编译的动态链接库)。APK文件中的动态链接库(Dynamic Link Library,简称DLL)是一种可重用的代码库,它包含在应用程序中,以便在运行时被调用。这些库通常包含许多常见的函数和程序,可以在多个应用程序中共享,从而提高了代码的复用性和效率。

lib文件夹下的每个目录都适用于不同的环境下,armeabi-v7a目录基本通用所有android设备,arm64-v8a目录只适用于64位的android设备,x86目录常见用于android模拟器,x86-64目录适用于支持x86_64架构的Android设备(适用于支持通常称为“x86-64”的指令集的 CPU)

META-INF文件夹

META-INF:在Android应用的APK文件中,META-INF文件夹是存放数字签名相关文件的文件夹,包含以下三个文件:

  1. MANIFEST.MF:MANIFEST.MF文件是一个摘要清单文件,它包含了apk文件中除自己以外所有文件的数字摘要。
  2. CERT.SF:CERT.SF文件是用于存储通过私钥加密后得到的MANIFEST.MF文件的数字签名信息以及MANIFEST.MF文件中数字摘要的数字签名信息。
  3. CERT.RSA:CERT.RSA文件包含了CERT.SF文件的数字签名和对文件签名时所使用的数字证书。

总之,META-INF文件夹中的文件是用于保护APK文件的完整性和真实性的重要文件,可以确保APK文件来自合法的开发者,并且没有被篡改过。

现在看这些东西是不是有些不明白?什么是数字摘要、什么是数字签名、什么是数字证书什么的,没事,我们接下来讲解这些东西,让你搞明白。

APK签名机制原理

问:什么是数字摘要?

答:数字摘要是一种数学算法,将任何长度的数据转换为固定长度的唯一字符串,而且不同的明文通过Hash算法转换成固定长度的密文,其结果总是不同的,而同样的明文其摘要必定一致。数字摘要通常用于数据完整性验证和加密技术中,以确保数据在传输或存储过程中没有被篡改或损坏。数字摘要算法是单向的,即无法通过数字摘要反推出原始数据。常见的数字摘要使用的Hash算法有MD5、SHA-1、SHA-256等。这里插一嘴,现在MD5在有的情况下已经不安全了,可能会出现不同的数据加密成相同的密文,因为明文的排列是有无数种可能的,而MD5所转换的密文长度是固定的128位,无限的数据对应有限的长度是不可能永远让不同的数据转换为不同的内容的。

问:为什么会出现数字签名?

答:要回答这个问题说来话长,我们要先从信息的传输开始讲起。

对称加密:

这天小红和小明在教室里通过纸条来传递信息,但小红和小明间隔甚远,需要经过其他同学帮助才能成功传输信息,在这个过程中是无法保证信息不被泄露的,那为了这个传输的信息不被泄露就需要对信息进行加密,而加密和解密是需要一个双方都知道的密钥来进行的,加密者用密钥对明文进行加密得到密文,解密者用密钥对密文进行解密得到明文,加解密都用同样的密钥,即为对称加密。所以双方该如何确定出一个同样的密钥呢?如果由一方生成密钥,再将密钥发送给对方,那么攻击者也可以获取到密钥,就会导致加解密没有了作用。

那么什么办法可以解决这个问题呢?可以用非对称加密。

非对称加密:

在非对称加密中,密钥总是成对出现的,分别称之为公钥和私钥,私钥由自己安全保管不外泄,而公钥则可以发给网络中的任何人。用其中一把密钥加密的明文只能用另一把密钥进行解密,无法使用同一把密钥进行加解密,比如用公钥进行加密的明文只能用私钥进行解密,公钥自己没法解密。

你可能还是会有疑问,如果一方将公钥发送给另一方,自己再用私钥对明文进行加密,再把加密的数据给另一方,那这不还是会导致加解密没有了作用?又或者一方将私钥发送给另一方,自己再用公钥对明文进行加密,再把加密的数据给另一方,这不还是会被攻击者获取到私钥从而解密数据?如果非对称加密这么使用确实会出现这种问题,但是非对称加密一般不这么用,而是下面这种操作:

现在是服务器和浏览器之间的信息传输,它们之间是怎么做才能信息不外泄呢?

第一步:服务器会将非对称加密中的公钥发送给浏览器,浏览器生成一串随机数字,而这串随机数字使用服务器发送过来的公钥进行加密。

第二步:将通过公钥加密的随机数字发送给服务器,服务器接收后使用私钥进行解密,这样双方就都得到了一个同样的随机数字,而这个随机数字可以作为对称加密的密钥。

第三步:使用随机数字作为密钥对真正需要传递的数据进行加密传输。

这样就算是攻击者拦截到了服务器发送给浏览器的公钥,也只能无济于事,因为通过公钥加密的数据没法通过公钥进行解密。这套流程也被称之为SSL(安全套接字层)。

这看似很美好,但是这真的无懈可击吗?不,因为服务器和浏览器之间无法得知接收的公钥或者数据来自于谁,这样攻击者可以先将服务器发送的公钥拦截下来替换成自己的公钥,浏览器接收到之后无法辨别这个公钥是来自于谁的,只会傻傻的使用这个公钥对生成的随机数字进行加密,然后返回给服务器,攻击者再将返回的内容拦截下来,通过自己的私钥进行解密,这样攻击者又获得了对称加密的密钥。

所以想要解决这个问题,那么就需要知道接收到的信息是由谁发送的,这就引出了下一个概念——数字证书。

数字证书:

在讲数字证书之前,我们需要知道一个第三方机构——CA机构。CA机构是指数字证书认证机构(Certificate Authority),也称为证书颁发机构。它是一种可信第三方机构,负责颁发数字证书,用于证明数字身份、数字签名等安全通信和交易中的身份验证和数据保护。CA机构通过对证书申请者的身份进行认证,为其颁发数字证书,使得用户可以在网络上进行加密通信、数字签名、身份认证等安全操作,保障网络安全和数据隐私。

我们来讲讲数字证书和CA机构在身份验证中是扮演一个什么样的角色:

第一步、服务器会将自己的公钥、域名,还有自己所申请认证证书的CA机构,以及数字摘要的Hash算法、签名算法(用于生成数字签名的加密算法)、数字摘要、原始数据等信息打包在一起发送给自己申请的CA机构,该机构也有一对公私钥对,CA机构会用它的私钥对打包数据中的数字摘要进行加密,得到一个密文,而这个密文就是签名,数字签名生成后会被放在证书中发送给服务器的管理员,而这个证书就叫做TLS证书。

第二步、服务器将CA机构发送过来的TLS证书代替原本要发送给浏览器的公钥发送给浏览器,浏览器拿到这个证书之后不会选择第一时间相信,而是拿CA机构公开的公钥对证书中的签名进行解密得到数字摘要,浏览器也会从证书中提取出原始数据和数字摘要的Hash算法进行转换,这样就可以获得原始数据的数字摘要,再将这两数字摘要一对比就知道数据在服务器发送过来的途中有没有被篡改了。

第三步、如果解密后的数字摘要和由原始数据转换成的数字摘要一致,那么浏览器就会从证书中提取出公钥,从而可以安全的进行SSL。

不过需要注意的是,上面的数字证书是https的验证流程,而apk文件和https在验证数字证书的过程中所用到的算法和流程也有所不同。

HTTPS所用的数字证书通常需要经过CA机构的认证和颁发。而apk文件的数字证书包含了签名者的公钥、签名算法、签名时间等信息,在Android系统中使用的数字证书,是可以由开发者自行生成和使用。

Android在安装APK时,会验证APK的数字签名是否合法。验证的过程包括以下几个步骤:

  1. 提取APK文件中的数字证书。
  2. 从数字证书中提取公钥。
  3. 使用该公钥对APK文件中的数字签名进行解密,得到数字摘要。
  4. 对APK文件进行Hash运算,生成数字摘要。
  5. 比较步骤3和步骤4中生成的数字摘要是否一致,如果一致则认为数字签名合法,否则认为数字签名不合法。

需要注意的是,数字证书中包含了数字签名的信息,包括签名者的公钥、签名算法、签名时间等。数字签名本身是对APK文件的数字摘要进行加密得到的,而不是对证书进行加密。

APK文件中的数字证书通常存储在META-INF目录下的CERT.RSA文件中。在安装APK文件时,Android系统会提取CERT.RSA文件中的数字证书,并使用证书中的公钥对APK文件进行验证,以确保APK的真实性和完整性。如果数字证书无效或不匹配,则会提示安装失败或警告用户存在安全风险。

MT管理器是一个可以对APK文件进行修改和重新签名的工具。它使用的签名工具是Android SDK中的apksigner工具,一个官方提供的APK签名工具。

当MT管理器对APK文件进行修改后,它会将修改后的文件打包成一个新的APK文件。然后,MT管理器会调用apksigner工具,使用开发者提供的数字证书对新的APK文件进行签名。签名过程中,apksigner会对APK文件进行Hash运算,生成数字摘要,并使用提供的数字证书对数字摘要进行加密,最后生成新的META-INF目录和CERT.RSA文件。

最后,MT管理器会将签名后的APK文件保存到指定位置。需要注意的是,MT管理器只能对已经进行过数字签名的APK文件进行修改和重新签名。因为apksigner要求原本的APK文件必须进行了数字签名才能进行重新签名的操作。所以如果APK文件没有进行数字签名,apksigner无法对其进行签名操作,从而无法通过MT管理器进行修改和签名。

总结来说,Android系统验证APK的数字签名是为了确保APK的真实性和完整性。MT管理器使用apksigner工具对APK文件进行签名,并要求原始APK文件已经进行了数字签名才能进行修改和重新签名操作。

AndroidManifest.xml配置文件

AndroidManifest.xml是Android应用程序中最重要的文件之一,它包含了应用程序的基本信息,如应用程序的名称、图标、版本号、权限、组件(Activity、Service、BroadcastReceiver、Content Provider)等等。在应用程序运行时,系统会根据这个文件来管理应用程序的生命周期,启动和关闭应用程序,管理应用程序的组件等等。

我们来了解一下AndroidManifest.xml文件的主要组成部分:

  1. manifest标签

manifest标签是AndroidManifest.xml文件的根标签,它包含了应用程序的基本信息,如包名、版本号、SDK版本、应用程序的名称和图标等等。

  1. application标签

application标签是应用程序的主要标签,它包含了应用程序的所有组件,如Activity(活动)、Service(服务)、Broadcast Receiver(广播接收器)、Content Provider(内容提供者)等等。在application标签中,也可以设置应用程序的全局属性,如主题、权限等等。

  1. activity标签

activity标签定义了一个Activity组件,它包含了Activity的基本信息,如Activity的名称、图标、主题、启动模式等等。在activity标签中,还可以定义Activity的布局、Intent过滤器等等。

  1. service标签

service标签定义了一个Service组件,它包含了Service的基本信息,如Service的名称、图标、启动模式等等。在service标签中,还可以定义Service的Intent过滤器等等。

  1. receiver标签

receiver标签定义了一个BroadcastReceiver组件,它包含了BroadcastReceiver的基本信息,如BroadcastReceiver的名称、图标、权限等等。在receiver标签中,还可以定义BroadcastReceiver的Intent过滤器等等。

  1. provider标签

provider标签定义了一个Content Provider组件,它包含了Content Provider的基本信息,如Content Provider的名称、图标、权限等等。在provider标签中,还可以定义Content Provider的URI和Mime Type等等。

  1. uses-permission标签

uses-permission标签定义了应用程序需要的权限,如访问网络、读取SD卡等等。在应用程序安装时,系统会提示用户授权这些权限。

  1. uses-feature标签

uses-feature标签定义了应用程序需要的硬件或软件特性,如摄像头、GPS等等。在应用程序安装时,系统会检查设备是否支持这些特性。

以上是AndroidManifest.xml文件的主要组成部分,它们共同定义了应用程序的基本信息和组件,是应用程序的重要配置文件。现在如果看起来有点懵,没关系,后面实战会使用到它的,以后也会对它进行详解,那时你或许会有一点对它的理解了。

resources.arsc文件

resources.arsc文件是Android应用程序的资源文件之一,它是一个二进制文件,包含了应用程序的所有资源信息,例如布局文件、字符串、图片等。这个文件在应用程序编译过程中由aapt工具生成,并被打包进APK文件中。

resources.arsc文件的主要作用是提供资源的索引和映射关系。它将资源文件名、类型、值等信息映射到一个唯一的整数ID上。这个ID在R文件中定义,并且可以通过代码中的R类来引用。例如,R.layout.main表示布局文件main.xml对应的ID,R.string.app_name表示字符串资源app_name对应的ID。

当应用程序运行时,系统会根据R类中的ID来查找对应的资源,并将其加载到内存中,供应用程序使用。这个过程是通过解析resources.arsc文件和R类实现的。通过这种方式,应用程序可以方便地访问和使用资源,而不需要手动处理资源文件的位置和命名等问题。

需要注意的是,resources.arsc文件只包含资源的索引和映射关系,并不包含实际的资源内容。实际的资源内容存储在res文件夹中,按照资源类型和名称进行组织。当应用程序需要使用资源时,系统会根据resources.arsc文件中的索引信息找到对应的资源文件,并将其加载到内存中。

总之,resources.arsc文件是Android应用程序的资源文件之一,包含了资源的索引和映射关系。它和R类一起构成了应用程序访问和使用资源的基础。通过解析resources.arsc文件和使用R类,应用程序可以方便地加载和使用资源。

这里只是简单介绍了resources.arsc文件,其实还有一个比较重要的知识点,那就是resources.arsc文件结构,我怕篇幅太过于长了,这里就不细讲了,有兴趣的可以自行去了解,比如可以观看以下这些文章:

Android资源管理及资源的编译和打包过程分析 - 掘金 (juejin.cn)

(32条消息) 手把手教你解析Resources.arsc_beyond702的博客-CSDN博客

Android逆向:resource.arsc文件解析(Config List) - 掘金 (juejin.cn)

(32条消息) resource.arsc二进制内容解析 之 Dynamic package reference_BennuCTech的博客-CSDN博客

如果可以全部看完,那你对resources的文件结构和打包流程、资源管理及资源的编译的有了一定的了解。

res文件夹

res:资源文件目录,二进制格式。实际上,APK文件下的res文件夹并不是二进制格式,而是经过编译后的二进制资源文件。在Android应用程序开发中,资源文件通常是以XML格式存储的,如布局文件、字符串资源、颜色资源等。在编译时,Android编译器会将这些XML资源文件编译成二进制格式的资源文件,以提高应用程序的运行效率和安全性。虽然res文件夹下的二进制资源文件不能直接编辑和修改,但是开发者仍然可以通过Android提供的资源管理工具,如aapt、apktool等,来反编译和编辑这些资源文件的。

在res文件夹中,主要包含以下子文件夹和文件:

res子目录存储的资源类型
animator/用于定义属性动画的 XML 文件。
anim/用于定义补间动画的 XML 文件。属性动画也可保存在此目录中,但为了区分这两种类型,属性动画首选 animator/ 目录。
color/定义颜色状态列表的 XML 文件。如需了解详情,请参阅颜色状态列表资源
drawable/位图文件(PNG、.9.png、JPG 或 GIF)或编译为以下可绘制资源子类型的 XML 文件:位图文件九宫图(可调整大小的位图)状态列表形状动画可绘制对象其他可绘制对象如需了解详情,请参阅可绘制资源
mipmap/适用于不同启动器图标密度的可绘制对象文件。如需详细了解如何使用 mipmap/ 文件夹管理启动器图标,请参阅将应用图标放在 mipmap 目录中
layout/用于定义界面布局的 XML 文件。如需了解详情,请参阅布局资源
menu/用于定义应用菜单(例如选项菜单、上下文菜单或子菜单)的 XML 文件。如需了解详情,请参阅菜单资源
raw/需以原始形式保存的任意文件。如要使用原始 InputStream 打开这些资源,请使用资源 ID(即 R.raw.*filename*)调用 Resources.openRawResource()。但是,如需访问原始文件名和文件层次结构,请考虑将资源保存在 assets/ 目录(而非 res/raw/)下。assets/ 中的文件没有资源 ID,因此您只能使用 AssetManager 读取这些文件。
values/包含字符串、整数和颜色等简单值的 XML 文件。其他 res/ 子目录中的 XML 资源文件会根据 XML 文件名定义单个资源,而 values/ 目录中的文件可描述多个资源。对于此目录中的文件,<resources> 元素的每个子元素均会定义一个资源。例如,<string> 元素会创建 R.string 资源,<color> 元素会创建 R.color 资源。由于每个资源均使用自己的 XML 元素进行定义,因此您可以随意命名文件,并在某个文件中放入不同的资源类型。但是,您可能需要将独特的资源类型放在不同的文件中,使其一目了然。例如,对于可在此目录中创建的资源,下面给出了相应的文件名约定:arrays.xml 用于资源数组(类型化数组colors.xml 用于颜色值dimens.xml 用于维度值strings.xml 用于字符串值styles.xml 用于样式如需了解详情,请参阅字符串资源样式资源更多资源类型
xml/可在运行时通过调用 Resources.getXML() 读取的任意 XML 文件。各种 XML 配置文件(例如搜索配置)都必须保存在此处。
font/带有扩展名的字体文件(例如 TTF、OTF 或 TTC),或包含 <font-family> 元素的 XML 文件。如需详细了解以资源形式使用的字体,请参阅将字体添加为 XML 资源

上表的内容为安卓官方文档中所记录的内容。

上表所定义的子目录中,保存的资源为默认资源。换言之,这些资源定义应用的默认设计和内容。然而,不同类型的 Android 设备可能需要不同类型的资源。

例如,开发者可以为屏幕尺寸大于普通屏幕的设备提供不同的布局资源,以充分利用额外的屏幕空间。还可以提供不同的字符串资源,以便根据设备的语言设置翻译界面中的文本。如需为不同设备配置提供这些不同资源,除默认资源以外,开发者还需提供备用资源。这个现在可能不太明白,但后面实战会讲到。

在Android应用程序中,res文件夹中的资源文件可通过引用其资源 ID 来应用该资源。所有资源 ID 都在项目的 R 类中进行定义,该类由 aapt 工具自动生成,可以通过这些ID值来访问和使用应用程序中的资源。

那R类和res文件夹的关系是怎么样的呢?

R类与res文件夹下的资源文件之间的关系如下:

  1. R类的包名与应用程序的包名相同,即com.example.myapp。
  2. R类中的子类与res文件夹下的子文件夹相对应,如Rdrawable对应drawable文件夹,Rdrawable对应drawable文件夹,Rlayout对应layout文件夹(前面的类名表示表示子类,前面的类名表示表示子类,后面的类名表示父类)。
  3. R类中的每个子类都包含了对应资源文件的ID值,如R$drawable中包含了所有drawable文件夹下的图片的ID值。
  4. R类中的ID值是由Android编译器(aapt工具)自动生成的,每个ID值都是唯一的,可以通过这些ID值来访问和使用对应的资源文件。

虽说所有资源 ID 都在项目的 R 类中进行定义,但是有的安卓应用程序中R类中有attr子类,而res下却没有attr子目录的。遇到这种情况不要觉得惊讶,下面就要着重讲讲res下的values子目录了。

所有资源ID都在项目的R类中进行定义,也就是说是可以通过资源ID来进行引用的资源那就会在项目的R类中进行定义。所以res文件夹下的子目录也就官方列出的那些,而且每个子目录都装有特定类型的资源,资源还不能乱放,那有的在R类中定义了资源ID的资源但res下没有对应资源的子类,如attr、bool等资源都会在values子目录中声明,前面官方文档中也提到了 values/ 目录中的文件可描述多个资源,所有在values子目录中一个xml文件就描述了特定类型的多个资源。我们来看看这里面有哪些资源在values子目录中并且于R类中声明:

Bool:

包含布尔值的 XML 资源,保存在 的 XML 文件: res/values-small/bools.xml

color:

包含颜色值(十六进制颜色)的 XML 资源,保存在 的 XML 文件: res/values/colors.xml

dimen

包含尺寸值(及度量单位)的 XML 资源,保存在 的 XML 文件: res/values/dimens.xml

id:

提供应用资源和组件的唯一标识符的 XML 资源,保存在 的 XML 文件:res/values/ids.xml

integer:

包含整数值的 XML 资源,保存在 的 XML 文件:res/values/integers.xml

integers:

提供整数数组的 XML 资源,保存在 的 XML 文件: res/values/integers.xml

array:

提供 (可用于可绘制对象数组)的 XML 资源,保存在 的 XML 文件: res/values/arrays.xml

这些虽说是安卓官方文档所展示的values文件夹中的资源类型,但其实values中的资源类型还不止这些,如drawable、plural等资源类型。还有一点,上面所述的资源类型integers和array都是通过名称进行引用的,而不是通过资源ID来进行引用的。

总的来说就是通过资源ID来进行引用的资源那就会在项目的R类中进行定义,在R类中定义的资源在res下的子目录中找不到,那就去res/values中寻找。有的资源类型没有在R类中定义的是因为该资源类型不是通过资源的ID去引用的,而是通过名称等其他方式进行的引用。

为了更好的理解这玩意实战中的作用,我们来进行次实战——将程序的默认启动activity改为我们自己的activity。

我这里使用的是apktool。

第一步:先把要反编译的apk文件放到apktool所在的文件夹

第二步:在此文件夹中打开powershell并且输入cmd

第三步:输入命令:apktool.bat d 要反编译的apk文件名

 

完成上面步骤后在apktool文件夹中就会生成一个与要反编译的apk文件同名的文件夹,反编译的结果就在里面

我们下一步需要通过反编译查看Java源码,这里使用的是dex2jar。

第一步:将apk文件修改为.zip后缀后将.dex文件解压到dex2jar目录下

第二步:在dex2jar目录下使用命令行中输入“d2j-dex2jar.bat <dex文件名>”命令运行dex2jar工具

第三步:等待dex2jar工具完成转换。转换完成后,会在dex2jar目录下生成一个与.dex文件同名的.jar文件,这个就是源码了

我们使用jd-gui打开刚才得到的源码,大概就是这个样子:

这些工具在爱盘中都有存放,可以直接拿来使用。大家可能有些疑惑,为什么有那么多好使的工具,非要如此麻烦使用这三个工具呢?原因也很简单,虽然这三个工具用起来麻烦些,但是在显示上会更加的直观。

环境配置好,我们接下来就正式进行实战操作了:

第一步:写一个简单的Activity和布局文件。

Activity:

###### Class com.p005zj.wuaipojie.p006ui.MyActivity (com.zj.wuaipojie.ui.MyActivity)

.class public Lcom/zj/wuaipojie/ui/MyActivity;

.super Landroidx/appcompat/app/AppCompatActivity;

.source "MyActivity.java"

# direct methods

.method public constructor <init>()V

    .registers 1

    .line 7

    invoke-direct {p0}, Landroidx/appcompat/app/AppCompatActivity;-><init>()V

    return-void

.end method

# virtual methods

.method public onBackPressed()V

    .registers 2

    invoke-super {p0}, Landroidx/appcompat/app/AppCompatActivity;->onBackPressed()V

    invoke-virtual {p0}, Landroid/app/Activity;->finish()V

    return-void

.end method

.method protected onCreate(Landroid/os/Bundle;)V

    .registers 6

    const p1, 0x7f0b007e

    invoke-virtual {p0, p1}, Lcom/zj/wuaipojie/ui/MyActivity;->setContentView(I)V

    new-instance v0, Landroid/os/Handler;

    invoke-direct {v0}, Landroid/os/Handler;-><init>()V

    iput-object v0, p0, Lcom/zj/wuaipojie/ui/MyActivity;->handler:Landroid/os/Handler;

    new-instance v0, Lcom/zj/wuaipojie/ui/MyActivity$1;

    invoke-direct {v0, p0}, Lcom/zj/wuaipojie/ui/MyActivity$1;-><init>(Lcom/zj/wuaipojie/ui/MyActivity;)V

    const-wide/16 v1, 0x12c

    invoke-virtual {p0, v0, v1, v2}, Landroid/os/Handler;->postDelayed(Ljava/lang/Runnable;J)Z

    return-void

    .line 20

    new-instance v0, Landroid/content/Intent;

    const-class v1, Lcom/zj/wuaipojie/ui/MainActivity;

    invoke-direct {v0, p0, v1}, Landroid/content/Intent;-><init>(Landroid/content/Context;Ljava/lang/Class;)V

    invoke-virtual {p0, v0}, Landroid/content/Context;->startActivity(Landroid/content/Intent;)V

    return-void

.end method

.method public onCreateOptionsMenu(Landroid/view/Menu;)Z

    .registers 3

    invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onCreateOptionsMenu(Landroid/view/Menu;)Z

    const/4 v0, 0x1

    return v0

.end method

.method public onOptionsItemSelected(Landroid/view/MenuItem;)Z

    .registers 3

    invoke-super {p0, p1}, Landroidx/appcompat/app/AppCompatActivity;->onOptionsItemSelected(Landroid/view/MenuItem;)Z

    const/4 v0, 0x1

    return v0

.end method

可能有的同学看起smali代码看起来比较累,那下面是该activity转成Java后的代码:

import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;

import androidx.appcompat.app.AppCompatActivity;

public class MyActivity extends AppCompatActivity {

    private Handler handler;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);
    
        handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Intent intent = new Intent(MyActivity.this, MainActivity.class);
                startActivity(intent);
                finish();
            }
        }, 5000); // 5秒后跳转
    }

}

布局文件:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

<?xml version="1.0" encoding="utf-8"?>

<!-- 声明 XML 文件版本和编码方式。-->

<RelativeLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    android:layout_width="match_parent"

    android:layout_height="match_parent">

    <!-- RelativeLayout 布局,定义根布局。-->

    <TextView

        android:id="@+id/text_view"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:layout_centerInParent="true"

        android:text="Hello World!"

        android:textSize="24sp"/>

    <!-- TextView 组件,显示文本。-->

</RelativeLayout>

<!-- RelativeLayout 布局结束。-->

第二步:将Activity和布局文件放到反编译得到的文件夹中去,并定义好对应的资源ID。

找Activity文件的位置我是使用MT管理器去查找一个Activity文件,得到该文件的路径,然后我就顺着路径找了过去,就这样我将我写的Activity放到了里头:

放好了Activity文件,那布局文件放在哪里呢?之前就提到了res/layout文件夹就包含了应用程序中所有的布局文件,那我们直接将布局文件丢res/layout文件夹里就行了,但到res文件夹中我看到了别的东西:

 

可以看到上面有好多layout-xx的文件夹,还记得这是什么吗?layout-xx是Android应用程序中用于支持不同屏幕方向和多语言的布局文件目录,比如:layout-land:用于存放横屏方向下的布局文件,当设备旋转为横屏时,系统会自动加载该目录下的布局文件。

那我们直接将布局文件丢到layout文件夹中去:

 

我们添加了一个布局文件,之前说过每一个资源文件都有对应的ID值,我们可以通过这些ID值来访问和使用对应的资源文件。那我们需要去给我们这个布局文件添加资源ID,这怎么搞呢?别急,还记得之前说过R类和res的子文件夹之间的关系吗?我们在res/layout文件夹中添加了布局文件,那么我们需要到R$layout文件中去添加这个布局文件的资源ID:

 

能看出来规律吗?其实在最底下添加一个资源ID,而这个资源ID的值就是上一个加一,添加完后一定要去res/values\public.xml文件中去确认一下资源ID是否具有唯一性,我这里添加的资源ID并不具备唯一性,后面在values\public.xml文件中添加资源ID时才发现。而且要注意资源ID是具有固定的格式:0xPPTTEEEE。

PP:表示某一个资源ID所在包的Package ID,包的命名空间,在资源ID中占两位大小,取值范围为[01, 7f],第三方应用均为7f。

TT:表示某一个资源ID的资源类型,资源ID有anim、attr、color、dimen、drawable、id、layout等资源类型。

EEEE:表示某一个资源ID在所属资源类型中的偏移量。

我们添加资源ID时要注意所添加的资源ID是否具有唯一性以及是否符合资源ID的固定格式。

我们继续讲,这里有个情况,当时我看到R$layout中记录的资源ID与res/layout中的布局文件数量不对等,这是因为在某些情况下,反编译后可能会出现多个R类。

在开发者构建相关应用模块时,库模块会先编译到 AAR 文件中,然后再添加到应用模块中。因此,每个库都有自己的 R 类,并根据库的软件包名称命名。所需的所有软件包中都会创建从主模块和库模块生成的 R 类,包括主模块的软件包和库的软件包。

或许有的人不清楚什么是库模块,下面就讲一讲什么是库模块:

Android 库的结构与 Android 应用模块的结构相同。它为构建应用提供了所需的一切,包括源代码、资源文件和 Android 清单。

不过,Android 库将编译为开发者可以用作 Android 应用模块依赖项的 Android ARchive (AAR) 文件,而不是编译为在设备上运行的 APK。与 JAR 文件不同,AAR 文件会为 Android 应用提供以下功能:

  • AAR 文件可以包含多项 Android 资源和一个清单文件,让开发者除了能够在 Kotlin 或 Java 类和方法中进行捆绑以外,还能够在布局和可绘制对象等共享资源中进行捆绑。
  • AAR 文件可以包含 C/C++ 库,供应用模块的 C/C++ 代码使用。

库模块在以下情况下非常有用:

  • 构建使用某些相同组件(例如 activity、服务或 UI 布局)的多个应用
  • 构建存在于多个 APK 变体中的应用,例如使用相同核心组件的免费版本和付费版本

无论在哪种情况下,开发者都只需将想要重复使用的文件移到库模块中,然后将库添加为每个应用模块的依赖项。

多个库模块的资源与相关应用模块的资源进行合并难免会出现资源合并冲突的情况,为了避免资源合并冲突的情况,安卓做了以下方案:1、向 Android 应用模块添加对库模块的引用后,开发者可以设置它们的相对优先级。在构建时,库会按照优先级由低到高的顺序逐一与应用合并。

2、如果库模块和应用模块中都定义了给定的资源 ID,系统会使用应用模块中的资源。如果多个 AAR 库之间发生冲突,系统会使用依赖项列表中优先列出的库(最靠近 dependencies 块顶部)中的资源。

3、为避免资源冲突,也可能会使用非传递R类,这样做有助于确保每个模块的 R 类仅包含对其自身资源的引用,而不会从其依赖项中提取引用,从而帮助防止资源重复。如果无法做到这一点,开发者也会考虑使用模块独有(或在所有项目模块中具有唯一性)的前缀或其他一致的命名方案。

但还是要注意一个APK文件中只会有一个R类,虽说每个库模块都会生成一个对应的R类,用于引用该模块中的资源。但是这些R类会在编译后合并成一个R类,最终打包到APK文件中。这个合并的R类包含了所有模块中的资源ID,可以在应用程序的代码中引用这些资源。

现在了解完了库模块,那我们继续讲,现在已经定义好了资源ID了吗?还没有,除了这里,我们还需要在一处地方定义我们布局文件的资源ID,那就是values\public.xml文件中。

values\public.xml是一个用于指定资源ID的XML文件。该文件中包含了一系列的<public>标签,每个标签定义了一个资源的名称、类型和ID。这些资源可以是应用程序内部使用的资源,也可以是外部库或框架中定义的资源。

反编译后values和values-xx文件夹会在res文件夹下,实则values这些文件以及文件夹就是未反编译时的resources.arsc文件,这里应该是apktool反编译之后把它们放在了一起。

但我去那一看,发现一个问题:values\public.xml文件中定义的资源ID数量比R$layout中定义的资源ID多得多。

 

看这密密麻麻类型为layout的<public>,还有好多类型为layout的<public>没法截下来,可见数量之多,但这不应该呀!R$layout中定义的资源ID数量应该与public.xml文件中定义的资源ID数量一致呀!我找了会,我找到了这些:

 

我发现除了com.zj.wuaipojie下的R类,在com下的其他包里也有R类,那么根据前面所讲的库模块,public.xml中定义的资源ID数量是这些R类定义的资源ID数量之和。

那么我们就先去public.xml文件给我们写的布局文件定义资源ID

 

修改完成,我们就完成第二步了!我们的布局文件已经放好并且定义好了资源ID,但是我们的Activity只放到了反编译文件夹中却没有去声明它,那我们下一步就是去声明Activity并设为默认启动。

第三步:声明Activity并设为默认启动

我们来看一下配置信息:

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

<!-- <?xml version="1.0" encoding="utf-8" standalone="no"?> 表示这个XML文件使用的XML版本是1.0,编码格式为UTF-8,依赖于外部文档 -->

<?xml version="1.0" encoding="utf-8" standalone="no"?>

<!-- 定义应用程序的清单文件 -->

<!-- <manifest> 标签是清单文件的根标签,xmlns:android="http://schemas.android.com/apk/res/android"它定义了Android命名空间,命名空间用于标识 Android 框架定义的元素和属性,以及应用程序定义的元素和属性。而"http://schemas.android.com/apk/res/android"这个URL将作为Android命名空间用于存储XML元素和属性,并通过命名空间绑定和前缀来简化代码。如果不知道是什么意思,那就理解为这是一个导入库,当使用 Android 标签时,需要在标签前加上 android: 前缀。 -->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    android:compileSdkVersion="32"

    android:compileSdkVersionCodename="12"

    package="com.zj.wuaipojie"

    platformBuildVersionCode="32"

    platformBuildVersionName="12">

    <!-- 定义应用程序的权限 -->

    <uses-permission android:name="android.permission.INTERNET"/>

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    <!-- 定义应用程序的组件 -->

    <application

        android:allowBackup="true"

        android:appComponentFactory="androidx.core.app.CoreComponentFactory"

        android:dataExtractionRules="@xml/data_extraction_rules"

        android:extractNativeLibs="false"

        android:fullBackupContent="@xml/backup_rules"

        android:icon="@mipmap/ic_launcher"

        android:label="@string/app_name"

        android:networkSecurityConfig="@xml/network_config"

        android:supportsRtl="true"

        android:theme="@style/Theme.Wuaipojie">

        <!-- 定义应用程序的活动 -->

        <activity android:exported="true" android:name="com.zj.wuaipojie.ui.ChallengeFifth"/>

        <activity android:exported="true" android:name="com.zj.wuaipojie.ui.ChallengeFourth"/>

        <activity android:exported="false" android:name="com.zj.wuaipojie.ui.ChallengeThird"/>

        <activity android:exported="false" android:name="com.zj.wuaipojie.ui.ChallengeSecond"/>

        <activity android:name="com.zj.wuaipojie.ui.AdActivity"/>

        <activity android:exported="true" android:label="@string/app_name" android:name="com.zj.wuaipojie.ui.MainActivity">

            <intent-filter>

                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>

            </intent-filter>

        </activity>

        <activity android:name="com.zj.wuaipojie.ui.ChallengeFirst"/>

    </application>

</manifest>

我们看上面的代码,我们需要去声明我们的Activity并设为默认启动,那就需要将com.zj.wuaipojie.ui.MainActivity替换成我们的Activity,但这样会出现问题的,在《安卓逆向这档事》四、恭喜你获得广告&弹窗静默卡中提到过,一般在启动Activity的时候会预先加载一些数据,这样修改有很大概率会因为某些数据没有加载上从而导致应用出现闪退或者其他奇奇怪怪的问题。

然后我想使用这个方法——Acitivity切换定位,修改Intent的Activity类名,可是我看到MainActivity没有人调用它,暂时只能是另寻他法了。

现在我能想到的方法就是将我们自己的Activity代替MainActivity的位置,再为MainActivity重新进行声明,我想或许正己大佬讲的预加载数据未加载完全的问题是因为将之后启动的Activity作为主Activity进行默认启动,这样有些之后才会加载的数据在启动时因为未被加载,而之后启动的Activity却先加载了,这样就导致有些数据没有被加载从而出现奇奇怪怪的问题。而我只是将我自己写的Activity进行默认启动,这样貌似没有数据加载不全一说呢!只不过我能力有限,也不知道我的猜想是否正确,但现在别无他法,只能硬着头皮试试看了。

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

<?xml version="1.0" encoding="utf-8" standalone="no"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

    android:compileSdkVersion="32"

    android:compileSdkVersionCodename="12"

    package="com.zj.wuaipojie"

    platformBuildVersionCode="32"

    platformBuildVersionName="12">

    <uses-permission android:name="android.permission.INTERNET" />

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application

        android:allowBackup="true"

        android:appComponentFactory="androidx.core.app.CoreComponentFactory"

        android:dataExtractionRules="@xml/data_extraction_rules"

        android:extractNativeLibs="false"

        android:fullBackupContent="@xml/backup_rules"

        android:icon="@mipmap/ic_launcher"

        android:label="@string/app_name"

        android:networkSecurityConfig="@xml/network_config"

        android:supportsRtl="true"

        android:theme="@style/Theme.Wuaipojie">

        <activity android:exported="true" android:name="com.zj.wuaipojie.ui.ChallengeFifth" />

        <activity android:exported="true" android:name="com.zj.wuaipojie.ui.ChallengeFourth" />

        <activity android:exported="false" android:name="com.zj.wuaipojie.ui.ChallengeThird" />

        <activity android:exported="false" android:name="com.zj.wuaipojie.ui.ChallengeSecond" />

        <activity android:name="com.zj.wuaipojie.ui.AdActivity" />

        <activity

            android:exported="true"

            android:label="@string/app_name"

            android:name="com.zj.wuaipojie.ui.MainActivity" />

        <activity android:exported="true" android:name="com.zj.wuaipojie.ui.MyActivity">

            <intent-filter>

                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />

            </intent-filter>

        </activity>

        <activity android:name="com.zj.wuaipojie.ui.ChallengeFirst" />

    </application>

</manifest>

这是经过我修改后的配置文件,为什么要这么改呢?我来讲一下主activity:

1

2

3

4

5

6

<activity android:exported="true" android:name="com.zj.wuaipojie.ui.MyActivity">

    <intent-filter>

        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />

    </intent-filter>

</activity>

  • android:exported="true":指定该 Activity 是否可以被其他应用程序调用。如果设置为 true,则表示该 Activity 可以被其他应用程序调用;如果设置为 false,则表示该 Activity 只能被当前应用程序调用。
  • android:name="com.zj.wuaipojie.ui.MyActivity":指定该 Activity 的完整类名。
  • action android:name="android.intent.action.MAIN":表示activity作为一个什么动作启动,android.intent.action.MAIN表示作为主activity启动
  • category android:name="android.intent.category.LAUNCHER":表示这个activity为当前应用程序优先级最高的Activity

现在对主Activity有了解了,但是你现在应该还有一个问题,那就是为什么将自己的Activity代替掉原先的主Activity,而原先的主Activity不需要重新进行声明呢?

这是因为你将自己写的 Activity 设为主 Activity 启动且优先级最高,那么你只是将自己的 Activity 替换了 MainActivity 在 AndroidManifest.xml 文件中的位置。这样,当应用程序启动时,系统将启动 MyActivity,而不是 MainActivity,但是MainActivity仍然存在于 AndroidManifest.xml 文件中,可以在应用程序中使用。

第四步:用apktool的打包功能重新打包反编译的apk文件

在需要重新打包反编译的apk文件所处的文件夹中打开powershell并且输入cmd;

输入命令:apktool.bat b 需要打包的文件夹 -o 重新打包反编译的apk文件存放的路径

我输入命令后出现了一个报错:

我去查了一下,这个错误提示是由 APKTOOL 处理 APK 时出现的,提示资源目录名无效。具体原因可能是因为你的资源目录名包含了非法字符,比如空格或其他特殊字符,导致 APKTOOL 无法处理它们。

这个错误信息中显示了res文件夹下的一个资源目录名为“navigation”,可能是导致错误的原因之一,我们去看看该文件夹:

但是这个文件没法删除,因为R类中有子类对应着这个资源文件夹,我不管怎么改文件名字都出了问题,我只能另寻他法,我查到除了资源文件夹名称无效还有可能是由以下原因导致的:

  1. 目录不存在:请检查命令中指定的资源目录路径是否正确,如果路径不正确请修改为正确的路径。如果路径正确但目录不存在,请创建该目录。
  2. 目录为空:请检查命令中指定的资源目录中是否包含有效的资源文件。如果目录为空,请将需要的资源文件放入该目录。
  3. 权限问题:请确保您具有足够的权限访问命令中指定的目录。如果您不确定是否具有足够的权限,请使用管理员权限运行命令。
  4. 其他原因:
  • 重新安装或更新apktool:如果您的apktool版本过旧或者出现了其他问题,可以尝试重新安装或更新apktool。
  • 使用其他工具进行反编译和回编译:如果apktool无法解决问题,您可以尝试使用其他工具进行反编译和回编译,例如dex2jar和jd-gui等。
  • 使用MT管理器进行打包时是没有任何问题的,如果不是我的操作问题那就应该大概是apktool回编译执行的方法流程和需要回编译的应用程序的代码之间发生了某种冲突导致出现此问题,想要解决这种情况引发的问题需要通过修改代码来解决这个问题。

我对以上原因进行检查,排除掉了第一、第二、第三和apktool版本过旧这些原因,现在只剩下了两种情况,一是需要使用其他工具,二是apktool和需要回编译的应用程序的代码之间发生了某种冲突,这两种情况我更倾向于第二种。

我已然无计可施,只能向大佬们求教,经过大佬指点后得知是因为resources.arsc 文件的“complex”条目中有重复资源项。

在解决之前需要去了解一下resource.arsc的Config List块,我们可以阅读Android逆向:resource.arsc文件解析(Config List) - 掘金 (juejin.cn)来获取知识,但这篇文章可能对一些新手来说读起来有点点费劲,我下面对这篇文章做点帮助新手更加易懂的补充,当然,如果能把resources的文件结构和打包流程、资源管理及资源的编译这些内容搞懂那是最好的了:

其实这篇文章只讲了三部分,分别是ResTable_type、ResTable_entry偏移数组和若干ResTable_entry:

上图红色框框所框住的就是ResTable_type,蓝色框框所框住的就是ResTable_entry偏移数组,而黄色框框所框住的就是若干ResTable_entry了。

要注意的是ResTable_entry有两种类型:bag和非bag,这样两者的结构也就不同了。

非bag类型的ResTable_entry结构:

按照作者所给出的例子来看:08000000 87020000 08000001 2E00087F,前八个字节是ResTable_entry,后八个字节是Res_value。

ResTable_entry:

0800是ResTable_entry资源项大小,占2字节;

0000是flag,0x00表示非bag类型ResTable_entry,0x01表示bag类型ResTable_entry;

87020000是资源项名称在资源项名称字符串资源池的索引(资源项名称Index)。

Res_value:

0800是Res_value头部大小;

00是保留字段(一字节);

01是Res_value的数据类型,01表示TYPE_REFERENCE,即该ResTable_entry资源项被引用;

2E00087F是Res_value的实际数据,这里是个资源ID,即0x7F08002E。

bag类型的ResTable_entry结构(这里的数据块是ResTable_map_entry,它继承自ResTable_entry):

ResTable_map整体结构(把Res_value结构给分解到ResTable_map里面来了):

这里也用按照作者给出的例子来看:10000100 EA020000 C700097F 03000000 F300017F 08000005 01180000 F400017F

08000005 01030000 F700017F 08000005 01120000,前十六个字节就是bag类型的ResTable_entry结构,后面则是ResTable_map整体结构。

前十六个字节:

1000是ResTable_entry资源项大小,占2字节;

0100 是flag,0x00表示非bag类型ResTable_entry,0x01表示bag类型ResTable_entry;

EA020000是资源项名称在资源项名称字符串资源池的索引(资源项名称Index);

C700097F是数据块ResTable_map_entry下的parent,parent变量指向ResTable_map_entry的父级的资源ID。如果当前的ResTable_map_entry没有ResTable_map_entry的父级,即它就是顶级映射项,那么parent变量的值将为0;例子这里C700097F是parent变量的值,由于这个例子是在描述一个style的情况,所以C700097F表示这个style的父级style的资源ID,即描述一个style的父级style的资源ID是0x7F0900C7;

03000000是数据块ResTable_map_entry下的count,记录下面ResTable_map的数量,即0x03。

后面的字节:

根据count得知有三个ResTable_map结构数据,分别为:

第一个ResTable_map结构数据——F300017F 08000005 01180000

第二个ResTable_map结构数据——F400017F 08000005 01030000

第三个ResTable_map结构数据——F700017F 08000005 01120000

这里只讲第一个ResTable_map结构数据:

F300017F是bag的资源项ID,即0x7F0100F3;

0800是Res_value头部大小;

00是保留字段(一字节);

05是Res_value的数据类型,05表示TYPE_DIMENSION,即该ResTable_entry资源项的值是一个尺寸值,通常以像素(px)或设备无关像素(dp)为单位;

01180000是Res_value的实际数据,01代表dp。后面的是数值,考虑字序是0x18,即24,所以应该是24dp。

该篇文章的补充我已经做完了,新手可以结合着去看,阅读效果会好上不少。

我们现在继续尝试解决这个问题,先来看看在什么情况下会报这个错误:

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

private void decodeManifestWithResources(ResTable resTable, ExtFile apkFile, File outDir)

        throws AndrolibException {

        Duo<ResFileDecoder, AXmlResourceParser> duo = getManifestFileDecoder(true);

        ResFileDecoder fileDecoder = duo.m1;

        ResAttrDecoder attrDecoder = duo.m2.getAttrDecoder();

        attrDecoder.setResTable(resTable);

     

        Directory inApk, out;

        try {

            inApk = apkFile.getDirectory();

            out = new FileDirectory(outDir);

            LOGGER.info("Decoding AndroidManifest.xml with resources...");

     

            fileDecoder.decodeManifest(inApk, "AndroidManifest.xml", out, "AndroidManifest.xml");

     

            // Remove versionName / versionCode (aapt API 16)

            if (!mConfig.analysisMode) {

     

                // check for a mismatch between resources.arsc package and the package listed in AndroidManifest

                // also remove the android::versionCode / versionName from manifest for rebuild

                // this is a required change to prevent aapt warning about conflicting versions

                // it will be passed as a parameter to aapt like "--min-sdk-version" via apktool.yml

                adjustPackageManifest(resTable, outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml");

     

                ResXmlPatcher.removeManifestVersions(new File(

                    outDir.getAbsolutePath() + File.separator + "AndroidManifest.xml"));

            }

        } catch (DirectoryException ex) {

            throw new AndrolibException(ex);

        }

    }

以上代码在以下情况下可能会抛出异常:

  1. 在获取APK文件目录或输出目录时发生错误,例如文件未找到或无法读取目录等。

  2. 在解码AndroidManifest.xml文件时发生错误,例如文件损坏或无效的XML格式。

  3. 在调用adjustPackageManifest()方法时,如果资源包(resources.arsc)中的包名与AndroidManifest.xml中声明的包名不匹配,则可能会抛出异常。

  4. 在调用ResXmlPatcher.removeManifestVersions()方法时,如果无法删除AndroidManifest.xml中的版本号(versionCode / versionName)元素,则可能会抛出异常。

  5. 在处理DirectoryException异常时,可能会抛出AndrolibException异常。

并且为了确认是不是我的操作导致的resources.arsc 文件的“complex”条目中有重复资源项,我特意将apk文件使用apktool反编译后不做任何修改直接回编译,结果如下:

根据以上结果,现在我们能确认并不是我的修改操作导致的resources.arsc 文件的“complex”条目中有重复资源项,而是apktool回编译该文件时就会出现这个问题。

如果有感兴趣的大佬可以自行研究一下,当然,大家也可以共同讨论研究这个问题,以下是大佬所提供的与该问题相关的资料:

更好地支持资源复杂条目中的重复条目.arsc 文件 ·问题 #3098 ·iBotPeaches/Apktool (github.com)

Code search results (github.com)

Apktool/brut.apktool/apktool-lib/src/main/java/brut/androlib/res/ResourcesDecoder.java at 2b8121584624f0a1b1602992a48fcdbf60c0347d · iBotPeaches/Apktool (github.com)

第五步:重新签名

如果反编译的apk文件没有签名校验的话,可以直接将反编译的apk文件打包好安装在模拟器上,然后再使用MT管理器/NP管理器进行apk签名,这样简单又省事。

重新签名原理简述:

当需要对已经签名的apk文件进行重新签名时,会先解压需要重新签名的apk文件,再将原先的签名文件删除,然后可以通过使用Java的keytool工具生成证书和私钥,并使用Jarsigner工具将其应用于apk文件。还有一点需要注意,那就是重新签名后的apk文件可能会失去原先签名文件的一些权限,如无法更新原先的应用、无法与其他应用进行交互等。因此,建议在进行重新签名前,先备份原先的签名文件,以便在需要时进行恢复。

classes.dex文件

classes.dex文件是Android应用程序的核心组件之一,它是应用程序的可执行文件,包含了应用程序的所有Java类和方法。它是一个被编译过的DEX(Dalvik Executable)文件,是Dalvik虚拟机的格式,用于在Android设备上运行Java应用程序。

在Android开发中,Java源码需要通过Android SDK提供的工具链进行编译,并且会被转换成Dalvik虚拟机可以执行的DEX格式,生成classes.dex文件。这个文件包含了应用程序的所有Java类和方法,可以在Android设备上运行。

Dalvik虚拟机是Google专门为Android操作系统设计的一个虚拟机,与标准的Java虚拟机JVM有一些区别。Dalvik虚拟机是基于寄存器的,而JVM是基于栈的。这种设计使得Dalvik虚拟机能够更好地适应移动设备的资源限制,提供更高效的执行性能。

然而,自Android 5.0(Lollipop)起,Android引入了ART(Android Runtime)作为新的运行时环境,取代了之前的Dalvik虚拟机。在ART环境下,应用程序的Java字节码文件仍然会被编译成DEX格式的可执行文件,即classes.dex文件。

当用户安装应用程序时,系统会将APK文件解压,并将其中的classes.dex文件加载到ART虚拟机中,以执行应用程序的Java代码。classes.dex文件中的Java代码实现了APK的主要逻辑,包括各种功能和业务逻辑的实现。

总结来说,classes.dex文件是Android应用程序的核心组件之一,包含了应用程序的所有Java类和方法。它是一个被编译过的DEX文件,用于在ART虚拟机上执行Java代码。通过加载和执行classes.dex文件,Android应用程序能够在Android设备上运行并提供各种功能。

到这里apk的结构总算讲完了,我们现在讲讲额外的知识点——十六进制状态下的apk文件

为什么要讲这个呢?因为我闲逛时看到了这篇帖子——[原创]基于APK文件格式的反编译对抗机制-Android安全-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com),我觉得甚是有趣,就想借着讲apk文件结构顺便把apk文件格式讲了。

编译后生成的apk文件实质上就是一个.zip的压缩包,是可以直接通过解压缩工具打开和解压,我们先来详细了解一下ZIP文件结构。

ZIP文件结构

ZIP格式压缩包主要由三个部分组成:数据区、中央目录记录区和中央目录记录尾部区。

ZIP文件格式通过中央目录记录尾部区来获取到中央目录区的相对偏移地址。每个中央目录区记录文件的相关信息,包括文件名、文件的压缩方法、未压缩大小、压缩后大小、CRC校验值等等,并记录了数据区的相对偏移地址和大小。数据区中的每个文件被分为一个或多个压缩块,每个压缩块都有本地文件头、文件数据和数据描述段三个部分。本地文件头记录了该文件的一些元数据信息(对数据的描述信息),如是否加密、压缩方法等,文件数据是经过压缩处理的实际数据,数据描述段部分记录的则是该块的大小和CRC校验值等信息。

看不懂不要紧,这就进行讲解:

中央目录记录尾部区

中央目录记录尾部区主要作用是获取到中央目录区的相对偏移地址。

 

上图是中央目录记录尾部区的字段信息,每个字段的详细解释如下:

  1. 签名(4字节):标识该区域的开始,固定值为0x06054b50。
  2. 磁盘编号(2字节):指定中央目录记录所在的磁盘编号,如果ZIP文件只有一个磁盘,则该值为0。
  3. 中央目录记录起始磁盘编号(2字节):指定中央目录记录的起始磁盘编号,如果ZIP文件只有一个磁盘,则该值为0。
  4. 中央目录记录数量(2字节):指定ZIP文件中的中央目录记录数量。
  5. 中央目录记录总数(2字节):指定ZIP文件中的中央目录记录总数。
  6. 中央目录大小(4字节):指定ZIP文件中的中央目录大小。
  7. 中央目录偏移量(4字节):指定ZIP文件中的中央目录相对于ZIP文件起始位置的偏移量。
  8. 注释长度(2字节):指定ZIP文件的注释长度,如果没有注释,则该值为0。
  9. 注释(可变长度):ZIP文件的注释内容,长度由注释长度字段指定。

 

上图中所划的区域就是该ZIP文件的中央目录记录尾部区,我们可以得到中央目录记录区的相对偏移地址为03221000,中央目录记录区的总大小为00028097,中央目录数量为085F。得到了这些信息,我们这就去中央目录记录区一探究竟。

中央目录记录区

中央目录记录区是ZIP文件格式中的一个区域,用于存储ZIP文件中的每个被压缩文件的元数据信息。

 

上图是中央目录记录区的字段信息,每个字段的详细解释如下:

  1. 签名(4字节):标识该记录的开始,固定值为0x02014b50。
  2. 版本号(2字节):指定ZIP规范的版本号。
  3. 版本需要(2字节):指定进行解压缩所需的最低版本号。
  4. 通用标志位(2字节):指定一些通用属性,如是否加密、是否压缩、是否有数据描述等。
  5. 压缩方法(2字节):指定文件的压缩方法,如存储、缩小、最大压缩等。
  6. 文件修改时间(2字节):指定文件的最后修改时间。
  7. 文件修改日期(2字节):指定文件的最后修改日期。
  8. CRC-32校验值(4字节):指定文件的CRC-32校验值,用于验证文件的完整性。
  9. 压缩前大小(4字节):指定文件压缩前的大小,即未压缩时的大小。
  10. 压缩后大小(4字节):指定文件压缩后的大小,即压缩后的大小。
  11. 文件名长度(2字节):指定文件名的长度。
  12. 扩展字段长度(2字节):指定扩展字段的长度。
  13. 文件注释长度(2字节):指定文件的注释长度。
  14. 磁盘编号开始(2字节):指定该文件的起始磁盘编号。
  15. 内部文件属性(2字节):指定该文件的内部属性。
  16. 外部文件属性(4字节):指定该文件的外部属性。
  17. 相对偏移量(4字节):指定该文件在ZIP文件中的相对偏移量。
  18. 文件名(可变长度):指定文件名,长度由文件名长度字段指定。
  19. 额外字段 (可变长度): 记录额外的元数据信息,例如文件的注释、扩展属性等。

这里再注重解释一下通用标志位字段中数据的含义:

  1. 第0位:如果该位被设置为1,则说明该文件被加密。
  2. 第1位:如果该位被设置为1,则说明该文件使用了强制压缩算法。
  3. 第2位:如果该位被设置为1,则说明该文件使用了一种被废弃的压缩算法。
  4. 第3位:如果该位被设置为1,则说明该文件有一个数据描述段。
  5. 第4位:保留位。
  6. 第5位:保留位。
  7. 第6位:如果该位被设置为1,则说明该文件是由一个跨磁盘的文件组成的。
  8. 第7位:如果该位被设置为1,则说明该文件是由一个压缩的数据流组成的。
  9. 第8位:如果该位被设置为1,则说明该文件的压缩使用了压缩算法以外的其他技术来提高压缩效率。
  10. 第9位:如果该位被设置为1,则说明该文件的压缩使用了压缩算法技术来提高压缩效率。

 

上图是中央目录记录区的字段信息,每个字段的详细解释如下:

  1. 签名(4字节):标识该记录的开始,固定值为0x02014b50。
  2. 版本号(2字节):指定ZIP规范的版本号。
  3. 版本需要(2字节):指定进行解压缩所需的最低版本号。
  4. 通用标志位(2字节):指定一些通用属性,如是否加密、是否压缩、是否有数据描述等。
  5. 压缩方法(2字节):指定文件的压缩方法,如存储、缩小、最大压缩等。
  6. 文件修改时间(2字节):指定文件的最后修改时间。
  7. 文件修改日期(2字节):指定文件的最后修改日期。
  8. CRC-32校验值(4字节):指定文件的CRC-32校验值,用于验证文件的完整性。
  9. 压缩前大小(4字节):指定文件压缩前的大小,即未压缩时的大小。
  10. 压缩后大小(4字节):指定文件压缩后的大小,即压缩后的大小。
  11. 文件名长度(2字节):指定文件名的长度。
  12. 扩展字段长度(2字节):指定扩展字段的长度。
  13. 文件注释长度(2字节):指定文件的注释长度。
  14. 磁盘编号开始(2字节):指定该文件的起始磁盘编号。
  15. 内部文件属性(2字节):指定该文件的内部属性。
  16. 外部文件属性(4字节):指定该文件的外部属性。
  17. 相对偏移量(4字节):指定该文件在ZIP文件中的相对偏移量。
  18. 文件名(可变长度):指定文件名,长度由文件名长度字段指定。
  19. 额外字段 (可变长度): 记录额外的元数据信息,例如文件的注释、扩展属性等。

这里再注重解释一下通用标志位字段中数据的含义:

  1. 第0位:如果该位被设置为1,则说明该文件被加密。
  2. 第1位:如果该位被设置为1,则说明该文件使用了强制压缩算法。
  3. 第2位:如果该位被设置为1,则说明该文件使用了一种被废弃的压缩算法。
  4. 第3位:如果该位被设置为1,则说明该文件有一个数据描述段。
  5. 第4位:保留位。
  6. 第5位:保留位。
  7. 第6位:如果该位被设置为1,则说明该文件是由一个跨磁盘的文件组成的。
  8. 第7位:如果该位被设置为1,则说明该文件是由一个压缩的数据流组成的。
  9. 第8位:如果该位被设置为1,则说明该文件的压缩使用了压缩算法以外的其他技术来提高压缩效率。
  10. 第9位:如果该位被设置为1,则说明该文件的压缩使用了压缩算法技术来提高压缩效率。

 

上图所划区域为中央目录记录区中的第一个中央目录,可以看到它的起始位置为03221000,从中央目录记录尾部区中获取到中央目录记录区的相对偏移位置,跳转到这,这里由一个一个的中央记录目录区组成,每个中央目录记录区记录着一个文件的相关信息,我们拿着上面所说的字段去对应上图所划区域可以得到很多有用信息,但现在要去看看数据区,那就需要获取到中央目录记录区对应文件的相对偏移量,值为00000000,中央目录记录区中记录的文件数据大小通常是看压缩后的大小,该值为3D7C。

数据区

数据区是zip格式压缩包最重要的部分,它存储了被压缩的文件内容。在zip格式压缩包中,每个被压缩的文件都会被分成多个小块,并分别存储在数据区中,这些小块被称为压缩块(compressed block)。每个压缩块都有一个头部,用于标识该块的大小、压缩方式以及解压后的大小等信息。

本地文件头

 

下面是本地文件头每个字段的详解:

  1. 本地文件头标识 (4字节): 记录数据区本地文件头的标识符,固定为 0x04034B50。
  2. 解压所需的最低版本号 (2字节): 记录解压该文件所需的最低版本号。如果解压工具版本低于该值,则无法解压该文件。
  3. 通用标志位 (2字节): 用于记录一些标志位,例如是否加密、是否压缩、是否有数据描述符等。
  4. 压缩算法 (2字节): 记录文件压缩使用的算法,例如Deflate、LZMA、BZIP2等。
  5. 文件最后修改时间 (2字节): 记录文件的最后修改时间,以 MS-DOS 格式存储。
  6. 文件最后修改日期 (2字节): 记录文件的最后修改日期,以 MS-DOS 格式存储。
  7. CRC-32 校验值 (4字节): 记录文件内容的 CRC 校验值,用于验证文件的完整性。
  8. 压缩后的文件大小 (4字节): 记录文件压缩后的大小,以字节为单位。
  9. 未压缩的文件大小 (4字节): 记录文件未压缩时的大小,以字节为单位。
  10. 文件名长度 (2字节): 记录文件名的长度,以字节为单位。
  11. 额外字段长度 (2字节): 记录额外字段的长度,以字节为单位。
  12. 文件名 (可变长度): 记录文件名,以UTF-8编码格式存储。
  13. 额外字段 (可变长度): 记录额外的元数据信息,例如文件的注释、扩展属性等。

 

 

本地文件头记录了该文件的基本属性和元数据信息。

文件数据

文件数据紧跟在本地文件头之后,文件数据是被压缩的数据,它们存储在Compressed Data中。Compressed Data的大小是根据文件的实际情况动态计算的,因此文件数据的大小是不固定的。在解压缩时,解压工具会读取Compressed Data中的数据,根据压缩算法和数据进行解压缩,从而还原出原始文件的数据。

数据描述段

ZIP文件中的数据描述段是可选的,仅在通用标志位的第 3 位被设置为1时才存在,它紧跟在文件数据的最后一个字节之后。

数据描述段由三个部分组成:CRC-32校验值、压缩后的大小和压缩前的大小。其中,CRC-32校验值是对文件数据进行校验的一种方法,它可以检测文件数据是否被修改或损坏。

需要注意的是,ZIP文件的数据描述段是可选的,因此并不是所有的ZIP文件都包含数据描述段。在解压缩时,解压工具会根据数据描述段的存在与否,确定文件数据的压缩前后大小和CRC-32校验值。如果数据描述段不存在,解压工具也可以通过其他方式计算文件数据的压缩前后大小和CRC-32校验值。

看到这,你可能会好奇中央目录记录区和本地文件头都有字段记录了文件的压缩前后大小、CRC-32校验值等信息,为什么还需要数据描述段?

因为中央目录记录区和本地文件头中确实包含了文件的压缩前后大小、CRC-32校验值等信息,但这些信息并不是必须的,一些ZIP文件可能不包含数据描述段,或者数据描述段中的信息不完整或不准确。

具体来说,数据描述段用于存储文件的压缩前后大小和CRC-32校验值等信息,它通常包含在数据区和中央目录记录区中。当解压缩工具读取ZIP文件时,它会首先查找数据区中的数据描述段,如果数据区中没有数据描述段,则会查找中央目录记录区中的数据描述段。如果ZIP文件中都没有数据描述段,则解压缩工具将使用其他方法来计算文件的压缩前后大小和CRC-32校验值。

因此,中央目录记录区和本地文件头中记录文件的压缩前后大小、CRC-32校验值等信息是非常必要的,它们可以确保ZIP文件可以正确地被读取和解压缩,即使ZIP文件中不包含数据描述段也可以正常解压缩。

到这里就讲完了ZIP文件结构,我们接下来就开始实战了,下面实战中所用到的工具是WinHex。

基于ZIP文件格式的反编译对抗

通用标志位

我们先来玩玩通用标志位字段,这次使用的测试对象为Smali语法查询_1.0.0.apk文件,我们要做的就是将它修改成一个连我们自己都不知道密码的加密文件。

我们先来看一下正常的解压和安装文件:

 

 

我们通过ctrl+f将数据区和中央目录记录区所有记录的通用标志位的第零位换成了1。

数据区:

 

 

我们保存后看看效果:

解压的时候果然出现没有设置密码要我们输入密码的情况

但是安装该apk文件时是没有任何问题的

而且我使用jadx-jui反编译该文件也是无法反编译的

其他字段的玩法我这里就不演示了,如果对这个感兴趣的话可以去看原文,自己去玩一下、动动手,或许会有新的收获。

 

 

;