测试您的安全性 IQ
Michael Howard 和 Bryan Sullivan
![](http://i.msdn.microsoft.com/Global/Images/clear.gif)
我们都喜欢通过复查代码来检查安全性错误。甚至可以说,我们对此非常擅长。我们并不是在自夸我们是最好的,但我们通常都可以迅速地找出大量错误。你能做到吗?
如果看到一个安全性错误,您能否识别出来?通过进行这一测验来评估一下。每个代码示例都至少有一个安全漏洞。尝试找出错误并看一看您的得分。在代码后面是对这些漏洞、注释的总结,如果条件允许,还会讲述安全性开发生命周期 (SDL) 如何帮助查找这些错误。感谢 Peter Torr 和 Eric Lippert 提供输入和代码示例。
错误 #1(C 或 C++)
- void func(char *s1, char *s2) {
- char d[32];
- strncpy(d,s1,sizeof d - 1);
- strncat(d,s2,sizeof d - 1);
- ...
- }
从技术上讲,第一个调用是安全的,但第二个调用却是错误的。strncpy 和 strncat 函数的最后一个参数是缓冲区中保留的空间量,而您刚刚通过调用 strncpy 占用了其中的一部分或全部空间。缓冲区溢出。Michael 在 2004 年发表了一篇博客文章,其中讲述了与此完全相同的
错误类型。
在 Visual C++ 2005 及以后的版本中,警告 C4996 是告诉您应将错误的函数调用替换为更安全的调用,而 /analyze 选项会发出 C6053 警告,指出 strncat 可能不会以零终止字符串。
老实说,由于种种原因,strncpy 和 strncat(及其 "n" 同类)要比 strcpy 和 strcat(及其同类)更糟糕。首先,返回值有点多余 — 它是指向缓冲区的指针,而缓冲区可能有效,也可能无效。您没有办法知道!其次,获取正确的目标缓冲区大小的确很难。如果您找出了错误,可以给自己加一分。
错误 #2(C 或 C++)
- int main(int argc, char* argv[]) {
- char d[32];
- if (argc==2)
- strcpy(d,argv[1]);
- ...
- return 0;
- }
如果这是标准的 Win32 EXE,则此处不存在安全性错误,因为可能发生的最糟糕后果就是攻击您自己并自行运行代码,但这并不是安全性错误。
现在,如果此代码位于以 SYSTEM 权限运行的 ServiceMain Windows 服务中,或者用作 Linux 应用程序 setuid 根的主函数,则此代码将变为不折不扣的安全性错误。
让我们假设此代码被用作标记为 setuid 根的 Linux 应用程序。当应用程序由普通用户启动时,应用程序实际上以根身份运行,这意味着这是一个本地权限提升漏洞。
如“错误 #1”中给出的代码示例所示,调用 strcpy 时会发出 C4996 警告,/analyze 将发出 C6204,指示存在潜在的缓冲区溢出。如果您回答“喔!我需要更多的上下文”,那么给自己加两分;否则不得分。
错误 #3(可以是任何语言,示例为 C#)
- byte[] GetKey(UInt32 keySize) {
- byte[] key = null;
- try {
- key = new byte[keySize];
- RNGCryptoServiceProvider.Create().GetBytes(key);
- }
- catch (Exception e) {
- Random r = new Random();
- r.NextBytes(key);
- }
- return key;
- }
但还有一个错误:代码会捕获所有异常。除了极少数情况外,捕获所有异常(无论是 C++ 异常、Microsoft .NET Framework 异常,还是 Windows 中的结构化异常处理)会掩盖真正的错误。因此不要这样做。
在使用 /analyze 进行编译时,C 或 C++ 中捕获所有异常(包括缓冲区溢出等访问保护错误)的结构化异常处理程序将产生一个 C6320 警告。这种设计使攻击者能够反复发起对
动画光标错误 MS07-017 的攻击。如果您找出了异常处理错误,就给自己再加一分。
错误 #4
- void func(const char *s) {
- if (!s) return;
- char t[3];
- memcpy(t,s,3);
- t[3] = 0;
- ...
- }
但是,如果该错误看上去像下面的示例那样,攻击者控制了 i,那么就意味着攻击者可以在内存中的任意位置写入零。这时它就成了真正的安全性错误:
- void func(const char *s, int i) {
- if (!s) return;
- char t[3];
- memcpy(t,s,3);
- t[i] = 0;
- ...
- }
错误 #5
- public class Barrel {
- // By default, a barrel contains one rhesus monkey.
- private static Monkey[] defaultMonkeys =
- new[] { new RhesusMonkey() };
- // backing store for property.
- private IEnumerable<Monkey> monkeys = null;
- public IEnumerable<Monkey> Monkeys {
- get {
- if (monkeys == null) {
- if (MonkeysReady())
- monkeys = PopulateMonkeys();
- else
- monkeys = defaultMonkeys;
- }
- return monkeys;
- }
- }
- }
答案:这是一个难题。此类作者认为它们是安全高效的。其后备存储是保密的,属性为只读,属性类型为 IEnumerable<T>,因此调用方无法执行任何操作,只能读取 Barrel 的状态。
作者忘记了心怀叵测的调用方可能会尝试将属性的返回值转换为 Monkey[]。如果有两个 Barrel,每个都有默认的 Monkey 列表,那么拥有其中一个 Barrel 的恶意调用方则可以使用其他任何 Monkey(或 null)来替换静态默认列表中的 RhesusMonkey,从而实际改变另一个 Barrel 的状态。
此处的解决方案是缓存 ReadOnlyCollection<T> 或其他某个真正的只读存储,以保护底层数组免受调用方恶意或意外的转换。如果您抓住了这一点,就给自己加两分。
错误 #6 (C#)
- protected void Page_Load(object sender, EventArgs e) {
- string lastLogin = Request["LastLogin"];
- if (String.IsNullOrEmpty(lastLogin)) {
- HttpCookie lastLoginCookie = new HttpCookie("LastLogin",
- DateTime.Now.ToShortDateString());
- lastLoginCookie.Expires = DateTime.Now.AddYears(1);
- Response.Cookies.Add(lastLoginCookie);
- }
- else {
- Response.Write("Welcome back! You last logged in on " + lastLogin);
- Response.Cookies["LastLogin"].Value =
- DateTime.Now.ToShortDateString();
- }
- }
换句话说,无论 lastLogin cookie 被设置为何值,只要攻击者将成对的名称/值 lastLogin=<script>alert('0wned!')</script> 添加到查询字符串中,应用程序就会为 lastLogin 变量的值选择恶意脚本输入。如果您的回答是 XSS,则给自己加一分。
错误 #7 (C#)
- private decimal? lookupPrice(XmlDocument doc) {
- XmlNode node = doc.SelectSingleNode(
- @"//products/product[id/text()='" +
- Request["itemId"] + "']/price");
- if (node == null)
- return null;
- else
- return (Convert.ToDecimal(node.InnerText));
- }
错误 #8 (C#)
- public class CustomSessionIDManager : System.Web.Session State.SessionIDManager
- {
- private static object lockObject = new object();
- public override string CreateSessionID(HttpContext context)
- {
- lock (lockObject)
- {
- Int32? lastSessionId = (int?)context.Application ["LastSessionId"];
- if (lastSessionId == null)
- lastSessionId = 1;
- else
- lastSessionId++;
- context.Application["LastSessionId"] = lastSessionId;
- return lastSessionId.ToString();
- }
- }
- }
另一个严重的问题在于可以轻松猜出此类生成的会话 ID 是一个连续整数。如果某用户看到了他的会话令牌并注意到其会话 ID 为 100,则该用户可以使用简单的浏览器实用程序将会话 ID 改为 99 或 98 或其他任何更小的值,从而拦截这些用户的会话。
在这种情况下,更适合开发人员的方案是使用 GUID 或其他较大的、随机的字符串组合字母和数字。如果您意识到有序整数对会话 ID 令牌而言是糟糕的选择,则您获得一分。
错误 #9 (C#)
- bool login(string username,
- string password,
- SqlConnection connection,
- out string errorMessage) {
- SqlCommand selectUserAndPassword = new SqlCommand(
- "SELECT Password FROM UserAccount WHERE Username = @username",
- connection);
- selectUserAndPassword.Parameters.Add(
- new SqlParameter("@username", username));
- string validPassword =
- (string)selectUserAndPassword.ExecuteScalar();
- if (validPassword == null) {
- // the user doesn't exist in the database
- errorMessage = "Invalid user name";
- return false;
- }
- else if (validPassword != password) {
- // the given password doesn't match
- errorMessage = "Incorrect password";
- return false;
- }
- else {
- // success
- errorMessage = String.Empty;
- return true;
- }
- }
如果您发现了这一点,则加一分。如果您还记得应用程序不应在数据库中存储纯文本密码,再奖励您一分;在这种情况下应存储和比较经过处理的密码哈希值。
错误 #10 (Silverlight CLR C#)
- bool verifyCode(string discountCode) {
- // We store the hash of the secret code instead of
- // the plaintext of the secret code.
- // Hash the incoming value and compare it against
- // the stored hash.
- SHA1Managed hashFunction = new SHA1Managed();
- byte[] codeHash =
- hashFunction.ComputeHash(
- System.Text.Encoding.Unicode.GetBytes(discountCode));
- byte[] secretCode = new byte[] {
- 116, 46, 130, 122, 36, 234, 158, 125, 163, 122,
- 157, 186, 64, 142, 51, 153, 113, 79, 1, 42 };
- if (codeHash.Length != secretCode.Length) {
- // The hash lengths don't match, so the strings don't
- // match this should never happen, but we check anyway
- return false;
- }
- // perform an element-by-element comparison of the arrays
- for (int i = 0; i < codeHash.Length; i++) {
- if (codeHash[i] != secretCode[i])
- return false; // the hashes don't match
- }
- // all the elements match, so the strings match
- // the discount code is valid, inform the server
- WebServiceSoapClient client = new WebServiceSoapClient();
- client.ApplyDiscountCode();
- return true;
- }
答案:开发人员做出了一个明智决定,不在代码中嵌入纯文本形式的加密代码。如果您只需测试用户是否知道机密内容(如折扣代码或密码),存储该机密内容的哈希值并比较哈希值肯定要比存储纯文本并直接比较字符串要好一些。遗憾的是,开发人员选择了使用 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 窗体代码是在服务器上运行。在客户端上运行的代码可能会被攻击者查看和篡改。绝不要将机密内容嵌入到客户端代码中,或允许客户端代码执行特权决策(诸如折扣代码是否有效,或是否授权用户执行某个操作)。如果您捕获了此错误,则给自己加一分。