我的主要看起来像这样:
int main(int argv,char **argc)
{
extern system,puts;
void (*fn)(char*)=(void(*)(char*))&system;
char buf[256];
fn=(void(*)(char*))&puts;
strcpy(buf,argc[1]);
fn(argc[2]);
exit(1);
}
我对extern
关键字的含义及其在此代码中的工作原理有一个大概的了解。我对fn
函数的声明感到困惑。是在这里声明的函数指针,还是函数?另外,为什么&system
和&puts
位于fn
定义的末尾。
答案 0 :(得分:3)
这段代码是一个非常人为的例子,说明如何使用缓冲区溢出来调用代码最初不能调用的函数。它不应该被视为编写良好的代码的例子,事实恰恰相反。
以下行声明了一个指向函数的指针,并将其初始化为指向system
函数。
void (*fn)(char*)=(void(*)(char*))&system;
请注意,编写后,代码实际上从不调用system
函数,因为以下行将指针更改为指向fputs
函数。
fn=(void(*)(char*))&puts;
程序中的漏洞就在这一行
strcpy(buf,argc[1]);
如果strlen
的{{1}}大于缓冲区大小,则缓冲区溢出可能会将值argc[1]
更改为指向某个任意函数,然后将由此行调用
fn
附注:正如有人在评论中指出的那样,应切换fn(argc[2]);
和argc
的名称。
答案 1 :(得分:2)
首先让我说这是一种可怕的编码方式。
那就是说,让我们看看它是如何运作的:
extern system, puts;
这表示system
和puts
在其他地方被定义,链接器将负责为您提供地址。编译器不知道它们具有什么类型,因此,gcc至少假定它是int
并发出警告。我找不到标准中的条款,说明这是明确定义的,实现定义的还是未定义的行为,但肯定是坏的。这特别糟糕,因为这些符号不是int
。我确实找到了这个(C11,附件J.2 - 未定义的行为):
同一对象或函数的两个声明指定不兼容的类型 (6.2.7)。
int
也可能太小而无法容纳指针。这是多么糟糕的事情是无止境的。
所以,让我们在内存中看到它的外观:
system (as 4-byte int) system (as function)
BYTE1 INSTRUCTION1
BYTE2 INSTRUCTION2
BYTE3 INSTRUCTION3
BYTE4 INSTRUCTION4
INSTRUCTION5
INSTRUCTION6
INSTRUCTION7
INSTRUCTION8
...
所以链接器的作用是使system
的地址在每个编译对象上相同(有效地将它们链接在一起)。另一方面,system
的内容取决于链接器不知道或不关心的类型。
所以在你的文件中,如果紧接在:
extern system, puts;
你写道:
printf("%d\n", system);
您将获得system
函数的前几个指令,就像它是int
并打印一样。这很糟糕,不要这样做。
另一方面,正如我所说,程序中定义为system
的{{1}}的地址与真实的extern
功能相同。所以如果你拿地址:
system
并将其转换为正确的类型:
&system
你得到一个函数指针,指向真正的(void(*)(char*))&system
函数。请注意,system
现在只是system
,我们将其称为函数,这本身就是未定义的行为:
C11附件J-2(未定义的行为):
对象的存储值不是由允许类型的左值访问 (6.5)。
C11 6.5(强调我的):
6。用于访问其存储值的对象的有效类型是对象的声明类型(如果有)。 ...
7。对象的存储值只能由具有以下类型之一的左值表达式访问:88)
- 与对象的有效类型兼容的类型
- 与对象的有效类型兼容的类型的限定版本,
- 对应于有效类型的有符号或无符号类型的类型 对象,
- 与对象的有效类型的限定版本对应的有符号或无符号类型的类型,
- 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或
- 字符类型。
通过所有这些,您现在有一个包含int
实际地址的函数指针。然后,您可以根据自己的心意调用它。使用system
也是如此。
那很糟糕。这就是实际应该如何做到的:
puts
除了使用实现提供的头文件的好处外,让我们看看这个头文件是如何实际工作的。
在头文件中,您有许多具有许多声明的行。 #include <stdlib.h>
#include <stdio.h>
int main(int argv,char **argc)
{
char buf[256];
/* system was unused */
strcpy(buf,argc[1]);
puts(argc[2]);
return 0; /* 0 means no error */
}
的内容可能如下所示:
puts
首先,请注意代码中的函数指针实际上是如何使函数签名错误的?那很不好。这很糟糕,因为调用int puts(const char *s);
函数,调用者不需要为它提供返回值空间,而void
期望有一个返回值空间,因此puts
将写入返回值它期望找到它,但实际上空间属于其他东西。这称为堆栈损坏。
无论如何,当编译器看到这样的一行时:
puts
它会知道int puts(const char *s);
是外部定义:
C11 6.2.2(强调我的):
5。 如果函数的标识符声明没有存储类说明符,则确定其链接与使用存储类说明符extern 声明的完全相同。如果对象的标识符声明具有文件范围而没有存储类说明符,则其链接是外部的。
它会知道它是一个功能,它的签名是什么。
所以只需调用puts
,编译器就可以正确地完成所有操作。
答案 2 :(得分:0)
extern system, puts;
// equivalent to
extern int system, puts;
以上语句声明类型为system
的两个变量puts
和int
,这是未指定时的默认类型(gcc会发出警告)对此)。声明意味着它引入了变量名称和类型,但没有为它分配空间。
是在这里声明了一个函数指针,还是一个函数?
void (*fn)(char*);
将fn
定义为指向函数的指针,该函数接受char *
并且不返回任何值。表达式
(void(*)(char*))&system
获取变量system
的地址并将其强制转换为类型为void(*)(char *)
的指针,即指向函数的指针,该函数接受char *
并且不返回任何值。因此声明
void (*fn)(char*)=(void(*)(char*))&system;
将system
的地址转换为指针fn
的正确类型后指定地址。
为什么&amp; system和&amp;放在fn
的定义的末尾
那是因为fn
是一个函数指针。在编译的链接阶段,标识符system
和puts
与库函数链接。这意味着在分配给函数指针fn
之前,必须将变量的地址强制转换为适当的类型。
然而,这不是正确的方法。必须使用包含在其中的头文件来包含函数原型。
答案 3 :(得分:0)
fn
的声明不是声明一个函数,它声明一个函数指针,并初始化它。这一行:
void (*fn)(char*)=(void(*)(char*))&system;
相当于:
void (*fn)(char*);
fn = (void(*)(char*)) &system;
其中声明fn
是一个指向函数的指针,该函数接收一个指向char的指针(并且不返回任何内容)。然后,为此指针分配system()
的地址,该地址已转换为适当的类型以匹配fn
的类型。