GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

软件逆向 --- 修改安卓so

要想修改so是一件非常困难的事情,目前没有类似工具,下面只是实现思路,具体实现方法正在尝试。一个可靠的方法等价替换,我们将所有函数的是输出和输入进行模拟可以实现替换原有so文件

方法一:十六进制编辑(直接字节修改法)

这是最直接但也是最危险的方法。它相当于用记事本打开一个程序,直接修改它的机器码。

  • 适用场景:

    • 进行极微小的修改,例如将一个条件跳转指令反转(如 BNE 改为 BEQ)。

    • 将某些指令“抹掉”(用 NOP 指令覆盖)。

    • 你已经精确知道要修改的字节和替换后的字节。

  • 优点: 快速、直接,不需要复杂的工具链。

  • 缺点:

    • 极易出错,一个字节的错误就可能导致整个程序崩溃。

    • 无法增加或减少代码,因为这会破坏文件中所有的地址引用。

    • 需要你手动计算或查询指令对应的机器码(Opcode)。

  • 所需工具:

    • 反汇编器: IDA Pro, Ghidra, Cutter/Radare2 (用于定位代码)。

    • 十六进制编辑器: HxD (Windows), 010 Editor (跨平台), Hex Fiend (macOS)。

操作步骤:

  1. 定位代码:

    • 使用 IDA Pro 或 Ghidra 打开你的 .so 文件。

    • 在反汇编视图中,找到你想要修改的函数和指令。

    • 记下这条指令的 虚拟地址(Virtual Address, VA)。例如,地址是 00008D54

    <p align="center">图:在 IDA Pro 中找到指令地址</p>

  2. 计算文件偏移(File Offset):

    • 程序在内存中的地址(VA)和在文件中的地址(Offset)通常是不同的。你需要将 VA 转换成文件偏移量才能在 Hex 编辑器中定位。

    • 最简单的方法是在 IDA Pro 的 "Hex View" 窗口中,右键点击 VA 地址,选择 "Jump to file offset",IDA 会自动帮你跳转。

    • 手动计算公式通常是:File Offset = VA - ImageBase - SectionVA + SectionRawOffset。对于初学者,使用工具自动转换更可靠。

  3. 获取原始和目标机器码:

    • 在 IDA Pro 中,切换到 Hex View,你可以看到 00008D54 地址对应的机器码字节。假设你想修改的指令是 CMP R0, #1,它的机器码可能是 38 28

    • 假设你想把它改成 MOV R0, #1,你需要知道这条新指令的机器码。你可以通过 IDA 的汇编功能(Edit -> Patch program -> Assemble)输入新指令,IDA 会告诉你对应的机器码,或者使用在线 ARM 转换工具。假设 MOV R0, #1 的机器码是 01 20

  4. 执行修改:

    • 用 HxD 或其他十六进制编辑器打开 .so 文件。

    • 跳转到你计算出的文件偏移量。

    • 你会看到原始的字节序列(例如 38 28)。

    • 直接用键盘输入新的字节(01 20)来覆盖它。

    • 保存文件。

  5. 测试:

    • 将修改后的 .so 文件放回 APK,重新打包、签名,然后安装测试。

方法二:使用 IDA/Ghidra 直接 Patch(推荐方法)

这是最常用且相对安全的方法,利用强大工具的内置功能来修改汇编代码。

  • 适用场景:

    • 修改现有指令。

    • 进行小范围的代码逻辑变更。

    • 比方法一更安全,因为工具会帮你处理机器码转换。

  • 优点:

    • 所见即所得,直接在汇编代码上修改。

    • 工具自动完成汇编到机器码的转换。

    • 不易犯低级错误。

  • 缺点:

    • 同样不适合增加或删除代码,本质上还是原地替换。

  • 所需工具:

    • IDA Pro (带有 Hex-Rays 反编译器更佳)

    • Ghidra (免费且强大的替代品)

操作步骤 (以 IDA Pro 为例):

  1. 定位代码:

    • 用 IDA Pro 打开 .so 文件,等待分析完成。

    • 导航到你想要修改的汇编指令。

  2. 进行 Patch:

    • 将光标定位到要修改的指令行。

    • 点击菜单 Edit -> Patch program -> Assemble

    • 会弹出一个对话框,里面显示了当前的汇编指令。

    • 在对话框中输入你的新汇编指令,例如将 BNE loc_12345 修改为 BEQ loc_12345 或者 NOP

    • 点击 "OK",IDA 会自动用新指令的机器码替换旧的。被修改的代码行会以不同颜色高亮显示。

    <p align="center">图:使用 IDA 的 Assemble 功能进行 Patch</p>

  3. 保存修改:

    • 仅仅在 IDA 数据库里修改是不够的,你需要将更改应用到原始文件。

    • 点击菜单 Edit -> Patch program -> Apply patches to input file...

    • 选择创建备份或直接覆盖原文件。

    • 保存后,你的 .so 文件就已经被修改了。

  4. 测试:

    • 将修改后的 .so 文件放回 APK,重新打包、签名并测试。

方法三:代码注入(Code Cave / 代码洞注入法)

当你需要增加代码时,上述方法就不再适用。你需要找到一块“空地”来存放你的新代码,然后让原始代码跳转过来执行。

  • 适用场景:

    • 需要增加新的功能逻辑,而不仅仅是修改。

    • 原始函数空间太小,无法在原地完成修改。

  • 优点:

    • 可以实现复杂逻辑的添加。

  • 缺点:

    • 技术上最复杂,需要对 ELF 文件格式和 ARM 汇编有深入理解。

    • 寻找和利用代码洞需要技巧。

  • 所需工具:

    • IDA Pro / Ghidra

    • ARM 汇编器 (如 keystone-engine)

    • 十六进制编辑器

操作步骤:

  1. 寻找代码洞 (Code Cave):

    • 代码洞是文件中一块连续的、未被使用的、且有执行权限的区域。通常是一长串 00 字节。

    • 在 IDA 中,你可以滚动寻找 .text 段末尾或其他段中的大片 00 区域。记下这个洞的起始地址。

  2. 编写并汇编你的新代码 (Payload):

    • 用 ARM 汇编写出你想要添加的功能。例如,一段打印日志或修改寄存器值的代码。

    • 关键: 在你的新代码执行完毕后,必须包含两部分: a. 执行被你覆盖掉的原始指令。 b. 一个无条件跳转,跳回到原始代码中被覆盖指令的下一条指令,以确保程序能继续正常执行。

    • 使用 keystone 这样的库或在线工具,将你的汇编代码转换成十六进制机器码。

  3. 注入 Payload:

    • 使用十六进制编辑器或 IDA 的 Patch 功能,将你生成的机器码写入之前找到的代码洞中。

  4. 创建跳转 (Hook):

    • 回到你想要修改的原始代码位置。

    • 用一条跳转指令(例如 B <代码洞地址>BL <代码洞地址>)覆盖掉原始的一条或几条指令。B 是简单跳转,BL 是带链接的跳转(会把返回地址存入 LR 寄存器),根据需要选择。

    • 注意: 你覆盖的原始指令必须被记录下来,并在你的 Payload 中重新执行。

  5. 保存与测试:

    • 保存所有修改,打包 APK 并进行严格测试。这个过程非常容易出错。

方法四:使用 Hook 框架(运行时修改法)

这是一种动态修改方法,它不直接修改磁盘上的 .so 文件,而是在程序运行时,在内存中修改代码逻辑。

  • 适用场景:

    • 开发非侵入性的 MOD 或插件。

    • 需要进行复杂的逻辑注入,甚至用 C++ 或 Java/Kotlin 来实现新逻辑。

    • 不想破坏原始 APK 的签名。

  • 优点:

    • 极其灵活和强大,可以在运行时动态开启或关闭。

    • 无需关心文件偏移、代码洞等静态修改的麻烦事。

    • 可以用高级语言编写 Hook 逻辑。

  • 缺点:

    • 通常需要 Root 权限或一个特殊的运行环境 (如 VirtualXposed)。

    • 配置和环境设置相对复杂。

  • 所需工具:

    • Frida: 当今最流行、最强大的动态插桩(Hook)框架。

    • Xposed / EdXposed / LSPosed: 经典的 ART Hook 框架。

操作思路 (以 Frida 为例):

  1. 环境准备:

    • 在你的电脑上安装 Frida 工具 (pip install frida-tools)。

    • 在你的 Root 过的安卓设备或模拟器上运行 frida-server

  2. 编写 Hook 脚本 (JavaScript):

    • 你将在电脑上编写一个 JS 脚本来定义 Hook 逻辑。

    • 首先,获取目标 .so 模块的基地址。

    • 然后,使用 Interceptor.attach 来 Hook 目标函数的地址(基地址 + 函数偏移)。

    • onEnter 回调中,你可以读取或修改传入函数的参数(存放在 r0-r3 等寄存器中)。

    • onLeave 回调中,你可以读取或修改函数的返回值(存放在 r0 寄存器中)。

    • 你甚至可以用 Interceptor.replace 完全替换掉一个原生函数,用你自己的 JS 或 C 代码实现。

    // 示例 Frida 脚本
    // frida -U -f com.example.app -l hook.js
    
    Interceptor.attach(Module.findExportByName("libtarget.so", "Java_com_example_MainActivity_stringFromJNI"), {
        onEnter: function(args) {
            // args[0] 是 JNIEnv*, args[1] 是 jobject
            console.log("Hooked function entered!");
        },
        onLeave: function(retval) {
            console.log("Original return value: " + retval);
            // 修改返回值
            // retval.replace(ptr(new_string_address)); // 替换为一个新的 jstring
            console.log("Return value modified!");
        }
    });
    
  3. 执行 Hook:

    • 在电脑上运行命令,将脚本注入到目标 App 进程中。Frida 会自动处理内存的修改。

总结对比

方法

难度

适用场景

能否增/删代码?

优点

缺点

十六进制编辑

★★★☆☆

极微小修改 (NOP, 改标志位)

快速直接

极易出错,功能有限

IDA/Ghidra Patch

★★☆☆☆

修改指令、改变逻辑分支

安全、直观、常用

无法增加代码

代码洞注入

★★★★★

增加新功能、复杂逻辑

功能强大,静态永久

极其复杂,易出错

Hook 框架

★★★★☆

动态分析、插件开发、复杂注入

是 (在内存中)

极其灵活,可逆

需要 Root,环境复杂

对于初学者,强烈建议从 方法二:使用 IDA/Ghidra 直接 Patch 开始练习。当你需要增加代码时,再深入研究 方法三方法四

 

 

具体讨论一下为什么我们不能直接嵌入汇编代码

比如我在两个代码只有一个字符串不一样,几乎每个地方都会轻微发生变化,主要影响的是地址

 

 

好的,这是一个非常好的问题,它揭示了编译和链接过程中的一个核心概念:二进制文件不是一个简单的字节序列,而是一个高度结构化、内部引用错综复杂的系统。

当你仅仅修改了一个字符串,从 "GK" 变为 "GKLBB",你不仅仅是替换了几个字节,而是改变了数据段的大小。这个大小的改变(从3字节 G K \0 变为6字节 G K L B B \0)会引发一系列连锁反应,导致文件中许多看似无关的部分都发生变化。

下面我们根据你提供的截图,逐步分析这些连锁变化的原因。


1. 直接修改:数据段中的字符串内容

这是最直观的变化。

  • 文件 

    • 在地址 0x48D 附近,我们可以看到十六进制 47 4B,其右侧的ASCII表示为 .GK.。这正是字符串 "GK"。

  • 文件 

    • 在相同位置附近,我们可以看到十六进制 47 4B 4C 42 42,ASCII表示为 .GKLBB.

    • 这个变化使 .rodata (只读数据) 段的大小增加了3个字节。

这个大小的改变是后续所有连锁反应的根源。

2. 连锁反应 1:文件偏移量 (File Offsets) 的变化

ELF 文件格式包含许多指向文件内其他位置的指针,这些指针被称为“文件偏移量”。当字符串 "GK" 变长后,它之后的所有数据在文件中的位置都向后移动了3个字节(或者更多,取决于对齐)。因此,所有指向这些移动后数据的偏移量都必须由链接器重新计算和更新。

  • 证据:ELF头中的节区头表偏移量 (

    • ELF文件的头部(文件最开始的部分)包含了指向“节区头表”(Section Header Table)的偏移量。这个表描述了文件中所有段(如 .text.data 等)的位置和大小。

    • arm64-v8a架构中,e_shoff 位于文件偏移 0x28 处。

    • GK.so0x28 处的值是 40 0E 00 00 00 00 00 00,小端序表示偏移量为 0x0E40

    • GKLBB.so0x28 处的值是 60 0E 00 00 00 00 00 00,小端序表示偏移量为 0x0E60

    • 分析:节区头表的位置因为文件中间数据增多而被向后推移了 0x20 (32) 字节。这证明了链接器重新计算了整个文件布局,而不仅仅是简单地移动数据。

3. 连锁反应 2:PC相对寻址导致的代码变化

现代编译器经常使用PC相对寻址来加载数据。这意味着生成的机器码不是加载一个绝对地址,而是加载一个相对于当前指令指针(PC)的偏移量。

  • 指令: LDR W8, =0x48D (伪代码,表示加载地址0x48D处的数据)

  • 工作原理: 编译器会计算出 0x48D 相对于当前PC的距离,然后生成一条 LDR 指令来加载。

当数据段中字符串的位置发生变化时(即使只是因为前面的数据变长了),所有引用该字符串或其后数据的PC相对寻址指令都必须改变它们的立即数偏移量,这直接导致了 .text(代码)段的变化。

  • 证据:代码段的差异

    • 在地址 0x2AD 处,两边的机器码完全不同。

    • GK.soDA 43 F2 67

    • GKLBB.soEA 20 29 15

    • 分析:这很可能是一条 LDR 或 ADR 指令,用于加载一个数据段中的地址。由于数据段的布局发生了变化,目标地址的相对偏移也变了,因此生成的机器码也必须改变。

4. 连锁反应 3:哈希表和符号表的变化

为了让动态链接器能够快速查找函数和变量(符号),ELF文件包含一个哈希表(.hash 或 .gnu.hash 段)。

  • 工作原理: 哈希表是对符号字符串名称计算哈希值后构建的。

  • 连锁反应:

    1. 文件内容的任何改变,都会导致链接器可能重新排列符号表(.dynsym)。

    2. 符号表项的顺序或地址的改变,会使得整个哈希表需要被重新计算。哈希值会完全不同。

  • 证据:文件头部的哈希/符号相关区域

    • 在截图的 0x000000C7 到 0x0000010F 区域,我们可以看到大量的差异。例如:

    • GK.so at 0xC7F0 09

    • GKLBB.so at 0xC7E0 09

    • 分析:这些字节是动态链接信息的一部分,包含了符号表、字符串表和哈希表的各种偏移和索引。布局的微小变化导致了这些元数据的全面更新。

5. 连锁反应 4:调试和异常处理信息的变化

ARM64使用DWARF格式将调试和栈回溯(unwinding)信息存储在 .eh_frame 和 .eh_frame_hdr 段中。这些信息与代码的地址和栈操作指令紧密耦合。

  • 连锁反应:

    • 当代码段因为PC相对寻址等原因发生改变时,哪怕只是一条指令,函数的起始/结束地址、栈指针的修改方式等都可能变化。

    • 因此,链接器必须重新生成几乎全部的DWARF调试和异常处理信息。这部分数据非常敏感,微小的代码变动都可能导致其内容面目全非。

  • 证据:文件后半部分的大量差异

    • 观察截图的后半部分,例如从 0x5CD 开始的区域。

    • GK.so9D 42 F9 40 1F D6 ...

    • GKLBB.so81 F6 FF 10 02 1F ...

    • 分析:这部分区域的差异非常大,几乎没有相同的字节。这正是 .eh_frame 段,它的内容被完全重写了,以匹配新的代码布局。

总结

将一个二进制文件想象成一张由线连接起来的网。字符串是网上的一个节点,而文件头、偏移量、指针、哈希表、调试信息等都是连接节点的线。

当你把 "GK" 换成 "GKLBB" 时,你不仅替换了节点,还拉长了它。这张网为了保持自身的结构和正确性,链接器必须重新调整几乎所有连接线的长度和位置,甚至重新编织一大部分网。

这就是为什么一个看似微不足道的字符串长度变化,会导致整个二进制文件发生广泛而深刻的改变。

 

 

 

posted on 2025-07-17 01:13  GKLBB  阅读(509)  评论(0)    收藏  举报