《2022年c语言数组越界 .pdf》由会员分享,可在线阅读,更多相关《2022年c语言数组越界 .pdf(12页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、浅谈函数指针的实际应用刘 莹 南天软件更新日期: 2010-01-01 浏览: 1261 字号选择: 大中小开篇之谈有人曾说: 正是指针使得C语言威力无穷!的确,提及指针这个充满玄机的东西,很容易令人想到很多很多。指针是 C语言中一个重要的数据类型,指针和数组等复合类型的相关性更增加了其神秘色彩。这就注定了学习C语言的路上布满荆棘。每个指针都有一个与之相关的类型,不同数据类型的指针之间的区别不是在指针的表示上,也不是在指针所持有的值(地址)上,对所有类型的指针这两方面都是相同的,不过函数指针略有区别!函数指针是一类特殊的指针,它指向函数的首地址。以下仅仅是我个人在学习过程中对函数指针的认识和体
2、会:“和函数指针的n 次亲密接触”一节涵盖了函数指针的重要理论知识;“众里寻她千百度”说明了函数指针在UNIX操作系统层面上的“公开”应用;“他乡遇故知”体现了我在开发平台上发现了函数指针的应用之后的喜悦心情;“莫愁前路无知己”从开发应用角度说明了现实生活中对函数指针的应用已经“漫山遍野”,再次说明函数指针在设计开发中的重要作用。当然,这些只是我个人的一点看法,对于其中的谬误希望读者能够给我提出宝贵的修正意见。和函数指针的n 次“亲密接触”和函数指针的第一次接触是大学课堂上听教师提及,当时听的很是模糊,经过课下查询资料后发现了各种资料上对函数指针也没有详细的论述,大都提供如下简要说明: 1)
3、函数指针的定义形式:函数的返回值类型(* 函数指针名称)(参数列表); 2) 函数指针的赋值:函数指针名称 = 函数名称; 3) 函数指针调用函数方法:只需将函数名用(*函数指针名)来代替即可,例如(*p)(a,b);其中 p 为函数指针,a 和 b 是实际参数。以上是大多资料提供的有关函数指针的论述,但是就我个人认为,单纯从这几个方面不足以说出函数指针的真正内涵。下面是我经过多次查阅资料进行的总结。合理转换,去伪存真首先函数指针也是指针,同样表示内存中的地址,但其声明形式(见上)却不好理解,尤其是定义函数指针数组的时候: int (*funp) ( int, int ); /* 声明了一个函
4、数指针 */ int (*funparray 10 )( int , int ); /* 声明了一个函数指针数组 */ 以上的声明形式令人费解,可以通过如下形式进行转换: typedef 函数的返回值类型(* 函数指针类型名)(参数列表);例如: typedef int (*FUNP)( int, int ); 声明了一个指向返回值为整型且带有两个整型参数的函数指针类型;这样就可以像C语言提供的基本数据类型那样使用“函数指针类型”了,经过转换上面的形式可以写作: FUNP funcaion_pointer; /* 声明了一个函数指针 */ 名师资料总结 - - -精品资料欢迎下载 - - -
5、- - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 1 页,共 12 页 - - - - - - - - - FUNP fun_array 10 ; /* 声明了一个函数指针数组 */ 追根溯源,把握真谛前面曾经提及, 指针都和类型相关,那么函数指针和哪个类型相关呢?为了弄清楚这个问题,我们需要先要搞清函数和类型之间的关系。我们知道: 整型指针是指向整数类型的指针;浮点型指针是指向浮点类型的指针;函数指针是指向函数的指针;而整型,浮点型都是C语言提供的基本数据类型,若从这个角度看, 函数似乎也变成了一个不折不扣的“类型”了!那么, 函数究竟
6、是一种什么样的“类型”呢?我们可以先考虑这样一个问题:不同的函数如何区分?或者说函数的“属性”有几个?如何把函数进行归类呢?这要从函数的原型说起,函数的原型由函数的返回值类型,函数名称以及函数参数类型列表来表示(当然,函数定义还要加上函数体)。函数名称本身不足为奇,因为叫张三或者叫李四都不能影响到函数的属性,所以函数也可以说成是一种特殊的“类型”,它可以通过返回值类型和参数类型列表两个属性来表示。如下: double d_max( double, double ); int i_max( int, int ); 上面的两个函数d_max和 i_max 属于不同“类型”的函数。函数类型之间的差别
7、导致函数指针的类型也会有所不同,如下: typedef double (*DFUNPD) (double); /* 返回值为double ,含有一个double 参数*/ typedef double (*DFUNPN) (); /* 返回值为double ,但不含参数 */ typedef char (*CFUNPC)(char); /* 返回值为char ,并且含有一个char 参数 */ typedef char (*CFUNPN)(); /* 返回值为char ,但不含参数 */ 上面所列举的四个虽然都是函数指针,但却存在差异, 因为其各自所指向的函数的“类型”有所不同。从这点看来,
8、函数指针对类型的要求似乎显得更加细腻,它不同于普通数据类型的指针,因为不能笼统地说它是指向“函数类型”的指针,而应该说它是“指向特定函数类型”的指针。 即:一个普通数据类型的指针可以指向相同类型的任何一个变量的地址,而一个函数指针却不可以指向任意一个函数的地址,如: long l_var, l_tmp, *l_p; l_p = &l_var; l_p = &l_tmp; 都是合法的,而 CFUNPN funp = d_max; 却会被编译器拦住,并且给出一个警告!当然你可以通过强制类型转换来绕过它!? 声东先击西,一语破天机如前所述,利用函数指针间接调用函数的方法如下: typedef int
9、 (*FUNP) (int, int ); int max( int a, int b ) return( (a)(b)?(a)(b); FUNP funp=max; /* 对函数指针的赋值 */ funp( a, b ); /* 利用函数指针调用函数 */ 从上面的程序片段中我们可以看出两个不寻常的东西: 1) 对函数指针的赋值:既然funp 指向函数max,那么为什么不像普通类型的指针那名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 2 页,共 12 页 - - - - - -
10、- - - 样赋值呢?对于普通数据类型指针,需要要把变量的地址赋值给指针,如下: int a; int *ap = &a; 但给函数指针的赋值怎么就变得如此“直截了当”? 2) 利用函数指针调用其指向的函数:这和利用普通类型指针取其变量内容的形式也不大一致, 对普通类型指针需要用取内容操作符取其指向变量的内容,但是利用函数指针调用函数却在“偷懒”: funp( a, b ); 当然也可以很正规的写成这样: (*funp)( a, b ); 难道函数指针等同于函数指针的内容?即(*funp) = funp? 回答是肯定的!若上面这个等式成立,那么将这个等式根据“指针内容”的理论稍加转换就可以转变
11、成如下的等式: funp = &funp;这个东西看起来倒似曾相识: char str 10 ; str = &str; 在说明原由之前,我们有必要先统一一下术语: 1) 对数组: 为下标标识符,我们可以str 1 ; 2) 对函数: ( ) 为调用操作符,我们可以max( a, b );好了, 术语统一了, 言归正传吧!对数组而言,因为不带下标标识符的数组名会被解释成指向该数组首元素的指针:即满足 char str 3 ; str = &(str 0 );同样,当函数名称没有被调用操作符修饰时,会被解释成指向“该种类型”函数的指针,即满足:int max( int, int );max; 会
12、被解释成int (*)( int, int ) 类型(即返回值为int并且含有两个 int型参数) 的指针。 和数组名的解释类似,取地址操作符作用在函数名上同样能产生指向该类函数的指针,于是有: max=&max; 或者 funp =&max ;怎么样?上面的疑云就要随之散去了吧!现在,让我们临时总结一下函数指针赋值和通过指针对函数的调用方法吧: 1) funp = &max; (*funp)( a, b ); 2) funp = max; funp( a, b ); 若你是个循规蹈矩,思想又有些保守的人,我猜想你会按照上述的方法1) 来使用,因为你为了和普通数据类型的指针保持一致;若你是个个
13、性十足,又有些张狂的人,方法)则是你的选择。为了说明函数指针的赋值和利用函数指针调用函数引到众所周知的数组和指针的解释,称之为“声东先击西”;同样和数组类似, 函数指针的实质是函数指针是指向函数代码段的首地址的指针,此乃“一语破天机”也!? 反客为主,小议回调众所周知, 操作系统给我们提供了好多系统调用,以简化我们的程序设计。从这个意义上来说,我们是受益者,但是操作系统同样为我们提供了展示“个性”的机会,它有时需要调用我们的函数,即所谓的“回调函数”:就是我们提供一个函数,通过一定的方法使操作系统“认识”到这个函数,必要时由操作系统来调用它。那么如何才能让操作系统认识我们的函数呢?常见的实现方
14、法是使用函数指针作为参数传递给操作系统。在 WINDOWS编程中,这种方法用的很广,因为 WINDOWS 程序的灵魂是“以消息为基础,以事件驱动之”。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 3 页,共 12 页 - - - - - - - - - “鼠标的按下和弹起”都会被操作系统捕捉,然后发送消息到应用程序,应用程序根据用户定义的事件加以响应。当然消息的绑定和发送都不用我们担心,开发环境或者编程应用框架(在 WINDDOWS编程中典型的是MFC )都为我们做好了从消息到消息
15、函数的映射。在MFC中存在“消息映射表”,其基本雏形如下: struct MSGMAP_ENTRY UINT nMessAge; LONG (*pfn)( HWND, UINT, WPARAM, LPARAM ); ; struct MSGMAP_ENTRY _messageEntryes = WM_CREATE OnCreate; WM_PAINT OnPaint; WM_SIZE OnSize; ,. /* 这是消息这是消息处理函数 */ 上面的代码是一些好心人煞费苦心从MFC程序中挖掘出来并加以模仿的,我在大学时曾经读到过。虽然没有必要考究其真伪,不过从中我们可以看到函数指针的应用。我由
16、衷地感谢那些好心人, 要不是他们我们没有办法获取藏于系统内部的代码,从而看到函数指针在系统中的广泛应用,但代码没有公开就不足以成为函数指针在系统应用的理论依据。众里寻她千百度函数指针与信号前面曾经提到函数指针在WINDOWS编程中应用极为广泛,但是多被开发环境(IDE)所掩盖,我曾经在WINDOWS程序中苦苦寻觅回调函数的公开应用, 但未果。而工作后接触UNIX操作系统后发现,这个被称为“旧时王榭堂前燕”的操作系统人性化了许多。在UNIX操作系统中, 信号是一个软件形式的异常,就是我们常说的软件中断。一个信号就是一条消息,它通知进程某种类型的事件已经在系统中发生了。信号提供了一种异步事件的处理
17、方法,所以很多比较重要的应用程序都需处理信号。其实信号的应用不仅仅在操作系统中,早在我国若干年前就有,要不怎么会有“烽火戏诸侯”的典故呢? UNIX信号机制最简单的界面就是signal函数: #include void (*signal(int sig, void (*func)(int) (int); 先不必理会这个函数参数的具体意义,从表面上一看这个函数,给人的感觉很不适应。不过也无妨,用前面提到的方法进行转换一下吧: typedef void Sigfunc ( int ); 然后将 signal函数转换成如下的形式: Sigfunc *signal( int signo, Sigfun
18、c *func ); 经过转换之后, 本函数的原型就清晰的展现在我们面前:这是一个函数, 其返回值是一个函数指针,参数列表由一个整型(信号名称)和一个函数指针构成,其中参数func 的值可以是: 1) 常数 SIG_IGN 表示忽略这个信号; 2) 常数 SIG_DFL表示接收到此函数采用系统默认的动作; 3) 用户编写自己的信号处理函数来捕捉这个信号;名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 4 页,共 12 页 - - - - - - - - - 函数的返回值则是指向以前的
19、信号处理函数的指针。至于此函数的具体应用及其缺陷,可以参阅相关资料,不过由此我们发现函数指针不仅仅作为参数,同时也作为函数的返回值两次出现在signal函数中,函数指针在信号机制中的广泛应用体现了函数指针在操作系统中的重要地位。他乡遇故知函数指针在CoreBanking 中的应用如今工作已经两年有余,伴随着工作中的学习和积累,对公司的开放式金融平台(OFP )的理解也日益加深。CoreBanking 作为 OFP的重要组成部分,在构建核心服务时给我们提供了极大的便利性。所谓核心服务在系统里都是以Bea Tuxedo 应用服务进程来体现,即我们常说的AP SERVER ,每个服务完成了多种功能,
20、或者说是多个(当然也可以是一个)交易的集合。 而每支交易都有很多共同的处理逻辑,例如交易的打包和解包,各种合法性检查、组织公共信息等,区别在于不同的交易有各自的应用逻辑。可是,CoreBanking 是如何将这些交易的公共处理逻辑和具体应用整合起来的呢?让我们看一下代码吧,每个服务目录下边有一个称为主控的文件和一些具体的交易文件,从表面来看它们好像没有什么关联。在$HOME/def/下我们会看到好多以SERVER 名称来命名的头文件,下面是某个头文件的片段:, #ifndef EXTERN long (*AtomFunc)()= 0, HandAcc, /*No:0001 Name:手工会计事
21、项 */ FHandin, /*No:0002 Name:缴款 */ CshRegReg, /*No:0003 Name:出纳现金登记簿登记 */ , ; 从上面的头文件片段中我们可以看到一个函数指针数组,而此数组中的每个成员正是SERVER 服务下的源文件中的函数,也就是一个交易的应用逻辑部分。所有的公共处理逻辑都放在主控程序中。每个交易都有一个原子交易码,交易处理就是由原子交易码在函数指针数组中的位置来决定。在服务的响应过程中,主控就是根据输入的交易码,找到对应的原子交易码,再从函数指针数组中选择相应的函数进行调用来完成相应交易的处理。SYSMNG正是利用这个函数指针数组使得那些从表面看来
22、没有关联的源文件有机结合在一起。若将一个服务展开,就变成了这个样子:上图中的每个处理(含公共处理和交易处理)都是任何一个交易必要的组成部分:交易上来后, 首先进入主控程序,主控将所有的公共处理完成后,根据交易码选择不同的原子交易来执行,原子交易执行完毕后将执行权重新交回主控。 CoreBanking中的 AtomFunc 和 MFC 中 messageEntryes 似乎有些不谋而合了,以前在WINDOWS 编程中见过的东西,又再次出现,看起来自然亲切了许多!莫愁前路无知己函数指针是美丽的,它为我们提供了一种构造程序的方法:根据“实际情况”做出相应的“反应”!不管是WINDOWNS程序中的消息
23、MAP还是 UNIX系统中的信号机制,都充分体现了这种设计思想。在我们的程序中也是如此,有许多程序中都很好的利用了函数指针。如在调整交易中就有利用函数指针根据不同的交易码调用不同的调整函数以满足不同的调名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 5 页,共 12 页 - - - - - - - - - 整逻辑。结合前面所述,现提供一个小程序来说明这种设计思想的灵活性: int add( int a, int b ) return( a + b ); int sub( int a,
24、 int b ) return( a - b ); int mul( int a, int b ) return( a * b ); int dev( int a, int b ) return( a / b ); enum OPER ADD, SUB, MUL, DEV ; typedef int (*OPERFUNP)( int, int ); OPERFUNP FunArray = add, sub, mul,dev ; int oper( int a, int b, enum OPER oper ) return( FunArray oper ( a, b ) ); 怎么样?看到这个小
25、小的程序,或悲,或喜?皆不足道,由它去吧!浅析 C程序中数组越界作为软件开发人员,我们最主要的是使用C进行编程。 我们最担心的是什么,当然就是写出的程序在未知的情况宕掉(coredump)。而在怎样的情况下程序会coredump 呢?大家肯定会说: 数组越界是经常的一个因素。没错,今天我们要讨论的,就是在C程序中由于数据越界产生的一些奇妙的现象,并引申到当前很流行的一个词shellcode 。本篇文章最主要的目的其实是激发大家的学习兴趣,大家基本功要求是:会 C语言就行(大概能编水仙花数的水平)。 我会尽量用最最傻瓜的文字来阐述这些内存中的二进制概念。名师资料总结 - - -精品资料欢迎下载
26、- - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 6 页,共 12 页 - - - - - - - - - 为了避免一开始涉及太多枯燥的基础知识让您失去了兴趣,我并不提倡从汇编和寄存器开始,也不想用函数和栈开头。我准备用一个自己设计的小例子开始讲解。您如果对这些内容感兴趣,然后用十分钟时间照猫画虎地在编译器里把例子跟着走一遍,坚持一个星期之后您就会发现世界真奇妙了。不懂汇编不是拒绝这门迷人技术的理由今天的课程就不涉及汇编并且以后遇到会随时讲解滴。所以如果您懂C语言的话,不许不学,不许说学不会,也不许说难,哈哈!下面进入正题。
27、让我们来一起研究一段极简单无比的C语言小程序, 看看编程中如果不小心出现数组越界将会出现哪些问题。C代码:#include #define PASSWORD 1234567int verify_password (char *password)int authenticated;char buffer8; / add local buff to be overflowedauthenticated=strcmp(password,PASSWORD);strcpy(buffer,password); /over flowed here! return authenticated;main()in
28、t valid_flag=0;名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 7 页,共 12 页 - - - - - - - - - char password1024;while(1)printf(please input password: );scanf(%s,password);valid_flag = verify_password(password);if(valid_flag)printf(incorrect password!nn);elseprintf(Congr
29、atulation! You have passed the verification!n);break;对于这几行简单无比的程序,我还是稍作解释。1) 程序运行后将提示输入密码。2) 用户输入的密码将被程序与宏定义中的“1234567”比较。3) 密码错误,提示验证错误,并提示用户重新输入。4) 密码正确,提示正确,程序退出。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 8 页,共 12 页 - - - - - - - - - 所谓的漏洞在于verify_password()函数
30、中的strcpy(buffer,password)调用。由于程序将把用户输入的字符串原封不动地复制到verify_password函数的局部数组char buffer8中,但用户的字符串可能大于8 个字符。当用户输入大于8 个字符的缓冲区尺寸时,缓冲区就会被撑暴即所谓的缓冲区溢出漏洞。缓冲区给撑暴了又怎么样?大不了程序崩溃,coredump 了么,有什么了不起!此话不然, 如果只是导致程序崩溃就不用我在这里浪费大家时间了。根据缓冲区溢出发生的具体情况, 巧妙地填充缓冲区不但可以避免崩溃,还能影响到程序的执行流程,甚至让程序去执行缓冲区里的代码。今天我们先介绍一个最简单的。我们看上面的函数ver
31、ify_password()里边申请了两个局部变量int authenticated;char buffer8; 当 verify_password被调用时, 系统会给它分配一片连续的内存空间( 栈空间 ) ,这两个变量就分布在那里(专业术语叫函数栈帧(StackFrame) ,我们后面会详细讲解),如图一。图一变量和变量紧紧地挨着。为什么紧挨着?当然不是他俩关系好,省空间啊, 很简单的问题。用户输入的字符串将拷贝进buffer8,从示意图中可以看到,如果我们输入的字符超过 7 个 (注意有串截断符也算一个), 那么超出的部分将破坏掉与它紧邻着的authenticated变量的内容!再复习一下
32、程序,authenticated变量实际上是一个标志变量,其值将决定着程序进入错误重输的流程(非0)还是密码正确的流程(0)。下面是比较有趣的部分:名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 9 页,共 12 页 - - - - - - - - - 当密码不是宏定义的1234567 时,字符串比较将返回1 或-1(这里只讨论1,结尾的时候会简单介绍 -1 的情况),我们都知道,内存中的数据按照4 字节( DWORD)逆序存储,所以 authenticated为 1 时,内存中存的
33、是0 x01000000,如果我们输入包含8个字符的错误密码,如“ qqqqqqqq”,那么字符串截断符0 x00 将写入 authenticated变量,这溢出数组的一个字节0 x00 将恰好把逆序存放的authenticated变量改为0 x00000000 。函数返回后,main 函数中一看authenticated是 0,就会欢天喜地的告诉你:密码正确!这样,我们就用错误的密码得到了正确密码的运行效果。下面用 5 分钟实验一下这里的分析吧。将代码用VC6.0 编译链接, 生成可执行文件。注意, 是 VC6.0 或者更早的编译器, 不是 7.0 , 不是 8.0 , 不是.net , 不
34、是 VS2003, 不是 VS2005。为什么,其实不是高级的编译器不能搞,是比较难搞,它们有特殊的GS编译选项(该GS编译选项就是加入检测函数堆栈缓存溢出错误额外代码),为了不给咱们扫盲班增加负担,所以暂时略过,用6.0 !按照程序的设计思路,只有输入了正确的密码“1234567”之后才能通过验证。程序运行情况如下(如图二):图二名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 10 页,共 12 页 - - - - - - - - - 要是输入几十个字符的长串,应该会崩溃。 多少个
35、字符会崩溃?为什么?卖个关子,以后如果有机会慢慢讲。现在来个8 个字符的密码试一下(如图三):图三注意为什么01234567 不行?因为字符串大小的比较是按字典序来的,所以这个串小于“1234567”, authenticated的值是 -1,在内存里将按照补码存负数,所以实际存的不是0 x01000000 而是 0 xffffffff。那么字符串截断后符0 x00 淹没后,变成0 x00ffffff,还是非 0,所以没有进入正确分支。总结一下,由于编程员的粗心,有可能造成程序中出现缓冲区溢出的缺陷。这种缺陷大多数情况下会导致崩溃,但是结合内存中的具体情况,如果精心构造缓冲区的话,是有可能让程
36、序作出设计人员根本意向不到的事情的。本节只是用一个字节淹没了邻接变量,导致了程序进入密码正确的处理流程,使设计的验证功能失效。如果作为一个软件破解人员(cracker ),大家可能会说这有什么难的,我可以说出一堆方法做到这一点:i).直接查看PE ,找出宏定义中的密码值,得到正确密码。名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 11 页,共 12 页 - - - - - - - - - ii).反汇编 PE ,找到爆破点 ,JZ JNZ 的或者 TEST EAX,EAX变 XOR
37、 EAX,EAX 的在分支处改它一个字节。,但是今天介绍的这种方法与crack 的方法有一个非常重要的区别,非常非常重要!就是, 我们是在程序允许的情况下,用合法的输入数据(对于程序来说)得到了非法的执行效果(对于程序员来说)这是骇客(hack) 与破解( crack )之间的一个重要区别,因为大多数情况下hack 是没有办法直接修改PE(windows 下面可以执行程序格式,大家理解为一个可执行的.exe 文件即可 ) 的,他们只能通过影响输入来影响程序的流程,这将使 hack受到很多限制,从某种程度上讲也更加困难。比如, 大家都听说过的网站挂马,即为黑客建一个网站,只要你登陆网站,网站中黑
38、客精心构造的某串数据,例如一个flash ,一个奇怪的ieframe ,等等,就好比我们在上面的例子程序中构造的qqqqqqqq, 这类畸形的数据就会利用ie 程序本身的漏洞造成缓冲区溢出,从而影响程序流程,达到黑客特定的要求(到某 ftp服务器下载木马等)。这种影响程序流程的数据就叫shellcode。好了,今天的扫盲课程暂时结束,希望这个自制的漏洞程序能够给您带来一点点帮助名师资料总结 - - -精品资料欢迎下载 - - - - - - - - - - - - - - - - - - 名师精心整理 - - - - - - - 第 12 页,共 12 页 - - - - - - - - -