如何反汇编、修改然后重新组装 Linux 可执行文件?
- 2024-11-01 08:41:00
- admin 原创
- 56
问题描述:
有什么办法可以做到这一点吗?我使用过 objdump,但它产生的汇编输出无法被我所知道的任何汇编程序接受。我希望能够更改可执行文件中的指令,然后对其进行测试。
解决方案 1:
我认为没有任何可靠的方法可以做到这一点。机器代码格式非常复杂,比汇编文件更复杂。实际上不可能采用已编译的二进制文件(例如,ELF 格式)并生成将编译为相同(或足够相似)二进制文件的源汇编程序。要了解差异,请比较 GCC 直接编译到汇编程序的输出(gcc -S
)与可执行文件上的 objdump 的输出(objdump -D
)。
我能想到的有两个主要问题。首先,由于指针偏移等原因,机器码本身与汇编代码并不是一一对应的。
例如,考虑一下 Hello world 的 C 代码:
int main()
{
printf("Hello, world!
");
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 指令将其填充。如果它较长,您可能会遇到麻烦,并且可能不得不创建新函数并调用它们。
解决方案 2:
我使用文本编辑器完成此操作hexdump
。您必须非常熟悉机器代码和存储它的文件格式,并且灵活掌握“拆卸、修改然后重新组装”的含义。
如果您可以只进行“点更改”(重写字节,但不添加或删除字节),那么它会很容易(相对而言)。
您确实不想取代任何现有的指令,因为这样您就必须手动调整机器代码中相对于程序计数器的跳转/分支/加载/存储的任何受影响的绝对地址或相对偏移量,无论是在反汇编中可以清楚看到的硬编码立即值,还是动态计算的立即值,只能通过更改在使用前更改寄存器中的地址或偏移量的指令来修改。
您应该始终能够避免删除字节。对于更复杂的修改,可能需要添加字节,而且会变得更加困难。
步骤 0(准备)
在您实际正确地反汇编文件objdump -D
或使用您通常首先使用的任何工具来真正理解它并找到您需要更改的位置之后,您需要注意以下几点以帮助您找到要修改的正确字节:
您需要更改的字节的“地址”(相对于文件开头的偏移量)。
这些字节的当前原始值(此处
--show-raw-insn
的选项objdump
非常有用)。
您还需要检查它是否hexdump -R
在您的系统上有效。如果不行,那么对于其余步骤,请使用xxd
命令或类似命令,而不是hexdump
以下所有步骤(请查阅您使用的任何工具的文档,我hexdump
现在只在这个答案中解释,因为这是我熟悉的工具)。
步骤 1
使用 转储二进制文件的原始十六进制表示形式hexdump -Cv
。
第 2 步
打开hexdump
ed 文件并找到您要更改的地址处的字节。
输出速成课程hexdump -Cv
:
最左边的列是字节的地址(相对于二进制文件本身的开始,就像
objdump
提供的一样)。最右边的列(被
|
字符包围)只是字节的“人类可读”表示 - 与每个字节匹配的 ASCII 字符写在那里,代表.
所有不映射到 ASCII 可打印字符的字节。重要的内容介于两者之间 - 每个字节为两个十六进制数字,中间以空格分隔,每行 16 个字节。
注意:与 不同objdump -D
,它会给出每条指令的地址并根据指令的编码方式显示指令的原始十六进制,hexdump -Cv
它会完全按照文件中出现的顺序转储每个字节。这在机器上可能一开始会有点混乱,因为由于字节序差异,指令字节的顺序是相反的,当您期望在特定地址处找到特定字节时,这也会令人困惑。
步骤3
修改需要更改的字节——您显然需要弄清楚原始机器指令编码(而不是汇编助记符)并手动写入正确的字节。
注意:您不需要更改最右边一列中人类可读的表示形式。hexdump
当您“取消转储”它时将忽略它。
步骤4
使用“取消转储”修改后的十六进制转储文件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
似乎是我的无操作指令的一个很好的候选。我相应地修改了 ed 文件中的行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) 个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
、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 字符串”,则包括终止空字节),并在必要时覆盖使用它的机器代码中字符串的硬编码大小。
您还可以将所有不需要的指令替换为无操作指令。换句话说,我们可以将不需要的代码变成所谓的“无操作雪橇”:
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 指令进行字对齐,则该死点可以适合您的代码并且正确对齐)。然后您可以覆盖它。
如果你运气不好,就无法直接添加代码,而且也没有可以用机器代码填充的死区。这时,你基本上必须非常熟悉可执行格式,并希望你能在这些限制内找出一些可行的方法,在合理的时间内手动完成,并且有合理的机会不搞砸。
解决方案 3:
@mgiuca 从技术角度正确地回答了这个问题。事实上,将可执行程序反汇编成易于重新编译的汇编源并不是一件容易的事。
为了给讨论增添一些内容,有几种技术/工具可能值得探索,尽管它们在技术上很复杂。
静态/动态插桩。该技术需要分析可执行文件格式,插入/删除/替换特定汇编指令以达到特定目的,修复可执行文件中对变量/函数的所有引用,并生成新的修改后的可执行文件。我知道的一些工具有:PIN、Hijacker、PEBIL、DynamoRIO。考虑到将这些工具配置为与设计目的不同的目的可能会很棘手,并且需要了解可执行文件格式和指令集。
完整的可执行文件反编译。此技术尝试从可执行文件重建完整的汇编源代码。您可能想看一下在线反汇编程序,它试图完成这项工作。无论如何,您都会丢失有关不同源模块以及可能的函数/变量名称的信息。
可重定向反编译。该技术试图从可执行文件中提取更多信息,查看编译器指纹(即已知编译器生成的代码模式)和其他确定性内容。主要目标是从可执行文件重建更高级的源代码,如 C 源代码。这有时能够重新获得有关函数/变量名称的信息。考虑到编译源代码
-g
通常会提供更好的结果。您可能想尝试一下可重定向反编译器。
其中大部分来自漏洞评估和执行分析研究领域。它们是复杂的技术,并且这些工具通常不能立即使用。尽管如此,当尝试对某些软件进行逆向工程时,它们提供了宝贵的帮助。
解决方案 4:
要更改二进制程序集中的代码,通常有 3 种方法可以做到。
如果只是一些像常数这样的小东西,那么你只需用十六进制编辑器更改位置即可。假设你能找到它。
如果需要修改代码,则利用 LD_PRELOAD 覆盖程序中的某些函数。但如果函数不在函数表中,则此方法无效。
对要修复的函数的代码进行修改,使其直接跳转到通过 LD_PRELOAD 加载的函数,然后跳转回相同的位置(这是上述两种方法的组合)
当然,如果程序集进行任何类型的自我完整性检查,那么只有第二个会起作用。
编辑:如果不明显的话,那么玩弄二进制程序集是非常高级的开发人员的事情,除非你问的是非常具体的事情,否则你会很难在这里问到它。
解决方案 5:
瘴气
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
,它使用 读取 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 ),确定该图并不容易,另请参阅:间接跳转目的地计算
解决方案 6:
我的“ci 汇编器反汇编器”是我所知道的唯一系统,它是围绕以下原则设计的:无论反汇编是什么,它都必须重新组装为逐字节相同的二进制文件。
https://github.com/albertvanderhorst/ciasdis
给出了两个 elf-executable 及其反汇编和重组的示例。它最初设计用于修改引导系统,包括代码、解释代码、数据和图形字符,并具有从实模式到保护模式的转换等细节。(它成功了。)这些示例还演示了从可执行文件中提取文本,这些文本随后用于标签。debian 软件包适用于 Intel Pentium,但也有适用于 Dec Alpha、6809、8086 等的插件。
反汇编的质量取决于您付出的努力。例如,如果您甚至不提供它是 elf 文件的信息,反汇编将由单个字节组成,而重新组装则很简单。在示例中,我使用了一个提取标签的脚本,并制作了一个可修改的真正可用的逆向工程程序。您可以插入或删除某些内容,然后自动生成的符号标签将重新计算。使用提供的工具,将为跳转结束的所有位置生成标签,然后将标签用于这些跳转。这意味着在大多数情况下,您可以插入一条指令并重新组装修改后的源代码。
对二进制 blob 没有任何假设,但英特尔反汇编对于 Dec Alpha 二进制文件来说当然用处不大。
解决方案 7:
您可能还有兴趣做另一件事:
二进制检测 - 改变现有代码
如果有兴趣,请查看:Pin、Valgrind(或正在做这样的项目:NaCl - Google 的 Native Client,也许是 QEmu。)
解决方案 8:
您可以在 ptrace(换句话说,像 gdb 这样的调试器)的监督下运行可执行文件,这样就可以随时控制执行,而无需修改实际文件。当然,这需要常见的编辑技能,例如找到您想要影响的特定指令在可执行文件中的位置。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件