安全性测验

来源:互联网 发布:淘宝差评申诉入口 编辑:程序博客网 时间:2024/04/29 03:38
                                              测试您的安全性 IQ
                                                                                                                                                                Michael Howard 和 Bryan Sullivan

我们都喜欢通过复查代码来检查安全性错误。甚至可以说,我们对此非常擅长。我们并不是在自夸我们是最好的,但我们通常都可以迅速地找出大量错误。你能做到吗?
如果看到一个安全性错误,您能否识别出来?通过进行这一测验来评估一下。每个代码示例都至少有一个安全漏洞。尝试找出错误并看一看您的得分。在代码后面是对这些漏洞、注释的总结,如果条件允许,还会讲述安全性开发生命周期 (SDL) 如何帮助查找这些错误。感谢 Peter Torr 和 Eric Lippert 提供输入和代码示例。
 
错误 #1(C 或 C++)
  1. void func(char *s1, char *s2) {
  2.   char d[32];
  3.   strncpy(d,s1,sizeof d - 1);
  4.   strncat(d,s2,sizeof d - 1);
  5.   ...
  6. }
答案
:我们认为应该先来讲一讲虽然古老但却非常便于理解的缓冲区溢出。对许多人而言此代码已足够安全,因为代码使用的是受限的 strncpy 和 strncat 函数。但是,只有当缓冲区大小合适时,这些函数才是安全的,而在本例中缓冲区大小是错误的。彻头彻尾的错误。
从技术上讲,第一个调用是安全的,但第二个调用却是错误的。strncpy 和 strncat 函数的最后一个参数是缓冲区中保留的空间量,而您刚刚通过调用 strncpy 占用了其中的一部分或全部空间。缓冲区溢出。Michael 在 2004 年发表了一篇博客文章,其中讲述了与此完全相同的错误类型
在 Visual C++ 2005 及以后的版本中,警告 C4996 是告诉您应将错误的函数调用替换为更安全的调用,而 /analyze 选项会发出 C6053 警告,指出 strncat 可能不会以零终止字符串。
老实说,由于种种原因,strncpy 和 strncat(及其 "n" 同类)要比 strcpy 和 strcat(及其同类)更糟糕。首先,返回值有点多余 — 它是指向缓冲区的指针,而缓冲区可能有效,也可能无效。您没有办法知道!其次,获取正确的目标缓冲区大小的确很难。如果您找出了错误,可以给自己加一分。

错误 #2(C 或 C++)
  1. int main(int argc, char* argv[]) {
  2.   char d[32];
  3.   if (argc==2) 
  4.     strcpy(d,argv[1]);
  5.   ...
  6.   return 0;
  7. }
答案
:这是一个很棘手的错误。过去我们曾多次看到此错误被用作缓冲区溢出的示例,而大多数情况下根本无法确定代码中是否存在安全性错误。这完全取决于代码的使用方式。
如果这是标准的 Win32 EXE,则此处不存在安全性错误,因为可能发生的最糟糕后果就是攻击您自己并自行运行代码,但这并不是安全性错误。
现在,如果此代码位于以 SYSTEM 权限运行的 ServiceMain Windows 服务中,或者用作 Linux 应用程序 setuid 根的主函数,则此代码将变为不折不扣的安全性错误。
让我们假设此代码被用作标记为 setuid 根的 Linux 应用程序。当应用程序由普通用户启动时,应用程序实际上以根身份运行,这意味着这是一个本地权限提升漏洞。
如“错误 #1”中给出的代码示例所示,调用 strcpy 时会发出 C4996 警告,/analyze 将发出 C6204,指示存在潜在的缓冲区溢出。如果您回答“喔!我需要更多的上下文”,那么给自己加两分;否则不得分。

错误 #3(可以是任何语言,示例为 C#)
  1. byte[] GetKey(UInt32 keySize) {
  2.   byte[] key = null;
  3.   try {
  4.     key = new byte[keySize];
  5.     RNGCryptoServiceProvider.Create().GetBytes(key);
  6.   }
  7.   catch (Exception e) {
  8.     Random r = new Random();
  9.     r.NextBytes(key);
  10.   }
  11.   return key;
  12. }
答案
:在这个糟糕的密钥生成代码中有两个错误。第一个非常明显:如果在调用从密码学角度上来说非常强大的随机数字生成器时失败,则代码会捕获此异常,然后调用根本没有任何用处而且可预测的随机生成器。如果您已发现这一点,就给自己加一分。SDL 要求使用在密码学角度上随机的数字来生成密钥。
但还有一个错误:代码会捕获所有异常。除了极少数情况外,捕获所有异常(无论是 C++ 异常、Microsoft .NET Framework 异常,还是 Windows 中的结构化异常处理)会掩盖真正的错误。因此不要这样做。
在使用 /analyze 进行编译时,C 或 C++ 中捕获所有异常(包括缓冲区溢出等访问保护错误)的结构化异常处理程序将产生一个 C6320 警告。这种设计使攻击者能够反复发起对动画光标错误 MS07-017 的攻击。如果您找出了异常处理错误,就给自己再加一分。

错误 #4
  1. void func(const char *s) {
  2.   if (!s) return;
  3.   char t[3];
  4.   memcpy(t,s,3);
  5.   t[3] = 0;
  6.   ...
  7. }
答案:几年前当 Windows Vista 还处在开发过程中的时候,我们曾在其中发现了一个类似错误。但这是安全性错误吗?很明显,它是编码错误,因为代码将写入第四个数组元素,但数组只有三个元素长。请记住,数组是从零开始,而不是一。我坚持认为这不是安全性错误,因为攻击者没有获取任何控制权限。
但是,如果该错误看上去像下面的示例那样,攻击者控制了 i,那么就意味着攻击者可以在内存中的任意位置写入零。这时它就成了真正的安全性错误:
  1. void func(const char *s, int i) {
  2.   if (!s) return;
  3.   char t[3];
  4.   memcpy(t,s,3);
  5.   t[i] = 0;
  6.   ...
  7. }
使用 /analyze 进行编译时,此代码将生成 C6201“超出有效索引范围”警告。如果您说“不是安全性错误”,就给自己加一分。

错误 #5
  1. public class Barrel {
  2.   // By default, a barrel contains one rhesus monkey.  
  3.   private static Monkey[] defaultMonkeys = 
  4.     new[] { new RhesusMonkey() };
  5.   // backing store for property.
  6.   private IEnumerable<Monkey> monkeys = null
  7.   public IEnumerable<Monkey> Monkeys {
  8.     get {
  9.       if (monkeys == null) {
  10.         if (MonkeysReady())
  11.           monkeys = PopulateMonkeys();
  12.         else
  13.           monkeys = defaultMonkeys;
  14.       }
  15.       return monkeys;
  16.     }
  17.   }
  18. }
答案:这是一个难题。此类作者认为它们是安全高效的。其后备存储是保密的,属性为只读,属性类型为 IEnumerable<T>,因此调用方无法执行任何操作,只能读取 Barrel 的状态。
作者忘记了心怀叵测的调用方可能会尝试将属性的返回值转换为 Monkey[]。如果有两个 Barrel,每个都有默认的 Monkey 列表,那么拥有其中一个 Barrel 的恶意调用方则可以使用其他任何 Monkey(或 null)来替换静态默认列表中的 RhesusMonkey,从而实际改变另一个 Barrel 的状态。
此处的解决方案是缓存 ReadOnlyCollection<T> 或其他某个真正的只读存储,以保护底层数组免受调用方恶意或意外的转换。如果您抓住了这一点,就给自己加两分。

错误 #6 (C#)
  1. protected void Page_Load(object sender, EventArgs e) {
  2.   string lastLogin = Request["LastLogin"];
  3.   if (String.IsNullOrEmpty(lastLogin)) {
  4.     HttpCookie lastLoginCookie = new HttpCookie("LastLogin",
  5.       DateTime.Now.ToShortDateString());
  6.     lastLoginCookie.Expires = DateTime.Now.AddYears(1);
  7.     Response.Cookies.Add(lastLoginCookie);
  8.   }
  9.   else {
  10.     Response.Write("Welcome back! You last logged in on " + lastLogin);
  11.     Response.Cookies["LastLogin"].Value = 
  12.       DateTime.Now.ToShortDateString();
  13.   }
  14. }
答案
:这是一个非常简单的、跨站点的脚本编写漏洞,也是 Web 上最常见的漏洞。尽管代码似乎暗示 lastLogin 值始终来自 cookie,但实际上 HttpRequest.Item 属性更倾向于使用来自查询字符串的值,而不是来自 cookie 的值。
换句话说,无论 lastLogin cookie 被设置为何值,只要攻击者将成对的名称/值 lastLogin=<script>alert('0wned!')</script> 添加到查询字符串中,应用程序就会为 lastLogin 变量的值选择恶意脚本输入。如果您的回答是 XSS,则给自己加一分。

错误 #7 (C#)
  1. private decimal? lookupPrice(XmlDocument doc) {
  2.   XmlNode node = doc.SelectSingleNode(
  3.     @"//products/product[id/text()='" + 
  4.     Request["itemId"] + "']/price");
  5.   if (node == null)
  6.     return null;
  7.   else
  8.     return (Convert.ToDecimal(node.InnerText));
  9. }
答案
:如果您说是 XPath 注入,则给自己加一分。XPath 注入的工作原理与其同类但却更为著名(也可以说是臭名昭著)的 SQL 注入完全一样。此代码创建了一个将 XPath 代码和未经验证的保留用户输入合并在一起的查询,因此很容易受到注入攻击。任何应用程序如果它所处理的文本随后将被用于执行某种形式的操作,则它们都面临注入攻击的威胁。

错误 #8 (C#)
  1. public class CustomSessionIDManager : System.Web.Session    State.SessionIDManager
  2. {
  3.     private static object lockObject = new object();
  4.     public override string CreateSessionID(HttpContext context)
  5.     {
  6.         lock (lockObject)
  7.         {
  8.             Int32? lastSessionId = (int?)context.Application                ["LastSessionId"];
  9.             if (lastSessionId == null)
  10.                 lastSessionId = 1;
  11.             else
  12.                 lastSessionId++;
  13.             context.Application["LastSessionId"] = lastSessionId;
  14.             return lastSessionId.ToString();
  15.         }
  16.     }
  17. }
答案
:这里有两个主要问题。虽然此代码可以正确地对应用程序逻辑加一道锁,从而确保两个线程不会同时创建相同的会话 ID,但它仍然不能在服务器场中进行安全部署。HttpContext.Application 对象所引用的应用程序状态并不在各个服务器之间共享。如果此应用程序已在服务器场中部署,则可能会导致会话 ID 冲突。如果您捕获了此错误,则给自己加一分。
另一个严重的问题在于可以轻松猜出此类生成的会话 ID 是一个连续整数。如果某用户看到了他的会话令牌并注意到其会话 ID 为 100,则该用户可以使用简单的浏览器实用程序将会话 ID 改为 99 或 98 或其他任何更小的值,从而拦截这些用户的会话。
在这种情况下,更适合开发人员的方案是使用 GUID 或其他较大的、随机的字符串组合字母和数字。如果您意识到有序整数对会话 ID 令牌而言是糟糕的选择,则您获得一分。

错误 #9 (C#)
  1. bool login(string username, 
  2.            string password, 
  3.            SqlConnection connection, 
  4.            out string errorMessage) {
  5.   SqlCommand selectUserAndPassword = new SqlCommand(
  6.     "SELECT Password FROM UserAccount WHERE Username = @username",
  7.     connection);
  8.   selectUserAndPassword.Parameters.Add(
  9.     new SqlParameter("@username",  username));
  10.   string validPassword = 
  11.     (string)selectUserAndPassword.ExecuteScalar();
  12.   if (validPassword == null) {
  13.     // the user doesn't exist in the database
  14.     errorMessage = "Invalid user name";
  15.     return false;
  16.   }
  17.   else if (validPassword != password) {
  18.     // the given password doesn't match 
  19.     errorMessage = "Incorrect password";
  20.     return false;
  21.   }
  22.   else {
  23.     // success
  24.     errorMessage = String.Empty;
  25.     return true;
  26.   }
  27. }
答案
:此处最大的问题在于,当登录失败时,应用程序向用户返回的信息过多。虽然对用户来说,弄清到底是输错了密码还是完全忘记了用户名无疑很有帮助,但此信息也帮助了那些试图对应用程序发起强力攻击的攻击者。尽管听起来与常理相悖,但在此情况下还是不要提供帮助。如果登录失败则显示“用户名或密码无效”等消息,而不是“用户名无效”和“密码无效”。
如果您发现了这一点,则加一分。如果您还记得应用程序不应在数据库中存储纯文本密码,再奖励您一分;在这种情况下应存储和比较经过处理的密码哈希值。
那么该如何评定结果呢?
分数评论
15+我们的理想人选。
11-14
还不错吧,应该说很不错。现在将您的才干应用到您的搭档编写的所有代码中。
7-10
嗯。还不错。您无疑还有一些方面需要学习,但是要比现在编写 Web 应用程序的其中 50% 的开发人员强一些。
4-6
您有很多方面需要学习。请到您喜欢的书店购买本文两位作者撰写的全部书籍。

0-3退出编辑器和编译器,以免任何人受到伤害。

错误 #10 (Silverlight CLR C#)
  1. bool verifyCode(string discountCode) {
  2.   // We store the hash of the secret code instead of 
  3.   // the plaintext of the secret code.
  4.   // Hash the incoming value and compare it against 
  5.   // the stored hash.
  6.   SHA1Managed hashFunction = new SHA1Managed();
  7.   byte[] codeHash = 
  8.     hashFunction.ComputeHash(
  9.       System.Text.Encoding.Unicode.GetBytes(discountCode));
  10.   byte[] secretCode = new byte[] { 
  11.     116, 46, 130, 122, 36, 234, 158, 125, 163, 122,
  12.     157, 186, 64, 142, 51, 153, 113, 79, 1, 42 };
  13.   if (codeHash.Length != secretCode.Length) {
  14.     // The hash lengths don't match, so the strings don't
  15.     // match this should never happen, but we check anyway
  16.     return false;
  17.   }
  18.   // perform an element-by-element comparison of the arrays
  19.   for (int i = 0; i < codeHash.Length; i++) {
  20.     if (codeHash[i] != secretCode[i])
  21.       return false// the hashes don't match
  22.   }
  23.   // all the elements match, so the strings match
  24.   // the discount code is valid, inform the server
  25.   WebServiceSoapClient client = new WebServiceSoapClient();
  26.   client.ApplyDiscountCode();
  27.   return true;
  28. }
答案:开发人员做出了一个明智决定,不在代码中嵌入纯文本形式的加密代码。如果您只需测试用户是否知道机密内容(如折扣代码或密码),存储该机密内容的哈希值并比较哈希值肯定要比存储纯文本并直接比较字符串要好一些。遗憾的是,开发人员选择了使用 SHA-1 哈希算法,它暴露出了严重的问题,之后被 SDL 禁止。更好的选择是使用 SHA256Managed 类,它可以实现经 SDL 批准和推荐的 SHA-256 哈希算法。如果您发现了这一点,则加一分。
比选择 SHA-1 而不是 SHA-256 还要糟糕的是开发人员忽略了对哈希值进行处理。未经处理的哈希值非常容易通过预先计算的哈希表(通常称为彩虹表)破解。攻击者可能在很短的时间内便可从未经处理的哈希值判断出原始的纯文本加密代码。(作者将在 SDL 博客中发表贺词,以感谢第一个使用纯文本加密代码响应我们号召的人。)如果您捕获了未经处理的哈希值,则给自己加一分。
但是,此代码存在的最大问题在于它竟然在客户端计算机上执行!要记得我们开头所说的,是 Silverlight CLR 代码在用户的浏览器中运行。在客户端上运行的任何代码都可能被攻击者所操控。无需多言。对于铁了心的用户,没有什么能阻止他在运行 Silverlight 代码的浏览器实例中附加调试程序并在代码执行时单步调试它。
他可以将 codeHash 变量设置为等于 secretCode 哈希值,这样一来比较逻辑始终都会成功。或者他可以完全略过验证逻辑,直接跳到应用折扣代码的 Web 服务调用的当前指令处。最简单的方式是,他可以完全避开调试程序,只需直接调用 Web 服务方法 ApplyDiscountCode!
必须要知道,即使您可以使用 C# 或 Visual Basic 来创建 Silverlight 应用程序(就像您处理 ASP.NET Web 窗体那样),Silverlight 代码也是在客户端计算机上运行,而 Web 窗体代码是在服务器上运行。在客户端上运行的代码可能会被攻击者查看和篡改。绝不要将机密内容嵌入到客户端代码中,或允许客户端代码执行特权决策(诸如折扣代码是否有效,或是否授权用户执行某个操作)。如果您捕获了此错误,则给自己加一分。