如何调用存储在char数组中的机器码?
How to call machine code stored in char array?
我正在尝试调用本地机器语言代码。这是我目前所拥有的(出现总线错误):
char prog[] = {'\xc3'}; // x86 ret instruction
int main()
{
typedef double (*dfunc)();
dfunc d = (dfunc)(&prog[0]);
(*d)();
return 0;
}
它确实正确地调用了函数,并且到达了 ret 指令。但是当它尝试执行 ret 指令时,出现 SIGBUS 错误。是因为我在未清除执行权限的页面上执行代码还是类似原因?
那么我做错了什么?
一个明显的错误是 \xc3
没有返回您声称它返回的 double
。
第一个问题可能是存储 prog 数据的位置不可执行。
至少在 Linux 上,生成的二进制文件会将全局变量的内容放入 "data" segment or here, which is not executable in most normal cases。
第二个问题可能是您调用的代码在某些方面无效。在 C 中有调用方法的特定过程,称为 calling convention(例如,您可能正在使用 "cdecl")。被调用函数仅 "ret" 可能还不够。它可能还需要做一些堆栈清理等,否则程序会出现意外行为。一旦你解决了第一个问题,这可能会证明是一个问题。
您需要调用memprotect 才能使prog 所在的页面可执行。以下代码确实进行了此调用,并且可以执行 prog.
中的文本
#include <unistd.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>
char prog[] = {
0x55, // push %rbp
0x48, 0x89, 0xe5, // mov %rsp,%rbp
0xf2, 0x0f, 0x10, 0x05, 0x00, 0x00, 0x00,
//movsd 0x0(%rip),%xmm0 # c <x+0xc>
0x00,
0x5d, // pop %rbp
0xc3, // retq
};
int main()
{
long pagesize = sysconf(_SC_PAGE_SIZE);
long page_no = (long)prog/pagesize;
int res = mprotect((void*)(page_no*pagesize), (long)page_no+sizeof(prog), PROT_EXEC|PROT_READ|PROT_WRITE);
if(res)
{
fprintf(stderr, "mprotect error:%d\n", res);
return 1;
}
typedef double (*dfunc)(void);
dfunc d = (dfunc)(&prog[0]);
double x = (*d)();
printf("x=%f\n", x);
fflush(stdout);
return 0;
}
基本上这已被取缔,因为它是对病毒编写者的公开邀请。但是您可以在直接 C 中分配和缓冲并使用本机机器代码进行设置——这没问题。问题是调用它。虽然您可以尝试使用缓冲区的地址设置一个函数指针并调用它,但这不太可能起作用,并且如果您以某种方式设法哄骗它做您想做的事情,很可能会在下一个版本的编译器上中断.所以最好的办法是简单地求助于一些内联汇编,设置 return 并跳转到自动生成的代码。但是,如果系统对此进行保护,您将不得不找到规避保护的方法,正如 Rudi 在他的回答中所描述的那样(但非常特定于一个特定系统)。
正如大家已经说过的,您必须确保 prog[]
是可执行的,但是除非您正在编写 JIT 编译器,否则正确的方法是将符号放入可执行区域,或者通过使用链接器脚本或在编译器允许的情况下在 C 代码中指定部分,例如:
const char prog[] __attribute__((section(".text"))) = {...}
几乎所有 C 编译器都允许您通过在代码中嵌入常规汇编语言来执行此操作。当然,它是对 C 的非标准扩展,但编译器编写者认识到它通常是必需的。作为非标准扩展,您必须阅读编译器手册并检查如何操作,但 GCC "asm" extension 是一种相当标准的方法。
void DoCheck(uint32_t dwSomeValue)
{
uint32_t dwRes;
// Assumes dwSomeValue is not zero.
asm ("bsfl %1,%0"
: "=r" (dwRes)
: "r" (dwSomeValue)
: "cc");
assert(dwRes > 3);
}
由于在汇编程序中很容易破坏堆栈,因此编译器通常还允许您识别将用作汇编程序一部分的寄存器。然后,编译器可以确保该函数的其余部分避开这些寄存器。
如果您自己编写汇编代码,则没有充分的理由将该汇编程序设置为字节数组。这不仅仅是代码味道 - 我想说这是一个真正的错误,只有在不知道 "asm" 扩展时才会发生,这是将汇编程序嵌入 C 的正确方法。
我正在尝试调用本地机器语言代码。这是我目前所拥有的(出现总线错误):
char prog[] = {'\xc3'}; // x86 ret instruction
int main()
{
typedef double (*dfunc)();
dfunc d = (dfunc)(&prog[0]);
(*d)();
return 0;
}
它确实正确地调用了函数,并且到达了 ret 指令。但是当它尝试执行 ret 指令时,出现 SIGBUS 错误。是因为我在未清除执行权限的页面上执行代码还是类似原因?
那么我做错了什么?
一个明显的错误是 \xc3
没有返回您声称它返回的 double
。
第一个问题可能是存储 prog 数据的位置不可执行。
至少在 Linux 上,生成的二进制文件会将全局变量的内容放入 "data" segment or here, which is not executable in most normal cases。
第二个问题可能是您调用的代码在某些方面无效。在 C 中有调用方法的特定过程,称为 calling convention(例如,您可能正在使用 "cdecl")。被调用函数仅 "ret" 可能还不够。它可能还需要做一些堆栈清理等,否则程序会出现意外行为。一旦你解决了第一个问题,这可能会证明是一个问题。
您需要调用memprotect 才能使prog 所在的页面可执行。以下代码确实进行了此调用,并且可以执行 prog.
中的文本#include <unistd.h>
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/mman.h>
char prog[] = {
0x55, // push %rbp
0x48, 0x89, 0xe5, // mov %rsp,%rbp
0xf2, 0x0f, 0x10, 0x05, 0x00, 0x00, 0x00,
//movsd 0x0(%rip),%xmm0 # c <x+0xc>
0x00,
0x5d, // pop %rbp
0xc3, // retq
};
int main()
{
long pagesize = sysconf(_SC_PAGE_SIZE);
long page_no = (long)prog/pagesize;
int res = mprotect((void*)(page_no*pagesize), (long)page_no+sizeof(prog), PROT_EXEC|PROT_READ|PROT_WRITE);
if(res)
{
fprintf(stderr, "mprotect error:%d\n", res);
return 1;
}
typedef double (*dfunc)(void);
dfunc d = (dfunc)(&prog[0]);
double x = (*d)();
printf("x=%f\n", x);
fflush(stdout);
return 0;
}
基本上这已被取缔,因为它是对病毒编写者的公开邀请。但是您可以在直接 C 中分配和缓冲并使用本机机器代码进行设置——这没问题。问题是调用它。虽然您可以尝试使用缓冲区的地址设置一个函数指针并调用它,但这不太可能起作用,并且如果您以某种方式设法哄骗它做您想做的事情,很可能会在下一个版本的编译器上中断.所以最好的办法是简单地求助于一些内联汇编,设置 return 并跳转到自动生成的代码。但是,如果系统对此进行保护,您将不得不找到规避保护的方法,正如 Rudi 在他的回答中所描述的那样(但非常特定于一个特定系统)。
正如大家已经说过的,您必须确保 prog[]
是可执行的,但是除非您正在编写 JIT 编译器,否则正确的方法是将符号放入可执行区域,或者通过使用链接器脚本或在编译器允许的情况下在 C 代码中指定部分,例如:
const char prog[] __attribute__((section(".text"))) = {...}
几乎所有 C 编译器都允许您通过在代码中嵌入常规汇编语言来执行此操作。当然,它是对 C 的非标准扩展,但编译器编写者认识到它通常是必需的。作为非标准扩展,您必须阅读编译器手册并检查如何操作,但 GCC "asm" extension 是一种相当标准的方法。
void DoCheck(uint32_t dwSomeValue)
{
uint32_t dwRes;
// Assumes dwSomeValue is not zero.
asm ("bsfl %1,%0"
: "=r" (dwRes)
: "r" (dwSomeValue)
: "cc");
assert(dwRes > 3);
}
由于在汇编程序中很容易破坏堆栈,因此编译器通常还允许您识别将用作汇编程序一部分的寄存器。然后,编译器可以确保该函数的其余部分避开这些寄存器。
如果您自己编写汇编代码,则没有充分的理由将该汇编程序设置为字节数组。这不仅仅是代码味道 - 我想说这是一个真正的错误,只有在不知道 "asm" 扩展时才会发生,这是将汇编程序嵌入 C 的正确方法。