无论如何这可以做到吗?我已经使用了objdump但是这不会产生任何我知道的汇编程序可以接受的汇编输出。我希望能够在可执行文件中更改指令,然后再对其进行测试。
答案 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种方法可以执行此操作。
如果议会进行任何形式的自我完整性检查,那么只有第二个才能奏效。
编辑:如果不是很明显那么玩二进制程序集是非常高级的开发人员的东西,你将很难在这里询问它,除非它是你要求的特定事情。
答案 2 :(得分:7)
@mgiuca从技术角度正确地解决了这个问题。实际上,将可执行程序反汇编成易于重新编译的汇编源并不是一件容易的事。
为了在讨论中添加一些内容,有一些技术/工具可供探讨,但它们技术上很复杂。
-g
编译源通常可以提供更好的结果。您可能想尝试Retargetable Decompiler。其中大部分来自脆弱性评估和执行分析研究领域。它们是复杂的技术,通常不能立即使用工具。然而,在尝试对某些软件进行逆向工程时,它们提供了宝贵的帮助。
答案 3 :(得分:2)
我使用hexdump
和文本编辑器来完成此操作。您必须真的熟悉机器代码和存储它的文件格式,并灵活地算作“反汇编,修改然后重新组装”。
如果您只需要“更改点”(重写字节,而不添加或删除字节)就可以了(相对而言)。
您真的不想替换任何现有的指令,因为那样您将不得不在机器代码内手动调整任何受影响的相对偏移量,以便相对于跳转/分支/装载/存储程序计数器,包括硬编码的立即值和通过寄存器计算的值。
您应该始终能够不删除字节而逃脱。对于更复杂的修改,添加字节可能是必需的,并且会变得更加困难。
使用objdump -D
或您通常首先用来真正理解文件并找到需要更改的地方正确地 正确地分解了文件之后,您需要请注意以下事项,以帮助您找到要修改的正确字节:
--show-raw-insn
的{{1}}选项在这里确实很有帮助)。使用objdump
转储二进制文件的原始十六进制表示形式。
打开hexdump -Cv
版本的文件,然后在您要更改的地址中找到字节。
hexdump
输出中的速成课程:
hexdump -Cv
提供的一样。)objdump
字符包围)只是字节的“人类可读”表示-与每个字节匹配的ASCII字符都写入那里,其中|
代表所有字节无法映射为ASCII可打印字符的字节。要当心:与.
不同的是,objdump -D
会为您提供每条指令的地址,并根据记录的编码方式显示该指令的原始十六进制,而hexdump -Cv
会完全按照其顺序转储每个字节出现在文件中。首先,由于字节顺序差异,在指令字节以相反顺序排列的机器上,这可能会造成一些混乱。当您期望将特定字节作为特定地址时,这也会使您迷失方向。
修改需要更改的字节-您显然需要弄清楚原始机器指令的编码(而不是汇编助记符),并手动写入正确的字节。
注意:您不需要,需要在最右侧的列中更改易于理解的表示形式。 hexdump
将在您“卸载”它时将其忽略。
使用hexdump -R
“取消转储”已修改的hexdump文件。
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
基本上应该这样:
我认为它不会生成文本反汇编表示,您可能需要遍历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这样的调试器)并以这种方式控制执行,而无需修改实际文件。当然,需要通常的编辑技能,例如查找您想要影响的特定指令在可执行文件中的位置。