你的代码不是位置无关的,即使是这样,你也没有正确的重定位来将其移动到任意位置。你对printf
(或任何其他函数)的调用将使用pc相对寻址进行(通过PLT,但这与此处的重点无关)。这意味着生成的调用printf的指令不是对静态地址的调用,而是“从当前指令指针开始调用函数X个字节”。由于你移动了代码,所以调用是在错误的地址上完成的。(我假设这里是i386或amd64,但通常这是一个安全的假设,使用奇怪平台的人通常会提到)。
更具体地说,x86有两个不同的函数调用指令。其中一个是相对于指令指针的调用,它通过将一个值添加到当前指令指针来确定函数调用的目的地。这是最常用的函数调用。第二个指令是调用寄存器或内存位置内部的指针。编译器很少使用它,因为它需要更多的内存间接引用并阻止管道。共享库的实现方式(你对printf
的调用实际上将转到共享库中)是,对于你在自己的代码之外进行的每个函数调用,编译器都会在你的代码附近插入假函数(这就是我上面提到的PLT)。你的代码对此假函数进行了正常的pc相对调用,假函数会找到printf
的真实地址并调用它。虽然这并不重要。几乎所有正常的函数调用都将是pc相对的,并且将失败。在这种代码中唯一的希望是函数指针。
你可能还会遇到对可执行mprotect
的限制。检查mprotect
的返回值,在我的系统上,你的代码不起作用的另一个原因是: mprotect
不允许我这样做。可能是因为malloc
的后端内存分配器有额外的限制,防止其内存受到执行保护。这引出了下一个问题:
通过调用mprotect
来破坏未被你管理的内存,包括从malloc
获取的内存。你应该只对通过mmap
从内核自己获取的东西进行mprotect
。
以下是演示如何使此代码工作的版本(在我的系统上):
#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <err.h>
int
foo(int x, int (*fn)(const char *, ...))
{
fn("%d\n", x);
return 42;
}
int
bar(int x)
{
return 0;
}
int
main(int argc, char **argv)
{
size_t foo_size = (char *)bar - (char *)foo;
int ps = getpagesize();
void *buf_ptr = mmap(NULL, ps, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANON|MAP_PRIVATE, -1, 0);
if (buf_ptr == MAP_FAILED)
err(1, "mmap");
memcpy(buf_ptr, foo, foo_size);
int (*ptr)(int, int (*)(const char *, ...)) = buf_ptr;
printf("%d\n", ptr(3, printf));
return 0;
}
我在这里滥用了编译器生成函数调用代码的知识。通过使用函数指针,我强制它生成一个不是pc-relative的调用指令。此外,我自己管理内存分配,以便从一开始就获得正确的权限,并且不会遇到任何可能由brk
引起的限制。作为奖励,我们进行错误处理,实际上帮助我找到了第一个版本中的一个错误,并纠正了其他一些小错误(如缺少包含文件),这使我能够在编译器中启用警告并捕获另一个潜在的问题。
如果您想深入了解此内容,可以尝试以下方法。我添加了两个版本的函数:
int
oldfoo(int x)
{
printf("%d\n", x);
return 42;
}
int
foo(int x, int (*fn)(const char *, ...))
{
fn("%d\n", x);
return 42;
}
编译整个东西并反汇编它:
$ cc -Wall -o foo foo.c
$ objdump -S foo | less
我们现在可以看一下生成的两个函数:
0000000000400680 <oldfoo>:
400680: 55 push %rbp
400681: 48 89 e5 mov %rsp,%rbp
400684: 48 83 ec 10 sub $0x10,%rsp
400688: 89 7d fc mov %edi,-0x4(%rbp)
40068b: 8b 45 fc mov -0x4(%rbp),%eax
40068e: 89 c6 mov %eax,%esi
400690: bf 30 08 40 00 mov $0x400830,%edi
400695: b8 00 00 00 00 mov $0x0,%eax
40069a: e8 91 fe ff ff callq 400530 <printf@plt>
40069f: b8 2a 00 00 00 mov $0x2a,%eax
4006a4: c9 leaveq
4006a5: c3 retq
00000000004006a6 <foo>:
4006a6: 55 push %rbp
4006a7: 48 89 e5 mov %rsp,%rbp
4006aa: 48 83 ec 10 sub $0x10,%rsp
4006ae: 89 7d fc mov %edi,-0x4(%rbp)
4006b1: 48 89 75 f0 mov %rsi,-0x10(%rbp)
4006b5: 8b 45 fc mov -0x4(%rbp),%eax
4006b8: 48 8b 55 f0 mov -0x10(%rbp),%rdx
4006bc: 89 c6 mov %eax,%esi
4006be: bf 30 08 40 00 mov $0x400830,%edi
4006c3: b8 00 00 00 00 mov $0x0,%eax
4006c8: ff d2 callq *%rdx
4006ca: b8 2a 00 00 00 mov $0x2a,%eax
4006cf: c9 leaveq
4006d0: c3 retq
对于printf
函数调用的指令是“e8 91 fe ff ff”。这是一个相对于程序计数器(pc)的函数调用,偏移量为0xfffffe91字节,它被视为带符号32位值,计算中使用的指令指针是下一条指令的地址。因此,0x40069f(下一条指令)-0x16f(0xfffffe91在前面,带符号数学表示为0x16f字节后退)得到地址0x400530,在反汇编代码中我找到了这个地址处的内容:
0000000000400530 <printf@plt>:
400530: ff 25 ea 0a 20 00 jmpq *0x200aea(%rip) # 601020 <_GLOBAL_OFFSET_TABLE_+0x20>
400536: 68 01 00 00 00 pushq $0x1
40053b: e9 d0 ff ff ff jmpq 400510 <_init+0x28>
这是我之前提到的神奇的“假函数”,不要深究它是如何工作的。对于共享库的运行,它是必需的,我们现在只需要知道这些。
第二个函数生成函数调用指令“ff d2”。这意味着“调用存储在rdx寄存器内地址的函数”。没有PC相对寻址,这就是它能够工作的原因。
foo()
和bar()
中漏了返回语句。 - John Zwinck