快捷搜索:  汽车  科技

c语言如何配置可变参数(C语言可变参数及其原理)

c语言如何配置可变参数(C语言可变参数及其原理)我的balloon OS中,printk是这样运行的:好像看起来还是很混乱,所以我从网上又查到了一个相同原理的更简单粗暴的实现(这段代码同时也在我的balloon OS的vsprintf.c中被使用):所幸我找到了一份linux 0.01的代码,代码里有printk的实现,printk在某种意义上来说相当于是内核中的printf,其使用方法和printf基本没有区别。而且最重要的是,在这份代码里我们能看到可变参数能够被使用的关键代码,并且不用被windows平台的头文件内容给搅浑了思路……下面的内嵌汇编我们就暂时忽视了吧,只看我们需要看的三条语句,一个是va_start,一个是vsprintf调用,一个是va_end。va_start,va_end都被定义在include/stdarg.h中,定义如下:

在刚学C没多久的时候,我一直有这么个疑问:

c语言如何配置可变参数(C语言可变参数及其原理)(1)

printf这个函数是怎么做到可以处理不同数量的参数的?在初学C的时候,根本就没有遇到过(事实上程序设计课也没讲过)。

我们追踪一下printf的实现:

c语言如何配置可变参数(C语言可变参数及其原理)(2)

这里用到了__builtin_va_list __builtin_va_start __builtin_va_end,在中间调用了__mingw_vprintf函数,把类型为__builtin_va_list的__local_argv和第一个参数__format传了进去……然后就不明所以了。

所幸我找到了一份linux 0.01的代码,代码里有printk的实现,printk在某种意义上来说相当于是内核中的printf,其使用方法和printf基本没有区别。而且最重要的是,在这份代码里我们能看到可变参数能够被使用的关键代码,并且不用被windows平台的头文件内容给搅浑了思路……

c语言如何配置可变参数(C语言可变参数及其原理)(3)

下面的内嵌汇编我们就暂时忽视了吧,只看我们需要看的三条语句,一个是va_start,一个是vsprintf调用,一个是va_end。

va_start,va_end都被定义在include/stdarg.h中,定义如下:

c语言如何配置可变参数(C语言可变参数及其原理)(4)

好像看起来还是很混乱,所以我从网上又查到了一个相同原理的更简单粗暴的实现(这段代码同时也在我的balloon OS的vsprintf.c中被使用):

c语言如何配置可变参数(C语言可变参数及其原理)(5)

我的balloon OS中,printk是这样运行的:

c语言如何配置可变参数(C语言可变参数及其原理)(6)

typedef char* va_list; #define _INTSIZEOF(n) ((sizeof(n) sizeof(int)-1)&~(sizeof(int)-1)) #define va_start(ap v) (ap=(va_list)&v _INTSIZEOF(v)) #define va_arg(ap t) (*(t*)((ap =_INTSIZEOF(t))-_INTSIZEOF(t))) #define va_end(ap) (ap=(va_list)0)

第一句typedef char* va_list;

意思就是 va_list其实本质是char* 字符指针,只不过给它换了个名字而已。

第二句 _INTSIZEOF(n)定义为((sizeof(n) sizeof(int)-1)&~(sizeof(int)-1))

这个宏的目的是根据各种变量类型,取得他们的变量存在栈中的长度。实际使用中,每个传参存储的空间都必须是int类型长度的整数倍,即4 8 12 16……

我们拿char short int double来举例子

_INTSIZEOF(char) 会被替换为((sizeof(char) sizeof(int)-1)&~(sizeof(int)-1))

这时算出的结果为((1 4-1)&~3),即4和非3进行与操作,非3执行后,该数据的低2位都为0,高位全为1,和4相与,结果为4

_INTSIZEOF(short)结果为((2 4-1)&~3),5和非3相与,结果为4

_INTSIZEOF(int)结果为((4 4-1)&~3),7和非3相与,结果为4

_INTSIZEOF(double)结果为((8 4-1)&~3),11和非3相与,结果为8

不得不说这个宏的设计非常巧妙。

第三句 va_start(ap v)定义为(ap=(va_list)&v _INTSIZEOF(v))

这个宏的目的就是根据传入参数的第一个参数,获取可变参数列表的第一个参数的地址。printk在调用时,第一个传入的参数是const char* fmt,那么va_start(ap fmt)就被展开为(ap=(char*)&fmt _INTSIZEOF(fmt)),即获取fmt的指针之后,跳到fmt指针指向的空间存储的内容的下一个内容处。

最后一句话可能有点晦涩,这时候就需要来点图……

在现代计算机中,栈是一个地址从大往小增长的数据结构,即栈里面填入的内容越多,栈顶的地址越小。

c语言如何配置可变参数(C语言可变参数及其原理)(7)

拿开头的那个printf调用做例子,参数是从右到左依次入栈的,所以栈顶的fmt,对应的地址是最小的。那么我在执行va_start(ap fmt)的时候,(char*)&fmt获得了fmt存储的地址0x40007924,再加上sizeof(const char*)的4个字节,就到了0x40007924。

注意,在32位机中,指针类型的长度是4字节,64位机中则长度是8字节,那么在64位机中,fmt存储的位置就要从0x0000000040007920开始,一直跨越到0x0000000040007927……那么ap就获得了可变参数列表的入口地址了。

第四句va_arg(ap t)定义为(*(t*)((ap =_INTSIZEOF(t))-_INTSIZEOF(t)))

这个宏的目的是从列表中取出一个参数,然后让ap跳到下一个参数的地址处。很明显,ap先执行了内部的 =操作更新了自己,然后通过后面的-操作获得ap之前指向的数据的地址,然后用*操作取出内容。

第五句 va_end简单粗暴,就是把ap设置为空指针。

通过这几个宏,我们可以让vsprintf这个函数通过格式字符串,一个个对应获取数据,转换成字符,放入buf中,跳转回printf/printk,然后调用系统的接口,把buf内的内容展现出来……

c语言如何配置可变参数(C语言可变参数及其原理)(8)

上图为vsprintf内部的一小段,其中的va_arg就是在从参数列表中获取内容。

那么既然我们能通过格式化字符串来获得栈上内容,我可不可以故意写错格式化字符串,导致printf/printk崩溃呢?其实是可以的……而且,在格式化字符串中有个非常有趣的控制格式符%n,这个格式符并不会进行任何输出,但是会在获取的地址位置填入之前输出的字符的个数。

c语言如何配置可变参数(C语言可变参数及其原理)(9)

上图为vsprintf中对%n格式符的操作,ip为int*类型,最后我们在ip指向的位置存入了str-buf,即已经输出的字符的个数。

在无canary等栈保护机制的条件下,格式化字符串会把栈直接暴露给用户,如果字符串写法不当,很容易造成漏洞。而函数在调用时,会将ret指令需要使用的指令地址存在栈上的某一位置,如下图:

c语言如何配置可变参数(C语言可变参数及其原理)(10)

图中0x4000791c位置是调用vsprintf留下的返回地址,vsprintf执行完之后会通过这个回到printf/printk中。printf/printk执行完之后,会根据0x40007930处的地址返回到之前调用他们的函数中(这里假设是main函数)。

那么我要是把格式化字符串写成"%d %c %n"会怎样?答案是vsprintf照样会在遇到%n时选择访问0x40007930,并且把已经输出的字符数量存入这个位置,而这个位置本来存的是外部函数调用printf时,call printf指令的下一条指令的地址。

如果我们控制输出的字符数量为一个非常特殊的值会怎么样?那么vsprintf仍然会在该处填入这个值。如果这个值指向一个后门函数中的一条关键指令。那么恭喜你,在运行完vsprintf和它的上层函数(printf/printk,或其他任意调用了vsprintf的,有可变参数的函数)之后,上层函数返回时,你的程序就中招了……

猜您喜欢: