mirror of
https://github.com/feicong/rom-course.git
synced 2025-05-05 18:16:57 +00:00
第六章内容优化
This commit is contained in:
parent
abfb7b90a7
commit
f867630927
@ -1,39 +1,176 @@
|
||||
# 第六章 功能定制
|
||||
|
||||
上一章中系统内置的过程和`Android`系统的编译流程息息相关,而本章功能的定制就是和安卓源码的执行紧密相连,通过对源码运行的理解,在执行过程的源码中添加需求功能,插入自己的业务逻辑,例如对其插桩输出,可以帮助我们更好的理解源码的执行过程。在本章中,将头开始分析功能,分析其原理,然后逐步实现。
|
||||
在上一章中,我们了解到系统内置的过程与`Android`系统的编译流程密切相关。而本章的主题是定制功能,这与安卓源码的执行有着紧密的联系。通过理解源码运行过程,在执行过程中添加需求功能并插入自己的业务逻辑,比如对其进行插桩输出,可以帮助我们更好地理解源码的执行过程。
|
||||
|
||||
在本章中,我们将首先开始分析所需实现功能,并深入研究其原理。然后逐步实现这些功能。
|
||||
|
||||
|
||||
## 6.1 如何进行功能定制
|
||||
|
||||
在开始动手前,必须要明确需求,规划要实现功能的具体表现。根据预定好的目标方向,将能抽取的业务部分隔离开来,通过开发普通的`App`先将业务逻辑实现,如果直接在`AOSP`中修改源码,发现问题后,反复编译排查这些逻辑的细节,将导致耗费大量的时间成本在简单问题上。
|
||||
在开始实际操作之前,我们必须明确需求,并规划所要实现功能的具体表现。根据预定的目标方向,我们需要将可提取的业务部分与源码隔离开来。通过先开发一个普通的App来实现业务逻辑,而不是直接在AOSP中修改源码。这样做可以避免在排查细节时反复编译和耗费大量时间成本。
|
||||
|
||||
除此之外,尽量使用源码版本管理工具来维护代码,避免长期迭代后,无法找到自己修改的相关代码,导致维护非常困难,以及迁移代码的不便利,如果不想搭建源码版本管理AOSP,则尽量保持代码开发的风格,将自己修改处的代码统一打上标识,功能完成到一定阶段进行备份,避免因为修改导致系统异常,却又无法回退代码解决问题。
|
||||
此外,尽可能使用源码版本管理工具来维护代码,以避免长期迭代后无法找到自己修改过的相关代码导致维护困难以及代码迁移不便利。如果无法搭建AOSP作为源码版本管理工具,则应保持一致的代码开发风格,并对自己所做修改处统一打上标识,在功能达到一定阶段时进行备份,以防因修改导致系统异常但又无法回退代码解决问题。
|
||||
|
||||
在进行功能定制时,需要先对目标的执行流程和实现过程有一定的了解,然后找到合适的切入点,在实现过程中分析源码时,需要留意`AOSP`中提供的各种功能函数,有些常见的功能函数不要自己重新写一套实现,如果要定制的功能在AOSP源码中有类似的实现,则直接参考官方的实现。
|
||||
在进行功能定制时,首先需要对目标执行流程和实现过程有一定了解,并找到合适的切入点。在分析源码时,请注意`AOSP`中提供的各种常用功能函数。如果`AOSP`已经提供了类似功能的实现方式,则直接参考官方实现即可,不必重新编写。
|
||||
|
||||
事实上,在功能开发过程中就是不断熟悉源码和理解源码的过程。接下来,在本章中我们将完成以下几个目标:
|
||||
|
||||
- 学习插桩技术,加深对源码执行过程的印象。
|
||||
- 模仿`AOSP`自身的系统服务,添加一个自己的系统服务。
|
||||
- 在应用启动过程中注入Java代码。
|
||||
- 修改默认权限,了解Android是如何加载解析AndroidManifest.xml文件。
|
||||
|
||||
功能开发的过程实际就是不断熟悉源码的过程,以及对源码的理解。接下来这一章中将完成下列几个目标。
|
||||
|
||||
* 学习插桩,加深源码执行的印象。
|
||||
* 模仿`AOSP`自身的系统服务,来添加一个自己的系统服务。
|
||||
* 在应用启动过程中注入`Java`代码。
|
||||
* 修改默认权限,了解`Android`是如何加载解析`AndroidManifest.xml`。
|
||||
|
||||
## 6.2 插桩
|
||||
|
||||
在`Android`逆向中,插桩是非常常见的手段,它可以帮助开发人员检测和诊断代码问题。插桩是指在程序运行时向代码中插入额外的指令或代码段来收集有关程序执行的信息。这些信息可以用于分析程序执行流程、性能瓶颈等问题。常见的插桩方式分为静态插桩和动态插桩。
|
||||
在Android逆向中,插桩是一种非常常见的技术手段,它能够帮助开发人员检测和诊断代码问题。插桩指的是在程序运行时向代码中插入额外的指令或代码段,以收集与程序执行相关的信息。这些信息可以用于分析程序的执行流程、性能瓶颈等问题。常见的App插桩方式包括静态插桩和动态插桩两种。
|
||||
|
||||
静态插桩是指将额外的指令或代码段直接嵌入到源码中,并通过编译生成修改后的可执行文件。这种方式需要重新编译源码,并且对于已经发布的应用来说不太实际。
|
||||
|
||||
动态插桩则是在应用运行时通过注入代码来实现,在不改变原始源码结构和重新编译应用的情况下进行操作。这样做既方便又灵活,可以针对具体需求选择合适位置进行插入。
|
||||
|
||||
无论是静态还是动态插桩,它们都为开发人员提供了强大而有力的工具来深入理解和调试复杂程序。在进行Android逆向工程时,掌握并善于使用这些技术将会极大地提高我们解决问题和优化代码质量的能力。
|
||||
|
||||
除了App级别的插桩,对于系统来说,还有一种ROM级别的插桩,这种可以算作源码级别的插桩技术。
|
||||
|
||||
|
||||
### 6.2.1 静态插桩
|
||||
|
||||
静态插桩是指将插入的代码直接嵌入到源代码中,并在编译期间将其转换为二进制形式。静态插桩通常用于收集静态信息,例如函数调用图、代码覆盖率等。比如将一个`APP`反编译后,找到要分析的目标函数,在`smali`指令中插入日志输出的指令,并且将函数的参数或返回值,或全局变量、局部变量等需要观测的相关信息进行输出,最后将代码回编成`apk`后,重新签名。
|
||||
安卓App的静态插桩通常是指`smali`反编译文件的静态插桩。这种技术是指在应用程序的dex文件中直接修改smali代码,以实现对应用程序行为的改变或扩展。Smali是一种类似于Java字节码的低级语言,它是Android平台上Dalvik虚拟机所使用的指令集。
|
||||
|
||||
通过进行smali静态插桩,我们可以在目标应用程序中添加新的方法、修改现有方法体、注入特定逻辑等操作。这样做能够提供更大程度的控制,并且不需要重新编译整个应用。
|
||||
|
||||
下面给出一个简单例子来说明smali静态插桩:
|
||||
|
||||
假设我们有一个目标应用程序,其中存在一个名为`calculateSum()`的方法,该方法接受两个参数并返回它们之和。我们想要在调用`calculateSum()`前后打印日志以便跟踪其执行。
|
||||
|
||||
首先,在目标应用程序中找到相应类文件对应的smali文件(通常位于`/smali/com/example/YourClass.smali`)。
|
||||
|
||||
然后,在`YourClass.smali`文件中找到包含`calculateSum()`方法定义部分,并在其前后添加以下代码:
|
||||
|
||||
```smali
|
||||
.method public calculateSum(II)I
|
||||
.locals 2
|
||||
|
||||
; 在调用calculateSum()之前打印日志
|
||||
const-string v0, "Before calculateSum()"
|
||||
invoke-static {v0}, Landroid/util/Log;->d(Ljava/lang/String;)I
|
||||
|
||||
; ... 其他原始的calculateSum()方法代码 ...
|
||||
|
||||
; 在调用calculateSum()之后打印日志
|
||||
const-string v0, "After calculateSum()"
|
||||
invoke-static {v0}, Landroid/util/Log;->d(Ljava/lang/String;)I
|
||||
|
||||
; ... 其他原始的calculateSum()方法代码 ...
|
||||
|
||||
.end method
|
||||
```
|
||||
|
||||
以上代码在`calculateSum()`方法前后分别添加了打印日志的逻辑。这样,在每次调用`calculateSum()`时,我们都可以看到相应的日志信息。
|
||||
|
||||
需要注意的是,进行smali静态插桩需要对Dalvik虚拟机指令集和smali语法有一定了解,并且需要小心操作以避免引入错误或破坏应用程序结构。因此,在实际使用中,建议参考相关文档和教程,并谨慎处理目标应用程序的smali文件。如果读者对这一块内容感兴趣,可以参考阅读笔者另外一本安卓软件逆向工程相关的书籍。
|
||||
|
||||
|
||||
### 6.2.2 动态插桩
|
||||
|
||||
动态插桩是指在程序运行时动态地将插入的代码加载到内存中并执行。动态插桩通常用于收集动态信息,例如内存使用情况、线程状态等。对函数进行`Hook`就是一种动态插桩技术,在`Hook`中,通过修改函数入口地址,将自己的代码"钩"入到目标函数中,在函数调用前或调用后执行一些额外的操作,例如记录日志、篡改数据、窃听函数调用等。`Hook`通常用于调试、性能分析和安全审计等方面。它可以帮助开发人员诊断代码问题,提高程序的稳定性和性能,并增强程序的安全性。同时,`Hook`还可以被黑客用于攻击应用程序。
|
||||
安卓App的动态插桩是指在应用程序运行时通过注入代码来修改或扩展应用程序的行为。与静态插桩不同,动态插桩不需要对源码进行修改或重新编译,而是在应用程序加载和执行过程中实时注入代码。
|
||||
|
||||
`Frida`是一种常用的动态插桩工具,它可以帮助我们在Android设备上进行运行时的代码注入和修改。下面给出一个简单例子来说明`Frida`动态插桩:
|
||||
|
||||
首先,在你的Android设备上安装好`Frida`,并确保设备与计算机处于相同网络环境。
|
||||
|
||||
然后,创建一个Python脚本(例如`frida_script.py`),并使用以下代码示例:
|
||||
|
||||
```python
|
||||
import frida
|
||||
|
||||
# 定义要注入的JavaScript代码
|
||||
js_code = """
|
||||
Java.perform(function() {
|
||||
// 找到目标类及方法
|
||||
var targetClass = Java.use('com.example.YourClass');
|
||||
var targetMethod = targetClass.calculateSum;
|
||||
|
||||
// 将目标方法替换为新逻辑
|
||||
targetMethod.implementation = function(a, b) {
|
||||
console.log('Before calculateSum()');
|
||||
|
||||
var result = this.calculateSum(a, b); // 调用原始方法
|
||||
|
||||
console.log('After calculateSum(), Result: ' + result);
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
"""
|
||||
|
||||
# 连接到目标进程,并将JavaScript代码注入
|
||||
def on_message(message, data):
|
||||
print(message)
|
||||
|
||||
process_name = "com.example.yourapp" # 替换为目标应用程序的进程名
|
||||
session = frida.get_usb_device().attach(process_name)
|
||||
script = session.create_script(js_code)
|
||||
script.on('message', on_message) # 设置消息监听器
|
||||
script.load() # 加载并执行注入的代码
|
||||
|
||||
# 持续运行,直到手动中断脚本
|
||||
frida.resume(pid)
|
||||
|
||||
```
|
||||
|
||||
以上代码使用`Frida`在运行时动态插桩了一个名为`calculateSum()`的方法。它首先找到目标类和方法,然后将原始方法替换为新逻辑,在调用前后打印日志。
|
||||
|
||||
你需要将示例代码中的`com.example.YourClass`和`com.example.yourapp`替换为实际目标类和应用程序的名称。另外,请确保你已经安装了必要的Python依赖项(如`frida`、`frida-tools`等)。
|
||||
|
||||
通过执行上述Python脚本,`Frida`会自动连接到指定进程,并在运行时进行动态插桩操作。当你启动或触发相关操作时,可以在控制台或日志输出中看到相应的信息。
|
||||
|
||||
需要注意的是,使用Frida进行动态插桩可能涉及一些安全风险,并且对于某些防护机制可能无法正常工作。因此,在进行实际应用程序测试之前,请确保遵循合法和道德准则,并仔细研究相关文档和教程。
|
||||
|
||||
|
||||
### 6.2.3 ROM插桩
|
||||
|
||||
`ROM`插桩是指在预置的`ROM`固件中进行插桩,和前面的两种方式不同,直接通过修改系统代码,对想要关注的信息进行输出即可,过程等于是在开发的`APP`中添加`LOG`日志,虽然插桩非常方便,但是这并不是一个小型的`APP`项目,其中的调用流程相当复杂,所以前提是我们必须熟悉`AOSP`的源码,才能更加优雅的输出日志。
|
||||
安卓的ROM插桩是指在Android操作系统的固件(ROM)级别上,通过修改系统代码来实现功能扩展或行为修改。与应用程序层面的动态插桩不同,ROM插桩涉及对底层系统组件和服务进行修改,以实现更广泛、更深入的影响。
|
||||
|
||||
## 6.3 RegisterNative插桩
|
||||
由于ROM插桩需要直接修改Android操作系统的代码,因此它通常需要具备特定技术知识和足够权限才能进行。一些常见的用例包括:
|
||||
|
||||
- 修改设备启动流程。
|
||||
- 动态调整CPU频率和性能参数。
|
||||
- 添加自定义模块或驱动。
|
||||
- 实施反编译保护机制。
|
||||
- 实现App代码方法与参数跟踪。
|
||||
|
||||
以下是一个简单示例来说明如何在安卓ROM中进行代码插桩:
|
||||
|
||||
1. 首先,在你的计算机上设置好Android开发环境,并获取到目标设备所使用的ROM源码。
|
||||
|
||||
2. 找到要进行插桩操作的源码文件,在适当位置添加你想要注入执行逻辑或修改原有逻辑。
|
||||
|
||||
例如,在`frameworks/base/core/java/android/widget/Button.java`文件中找到`Button`类,并在其中添加以下代码:
|
||||
|
||||
```java
|
||||
// 插入前置逻辑
|
||||
Log.d("Button", "Before onClick()");
|
||||
|
||||
// 调用原始方法
|
||||
super.onClick(v);
|
||||
|
||||
// 插入后续逻辑
|
||||
Log.d("Button", "After onClick()");
|
||||
```
|
||||
|
||||
3. 构建并编译ROM,确保修改后的代码被正确集成到系统中。
|
||||
|
||||
4. 将编译好的新ROM安装到目标设备上,并验证插桩逻辑是否按预期生效。
|
||||
|
||||
在以上示例中,我们向`Button`类的`onClick()`方法添加了前置和后续逻辑。每当按钮被点击时,在日志输出中将显示相应的信息。
|
||||
|
||||
需要注意的是,ROM插桩操作属于底层系统级别,因此如果不了解相关技术或没有足够权限进行操作,则可能会导致设备无法正常工作甚至变砖。因此,在进行ROM插桩之前,请务必谨慎行事,并确保遵循官方文档、参考其他资源以及与经验丰富的开发者交流。
|
||||
|
||||
|
||||
## 6.3 监控Native方法注册
|
||||
|
||||
`Native`函数是指在`Android`开发中,`Java`代码调用的由`C、C++`编写的函数。`Native`函数通常用来访问底层系统资源,或进行高性能的计算操作。和普通`Java`函数不一样,`Native`函数需要通过`JNI(Java Native Interface)`进行调用。而`Native`函数能被调用到的前提是需要先进行注册,有两种方式进行注册,分别是静态注册和动态注册。
|
||||
|
||||
@ -43,6 +180,7 @@
|
||||
|
||||
下面开始了解两种注册方式的实现原理,最终在系统执行过程中找到一个共同调用处进行插桩,将所有`App`的静态注册和动态注册进行输出,打印出进行注册的目标函数名,以及注册对应的`C++`函数的偏移地址。
|
||||
|
||||
|
||||
### 6.3.1 静态注册
|
||||
|
||||
通过前文的介绍,了解到`Native`函数必须要进行注册才能被找到并调用,接下来看两个例子,展示了如何对`Native`函数进行静态注册和动态注册的。
|
||||
@ -68,13 +206,14 @@ Java_com_mik_nativecppdemo_MainActivity_stringFromJNI(
|
||||
}
|
||||
```
|
||||
|
||||
静态注册函数必须使用`JNIEXPORT`和`JNICALL`来修饰,这两个修饰符是`JNI`中的预处理器宏。其中`JNIEXPORT`会将函数名称保存到动态符号表,当`Linker`在注册时就能通过`dlsym`找到该函数。
|
||||
|
||||
`JNICALL` 宏主要用于消除不同编译器和操作系统之间的调用规则的差异。在不同的平台上,本地方法的参数传递、调用约定和名称修饰等方面可能存在一些差异。这些差异可能会导致在一个平台上编译的共享库无法在另一个平台上运行。为了解决这个问题,`JNI`规范定义了一种标准的本地方法命名方式,即 `Java_包名_类名_方法名` 的格式。使用 `JNICALL` 宏,我们可以让编译器自动根据规范来生成符合要求的本地方法名,从而保证在不同平台上都能正确调用本地方法。
|
||||
静态注册函数必须使用`JNIEXPORT`和`JNICALL`修饰符,这两个修饰符是JNI中的预处理器宏。其中,`JNIEXPORT`会将函数名称保存到动态符号表,在注册时通过`dlsym`函数找到该函数。
|
||||
|
||||
需要注意的是,虽然 `JNICALL` 可以帮助我们消除平台差异,但在某些情况下,我们仍然需要手动指定本地方法的名称,例如当我们需要使用`JNI`的反射机制来动态调用本地方法时。此时,我们需要在注册本地方法时显式地指定方法名,并将其与`Java`代码中的方法名相对应。
|
||||
`JNICALL`宏主要用于消除不同编译器和操作系统之间的调用规则差异。在不同平台上,本地方法的参数传递、调用约定和名称修饰等方面可能存在差异。这些差异可能导致在一个平台上编译的共享库无法在另一个平台上运行。为了解决这个问题,JNI规范定义了一种标准的本地方法命名方式,即"Java_包名_类名_方法名"的格式。使用`JNICALL`宏可以让编译器根据规范自动生成符合要求的本地方法名,从而确保能够正确调用本地方法。
|
||||
|
||||
对于静态注册而言,尽管没有看到使用`RegisterNative`进行注册,但是在内部有进行隐式注册的,当`java`类被加载时会调用`LoadMethod`将方法加载到虚拟机中,随后调用`LinkCode`将`Native`函数与`Java`函数进行链接。下面看`LoadClass`的相关代码。
|
||||
需要注意的是,尽管J`NICALL`可以帮助我们消除平台差异,在某些情况下仍然需要手动指定本地方法名称。例如当我们需要使用JNI反射机制来动态调用本地方法时。此时,我们需要显式指定注册本地方法时所需绑定到Java代码中相应方法上去。
|
||||
|
||||
对于静态注册而言,并未看到直接使用`RegisterNative`进行注册操作, 但实际内部已经进行了隐式注册。具体来说,当Java类被加载时,会调用`LoadMethod`将方法加载到虚拟机中,并随后通过`LinkCode`将Native函数与Java函数进行链接。下面是相关代码片段:
|
||||
|
||||
```c++
|
||||
void ClassLinker::LoadClass(Thread* self,
|
||||
@ -207,6 +346,7 @@ extern "C" const void* artFindNativeMethodRunnable(Thread* self)
|
||||
|
||||
`FindCodeForNativeMethod`执行到内部最后是通过`dlsym`查找符号,并且成功在这里看到了前文所说的隐式调用的`RegisterNative`。
|
||||
|
||||
|
||||
### 6.3.2 动态注册
|
||||
|
||||
动态注册一般是写代码手动注册,将指定的符号名与对应的函数地址进行关联,在`AOSP`源码中`Native`函数大部分都是使用动态注册方式的,动态注册例子如下。
|
||||
@ -324,6 +464,7 @@ const void* ClassLinker::RegisterNative(
|
||||
|
||||
分析到这里,就已经明白两个目标需求如何实现了:` ClassLinker::RegisterNative`是静态注册和动态注册执行流程中的共同点,该函数的返回值就是`Native`函数的入口地址。接下来可以开始进行插桩输出了。
|
||||
|
||||
|
||||
### 6.3.3 RegisterNative实现插桩
|
||||
|
||||
前文简单介绍`ROM`插桩其实就是输出日志,找到了合适的时机,以及要输出的内容,最后就是输出日志即可。在函数`ClassLinker::RegisterNative`调用结束时插入日志输出如下
|
||||
@ -411,18 +552,20 @@ mik.nativedem: mikrom Library found address: 0x7a62102000
|
||||
mik.nativedem: mikrom ClassLinker::RegisterNative java.lang.String cn.mik.nativedemo.MainActivity.stringFromJNI() native_ptr:0x7a621106e8 method_idx:0x277 offset:0xe6e8
|
||||
```
|
||||
|
||||
|
||||
## 6.4 自定义系统服务
|
||||
|
||||
自定义系统服务是指在操作系统中创建自己的服务,以便在需要时可以使用它。系统服务可以在系统启动时自动运行且没有`UI`界面,在后台执行某些特定任务或提供某些功能。由于系统服务有着`system`身份的权限,所以自定义系统服务可以用于各种用途。例如如下:
|
||||
自定义系统服务是指在操作系统中创建自己的服务,以便在需要时可以使用它。系统服务可以在系统启动时自动运行且没有UI界面,在后台执行某些特定任务或提供某些功能。由于系统服务有着`system`身份的权限,所以自定义系统服务可以用于各种用途。
|
||||
|
||||
1. 系统监控与管理:通过定期收集和分析系统数据,自动化报警和管理,保证系统稳定性和安全性;
|
||||
2. 自动化部署和升级:通过编写脚本和程序实现自动化部署和升级软件,简化人工干预过程;
|
||||
3. 数据备份与恢复:通过编写脚本和程序实现数据备份和恢复,保证数据安全性和连续性;
|
||||
4. 后台任务处理:例如定时清理缓存、定时更新索引等任务,减轻人工干预压力,提高系统效率。
|
||||
以下是一些例子:
|
||||
1. **系统监控与管理**:通过定期收集和分析系统数据,自动化报警和管理,保证系统稳定性和安全性;
|
||||
2. **自动化部署和升级**:通过编写脚本和程序实现软件的自动化部署和升级,简化人工干预过程;
|
||||
3. **数据备份与恢复**:通过编写脚本和程序实现数据备份和恢复,保证数据安全性和连续性;
|
||||
4. **后台任务处理**:例如定时清理缓存、定时更新索引等任务,减轻人工干预压力,并提高系统效率。
|
||||
|
||||
在第三章简单介绍过系统服务的启动,添加一个自定义的系统服务可以参考`AOSP`源码中的添加方式来逐步完成。接下来参考源码来添加一个最简单的系统服务`MIK_SERVICE`。
|
||||
第三章已经简单介绍了如何启动一个系统服务。要添加一个新的自定义系统服务,请参考 AOSP 源码中的添加方式来逐步完成。接下来我们将参考源码来添加一个最简单的名为 `MIK_SERVICE` 的自定义系统服务。
|
||||
|
||||
首先在文件`frameworks/base/core/java/android/content/Context.java`看到了定义了各种系统服务的名称,在这里参考`POWER_SERVICE`服务的添加,在这个服务的下面添加自定义的服务,同时找到该文件中,其他对`POWER_SERVICE`进行处理的地方,将自定义的服务做同样的处理,相关代码如下
|
||||
首先,在文件`frameworks/base/core/java/android/content/Context.java`中可以找到定义了各种系统服务的名称。在这里,我们将参考`POWER_SERVICE`服务的添加方式,在其下面添加自定义的服务。同时,在该文件中寻找其他处理`POWER_SERVICE`的代码段,并将自定义的服务同样进行处理。以下是相关代码示例:
|
||||
|
||||
```java
|
||||
public abstract class Context {
|
||||
@ -471,7 +614,7 @@ public final class SystemServiceRegistry {
|
||||
}
|
||||
```
|
||||
|
||||
`PowerManager`的功能中用到了`THERMAL_SERVICE`系统服务,所以这里不必完全照搬,省略掉这个参数即可。接下来发现注册时用到的`IMikRomManager`、`MikRomManager`并不存在,所以继续参考`PowerManager`的实现,先寻找`IPowerManager`在哪里定义的,通过搜索,发现该接口在文件`frameworks/base/core/java/android/os/IPowerManager.aidl`中。在同目录下新建文件`IMikRomManager.aidl`并添加简单的接口内容如下。
|
||||
`PowerManager`的功能中用到了`THERMAL_SERVICE`系统服务,所以这里不必完全照搬,省略掉这个参数即可。接下来发现注册时用到的`IMikRomManager`、`MikRomManager`并不存在,所以继续参考`PowerManager`的实现,先寻找`IPowerManager`在哪里定义的,通过搜索,发现该接口在文件`frameworks/base/core/java/android/os/IPowerManager.aidl`中。在同目录下新建文件`IMikRomManager.aidl`并添加简单的接口内容如下。
|
||||
|
||||
```java
|
||||
package android.os;
|
||||
@ -589,7 +732,7 @@ frameworks/base/core/java/android/os/MikRomManager.java:16: error: Missing nulla
|
||||
ability]
|
||||
```
|
||||
|
||||
这是由于`Android 11`以后谷歌强制开启`lint`检查来提高应用程序的质量和稳定性。`Lint`检查是`Android Studio`中的一个静态分析工具,用于检测代码中可能存在的潜在问题和错误。它可以帮助开发人员找到并修复代码中的`bug`、性能问题、安全漏洞等。可以设置让其忽略掉对这个`android.os`目录的检查,修改文件`framewoks/base/Android.bp`文件如下。
|
||||
这是由于`Android` 11以后谷歌强制开启`lint`检查来提高应用程序的质量和稳定性。`Lint`检查是`Android Studio`中的一个静态分析工具,用于检测代码中可能存在的潜在问题和错误。它可以帮助开发人员找到并修复代码中的`bug`、性能问题、安全漏洞等。可以设置让其忽略掉对这个`android.os`目录的检查,修改文件`framewoks/base/Android.bp`文件如下。
|
||||
|
||||
```
|
||||
metalava_framework_docs_args = "--manifest $(location core/res/AndroidManifest.xml) " +
|
||||
@ -773,12 +916,14 @@ public class MainActivity extends AppCompatActivity {
|
||||
cn.mik.myservicedemo I/MainActivity: msg: hello mikrom service
|
||||
```
|
||||
|
||||
## 6.5 APP权限
|
||||
|
||||
## 6.5 APP权限修改
|
||||
|
||||
`Android`中的权限是指应用程序访问设备功能和用户数据所需的授权。在`Android`系统中,所有的应用程序都必须声明其需要的权限,以便在安装时就向用户展示,并且在运行时需要获取相应的授权才能使用。
|
||||
|
||||
这一节将介绍`APP`的权限,以及在源码中是如何加载`AndroidManifest.xml`文件获取到权限,最后尝试在加载流程中进行修改,让`App`默认具有一些权限,无需`APP`进行申请。
|
||||
|
||||
|
||||
### 6.5.1 APP权限介绍
|
||||
|
||||
`Android`系统将权限分为普通权限和危险权限两类,其中危险权限需要用户明确授权才能使用,而普通权限则不需要。普通权限通常不涉及到用户隐私和设备安全问题,例如访问网络、读取手机状态等。而危险权限则可能会涉及到用户隐私和设备安全问题,例如读取联系人信息、访问摄像头等。
|
||||
@ -807,6 +952,7 @@ cn.mik.myservicedemo I/MainActivity: msg: hello mikrom service
|
||||
|
||||
`androidManifest.xml`文件是`Android`应用程序的清单文件,它在应用程序安装和运行过程中都会被解析。`Android`系统启动时也会解析每个已安装应用程序的清单文件,以了解应用程序所需的权限、组件等信息,并将这些信息记录在系统中。而这项解析工作是由`PackageManagerService`系统服务来完成的。开始分析的入手点可以从该系统服务的启动开始。
|
||||
|
||||
|
||||
### 6.5.2 权限解析源码跟踪
|
||||
|
||||
`PackageManagerService`系统服务的启动也是在`SystemServer`进程中,所以在`SystemServer.java`中搜索就能该进程启动的入口,相关代码如下。
|
||||
@ -1351,23 +1497,29 @@ private ParseResult<ParsingPackage> parseBaseApplication(ParseInput input,
|
||||
|
||||
理解源码中的实现原理后,使用各种方式都能完成修改`APP`权限,由此可见,阅读跟踪源码观察实现原理是非常重要的手段。
|
||||
|
||||
|
||||
## 6.6 进程注入
|
||||
|
||||
在上一小节中,通过对加载解析`xml`文件的流程进行分析,最终找到了合适的时机对默认权限进行修改,而在第三章的学习中,详细介绍了一个`APP`运行起来的流程,当对源码的运行流程有了足够的了解后,同样可以在其中找到合适的时机对普通用户的`APP`进行一些定制化的处理,例如对该进程进行注入,这一小节将介绍如何为用户进程注入`jar`包。
|
||||
在上一小节中,我们通过分析加载解析`xml`文件的流程,最终找到了一个合适的时机来修改默认权限。同时,在第三章中详细介绍了一个应用程序运行起来的流程。当对源码的运行流程有足够的了解后,同样可以在其中找到合适的时机对普通用户的应用程序进行定制化处理,例如注入`jar`包。
|
||||
|
||||
本节将介绍如何为用户进程注入`jar`包。
|
||||
|
||||
|
||||
### 6.6.1 注入时机的选择
|
||||
|
||||
`ActivityThread`负责管理应用程序的主线程以及所有活动`Activity`的生命周期。通过`MessageQueue`和`Handler`机制与其他线程进行通信,处理来自系统和应用程序的各种消息。
|
||||
`ActivityThread`负责管理应用程序的主线程以及所有活动`Activity`的生命周期。它通过`MessageQueue`和`Handler`机制与其他线程进行通信,处理来自系统和应用程序的各种消息。
|
||||
|
||||
在应用程序启动时,`ActivityThread`会被创建并开始运行,它会负责创建应用程序的主线程,并调用`Application`对象的`onCreate`方法初始化应用程序。同时,`ActivityThread`还会负责加载和启动应用程序中的第一个`Activity`,即启动界面`Splash Screen`或者主界面`Main Activity`,并处理`Activity`的生命周期事件,如`onCreate()、onResume()、onPause()`等。
|
||||
在应用程序启动时,`ActivityThread`会被创建并开始运行。它负责创建应用程序的主线程,并调用`Application`对象的`onCreate()`方法初始化应用程序。同时,`ActivityThread`还会负责加载和启动应用程序中的第一个活动(即启动界面或者主界面),并处理该活动的生命周期事件,如 `onCreate()`、`onResume()`、`onPause()` 等。
|
||||
|
||||
所以可以在`ActivityThread`的调用中寻找合适的时机,那么什么叫合适的时机呢,可以将注入的需求进行整理,然后所有符合条件的调用时机都可以算作合适的时机。
|
||||
因此,在调用`ActivityThread`中寻找合适的时机可以满足我们注入代码的需求。那么什么是合适的时机呢?我们可以将注入需求整理一下,并找到所有符合条件的调用时机。
|
||||
|
||||
注入时机尽量在一个仅调用一次的函数中,避免多次注入出现不可预料的异常情况。
|
||||
为了避免出现不可预料的异常情况,最好选择一个只会被调用一次的函数作为注入时机。
|
||||
|
||||
根据执行顺序分为早期和晚期两个阶段。早期表示在整个调用链尽量靠前位置完成注入代码,在进程业务代码开始执行之前完成注入操作。但过早地进行注入可能导致某些需要使用到数据未准备就绪,比如尚未创建完毕的`Application`对象。如果你的注入代码不需要依赖这些数据,那么可以选择尽早的时机,比如在`Zygote`进程孵化阶段。
|
||||
|
||||
第三章中介绍的`handleBindApplication`是一个相对合适的注入时机。在主线程中调用该方法来绑定应用程序,在此方法中创建了`Application`对象,并调用了其`attachBaseContext()`和`onCreate()`方法进行初始化。因此,在创建完 `Application`对象后,就可以进行自己的注入操作,包括注入自定义的JAR包和动态库(`.so` 文件)。
|
||||
|
||||
注入时机分为早期和晚期,早期表示在一个调用链尽量靠前时机,这时进程的业务代码还没开始执行,就完成注入代码了,但是过早的时机会导致有些需要用到的数据还未准备就绪,例如`Application`未完成创建。如果你注入的代码无需涉及这些数据,那么可以选择尽量早的时机。例如在`Zygote`进程孵化的时机也是可以的。
|
||||
|
||||
第三章中介绍到的`handleBindApplication`就是比较合适的注入时机,主线程中通过调用这个方法来绑定应用程序,在该方法中创建了`Application`对象,并且调用了`attachBaseContext`方法和`onCreate`方法进行初始化。可以选择在创建`Application`对象后,就注入自己的`jar`包和`so`动态库。
|
||||
|
||||
### 6.6.2 注入jar包
|
||||
|
||||
@ -1423,11 +1575,8 @@ public class MyCommon {
|
||||
|
||||
```
|
||||
unzip app-debug.apk -d app-debug
|
||||
|
||||
dx --dex --min-sdk-version=26 --output=./kjar.jar ./app-debug/classes.dex
|
||||
|
||||
cp ./kjar.jar ~/android_src/aosp12/framewoorks/native/myjar/
|
||||
|
||||
```
|
||||
|
||||
最后添加注入代码如下。
|
||||
@ -1477,3 +1626,12 @@ private void handleBindApplication(AppBindData data) {
|
||||
编译并刷入手机中,安装任意`app`后,都会注入该`jar`包并打印日志。
|
||||
|
||||
注入`so`动态库同样和内置`jar`的步骤没有任何区别,直接通过在`jar`包中加载动态库即可,无需另外添加代码。这里不再展开讲诉。
|
||||
|
||||
|
||||
## 6.7 本章小结
|
||||
|
||||
本章主要介绍了在功能定制过程中使用的一些插桩技术,其中静态插桩是一种古老且广泛应用的方法。
|
||||
|
||||
早在十年前,笔者研究安卓软件安全时,就特别关注类似Apktool这样的反编译工具以及相关的Smali文件插桩。那个时代移动安全还没有兴起,各种安全对抗技术尚未出现,静态插桩在那时是最常用的代码逻辑修改方式。例如后来出现的MIUI系统,在早期也采用了静态插桩来定制AOSP。技术并不存在好坏先进与否之分,它们都是时代所产生的产物。包括新技术的出现,也总是经历由简入繁、低维向高维升级等过程。
|
||||
|
||||
然而,在当前复杂的App程序结构和不断升级的安全对抗环境下,仅依靠静态插桩实现功能增强已经有些力不从心了。动态插桩成为目前主要应用于此领域中最常见手段,其中`Frida`在2017年崭露头角,并成为目前使用最广泛的动态插桩工具之一。除了在安全分析中进行动态插桩外,`Frida`还允许分析人员和开发人员通过静态插桩方式,将`Frida`核心逻辑注入到App或系统中,并在运行时启动其动态插桩功能。这也是在实际的开发定制过程当中,动静相结合的完美体现。
|
||||
|
Loading…
x
Reference in New Issue
Block a user