格式输出安全实验
实验目的
了解格式化输出函数
了解格式字符串定义
掌握安全的格式化输出
实验原理
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将格式字符串确定下来
}