一道“简单”的二进制题目

  • A+
所属分类:安全博客

一道“简单”的二进制题目

Last updated:May.03, 2018 CST 19:03:10

这道题本来想当作面试题给出,但认真考虑了一下,还是算了,怕被找上门真人快打……找好的懂底层的开发真难啊。

题目

请指出下列代码是否存在问题。如有,请指明错误,并预测程序运行输出,并说明原因。

// gcc test.c -o test -g && ./test
#include <stdio.h>

int main(void){
    unsigned int i = 0x12345678;
    float j = 0x9abcdef0;
    printf("%dt%ft", j, i);
    printf("%dt%fn", i, j);

    printf("%pt%xt", j, i);
    printf("%pt%xn", i, j);
}

答案

这道题的来源是最近同事调试exp中遇到的小问题,主要涉及到了:

  • 调用约定
  • printf原理
  • 浮点数存储

首先说下正确的答案:

存在错误,在第三次和第四次时传入参数类型和格式化字符串不符。打印输出的第一行可能是:
305419896   2596069120.000000   305419896   2596069120.000000
第二行中第一个、第三个输出数字为0x12345678,第二个和第四个不可预测。

分析

一般来说比较容易从源代码发现的问题是:

  • 第三行、第四行的格式化字符串中,参数给定错误。
  • 声明的变量j由于浮点数自身问题,会丢失精度,变成0x9abcdf00,也就是2596069120.000000。这里涉及到的就是浮点数存储问题,不再赘述。

但观察输出,又有了几个新问题,分别是:

  • 第一次、第二次与第三次、第四次调用printf时,参数顺序有了变化,但为何没有体现在输出结果中
  • 第三次、第四次调用printf时,输出为何是不可预测的

printf的特殊之处在于,他是一个变参函数。变参函数的传参方式,在System V AMD64 ABI(Page 20)中有如下描述:

  • 整数类型按照rdi->rsi->rdx->rcx->r8->r9的顺序
  • 浮点数类型按照xmm0->xmm1->...->xmm6->xmm7的顺序,部分类型只占用半个寄存器的,可以将寄存器拆成两半使用
  • 其他参数和复杂类型(如直接传结构体)从栈走

而在printf->vfprintf内部的代码中,处理这些变参的方法如下:

    LABEL (form_integer):                                                      
      /* Signed decimal integer.  */                                              
      base = 10;                                                              
                                                                              
      if (is_longlong)                                                              
        {                                                                      
          long long int signed_number;                                              
                                                                              
          if (fspec == NULL)                                                      
            signed_number = va_arg (ap, long long int);                              
          else                                                                      
            signed_number = args_value[fspec->data_arg].pa_long_long_int;     

//...
    LABEL (form_float):                                                              
      {                                                                              
        /* Floating-point number.  This is handled by printf_fp.c.  */              
  //...
            if (is_long_double)                                                      
              the_arg.pa_long_double = va_arg (ap, long double);              
            else                                                              
              the_arg.pa_double = va_arg (ap, double);                              
            ptr = (const void *) &the_arg;                                      
                                                                              

va_arg总是按照这个宏里指定的类型提取参数。


说到这,上面两个问题的答案都明确了:

  • 前两次调用printf时,%d%f分别从rdixmm0中取参数,使用的索引是两套体系,因此无论传入参数顺序如何变化,都不影响取值顺序。
  • 后两次调用prinf时,需要从rdirsi中取值,rdi为之前设定的值,但rsi没有被特定赋值过,因此无法确定这个值。

尝试更改编译参数加入-Wall,会发现出现了编译警告。此外,类似的问题在ARM64等其他平台下仍然存在,可以自行检索查询。

FROM :blog.iret.xyz | Author:blog.iret.xyz

相关推荐: 痕迹擦除

/01 痕迹清除简介在渗透测试过程中,Windows日志往往会记录系统上的敏感操作,如添加用户,远程登录,执行命令等。攻击者 通常会对Windows日志进行清除和绕过。 /02 Windows痕迹清除如何查看: 事件查看器->Windows日志 Win …

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: