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

时间:2016-11-11 18:13:20

标签: c assembly lambda functional-programming shellcode

我知道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    $0x3,%eax
   8:   5d                      pop    %ebp
   9:   c3                      ret    
   a:   90                      nop
   b:   90                      nop

所以我可以像这样做主:

的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,回调是非常痛苦的;一旦你超过C一步就得到“lambdas”的概念,你就会失去语言的ABI(例如,C ++有lambda,但你在C ++中所做的一切都突然依赖于实现),以及“Lisplike”脚本插件(例如,插入Lisp,Perl,JavaScript / V8,任何其他已经知道如何推广回调的运行时)使回调变得非常简单,但也比抛出shellcode要昂贵得多。

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

所以问题是:这是否实用(对于完全自包含的函数,例如,即使他们想要调用puts,puts也必须作为参数给出,或者在参数中的哈希表/结构内)?或者是否有一些问题妨碍了这种实践?

3 个答案:

答案 0 :(得分:5)

Apple在clang中实现了一个非常相似的功能,它被称为“块”。这是一个示例:

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

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

    return 0;
}

更多信息:

答案 1 :(得分:3)

  

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

将源代码转换为机器代码是编译 的原因。 Shellcode是具有特定约束的机器代码,其中没有一个适用于此用例。你只需要普通的机器代码,比如编译器在正常编译函数时生成的。

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);
}

asm输出是:

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-immediate指令正在将用于trampoline函数的机器代码写入堆栈,并将 作为实际回调传递

gcc必须确保最终二进制文件中的ELF元数据告诉操作系统该进程需要一个可执行堆栈(注意readelf -l显示具有RWE权限的GNU_STACK)。因此,在其范围之外访问的嵌套函数会阻止整个过程具有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是&#34;静态链指针&#34;,但C / C ++没有使用它。 (这就是为什么r10被视为r11,作为纯刮擦寄存器)。

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

答案 2 :(得分:1)

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

shellcode.c

heroku addons:rename postgresql-loudly-9983 stats-db -a bumping-softly-6892

main.c

PropertiesService.getScriptProperties() 

生成文件:

 int anon() { return 3; }

其中 ... uint8_t shellcode[] = { #include anon.shell }; int (*p_give3)() = (int (*)())shellcode; printf("%d.\n", (*p_give3)()); 是您编写的脚本,它只打印来自objdump输出的原始逗号分隔代码字节。