我试图理解为什么ret();
在以下C程序中起作用:
#include<stdio.h>
#include<string.h>
unsigned char code[] = \
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
"\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";
main()
{
printf("Shellcode Length: %d\n", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
为了使其工作,您必须编译上面没有堆栈保护,允许堆栈可执行。
我想知道的是为什么调用ret();
,这似乎是一个分配给值(int(*)())code;
的整数。
我猜它与函数指针实际上是整数有关,但我无法在心理上解包表达式int (*ret)() = (int(*)())code;
的含义
感谢您的帮助
答案 0 :(得分:8)
我想知道的是为什么调用
ret()
,它似乎是一个分配给值(int(*)())code
的整数
ret
不是整数,它是指向返回整数的函数的指针。 &#34;内联&#34;语法,即int (*ret)()
更难解读&#34;而不是等价的typedef
,即
typedef int (*func_returning_int)();
...
func_returning_int ret = (func_returning_int)code;
注意:不用说,无论你如何投射指针,这都是未定义的行为。
答案 1 :(得分:2)
强制转换将数组code
转换为指向函数的指针,并将其分配给函数指针ret
。由于ret
是指向函数的指针,因此当您调用指向所发生的事件的函数时,将执行数组code
中的机器代码。
这就是理论。不确定已存储到数组code
中的实际机器代码是什么。
因此ret
是指向返回int
的函数的指针。
函数指针不是int
,而是函数指针。
功能指针定义和声明
对于诸如int (*ret)()
之类的变量定义,您必须从变量名称ret
开始解析定义。使函数指针变量定义更难以解密的原因是括号用于定义表达式的解析顺序,括号也用作特殊符号来表示变量是函数指针。
标准函数声明看起来像int retFunc ();
,它声明了一个返回int
的函数。在这种旧的函数声明中没有指定参数,所以如果有参数,我们就不知道是否有参数或几个或它们的类型。顺便说一句,标准整数变量声明看起来像int intVar;
。
要创建函数指针变量,您需要为函数指针指定相同的信息,就像为函数声明指定一条附加信息一样,指示这是函数指针的声明或定义,而不是宣布职能。
下面是一些代码,其中包含语法变体,以显示函数声明和函数指针声明之间的差异。
main () {
extern int retFunc(); // declaration of a function, returns int
extern int (*ret)(); // declaration of a function pointer, function returns int
extern int *retFunc2(); // declaration of a function, returns int pointer
extern int *(*retVar)(); // declaration of a function pointer, function returns a pointer to an int
extern int (*((*ret2)()))(); // declaration of a function pointer, function returns a function pointer which points to a function that returns an int
}
这五个区别的是使用括号在第二,第四和第五个声明中使用指针指示符来强制执行编译器如何解释声明。由于运算符优先级规则导致编译器在括号中放置更高的优先级来指示函数,因此需要分组括号,因此我们通过使用分组括号来覆盖优先级规则。
第五个特别有趣,extern int (*((*ret2)()))();
可以分两个阶段进行解析。第一个是段((*ret2)()))
,表示符号ret2
是指向函数的指针,第二个阶段是确定指向的函数的返回类型,指向返回{的函数的指针{1}}将第一部分替换为int
中的任意符号 x 。
在创建函数指针声明时,我们必须知道C的运算符优先级规则以及它们如何影响编译器解释声明或定义的方式。我们需要在函数指针声明中的int (*x)();
周围添加附加括号,以便编译器将其视为指向返回*ret
的函数的指针,而不是返回指向{的指针的函数。 {1}}。
C编译器使用的规则有时要求使用括号来强制执行表达式的转换顺序,以使表达式具有所需的含义。而这些规则有时会导致相同的字符或符号在不同的上下文中具有不同的含义。所以int
括号使符号int
成为函数,int ret();
括号用于对符号进行分组,在这种情况下只使用一个符号并使用ret
括号对于组符号和表示函数,在这种情况下int (ret);
是指向函数的指针。
在您的示例中,您不是将变量int (*ret)();
声明为函数指针,而是定义变量并在语句ret
中为其赋值。解析定义的规则类似于解析声明的规则。
在您的示例中,ret
被定义为int (*ret)() = (int(*)())code;
的数组,我假设是在数组的初始化中指定的机器代码。
在C中,数组变量可以在很多方面被视为常量指针变量。因此,您可以取消引用数组名称,这意味着code
与unsigned char
相同但是因为它是一个常量指针,所以您不能执行code[1]
之类的操作,尽管您可以执行*(code + 1)
之类的操作1}}与code = code + 1;
相同。
因此,在语句unsigned char *code1 = (code + 1);
中,您正在转换常量指针unsigned char *code1 = &code[1];
,该指针指向int (*ret)() = (int (*)())code;
指向返回code
的函数的函数指针。只要有一些方法可以从赋值运算符右侧的类型转到赋值运算符左侧的类型,C编译器很乐意强制要创建您想要创建的幻想。 / p>
然而,仅仅因为编译器很乐意从表达式生成机器代码并不意味着当程序实际运行时底层操作系统和硬件对结果感到满意。这些灰色区域,未定义行为的区域,可能导致程序有时运行而不是其他时间运行,或者可能在一个环境中运行而不是在另一个环境中运行。
数组unsigned char
的强制转换使得这更难以理解,因为对返回int
的函数指针的强制转换语法类似于声明或定义函数的语法返回code
的指针,除了在转换int
中的星号后没有变量。所以所有这些括号都会让它有点混乱。
在此演员表的情况下,我们使用括号对完整的类型转换,int
以及括号进行分组以强制执行订单(int(*)())
和括号以指示这是一个函数,(int(*)())
。所以在这种类型演员中有很多括号飞扬。
当它变得更复杂时,就像(*)
这是一个指向函数的函数指针,该函数返回指向()
的指针。
在这种情况下,我更喜欢明确使用括号来指定解释的顺序,而不是依赖于我对顺序运算符优先级的记忆。