cell-click 传参数_C语言可变参数及其原理

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

068ec1090046906ee06cae19128a01be.png

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

我们追踪一下printf的实现:

e695a8d648f3641f78e7e8e0a170b521.png

这里用到了__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平台的头文件内容给搅浑了思路……

2445d39d0fb4a85cf7ce6a2af8246d64.png

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

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

ab58faaaee010e2b5bd5ac0a23869202.png

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

e4c834cf97a559189649f19498bd6edf.png

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

13ed8306095ed56489f020a1f421886a.png
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指针指向的空间存储的内容的下一个内容处。

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

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

89e073583fe60c071dfeb68853dc0ae7.png

拿开头的那个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内的内容展现出来……

a6923e3cfbda71b17f686426da19afd1.png

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

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

0b54fa07a48122abbeb84a84841aeb8c.png

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

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

07df141b94f37e349f469e6683ab663c.png

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值