在 Linux 中,您可以使用mprotect() 来启用/禁用运行时代码的文本部分写保护;请参阅man 2 mprotect 中的备注部分。
这是一个真实的例子。但是,首先要注意:
我认为这只是概念实现的证明,而不是我在现实世界的应用程序中使用过的东西。在某种高性能库中使用它可能看起来很诱人,但根据我的经验,更改库的 API(或范例/方法)通常会产生更好的结果 - 并且更少难以调试的错误。
考虑以下六个文件:
foo1.c:
int foo1(const int a, const int b) { return a*a - 2*a*b + b*b; }
foo2.c:
int foo2(const int a, const int b) { return a*a + b*b; }
foo.h.header:
#ifndef FOO_H
#define FOO_H
extern int foo1(const int a, const int b);
extern int foo2(const int a, const int b);
foo.h.footer:
#endif /* FOO_H */
main.c:
#include <unistd.h>
#include <sys/mman.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include "foo.h"
int text_copy(const void *const target,
const void *const source,
const size_t length)
{
const long page = sysconf(_SC_PAGESIZE);
void *start = (char *)target - ((long)target % page);
size_t bytes = length + (size_t)((long)target % page);
/* Verify sane page size. */
if (page < 1L)
return errno = ENOTSUP;
/* Although length should not need to be a multiple of page size,
* adjust it up if need be. */
if (bytes % (size_t)page)
bytes = bytes + (size_t)page - (bytes % (size_t)page);
/* Disable write protect on target pages. */
if (mprotect(start, bytes, PROT_READ | PROT_WRITE | PROT_EXEC))
return errno;
/* Copy code.
* Note: if the target code is being executed, we're in trouble;
* this offers no atomicity guarantees, so other threads may
* end up executing some combination of old/new code.
*/
memcpy((void *)target, (const void *)source, length);
/* Re-enable write protect on target pages. */
if (mprotect(start, bytes, PROT_READ | PROT_EXEC))
return errno;
/* Success. */
return 0;
}
int main(void)
{
printf("foo1(): %d bytes at %p\n", foo1_SIZE, foo1_ADDR);
printf("foo2(): %d bytes at %p\n", foo2_SIZE, foo2_ADDR);
printf("foo1(3, 5): %d\n", foo1(3, 5));
printf("foo2(3, 5): %d\n", foo2(3, 5));
if (foo2_SIZE < foo1_SIZE) {
printf("Replacing foo1() with foo2(): ");
if (text_copy(foo1_ADDR, foo2_ADDR, foo2_SIZE)) {
printf("%s.\n", strerror(errno));
return 1;
}
printf("Done.\n");
} else {
printf("Replacing foo2() with foo1(): ");
if (text_copy(foo2_ADDR, foo1_ADDR, foo1_SIZE)) {
printf("%s.\n", strerror(errno));
return 1;
}
printf("Done.\n");
}
printf("foo1(3, 5): %d\n", foo1(3, 5));
printf("foo2(3, 5): %d\n", foo2(3, 5));
return 0;
}
function-info.bash:
#!/bin/bash
addr_prefix=""
addr_suffix="_ADDR"
size_prefix=""
size_suffix="_SIZE"
export LANG=C
export LC_ALL=C
nm -S "$@" | while read addr size kind name dummy ; do
[ -n "$addr" ] || continue
[ -n "$size" ] || continue
[ -z "$dummy" ] || continue
[ "$kind" = "T" ] || continue
[ "$name" != "${name#[A-Za-z]}" ] || continue
printf '#define %s ((void *)0x%sL)\n' "$addr_prefix$name$addr_suffix" "$addr"
printf '#define %s %d\n' "$size_prefix$name$size_suffix" "0x$size"
done || exit $?
记得使用chmod u+x ./function-info.bash使其可执行
首先,使用有效大小但无效地址编译源代码:
gcc -W -Wall -O3 -c foo1.c
gcc -W -Wall -O3 -c foo2.c
( cat foo.h.header ; ./function-info.bash foo1.o foo2.o ; cat foo.h.footer) > foo.h
gcc -W -Wall -O3 -c main.c
尺寸正确,但地址不正确,因为代码尚未链接。相对于最终的二进制文件,目标文件内容通常在链接时重新定位。因此,链接源以获得示例可执行文件,example:
gcc -W -Wall -O3 main.o foo1.o foo2.o -o example
提取正确的(大小和)地址:
( cat foo.h.header ; ./function-info.bash example ; cat foo.h.footer) > foo.h
重新编译并链接,
gcc -W -Wall -O3 -c main.c
gcc -W -Wall -O3 foo1.o foo2.o main.o -o example
并验证现在的常量是否匹配:
mv -f foo.h foo.h.used
( cat foo.h.header ; ./function-info.bash example ; cat foo.h.footer) > foo.h
cmp -s foo.h foo.h.used && echo "Done." || echo "Recompile and relink."
由于高度优化 (-O3),使用常量的代码可能会改变大小,需要再次重新编译重新链接。如果最后一行输出"Recompile and relink",只需重复最后两步,即五行。
(请注意,由于 foo1.c 和 foo2.c 不使用 foo.h 中的常量,它们显然不需要重新编译。)
在 x86_64 (GCC-4.6.3-1ubuntu5) 上,运行 ./example 输出
foo1(): 21 bytes at 0x400820
foo2(): 10 bytes at 0x400840
foo1(3, 5): 4
foo2(3, 5): 34
Replacing foo1() with foo2(): Done.
foo1(3, 5): 34
foo2(3, 5): 34
这表明foo1() 函数确实被替换了。请注意,较长的函数总是被较短的函数替换,因为我们不能覆盖这两个函数之外的任何代码。
您可以修改这两个函数来验证这一点;只需记住重复整个过程(以便在main() 中使用正确的_SIZE 和_ADDR 常量)。
只是为了咯咯笑,这是上面生成的foo.h:
#ifndef FOO_H
#define FOO_H
extern int foo1(const int a, const int b);
extern int foo2(const int a, const int b);
#define foo1_ADDR ((void *)0x0000000000400820L)
#define foo1_SIZE 21
#define foo2_ADDR ((void *)0x0000000000400840L)
#define foo2_SIZE 10
#define main_ADDR ((void *)0x0000000000400610L)
#define main_SIZE 291
#define text_copy_ADDR ((void *)0x0000000000400850L)
#define text_copy_SIZE 226
#endif /* FOO_H */
您可能希望使用更智能的 scriptlet,例如 awk 一个使用 nm -S 获取所有函数名称、地址和大小,并在头文件中仅替换现有定义的值,以生成您的头文件。我会使用 Makefile 和一些辅助脚本。
补充说明:
-
功能代码按原样复制,不进行重定位等。 (这意味着如果替换函数的机器代码包含绝对跳转,则在原始代码中继续执行。选择了这些示例函数,因为它们不太可能有绝对跳转。运行objdump -d foo1.o foo2.o 从大会。)
如果您使用该示例只是为了研究如何在正在运行的进程中修改可执行代码,那是无关紧要的。但是,如果您在此示例之上构建运行时功能替换方案,您可能需要对替换代码使用与位置无关的代码(请参阅 the GCC manual 了解您架构的相关选项)或自己进行重定位。
如果另一个线程或信号处理程序执行正在修改的代码,那么您就遇到了严重的麻烦。你会得到不确定的结果。不幸的是,一些库会启动额外的线程,这可能不会阻塞所有可能的信号,因此在修改可能由信号处理程序运行的代码时要格外小心。
-
不要假设编译器以特定方式编译代码或使用特定组织。我的示例使用单独的编译单元,以避免编译器可能在相似函数之间共享代码的情况。
此外,它还直接检查最终的可执行二进制文件,以获取要修改的大小和地址,从而修改整个函数实现。所有的验证都应该在目标文件或最终的可执行文件上进行,并进行反汇编,而不是仅仅查看 C 代码。
将任何依赖地址和大小常量的代码放入单独的编译单元中,可以更轻松、更快速地重新编译和重新链接二进制文件。 (您只需要直接重新编译使用常量的代码,您甚至可以对该代码使用较少的优化,以消除额外的重新编译-重新链接周期,而不会影响整体代码质量。)
在我的main.c 中,提供给mprotect() 的地址和长度都是页面对齐的(基于用户参数)。文件说只有地址必须是。由于保护是以页面为单位的,因此确保长度是页面大小的倍数并没有什么坏处。
1234563
无论如何,如果您有任何疑问,我很乐意尝试澄清上述内容。
附录:
事实证明,使用 GNU 扩展 dl_iterate_phdr() 您可以轻松地启用/禁用所有文本部分的写保护:
#define _GNU_SOURCE
#include <unistd.h>
#include <dlfcn.h>
#include <sys/mman.h>
#include <link.h>
static int do_write_protect_text(struct dl_phdr_info *info, size_t size, void *data)
{
const int protect = (data) ? PROT_READ | PROT_EXEC : PROT_READ | PROT_WRITE | PROT_EXEC;
size_t page;
size_t i;
page = sysconf(_SC_PAGESIZE);
if (size < sizeof (struct dl_phdr_info))
return ENOTSUP;
/* Ignore libraries. */
if (info->dlpi_name && info->dlpi_name[0] != '\0')
return 0;
/* Loop over each header. */
for (i = 0; i < (size_t)info->dlpi_phnum; i++)
if ((info->dlpi_phdr[i].p_flags & PF_X)) {
size_t ptr = (size_t)info->dlpi_phdr[i].p_vaddr;
size_t len = (size_t)info->dlpi_phdr[i].p_memsz;
/* Start at the beginning of the relevant page, */
if (ptr % page) {
len += ptr % page;
ptr -= ptr % page;
}
/* and use full pages. */
if (len % page)
len += page - (len % page);
/* Change protections. Ignore unmapped sections. */
if (mprotect((void *)ptr, len, protect))
if (errno != ENOMEM)
return errno;
}
return 0;
}
int write_protect_text(int protect)
{
int result;
result = dl_iterate_phdr(do_write_protect_text, (void *)(long)protect);
if (result)
errno = result;
return result;
}
这是一个示例程序,您可以使用它来测试上述write_protect_text() 函数:
#define _POSIX_C_SOURCE 200809L
int dump_smaps(void)
{
FILE *in;
char *line = NULL;
size_t size = 0;
in = fopen("/proc/self/smaps", "r");
if (!in)
return errno;
while (getline(&line, &size, in) > (ssize_t)0)
if ((line[0] >= '0' && line[0] <= '9') ||
(line[0] >= 'a' && line[0] <= 'f'))
fputs(line, stdout);
free(line);
if (!feof(in) || ferror(in)) {
fclose(in);
return errno = EIO;
}
if (fclose(in))
return errno = EIO;
return 0;
}
int main(void)
{
printf("Initial mappings:\n");
dump_smaps();
if (write_protect_text(0)) {
fprintf(stderr, "Cannot disable write protection on text sections: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
printf("\nMappings with write protect disabled:\n");
dump_smaps();
if (write_protect_text(1)) {
fprintf(stderr, "Cannot enable write protection on text sections: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
printf("\nMappings with write protect enabled:\n");
dump_smaps();
return EXIT_SUCCESS;
}
示例程序在更改文本部分写保护之前和之后转储/proc/self/smaps,表明它确实在所有文本部分(程序代码)上启用/禁用写保护。它不会尝试更改动态加载库的写保护。使用 Ubuntu 3.8.0-35-generic 内核对其进行了测试,可以在 x86-64 上运行。