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

Fla*_*per 48 linux x86 objdump disassembly

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

mgi*_*uca 29

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

我能想到两个主要的并发症.首先,由于指针偏移之类的东西,机器代码本身与汇编代码不是一对一的对应关系.

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

int main()
{
    printf("Hello, world!\n");
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

这将编译为x86汇编代码:

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

其中.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>
Run Code Online (Sandbox Code Playgroud)

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

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

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

第二个主要的复杂因素是汇编源文件不能包含原始ELF文件头中存在的所有信息,例如动态链接的库以及原始编译器放置在其中的其他元数据.重建这个很难.

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

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

  • 我认为你夸大了代码找到常量的一些困难.主要问题是没有asm语法可以唯一地表示同一指令的不同长度编码.Agner Fog的[`objconv`反汇编程序](http://agner.org/optimize/)将反汇编为NASM,YASM,MASM或GNU语法.该输出可以组装回类似的二进制文件,但代码对齐/偏移的任何假设都可能已经改变.例如,PLT(过程链接表)需要使用`jmp rel32`编码,因此可以在运行时填充右偏移量. (2认同)

Cin*_*ine 7

对于更改二进制程序集内部的代码,通常有3种方法可以执行此操作.

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

当然,如果集会做任何形式的自我完整性检查,那么只有第二个会起作用.

编辑:如果不是很明显那么玩二进制程序集是非常高级的开发人员的东西,你将很难在这里询问它,除非它是你要求的特定事项.


ilp*_*lle 7

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

为了在讨论中添加一些内容,有一些技术/工具可供探讨,但它们在技术上很复杂.

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

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


mtr*_*eur 5

我使用hexdump和文本编辑器来完成此操作。您必须对机器代码和存储它的文件格式非常满意,并且灵活地算作“反汇编,修改然后重新组装”。

如果您仅通过“现场更改”(重写字节,而不添加或删除字节)就可以摆脱困境,那么这将很容易(相对而言)。

真的不想取代任何现有的指令,因为这样你就不得不手动调节相对于程序计数器任何影响的相对本机代码中的偏移量,用于跳跃/分支机构/加载/存储,无论是在硬编码的即时通过寄存器计算的。

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

步骤0(准备工作)

实际地使用objdump -D或通常用来真正理解文件并找到需要更改的地方正确地反汇编文件之后,需要注意以下几点,以帮助您找到要修改的正确字节:

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

步骤1

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

第2步

打开hexdumped文件,然后在您要更改的地址中找到字节。

hexdump -Cv输出中的快速崩溃过程:

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

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

第三步

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

注意:您没有需要改变的最右列中的人类可读表示。hexdump当您“卸载”它时,它将忽略它。

第四步

使用取消转储修改后的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]
Run Code Online (Sandbox Code Playgroud)

如您所见,我们的程序通过跳转到一个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|
Run Code Online (Sandbox Code Playgroud)

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

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.
Run Code Online (Sandbox Code Playgroud)

无论如何,我从查看其他可反汇编的反汇编中了解到0xd503201fnop这似乎是我的无操作指令的理想人选。我相应地修改了hexdumped文件中的行:

00000f40  e3 03 15 aa 1f 20 03 d5  f7 13 40 f9 e8 02 40 39  |..........@...@9|
Run Code Online (Sandbox Code Playgroud)

使用转换回二进制文件hexdump -R,使用分解新的二进制文件objdump --show-raw-insn -d并验证更改正确:

     f40:   aa1503e3    mov x3, x21
     f44:   d503201f    nop
     f48:   f94013f7    ldr x23, [sp, #32]
Run Code Online (Sandbox Code Playgroud)

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

机器码修改成功。

!!! 警告 !!!

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

我确定您已经做到了-因为您在询问如何手动修改程序的机器代码,所以您大概知道自己在做什么。但是为了使可能正在阅读中学习的任何读者受益,我将详细说明:

我只更改了错误情况分支中的最后一条指令!跳入退出问题的功能。但正如您所看到的,注册x3mov上面的内容修改了!实际上,总共有四(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]
Run Code Online (Sandbox Code Playgroud)

分支之后的所有代码都是由编译器根据程序状态与条件跳转之前的状态生成的!但是,通过仅使对error功能代码的最后一次跳转成为无操作,我创建了一个代码路径,在该路径下,程序状态不一致/不正确就可以到达该代码!

就我而言,这实际上似乎不会引起任何问题。所以我很幸运。幸运的,只有在我已经跑了我的改进型二(顺便说一下,是一个安全关键二进制:它有能力setuidsetgid以及变化的SELinux!),我才意识到,我忘了其实追查是否代码路径这些寄存器更改影响了后来出现的代码路径!

那可能是灾难性的-这些寄存器中的任何一个都可能在以后的代码中使用过,并假设它包含一个先前值而现在被覆盖了!我是那种人们对代码进行细致周到的思考的人,并且是一个始终忠于计算机安全性的书呆子和顽固主义者。

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

因此,这是我的提醒性提醒:手动旋转二进制文件实际上剥夺了您与机器和操作系统所允许的一切安全。从字面上看,我们在工具中自动捕获程序错误所取得的所有进步都消失了

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

删除代码

为了有效地 / 逻辑上 “删除”多条指令,您可以将要删除的第一条指令无条件地替换为“已删除”指令末尾的第一条指令。对于这个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]
Run Code Online (Sandbox Code Playgroud)

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

您也可以将所有不需要的指令替换为无操作。换句话说,我们可以将不需要的代码转换为所谓的“ 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]
Run Code Online (Sandbox Code Playgroud)

我希望那只是浪费CPU周期,不是跳过它们,但是这样容易出错,因此更安全,因为您不必手动找出如何编码跳转指令,包括找出要使用的偏移量/地址。在其中-您无需为无操作雪橇考虑太多

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

添加代码

从理论上讲,您也可以使用这种技术来添加机器指令,但是它更加复杂,而且我从来不需要这样做,因此目前没有可用的示例。

从机器代码的角度来看,这很简单:在要添加代码的位置选择一条指令,然后将其转换为跳转指令,以添加到需要添加的新代码(不要忘记添加要添加的指令)替换为新代码,除非您不需要添加逻辑,并跳回添加末尾要返回的指令)。基本上,您是在“拼接”新代码。

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

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

以我的经验,不仅会hexdump -R忽略最右边的列,还会忽略最左边的列-因此您可以为所有手动添加的行直接添加零地址,这样就可以解决。

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

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

如果您真的很不幸,您不能只是追加代码,就不会有死角,您可以在其中填充机器代码。在这一点上,您基本上必须非常熟悉可执行文件格式,并希望您可以找出那些在合理的时间内以合理的时间手动退出并且有合理的机会将其搞乱的约束条件中的某些内容。 。