-->
当前位置:首页 > 实验 > 正文内容

格式输出安全实验

Luz3年前 (2020-12-11)实验2352

实验目的

  • 了解格式化输出函数

  • 了解格式字符串定义

  • 掌握安全的格式化输出

实验原理

1.格式化输出函数
  各个格式化输出函数由于历史不同, 实现上也有显著的差异。C标准中定义的格式化输出函数如下所示。

  • fprintf()按照格式字符串的内容将输出写入流中。流、格式字符串和变参列表一起作为参数提供给函数。

  • printf()等同于fprintf(),除了前者假定输出流为stdout外。

  • sprintf() 等同于fprintf(),但是输出不是写入流而是写入数组中。C标准规定在写入的字符末尾必须添加一个空字符。

  • snprintf()等同于sprintf(),但是它指定了可写入字符的最大值n。当n非零时输出的字符超过第n-l个的部分会被舍弃而不会写人数组中。并且,在写人数组的字符末尾会添加一个空字符。

  • vfprintf()、vprintf()、vsprintf()、vsnprintf()分别对应于fprintf()、printf()、sprintf()和snprintf(),只是它们将后者的变参列表换成了va_list类型的参数。当参数列表是在运行时决定时,这些函数非常有用。

  另外一个名为syslog()的格式化输出函数并没有定义在C规范中,而是定义在POSIX中。这个函数接受一个优先级参数、一个格式规范以及该格式所需的任意参数,并且在系统的日志记录器(syslogd) 中生成一条日志消息。syslog()函数最先出现在BSD4.2中,后来Linux和其他现代POSIX的实现中也开始支持它,但未得到Windows系统的支持。
  对格式字符串的解释在C标准中规定。C运行时库一般遵从C标准,但是常常会包含一些非标准扩展。你通常可以使用任何一个格式化输出函数为一个特定的C运行时库以同样的方式解释格式字符串,因为它们几乎总是使用同一个子例程实现的。

2.格式字符串
  格式字符串是由普通字符(ordinary character) (包括%)和转换规范(conversionspecification) 构成的字符序列。普通字符被原封不动地复制到输出流中。转换规范根据与实参对应的转换指示符对其进行转换,然后将结果写入输出流中。
  转换规范通常以" %"开始按照从左向右的顺序解释。大多数转换规范都需要单个参数,但有时也可能需要多个或者完全不需要。程序员必须根据指定的格式提供相应个数的参数。当参数多于转换规范时,多余的将被忽略,而当参数不足时,则结果是未定义的。
  一个转换规范是由可选域(标志、宽度、精度以及长度修饰符) 和必需域(转换指示符)按照下面的格式组成的:

%[标志J[宽度][.精度][(长度修饰符1] 转换指示符

  例如,对转换规范%-10.8ld来说,一是标志位,10代表宽度,8代表精度,字母l是长度修饰符,d是转换指示符。这个转换规范将一个1ong int型的参数按照十进制格式打印,在一个最小宽度为10个字符的域中保持最少8位左对齐。
  每一个域都是代表特定格式选项的单个字符或数字。最简单的转换规范仅仅包含一个"%"和一个转换指示符(例如%s) 。

3.转换指示符
  转换指示符用来指示所应用的转换类型。它是唯一必需的格式域,出现在任意可选格式域之后。下图中列举了C标准中的一些转换指示符,包括在许多漏洞利用中扮演关键角色的n。


实验步骤

步骤1:格式化输出函数的格式化参数和实参类型必须匹配

  使用格式化字符串应该小心,确保格式字符和参数在数据类型上的匹配。格式字符和参数之间的不匹配会导致未定义的行为。大多数情况下,不正确的格式化字符串会可能会导致格式化漏洞,使程序异常终止。
  1.1 错误示例1:格式字符和参数的类型不匹配。

void  Noncompliant_ArgMismatch()

{

   char *error_msg = "Resource not available to user.";

   int error_type = 3;

   /* ...do something... */

   printf("Error (type %s): %d\n", error_type, error_msg); /*【错误】格式化参数类型不匹配 */

}

  推荐做法:

void  Noncompliant_ArgMismatch()

{

   char *error_msg = "Resource not available to user.";

   int error_type = 3;

   /* ...do something... */

   printf("Error (type %s): %d\n", error_msg, error_type); /*【修改】匹配格式化参数类型 */

}

  1.2 错误示例2:将结构体作为参数。

void  Noncompliant_StructAsArg()

{

   struct sParam

   {

       int num;

       char msg[100];

       int result;

   };

   struct sParam tmp = {10, "hello Baby!", 0};

   char *errormsg = "Resource not available to user.";

   int errortype = 3;

   /* ...do something... */

   if (tmp.result == 0)

   {

       printf("Error Param: %s \n", tmp); /*【错误】不能将整个结构体作为格式化参数 */

   }  

}

  推荐做法:

void  Noncompliant_StructAsArg()

{

   struct sParam

   {

       int num;

       char msg[100];

       int result;

   };

   struct sParam tmp = {10, "hello Baby!", 0};

   char *errormsg = "Resource not available to user.";

   int errortype = 3;

   /* ...do something... */

   if (tmp.result == 0)

   {

       printf("Error Param:num=%d, msg=%s, result=%d\n", tmp.num, tmp.msg, tmp.result);  //【修改】将结构体的内部变量作为格式化参数

   }  

}

步骤2:格式化输出函数的格式化参数和实参个数必须匹配

  2.1 使用格式化字符串应该小心,确保格式字符和参数在数量上的匹配。格式字符和参数之间的不匹配会导致未定义的行为。大多数情况下,不正确的格式化字符串会导致程序异常终止。
  错误示例:格式字符和参数的数量不匹配,格式化字符串在编码时会大量使用,如拼装SQL语句和拼装调试信息。尤其是调试信息,量大时容易copy-paste省事,这就容易出现不匹配的错误。

void  Noncompliant()

{

   char *error_msg = "Resource not available to user.";

   /* ...do something... */

   printf("Error (type %s)\n");    //【错误】格式化参数个数不匹配

}

  推荐做法:

void  Compliant()

{

   char *error_msg = "Resource not available to user.";

   /* ...do something... */

   printf("Error (type %s)\n", error_msg); //【修改】使格式化参数个数匹配

}

步骤3:禁止以用户输入来构造格式化字符串
  3.1 调用格式化I/O函数时,不要直接或者间接将用户输入作为格式化字符串的一部分或者全部。如果攻击者对一个格式化字符串可以部分或完全控制,将导致进程崩溃、查看栈的内容、改写内存、甚至执行任意代码等风险。
  错误示例:下列代码直接将用户输入作为格式字符串输出。

void Noncompliant(char *user, char *password)

{

   char input[1000];

   if (fgets(input, sizeof(input) - 1, stdin) == NULL)

   {

       /* handle error */

   }

   input[sizeof(input)-1] = '\0';

   printf(input); //【错误】不允许将用户输入直接作为格式字符串

}

  示例代码的input直接来自用户输入,并作为格式化字符串直接传递给printf()。当用户输入的是"%s%s%s%s%s%s%s%s%s%s%s%s",就可能触发无效指针或未映射的地址读取。格式字符%s显示栈上相应参数所指定的地址的内存。这里input被当成格式化字符串,而没有提供参数,因此printf()读取栈中任意内存位置,直到格式字符耗尽或者遇到一个无效指针或未映射地址为止。
  推荐做法:通过显式参数"%s"将 printf()的格式化字符串确定下来。

void Compliant(char *user, char *password)

{

   char input[1000];

   if (fgets(input, sizeof(input)-1, stdin) == NULL)

   {

       /* handle error */

   }

   input[sizeof(input)-1] = '\0';

   printf("%s", input);  //【修改】通过%s将格式字符串确定下来

}

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。