2017 年 11 月

第 33 卷,第 11 期

安全性 - 保护数据和应用免受未经授权的披露和使用

作者 Joe Sewell

“数据泄露” 是软件开发中最令人胆寒的一个词。软件归根到底和数据有关:处理数据、管理数据和保护数据。如果攻击者可以泄露数据,企业可能会失去对成功至关重要的机密信息,承担难以想象的责任并失去宝贵的品牌尊重和客户忠诚度。

为了管理这种风险并遵守 HIPAA、GDPR 等规定,明智的开发者和运营团队会对数据库和 Web 服务实施安全控制措施。这些控制措施包括端到端加密、严格的身份管理以及网络行为异常检测。在发生安全事件时,许多此类控制措施将通过记录事件详细信息、发送警报和阻止可疑请求而作出积极反应。

虽然这些控制措施和其他最佳实践可以保护你的服务器组件,但它们对于客户端软件却做不了太多。并且,为了使数据发挥作用,必须将其以某种形式对特权用户和软件公开。那么,如何确定客户端软件不会导致数据泄露?

例如,许多公司会创建专门供员工使用的软件,通常旨在访问敏感的企业数据。另外,虽然 Microsoft .NET Framework 让通过类似 C# 或 Visual Basic .NE 的语言开发业务线 (LOB) 应用程序变得简单,但这些已编译的应用程序仍包含高级元数据和中间代码。这使得恶意人员很容易在未经授权使用调试器的情况下操纵应用程序,或者对应用程序执行反向工程并创建易受攻击的版本。即使服务器组件非常安全,上述两种情况都可能导致数据泄露。

尽管可以采取一些措施(仅列举两个:Authenticode 签名和代码混淆)来防范这些攻击,但其中大多数都是被动防御,因为它们只是阻止攻击,而不是检测、报告攻击并做出反应。但在最近,Visual Studio 中的新功能允许在 .NET 应用程序中注入威胁检测、报告和反应功能,几乎不需要额外的编码。这些运行时检查属于主动防护措施,可以根据安全威胁更改应用程序的行为,从而保护敏感数据。

在本文中,我将使用典型的 LOB 应用程序作为示例,提供有效使用运行时检查的方法。我将探讨攻击者如何利用调试器泄露数据,详细说明运行时检查如何防止这种泄露,并讨论这些控制措施在分层保护方法中的角色。

示例解决方案

为了帮助解释数据泄露如何在客户端发生,我准备了一个可以从 bit.ly/2yiT2eY 下载的示例 LOB 解决方案。示例代码的 READ-ME 中提供了有关构建、运行和使用解决方案各个部分的说明。

该解决方案有四个组件:

AdventureWorks2014 数据库:这是包含 Adventure Works 2014 OLTP 数据库示例的 Microsoft SQL Server 数据库。可以从 bit.ly/2wM0yy0 下载该示例。

****Adventure Works 销售服务:这是一个 ASP.NET Web 服务,用于公开数据库中的客户数据,其中包括信用卡等敏感数据。为了使这个组件更容易设置,示例代码省略了大多数的安全控制措施,但就本文而言,我将假定该服务实现了以下内容:

  • 身份验证的两个因素—用户密码和登录时发送到用户手机的短信
  • 限时会话
  • 针对所有请求和响应的 SSL 加密

Adventure Works Sales Client:这是一个 Windows Presentation Foundation (WPF) 桌面客户端,它连接到销售服务以操纵客户数据。这是本文最关注的组件。

销售员工运行该客户端时,他们通过 LoginDialog 登录,LoginDialog 可以启动身份验证会话并打开 CustomerWindow。在此窗口中,员工可以查看和编辑客户姓名,打开 EmailWindow、PhoneWindow 或 CreditCardWindow 来编辑客户的敏感数据。在名为 Utilities 的静态类中也提供了一些常用函数。

****Application Insights:虽然不需要运行示例,但销售服务和客户端都可以将使用情况和错误遥测发送到 Application Insights。通过本文讨论的运行时检查,客户端的遥测还包括安全事件报告。

就本文而言,我将重点关注如何保护销售客户端。我将假设已对数据库和销售服务进行了保护。当然,在真实情况下这不是一个安全的假设,但是它有助于说明一个问题:即使在服务器安全方面“做得滴水不漏”,仍然可以通过客户端软件进行数据泄露。

我还将客户姓名视为非敏感数据,而是重点保护电子邮件地址、电话号码和信用卡。在真实情况下,客户姓名也被认为是敏感数据,而非敏感数据可能包括诸如零售商店地址之类的信息。

利用调试器进行数据泄露

调试器是一款优秀的开发工具。通过调试器,你可以发现严重的逻辑错误,逐步执行棘手的控制流方案并诊断故障转储。但是,像任何工具一样,调试器也可以被恶意利用。

假设 Adventure Works Intranet 有一个恶意人员,可能是员工、外部承包商,甚至是未经授权访问 Intranet 的外部攻击者。此攻击者无权访问数据库或销售服务,但可以访问销售员工的便携式计算机。当然,这是一个安全问题,但是由于销售服务实施了双重身份验证,攻击者无法访问员工的电话,客户数据应该是安全的,对吗?

其实不然。攻击者可以等待销售员工通过销售客户端登录,然后手动或通过脚本将调试器附加到客户端进程。由于客户端是一个 .NET 应用程序,即使没有调试符号(例如 PDB 文件),调试器也会显示大量的高级信息,其中包括会话令牌。

图 1演示了这种情况。使用带有 Psscor4 扩展 (bit.ly/2hbG2kk) 的 WinDbg 调试器 (bit.ly/2sh4clf),我将不同的 .NET 对象转储到正在运行销售客户端进程的内存中。我最终找到了 AuthToken 对象,并转储了其 HashField 属性的值。

WinDbg 在销售客户端中显示会话令牌

图 1 WinDbg 在销售客户端中显示会话令牌

使用此会话令牌,攻击者可以通过员工姓名向销售服务进行身份验证请求。攻击者无需继续调试或操纵客户端;一旦拥有会话令牌,他就可以直接转到 Web 服务并使用该令牌来导致数据泄露。

在一些其他情况下,恶意人员可能以未经授权的方式使用客户端:

直接操作敏感数据:虽然前面的方案是会话劫持攻击,但由于销售客户端在正常操作中也会访问敏感数据(如信用卡),因此也可以通过调试器查看此类数据。攻击者甚至可能导致应用程序行为异常或修改数据库中的数据。

****反向工程:攻击者也可以自己运行客户端并附加调试器来发现客户端的工作方式。结合反向编译 .NET 应用程序的简易性,攻击者可能会发现漏洞或其他有关该客户端或服务的重要详细信息,以帮助他们规划攻击。

篡改:如果攻击者可以对应用程序进行反向工程并访问员工的文件系统,则可以用修改的客户端替换合法的客户端,在员工登录时秘密提取或操纵数据。

其他应用程序可能以不同方式轻易受到调试器的影响。例如,为了跟踪现场工作而报告员工位置的应用程序可能会被操纵以提供不准确的数据。或者,一个游戏可能会在调试器中显示关键的战略信息。

关于运行时检查

运行时检查是 PreEmptive Protection - Dotfuscator Community Edition (CE) 中的新功能,它是自 2003 年以来 Visual Studio 附带的一个保护工具 (bit.ly/2wB0b9g)。你可能知道 Dotfuscator CE 可以混淆 .NET 程序集的中间代码,但是混淆不是本文的主题。相反,我将展示如何使用运行时检查(以下简称检查)以允许销售客户端在运行时保护自己。

检查是 Dotfuscator 可以注入到 .NET 应用程序的预生成验证。然后,应用程序将能够检测到未经授权的使用,如调试或篡改。尽管有名称,但检查不仅仅是检测这些状态,还会以预先指定的方式作出反应,例如退出应用程序。检查也可以调用应用程序代码,允许基于检查结果的自定义行为。这些报告和响应功能可以根据检查配置,因此你的所有应用程序都可以用相同方式检测未经授权的使用情况,但每个应用程序都可以用不同方式对检测结果作出反应。

该代码示例包含一个 Dotfuscator.xml 配置文件,可以指示 Dotfuscator 保护销售客户端免受未经授权的调试和篡改。在本文的其余部分中,我将解释如何创建此配置、我做了什么选择以及如何通过类似方式配置 Dotfuscator 来保护自己的应用程序。

设置工具

Dotfuscator CE 入门的最简单方法是使用 Visual Studio 快速启动 (Ctrl+Q) 来搜索“dotfuscator”。 如果未安装 Dotfuscator,则会显示安装 PreEmptive Protection - Dotfuscator 的选项;选择该选项并确认适当的对话框。

一旦安装了 Dotfuscator,重复此搜索将提供一个启动“工具 | PreEmptive Protection - Dotfuscator”的选项;选择该选项可以开始使用 Dotfuscator。在一些典型的首次使用对话框之后,Dotfuscator CE 用户界面打开。

重要事项: 这里介绍的和示例中包含的保护至少需要 Dotfuscator CE 5.32 版本。可以通过选择“帮助 | 关于”查看已安装的版本。如果使用的是早期版本,请从 bit.ly/2fuUeow 下载最新版本的 Community Edition。

Dotfuscator 在专用配置文件上运行,这些文件用于指定应保护的程序集以及如何应用保护。Dotfuscator 以加载的新配置文件开始;我使用以下步骤针对销售客户端进行了调整:

  1. 首先,我将新的配置文件保存为 AdventureWorksSalesClient\Dotfuscator.xml。
  2. 接下来,我告诉 Dotfuscator 在哪里找到客户端的程序集。我切换到 Dotfuscator 输入屏幕,并单击绿色的加号图标。从“选择输入”浏览对话框中,我导航至 AdventureWorksSalesClient\bin\Release 目录,然后单击“打开”而不选择文件。
  3. Dotfuscator 将整个目录添加为名为 Release 的输入。我展开了树节点来验证 AdventureWorksSalesClient.exe 程序集是否存在。
  4. 然后,我使配置文件变得可移植,而不是特定于我的环境中的绝对路径。我选择了 Release 节点,单击铅笔图标,并用 ${configdir}\bin\Release 替换绝对路径。${configdir} 是一个 Dotfuscator 宏(代表保存配置文件的目录)。
  5. 最后,由于本文并不关注 Dotfuscator 代码混淆功能,因此我通过右键单击 Dotfuscator 导航列表中的“重命名”项并取消选中“启用”来禁用了它们。

配置检查注入

通过 Dotfuscator,可以在“检查”选项卡的“注入”屏幕上配置检查。但是,为配置做出的选择取决于所保护的应用程序的类型。我没有列出所有功能和设置,而是逐步介绍了我为销售客户端示例所做的选择和配置。

对于该示例,我配置了三项检查:

  • 两项调试检查,检测未经授权使用调试器:
    • 一项“登录”调试检查,检测如前所述的会话劫持情况
    • 一项“查询”调试检查,检测用于读取/写入客户端敏感数据的调试器
  • 一项“篡改”检查,检测修改后的应用程序二进制文件的使用

****图 2 简要介绍了检查和与之交互的应用程序代码。在接下来的三节中,我将逐一解释这些检查的用途和配置。

使用注入的运行时检查的销售客户端

图 2 使用注入的运行时检查的销售客户端

配置登录调试检查

第一个调试检查解决了会话劫持情况。它检测身份验证过程中是否存在调试器,如果是,则通知应用程序。该应用程序会将事件报告给 Application Insights,然后再以非直观的方式报错。

我添加了该检查,方法就是单击“添加调试检查”按钮,这会显示一个新的配置窗口。如图 3 所示,我配置了该检查。

登录调试检查的配置

图 3 登录调试检查的配置

位置: 我首先选择了应用程序代码中应该运行此项检查的位置。由于该检查应在用户登录时检测调试,我选中了位置树中的 LoginDialog.ConfirmLogin 方法。

请注意,该检查仅在调用其位置时运行。如果攻击者稍后再附加调试器,则该检查将检测不到它;但是稍后我会用查询调试检查来解决这个问题。

应用程序通知: 检查运行后,它可以通知应用程序代码,以便应用程序可以报告并以自定义的方式作出反应。接收此通知的代码元素称为应用程序通知接收器,并使用以下检查属性进行配置:

  • ApplicationNotificationSinkElement:代码元素的类型(字段、方法等)
  • ApplicationNotificationSinkName:代码元素的名称
  • ApplicationNotificationSinkOwner:用来定义代码元素的类型

对于所有三项检查,我使用此功能将事件报告给 Application Insights。此外,对于此检查,我决定使用一个自定义响应,而不是 Dotfuscator 注入的默认响应(其他检查将使用)。我的响应允许成功登录,但很快应用程序就会崩溃。通过分离检测和响应,让攻击者更难发现和绕过控制措施。

为了完成这个响应,我在 LoginDialog 类中添加了一个布尔字段 isDebugged,并将其配置为 Check's Sink。检查运行时(也就是应用程序调用 LoginDialog.ConfirmLogin 时),调试器检测的结果存储在此字段中:检测到调试器为 true;否则为 false。

请注意,接收器必须可以从检查位置访问和写入。由于位置和接收器都是 LoginDialog 类的实例成员,所以符合这个规则。

接下来,我修改了 LoginDialog.RunUserSession 以将此字段传递给 CustomerWindow 构造函数:

// In LoginDialog class
private void RunUserSession(AuthToken authToken) 
{
  // ...
  var customerWindow = new Windows.CustomerWindow(clients, isDebugged);
  // ...
}

然后,我使 CustomerWindow 构造函数设置了自己的字段 CustomerWindow.isDebugged,然后将事件报告给 Application Insights:

// In CustomerWindow class
public CustomerWindow(Clients clients, bool isDebugged)
{
  // ...
  this.isDebugged = isDebugged;
  if (isDebugged)
  {
    // ClientAppInsights is a static class holding the Application 
    // Insights telemetry client
    ClientAppInsights.TelemetryClient.TrackEvent(
      "Debugger Detected at Login");
  }
  // ...
}

最后,我添加了将该字段读取到各种事件处理程序的代码。例如:

// In CustomerWindow class
private void FilterButton_OnClick(object sender, RoutedEventArgs e)
{
  // ...
  if (isDebugged) { throw new InvalidCastException(); }
  // ...
}

我将在本文稍后介绍字段名称 isDebugged 的显而易见性。

配置查询调试检查

由于调试器可以在执行期间的任何时候附加到客户端,因此仅有登录调试检查就足够。查询调试检查通过在应用程序即将查询敏感数据(例如信用卡号)时检查调试器来填补这一空白。这些数据的敏感性也意味着我不能像登录调试检查那样分离检测、报告和响应,因为这会让攻击者看到数据。相反,查询调试检查将报告事件,然后在检测到调试器时立即退出应用程序。

跟添加第一项检查的方式一样,我添加了第二项调试检查,但这次我进行的配置如****图 4 中所示。

查询调试检查的配置显示 CreditCardWindow.UpdateData 位置而不显示其他位置

图 4 查询调试检查的配置显示 CreditCardWindow.UpdateData 位置而不显示其他位置

位置: 我的方案中有三种敏感数据:电子邮件地址、电话号码和信用卡。幸运的是,你可以为单个检查选择多个位置。在这种情况下,这些位置是 EmailWindow.UpdateData、PhoneWindow.UpdatePhones 和 CreditCardWindow.UpdateData。无论何时调用这些位置,都将运行检查,这意味着我只需为所有三种敏感数据配置一组检查属性。

****应用程序通知: 如果有多个位置,则可以修改应用程序通知接收器的工作方式。在登录调试检查中,我可以将 LoginDialog.isDebugged 字段指定为“接收器”,因为该字段可以从检查的唯一位置 LoginDialog.ConfirmLogin 访问。这一次,每个位置必须都能够访问接收器。

值得注意的是,如果 ApplicationNotificationSinkOwner 属性为空,则接收器默认使用定义检查位置的类型。由于此检查具有多个位置,因此接收器将根据触发检查的位置而有所不同。在这种情况下,我将此属性留空,并将其他 ApplicationNotificationSink 属性设置为名为 ReportDebugging 的方法。

考虑使用 EmailWindow.ReportDebugging 方法:

// In EmailWindow class
private void ReportDebugging(bool isDebugging)
{
  if (isDebugging)
  {
    ClientAppInsights.TelemetryClient.TrackEvent(
      "Debugger Detected when Querying Sensitive Data",
      new Dictionary<string, string> { { "Query", "Email Addresses" } });
    ClientAppInsights.Shutdown();
  }

当应用程序调用 EmailWindow.UpdateData 方法时,检查运行,如果检测到调试,则调用参数为 true 的 ReportDebugging 方法,否则为 false。

应用程序代码调用 PhoneWindow.UpdatePhones 或 CreditCardWindow.UpdateData 时,会发生同样的情况,除了由检查调用的方法分别由 PhoneWindow 或 CreditCardWindow 定义。这些方法实施方式稍有不同,但它们都被命名为 ReportDebugging,取一个布尔参数并且不返回任何值。

操作: 如果连接调试器,为了使应用程序关闭,我将 Action 属性设置为 Exit。这会告诉 Dotfuscator 在检查检测到未授权状态时注入用来关闭应用程序的代码。请注意,检查在通知应用程序后执行此操作,所以在这种情况下,事件报告将在应用程序关闭之前发送。

配置篡改检查

最后,我添加了一个篡改检查来解决反向工程场景。我单击“添加篡改检查”按钮以配置新的篡改检查,如图 5 所示。

篡改检查的配置会显示 LoginDialog.ConfirmLogin 位置而不显示其他位置

图 5 篡改检查的配置会显示 LoginDialog.ConfirmLogin 位置而不显示其他位置

位置: 与查询调试检查一样,我选择了多个位置进行篡改检查:LoginDialog.ConfirmLogin、CustomerWindow.UpdateData 和 Utilities.ShowAndHandleDialog。

使用调试检查时,具有多个位置非常重要,因为调试器可以在执行期间的任何时候连接。但是一个篡改检查将在运行应用程序期间只有一个结果:运行时加载已修改或未修改的程序集。一个位置应该是不够的?实际上,由于这个检查是为了阻止被篡改的二进制文件,所以我不得不考虑攻击者能够从一个位置移除篡改检查本身的情况。通过拥有多个位置,应用程序更能主动地应对篡改。

你可能会注意到其中一个位置 LoginDialog.ConfirmLogin 与登录调试检查的位置相同。Dotfuscator 允许在同一位置注入不同类型的多个检查。在这种情况下,用户登录后,将检查调试和篡改。

****应用程序通知: 对于应用程序通知接收器,我认为在这种情况下最好仅为所有的位置提供一个接收器。这是因为与查询调试检查不同,我并不关注哪个位置触发检查的报告相关内容。

我选择将 Utilities.ReportTampering 方法定义为 Sink。由于每个位置的上下文不同,我不得不将 Sink 声明为静态,并确保从每个位置都能访问它。该方法定义如下:

// In Utilities static class
internal static void ReportTampering(bool isTampered)
{
  if (isTampered)
  {
    ClientAppInsights.TelemetryClient.TrackEvent(“Tampering Detected”);
    ClientAppInsights.Shutdown();
  }
}

每当调用该检查的任意位置时,检查都将确定自 Dotfuscator 处理它以来是否已被修改,然后在检测到修改的情况下调用参数为 true 的 ReportTampering 方法,否则为 false。

操作: 如果应用程序被修改,则继续操作是危险的。我将此检查的“操作”配置为“退出”,以便在发现篡改时关闭应用程序。

注入检查

配置完检查后,Dotfuscator 现在可以将它们注入到应用程序。为此,从 Dotfuscator CE 打开 AdventureWorksSalesClient \ Dotfuscator.xml 配置文件,然后单击“生成”按钮。

Dotfuscator 将处理客户端的程序集,注入检查,并将受保护的客户端写入 AdventureWorksSalesClient\Dotfuscated\Release。未受保护的应用程序保留在 AdventureWorksSalesClient\bin\Release 中。

测试检查

与任何安全控制措施一样,引入控制时测试应用程序的行为也很重要。

**正常情况:**检查不应对应用程序的合法用户造成任何影响。我正常运行受保护的客户端时没有发现意外的崩溃、应用程序退出或 Application Insights 事件。

未经授权的情况: 还应验证当应用程序以未经授权的方式使用时检查是否执行你期望的操作。示例代码的自述文件列出了用于测试调试检查和篡改检查的详细说明。例如,为了测试查询调试检查,我多次运行受保护的客户端,在不同点将 WinDbg 附加到进程。应用程序正确地报告并根据检查的配置对调试器的存在作出了响应。

分层保护策略

在大多数实际情况下,仅使用一种保护措施是不够的,检查也不例外。检查,连同端到端加密,Authenticode 程序集签名等技术,应该只是保护策略的一个层面。使用多层保护时,一层的强度可以抵消另一层的弱点。

在本文的示例中,检查使用的某些应用程序通知接收器具有诸如 isDebugged 或 ReportTampering 的名称。这些名称保留在已编译的 .NET 程序集中,攻击者可以轻松理解这些代码元素的意图并绕开。为了缓解此问题,Dotfuscator 除了注入检查之外,还可以在程序集上执行重命名混淆。有关详细信息,请参阅“PreEmptive Protection – Dotfuscator Community Edition”文档 (bit.ly/2y9oxYX)。

总结

本文介绍了运行时检查及其解决的一些问题。使用 LOB 应用程序,我演示了数据泄露如何在客户端软件中发生,以及如何配置运行时检查以检测、报告和响应此类泄露。

虽然本文介绍的是免费的 Dotfuscator Community Edition,但是同样的概念也可以转移到商用许可的 Professional Edition,它具有用于检查的附加功能 (bit.ly/2xgEZcs)。你也可以使用 Dotfuscator 的同级产品 PreEmptive Protection - DashO (bit.ly/2ffHTrN) 将检查注入到 Java 和 Android 应用程序中。


Joe Sewell* 是 PreEmptive Solutions 的 Dotfuscator 团队软件工程师和技术撰稿人。*

衷心感谢以下 Microsoft 技术专家对本文的审阅:Derick Campbell


在 MSDN MSDN