【问题标题】:How can I increase memory security in Delphi?如何提高 Delphi 中的内存安全性?
【发布时间】:2010-10-06 21:14:41
【问题描述】:

是否可以在 Delphi 中“擦除”字符串?让我解释一下:

我正在编写一个应用程序,其中包含一个用于授权用户的 DLL。它将加密文件读入 XML DOM,使用那里的信息,然后释放 DOM。

很明显,未加密的 XML 仍然位于 DLL 的内存中,因此容易受到检查。现在,我不会过分保护它——用户可以创建另一个 DLL——但我想采取一个基本步骤来防止用户名在内存中存在很长时间。但是,由于引用,我认为无论如何我都不能轻易擦除内存。如果我遍历我的 DOM(这是一个 TNativeXML 类)并找到每个字符串实例,然后将其变成类似“aaaaa”的内容,那么它实际上不会将新字符串指针分配给 DOM 引用,然后将旧字符串留在原处内存中有等待重新分配吗?有没有办法确保我正在杀死唯一的原始副本?

或者在 D2007 中是否有一种方法可以告诉它从堆中擦除所有未使用的内存?所以我可以释放 DOM,然后告诉它擦除。

或者我应该继续我的下一个任务并忘记它,因为它真的不值得打扰。

【问题讨论】:

    标签: security delphi memory-management ram-scraping


    【解决方案1】:

    混乱,但您可以记下您在堆中填充敏感数据时使用的堆大小,然后在释放时执行 GetMem 为您分配一个大块(例如)200%那个。对该块进行填充,并假设任何碎片对考官都有很大用处。 布里

    【讨论】:

    • 我想过这个问题,但是随着内存管理器变得更加智能,并且从不同大小的块中分配,一个 16 字节的字符串很可能来自一个完全不同的子堆到一个 12,000 字节的块.
    【解决方案2】:

    如何将文件解密为流,使用 SAX 处理器而不是 XML DOM 进行验证,然后在释放之前覆盖解密的流?

    【讨论】:

    • 我已经考虑过了,但我还需要能够在 DLL 的某些构建中更新 XML,这样 DOM 使事情变得更容易。
    • @Bruce McGee:如果您正在考虑一个 TMemoryStream,那么无法就地执行的内存重新分配会怎样,他们会不会将部分解密的数据留在某个地方而不知道它们的存在?
    • 我想总会有一些东西留在记忆中。
    【解决方案3】:

    如何将密码保留为 XML 中的哈希值,并通过将输入密码的哈希与 XML 中的哈希密码进行比较来验证。

    编辑:您可以对所有敏感数据进行加密,并仅在可能的最后一刻解密。

    【讨论】:

    • 密码已按照您的建议进行哈希处理。只有用户名和他们的访问级别是“可见的”,但即使是这些也是黑客的线索。
    • 保持这些值的加密和解密及时或转到下一个任务。
    【解决方案4】:

    DLL 不拥有分配的内存,进程拥有。一旦进程终止,您的特定进程分配的内存将被丢弃,无论 DLL 是否挂起(因为它正在被另一个进程使用)。

    【讨论】:

    • 确实如此,但 DLL 可以由黑客的应用程序加载,并调用加载 DOM 的函数。那时,他们可以断点并使用调试器查看内存。显然,如果它们在我的代码中设置断点,我无法阻止它,但我希望它在我返回时保持干净。
    • @mj2008:实际上比这更糟糕 - 拥有调试您自己的应用程序的权限就足够了,无需创建虚拟应用程序并找出如何调用您的 DLL。破解者基本上只需要windbg和一点诀窍。
    【解决方案5】:

    我认为这不值得打扰,因为如果用户可以使用 DLL 读取进程的内存,则同一用户也可以在任何给定时间点停止执行。在内存被擦除之前停止执行仍然会让用户完全访问未加密的数据。

    IMO 任何足够感兴趣并能够执行您所描述的操作的用户都不会因您的 DLL 擦除内存而严重不便。

    【讨论】:

      【解决方案6】:

      关于此的两个一般要点:

      首先,这是“如果你不得不问,你可能不应该这样做”的领域之一。请不要采取错误的方式;我的意思不是不尊重你的编程技能。只是编写安全、加密功能强大的软件是你要么是专家要么不是专家的事情。就像知道“一点点空手道”比完全不知道空手道要危险得多一样。有许多第三方工具可用于在 Delphi 中编写安全软件,并提供专家支持;我强烈鼓励没有深入了解 Windows 中的密码服务、密码学的数学基础以及击败侧信道攻击经验的人使用它们,而不是尝试“自己动手”。

      回答您的具体问题:Windows API 有许多有用的函数,例如CryptProtectMemory。但是,如果您对内存进行加密,但系统的其他地方有漏洞,或者暴露了侧通道,这将带来一种错误的安全感。这就像在你的门上锁上锁,但让窗户保持打开状态。

      【讨论】:

      • 谢谢 - 没有冒犯!我很清楚这些困难,因此我也问是否值得打扰。我会看一下 API - 这可能有助于阻止普通黑客。
      • @Craig Stuntz:感谢您发布指向该 API 函数的链接,页面文件中显示了有关内存内容的有趣信息 - 我没有想到。
      【解决方案7】:

      如果您在完全调试模式下使用 FastMM 内存管理器,那么您可以强制它在内存被释放时覆盖它。

      通常该行为用于检测野指针,但它也可以用于您想要的。

      另一方面,请确保您了解 Craig Stuntz 所写的内容:不要自己编写这些身份验证和授权内容,尽可能使用底层操作系统。

      顺便说一句:Hallvard Vassbotn 写了一篇关于 FastMM 的不错的博客: http://hallvards.blogspot.com/2007/05/use-full-fastmm-consider-donating.html

      问候,

      杰伦·普莱默斯

      【讨论】:

        【解决方案8】:

        是否可以将解密的 XML 加载到 char 或 byte 数组而不是字符串中?那么就没有写时复制处理,所以你可以在释放之前用#0回填内存吗?

        如果将 char 数组分配给字符串,请小心,因为 Delphi 在这里有一些智能处理,以便与传统的 char 压缩数组 [1..x] 兼容。

        另外,你能用 ShortString 吗?

        【讨论】:

        • 一个好主意 - 完全避免使用智能字符串。 DOM 可能不会这样做,但它会很聪明。
        【解决方案9】:

        如果您使用 XML(即使是加密的)来存储密码,您的用户也会面临风险。更好的方法是存储密码的哈希值,然后将哈希值与输入的密码进行比较。这种方法的优点是即使知道散列值,您也不会知道产生散列的密码。添加蛮力标识符(计算无效密码尝试次数,并在一定次数后锁定帐户)将进一步提高安全性。

        您可以使用多种方法来创建字符串的哈希值。一个好的起点是查看 turbo power 开源项目“LockBox”,我相信它有几个创建单向哈希键的示例。

        编辑

        但是,如果它是一种方法,那么知道哈希值有什么帮助呢?如果你真的很偏执,你可以通过只有你知道的可预测的东西来修改哈希值......比如说,使用特定种子值加上日期的随机数。然后,您可以在 xml 中仅存储足够的哈希值,以便将其用作比较的起点。伪随机数生成器的好处在于,它们总是在给定相同种子的情况下生成相同系列的“随机”数。

        【讨论】:

        • 所有优点,并且已经实施,但实际上并没有回答安全的核心问题。如果攻击者可以看到哈希,他们就向前迈出了一大步。我的问题是试图减少哈希在内存中可用的时间。
        【解决方案10】:

        这样的事情怎么样?

        procedure WipeString(const str: String);
        var
          i:Integer;
          iSize:Integer;
          pData:PChar;
        
        begin
            iSize := Length(str);
            pData := PChar(str);
        
            for i := 0 to 7 do
            begin
              ZeroMemory(pData, iSize);
              FillMemory(pData, iSize, $FF); // 1111 1111
              FillMemory(pData, iSize, $AA); // 1010 1010
              FillMemory(pData, iSize, $55); // 0101 0101
              ZeroMemory(pData, iSize);
            end;
        end;
        

        【讨论】:

          【解决方案11】:

          小心那些试图将字符串视为指针的函数,并尝试使用FillCharZeroMemory 擦除字符串内容。

          • 这都是错误的(字符串是共享的;你在搞砸正在使用该字符串的其他人)
          • 并且可能导致访问冲突(如果字符串恰好是一个常量,则它位于进程地址空间中的只读数据页上;尝试写入它是访问冲突)

           

          procedure BurnString(var s: UnicodeString);
          begin
              {
                  If the string is actually constant (reference count of -1), then any attempt to burn it will be
                  an access violation; as the memory is sitting in a read-only data page.
          
                  But Delphi provides no supported way to get the reference count of a string.
          
                  It's also an issue if someone else is currently using the string (i.e. Reference Count > 1).
                  If the string were only referenced by the caller (with a reference count of 1), then
                  our function here, which received the string through a var reference would also have the string with
                  a reference count of one.
          
                  Either way, we can only burn the string if there's no other reference.
          
                  The use of UniqueString, while counter-intuitiave, is the best approach.
                  If you pass an unencrypted password to BurnString as a var parameter, and there were another reference,
                  the string would still contain the password on exit. You can argue that what's the point of making a *copy*
                  of a string only to burn the copy. Two things:
          
                      - if you're debugging it, the string you passed will now be burned (i.e. your local variable will be empty)
                      - most of the time the RefCount will be 1. When RefCount is one, UniqueString does nothing, so we *are* burning
                          the only string
              }
              if Length(s) > 0 then
              begin
                  System.UniqueString(s); //ensure the passed in string has a reference count of one
                  ZeroMemory(Pointer(s), System.Length(s)*SizeOf(WideChar));
          
                  {
                      By not calling UniqueString, we only save on a memory allocation and wipe if RefCnt <> 1
                      It's an unsafe micro-optimization because we're using undocumented offsets to reference counts.
          
                      And i'm really uncomfortable using it because it really is undocumented.
                      It is absolutely a given that it won't change. And we'd have stopping using Delphi long before
                      it changes. But i just can't do it.
                  }
                  //if PLongInt(PByte(S) - 8)^ = 1 then //RefCnt=1
                  //  ZeroMemory(Pointer(s), System.Length(s)*SizeOf(WideChar));
          
                  s := ''; //We want the callee to see their passed string come back as empty (even if it was shared with other variables)
              end;
          end;
          

          拥有UnicodeString 版本后,您可以创建AnsiStringWideString 版本:

          procedure BurnString(var s: AnsiString); overload;
          begin
              if Length(s) > 0 then
              begin
                  System.UniqueString(s);
                  ZeroMemory(Pointer(s), System.Length(s)*SizeOf(AnsiChar));
          
                  //if PLongInt(PByte(S) - 8)^ = 1 then //RefCount=1
                  //  ZeroMemory(Pointer(s), System.Length(s)*SizeOf(AnsiChar));
          
                  s := '';
              end;
          end;
          
          procedure BurnString(var s: WideString);
          begin
              //WideStrings (i.e. COM BSTRs) are not reference counted, but they are modifiable
              if Length(s) > 0 then
              begin
                  ZeroMemory(Pointer(s), System.Length(s)*SizeOf(WideChar));
          
                  //if PLongInt(PByte(S) - 8)^ = 1 then //RefCount=1
                  //  ZeroMemory(Pointer(s), System.Length(s)*SizeOf(AnsiChar));
          
                  s := '';
              end;
          end;
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2021-07-30
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-11-25
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多