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

时间:2010-11-30 01:31:01

标签: linux x86 disassembly objdump

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

8 个答案:

答案 0 :(得分:28)

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

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

例如,考虑到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 headers包含一个查找表,该表在运行时动态替换其地址。因此,反汇编的代码与源汇编代码并不完全对应。

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

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

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

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

答案 1 :(得分:7)

要更改二进制程序集中的代码,通常有3种方法可以执行此操作。

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

如果议会进行任何形式的自我完整性检查,那么只有第二个才能奏效。

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

答案 2 :(得分:7)

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

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

  1. 静态/动态检测。该技术需要分析可执行格式,为给定目的插入/删除/替换特定汇编指令,修复对可执行文件中的变量/函数的所有引用,以及发出新的已修改可执行文件。我所知道的一些工具包括:PINHijackerPEBILDynamoRIO。考虑将这些工具配置为与其设计目的不同的目的可能很棘手,并且需要了解可执行格式和指令集。
  2. 完整的可执行文件反编译。此技术尝试从可执行文件重建完整的程序集源。您可能想要看一眼试图完成工作的Online Disassembler。无论如何,您都会丢失有关不同源模块以及可能的函数/变量名称的信息。
  3. 重定向反编译。该技术试图从可执行文件中提取更多信息,查看编译器指纹(即,由已知编译器生成的代码模式)和其他确定性内容。主要目标是从可执行文件重建更高级别的源代码,如C源代码。这有时能够重新获得有关函数/变量名称的信息。考虑使用-g编译源通常可以提供更好的结果。您可能想尝试Retargetable Decompiler
  4. 其中大部分来自脆弱性评估和执行分析研究领域。它们是复杂的技术,通常不能立即使用工具。然而,在尝试对某些软件进行逆向工程时,它们提供了宝贵的帮助。

答案 3 :(得分:2)

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

如果您只需要“更改点”(重写字节,而不添加或删除字节)就可以了(相对而言)。

真的不想替换任何现有的指令,因为那样您将不得不在机器代码内手动调整任何受影响的相对偏移量,以便相对于跳转/分支/装载/存储程序计数器,包括硬编码的立即和通过寄存器计算的值。

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

第0步(准备工作)

使用objdump -D或您通常首先用来真正理解文件并找到需要更改的地方正确地 正确地分解了文件之后,您需要请注意以下事项,以帮助您找到要修改的正确字节:

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

步骤1

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

步骤2

打开hexdump -Cv版本的文件,然后在您要更改的地址中找到字节。

hexdump输出中的速成课程:

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

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

步骤3

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

注意:您不需要,需要在最右侧的列中更改易于理解的表示形式。 hexdump将在您“卸载”它时将其忽略。

步骤4

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

步骤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处寻找字节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,因此对于我的无操作指令而言,这似乎是一个不错的选择。我相应地修改了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]

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

机器代码修改成功。

!!!警告!!!

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

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

我只更改了错误案例分支中的 last 指令!跳入退出问题的功能。但是正如您所看到的,寄存器x3被上面的mov修改了!实际上,总共有四(4)个寄存器被修改为调用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函数代码的最后一次跳转成为无操作,我创建了一个代码路径,使我们可以在程序状态不一致/错误的情况下到达该代码

在我看来,这实际上似乎不会引起任何问题。所以我很幸运。 非常很幸运:只有在我运行了修改过的二进制文件(顺便说一句,它是安全关键型二进制文件:它具有setuid,{{ 1}},然后更改 SELinux上下文!),我意识到我实际上忘记了跟踪那些寄存器更改是否影响了后来出现的代码路径的代码路径!

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

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

因此,我提醒您注意:手动旋转二进制文件实际上是剥夺了您与计算机和操作系统之间的每个安全允许。从字面上看 all 我们在工具中所取得的进步可以自动发现我们的程序 gone 的错误。

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

删除代码

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

setgid

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

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

     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]

我希望这只是浪费CPU周期,而不是跳过它们,但是更简单,因此对错误更安全,因为您无需手动弄清楚如何对跳转指令进行编码,包括弄清楚要在其中使用的偏移量/地址-您无需为无操作的滑板多想一想

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

添加代码

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

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

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

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

根据我的经验, 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] 不仅忽略最右边的列,而且也忽略最左边的列-因此,您可以为所有手动添加的行直接添加零地址,这样就可以解决。

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

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

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

答案 4 :(得分:2)

我的“ ci汇编程序反汇编程序”是我所知道的唯一一个基于以下原理设计的系统:无论反汇编是什么,它都必须重新组装为相同字节的字节。

https://github.com/albertvanderhorst/ciasdis

给出了两个小精灵可执行文件的示例,其中包括它们的分解和重新组装。它最初旨在能够修改由代码,解释代码,数据和图形字符组成的引导系统,并具有从实模式到保护模式的转换等优点。 (成功)。这些示例还演示了从可执行文件中提取文本,然后将其用于标签。 debian软件包用于Intel Pentium,但插件可用于Dec Alpha,6809、8086等。

拆卸的质量取决于您投入的精力。例如,如果您甚至没有提供有关它是elf文件的信息,则反汇编由单个字节组成,并且重组很简单。在示例中,我使用一个脚本来提取标签,并制作一个真正可用的可修改的反向工程程序。您可以插入或删除某些内容,然后将自动计算自动生成的符号标签。

根本没有关于二进制blob的假设,但是,对于Dec Alpha二进制文件,英特尔反汇编毫无用处。

答案 5 :(得分:1)

<强> miasm

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

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

  
      
  • 使用Elfesteem打开/修改/生成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,确定该图表并不容易,另请参阅:Indirect jump destination calculation

答案 6 :(得分:0)

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

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

如果有兴趣,请查看:Pin,Valgrind(或者这样做的项目:NaCl - Google的Native Client,也许是QEmu。)

答案 7 :(得分:0)

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