为 C# 提供一个简单的污点检查解决方案






4.91/5 (13投票s)
我们提出了一种通过模拟污点检查来保护 C# 程序的方法
引言
在本文中,我们提出了一种通过强制验证来自外部世界的潜在危险数据来保护 C# 程序的方法。该方法采用一种简单的、类似 Ruby 的解决方案,它允许开发人员通过将 C# 对象封装在一个通用的容器类中来“污点化”该对象。除非首先在容器上调用“清除污点”方法,否则不允许访问目标对象,即在对象被视为在易受攻击的环境中使用是安全的之前。定义允许清除对象的条件由软件工程实践者自行决定,并且如果数据对 C# 程序的多个部分都构成威胁,则可以重复此操作。
来自外部世界的数据被视为有害
接受应用程序的数据输入(或简单地使用应用程序外部的数据)是一项危险的操作,因为攻击者可以利用不当处理来利用或渗透系统。例如,C 和 C++ 程序容易受到栈溢出攻击,这种攻击利用了不安全的数组边界函数:如果没有数组边界检查,将相对容易地向数组发送比其预期(和设计)能够处理的数据更多的数据,这可能导致执行攻击者提供的任意代码。
使用 SQL 数据库进行数据持久化的应用程序面临着一个不同但可能具有破坏性的问题:SQL 注入攻击。SQL 注入是指攻击者在程序预期并需要来自用户的输入(例如用户名)的地方,包含一个 SQL 语句而不是期望的输入,或者作为其一部分。在这种情况下,此输入很可能被用作一个 string
来完成一个预定义的(合法的)SQL 语句,该语句将从数据库中检索用户。这种可能性提出了两个问题。首先,必须根据其来源将来自不可信源的数据识别为潜在威胁。其次,将不可信数据清除以使其安全必须从使用该数据的代码的角度进行,即只有需要不可信数据的代码才能知道其自身的弱点以及是否可以安全地使用该数据。
SQL 注入的一个例子
本文绝不旨在提供对 SQL 注入的全面描述。本节仅作为对该威胁的介绍,以便理解我们的解决方案如何工作,因此如果读者熟悉前者,可以跳过。SQL 注入可以通过一个(经典的)例子来轻松解释,我们将在下面展示。它展示了一个典型的 C# 数据库代理,它是身份验证例程的一部分,该例程使用应用程序用户提供的用户名作为 Users
表上的鉴别符的一部分。
public User GetUserByUsername(string typedUsername)
{
string strSQL = @"SELECT *
FROM Users
WHERE username = '" + typedUsername + "'";
// ^ ^
// Single quotes around inputed string
...
}
SQL 语句显然期望一个 string
,其中包含用户名。如果是这种情况,则会选择预期的用户,例程将按预期运行。然而,如果攻击者怀疑正在使用 SQL 数据库,她可以提供操纵查询的输入,以返回没有行被选中的值。一种经典的方法是添加一个条件,该条件将始终返回 true
,例如
' OR '1'='1' -- '
到初始查询中,以便返回 User
表中的所有行,这可能导致返回一个格式正确的 User
实例,具体取决于方法的其余部分如何设计。正如我们在下面的代码中看到的,string
将被附加到查询中,从而形成一个有效的 SQL 语句。最终的 SQL 语句将如何被解释也在此处显示。
public User GetUserByUsername(string typedUsername)
{
SELECT *
FROM Users
WHERE username = '" + 'OR '1'='1' -- ' + "'";
// ^ ^
// Single quotes around inputed string
...
}
SELECT *
FROM Users
WHERE username = ''
OR '1'='1'
-- ''
SQL 注入肯定不限于收集关于数据库架构的信息,或者未经授权访问系统。还可以使用数据操作语言(DML)指令来更新和/或删除表中的行,甚至删除表。此外,应该注意的是,注入攻击绝不限于 SQL;每当 string
被用作系统调用的一部分时,也可能发生注入攻击。
将对象标记为潜在威胁
处理输入数据的关键方面是根据数据的来源知道数据是否可信。
文献显示了不同的解决方案来跟踪对象的安全状态。Ruby 和 Perl 等语言实现了**污点检查**,这是一种优雅地跟踪可以放置在对象上的信任级别的方法。污点检查通过将来自不可信源的对象标记为**受污点**来强制执行。该状态可以传递给触及它的另一个对象。为了在不安全的执行环境中使��它,必须首先分析受污点的对象,以确保它不会构成威胁,然后由开发人员明确标记为已清除。
在 Ruby 中,污点检查与 Ruby 程序运行的 SAFE 模式级别密切相关,0 为最宽松,4 为最偏执。解释每个级别的细节超出了本文的范围,但 0 以上的每个级别都强制对外部提供的数据进行显式污点检查 [1]。可以通过 Object
超类的 tainted?
方法来检查对象是否被视为受污点。当对象被视为受污点时,Ruby 脚本禁止执行某些操作,具体取决于 SAFE 级别。
Ruby 使清除受污点的对象变得容易。除非 SAFE 模式设置为最高级别,否则可以通过调用对象上的 untaint
方法来清除任何对象,该方法不接受参数。Ruby 在调用此方法之前不强制进行任何初步检查,这取决于开发人员的判断。
Perl 提供了一个相对简单的机制来强制执行污点检查,称为污点模式。在某些情况下,例如当 Perl 程序打开用户未拥有的文件时,会自动进入此模式 [2]。通过在启动 Perl 解释器时在命令行提供 -T
参数,也可以显式进入污点模式。当进入污点模式时,Perl 解释器将在此模式下运行脚本的其余部分(同上)。当污点模式打开时,使用受污点的数据以可能危险的方式将触发“不安全依赖项”(致命)错误消息。例如,危险操作可能是写入一个文件名在受污点变量中的文件,或者更糟的是,将变量的内容作为系统调用执行。
与 Ruby 不同,Perl 没有提供显式的“清除污点”方法。通过在受污点数据上评估正则表达式来执行清除污点。结果匹配组将被视为未受污点。
我们的解决方案
最近,我们在一个 C# 程序中负责强制执行身份验证例程的安全性,但令人失望地发现 .NET 和 C# 似乎没有类似的机制。
因此,我们提出在 C# 中通过使用一个通用的 Tainted 容器类来模拟部分污点检查解决方案,该类封装了一个目标对象。该类提供了检查目标状态(是否受污点)、清除污点和再次污点化的方法。Tainted
类如下所示
public class Tainted<T>
{
private bool _tainted;
private T _target;
public delegate bool IsCleanUntaintTreatmentMethod(T taintedObject);
public Tainted(T target)
{
_tainted = true;
_target = target;
}
public bool IsTainted
{
get { return _tainted; }
}
public bool IsClean
{
get { return !this.IsTainted; }
}
public T Target
{
get
{
if(this.IsTainted)
{
throw new TaintException();
}
return _target;
}
}
public void Taint()
{
_tainted = true;
}
public void Untaint(IsCleanUntaintTreatmentMethod treatmentMethod)
{
_tainted = !treatmentMethod(_target);
}
}
该类的基本思想是,每当从程序外部获取数据时,存储该数据的对象必须被封装(之后称为目标)到 Tainted
类的实例中。此时,目标被视为受污点。只有通过 public
Target
getter 属性才能访问它。如果目标已被清除污点,getter 将返回它,否则仍被视为不安全。在这种情况下,为了防止需要目标的代码暴露于目标所代表的威胁,getter 将引发 TaintException
而不返回目标。
此条件是此解决方案的关键要素。在可以自由访问目标之前,必须先**清除**它。这似乎有些矛盾,因为为了分析目标而必须访问它。因此,想法是将受污点的目标仅提供给一个旨在验证它的方法。该方法(之后称为**清除污点器**)作为参数传递给 untaint
方法。该清除污点器的签名必须与 IsCleanUntaintTreatmentMethod
委托的签名匹配;它接收目标作为参数,如果目标是安全的(并且必须声明为未受污点),则返回 true
,否则返回 false
。
然后必须开发清除污点器,其中包含两个方法。第一个方法 IsFreeOfSQLInjectionUntainter
接收目标 string
,如果 string
不包含任何通常用于 SQL 注入攻击的 string
,则返回 true
。我们在一个网站 [3] 上找到了这些 SQL 关键字和字符。第二个方法只返回 true
,用于不需要实际验证目标 string
的情况,例如在数据被散列后再使用。我们可以看看下面的代码
public static class StringUntainter
{
private static string [] TabBadStrings = new string
{ "select", "drop", ";", "--", "insert", "delete", "xp_", "%", "&",
"'", "(", ")", "/", "\\", ":", ";", "<", ">", "=", "[", "]", "?",
"`", "|" };
public static string IsFreeOfSQLInjectionUntainter(string target)
{
string taintedStringLower = target.ToLower();
return !TabBadStrings.Any( s => taintedStringLower.Contains(s) );
}
public static string NOPUntainter(string target)
{
return true;
}
}
现在我们可以将以上内容放在一个示例中。下面显示的 SignIn
方法接收两个受污点的 string
:用户名和密码。我们将用户名提供给 User
数据库代理,该代理又将 StringUntainter
类的 IsFreeOfSQLInjectionUntainter
作为参数传递给 taintedUsername
参数的 Untaint
方法。然后,代理确保对象不再被标记为受污点,否则会引发 SQLInjectionException
,SignIn
方法知道如何处理。
一旦用户名被清除污点,就可以在 SQL 语句中使用该值从数据库中检索 User
。在我们的示例中,如果找到了用户名并检索到了用户,然后我们将受污点的密码提供给 HashPasswordForSignIn
方法。由于该方法对受污点的 string
使用哈希算法,因此它不易受到 SQL 注入攻击,并且不需要进一步分析。因此,我们使用 StringUntainter
类的 NOPUntainter
,它清除密码,然后对后者进行哈希。
public class Authentication
{
public bool SignIn(Tainted<string> taintedUsername, Tainted<string> taintedPassword)
{
bool authenticationSuceeded = false;
try
{
User existingUser =
UserBroker.getInstance().GetUserByUsername(taintedUsername);
if(existingUser != null)
{
if(existingUser.HashedPassword.equals
(this.HashPasswordForSignIn(taintedPassword)))
{
authenticationSuceeded = true;
}
else
{
...
}
}
else
{
...
}
}
catch (SQLInjectionException e)
{
...
}
return authenticationSuceeded;
}
public string HashPasswordForSignIn(Tainted<string> taintedPassword)
{
// Since the password string will be hashed, it poses no threat of SQL Injection.
// We just use a "No-check" untainter and then hash the target.
taintedPassword.Untaint( new Tainted<string>.IsCleanUntaintTreatmentMethod
( StringUntainter.NOPUntainter ) );
return MyHasher.Hash(taintedPassword.Target);
}
}
public class UserBroker
{
...
public User GetUserByUsername(Tainted<string> taintedUsername)
{
taintedUsername.Untaint( new Tainted<string>.IsCleanUntaintTreatmentMethod
( StringUntainter.IsFreeOfSQLInjectionUntainter ) );
if(taintedUsername.IsTainted)
{
throw new SQLInjectionException();
}
return this.GetUserByUsername(taintedUsername.Target);
}
private User GetUserByUsername(string username)
{
...
}
}
关注点
在本文中,我们提出了一个在 C# 中使用污点检查的简单解决方案。我们的解决方案将对象的状态与清理算法的实现分离,从而得到了一个通用的 Tainted
类。该类可以与任何对象一起使用,因此非常可重用,并且肯定不限于基本类型,还可以用于复杂对象(如 File
对象)。所使用的委托方法为清除污点算法的实现提供了类型安全性,这是一个 Strategy
设计模式的有趣例子。然后可以从承担风险的代码的角度进行清除污点;在我们的例子中,数据库代理“知道”什么可能对自己构成威胁,并确保受污点的用户名不会。尽管我们没有说明,但是一旦清除污点的受污点 string
,可以通过调用 Taint
方法再次将其污点化,前提是该对象稍后被其他敏感例程使用。还应该注意的是,使清除污点靠近使用敏感数据的代码会增加一致性。
我们可以选择一种更健壮的清除污点方法,通过消除清除对象污点的可能性来模仿 Perl,并通过将 Target
属性转换为一个接受委托并返回目标对象(如果委托返回 true
)的方法来修改它。即使这种方法可以降低将已清除污点的对象提供给敏感代码段或模块的风险,但它也会因为每次访问目标时都执行清理算法(如果访问代码的实现方式是天真的)而降低性能。此外,如果使用了 NOP 清理程序,它不会提供比我们的解决方案更额外的保护。
我们的解决方案虽然有用,但肯定不如 .NET 原生集成污点检查那样安全。开发人员必须知道数据的来源,并手动将其封装到 Tainted
类的实例中,而不是由语言本身强制执行此操作。
我们的身份验证示例也是故意简化的,以便于理解。它本身是不安全的,因为用户名和密码以明文形式在网络上传输。更现代的方法会哈希或加密这些信息,以便它们可以安全地通过公共或不安全网络传输。
与污点检查相对立的方法称为商标化 [4, p.18]。商标化包括通过维护一个对象列表来显式地白名单数据,这些对象已被 ApplyTrademark
方法商标化(被视为安全)[5]。希望使用商标化代码的敏感代码必须通过 VerifyTrademark
方法确保对象已被商标化,该方法在对象安全可用时返回 true
(同上)。这种方法的缺点是,它使得确保一个被视为安全的对象在特定上下文中确实安全使用变得更加困难,如果数据在一个模块中被商标化并在另一个模块中使用,可能会出现问题。我们可以想象最初在一个模块中被认为是安全并在 SQL 数据库上下文中使用的。然后,该数据被提供给一个模块,该模块将其用作系统调用的一部分。在这种情况下,即使对象已被商标化,也不能保证它对第二个模块是安全的。然而,在我们的方法中,对象在被第一个模块清除污点后,可以在提供给第二个模块之前显式地对其进行污点化,这将消除误报的风险。
历史
- 2011/03/14 首次发布