如何反汇编、修改然后重新组装 Linux 可执行文件?

发布于 2024-10-04 21:55:28 字数 85 浏览 12 评论 0原文

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

Is there anyway this can be done? I've used objdump but that doesn't produce assembly output that will be accepted by any assembler that I know of. I'd like to be able to change instructions within an executable and then test it afterwards.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

╭⌒浅淡时光〆 2024-10-11 21:55:29

您可能感兴趣的另一件事:

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

如果有兴趣,请查看:Pin、Valgrind(或执行此操作的项目:NaCl - Google's Native Client,也许是 QEmu。)

Another thing you might be interested to do:

  • binary instrumentation - changing existing code

If interested, check out: Pin, Valgrind (or projects doing this: NaCl - Google's Native Client, maybe QEmu.)

浅紫色的梦幻 2024-10-11 21:55:29

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

You can run the executable under supervision of ptrace (in other words, a debugger like gdb) and in that way, control execution as you go, without modifying the actual file. Of course, requires the usual editing skills like finding where particular instructions you want to influence are in the executable.

满地尘埃落定 2024-10-11 21:55:28

我认为没有任何可靠的方法可以做到这一点。机器代码格式非常复杂,比汇编文件还要复杂。实际上不可能采用已编译的二进制文件(例如,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 指令将其填充。如果它更长,您可能会遇到麻烦,并且可能必须创建新函数并调用它们。

I don't think there is any reliable way to do this. Machine code formats are very complicated, more complicated than assembly files. It isn't really possible to take a compiled binary (say, in ELF format) and produce a source assembly program which will compile to the same (or similar-enough) binary. To gain an understanding of the differences, compare the output of GCC compiling direct to assembler (gcc -S) versus the output of objdump on the executable (objdump -D).

There are two major complications I can think of. Firstly, the machine code itself is not a 1-to-1 correspondence with assembly code, because of things like pointer offsets.

For example, consider the C code to Hello world:

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

This compiles to the x86 assembly code:

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

Where .LCO is a named constant, and printf is a symbol in a shared library symbol table. Compare to the output of 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>

Firstly, the constant .LC0 is now just some random offset in memory somewhere -- it would be difficult to create an assembly source file which contains this constant in the correct place, since the assembler and linker are free to choose locations for these constants.

Secondly, I'm not entirely sure about this (and it depends on things like position independent code), but I believe the reference to printf is not actually encoded at the pointer address in that code there at all, but the ELF headers contain a lookup table which dynamically replaces its address at runtime. Therefore, the disassembled code doesn't quite correspond to the source assembly code.

In summary, source assembly has symbols while compiled machine code has addresses which are difficult to reverse.

The second major complication is that an assembly source file can't contain all of the information that was present in the original ELF file headers, like which libraries to dynamically link against, and other metadata placed there by the original compiler. It would be difficult to reconstruct this.

Like I said, it's possible that a special tool can manipulate all of this information, but it is unlikely that one can simply produce assembly code which can be reassembled back to the executable.

If you are interested in modifying just a small section of the executable, I recommend a much more subtle approach than recompiling the whole application. Use objdump to get the assembly code for the function(s) you are interested in. Convert it to "source assembly syntax" by hand (and here, I wish there was a tool that actually produced disassembly in the same syntax as the input), and modify it as you wish. When you are done, recompile just those function(s) and use objdump to figure out the machine code for your modified program. Then, use a hex editor to manually paste the new machine code over the top of the corresponding part of the original program, taking care that your new code is precisely the same number of bytes as the old code (or all the offsets would be wrong). If the new code is shorter, you can pad it out using NOP instructions. If it is longer, you may be in trouble, and might have to create new functions and call them instead.

淡莣 2024-10-11 21:55:28

我使用 hexdump 和文本编辑器来完成此操作。您必须真正熟悉机器代码和存储它的文件格式,并且灵活地处理“反汇编、修改然后重新汇编”的情况。

如果您可以只进行“点更改”(重写字节,但不添加或删除字节),那么这会很容易(相对而言)。

真的不想替换任何现有指令,因为那样您就必须手动调整机器代码中任何受影响的绝对地址或相对偏移量,以便相对于程序计数器,无论是在反汇编中可以清楚地看到的硬编码立即值,还是动态计算的并且只能通过更改寄存器之前更改地址或偏移量的指令来修改的值用过的。

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

第 0 步(准备)

在您使用 objdump -D 或您通常首先使用的任何工具实际上正确反汇编文件后,才能真正理解它并找到需要更改的位置,您需要注意以下事项,以帮助您找到要修改的正确字节:

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

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

步骤 1

使用 hexdump -Cv 转储二进制文件的原始十六进制表示形式。

步骤 2

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

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

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

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

步骤 3

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

注意:您不需要更改最右列中的人类可读表示。当您“取消转储”它时,hexdump 将忽略它。

步骤 4

使用 hexdump -R“取消转储”修改后的 hexdump 文件。

第 5 步(健全性检查)

objdump 新取消的 hexdump 文件,并验证您更改的反汇编内容看起来是否正确。将其与原始文件的 objdump 进行diff 比较。

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

示例

这是我最近修改 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 函数(终止程序)来“帮助”退出。不可接受的。所以我们要将该指令变成空操作。因此,我们正在地址/文件偏移 0xf44 处查找字节 0x97fffeeb

这是包含该偏移量的 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.

无论如何,我从查看其他反汇编中知道< code>0xd503201f 反汇编为 nop,因此这似乎是我的无操作指令的良好候选者。我相应地修改了 hexdump 文件中的行:

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) 个寄存器被修改,其中一个寄存器被修改。以下是该分支的完整机器代码,从 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 函数代码设置为无操作,我创建了一条代码路径,在该路径中我们到达了具有不一致/不正确的程序状态的代码!

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

这可能是灾难性的 - 这些寄存器中的任何一个都可能在以后的代码中使用,并假设它包含现在被覆盖的先前值!人们都知道我是那种对代码进行细致思考的人,并且是一个始终认真负责计算机安全的学究和坚持者。

如果我调用一个函数,其中参数从寄存器溢出到堆栈上(这在 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"),并在必要时覆盖使用该字符串的机器代码中的硬编码大小。

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

     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 指令进行字对齐,则死点可以正确对齐)。然后你可以覆盖它。

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

I do this with hexdump and a text editor. You have to be really comfortable with the machine code and the file format storing it, and flexible with what counts as "disassemble, modify, and then reassemble".

If you can get away with making just "spot changes" (rewriting bytes, but not adding nor removing bytes), it'll be easy (relatively speaking).

You really don't want to displace any existing instructions, because then you'd have to manually adjust any affected absolute address or relative offset within the machine code for jumps/branches/loads/stores relative to the program counter, both in hardcoded immediate values that can be clearly seen in the disassembly, and ones that are dynamically computed and can only be modified by changing instructions that change the address or offset in a register before it is used.

You should always be able to get away with not removing bytes. Adding bytes might be necessary for more complex modifications, and gets a lot harder.

Step 0 (preparation)

After you've actually disassembled the file properly with objdump -D or whatever you normally use first to actually understand it and find the spots you need to change, you'll need to take note of the following things to help you locate the correct bytes to modify:

  1. The "address" (offset from the start of the file) of the bytes you need to change.
  2. The raw value of those bytes as they currently are (the --show-raw-insn option to objdump is really helpful here).

You'll also need to check if hexdump -R works on your system. If not, then for the rest of these steps, use the xxd command or similar instead of hexdump in all of the steps below (consult the documentation for whatever tool you use, I only explain hexdump for now in this answer because that is the one I am familiar with).

Step 1

Dump the raw hexadecimal representation of the binary file with hexdump -Cv.

Step 2

Open the hexdumped file and find the bytes at the address you're looking to change.

Quick crash course in hexdump -Cv output:

  1. The left-most column is the addresses of the bytes (relative to the start of the binary file itself, just like objdump provides).
  2. The right-most column (surrounded by | characters) is just "human readable" representation of the bytes - the ASCII character matching each byte is written there, with a . standing in for all bytes which don't map to an ASCII printable character.
  3. The important stuff is in between - each byte as two hex digits separated by spaces, 16 bytes per line.

Beware: Unlike objdump -D, which gives you the address of each instruction and shows the raw hex of the instruction based on how it's documented as being encoded, hexdump -Cv dumps each byte exactly in the order it appears in the file. This can be a little confusing at first on machines where the instruction bytes are in opposite order due to endianness differences, which can also be disorienting when you're expecting a specific byte at a specific address.

Step 3

Modify the bytes that need to change - you obviously need to figure out the raw machine instruction encoding (not the assembly mnemonics) and manually write in the correct bytes.

Note: You don't need to change the human-readable representation in the right-most column. hexdump will ignore it when you "un-dump" it.

Step 4

"Un-dump" the modified hexdump file using hexdump -R.

Step 5 (sanity check)

objdump your newly unhexdumped file and verify that the disassembly that you changed looks correct. diff it against the objdump of the original.

Seriously, don't skip this step. I make a mistake more often than not when manually editing the machine code and this is how I catch most of them.

Example

Here's a real-life worked example from when I modified an ARMv8 (little endian) binary recently. (I know, the question is tagged x86, but I don't have an x86 example handy, and the fundamental principles are the same, just the instructions are different.)

In my situation I needed to disable a specific "you shouldn't be doing this" hand-holding check: in my example binary, in objdump --show-raw-insn -d output the line I cared about looked like this (one instruction before and after given for context):

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

As you can see, our program is "helpfully" exiting by jumping into an error function (which terminates the program). Unacceptable. So we're going to turn that instruction into a no-op. So we're looking for the bytes 0x97fffeeb at the address/file-offset 0xf44.

Here is the hexdump -Cv line containing that offset.

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

Notice how the relevant bytes are actually flipped (little endian encoding in the architecture applies to machine instructions like to anything else) and how this slightly unintuitively relates to what byte is at what byte offset:

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.

Anyway, I know from looking at other disassembly that 0xd503201f disassembles to nop so that seems like a good candidate for my no-op instruction. I modified the line in the hexdumped file accordingly:

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

Converted back into binary with hexdump -R, disassembled the new binary with objdump --show-raw-insn -d and verified that the change was correct:

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

Then I ran the binary and got the behavior I wanted - the relevant check no longer caused the program to abort.

Machine code modification successful.

!!! Warning !!!

Or was I successful? Did you spot what I missed in this example?

I am sure you did - since you're asking about how to manually modify the machine code of a program, you presumably know what you're doing. But for the benefit of any readers who might be reading to learn, I'll elaborate:

I only changed the last instruction in the error-case branch! The jump into the function that exits the program. But as you can see, register x3 was being modified by the mov just above! In fact, a total of four (4) registers were modified as part of the preamble to call error, and one register was. Here's the full machine code for that branch, starting from the conditional jump over the if block and ending where the jump goes to if the conditional if isn't taken:

     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]

All of the code after the branch was generated by the compiler on the assumption that the program state was as it was before the conditional jump! But by just making the final jump to the error function code a no-op, I created a code path where we reach that code with inconsistent/incorrect program state!

In my case, this actually seemed to not cause any problems. So I got lucky. Very lucky: only after I already ran my modified binary (which, incidentally, was a security-critical binary: it had the capability to setuid, setgid, and change SELinux context!) did I realize that I forgot to actually trace the code paths of whether those register changes effected the code paths that came later!

That could've been catastrophic - any one of those registers might've been used in later code with the assumption that it contained a previous value that now got overwritten! And I'm the kind of person that people know for meticulous careful thought about code and as a pedant and stickler for always being conscientious of computer security.

What if I was calling a function where the arguments spilled from the registers onto the stack (as is very common on, for example, x86)? What if there was actually multiple conditional instructions in the instruction set that preceded the conditional jump (as is common on, for example, older ARM versions)? I would've been in an even more recklessly inconsistent state after having done that simplest-seeming change!

So this my cautionary reminder: Manually twiddling with binaries is literally stripping away every safety between you and what the machine and operating system will permit. Literally all the advances that we have made in our tools to automatically catch mistakes our programs, gone.

So how do we fix this more properly? Read on.

Removing Code

To effectively/logically "remove" more than one instruction, you can replace the first instruction you want to "delete" with an unconditional jump to the first instruction at the end of the "deleted" instructions. For this ARMv8 binary, that looked like this:

     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]

Basically, you "kill" the code (turn it into "dead code"). Sidenote: You can do something similar with literal strings embedded in the binary: so long as you want to replace it with a smaller string, you can almost always get away with overwriting the string (including the terminating null byte if it's a "C-string") and if necessary overwriting the hard-coded size of the string in the machine code that uses it.

You can also replace all unwanted instructions with no-ops. In other words, we can turn the unwanted code into what's called a "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]

I would expect that that's just wasting CPU cycles relative to jumping over them, but it is simpler and thus safer against mistakes, because you don't have to manually figure out how to encode the jump instruction including figuring out the offset/address to use in it - you don't have to think as much for a no-op sled.

To be clear, error is easy: I messed up two (2) times when manually encoding that unconditional branch instruction. And it's not always our fault: the first time was because the documentation I had was outdated/wrong and said one bit was ignored in the encoding, when it actually wasn't, so I set it to zero on my first try.

Adding Code

You could theoretically use this technique to add machine instructions too, but it's more complex, and I've never had to do it, so I don't have a worked example at this time.

From a machine code perspective it's sorta easy: pick one instruction at the spot you want to add code, and convert it into a jump instruction to the new code that you need add (don't forget to add the instruction(s) you thus replaced into the new code, unless you didn't need that for your added logic, and to jump back to the instruction you want to come back to at the end of the addition). Basically, you're "splicing" the new code in.

But you have to find a spot to actually put that new code, and this is the hard part.

If you're really lucky, you can just append the new machine code at the end of the file, and it'll "just work": the new code will get loaded along with the rest into the same expected machine instructions, into your address space space that falls into a memory page properly marked executable.

In my experience hexdump -R ignores not just the right-most column but the left-most column too - so you could literally just put zero addresses for all manually added lines and it'll work out.

If you're less lucky, after adding the code you'll have to actually adjust some header values within the same file: if the loader for your operating system expects the binary to contain metadata describing the size of the executable section (for historical reasons often called the "text" section) you'll have to find and adjust that. In the old days binaries were just raw machine code - nowadays the machine code is wrapped in a bunch of metadata (for example ELF on Linux and some others).

If you're still a little lucky, you might have some "dead" spot in the file which does get properly loaded up as part of the binary at the same relative offsets as the rest of the code that's already in the file (and that dead spot can fit your code and is properly aligned if your CPU requires word-alignment for CPU instructions). Then you can overwrite it.

If you're really unlucky you can't just append code and there is no dead space you can fill with your machine code. At that point, you basically have to be intimately familiar with the executable format and hope that you can figure out something within those constraints that is humanly feasible to pull off manually within a reasonable amount fo time and with a reasonable chance of not messing it up.

那小子欠揍 2024-10-11 21:55:28

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

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

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

其中大部分来自漏洞评估和执行分析研究领域。它们是复杂的技术,并且通常无法立即使用这些工具。尽管如此,当尝试对某些软件进行逆向工程时,它们提供了宝贵的帮助。

@mgiuca has correctly addressed this answer from a technical point of view. In fact, disassemblying an executable program into an easy-to-recompile assembly source is not an easy task.

To add some bits to the discussion, there are a couple of techniques/tools which could be interesting to explore, although they are technically complex.

  1. Static/Dynamic instrumentation. This technique entails analyzing the executable format, insert/delete/replace specific assembly instructions for a given purpose, fix all references to variables/functions in the executable, and the emit a new modified executable. Some tools which I know of are: PIN, Hijacker, PEBIL, DynamoRIO. Consider that configuring such tools to a purpose different from what they were designed for could be tricky, and requires understanding of both executable formats and instruction sets.
  2. Full executable decompilation. This technique tries to reconstruct a full assembly source from an executable. You might want to give a glance to the Online Disassembler, which tries to do the job. You lose anyhow information about different source modules and possibly functions/variable names.
  3. Retargetable decompilation. This technique tries to extract more information from the executable, looking at compiler fingerprints (i.e., patterns of code generated by known compilers) and other deterministic stuff. The main goal is to reconstruct higher-level source code, like C source, from an executable. This is sometimes able to regain information about functions/variables names. Consider that compiling sources with -g often offers better results. You might want to give the Retargetable Decompiler a try.

Most of this comes from vulnerbility assessment and execution analysis research fields. They are complex techniques and often the tools cannot be used immediately out of the box. Nevertheless, they provide invaluable help when trying to reverse engineer some software.

美胚控场 2024-10-11 21:55:28

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

  • 如果它只是一些琐碎的事情,例如常量,那么您只需使用十六进制编辑器更改位置即可。假设你一开始就能找到它。
  • 如果您需要更改代码,请使用 LD_PRELOAD 覆盖程序中的某些函数。如果该函数不在函数表中,则该函数不起作用。
  • 破解您想要修复的函数处的代码,使其直接跳转到通过 LD_PRELOAD 加载的函数,然后跳回到同一位置(这是上述两个的组合)

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

编辑:如果不明显,那么使用二进制程序集是非常高级的开发人员的工作,并且您将很难在这里询问它,除非您询问的确实是具体的事情。

For changing code inside of an binary assembly, there are generally 3 ways to do it.

  • If it is just some trivial thing like a constant, then you just change the location with a hex editor. Assuming you can find it to begin with.
  • If you need to alter code, then utilize the LD_PRELOAD to overwrite some function in your program. That doesn't work if the function is not in the function tables though.
  • Hack the code at the function you want to fix to be a direct jump to a function you load via LD_PRELOAD and then jump back to the same location (This is a combi of the above two)

Ofcourse only the 2nd one will work, if the assembly does any kind of self-integrity-check.

Edit: If it isn't obvious then playing around with binary assemblies is VERY high-level developer stuff, and you will have a hard time asking about it here, unless it is really specific things you ask.

菊凝晚露 2024-10-11 21:55:28

miasm

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

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

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

所以它基本上应该:

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

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

TODO 找到一个关于如何使用该库完成所有这些操作的最小示例。一个好的起点似乎是 example/disasm/full .py,它解析给定的 ELF 文件。关键的顶层结构是Container,它通过Container.from_stream读取ELF文件。 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 ,确定该图并不容易,另请参阅:间接跳转目的地计算

miasm

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

This appears to be the most promising concrete solution. According to the project description, the library can:

  • Opening / modifying / generating PE / ELF 32 / 64 LE / BE using Elfesteem
  • Assembling / Disassembling X86 / ARM / MIPS / SH4 / MSP430

So it should basically:

  • parse the ELF into an internal representation (disassembly)
  • modify what you want
  • generate a new ELF (assembly)

I don't think it generates a textual disassembly representation, you will likely have to walk through Python data structures.

TODO find a minimal example of how to do all of that using the library. A good starting point seems to be example/disasm/full.py, which parses a given ELF file. The key top-level structurei is Container, which reads the ELF file with Container.from_stream. TODO how to reassemble it afterwards? This article seems to do it: http://www.miasm.re/blog/2016/03/24/re150_rebuild.html

This question asks if there are any other such libraries: https://reverseengineering.stackexchange.com/questions/1843/what-are-the-available-libraries-to-statically-modify-elf-executables

Related questions:

I think this problem is not automatable

I think the general problem is not fully automatable, and the general solution is basically equivalent to "how to reverse engineer" a binary.

In order to insert or remove bytes in a meaningful way, we would have to ensure that all possible jumps keep jumping to the same locations.

In formal terms, we need to extract the control flow graph of the binary.

However, with indirect branches for example, https://en.wikipedia.org/wiki/Indirect_branch , it is not easy to determine that graph, see also: Indirect jump destination calculation

噩梦成真你也成魔 2024-10-11 21:55:28

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

https://github.com/albertvanderhorst/ciasdis

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

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

根本没有对二进制 blob 做出任何假设,但当然 Intel 反汇编对于 Dec Alpha 二进制文件没有什么用处。

My "ci assembler disassembler" is the only system that I know is that is designed around the principle that whatever the disassembly is, it must reassemble to the byte for byte same binary.

https://github.com/albertvanderhorst/ciasdis

There are two examples given of elf-executables with their disassembly and reassembly. It was originally designed to be able to modify a booting system, consisting of code, interpreted code, data and graphic characters, with such niceties as a transition from real to protected mode. (It succeeded.) The examples demonstrate also the extraction of text from the executables, that is subsequently used for labels. The debian package is intended for Intel Pentium, but plug ins are available for Dec Alpha, 6809, 8086 etc.

The quality of the disassembly depends on how much effort you put into it. E.g., if you do not even supply the information that it is an elf file, the disassembly consist of single bytes, and the reassembly is trivial. In the examples I use a script that extracts labels, and makes for a truely usable reverse engineered program that is modifiable. You can insert or delete something and the automatically generated symbolic labels will get recalculated. With the tools provided labels are generated for all places where jumps end, and then the labels are used for those jumps. That means that in most case you can insert an instruction and reassemble the modified source.

No assumption at all is made about the binary blob, but of course an Intel disassembly is of little use for a Dec Alpha binary.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文