通常不需要使用 asm,你可以简单地通过一个函数指针,让编译器处理细节。
你确实需要在将机器代码复制到缓冲区之后使用__builtin___clear_cache(buf, buf+len),然后再取消引用指向它的函数指针,否则它可以是optimized away as a dead store.。 x86 具有连贯的指令缓存,因此它不会编译为任何额外的指令,但您仍然需要它,以便优化器知道发生了什么。
static inline
int func(char *dest, int len) {
__builtin___clear_cache(dest, dest+len); // no instructions on x86 but still needed
int ret = ((int (*)(void))dest)(); // cast to function pointer and deref
return ret;
}
compiles with GCC9.1 -O2 -m32 to
func(char*, int):
jmp [DWORD PTR [esp+4]] # tailcall
此外,您实际上并不需要复制字符串,您只需mprotect 或VirtualProtect 它所在的页面即可使其可执行。但是如果你想确保它确实停在第一个 0 字节来测试你的 shellcode,那么一定要复制它。
如果你仍然坚持使用 inline asm,你应该知道 gcc inline asm 是一个复杂的东西。此外,如果您希望函数返回,您应该确保它遵循调用约定,特别是它保留了它应该保留的寄存器。
AT&T 语法是 op src, dst,因此您的 mov 实际上是全局符号 dest 的存储。
也就是说,这里是问题的答案:
int ret;
__asm__ __volatile__ ("call *%0" : "=a" (ret) : "0" (dest) : "ecx", "edx", "memory");
解释:https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
call *%0 = %0 指代第一个替换参数,* 是用于间接调用的标准 gas 语法
"=a" (ret) = eax 寄存器中的输出参数应分配给块后的变量ret
"0" (dest) = 输入参数与输出参数0(即eax)应在块之前从dest 加载
"ecx", "edx" = 告诉编译器这些寄存器可能会被 asm 块改变,按照正常的调用约定。
"memory" = 告诉编译器 asm 块可能会对内存进行未指定的修改,所以不要缓存任何东西
请注意,在 x86-64 System V (Linux / OS X) 中,像这样从内联汇编进行函数调用是不安全的。无法在 RSP 下方的红色区域声明破坏者。