0x00 背景

TPCTF里和qym师傅研究了两天safehttpd这题(虽然我一直在背英语pre,没干什么活),把整个程序可能的漏洞点翻遍了也没找到突破口的off by null如何触发(实际上是测试过程中出现了重大失误,已经找对了地方却没有测试出漏洞),赛后看wp发现有这样一个CVE: 30068 – (CVE-2023-25139) incorrect printf output for integers with thousands separator and width field (CVE-2023-25139) (sourceware.org)

0x01 分析

先看上面链接中的复现样例:

#include <stdio.h>
#include <locale.h>

int main (void)
{
  if (setlocale (LC_ALL, ""))
    {
      printf ("1234567890123:\n");
      printf ("%0+ -'13ld:\n", 1234567L);
    }
  return 0;
}

在有漏洞的Glibc2.37下的输出:

1234567890123:
+1,234,567     :

输出的长度是15而不是13,因为两个千位分隔符没有被计入宽度,导致输出时多补了两个空格。 这是一个glibc 2.37里短暂出现就被迅速修复的漏洞:千位分隔符在限制长度的格式化输出时没有被正确计入宽度,导致出现了溢出。 由该commit修复: Account for grouping in printf width (bug 30068) · bminor/glibc@c980549 (github.com) 下面就通过这个修复的commit分析一下这个bug是如何产生的。

其中第266行由

width -= workend - string + prec

改成了

width -= number_length + prec_inc

这里的width变量为补足宽度限制需要添加的字符的宽度。prec和prec_inc的值是相同的,区别在于number_length和workend - string并不等同:(168-182行)

  int number_length;
#ifndef COMPILE_WPRINTF
  if (use_outdigits && base == 10)
    number_length = __translated_number_width (_NL_CURRENT_LOCALE,
                                               string, workend);
  else
    number_length = workend - string;
  if (group)
    number_length += iter.separators * strlen (thousands_sep);
#else
  number_length = workend - string;
  /* All wide separators have length 1.  */
  if (group && thousands_sep != L'\0')
    number_length += iter.separators;
#endif

在上面代码的后几行可以看到number_length是原本的数字长度加上千位分隔符的长度,而workend-string没有计算千位分隔符的长度,导致了错误的长度计算。

if (!left)
    {
      width -= number_length + prec;

      if (number.word != 0 && alt && (base == 16 || base == 2))
        /* Account for 0X, 0x, 0B or 0b hex or binary marker.  */
	@@ -221,7 +227,7 @@ LABEL (unsigned_number):      /* Unsigned number of base BASE.  */
          Xprintf_buffer_putc (buf, spec);
        }

      width += prec;
      Xprintf_buffer_pad (buf, L_('0'), width);

      if (octal_marker)
	@@ -237,6 +243,8 @@ LABEL (unsigned_number):      /* Unsigned number of base BASE.  */
    }
  else
    {
      if (is_negative)
        {
          Xprintf_buffer_putc (buf, L_('-'));
	@@ -263,9 +271,13 @@ LABEL (unsigned_number):      /* Unsigned number of base BASE.  */
      if (octal_marker)
	--width;

      width -= workend - string + prec;
	  ...
	}

另外,根据代码可以看出只有在左对齐的情况下(没有进入if(!left), 而是进入下面的else)长度计算是错误的,进行测试:

#include <locale.h>
#include <stdio.h>

int main() {
  if (setlocale(LC_ALL, "en_US.utf8")) {
    printf("1234567890123:\n");
    printf("%+-'13ld:\n", 1234567L);
    printf("%0+'13ld:\n", 1234567L);
  }
  return 0;
}

输出:

 ./test
1234567890123:
+1,234,567     :
+0001,234,567:

可以看到确实只有左对齐时由于未减去两个千位分隔符的宽度而多填充了两个空格,右对齐时是正常的。

0x02 其他

  1. 将prec替换成prec_inc可能是因为后续还需要用到原来的prec的值,并且这样使变量的语义更加清晰,(由于本人阅读代码水平不足没有查证。
  2. 千位分隔符和精度限制同时使用早在2.27时就有前导0缺少千位分隔符(不确定这是不是bug)以及空格的编码长度会影响补0的数量的bug。看来即使是写标准库的程序员处理这种东西也很头痛。 23432 – incorrect printf output for integers with thousands separator and precision field larger than the number of digits (needing leading zeros) (sourceware.org)
  3. 赛后复盘时发现搜索sprintf CVE/Glibc sprintf vuln关键词可以定位到这个CVE,而sprintf thousand separator vulnerability/bug/…则难以定位到,一部分原因是其它语言的sprintf造成的干扰、以及CVE的指向性才足够明确。