【问题标题】:Is it practical to create a C language addon for anonymous functions?为匿名函数创建 C 语言插件是否可行?
【发布时间】:2016-11-11 18:13:19
【问题描述】:

我知道 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:

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 也必须作为参数给出或在参数的哈希表/结构中给出)?还是有什么问题阻碍了它的实用性?

【问题讨论】:

  • 我看不出引入未定义的行为比简单地编写命名函数更可取。
  • 因为在代码中只调用一次的函数不应该污染全局范围。想象一个回调繁重的程序;有了这个,您可以轻松创建 5 个回调 pthread,而不会产生任何全局污染。是什么导致了未定义的行为?
  • GCC 有一个nested functions 扩展,但我不知道它们是否适用于 (p) 线程,尽管我对此表示怀疑。
  • 您知道您可以将函数声明为static,所以它不会污染全局范围?
  • 您在这里严重误用了“shellcode”一词。 Shellcode 是用于利用漏洞的特制机器代码。您所说的只是机器代码。正如 EOF 指出的那样,您尝试做的事情毫无意义,您可以只使用静态函数。与您提出的替代方案会造成的可怕的不可维护的混乱相比,这会“污染”文件范围名称空间这一事实是一个微不足道的问题。

标签: c assembly lambda functional-programming shellcode


【解决方案1】:

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

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

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

    return 0;
}

更多信息:

【讨论】:

  • 这真的很有趣,看起来比转换为函数指针更实用。这些可以在线程中使用/作为参数传递给函数吗?
  • 是的!事实上,Apple 广泛使用块来允许应用程序将回调传递给 macOS 和 iOS API。
  • 与 GCC 的嵌套函数相比,这个扩展似乎前进了一步,后退了两步。它允许匿名函数,但不允许完全访问嵌套范围并且不使用普通函数指针,因此不能与使用普通函数指针的回调一起使用。
  • @RossRidge 另一方面,嵌套函数在定义它们的函数退出后不能被调用。块可以;这是一个巨大的优势。
  • @Dmitry 我认为您还没有阅读所有文档! :) 块对于异步回调是完全可行的——它们可以使用Block_copy() 复制,这允许它们在定义函数之外使用。代码本身没有被复制,只是对它的引用以及它关闭的任何 __block 变量。
【解决方案2】:

我知道 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 立即指令正在将蹦床函数的机器代码写入堆栈,并将 that 作为实际回调传递.

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
   ...

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

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

x86-64 SysV ABI 说 r10 是“静态链指针”,但 C/C++ 不使用它。 (这就是为什么 r10 被视为 r11,作为一个纯粹的暂存寄存器)。

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

【讨论】:

  • 对于大多数用例来说似乎已经足够了,但我仍然会在编写 python 代码时感到空虚,我想要一个 iife,我想如果我真的需要它,我可以求助于自动化的插件名称生成,或调用支持 iife 的嵌入式运行时,如 javascript、perl 或 lisp,它有很多运行时编译的开销,但如果你已经拥有它,它有时是合理的,它是泄漏的,但更多价格昂贵。
  • @Dmitry:iife 与匿名(或至少是函数范围)回调函数有什么关系?如果要在定义的同一位置调用它,您提取机器代码的想法与您应该做的相反!因为如果您确实立即执行它,那么您希望它内联。
  • 是的,你是对的,iife 块也可能只是 c 已经通过 {} 支持的匿名块。我还没想好。我的错,除了你不能从他们那里返回到一个赋值中,但如果它只使用一次,一个常规的静态函数几乎肯定会被内联,所以抽象是不泄漏的
  • 您正在使用的这个编译器资源管理器非常漂亮,我需要使用它;通常我只是直接骚扰objdump。
【解决方案3】:

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

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 输出的原始逗号分隔的代码字节。

【讨论】:

  • 练习留给读者。使用您选择的任何语言。
  • 我已经在 Perl 中做过一次。我很好奇你是否真的有概念证明。
  • 你如何确保它进入一个可执行页面? (而不是没有执行权限的数据页。)如果你把它放在一个函数中,根据我的经验,gcc 会发出代码来将字节写入堆栈。在大多数操作系统中,不可执行的堆栈内存是一种常见的安全做法。 static const uint8_t 会有很大帮助,因为只读数据通常会进入成为文本段一部分的部分 (.rodata),因此它的映射与代码相同。但是谁知道是否有 C 实现甚至失败?
  • 无论如何,是的,您可以在某种程度上自动化该过程,但这样做意味着您的回调必须在单独的文件中定义,除非您的过滤器将函数名作为 arg 或其他东西。我不认为这比在同一个文件中包含一个 static 函数和想要将其作为回调传递的函数更好,这是整个荒谬的想法试图避免的。
  • @PeterCordes 关于确保页面可执行的有趣点。我习惯于堆叠可执行代码,但这确实会导致非常明显的众所周知的安全问题,这会使 lambda 在尝试跳转到它时引发系统级异常。静态损坏的 lambda 很便宜,但它们的抽象对我来说太漏了,因为它们的行为方式与我希望它们在函数体内定义的方式相反(它创建了一个不明显的静态依赖,而 lambda存储在与抽象看起来不同的地方)。
猜你喜欢
  • 2022-01-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-10
  • 1970-01-01
  • 1970-01-01
  • 2010-10-13
相关资源
最近更新 更多