65

无论如何这可以做到吗?我使用了 objdump ,但这不会产生我所知道的任何汇编程序都可以接受的汇编输出。我希望能够更改可执行文件中的指令,然后再对其进行测试。

4

8 回答 8

31

我认为没有任何可靠的方法可以做到这一点。机器码格式非常复杂,比汇编文件更复杂。实际上不可能获取已编译的二进制文件(例如,ELF 格式)并生成将编译为相同(或足够相似)二进制文件的源汇编程序。要了解这些差异,请将 GCC 直接编译到汇编器 ( gcc -S) 的输出与可执行文件 ( ) 上的 objdump 的输出进行比较objdump -D

我能想到两个主要的并发症。首先,由于指针偏移等原因,机器代码本身与汇编代码不是一一对应的。

例如,考虑 Hello world 的 C 代码:

int main()
{
    printf("Hello, world!\n");
    return 0;
}

这将编译为 x86 汇编代码:

.LC0:
    .string "hello"
    .text
<snip>
    movl    $.LC0, %eax
    movl    %eax, (%esp)
    call    printf

其中 .LCO 是命名常量,而 printf 是共享库符号表中的符号。对比 objdump 的输出:

80483cd:       b8 b0 84 04 08          mov    $0x80484b0,%eax
80483d2:       89 04 24                mov    %eax,(%esp)
80483d5:       e8 1a ff ff ff          call   80482f4 <printf@plt>

首先,常数 .LC0 现在只是内存中某处的某个随机偏移量——很难在正确的位置创建包含该常数的汇编源文件,因为汇编器和链接器可以自由选择这些常数的位置。

其次,我对此并不完全确定(它取决于位置无关代码之类的东西),但我相信对 printf 的引用实际上并没有在该代码中的指针地址处编码,但是 ELF 标头包含一个在运行时动态替换其地址的查找表。因此,反汇编代码与源汇编代码并不完全对应。

总之,源程序集有符号,而编译的机器代码有难以反转的地址。

第二个主要问题是程序集源文件不能包含原始 ELF 文件头中存在的所有信息,例如要动态链接的库以及原始编译器放置在那里的其他元数据。这将很难重建。

就像我说的那样,一个特殊的工具可能可以操纵所有这些信息,但是不可能简单地生成可以重新组装回可执行文件的汇编代码。

如果您只对修改可执行文件的一小部分感兴趣,我推荐一种比重新编译整个应用程序更微妙的方法。使用 objdump 获取您感兴趣的函数的汇编代码。手动将其转换为“源汇编语法”(在这里,我希望有一个工具能够以与输入相同的语法实际生成反汇编) , 并根据需要进行修改。完成后,仅重新编译这些函数并使用 objdump 找出修改后程序的机器代码。然后,使用十六进制编辑器将新机器代码手动粘贴到原始程序相应部分的顶部,注意您的新代码与旧代码的字节数完全相同(否则所有偏移量都是错误的)。如果新代码更短,您可以使用 NOP 指令填充它。如果它更长,您可能会遇到麻烦,并且可能必须创建新函数并调用它们。

于 2010-11-30T03:39:19.313 回答
23

我用hexdump一个文本编辑器来做这个。您必须对机器代码和存储它的文件格式非常熟悉,并且对“反汇编、修改然后重新组装”的内容具有灵活性。

如果您可以仅进行“局部更改”(重写字节,但不添加或删除字节),那将很容易(相对而言)。

真的不想替换任何现有指令,因为这样您就必须手动调整机器代码中任何受影响的绝对地址或相对偏移量,以相对于程序计数器进行跳转/分支/加载/存储,两者都是硬编码的立即数在反汇编中可以清楚地看到的值,以及动态计算的值,并且只能通过更改指令来修改,这些指令在使用之前更改寄存器中的地址或偏移量。

您应该始终能够避免不删除字节。对于更复杂的修改,添加字节可能是必要的,并且变得更加困难。

步骤 0(准备)

在您实际使用或通常首先使用的任何东西正确反汇编文件objdump -D以实际理解它并找到需要更改的地方之后,您需要注意以下事项以帮助您找到要修改的正确字节:

  1. 您需要更改的字节的“地址”(从文件开头的偏移量)。
  2. 这些字节当前的原始值(此处的--show-raw-insn选项objdump非常有用)。

您还需要检查hexdump -R您的系统是否有效。如果不是,那么对于这些​​步骤的其余部分,请使用xxd命令或类似命令,而不是hexdump在下面的所有步骤中(请查阅您使用的任何工具的文档,我hexdump现在只在这个答案中解释,因为那是我熟悉的和)。

第1步

使用 .dump 转储二进制文件的原始十六进制表示hexdump -Cv

第2步

打开hexdumped 文件并在您要更改的地址处找到字节。

输出中的快速速成课程hexdump -Cv

  1. 最左边的列是字节的地址(相对于二进制文件本身的开头,就像objdump提供的那样)。
  2. 最右边的列(由|字符包围)只是字节的“人类可读”表示 - 与每个字节匹配的 ASCII 字符写在那里,.代表所有不映射到 ASCII 可打印字符的字节。
  3. 重要的东西介于两者之间——每个字节为两个用空格分隔的十六进制数字,每行 16 个字节。

当心:与 不同objdump -D,它为您提供每条指令的地址并根据其记录为编码的方式显示指令的原始十六进制,它hexdump -Cv完全按照每个字节在文件中出现的顺序转储每个字节。这可能会有点令人困惑,因为首先在由于字节序差异而指令字节顺序相反的机器上,当您期望将特定字节作为特定地址时,这也会让人迷失方向。

第 3 步

修改需要更改的字节 - 您显然需要弄清楚原始机器指令编码(而不是汇编助记符)并手动写入正确的字节。

注意:您无需更改最右侧列中的人类可读表示。hexdump当你“取消转储”它时会忽略它。

第4步

使用 . "Un-dump" 修改后的 hexdump 文件hexdump -R

第 5 步(健全性检查)

objdump您新hexdump编辑的文件并验证您更改的反汇编是否正确。diff它违背了objdump原著。

说真的,不要跳过这一步。在手动编辑机器代码时,我经常犯错误,这就是我抓住大多数错误的方式。

例子

这是我最近修改 ARMv8(小端序)二进制文件时的一个真实示例。(我知道,问题被标记了x86,但我手头没有x86示例,基本原理相同,只是说明不同。)

在我的情况下,我需要禁用特定的“你不应该这样做”手持检查:在我的示例二进制文件中,在objdump --show-raw-insn -d输出中我关心的行看起来像这样(在给出上下文之前和之后的一条指令):

     f40:   aa1503e3    mov x3, x21
     f44:   97fffeeb    bl  af0 <error@plt>
     f48:   f94013f7    ldr x23, [sp, #32]

如您所见,我们的程序通过跳转到一个error函数(终止程序)来“帮助”退出。不可接受。所以我们要把这个指令变成无操作。0x97fffeeb所以我们在 address/file-offset寻找字节0xf44

这是hexdump -Cv包含该偏移量的行。

00000f40  e3 03 15 aa eb fe ff 97  f7 13 40 f9 e8 02 40 39  |..........@...@9|

请注意相关字节实际上是如何翻转的(体系结构中的小端编码适用于机器指令,就像其他任何东西一样)以及这有点不直观地与什么字节在什么字节偏移处相关:

00000f40  -- -- -- -- eb fe ff 97  -- -- -- -- -- -- -- --  |..........@...@9|
                      ^
                      This is offset f44, holding the least significant byte
                      So the *instruction as a whole* is at the expected offset,
                      just the bytes are flipped around. Of course, whether the
                      order matches or not will vary with the architecture.

无论如何,我从查看其他反汇编的0xd503201f反汇编中知道nop,这似乎是我的无操作指令的一个很好的候选者。我相应地修改了hexdumped 文件中的行:

00000f40  e3 03 15 aa 1f 20 03 d5  f7 13 40 f9 e8 02 40 39  |..........@...@9|

用 转换回二进制文件hexdump -R,用 反汇编新的二进制文件objdump --show-raw-insn -d并验证更改是否正确:

     f40:   aa1503e3    mov x3, x21
     f44:   d503201f    nop
     f48:   f94013f7    ldr x23, [sp, #32]

然后我运行了二进制文件并得到了我想要的行为——相关检查不再导致程序中止。

机器码修改成功。

!!!警告 !!!

还是我成功了?你发现我在这个例子中遗漏了什么吗?

我相信你做到了——因为你问的是如何手动修改程序的机器代码,你大概知道你在做什么。但是为了任何可能正在阅读以学习的读者的利益,我将详细说明:

我只更改了错误案例分支中的最后一条指令!跳转到退出程序的函数。但是正如你所看到x3的,上面的内容正在修改寄存器mov!事实上,总共四 (4) 个寄存器被修改为 call 前导码的一部分error,一个寄存器被修改。这是该分支的完整机器代码,从if块上的条件跳转开始,到如果条件if不被采用则跳转到的位置结束:

     f2c:   350000e8    cbnz    w8, f48
     f30:   b0000002    adrp    x2, 1000
     f34:   91128442    add x2, x2, #0x4a1
     f38:   320003e0    orr w0, wzr, #0x1
     f3c:   2a1f03e1    mov w1, wzr
     f40:   aa1503e3    mov x3, x21
     f44:   97fffeeb    bl  af0 <error@plt>
     f48:   f94013f7    ldr x23, [sp, #32]

分支之后的所有代码都是编译器在假设程序状态与条件跳转之前一样的情况下生成的!但是,通过将最终跳转到error函数代码变为无操作,我创建了一个代码路径,我们可以在其中到达具有不一致/不正确程序状态的代码!

就我而言,这实际上似乎没有引起任何问题。所以我很幸运。非常幸运:只有在我已经运行了修改后的二进制文件(顺便说一下,它是一个安全关键的二进制文件:它能够更改SELinux 上下文setuid!)之后,我才意识到我忘记实际跟踪是否这些寄存器更改影响了后来的代码路径!setgid

这可能是灾难性的——这些寄存器中的任何一个都可能在以后的代码中被使用,假设它包含一个现在被覆盖的先前值!而且我是人们所熟知的那种对代码一丝不苟的思考者,以及始终对计算机安全尽责的书呆子和固执己见的人。

如果我正在调用一个函数,其中参数从寄存器溢出到堆栈上(这在例如 x86 上很常见)怎么办?如果在条件跳转之前的指令集中实际上有多个条件指令(例如,在较旧的 ARM 版本上很常见),该怎么办?在完成了看似最简单的更改之后,我会处于更加鲁莽的不一致状态!

所以这是我的警告:手动玩弄二进制文件实际上是在剥夺您与机器和操作系统允许的安全之间的所有安全性。从字面上看,我们在自动捕捉程序错误的工具中取得的所有进步都消失了

那么我们如何更正确地解决这个问题呢?继续阅读。

删除代码

为了有效/逻辑地“删除”多个指令,您可以将要“删除”的第一条指令替换为无条件跳转到“已删除”指令末尾的第一条指令。对于这个 ARMv8 二进制文件,它看起来像这样:

     f2c:   14000007    b   f48
     f30:   b0000002    adrp    x2, 1000
     f34:   91128442    add x2, x2, #0x4a1
     f38:   320003e0    orr w0, wzr, #0x1
     f3c:   2a1f03e1    mov w1, wzr
     f40:   aa1503e3    mov x3, x21
     f44:   97fffeeb    bl  af0 <error@plt>
     f48:   f94013f7    ldr x23, [sp, #32]

基本上,您“杀死”代码(将其变成“死代码”)。旁注:您可以对二进制文件中嵌入的文字字符串执行类似的操作:只要您想用较小的字符串替换它,您几乎总是可以覆盖字符串(包括终止空字节,如果它是“C- string") 并在必要时覆盖使用它的机器代码中字符串的硬编码大小。

您还可以用无操作替换所有不需要的指令。换句话说,我们可以将不需要的代码变成所谓的“no-op sled”:

     f2c:   d503201f    nop
     f30:   d503201f    nop
     f34:   d503201f    nop
     f38:   d503201f    nop
     f3c:   d503201f    nop
     f40:   d503201f    nop
     f44:   d503201f    nop
     f48:   f94013f7    ldr x23, [sp, #32]

我希望这只是相对于跳过它们浪费 CPU 周期,更简单,因此更安全,因为您不必手动弄清楚如何对跳转指令进行编码,包括找出要使用的偏移量/地址在其中-您不必为无操作雪橇考虑太多。

需要明确的是,错误很容易:在手动编码该无条件分支指令时,我搞砸了两 (2)次。这并不总是我们的错:第一次是因为我拥有的文档已经过时/错误,并且说在编码中忽略了一个位,而实际上不是,所以我在第一次尝试时将其设置为零。

添加代码

理论上你也可以使用这种技术来添加机器指令,但它更复杂,而且我从来没有这样做过,所以我目前没有一个可行的例子。

从机器代码的角度来看,这很容易:在您要添加代码的位置选择一条指令,并将其转换为跳转指令到您需要添加的新代码(不要忘记添加您因此替换到新代码中,除非您不需要它来添加逻辑,并在添加结束时跳回您想要返回的指令)。基本上,您正在“拼接”新代码。

但是你必须找到一个实际放置新代码的位置,这是困难的部分。

如果你真的很幸运,你可以在文件末尾附加新的机器代码,它会“正常工作”:新代码将与其余代码一起加载到相同的预期机器指令中,进入你的落入正确标记为可执行的内存页的地址空间空间。

根据我的经验hexdump -R,它不仅忽略了最右边的列,还忽略了最左边的列 - 所以你实际上可以为所有手动添加的行输入零地址,它会解决的。

如果你不那么幸运,在添加代码之后,你将不得不在同一个文件中实际调整一些头值:如果你的操作系统的加载器期望二进制文件包含描述可执行部分大小的元数据(出于历史原因通常称为“文本”部分)您必须找到并调整它。在过去,二进制文件只是原始机器代码 - 现在机器代码被包装在一堆元数据中(例如 Linux 上的 ELF 和其他一些)。

如果你还有点幸运,你可能在文件中有一些“死”点,它确实作为二进制文件的一部分正确加载,与文件中已经存在的其余代码相同的相对偏移量(并且如果您的 CPU 需要 CPU 指令的字对齐,死点可以适合您的代码并正确对齐)。然后你可以覆盖它。

如果你真的很不幸,你不能只附加代码并且没有死区,你可以用你的机器代码填充。那时,您基本上必须非常熟悉可执行格式,并希望您能在这些限制范围内找出一些人类可行的东西,以便在合理的时间内手动完成,并且有合理的机会不会搞砸它.

于 2018-10-01T09:14:42.697 回答
8

@mgiuca 从技术角度正确地解决了这个答案。事实上,将可执行程序反汇编成易于重新编译的汇编源代码并非易事。

为了在讨论中添加一些内容,有一些技术/工具可能会很有趣,尽管它们在技术上很复杂。

  1. 静态/动态仪表。该技术需要分析可执行文件格式,插入/删除/替换特定目的的汇编指令,修复对可执行文件中变量/函数的所有引用,并发出新的修改后的可执行文件。我知道的一些工具是:PINHijackerPEBILDynamoRIO。考虑到将这些工具配置为不同于它们的设计目的可能会很棘手,并且需要了解可执行格式和指令集。
  2. 完全可执行的反编译。该技术尝试从可执行文件重建完整的程序集源。您可能想看一下Online Disassembler,它试图完成这项工作。无论如何,您都会丢失有关不同源模块以及可能的函数/变量名称的信息。
  3. 可重定向的反编译。这种技术试图从可执行文件中提取更多信息,查看编译器指纹(即已知编译器生成的代码模式)和其他确定性的东西。主要目标是从可执行文件重构更高级别的源代码,如 C 源代码。这有时能够重新获得有关函数/变量名称的信息。考虑编译源代码-g通常会提供更好的结果。您可能想尝试一下Retargetable Decompiler

其中大部分来自漏洞评估和执行分析研究领域。它们是复杂的技术,而且这些工具通常不能开箱即用。然而,在尝试对某些软件进行逆向工程时,它们提供了宝贵的帮助。

于 2016-02-10T12:30:24.897 回答
7

要更改二进制程序集中的代码,通常有 3 种方法可以做到。

  • 如果它只是一些像常量这样微不足道的东西,那么您只需使用十六进制编辑器更改位置即可。假设你可以找到它开始。
  • 如果您需要更改代码,请使用 LD_PRELOAD 覆盖程序中的某些功能。但是,如果该函数不在函数表中,那将不起作用。
  • 破解您要修复的函数的代码,直接跳转到您通过 LD_PRELOAD 加载的函数,然后跳回同一位置(这是上述两者的组合)

当然,如果组件进行任何类型的自我完整性检查,则只有第二个可以工作。

编辑:如果不是很明显,那么使用二进制程序集是非常高级的开发人员的东西,你将很难在这里询问它,除非它真的是你问的具体问题。

于 2010-11-30T04:17:42.010 回答
2

瘴气

https://github.com/cea-sec/miasm

这似乎是最有希望的具体解决方案。根据项目描述,该库可以:

  • 打开 / 修改 / 生成 PE / ELF 32 / 64 LE / BE 使用 Elfrespect
  • 组装/拆卸 X86 / ARM / MIPS / SH4 / MSP430

所以它基本上应该:

  • 将 ELF 解析为内部表示(反汇编)
  • 修改你想要的
  • 生成一个新的 ELF(程序集)

我认为它不会生成文本反汇编表示,您可能必须遍历 Python 数据结构。

TODO 找到一个如何使用库完成所有这些的最小示例。一个很好的起点似乎是example/disasm/full.py,它解析给定的 ELF 文件。关键的顶级结构是Container,它读取 ELF 文件时使用Container.from_stream. TODO 之后如何重新组装它?这篇文章似乎做到了:http: //www.miasm.re/blog/2016/03/24/re150_rebuild.html

这个问题询问是否还有其他这样的库:https ://reverseengineering.stackexchange.com/questions/1843/what-are-the-available-libraries-to-statically-modify-elf-executables

相关问题:

我认为这个问题是不可自动化的

我认为一般问题不是完全自动化的,一般解决方案基本上相当于“如何逆向工程”一个二进制文件。

为了以有意义的方式插入或删除字节,我们必须确保所有可能的跳转继续跳转到相同的位置。

在形式上,我们需要提取二进制的控制流图。

但是,对于间接分支,例如https://en.wikipedia.org/wiki/Indirect_branch,确定该图并不容易,另请参见:间接跳转目标计算

于 2017-12-06T15:12:03.277 回答
2

我的“ci assembler disassembler”是我所知道的唯一一个系统,它是围绕以下原则设计的:无论反汇编是什么,它都必须重新组装成一个字节一个字节的相同二进制文件。

https://github.com/albertvanderhorst/ciasdis

给出了两个关于 elf 可执行文件及其拆卸和重新组装的示例。它最初的设计目的是能够修改由代码、解释代码、数据和图形字符组成的引导系统,并具有从真实模式到保护模式的转换等细节。(它成功了。)这些示例还演示了从可执行文件中提取文本,随后将其用于标签。debian 软件包适用于 Intel Pentium,但插件可用于 Dec Alpha、6809、8086 等。

拆卸的质量取决于你付出了多少努力。例如,如果您甚至不提供它是 elf 文件的信息,则反汇编由单个字节组成,并且重组是微不足道的。在示例中,我使用了一个提取标签的脚本,并创建了一个真正可用的可修改的逆向工程程序。您可以插入或删除某些内容,自动生成的符号标签将被重新计算。使用提供的工具为所有跳转结束的地方生成标签,然后将标签用于这些跳转。这意味着在大多数情况下,您可以插入一条指令并重新组装修改后的源代码。

根本没有对二进制 blob 做出任何假设,但当然,英特尔反汇编对于 Dec Alpha 二进制文件几乎没有用处。

于 2018-11-24T02:20:58.130 回答
0

您可能有兴趣做的另一件事:

  • 二进制检测 - 更改现有代码

如果有兴趣,请查看:Pin、Valgrind(或这样做的项目:NaCl - Google 的本机客户端,也许是 QEmu。)

于 2011-01-02T22:31:45.183 回答
0

您可以在 ptrace(换句话说,像 gdb 之类的调试器)的监督下运行可执行文件,这样,您可以随时控制执行,而无需修改实际文件。当然,这需要通常的编辑技能,例如查找您想要影响的特定指令在可执行文件中的位置。

于 2011-01-02T22:39:56.307 回答