【问题标题】:understanding the dangers of sprintf(...)了解 sprintf(...) 的危险
【发布时间】:2011-04-09 10:33:13
【问题描述】:

OWASP 说:

"strcpy等C库函数 ()、strcat()、sprintf() 和 vsprintf () 对以空字符结尾的字符串进行操作 并且不执行边界检查。”

sprintf 将格式化数据写入字符串 int sprintf ( char * str, const char * 格式, ... );

例子:

sprintf(str, "%s", message); // assume declaration and 
                             // initialization of variables

如果我理解 OWASP 的评论,那么使用 sprintf 的危险在于

1) 如果message的长度> str的长度,则存在缓冲区溢出

2) 如果 message 不以 \0 为空终止,则 message 可能会被复制到 str 的内存地址之外消息,导致缓冲区溢出

请确认/拒绝。谢谢

【问题讨论】:

  • 另外,sprintf(str, message) 或类似的程序员错误是真正的风险。

标签: c++ printf


【解决方案1】:

您在这两个问题上都是正确的,尽管它们实际上是同一个问题(即访问数组边界之外的数据)。

第一个问题的解决方案是改用std::snprintf,它接受缓冲区大小作为参数。

第二个问题的解决方案是为snprintf 提供最大长度参数。例如:

char buffer[128];

std::snprintf(buffer, sizeof(buffer), "This is a %.4s\n", "testGARBAGE DATA");

// std::strcmp(buffer, "This is a test\n") == 0

如果你想存储整个字符串(例如sizeof(buffer)太小),运行snprintf两次:

int length = std::snprintf(nullptr, 0, "This is a %.4s\n", "testGARBAGE DATA");

++length;           // +1 for null terminator
char *buffer = new char[length];

std::snprintf(buffer, length, "This is a %.4s\n", "testGARBAGE DATA");

(您可能可以使用va 或可变参数模板将其放入函数中。)

【讨论】:

  • snprintf 只解决第一个问题时,他们是如何“同一个问题”的?
  • @Rob Kennedy,他们在不同的地方有同样的问题。抱歉,不清楚。你说得对; snprintf 盲目替换 sprintf 只修复了第一个。
  • snprintf 也会截断传递的参数,解决数字 2(如果消息不是 NULL 终止,则只会写入前 N 个字符)。
  • @dash-tom-bang,虽然这可能是真的,但它仍然可能涉足未定义行为的领域。
  • 但是@Dash,如果 N 很大,而 message 应该很短但缺少空终止符,那么 N 的巨大值并不能保护您免受访问冲突的影响读得太远,或泄露本应保密的记忆内容。
【解决方案2】:

您的解释似乎是正确的。但是,您的案例 #2 并不是真正的缓冲区溢出。这更像是内存访问冲突。虽然这只是术语,但它仍然是一个主要问题。

【讨论】:

    【解决方案3】:

    是的,这主要是缓冲区溢出的问题。然而,如今这些都是相当严重的业务,因为缓冲区溢出是系统破解者用来绕过软件或系统安全的主要攻击媒介。如果您将这样的内容暴露给用户输入,那么您很有可能会将您的程序(甚至您的计算机本身)的密钥交给破解者。

    从 OWASP 的角度来看,假设我们正在编写一个 Web 服务器,并且我们使用 sprintf 来解析浏览器传递给我们的输入。

    现在假设有人恶意向我们的网络浏览器传递了一个比我们选择的缓冲区大得多的字符串。他的额外数据将覆盖附近的数据。如果他把它做得足够大,他的一些数据将被复制到网络服务器的指令上,而不是它的数据上。现在他可以让我们的网络服务器执行他的代码

    【讨论】:

      【解决方案4】:

      你的两个断言都是正确的。

      还有一个未提及的问题。没有对参数进行类型检查。如果格式字符串和参数不匹配,可能会导致未定义和不良行为。例如:

      char buf[1024] = {0};
      float f = 42.0f;
      sprintf(buf, "%s", f);  // `f` isn't a string.  the sun may explode here
      

      这可能特别难调试。

      以上所有因素使许多 C++ 开发人员得出结论,您永远不应该使用sprintf 及其兄弟。实际上,您可以使用一些工具来避免上述所有问题。一,流,直接内置于语言中:

      #include <sstream>
      #include <string>
      
      // ...
      
      float f = 42.0f;
      
      stringstream ss;
      ss << f;
      string s = ss.str();
      

      ...对于像我一样仍然喜欢使用sprintf 的人来说,另一个受欢迎的选择来自boost Format libraries

      #include <string>
      #include <boost\format.hpp>
      
      // ...
      
      float f = 42.0f;
      string s = (boost::format("%1%") %f).str();
      

      您应该采用“从不使用 sprintf”的口头禅吗?自己决定。通常有适合这项工作的最佳工具,根据您的工作,sprintf 可能就是这样。

      【讨论】:

      • GCC(我相信还有很多其他人)执行软类型检查。另外,#include 中的路径分隔符是/,无论是什么平台。
      • @Potatoswatter:#include 中任何字符(包括\ /)的解释留给实现。该标准甚至没有声称存在文件系统,更不用说带有路径分隔符的路径了。
      • @MSalters:是的,但基本上每个支持路径名的实现都将支持/,以便与现有代码和教科书兼容,而不是与\ 兼容。也许更重要的是,在其中包含\u 可能会产生一个通用字符名称。就此而言,平台可以非常合理地决定替换转义序列来代替或除了用作分隔符之外。那么,有什么优势呢?
      【解决方案5】:

      sprintf 函数在与某些格式说明符一起使用时会带来两种类型的安全风险:(1) 写入不应写入的内存; (2) 不应该读取内存。如果 snprintf 与与缓冲区匹配的大小参数一起使用,它将不会写入任何不应该写入的内容。根据参数,它可能仍会读取不应读取的内容。根据操作环境和程序正在执行的其他操作,不正确读取的危险可能会或可能不会比不正确写入的危险更严重。

      【讨论】:

        【解决方案6】:

        您的 2 个编号结论是正确的,但不完整。

        还有一个额外的风险:

        char* format = 0;
        char buf[128];
        sprintf(buf, format, "hello");
        

        这里,format 不是以 NULL 结尾的。 sprintf() 也不检查。

        【讨论】:

          【解决方案7】:

          记住 sprintf() 在每个字符串的末尾添加 ASCII 0 字符作为字符串终止符非常重要。因此,目标缓冲区必须至少有 n+1 个字节(要打印单词“HELLO”,需要一个 6 字节的缓冲区,而不是 5 个)

          在下面的例子中,可能不是很明显,但是在 2 字节的目标缓冲区中,第二个字节会被 ASCII 0 字符覆盖。如果只为缓冲区分配 1 个字节,这将导致缓冲区溢出。

          char buf[3] = {'1', '2'};
          int n = sprintf(buf, "A");
          

          还要注意 sprintf() 的返回值不包括空终止字符。在上面的示例中,写入了 2 个字节,但函数返回 '1'。

          在下面的示例中,类成员变量“i”的第一个字节将被 sprintf() 部分覆盖(在 32 位系统上)。

          struct S
          {
              char buf[4];
              int i;
          };
          
          
          int main()
          {
              struct S s = { };
              s.i = 12345;
          
              int num = sprintf(s.buf, "ABCD");
              // The value of s.i is NOT 12345 anymore !
          
              return 0;
          }
          

          【讨论】:

            【解决方案8】:

            我几乎已经说明了一个小示例,您可以如何摆脱 sprintf 的缓冲区大小声明(当然,如果您打算这样做!)并且没有 snprintf 参与....

            注意:这是一个 APPEND/CONCATENATION 的例子,看看here

            【讨论】:

              猜你喜欢
              • 2013-03-21
              • 2017-12-25
              • 2018-09-04
              • 2021-12-11
              • 1970-01-01
              • 1970-01-01
              • 2010-12-10
              • 2015-07-11
              • 2014-07-11
              相关资源
              最近更新 更多