Last updated on May 11, 2024 pm
记录所有东拼西凑到处copy的安卓逆向知识
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:在Android应用的APK文件中,META-INF文件夹是存放数字签名相关文件的文件夹,包含以下三个文件:
MANIFEST.MF:MANIFEST.MF文件是一个摘要清单文件,它包含了apk文件中除自己以外所有文件的数字摘要。
CERT.SF:CERT.SF文件是用于存储通过私钥加密后得到的MANIFEST.MF文件的数字签名信息以及MANIFEST.MF文件中数字摘要的数字签名信息。
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的数字签名是否合法。验证的过程包括以下几个步骤:
提取APK文件中的数字证书。
从数字证书中提取公钥。
使用该公钥对APK文件中的数字签名进行解密,得到数字摘要。
对APK文件进行Hash运算,生成数字摘要。
比较步骤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文件的主要组成部分:
manifest标签
manifest标签是AndroidManifest.xml文件的根标签,它包含了应用程序的基本信息,如包名、版本号、SDK版本、应用程序的名称和图标等等。
application标签
application标签是应用程序的主要标签,它包含了应用程序的所有组件,如Activity(活动)、Service(服务)、Broadcast Receiver(广播接收器)、Content Provider(内容提供者)等等。在application标签中,也可以设置应用程序的全局属性,如主题、权限等等。
activity标签
activity标签定义了一个Activity组件,它包含了Activity的基本信息,如Activity的名称、图标、主题、启动模式等等。在activity标签中,还可以定义Activity的布局、Intent过滤器等等。
service标签
service标签定义了一个Service组件,它包含了Service的基本信息,如Service的名称、图标、启动模式等等。在service标签中,还可以定义Service的Intent过滤器等等。
receiver标签
receiver标签定义了一个BroadcastReceiver组件,它包含了BroadcastReceiver的基本信息,如BroadcastReceiver的名称、图标、权限等等。在receiver标签中,还可以定义BroadcastReceiver的Intent过滤器等等。
provider标签
provider标签定义了一个Content Provider组件,它包含了Content Provider的基本信息,如Content Provider的名称、图标、权限等等。在provider标签中,还可以定义Content Provider的URI和Mime Type等等。
uses-permission标签
uses-permission标签定义了应用程序需要的权限,如访问网络、读取SD卡等等。在应用程序安装时,系统会提示用户授权这些权限。
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/ 目录中的文件可描述多个资源。对于此目录中的文件, 元素的每个子元素均会定义一个资源。例如, 元素会创建 R.string 资源, 元素会创建 R.color 资源。由于每个资源均使用自己的 XML 元素进行定义,因此您可以随意命名文件,并在某个文件中放入不同的资源类型。但是,您可能需要将独特的资源类型放在不同的文件中,使其一目了然。例如,对于可在此目录中创建的资源,下面给出了相应的文件名约定:arrays.xml 用于资源数组(类型化数组 )colors.xml 用于颜色值 dimens.xml 用于维度值 strings.xml 用于字符串值 styles.xml 用于样式 如需了解详情,请参阅字符串资源 、样式资源 和更多资源类型 。
xml/
可在运行时通过调用 Resources.getXML() 读取的任意 XML 文件。各种 XML 配置文件(例如搜索配置 )都必须保存在此处。
font/
带有扩展名的字体文件(例如 TTF、OTF 或 TTC),或包含 元素的 XML 文件。如需详细了解以资源形式使用的字体,请参阅将字体添加为 XML 资源 。
上表的内容为安卓官方文档中所记录的内容。
上表所定义的子目录中,保存的资源为默认资源。换言之,这些资源定义应用的默认设计和内容。然而,不同类型的 Android 设备可能需要不同类型的资源。
例如,开发者可以为屏幕尺寸大于普通屏幕的设备提供不同的布局资源,以充分利用额外的屏幕空间。还可以提供不同的字符串资源,以便根据设备的语言设置翻译界面中的文本。如需为不同设备配置提供这些不同资源,除默认资源以外,开发者还需提供备用资源。这个现在可能不太明白,但后面实战会讲到。
在Android应用程序中,res文件夹中的资源文件可通过引用其资源 ID 来应用该资源。所有资源 ID 都在项目的 R 类中进行定义,该类由 aapt 工具自动生成,可以通过这些ID值来访问和使用应用程序中的资源。
那R类和res文件夹的关系是怎么样的呢?
R类与res文件夹下的资源文件之间的关系如下:
R类的包名与应用程序的包名相同,即com.example.myapp。
R类中的子类与res文件夹下的子文件夹相对应,如Rdrawable对应drawable文件夹,R layout对应layout文件夹(前面的类名表示子类,后面的类名表示父类)。
R类中的每个子类都包含了对应资源文件的ID值,如R$drawable中包含了所有drawable文件夹下的图片的ID值。
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去引用的,而是通过名称等其他方式进行的引用。
壳与脱壳 类的加载 类加载器 JVM的类加载器包括3种:
1)Bootstrap ClassLoader(引导类加载器)
C/C++代码实现的加载器,用于加载指定的JDK的核心类库,比如java.lang.、java.uti.等这些系统类。Java虚拟机的启动就是通过Bootstrap ,该Classloader在java里无法获取,负责加载/lib下的类。
2)Extensions ClassLoader(拓展类加载器)
Java中的实现类为ExtClassLoader,提供了除了系统类之外的额外功能,可以在java里获取,负责加载/lib/ext下的类。
3)Application ClassLoader(应用程序类加载器)
Java中的实现类为AppClassLoader,是与我们接触对多的类加载器,开发人员写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。
也可以通过继承java.lang.ClassLoader
的方式来实现自己的类加载器即可,
双亲委派 这几种加载器的加载顺序如下
Bootstrap CLassloder
Extention ClassLoader
AppClassLoader
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都不愿意干活,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这个就是双亲委派。
为什么要有双亲委派?
1)避免重复加载,如果已经加载过一次Class,可以直接读取已经加载的Class
2)更加安全,无法自定义类来替代系统的类,可以防止核心API库被随意篡改
加载时机 1、隐式加载:
创建类的实例
访问类的静态变量,或者为静态变量赋值
调用类的静态方法
使用反射方式来强制创建某个类或接口对应的java.lang.Class对象初始化某个类的子类
2、显示加载:
两者又有所区别使用LoadClass()加载
使用forName()加载
加载步骤 1、装载:查找和导入Class文件
2、链接:其中解析步骤是可以选择的
(a)检查:检查载入的class文件数据的正确性
(b)准备:给类的静态变量分配存储空间
(c)解析:将符号引用转成直接引用
3、初始化:即调用函数,对静态变量,静态代码块执行初始化工作
Android类加载器
Android系统中与ClassLoader相关的一共有8个:
ClassLoader为抽象类;
BootClassLoader预加载常用类,单例模式。与Java中的BootClassLoader不同,它并不是由C/C++代码实现,而是由Java实现的;
BaseDexClassLoader是PathClassLoader、DexClassLoader、InMemoryDexClassLoader的父类,类加载的主要逻辑都是在BaseDexClassLoader完成的。
SecureClassLoader继承了抽象类ClassLoader,拓展了ClassLoader类加入了权限方面的功能,加强了安全性,其子类URLClassLoader是用URL路径从jar文件中加载类和资源。
其中重点关注的是PathClassLoader和DexClassLoader。
PathClassLoader是Android默认使用的类加载器,一个apk中的Activity等类便是在其中加载。
DexClassLoader可以加载任意目录下的dex/jar/apk/zip文件,比PathClassLoader更灵活,是实现插件化、热修复以及dex加壳的重点。
Android8.0新引入InMemoryDexClassLoader,从名字便可看出是用于直接从内存中加载dex。
代码验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void testClassLoader () { ClassLoader thisClassLoader = MainActivity.class.getClassLoader(); Log.i("yring" ,"ThisClassLoader:" + thisClassLoader); assert thisClassLoader != null ; ClassLoader parentClassLoader = thisClassLoader.getParent(); while (parentClassLoader!=null ){ Log.i("yring" ,"ThisClassLoader:" + thisClassLoader +"---" +parentClassLoader); thisClassLoader = parentClassLoader; parentClassLoader = thisClassLoader.getParent(); } Log.i("yring" ,"Root:" + thisClassLoader); }2024 -04 -12 23 :05 :42.783 22509 -22509 yring com.example.classloadertest I ThisClassLoader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.classloadertest-Q_Zm6XbsIInboG7AI4BaRw==/base.apk" ],nativeLibraryDirectories=[/data/app/com.example.classloadertest-Q_Zm6XbsIInboG7AI4BaRw==/lib/arm64, /system/lib64, /system/product/lib64]]]2024 -04 -12 23 :05 :42.784 22509 -22509 yring com.example.classloadertest I ThisClassLoader:dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.classloadertest-Q_Zm6XbsIInboG7AI4BaRw==/base.apk" ],nativeLibraryDirectories=[/data/app/com.example.classloadertest-Q_Zm6XbsIInboG7AI4BaRw==/lib/arm64, /system/lib64, /system/product/lib64]]]---java.lang.BootClassLoader@11087c92024 -04 -12 23 :05 :42.784 22509 -22509 yring com.example.classloadertest I Root:java.lang.BootClassLoader@11087c9
使用DexClassLoader加载其他Dex 这里需要注意dexfilepath路径,我的是在/data/user/0/com.xxx.yyy/
目录下,可以用context.getCacheDir().getAbsolutePath()
来验证,然后还需要注意dex
文件的权限。
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 public void testDexClassLoader (Context context,String dexfilepath) { File optFile = context.getDir("opt_dex" ,0 ); File libFile = context.getDir("lib_path" ,0 ); DexClassLoader dexClassLoader = new DexClassLoader (dexfilepath,optFile.getAbsolutePath(), libFile.getAbsolutePath(), MainActivity.class.getClassLoader()); Class<?> clazz = null ; try { clazz = dexClassLoader.loadClass("com.example.dexclassloader.Test" ); } catch (ClassNotFoundException e) { throw new RuntimeException (e); } if (clazz!=null ){ try { Method testFuncMethod = clazz.getDeclaredMethod("testFunc" ); try { Object obj = clazz.newInstance(); testFuncMethod.invoke(obj); } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { throw new RuntimeException (e); } } catch (NoSuchMethodException e) { throw new RuntimeException (e); } } }
App运行流程 app运行流程
通过Zygote进程到最终进入到app进程世界,我们可以看到ActivityThread.main()是进入App世界的大门,下面对该函数体进行简要的分析。ActivityThreadh是在framework
代码中,是一个单例模式.
对于ActivityThread这个类,其中的sCurrentActivityThread静态变量用于全局保存创建的ActivityThread实例,同时还提供了public static ActivityThread currentActivityThread()静态函数用于获取当前虚拟机创建的ActivityThread实例。
获取实例后,再获取得到loadedapk
再通过loadedapk
获取mclassloader
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 5379 public static void main (String[] args) {5380 Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain" );5381 SamplingProfilerIntegration.start();5382 5383 5384 5385 5386 CloseGuard.setEnabled(false );5387 5388 Environment.initForCurrentUser();5389 5390 5391 EventLogger.setReporter(new EventLoggingReporter ());5392 5393 AndroidKeyStoreProvider.install();5394 5395 5396 final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());5397 TrustedCertificateStore.setDefaultUserDirectory(configDir);5398 5399 Process.setArgV0("<pre-initialized>" );5400 5401 Looper.prepareMainLooper();5402 5403 ActivityThread thread = new ActivityThread ();5404 thread.attach(false );5405 5406 if (sMainThreadHandler == null ) {5407 sMainThreadHandler = thread.getHandler();5408 }5409 5410 if (false ) {5411 Looper.myLooper().setMessageLogging(new 5412 LogPrinter(Log.DEBUG, "ActivityThread" ));5413 }5414 5415 5416 Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);5417 Looper.loop();5418 5419 throw new RuntimeException ("Main thread loop unexpectedly exited" );5420 }5421 }
ActivityThread.main()函数是java中的入口main函数,这里会启动主消息循环,并创建ActivityThread实例,之后调用thread.attach(false)完成一系列初始化准备工作,并完成全局静态变量sCurrentActivityThread的初始化。之后主线程进入消息循环,等待接收来自系统的消息。当收到系统发送来的bindapplication的进程间调用时,调用函数handlebindapplication来处理该请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private void handleBindApplication (AppBindData data) { data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo); ... final ContextImpl appContext = ContextImpl.createAppContext(this , data.info); mInstrumentation = new Instrumentation (); Application app = data.info.makeApplication(data.restrictedBackupMode, null ); mInitialApplication = app; List<ProviderInfo> providers = data.providers; installContentProviders(app, providers); mInstrumentation.callApplicationOnCreate(app);
在 handleBindApplication函数中第一次进入了app的代码世界,该函数功能是启动一个application,并把系统收集的apk组件等相关信息绑定到application里,在创建完application对象后,接着调用了application的attachBaseContext方法,之后调用了application的onCreate函数。由此可以发现,app的Application类中的attachBaseContext和onCreate这两个函数是最先获取执行权进行代码执行的。这也是为什么各家的加固工具的主要逻辑都是通过替换app入口Application,并自实现这两个函数,在这两个函数中进行代码的脱壳以及执行权交付的原因。
加壳应用的运行流程
生命周期类处理 DexClassLoader加载的类是没有组件生命周期的,也就是说即使DexClassLoader通过对APK的动态加载完成了对组件类的加载,当系统启动该组件时,依然会出现加载类失败的异常。为什么组件类被动态加载入虚拟机,但系统却出现加载类失败呢?是因为系统组件是由mClassLoader
加载,此时mClassLoader
并未加载相关组件。
两种解决方案:
1、替换系统组件类加载器为我们的DexClassLoader,同时设置DexClassLoader的parent为系统组件类加载器;
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 public void replaceClassLoader (ClassLoader classLoader) { try { @SuppressLint("PrivateApi") Class<?> activityThreadClazz = classLoader.loadClass("android.app.ActivityThread" ); @SuppressLint("DiscouragedPrivateApi") Method currentActivityThread = activityThreadClazz.getDeclaredMethod("currentActivityThread" ); Object activityThreadObj = currentActivityThread.invoke(null ); @SuppressLint("DiscouragedPrivateApi") Field mPackageField = activityThreadClazz.getDeclaredField("mPackages" ); mPackageField.setAccessible(true ); ArrayMap mPackageObj = (ArrayMap) mPackageField.get(activityThreadObj); assert mPackageObj != null ; WeakReference wr = (WeakReference) mPackageObj.get(this .getPackageName()); assert wr != null ; Object loadedApkObj = wr.get(); @SuppressLint("PrivateApi") Class loadedApkClazz = classLoader.loadClass("android.app.LoadedApk" ); @SuppressLint("DiscouragedPrivateApi") Field mClassLoader = loadedApkClazz.getDeclaredField("mClassLoader" ); mClassLoader.setAccessible(true ); mClassLoader.set(loadedApkObj,classLoader); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) { throw new RuntimeException (e); } }
2、打破原有的双亲关系,在系统组件类加载器和BootClassLoader的中间插入我们自己的DexClassLoader即可;
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 File optFile = context.getDir("opt_dex" ,0 ); File libFile = context.getDir("lib_path" ,0 ); ClassLoader pathClassLoader = MainActivity.class.getClassLoader(); ClassLoader bootClassLoader = MainActivity.class.getClassLoader().getParent(); DexClassLoader dexClassLoader = new DexClassLoader (dexfilepath,optFile.getAbsolutePath(), libFile.getAbsolutePath(), bootClassLoader); try { @SuppressLint("DiscouragedPrivateApi") Field parentField = ClassLoader.class.getDeclaredField("parent" ); parentField.setAccessible(true ); parentField.set(pathClassLoader,dexClassLoader); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException (e); } Class<?> clazz = null ; try { clazz = dexClassLoader.loadClass("com.example.dexclassloader.TestActivity" ); } catch (ClassNotFoundException e) { throw new RuntimeException (e); } if (clazz!=null ){ Intent intent = new Intent (); intent.setClass(context, clazz); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK ); context.startActivity(intent); }
高安卓版本可能不行
加壳技术发展
dex加固技术发展 1、dex整体加固:文件加载和内存加载
2、函数抽取:在函数粒度完成代码的保护
3、VMP和Dex2C:JAVA函数Native化
so加固种类 1、基于init、init_array以及JNI_Onload函数的加壳
2、基于自定义linker的加壳
Dex文件格式 一、文件头介绍 1、文件头简介 dex 文件头一般固定为 0x70 个字节大小,包含标志、版本号、校验码、sha-1 签名以及其他一些方法、类的数量和偏移地址等信息。如下图所示:
2、dex文件头各字段解析
magic : 包含了 dex 文件标识符以及版本,从 0x00 开始,长度为 8 个字节
checksum : dex 文件校验码,偏移量为: 0x08,长度为 4 个字节。
signature : dex sha-1 签名,偏移量为 0x0c, 长度为 20 个字节
file_szie : dex 文件大小,偏移量为 0x20,长度为 4 个字节
header_size : dex 文件头大小,偏移量为 0x24,长度为 4 个字节,一般为 0x70
endian_tag : dex 文件判断字节序是否交换,偏移量为 0x28,长度为 4 个字节,一般情况下为 0x78563412
link_size : dex 文件链接段大小,为 0 则表示为静态链接,偏移量为 0x2c,长度为 4 个字节
link_off : dex 文件链接段偏移位置,偏移量为 0x30,长度为 4 个字节
map_off : dex 文件中 map 数据段偏移位置,偏移位置为 0x34,长度为 4 个字节
string_ids_size : dex 文件包含的字符串数量,偏移量为 0x38,长度为 4 个字节
string_ids_off : dex 文件字符串开始偏移位置,偏移量为 0x3c,长度为 4 个字节
type_ids_size : dex 文件类数量,偏移量为 0x40,长度为 4 个字节
type_ids_off : dex 文件类偏移位置,偏移量为 0x44,长度为 4 个字节
proto_ids_size : dex 文件中方法原型数量,偏移量为 0x48,长度为 4 个字节
proto_ids_off : dex 文件中方法原型偏移位置,偏移量为 0x4c,长度为 4 个字节
field_ids_size : dex 文件中字段数量,偏移量为 0x50,长度为 4 个字节
field_ids_off : dex 文件中字段偏移位置,偏移量为 0x54,长度为 4 个字节
method_ids_size : dex 文件中方法数量,偏移量为 0x58,长度为 4 个字节
method_ids_off : dex 文件中方法偏移位置,偏移量为 0x5c,长度为 4 个字节
class_defs_size : dex 文件中类定义数量,偏移量为 0x60,长度为 4 个字节
class_defs_off : dex 文件中类定义偏移位置,偏移量为 0x64,长度为 4 个字节
data_size : dex 数据段大小,偏移量为 0x68,长度为 4 个字节
data_off : dex 数据段偏移位置,偏移量为 0x6c,长度为 4 个字节
这里放一张官网的解释
二、校验和解析 checksum(校验和)是 DEX 位于文件头部的一个信息,用来判断 DEX 文件是否损坏或者被篡改,它位于头部的0x08
偏移地址处,占用 4 个字节,采用小端序存储。
在 DEX 文件中,采用Adler-32
校验算法计算出校验和,将 DEX 文件从0x0C
处开始读取到文件结束,将读取到的字节数组使用Adler-32 校验算法
计算出结果即是校验和即 checksum 字段
Adler-32
算法如下步骤实现:
a、定义两个变量varA
、varB
,其中varA
初始化为1,varB
初始化为0。
b、 读取字节数组的一个字节(假设该字节变量名为byte
),计算varA = (varA + byte) mod 65521
,然后可以计算出varB = (varA + varB) mod 65521
。
c. 重复步骤,直到字节数组全部读取完毕,得到最终varA
、varB
两个变量的结果。
d. 根据第三步得到的varA
、varB
两个变量,可得到最终校验和checksum =(varB << 16)+ varA
。
三、字符串解析
在文件头中,关于字符串的相关信息一共有 8 个字节,分别位于 0x38(4 Bytes) 和 0x3c(4 Bytes) 处,前者说明了该 DEX 文件包含了多少个字符串,后者则是字符串索引区的起始地址
编写简单用例
1 2 3 4 5 public class hello { public void main () { System.err.println("Hello world" ); } }
如下命令得到dex文件,名字是classes.dex
可知此classes.dex中有0xEADD个字符串,索引从0x70位置处开始
前面通过文件头知道了字符串数量和字符串索引区起始地址等信息,接下来就来具体看一下字符串索引区。字符串索引区存储的是字符串真正存储在数据区的偏移地址,以 4 个字节为一组,表示一个字符串在数据区的偏移地址,所以索引区一个占字符串数量 * 4
个字节那么多,同样的,索引区也采用的是小端序存储,所以我们在读取地址时,需要与小端序的方式来读取真正的地址,如下所示:
第一个字符串地址0x4C45E6
开始
从上面我们已经知道了如何找到字符串在数据区的偏移地址,接下来我们需要做的就是解析这些数据区的字节。通过偏移地址我们可以在数据区找到代表字符串的这些字节,在 DEX 文件中,字符串是通过MUTF-8
编码而成的(至于 mutf-8 是什么编码,我会将一些相关博客链接贴在文末),在MUTF-8
编码中,第一个字节代表了这个字符串所需要用到的字节数目(不包括最后一个代表终结的字节),最后一个字节为0x00
,表示这个字符串到此结束,跟 c 语言有点类似,中间部分才是一个字符串的具体内容,如下所示
四、类的解析
Dex 文件中关于类的类型,就是一个对象的所属的类,例如在 java 中一个字符串,它的类型就是java/lang/String
。在 Dex 文件头中,跟类的类型有关的一共有八个字节,分别是位于0x40
处占四个字节表示类的类型的数量和位于0x44
处占四个字节表示类的类型索引值的起始偏移地址,如下所示:
可知此classes.dex中有0x1E35个类,索引从0x3ABE4位置处开始
对于类的类型偏移地址,找到偏移地址后,它是以四个字节为一组,对应了在解析出来的字符串数组中的索引值,如这里读出是0x1BA8
,那么其名称就是字符串索引[0x1BA8]
(以下图片换用010editor)
回到字符串对应索引
字符串索引0x1BA8
即7080
,可知其对应的字符串地址是0x586F3B
,010也自动帮忙解析,此字符串是一个字母B
五、方法原型的解析
关于 dex 文件中方法原型的解析,需要知道怎么解析出字符串和类的类型。DEX 文件中的方法原型定义了一个方法的返回值类型和参数类型,例如一个方法返回值为void
,参数类型为int
,那么在 dex 文件中该方法原型表示为V(I)
(smali
中V
表示void
,I
表示int
)。
在 dex 文件头部中,关于方法原型有两处,第一处位于0x48
处,用 4 个字节定义了方法原型的数量,在0x4C
处用 4 个字节定义了方法原型的偏移地址,如下所示:
可知此classes.dex有0x3041个方法原型,从0x424B8开始索引
此处注意一个方法原型所占字节数为 12 个字节 ,
第1个字节到第4个字节表示了定义方法原型的字符串,这四个字节按小端序存储,读取出来为在字符串列表的索引,例如一个方法原型返回值为void
,参数为boolean
,那么定义该方法原型的字符串即为VZ
;
java类型
类型描述符
boolean
Z
byte
B
short
S
char
C
int
I
long
J
float
F
double
D
void
V
对象类型
L
数组类型
[
第 5 个字节到第8个字节表示该方法原型的返回值类型,读取出来的值为前面解析出来的类的类型列表的索引;
第 8 个字节到第12字节表示该方法原型的参数,读取出来为一组地址,通过该地址可以找到该方法原型的参数,跳转到该地址去,首先看前 4 个字节,前四个字节按照小端序存储,读取出来的值为该方法原型参数的个数,接着根据参数个数,读取具体的参数类型,每个参数类型占 2 个字节,这两个字节读取出来的值为前面解析出来的类的类型列表的索引,如下所示:
几个例子
a. 这里第1到第4个字节是定义方法原型字符串 的索引,按照读字符串方法,得到B
,即此方法是返回Byte型的无参函数
b. 第5到第8个字节是定义方法原型返回类型,是类的类型列表 的索引,这里也是B,说明返回的是Byte类
c. 最后四字节是参数信息,这里没有参数
a. 第1到第4字节对应字符串是LLZ
,说明返回类型是一个对象,参数是一个对象,一个布尔型
b. 第5到第8个字节,表示返回值是布尔型数组的一个对象
c. 最后四字节是参数信息地址,跳转过去,前四字节是参数数量,这里读出来是2,后面两个字节一组,代表参数在类的类型列表索引,这里读出来一个是布尔型数组,一个是布尔型
六、字段解析
在 dex 文件头中,关于字段(ps:字段可以简单理解成定义的变量或者常量)相关的信息有 8 个字节,在0x50~0x53
这四个字节,按小端序存储这 dex 文件中的字段数量,在0x54~0x57
这四个字节,存储这读取字段的起始偏移地址,如下所示
可知此classes.dex有0x8369个字段,从0x667C4开始索引
根据上面的字段起始偏移地址,我们可以找到字段,表示一个字段需要用八个字节
前两个字节为我们在前面解析出来类的类型列表 的索引,通过该索引找到的类的类型表示该字段在该类中被定义的
第三个字节和第四个字节,也是类的类型列表 的索引,表示该字段的类型,例如我们在 java 某个类中定义了一个变量int a
,那么我们此处解析出来的字段类型就是int
;
最后四个字节,则是我们前面解析出来字符串列表的索引,通过该索引找到的字符串表示字段的,例如我们定义了一个变量String test;
,那么我们在这里解析出来的就是test
,如下图所示:
前两个字节解析出类是:android.app.Notification
3-4字节解析出字段类型是:int
型
最后四字节解析出字段名字:color
这里010也自动帮我们归纳好了,即int android.app.Notification.color
七、方法解析
在 dex 文件头中,关于方法定义的信息同样是八个字节,分别位于0x58
处和0x5c
处。在0x58
处的四个字节,指明了 dex 文件中方法定义的数量,在0x5c
处的四个字节,表明了 dex 文件中的方法定义的起始地址(ps:都是以小端序存储的),如下图所示:
可知此classes.dex有0xE7AE个方法,从0xA830C开始索引
在上面的一步以及找到了方法定义的起始地址,跟字段类似的,一个方法定义也需要八个字节。
在前两个字节,以小端序存储着解析出来的类的类型列表 的索引,表示该方法属于哪个类;
第三个字节和第四个字节,以小端序存储这解析出来的方法原型列表 的索引,通过该索引值找到的方法原型声明了该方法的返回值类型和参数类型;
最后四个字节则以小端序存储着前面解析出来的字符串列表 的索引,声明了该方法的方法名。如下图所示:
前两个字节解析出类是:android.accessibilityservice.AccessibilityServiceInfo
3-4字节解析出方法原型是:int()
最后四字节解析出方法名字:getCapabilities
这里010也自动帮我们归纳好了,即
int android.accessibilityservice.AccessibilityServiceInfo.getCapabilities()
八、类解析
uleb128编码
uleb128 编码,是一种可变长度的编码,长度大小为1-5字节
,uleb128 通过字节的最高位来决定是否用到下一个字节,如果最高位为 1,则用到下一个字节,直到某个字节最高位为 0 或已经读取了 5 个字节为止,接下来通过一个实例来理解 uleb128 编码。
假设有以下经过 uleb128 编码的数据(都为 16 进制)–81 80 04
,
首先来看第一个字节81
,他的二进制为10000001
,他的最高位为1
,则说明还要用到下一个字节,它存放的数据则为0000001
;
再来看第二个字节80
,它的二进制为10000000
,它的最高位为1
,则说明还需要用到第三个字节,存放的数据为0000000
;
再来看第三个字节04
,它的二进制为00000100
,最高位为0
,说明一共使用了三个字节,它存放的数据为0000100
;
通过上面的数据我们已经获取了存放的数据,接下来就是把这些 bit 组合起来获取解码后的数据,dex 文件里面的数据都是采用的小端序的方式,uleb128 也不例外,在这三个字节,也不例外,第三个字节04
存放的数据0000100
作为解码后的数据的高 7 位
,第二个字节80
存放的数据0000000
作为解码后的数据的中 7 位
,第一个字节81
存放的数据0000001
作为解码后的数据的低 7 位
;那么解码后的数据二进制则为0000100 0000000 0000001
,转换为 16 进制则为0x10001
在 dex 文件头0x60-0x63
这四个字节,指明了class
的数量,在0x64-0x67
这四个字节,指明的class_def_item
的偏移地址。
可知此classes.dex有0x1944个类,从0x11c07C开始索引
通过上面的偏移地址,我们可以找到 class_def_item 的起始地址,class_def_item 包含了一个类的类名、接口、父类、所属 java 文件名等信息。一个 class_def_item 结构大小为 32 字节,分别包含 8 个信息,每个信息大小为 4 字节(小端序存储):
第 1-4 字节 -- class_idx
(该值为前面解析出来的类的类型列表的索引,也就是这个类的类名);
第 5-8 字节 -- access_flags
(类的访问标志,也就是这个类是 public 还是 private 等,这个通过官方的文档查表得知,具体算法在最后面说明);
第 9-12 字节 -- superclass_idx
(该值也为前面解析出来的类的类型列表的索引,指明了父类的类名)
第 13-16 字节 -- interfaces_off
(该值指明了接口信息的偏移地址,所指向的地址结构为 typelist,如果该类没有接口,该值则为 0)
第 17-20 字节 -- source_file_idx
(该值为 dex 字符串列表的的索引,指明了该类所在的 java 文件名)
第 21-24 字节 -- annotations_off
(该值为注释信息的偏移地址,查看注释信息具体结构的可以参考官方文档,官方文档地址粘贴在文末)
第 25-28 字节 -- class_data_off
(该值是这个类数据第二层结构的偏移地址,在该结构中指明了该类的字段和方法)
第 29-32 字节 -- static_value_off
(该值也是一个偏移地址,指向了一个结构,不是重点,感兴趣的参考官方文档,如果没相关信息,则该值为 0)
通过上面 class_def_item 的分析,我们知道了类的基本信息,例如类名、父类等,接下来就是要找到类里面的字段和方法这些信息,而这些信息,在 class_def_item 里面的 class_data_off 字段给我们指明class_data_item
就包含这些信息并给出了偏移地址,即现在需要解析class_data_iem
结构获取字段和方法信息。(ps:以下的数据结构,不做特别说明都为 uleb128 编码格式)
class_data_item
结构包含以下信息
第一个uleb128编码--static_field_size
,指明了该类的静态字段的数量
第二个uleb128编码--instance_field_size
,指明了该类的实例字段的数量
第三个uleb128编码--direct_method_size
,指明了该类的直接方法的个数
第四个uleb128编码--virtual_method_size
,指明了该类的虚方法的个数
encoded_field--static_fields
,该结构指明了具体的静态字段信息,该结构的存在前提是
static_field_size >0
,该结构包含两个 uleb128 编码,
第一个 uleb128 编码为前面解析出来的字段列表的索引,
第二个 uleb128 编码指明了该字段的访问标志 encoded_field--instance_fields
,
encoded_method--direct_methods
,该结构指明了直接方法具体信息,该结构存在的前提同样是direct_method_size > 0
,该结构包含 3 个 uleb128 编码,
第一个 uleb128 为前面文章解析出来的方法原型列表的索引值,
第二个 uleb128 编码为该方法的访问标志,
第三个 uleb128 为 code_off,也就是该方法具体代码的字节码的偏移地址,对应的结构为 code_item,code_item 结构里面包含了该方法内部的代码,这里是字节码,也就是 smali(ps: 如果该方法为抽象方法,例如 native 方法,这时 code_off 对应的值为 0,即该方法不存在具体代码)
encoded_method--virtual_methods
,该结构指明了该类的虚方法的具体信息,存在前提为virtual_method_size > 0
,具体结构和上面一样
code_item
在上面的 class_data_item 结构中的encoded_method
结构的第三个 uleb128 编码中,指出了一个类中的方法具体代码的偏移地址,也就是 dv 虚拟机在执行该方法的具体指令的偏移地址,该值指向的地址结构为code_item
,里面包含了寄存器数量、具体指令等信息,下面来分析一下该结构。
第 1-2 字节 -- registers_size
,该值指明了该方法使用的寄存器数量,对应的 smali 语法中的.register
的值
第 3-4 字节 -- ins_size
,该值指明了传入参数的个数
第 5-6 字节 -- outs_size
,该值指明了该方法内部调用其他函数用到的寄存器个数
第 7-8 字节 -- tries_size
,该值指明了该方法用到的try-catch
语句的个数
第 9-12 字节 -- debug_info_off
,该值指明了调试信息结构的偏移地址,如果不存在调试信息,则该值为 0
第 13-16 字节 -- insns_size
,该值指明了指令列表的大小,可以这么理解:规定了指令所用的字节数大小–2 x insns_size
ushort[insns_size] -- insns
,这个是指令列表,包含了该方法所用到的指令的字节,每个指令占用的字节数可以参考官方文档,这个没什么算法,就是一个查表的过程,例如invoke-direct
指令占用 6 个字节,return-void
指令占用 2 个字节
2 个字节 -- padding
,该值存在的前提是tries-size > 0
,作用用来对齐代码
try_item--tries
,该值存在的前提是tries-size > 0
,作用是指明异常具体位置和处理方式,该结构不是解析重点,重点是解析指令,感兴趣的查看官方文档
encoded_catch_handler_list--handlers
, 该结构存在前提为tries-size > 0
,同样不是解析重点
Frida obejction 基本信息
手机上启动frida-server,并启动应用,这里以W4terCTF2024
为例
使用命令objection -g com.w4ter.w4terctf2024 explore
将objection注入应用,可以用frida-ps -U|grep -i w4ter
查看完整包名
启动objection之后,会出现提示它的logo,这时候不知道输入啥命令的话,可以按下空格,有提示的命令及其功能出来;再按空格选中,又会有新的提示命令出来,这时候按回车就可以执行该命令,见执行的应用环境信息命令env和frida-server版本信息命令。
提取内存信息
查看内存中加载的库 命令memory list modules
查看库的导出函数 命令memory list exports libgsl.so
可以将结果导出到文件中memory list exports libgsl.so --json /root/libart.json
提取整个(或部分)内存 命令memory dump all from_base
,在脱壳部分详细介绍
搜索整个内存 命令memory search --string --offsets-only
内存堆搜索与执行
在堆上搜索实例 命令android heap search instances com.w4ter.w4terctf2024.FirstChall
注意,这里可能需要先通过点击手机界面,触发相应的类才行,即需要实例化。 这里会得到一个HashCode,相当于这个实例的句柄,可以以此作更多操作。
调用实例方法 命令android heap execute 102367040 w4terCheck
同时可以使用命令android heap evaluate 102367040
进入一个mini的js编辑器,可以编写复杂点的代码,比如有参数的函数等。
启动activity或service
直接启动activity 命令android intent launch_activity com.w4ter.w4terctf2024.SecondChall
查看当前可用activity
命令android hooking list activities
列出所有的类 命令android hooking list classes
搜索包含特定关键字符串的类 命令android hooking search classes w4ter
同理,搜索特定字符串方法 命令android hooking search methods w4tercheck
特定类所有方法 命令android hooking list class_methods com.w4ter.w4terctf2024.FirstChall
hook操作
生成简易Hook模板 命令android hooking generate simple com.w4ter.w4terctf2024.FirstChall
动态查看类信息 命令android hooking watch class com.w4ter.w4terctf2024.FirstChall
后续,这个类的操作都会被自动打出
Hook方法的参数、返回值、调用栈 命令android hooking watch class_method com.w4ter.w4terctf2024.FirstChall.doWin --dump-args --dump-return --dump-backtrace
可以使用jobs list
和jobs kill
来取消hook
抓包原理 Http抓包只需要配置好代理即可,麻烦的是Https,如下是基本抓包原理。
有了中间代理置于中间之后,本来C/S
架构的通信过程会“分裂”为两个独立的通信过程,app
本来验证的是服务器的证书,服务器的证书手机的根证书是认可的,直接内置的;但是分裂成两个独立的通信过程之后,app
验证的是Charles
的证书,它的证书手机根证书并不认可,它并不是由手机内置的权威根证书签发机构签发的,所以手机不认,然后app
也不认;所以我们要把中间代理的的证书导入到手机根证书目录中去,这样手机就会认可,如果app
没有进行额外的校验(比如在代码中对该证书进行校验,也就是SSL pinning系列API,这种情况下一小节具体阐述)的话,app
也会直接认可接受。
既然app
客户端会校验服务器证书,那么服务器可不可能校验app
客户端证书呢?答案是肯定的。
在许多业务非常聚焦并且当单一,比如行业应用、银行、公共交通、游戏等行业,C/S
架构中服务器高度集中,对应用的版本控制非常严格,这时候就会在服务器上部署对app
内置证书的校验代码。
上一小节中已经看到,单一通信已经分裂成两个互相独立的通信,这时候与服务器进行通信的已经不是app
、而是Charles
了,所以我们要将app
中内置的证书导入到Charles
中去。
这个操作通常需要完成两项内容:
找到证书文件
找到证书密码
找到证书文件很简单,一般apk
进行解包,直接过滤搜索后缀名为p12
的文件即可,一般常用的命令为tree -NCfhl |grep -i p12
,直接打印出p12
文件的路径,当然也有一些app
比较“狡猾”,比如我们通过搜索p12
没有搜到证书,然后看jadx
反编译的源码得出它将证书伪装成border_ks_19
文件,我们找到这个文件用file
命令查看果然不是后缀名所显示的png
格式,将其改成p12
的后缀名尝试打开时要求输入密码,可见其确实是一个证书,见下图2-17。
想要拿到密码也很简单,一般在jadx
反编译的代码中或者so
库拖进IDA
后可以看到硬编码的明文;也可以使用下面这一段脚本,直接打印出来,终于到了Frida
派上用场的时候。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function hook_KeyStore_load ( ) { Java .perform (function ( ) { var StringClass = Java .use ("java.lang.String" ); var KeyStore = Java .use ("java.security.KeyStore" ); KeyStore .load .overload ('java.security.KeyStore$LoadStoreParameter' ).implementation = function (arg0 ) { printStack ("KeyStore.load1" ); console .log ("KeyStore.load1:" , arg0); this .load (arg0); }; KeyStore .load .overload ('java.io.InputStream' , '[C' ).implementation = function (arg0, arg1 ) { printStack ("KeyStore.load2" ); console .log ("KeyStore.load2:" , arg0, arg1 ? StringClass .$new(arg1) : null ); this .load (arg0, arg1); }; console .log ("hook_KeyStore_load..." ); }); }
SSL Pinning ByPass 上文中我们还有一种情况没有分析,就是客户端并不会默认信任系统根证书目录中的证书,而是在代码里再加一层校验,这就是证书绑定机制——SSL pinning
,如果这段代码的校验过不了,那么客户端还是会报证书错误。
遇到这种情况的时候,我们一般有三种方式,当然目标是一样的,都是hook
住这段校验的代码,使这段判断的机制失效即可。
hook
住checkServerTrusted
,将其所有重载都置空
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function hook_ssl ( ) { Java .perform (function ( ) { var ClassName = "com.android.org.conscrypt.Platform" ; var Platform = Java .use (ClassName ); var targetMethod = "checkServerTrusted" ; var len = Platform [targetMethod].overloads .length ; console .log (len); for (var i = 0 ; i < len; ++i) { Platform [targetMethod].overloads [i].implementation = function ( ) { console .log ("class:" , ClassName , "target:" , targetMethod, " i:" , i, arguments ); } } }); }
使用objection
,直接将SSL pinning
给disable
掉
1 # android sslpinning disable
Socket 抓包 当我们在使用Charles
进行抓包的时候,会发现针对某些IP
的数据传输一直显示CONNECT
,无法Complete
,显示Sending request body
,并且数据包大小持续增长,这时候说明我们遇到了Socket
端口通信。
Socket
端口通信运行在会话层,并不是应用层,Socket
抓包的原理与应用层Http(s)
有着显著的区别。准确的说,Http(s)
抓包是真正的“中间人”抓包,而Socket
抓包是在接口上进行转储;Http(s)
抓包是明显的将一套C/S
架构通信分裂成两套完整的通信过程,而Socket
抓包是在接口上将发送与接收的内容存储下来,并不干扰其原本的通信过程。
对于安卓应用来说,Socket
通信天生又分为两种Java
层Socket
通信和Native
层Socket
通信。
Java
层:使用的是java.net.InetAddress
、java.net.Socket
、java.net.ServerSocket
等类,与证书绑定的情形类似,也可能存在着自定义框架的Socket
通信,这时候就需要具体情况具体分析,比如谷歌的protobuf
框架等;
Native
层:一般使用的是C Socket API
,一般hook
住send()
和recv()
函数可以得到其发送和接受的内容
抓包方法分为三种,接口转储、驱动转储和路由转储:
接口转储:比如给outputStream.write
下hook
,把内容存下来看看,可能是经过压缩、或加密后的包,毕竟是二进制,一切皆有可能;
驱动转储:使用tcpdump
将经过网口驱动时的数据包转储下来,再使用Wireshark
进行分析;
路由转储:自己做个路由器,运行jnettop
,观察实时进过的流量和IP
,可以使用WireShark
实时抓包,也可以使用tcpdump
抓包后用WireShark
分析。
Frida 使用 1.基本使用
Hook模板
1 2 3 4 5 6 7 8 9 10 11 function hookTest1 ( ){ }function main ( ) { Java .perform (function ( ) { hookTest1 (); }); }setImmediate (main);
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); EdgeToEdge.enable(this ); setContentView(R.layout.activity_main); while (true ){ try { Thread.sleep(1000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } myAdd(2024 ,2024 ); } } public int myAdd (int x, int y) { Log.d("yring" ,String.valueOf(x+y)); return x + y; } }
2.Hook普通方法、打印参数和修改返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function hookTest1 ( ) { var utils = Java .use ("com.yring.frida.MainActivity" ); utils.myAdd .implementation = function (a, b ) { var retval = this .myAdd (a, b); console .log ("arg1,arg2,res" , a, b, retval); return retval; } }
打印调用堆栈只需加上这一行代码
1 2 console .log (Java .use ("android.util.Log" ).getStackTraceString (Java .use ("java.lang.Throwable" ).$new()));
3.Hook重载函数 新增重载函数
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 public class MainActivity extends AppCompatActivity { private String total = "yring" ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); EdgeToEdge.enable(this ); setContentView(R.layout.activity_main); while (true ){ try { Thread.sleep(1000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } myAdd(2024 ,2024 ); Log.d("yring" ,myAdd("YRING" )); } } public int myAdd (int x, int y) { Log.d("yring" ,String.valueOf(x+y)); return x + y; } public String myAdd (String x) { total += x; return x.toLowerCase(); } String secret () { return total; } }
不修改Js直接Hook回报错
根据提示选择重载类型即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function hookTest2 ( ) { var utils = Java .use ("com.yring.frida.MainActivity" ); utils.myAdd .overload ('int' , 'int' ).implementation = function (a, b ) { var retval = this .myAdd (a, b); console .log ("arg1,arg2,res" , a, b, retval); return retval; } utils.myAdd .overload ('java.lang.String' ).implementation = function (a ) { var result = this .myAdd ("Frida Hook" ); console .log ("arg1,res" , a, result); return result; } }
NOTE:有时Js字符串转为Java字符串会出问题,使用
Java.use("java.lang.String").$new("my string here");
来正确生成Java字符串
4.主动调用函数 1)动态方法,即需要实例调用
1 2 3 4 5 6 7 8 9 10 11 12 function hookTest2 ( ) { Java .choose ("com.yring.frida.MainActivity" , { onMatch : function (instance ) { console .log ("found instance:" , instance); console .log ("secret res:" , instance.secret ()); }, onComplete : function ( ) { } }) }
2)静态方法,无需实例,直接调用
新增一个静态方法
1 2 3 public static String secretStatic () { return total; }
主动调用
1 2 var res = Java .use ("com.yring.frida.MainActivity" ).secretStatic ();console .log ("res" , res);
5.Hook构造函数 1 2 3 4 5 6 7 8 function hookTest3 ( ){ var utils = Java .use ("com.yring.frida.MainActivity" ); utils.$init .overload ().implementation = function ( ){ this .$init(); } }
6.Hook字段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function hookTest5 ( ){ Java .perform (function ( ){ var utils = Java .use ("com.zj.wuaipojie.Demo" ); utils.staticField .value = "我是被修改的静态变量" ; console .log (utils.staticField .value ); Java .choose ("com.zj.wuaipojie.Demo" , { onMatch : function (obj ){ obj._privateInt .value = "123456" ; obj.privateInt .value = 9999 ; }, onComplete : function ( ){ } }); }); }
7.Hook内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 function hookTest6 ( ){ Java .perform (function ( ){ var innerClass = Java .use ("com.zj.wuaipojie.Demo$innerClass" ); console .log (innerClass); innerClass.$init .implementation = function ( ){ console .log ("eeeeeeee" ); } }); }
8.创建类的实例以主动调用 1 2 3 4 5 6 7 8 9 10 11 12 13 function hookTest6 ( ){ Java .perform (function ( ) { var MyClass = Java .use ('com.yring.frida.Water' ); var myInstance = MyClass .$new(); console .log ("water instance call still:" , myInstance.still (myInstance)); }); }
Frida构造数组、对象、Map、类参数 测试代码
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 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d("SimpleArray" , "onCreate: SImpleArray" ); char [][] arr = new char [4 ][]; arr[0 ] = new char [] { '春' , '眠' , '不' , '觉' , '晓' }; arr[1 ] = new char [] { '处' , '处' , '闻' , '啼' , '鸟' }; arr[2 ] = new char [] { '夜' , '来' , '风' , '雨' , '声' }; arr[3 ] = new char [] { '花' , '落' , '知' , '多' , '少' }; Log.d("SimpleArray" , "-----横版-----" ); for (int i = 0 ; i < 4 ; i++) { Log.d("SimpleArraysToString" , Arrays.toString(arr[i])); Log.d("SimpleStringBytes" , Arrays.toString (Arrays.toString (arr[i]).getBytes())); for (int j = 0 ; j < 5 ; j++) { Log.d("SimpleArray" , Character.toString(arr[i][j])); } if (i % 2 == 0 ) { Log.d("SimpleArray" , "," ); } else { Log.d("SimpleArray" , "。" ); } } } }
Hook toString
函数 1 2 3 4 5 6 7 8 9 function hookTest2 ( ) { var utils = Java .use ("java.lang.Character" ); utils.toString .overload ('char' ).implementation = function (x ) { var res = this .toString (x); console .log ("args,res:" , x, res); return res; } }
1 2 3 4 5 6 7 8 function hookTest2 ( ) { var utils = Java .use ("java.util.Arrays" ); utils.toString .overload ('[C' ).implementation = function (x ) { var res = this .toString (x); console .log ("args,res:" , x, res); return res; } }
打印出Object x内容,可以用JSON.stringify(x)
1 2 3 4 5 6 7 8 function hookTest2 ( ) { var utils = Java .use ("java.util.Arrays" ); utils.toString .overload ('[C' ).implementation = function (x ) { var res = this .toString (x); console .log ("args,res:" , JSON .stringify (x), res); return res; } }
或者使用看雪roysue 编译的gson
1 2 3 Java .openClassFile ("/data/local/tmp/r0gson.dex" ).load ();const gson = Java .use ('com.r0ysue.gson.Gson' );console .log (gson.$new().toJson (xxx));
1 2 3 4 5 6 7 8 9 10 11 12 function hookTest2 ( ) { var utils = Java .use ("java.util.Arrays" ); utils.toString .overload ('[C' ).implementation = function (x ) { Java .openClassFile ("/data/local/tmp/r0gson.dex" ).load (); const gson = Java .use ('com.r0ysue.gson.Gson' ); var res = this .toString (x); console .log ("args,res:" , gson.$new().toJson (x), res); return res; } }
构造一个Array 使用命令var charArray = Java.array("char", ['一', '去', '二', '三', '里']);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function hookTest2 ( ) { var utils = Java .use ("java.util.Arrays" ); utils.toString .overload ('[C' ).implementation = function (x ) { var charArray = Java .array ("char" , ['一' , '去' , '二' , '三' , '里' ]); Java .openClassFile ("/data/local/tmp/r0gson.dex" ).load (); const gson = Java .use ('com.r0ysue.gson.Gson' ); var res = this .toString (charArray); console .log ("args,res:" , gson.$new().toJson (x), res); return res; } }
(放一张AS捕获的日志信息)
类型转换 代码water.java
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Water { public static String flow (Water W) { Log.d("2Object" , "water flow: I`m flowing" ); return "water flow: I`m flowing" ; } public String still (Water W) { Log.d("2Object" , "water still: still water runs deep!" ); return "water still: still water runs deep!" ; } }
代码juice.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Juice extends Water { public String fillEnergy () { Log.d("2Object" , "Juice: i`m fillingEnergy!" ); return "Juice: i`m fillingEnergy!" ; } public static void main () { Water w1 = new Water (); flow(w1) ; Juice J = new Juice (); flow(J) ; Water w2 = new Juice (); ((Juice) w2).fillEnergy(); } }
代码 var JuiceHandle = Java.cast(JuiceHandle, Java.use("com.yring.frida.Water"));
,注意只能由子类向父类转换,由父类向子类转换是不可能的。
1 2 3 4 5 6 7 8 9 10 11 12 var JuiceHandle = null ;Java .choose ("com.yring.frida.Juice" , { onMatch : function (myInstance ) { console .log ("found instance" , myInstance); console .log ("water instance call still:" , myInstance.fillEnergy ()); JuiceHandle = myInstance; }, onComplete : function ( ) { console .log ("finish search" ) } })var JuiceHandle = Java .cast (JuiceHandle , Java .use ("com.yring.frida.Water" ));console .log ("Juice fillEnergy method:" , JuiceHandle .still (JuiceHandle ));
接口 代码liquid.java
1 2 3 public interface liquid { public String flow () ; }
代码milk.java
1 2 3 4 5 6 7 8 9 10 public class milk implements liquid { public String flow () { Log.d("3interface" , "flowing : interface " ); return "nihao" ; }; public static void main () { milk m = new milk (); m.flow(); } }
实现
1 2 3 4 5 6 7 8 9 10 11 12 var beer = Java .registerClass ({ name : "com.yring.frida.beer" , implements : [Java .use ("com.yring.frida.liquid" )], methods : { flow : function ( ) { console .log ("from beer" ); return "test beer" ; } } });console .log ("beer.flow:" , beer.$new().flow ());
找到接口类的实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function test ( ) { Java .enumerateLoadedClasses ({ onMatch : function (className ) { if (className.indexOf ("w4ter" ) < 0 ) { return ; } var hookCls = Java .use (className); var interfaces = hook.class .getInterfaces (); if (interfaces.length > 0 ) { console .log (className); for (var i in interfaces) { console .log ("\t" , interfaces[i].toString ()); } } } }) }
枚举 代码TrafficLight.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 enum Signal { GREEN, YELLOW, RED }public class TrafficLight { public static Signal color = Signal.RED; public static void main () { Log.d("4enum" , "enum " + color.getClass().getName()); switch (color) { case RED: color = Signal.GREEN; break ; case YELLOW: color = Signal.RED; break ; case GREEN: color = Signal.YELLOW; break ; } } }
可以调用枚举类的一些方法
1 2 3 4 5 6 7 Java.choose("com.yring.frida.Signal" , { onMatch: function (myInstance) { console.log("found Instance" , myInstance); console.log("invoke getDeclaringClass" , myInstance.getDeclaringClass()); }, onComplete: function () { console.log("finished search" ); } })
Map 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Map<String, String> mapr0ysue = new HashMap <>(); mapr0ysue.put("ISBN 978-7-5677-8742-1" , "Android项目开发实战入门" ); mapr0ysue.put("ISBN 978-7-5677-8741-4" , "C语言项目开发实战入门" ); mapr0ysue.put("ISBN 978-7-5677-9097-1" , "PHP项目开发实战入门" ); mapr0ysue.put("ISBN 978-7-5677-8740-7" , "Java项目开发实战入门" ); Set<String> set = mapr0ysue.keySet(); Iterator<String> it = set.iterator(); Log.d("5map" , "key值:" );while (it.hasNext()) { try { Thread.sleep(5000 ); Log.d("5map" , it.next()+" " ); } catch (InterruptedException e) { e.printStackTrace(); } }
JS
1 2 3 4 5 6 7 8 9 Java .choose ("java.util.HashMap" , { onMatch : function (myInstance ) { if (myInstance.toString ().indexOf ("ISBN" ) != -1 ) { console .log ("found instance:" , myInstance); console .log (myInstance.toString ()); } }, onComplete : function ( ) { console .log ("finished" ); } })
这里if用于过滤,否则会把内存中所有的HashMap全部打印出来
一道例题 Login 界面是登陆
Jadx打开,关键是a函数验证
两种方式去hook
重载
1 2 3 4 5 6 7 var util = Java .use ("com.example.androiddemo.Activity.LoginActivity" ); util.a .overload ('java.lang.String' , 'java.lang.String' ).implementation = function (x, y ) { var res = this .a (x, y); console .log ("args,args,res:" , x, y, res); return res; }
静态方法直接调用
1 2 var res = Java .use ("com.example.androiddemo.Activity.LoginActivity" ).a ("aaaa" , "aaaa" );console .log (res);
都能得到对应密码
Chall1
直接hook a方法返回对应字符串
1 2 3 4 var util = Java .use ("com.example.androiddemo.Activity.FridaActivity1" ); util.a .implementation = function (x ) { return 'R4jSLLLLLLLLLLOrLE7/5B+Z6fsl65yj6BgC6YWz66gO6g2t65Pk6a+P65NK44NNROl0wNOLLLL=' ; }
Chall2
这里可以hook字段,也可以直接调用两个函数,使得判断为真
1 2 3 4 5 6 7 8 9 10 var util = Java .use ("com.example.androiddemo.Activity.FridaActivity2" ); util.setStatic_bool_var ();Java .choose ("com.example.androiddemo.Activity.FridaActivity2" , { onMatch : function (instance ) { console .log ("found instace:" , instance); instance.setBool_var (); }, onComplete : function ( ) { } })
Chall3
显然只能Hook字段
1 2 3 4 5 6 7 8 9 10 11 var util = Java .use ("com.example.androiddemo.Activity.FridaActivity3" ); util.static_bool_var .value = true ;Java .choose ("com.example.androiddemo.Activity.FridaActivity3" , { onMatch : function (instance ) { console .log ("found instace:" , instance); instance.bool_var .value = true ; instance._same_name_bool_var .value = true ; }, onComplete : function ( ) { } })
Chall4
显然是Hook内部类,并需要Hook内部类的函数返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var util = Java .use ("com.example.androiddemo.Activity.FridaActivity4$InnerClasses" ); util.check1 .implementation = function ( ) { return true ; } util.check2 .implementation = function ( ) { return true ; } util.check3 .implementation = function ( ) { return true ; } util.check4 .implementation = function ( ) { return true ; } util.check5 .implementation = function ( ) { return true ; } util.check6 .implementation = function ( ) { return true ; }
或者利用反射拼接一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var util = Java .use ("com.example.androiddemo.Activity.FridaActivity4$InnerClasses" );var all_methods = util.class .getDeclaredMethods ();for (var i = 0 ; i < all_methods.length ; i++) { var method = all_methods[i]; var class_name = "com.example.androiddemo.Activity.FridaActivity4$InnerClasses" var substring = method.toString ().substr (method.toString ().indexOf (class_name) + class_name.length + 1 ); var final_method = substring.substring (0 , substring.indexOf ("(" )); util[final_method].implementation = function ( ) { return true ; } }
Chall5 可以发现很多反编译失败
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 public class FridaActivity5 extends BaseFridaActivity { private CheckInterface DynamicDexCheck = null ; @Override public String getNextCheckTitle () { return "当前第5关" ; } public static void copyFiles (android.content.Context r2, java.lang.String r3, java.io.File r4) { throw new UnsupportedOperationException ("Method not decompiled: com.example.androiddemo.Activity.FridaActivity5.copyFiles(android.content.Context, java.lang.String, java.io.File):void" ); } private void loaddex () { File filesDir = getFilesDir(); if (!filesDir.exists()) { filesDir.mkdir(); } String str = filesDir.getAbsolutePath() + File.separator + "DynamicPlugin.dex" ; File file = new File (str); try { if (!file.exists()) { file.createNewFile(); copyFiles(this , "DynamicPlugin.dex" , file); } } catch (IOException e) { e.printStackTrace(); } try { this .DynamicDexCheck = (CheckInterface) new DexClassLoader (str, filesDir.getAbsolutePath(), null , getClassLoader()).loadClass("com.example.androiddemo.Dynamic.DynamicCheck" ).newInstance(); if (this .DynamicDexCheck == null ) { Toast.makeText(this , "loaddex Failed!" , 1 ).show(); } } catch (Exception e2) { e2.printStackTrace(); } } public CheckInterface getDynamicDexCheck () { if (this .DynamicDexCheck == null ) { loaddex(); } return this .DynamicDexCheck; } @Override public void onCreate (Bundle bundle) { super .onCreate(bundle); loaddex(); } @Override public void onCheck () { if (getDynamicDexCheck() != null ) { if (getDynamicDexCheck().check()) { CheckSuccess(); startActivity(new Intent (this , FridaActivity6.class)); finishActivity(0 ); return ; } super .CheckFailed(); return ; } Toast.makeText(this , "onClick loaddex Failed!" , 1 ).show(); } }
主要通过classloader来找包、找函数然后hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Java .choose ("com.example.androiddemo.Activity.FridaActivity5" , { onMatch : function (instance ) { console .log ("found instace getDynamicDexCheck" , instance.getDynamicDexCheck ().$className ); }, onComplete : function ( ) { console .log ("finish" ); } });Java .enumerateClassLoaders ({ onMatch : function (loader ) { try { if (loader.findClass ("com.example.androiddemo.Dynamic.DynamicCheck" )) { console .log ("succefully found loader:" , loader); Java .classFactory .loader = loader; } } catch (err) { console .log ("err" , err); } }, onComplete : function ( ) { } })Java .use ("com.example.androiddemo.Dynamic.DynamicCheck" ).check .implementation = function ( ) { return true ; }
Chall6
更简单
1 2 3 Java .use ("com.example.androiddemo.Activity.Frida6.Frida6Class0" ).check .implementation = function ( ) { return true ; }Java .use ("com.example.androiddemo.Activity.Frida6.Frida6Class1" ).check .implementation = function ( ) { return true ; }Java .use ("com.example.androiddemo.Activity.Frida6.Frida6Class2" ).check .implementation = function ( ) { return true ; }
RPC 即远程过程调用
python调用 测试代码
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 package com.yring.frida;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import java.util.Arrays;import java.util.HashMap;import java.util.Iterator;import java.util.Map;import java.util.Set;public class MainActivity extends AppCompatActivity { private static String total = "" ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); while (true ){ try { Thread.sleep(3000 ); }catch (InterruptedException e){ Log.e("err" ,String.valueOf(e)); } fun(1 ,2 ); } } String fun (String x) { total += x; return x.toLowerCase(); } int fun (int x,int y) { Log.d("yring" ,String.valueOf(x+y)); return x+y; } String secret () {return total;} static String secretStatic () {return total;} }
Js代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function invoke ( ) { Java .choose ("com.yring.frida.MainActivity" , { onMatch : function (myInstance ) { console .log ("found instance:" , myInstance); myInstance.fun ("1" ) console .log ("found res:" , myInstance.secret ()); }, onComplete : function ( ) { } }) }function main ( ) { Java .perform (function ( ) { invoke (); }); } rpc.exports = { invokefunc : main }
Python代码
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 import timeimport fridadef my_message_handler (message, payload ): print (message) print (payload) device = frida.get_remote_device() pid = device.spawn(["com.yring.frida" ]) device.resume(pid) time.sleep(1 ) session = device.attach(pid)with open ("hook.js" ) as f: script = session.create_script(f.read()) script.on("message" , my_message_handler) script.load() command = "" while True : command = input ("Enter Command:" ) if command == "1" : break elif command == "2" : script.exports.invokefunc()
多端口,多主机,多手机 frida监听不同端口
1 ./data/local/tmp/fr14 -l 0.0.0.0:9999
使用python rpc连接 (由于是基于ip,需要在局域网内,可用热点)
1 device = frida.get_device_manager().add_remote_device("172.20.10.2:9999" )
多手机也一样,无非就是改ip而已
互联互通 测试代码(界面就是简单登陆界面,限制不能以admin
登陆)
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 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); EditText userName = (EditText) this .findViewById(R.id.userName); EditText password = (EditText) this .findViewById(R.id.password); TextView textSend = (TextView) this .findViewById(R.id.textView); Button login = (Button) this .findViewById(R.id.button); login.setOnClickListener(new View .OnClickListener() { @SuppressLint("SetTextI18n") @Override public void onClick (View v) { if (userName.getText().toString().compareTo("admin" ) == 0 ){ textSend.setText("Cannot login as admin" ); return ; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { textSend.setText("Sending to the server: " + Base64.getEncoder().encodeToString((userName.getText().toString() + ":" +password.getText().toString()).getBytes())); } } }); } }
因此这里hook目标就是把Base64第一部分替换为admin
可以先使用objection
看走了哪个重载
然后编写frida,进行hook和主动调用,只有当这两个步骤成功之后才能继续rpc
这里直接给出frida代码
hook.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Java .perform (function ( ) { Java .use ("android.widget.TextView" ).setText .overload ('java.lang.CharSequence' ).implementation = function (x ) { var string_to_send_x = x.toString (); var string_to_recv; send (string_to_send_x); recv (function (received_json_objection ) { string_to_recv = received_json_objection.my_data console .log ("string_to_recv:" + string_to_recv) }).wait (); var javaStringToSend = Java .use ('java.lang.String' ).$new(string_to_recv); var result = this .setText (javaStringToSend); return result; } })
需要注意,在python调用这个js时,只能是这样rpc
loader.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 31 32 33 34 35 36 37 38 import timeimport fridaimport base64def my_message_handler (message, payload ): print (message) print (payload) if message["type" ] == "send" : print (message["payload" ]) data = message["payload" ].split(":" )[1 ].strip() print ("data:" , data) data = str (base64.b64decode(data)) print ("original data:" , data) data = data.split(":" ) data[0 ] = "admin" data = data[0 ] + ":" + data[1 ] script.post({"my_data" : base64.b64encode(data.encode()).decode()}) device = frida.get_usb_device() session = device.attach("com.yring.frida" )with open ("frida.js" ) as f: script = session.create_script(f.read()) script.on("message" , my_message_handler) script.load()input ()
可以发现,rpc中数据交流格式是以Json进行的
又一个App app名称:kgb_messenger
初步分析 运行
使用Objection
除了MainActivity都可以被主动调用成功
地区、白名单绕过 使用Jadx分析,并使用字符串搜索方式找到关键地方
尝试hook函数getProperty
,可以现在smali里找到函数类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function invoke ( ) { var util = Java .use ("java/lang/System" ); util.getProperty .overload ('java.lang.String' ).implementation = function (x ) { var res = this .getProperty (x); console .log ("arg,res:" , x, res); return res; } }function main ( ) { Java .perform (function ( ) { invoke (); }); }setImmediate (main);
没有返回值,主动返回Russia
1 return Java .use ("java.lang.String" ).$new("Russia" );
过掉第一层检测,继续往下hook
在资源文件找到对应字符串
增加hook代码
1 2 3 4 5 6 util.getenv .overload ("java.lang.String" ).implementation = function (x ) { var res = this .getenv (x); console .log ("env arg,res:" , x, res); return res; }
同样直接返回
1 return Java .use ("java.lang.String" ).$new("RkxBR3s1N0VSTDFOR180UkNIM1J9Cg==" );
可以成功进入登陆界面
登陆绕过
相关i、j函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private void i ( ) { char[] cArr = {'(' , 'W' , 'D' , ')' , 'T' , 'P' , ':' , '#' , '?' , 'T' }; cArr[0 ] = (char) (cArr[0 ] ^ this .n .charAt (1 )); cArr[1 ] = (char) (cArr[1 ] ^ this .o .charAt (0 )); cArr[2 ] = (char) (cArr[2 ] ^ this .o .charAt (4 )); cArr[3 ] = (char) (cArr[3 ] ^ this .n .charAt (4 )); cArr[4 ] = (char) (cArr[4 ] ^ this .n .charAt (7 )); cArr[5 ] = (char) (cArr[5 ] ^ this .n .charAt (0 )); cArr[6 ] = (char) (cArr[6 ] ^ this .o .charAt (2 )); cArr[7 ] = (char) (cArr[7 ] ^ this .o .charAt (3 )); cArr[8 ] = (char) (cArr[8 ] ^ this .n .charAt (6 )); cArr[9 ] = (char) (cArr[9 ] ^ this .n .charAt (8 )); Toast .makeText (this , "FLAG{" + new String (cArr) + "}" , 1 ).show (); } private boolean j ( ) { byte[] digest; String str = "" ; for (int i = 0 ; i < this .m .digest (this .o .getBytes ()).length ; i++) { str = str + String .format ("%x" , Byte .valueOf (digest[i])); } return str.equals (getResources ().getString (R.string .password )); }
直接可以分析出账号为:codenameduchess
(资源文件),密码为guest
(直接搜md5)
聊天界面分析 字符串查询相关聊天记录,找到对应位置 hook对应构造函数
1 2 3 4 5 6 var util2 = Java .use ("com.tlamb96.kgbmessenger.b.a" ) util2.$init .implementation = function (str0, str1, str2, str3 ) { var res = this .$init(str0, str1, str2, str3); console .log ("str0,str1,str2,str3:" , str0, str1, str2, str3); return res; }
主动Send 123来触发
打印调用堆栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 java.lang .Throwable at com.tlamb96 .kgbmessenger .b .a .<init>(Native Method) at com.tlamb96 .kgbmessenger .MessengerActivity .onSendMessage (Unknown Source:40 ) at java.lang .reflect .Method .invoke (Native Method) at android.support .v7 .app .m$a .onClick (Unknown Source:25 ) at android.view .View .performClick (View.java :7169 ) at android.view .View .performClickInternal (View.java :7139 ) at android.view .View .access$3900 (View.java :808 ) at android.view .View$PerformClick .run (View.java :27481 ) at android.os .Handler .handleCallback (Handler.java :883 ) at android.os .Handler .dispatchMessage (Handler.java :100 ) at android.os .Looper .loop (Looper.java :214 ) at android.app .ActivityThread .main (ActivityThread.java :7615 ) at java.lang .reflect .Method .invoke (Native Method) at com.android .internal .os .RuntimeInit$MethodAndArgsCaller .run (RuntimeInit.java :492 ) at com.android .internal .os .ZygoteInit .main (ZygoteInit.java :964 )
可知是由om.tlamb96.kgbmessenger.MessengerActivity.onSendMessage
调用,查看该函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void onSendMessage (View view) { EditText editText = (EditText) findViewById(R.id.edittext_chatbox); String obj = editText.getText().toString(); if (TextUtils.isEmpty(obj)) { return ; } this .o.add(new com .tlamb96.kgbmessenger.b.a(R.string.user, obj, j(), false )); this .n.c(); if (a(obj.toString()).equals(this .p)) { Log.d("MessengerActivity" , "Successfully asked Boris for the password." ); this .q = obj.toString(); this .o.add(new com .tlamb96.kgbmessenger.b.a(R.string.boris, "Only if you ask nicely" , j(), true )); this .n.c(); } if (b(obj.toString()).equals(this .r)) { Log.d("MessengerActivity" , "Successfully asked Boris nicely for the password." ); this .s = obj.toString(); this .o.add(new com .tlamb96.kgbmessenger.b.a(R.string.boris, "Wow, no one has ever been so nice to me! Here you go friend: FLAG{" + i() + "}" , j(), true )); this .n.c(); } this .m.b(this .m.getAdapter().a() - 1 ); editText.setText("" ); }
直接hook
1 2 3 4 5 6 7 Java .use ("com.tlamb96.kgbmessenger.MessengerActivity" ).a .implementation = function (x ) { var res = this .a (x); console .log ("a:,res:" , x, res); return Java .use ("java.lang.String" ).$new("V@]EAASB\u0012WZF\u0012e,a$7(&am2(3.\u0003" ); }
然而绕过是不够的,必须要输入正确才能得到最终的flag,不过这就是简单的算法逆向了。 这里可以使用Java制作一个解密的dex,然后js中直接调用即可,大致语句如下
1 2 3 Java .openClassFile ("/data/local/tmp/r0gson.dex" ).load ();const gson = Java .use ('com.r0ysue.gson.Gson' );console .log (gson.$new().toJson (xxx));