为匿名函数创建一个 C 语言插件是否实用?

Is it practical to create a C language addon for anonymous functions?

我知道 C 编译器能够获取独立代码,并从中为它们所针对的特定系统生成独立的 shellcode。

例如,在 anon.c 中给出以下内容:

int give3() {
    return 3;
}

我可以运行

gcc anon.c -o anon.obj -c
objdump -D anon.obj

这给了我(在 MinGW 上):

anon1.obj:     file format pe-i386


Disassembly of section .text:

00000000 <_give3>:
   0:   55                      push   %ebp
   1:   89 e5                   mov    %esp,%ebp
   3:   b8 03 00 00 00          mov    [=12=]x3,%eax
   8:   5d                      pop    %ebp
   9:   c3                      ret    
   a:   90                      nop
   b:   90                      nop

所以我可以像这样制作 main:

main.c

#include <stdio.h>
#include <stdint.h>

int main(int argc, char **argv)
{
    uint8_t shellcode[] = {
        0x55,
        0x89, 0xe5,
        0xb8, 0x03, 0x00, 0x00, 0x00,
        0x5d, 0xc3,
        0x90,
        0x90
    };

    int (*p_give3)() = (int (*)())shellcode;
    printf("%d.\n", (*p_give3)());
}

我的问题是,将不引用任何不在其范围内或参数中的自包含匿名函数的转换过程自动化是否可行?

例如:

#include <stdio.h>
#include <stdint.h>

int main(int argc, char **argv)
{
    uint8_t shellcode[] = [@[
        int anonymous() {
            return 3;
        }
    ]];

    int (*p_give3)() = (int (*)())shellcode;
    printf("%d.\n", (*p_give3)());
}

哪个会将文本编译成 shellcode,并将其放入缓冲区?

我问的原因是因为我真的很喜欢写C,但是做pthreads,callbacks非常痛苦;一旦你比 C 高出一步以获得 "lambdas" 的概念,你就会失去你的语言的 ABI(例如,C++ 有 lambda,但你在 C++ 中所做的一切突然都依赖于实现),并且 "Lisplike" 脚本插件(例如插入 Lisp、Perl、JavaScript/V8,任何其他 运行 已经知道如何概括回调的时间)使回调变得非常容易,但也比到处乱扔 shellcode 昂贵得多。

如果这可行,那么可以将只调用一次的函数放入调用它的函数体中,从而减少全局范围污染。这也意味着您不需要为每个目标系统手动生成 shellcode,因为每个系统的 C 编译器已经知道如何将自包含的 C 转换为汇编,所以您为什么要为它做这件事,并破坏您的可读性拥有一堆二进制 blob 的代码。

所以问题是:这是否实用(对于完全自包含的函数,例如,即使他们想调用 puts,puts 也必须作为参数或在哈希 table/struct 中给出争论)?还是有什么问题阻止了它的实用性?

这似乎是可能的,但不必要的复杂:

shellcode.c

 int anon() { return 3; }

main.c

 ...
 uint8_t shellcode[] = {
 #include anon.shell
};

int (*p_give3)() = (int (*)())shellcode;
printf("%d.\n", (*p_give3)());   

生成文件:

anon.shell:
   gcc anon.c -o anon.obj -c; objdump -D anon.obj | extractShellBytes.py anon.shell

其中 extractShellBytes.py 是您编写的脚本,它仅打印来自 objdump 输出的原始逗号分隔代码字节。

Apple 在 clang 中实现了一个非常相似的功能,称为 "blocks"。这是一个示例:

int main(int argc, char **argv)
{
    int (^blk_give3)(void) = ^(void) {
        return 3;
    };

    printf("%d.\n", blk_give3());

    return 0;
}

更多信息:

I know that C compilers are capable of taking standalone code, and generate standalone shellcode out of it for the specific system they are targeting.

将源代码转换为机器代码就是编译。 Shellcode 是具有特定约束的机器代码,其中 none 适用于此用例。您只需要像编译器在正常编译函数时生成的普通机器代码。

AFAICT,你想要的是正是你从static foo(int x){ ...; }得到的,然后将foo作为函数指针传递。即在可执行文件的代码部分中附有标签的机器代码块。

为了将编译器生成的机器代码放入一个数组中而跳来跳去甚至不值得移植性的缺点(特别是在确保数组在可执行内存中)。


看来您想要避免的唯一一件事就是拥有一个具有自己名称的单独定义的函数。这是一个非常小的好处,无法证明您在问题中建议的任何事情都是合理的。 AFAIK,在 ISO C11 中没有实现它的好方法,但是:

Some compilers support nested functions as a GNU extension:

这可以编译(使用 gcc6.2)。 On Godbolt, I used -xc to compile it as C, not C++.。它也可以使用 ICC17 编译,但不能使用 clang3.9。

#include <stdlib.h>

void sort_integers(int *arr, size_t len)
{
  int bar(){return 3;}  // gcc warning: ISO C forbids nested functions [-Wpedantic]

  int cmp(const void *va, const void *vb) {
    const int *a=va, *b=vb;       // taking const int* args directly gives a warning, which we could silence with a cast
    return *a > *b;
  }

  qsort(arr, len, sizeof(int), cmp);
}

汇编输出为:

cmp.2286:
    mov     eax, DWORD PTR [rsi]
    cmp     DWORD PTR [rdi], eax
    setg    al
    movzx   eax, al
    ret
sort_integers:
    mov     ecx, OFFSET FLAT:cmp.2286
    mov     edx, 4
    jmp     qsort

请注意,没有发出 bar() 的定义,因为它未被使用。

未经优化构建的带有嵌套函数的程序将具有可执行堆栈。 (原因如下)。因此,如果您使用它,如果您关心安全性,请确保使用优化。


顺便说一句,嵌套函数甚至可以访问其父函数中的变量(如 lambas)。cmp 更改为执行 return len 的函数会导致 this highly surprising asm:

__attribute__((noinline)) 
void call_callback(int (*cb)()) {
  cb();
}

void foo(int *arr, size_t len) {
  int access_parent() { return len; }
  call_callback(access_parent);
}

## gcc5.4
access_parent.2450:
    mov     rax, QWORD PTR [r10]
    ret
call_callback:
    xor     eax, eax
    jmp     rdi
foo:
    sub     rsp, 40
    mov     eax, -17599
    mov     edx, -17847
    lea     rdi, [rsp+8]
    mov     WORD PTR [rsp+8], ax
    mov     eax, OFFSET FLAT:access_parent.2450
    mov     QWORD PTR [rsp], rsi
    mov     QWORD PTR [rdi+8], rsp
    mov     DWORD PTR [rdi+2], eax
    mov     WORD PTR [rdi+6], dx
    mov     DWORD PTR [rdi+16], -1864106167
    call    call_callback
    add     rsp, 40
    ret

我只是在单步执行时弄清楚了这个烂摊子是怎么回事:那些 MOV 立即指令正在将蹦床函数的机器代码写入堆栈,并将 that 作为实际回调。

gcc 必须确保最终二进制文件中的 ELF 元数据告诉 OS 该进程需要一个可执行堆栈(注意 readelf -l 显示 GNU_STACK 具有 RWE 权限)。因此,在其范围之外访问的嵌套函数会阻止整个过程获得 NX stacks 的安全优势。 (禁用优化后,这仍然会影响使用不从外部范围访问内容的嵌套函数的程序,但启用优化后 gcc 意识到它不需要蹦床。)

蹦床(来自我桌面上的 gcc5.2 -O0)是:

   0x00007fffffffd714:  41 bb 80 05 40 00       mov    r11d,0x400580   # address of access_parent.2450
   0x00007fffffffd71a:  49 ba 10 d7 ff ff ff 7f 00 00   movabs r10,0x7fffffffd710   # address of `len` in the parent stack frame
   0x00007fffffffd724:  49 ff e3        rex.WB jmp r11 
    # This can't be a normal rel32 jmp, and indirect is the only way to get an absolute near jump in x86-64.

   0x00007fffffffd727:  90      nop
   0x00007fffffffd728:  00 00   add    BYTE PTR [rax],al
   ...

(蹦床可能不是这个包装函数的正确术语;我不确定。)

这终于说得通了,因为 r10 通常会在不保存的情况下被函数破坏。没有 foo 可以设置的寄存器可以保证在最终调用回调时仍然具有该值。

x86-64 SysV ABI 表示 r10 是“静态链指针”,但 C/C++ 不要使用它。 (这就是为什么 r10 与 r11 一样被视为纯暂存寄存器的原因)。

显然访问外部作用域变量的嵌套函数不能在外部函数之后调用returns。例如如果 call_callback 保留指针以供将来其他调用者使用,您将得到虚假结果。当嵌套函数不这样做时,gcc 不会做蹦床的事情,所以函数就像一个单独定义的函数一样工作,所以它是一个函数指针,你可以任意传递。