android 逆向工程(待续)

工具类

IDA

Segment registers(shift+F8)

当IDA遇到改变段寄存器的指令时,它会创建一个段寄存器变化点。所以,大多数变化点都由IDA自己维护。IDA假设段寄存器在变化点之间不会改变其值。如果您发现IDA无法找到段寄存器的变化,或者想要更改寄存器的值,则可以使用Change Segment Register命令创建一个变化点。您也可以使用Set default segment register value命令更改段寄存器的值。
IDA对变化点进行分类。在变化点列表中,您可以看到在寄存器值后面的以下后缀:
a(自动)-由IDA创建。之后可能会由IDA更改。
u(由用户)-由用户创建。IDA不会更改它。
如果未被相应命令禁用,则IDA会为变化点生成相应的“assume”指令。

反调试

利用IO重定向来绕过反调试

IO重定向是一种在操作系统级别上修改输入输出流向的技术。在某些情况下,它可以用来绕过反调试措施。反调试技术通常用于检测和阻止调试器或其他分析工具的接入,以保护软件不被篡改或逆向工程。以下是一些基本的步骤和技巧,用于利用IO重定向来绕过反调试:
启用IO重定向:在一些系统中,你可以通过修改系统的IO端口或使用特定的指令来启用IO重定向。这通常涉及到向 IO_BASE 寄存器写入一个特定的值,以启用重定向。
修改内存映射:修改内存映射是一种常见的技术,用于将内存中的某些区域映射到其他区域。这可以通过修改 GOT(全局偏移表)或 PLT(程序链接表)来实现。
使用原始指令:在一些情况下,原始指令(如 NOP 指令)可以用来绕过反调试检查,因为它们不修改任何寄存器或内存状态。
利用代码覆盖:通过覆盖特定的代码区域,可以使得反调试器认为程序正在执行正常的代码路径,从而绕过防护。
修改调试信息:修改程序的调试信息,如函数名、变量名等,可以使得反调试器难以识别程序的结构,从而降低逆向工程的效果。
使用第三方工具:有些第三方工具,如 Frida、DBI 等,可以用来动态修改程序的行为,从而绕过反调试防护。

EBPF

使用eBPF完成安卓App hook

使用 eBPF (Extended Berkeley Packet Filter) 完成安卓 App 的 hook 是一个高级的逆向工程任务,它通常需要对 Linux 内核、eBPF 编程以及安卓系统的内部工作原理有深入的了解。以下是一个简化的步骤指南,用于在安卓应用中实现 eBPF hook:
设置 eBPF 环境:
确保你的开发环境支持 eBPF,例如使用最新的 Linux 内核。
安装必要的工具,如 clang、llvm、bpftool 等。
编写 eBPF 程序:
使用 eBPF 语言编写你的 hook 程序,它将运行在安卓设备的内核空间。
编写程序以过滤和处理你感兴趣的事件,例如系统调用、网络流量等。
编译 eBPF 程序:
使用 clang 编译你的 eBPF 程序为二进制格式。
安装 eBPF 程序到安卓设备:
使用 adb 或其他工具将编译后的 eBPF 二进制文件安装到安卓设备上。
确保你有权限在设备上安装内核模块。
加载 eBPF 程序:
使用 bpftool 或系统调用将 eBPF 程序加载到内核中。
配置 eBPF 程序的参数,如挂钩点、过滤条件等。
测试和验证:
运行你的安卓应用,并验证 eBPF 程序是否按预期工作。
调试和调整 eBPF 程序,直到它能够稳定地提供你想要的数据。
持久化 eBPF 程序:
如果需要,将 eBPF 程序打包到安卓系统的启动脚本中,使其在设备启动时自动加载。
请注意,这个过程涉及到对系统内核的修改,可能会对设备的保修状态产生影响,并且在某些情况下可能违反软件的服务条款。在进行这些操作之前,请确保你有足够的权限,并且对你的行为负责。此外,由于安卓系统的复杂性和安全性,eBPF hook 的实现可能需要特定的知识和技能,因此在实际应用中可能需要专业的逆向工程师来完成。

以下是一个简单的示例,展示了如何使用eBPF在安卓应用中插入代码:
首先,创建一个名为hook_example.c的C文件,内容如下:

#include <uapi/linux/ptrace.h>
#include <linux/sched.h>
#include <bcc/proto.h>

int count = 0;

int trace_syscalls(struct pt_regs *ctx, struct kernel_syscall_args *) {
    count++;
    return 0;
}

char __license[] __section("license") = "GPL";

编译这个C文件为eBPF程序:
bash

$ clang -target bpf -O2 -Wall -Werror -c hook_example.c -o hook_example.o
$ llc -march=bpf -filetype=obj -o hook_example.o.bc hook_example.o

将生成的.o.bc文件转换为eBPF字节码:

$ sudo bpftool c-to-bpf hook_example.o.bc

加载并运行eBPF程序:

$ sudo bpftool prog load hook_example.o
$ sudo bpftool map create count_map pinned /sys/fs/bpf/count_map type long
$ sudo bpftool map dump count_map

在你的安卓应用中,使用JNI调用上述命令来加载和运行eBPF程序。例如,你可以在你的Java代码中添加以下方法:

public native void runEbpf();

static {
    
    
    System.loadLibrary("your_native_library");
}

然后在你的C或C++代码中实现这个方法:

#include <jni.h>
#include "your_header_file.h"

JNIEXPORT void JNICALL Java_your_package_name_YourClassName_runEbpf(JNIEnv *env, jobject obj) {
    // 在这里调用上述命令来加载和运行eBPF程序
}

这样,每当你的安卓应用调用runEbpf()方法时,eBPF程序就会被加载并运行,从而实现对安卓应用的hook。

xHook

xHook 是一个用于Android原生ELF(可执行文件和共享库)的PLT(过程链接表)钩子库。它具有以下特点:
支持Android 4.0至10(API级别14至29)。
支持armeabi、armeabi-v7a、arm64-v8a、x86和x86_64。
支持ELF HASH和GNU HASH索引的符号。
支持SLEB128编码的相对定位信息。
支持通过正则表达式设置钩子信息。
不需要root权限或任何系统权限。
不依赖任何第三方共享库。
xHook的功能包括:
注册钩子信息:通过xhookregister函数,可以注册需要钩子的ELF文件和符号。
忽略钩子信息:通过xhookignore函数,可以忽略某些钩子信息。
执行钩子操作:通过xhookrefresh函数,可以实际执行钩子操作。
清除缓存:通过xhookclear函数,可以清除xHook持有的所有缓存,将所有全局标志重置为默认值。
启用/禁用调试信息:通过xhookenabledebug函数,可以启用或禁用调试信息。
启用/禁用SFP(段错误保护):通过xhookenablesigsegvprotection函数,可以启用或禁用SFP。
xHook的使用案例主要包括:
修改ELF文件中的函数实现:可以通过钩子机制,在加载的ELF文件中替换原始函数实现。
忽略特定ELF文件的钩子:可以根据正则表达式忽略特定ELF文件的钩子。
总之,xHook是一个在Android原生ELF文件上实现钩子操作的库,可以用于修改、忽略或执行特定ELF文件的钩子。它具有较高的兼容性和稳定性,适用于不同版本的Android系统和多种架构。

UNIDBG

补环境

Unidbg 是一个强大的逆向调试工具,它支持多种调试技术和插件,可以模拟执行 Android 和 iOS 平台上的应用程序。为了能够正确地调试和分析应用程序,Unidbg 需要一个完整的补环境(bootstrap environment),这个环境包括了 JNI 补环境、文件访问补环境以及补系统调用和库函数。以下是对这些补环境的详细介绍:
JNI 补环境: JNI(Java Native Interface)是 Java 用于与本地代码(如 C/C++)交互的接口。在逆向 Android 应用程序时,如果应用程序使用了 JNI 调用本地库,Unidbg 需要一个 JNI 补环境来模拟这些调用。这个补环境包括了 JNI 函数的声明和实现,它们可以被 Unidbg 用来拦截和模拟 JNI 调用。
文件访问补环境: 应用程序可能需要访问文件系统来读取或写入文件。Unidbg 的文件访问补环境允许模拟这些文件操作,包括文件路径、权限和文件内容。这样,即使在模拟环境中,应用程序也能够像在真实设备上一样进行文件访问,从而保证调试的连贯性。
补系统调用和库函数: 系统调用和库函数是操作系统和应用程序交互的接口。Unidbg 提供了补系统调用和库函数的功能,这允许它模拟系统调用和库函数的行为。这对于调试那些依赖于特定操作系统功能的应用程序至关重要,如权限检查、进程管理和硬件访问等。
为了创建这些补环境,通常需要编写相应的脚本和插件。Unidbg 社区提供了许多现成的补环境脚本和插件,这些可以被下载和使用。用户也可以根据自己的需要定制和扩展这些补环境,以支持特定的应用程序或平台。
使用 Unidbg 的补环境时,用户可以加载和执行应用程序的 SO 文件,设置断点,观察程序的执行,以及跟踪和修改程序的状态。这些功能使得 Unidbg 成为一个强大的工具,用于 Android 和 iOS 应用程序的逆向工程和调试。

文件结构

ELF

section和segment的关系

在ELF(Executable and Linkable Format)文件中,section和segment是描述文件中数据组织的两个概念。它们之间的关系可以这样理解:
Section(节):
Section是编译器创建的,用于将程序的不同部分逻辑地组织在一起。例如,.text节包含程序的代码,.data节包含初始化的数据,.bss节包含未初始化的数据等。
Section的边界在编译时是固定的,它们是目标文件(object file)中的一个连续的数据块。
Section头表(Section Header Table)包含了有关每个section的信息,如大小、位置和属性等,这些信息对于链接器(linker)来说是重要的,因为链接器需要它们来将不同的目标文件合并成一个可执行文件。
Segment(段):
Segment是加载器(loader)创建的,它们代表了程序在内存中的物理布局。每个段都有特定的访问权限,如读(Read)、写(Write)和执行(Execute)。
Segment的边界在加载时是动态的,它们可以跨越多个section。例如,代码段可能包含.text和.plt节。
Segment头表(Program Header Table)包含了有关每个segment的信息,如大小、文件偏移量、虚拟地址和物理地址等,这些信息对于加载器来说是重要的,因为加载器需要它们来将程序的各个部分映射到内存中的正确位置。
总结来说,section是编译器层面的概念,用于源代码的编译和链接,而segment是加载器层面的概念,用于程序的加载和执行。在编译过程中,编译器会生成section,然后在链接过程中,链接器会根据section信息来创建segment。加载器在程序运行时会使用segment信息来将程序的各个部分映射到内存中。

.dynamic段

.dynamic段包含了一些用于动态链接的信息。.dynamic段不是标准ELF段名,但它通常出现在可执行文件和共享库文件中,用于存储与动态链接相关的数据。.dynamic段包含了三个主要的部分:.dynsym、.dynstr和.hash。
.dynsym(动态符号表):
.dynsym段包含了动态链接时需要的符号信息。这些符号是动态符号表的一部分,用于在运行时将程序中的符号解析为相应的函数和变量。
动态符号表中的条目通常包括符号的名字、类型、属性(如是否为外部符号)、大小和位置等信息。
.dynstr(动态字符串表):
.dynstr段包含了动态链接时需要的字符串字面量。这些字符串通常用于动态符号表中的符号名称和版本控制。
动态字符串表中的条目包含了字符串的内容和它们在文件中的偏移量。
.hash(动态哈希表):
.hash段包含了用于加速动态链接过程的哈希表。这个哈希表是根据符号的名称和属性来组织的,它使得查找符号的速度更快。
动态哈希表通常包括符号的哈希值、名称和偏移量等信息。
这些段落通常在程序被动态链接器处理时会用到。当程序被加载时,动态链接器会使用.dynamic段中的信息来解析符号和字符串,并创建相应的符号表和哈希表。这使得程序能够在运行时动态地引用其他共享库中的函数和变量。
需要注意的是,这些段落的布局和内容可能会根据不同的编译器和目标系统有所不同。因此,当处理.dynamic段时,需要参考特定编译器和目标系统的文档。

.init和.init_array段

.init和.init_array段是用于初始化代码和初始化调用序列的特殊段。
.init 段
.init段包含了程序的初始化代码。这部分代码在程序启动时被调用,用于执行任何必要的初始化任务。这通常包括设置全局变量、初始化静态数据结构、注册信号处理程序等。
.init段中的代码是程序启动时执行的第一批代码之一。在Linux系统上,.init段通常会由系统初始化程序(如init)或者脚本(如startup.script)调用。
.init_array 段
.init_array段是一个特殊的节,它包含了初始化函数的数组。这些函数在程序启动时按顺序被调用。.init_array中的函数通常用于初始化共享库或者程序的特定部分。
在Linux系统上,当一个共享库被加载时,动态链接器会查找.init_array段,并按照其中的函数列表调用它们。这些函数可以用来进行全局初始化,比如设置全局变量、初始化数据结构等。
.init_array段中的函数通常以弱符号(weak symbols)的形式声明,这意味着如果同一个符号在其他的.init段或者.init_array段中被更强地定义,那么这个弱符号的初始化函数就不会被调用。
需要注意的是,.init和.init_array段是可选的,并不是所有的ELF程序都会使用它们。如果一个程序或者共享库不需要在启动时执行初始化代码,那么这些段就可以省略。
在 objdump 工具的输出中,你可以看到.init和.init_array段的内容,包括它们包含的函数和数据。这些信息对于理解程序的初始化流程和调试初始化问题时非常有用。
.init和.init_array两个section用于在so文件被加载时做初始化工作,系统在加载so文件时会先执行.init中的代码,然后再顺序执行.init_array中的各个代码块,且.init和.init_array的执行时机均早于JNI_OnLoad方法,所以这两个section的执行时间是比较早的,所以有时解密函数会放在.init或者.init_array中.

花指令

花指令5要素

花指令(Junk Code)是一种用于混淆和干扰逆向工程分析的技巧。它通过在程序中插入无用的、看似随机或者无害的代码来迷惑逆向工程师,使得他们难以理解程序的真实逻辑。花指令的有效性通常取决于以下五个要素:
复杂性:花指令应该足够复杂,以至于不会被轻易识别出其真实的意图。这通常意味着它们包含了一些不常见的指令或者特殊的编码模式。
随机性:花指令通常看起来像是随机生成的,没有明显的模式或者逻辑。这种随机性可以防止逆向工程师通过统计分析来识别代码的模式。
误导性:花指令可能会模仿有效的指令,但是执行时并不会产生预期的效果,或者它们可能会在某些条件下执行,从而误导逆向工程师认为它们是有用的代码。
变化性:花指令可能会根据不同的条件或者执行阶段发生变化,使得同一代码在不同情况下有不同的表现,这增加了逆向分析的难度。
叠加性:花指令可能会与程序中的其他代码片段叠加,形成一个新的代码序列,这个新的序列可能会对逆向工程师造成额外的混淆。
为了提高花指令的效果,通常会结合使用多种技巧,比如在代码中插入特殊的字符串或者模式,使用复杂的跳转逻辑,或者在不同的执行上下文中插入不同的代码片段。逆向工程师在分析花指令时,需要具备深厚的汇编语言和程序设计知识,以便能够识别和理解这些复杂的干扰模式。

Smali

基础

指令:Smali 包含了一系列指令,用于表示程序的控制流、操作数和返回值。例如,nop 用于无操作,move-result 用于将操作结果移动到指定变量。
局部变量:Smali 允许定义局部变量,并使用 . 操作符来访问它们。局部变量通常以数字索引的方式进行引用。
常量池:Smali 中的常量池用于存储字符串、数字和其他常量。常量池索引通常以 # 开头。
方法调用:Smali 中的方法调用使用 invoke 指令。调用可以是有返回值的,也可以是虚拟调用,还可以是静态调用。
控制流:Smali 支持条件分支和循环控制流。条件分支使用 if-eq、if-ne 等指令,循环使用 goto 和 if 指令。
异常处理:Smali 提供了异常处理的机制,使用 try、catch 和 finally 指令来捕获和处理异常。

打包

重打包技巧通常涉及到对 Smali 代码的修改,以实现特定的目的,比如修改应用程序的行为、移除权限要求或者绕过某些安全检查。以下是一些重打包技巧:
修改方法签名:通过改变方法的签名,可以创建一个新的方法,该方法与原始方法具有相同的名称,但参数列表不同。
修改常量池:通过修改常量池中的内容,可以改变代码中的字符串和数字值。
添加或删除指令:通过添加或删除 Smali 指令,可以改变程序的控制流和行为。
修改局部变量和参数:通过修改局部变量和参数的索引,可以改变变量和参数的名称和作用。
使用 Smali 混淆:通过使用 Smali 混淆技术,可以使得代码更难以阅读和理解。
在进行重打包时,需要谨慎操作,因为不当的修改可能导致应用程序崩溃或行为异常。此外,重打包可能会违反软件许可协议,因此在进行此类操作之前,请确保您有权修改和使用该软件。

猜你喜欢

转载自blog.csdn.net/yangzex/article/details/134663732