1. 程式人生 > >真實、可量化的密碼強度以及如何衡量密碼強度

真實、可量化的密碼強度以及如何衡量密碼強度

服務使用者賬戶因為不能被鎖定,所以成為暴力密碼破解攻擊的最好目標。理想情況下,所有的賬戶都應該使用強密碼,但服務賬戶(或者其他不被鎖定的賬戶)還是應該特別注意。

有些讀者可能已經在筆者的網站上看過那篇關於密碼的論文。在那篇文章裡,提出了一種與簡單複雜度規則不同的密碼評價方法。這篇文章收到很多讀者的積極反饋,本人覺得它是對本章內容的很好補充。筆者也會在本書附上相關的原始碼,如果想自己編寫密碼強度檢查工具,可以查考這些原始碼。接下來將介紹這種密碼評價方法,以及這種方法是如何工作的。

管理員總是告訴使用者:金鑰必須足夠複雜(使用者的感覺就是麻煩),但是他們沒有告訴我們如何才能設定出好的或強壯的密碼。筆者見過的一些密碼強度計量工具都是盡最大努力給出評級,糟糕的、好的、更好的或者其他一些模稜兩可的評語,都沒有對密碼強度給出定量結果,只是基於複雜度給出假設——越複雜的密碼越好。雖然這是事實,但是我們仍然得不到任何資訊用來判斷密碼應該多複雜才算好,或者為什麼需要這樣做。

正因為如此,我想做一些不同的事情,所以決定增加可衡量的、並且有價值的屬性,通過這一屬性可以計算出破解密碼的金鑰空間(所有可能用來組成密碼的字元個數)需要多長時間,而密碼是基於使用的基本字元組成的。密碼強度關心的就是如何使密碼更難或不可能被破解,越長和越複雜的密碼,破解需要的時間就越長。就像安全組合鎖,使用的鎖越多就越安全。

在筆者看來,要真正選擇好的密碼,就應該總是假設對攻擊者最有利的情況和對你最不利的情況。當攻擊者暴力破解密碼的時候,所用的方法基本上就是列舉所有可能的字元組合:a–z、A–Z、0–9,以及鍵盤上第一排的那些字元——!、@、#、$、%、^、*、(、)、-、=、_、+,還有其他能列印的可見字元,比如_、{、>,等等。當然,攻擊者使用的方法並不總是類似這樣教科書一般的方法,但這是他們考慮問題的典型思路。如果輸入abcd作為密碼,4個小寫字母,攻擊者不知道密碼的結構,所以必須假設密碼可以是任何情況,這意味著必須選擇使用哪種方法攻擊密碼(方法有很多),以及選擇哪些字元進行密碼組合列舉。如果攻擊者已經知道密碼的組成結構,密碼就更容易破解。攻擊者瞭解的事情越多,對他們減少金鑰空間就越有利。

一種新方法

所有攻擊者要做的事情就是列舉所有可能的字元組合,直到猜到密碼為止。也就是說,從字元a開始迴圈,每迴圈一次,增加一個字元,也許不該多說這一句,因為這看起來簡單得微不足道。無論如何,在整個金鑰空間中猜解由4個小寫英文字母組成的密碼需要迭代475 254次,在網際網路上可以看到,許多暴力破解程式或白皮書都介紹說,對由4個小寫英文字母組成的密碼進行猜解需要迭代456976(也就是264次方)次,但這是錯誤的。只有從一開始就認定密碼必須是4個字母,需要的猜解次數才是264次方。也就是說,列舉密碼是從aaaa開始,而不是從a開始。實際上,在得到aaaa之前必須先經過aaaaaa,所以正確的計算方法是使用26^4+26^3+26^2+26^1

作為計算公式。475254456976這兩個數字看起來差不多,但是對於密碼是10個字母的情況,這兩個數字的差別就是56億。所以從一開始,系統使用的公式就是錯誤的,筆者希望計算從a開始的完整的暴力破解所需要的密碼猜測次數,攻擊者通常也是這樣做的。

為了確定密碼列舉使用的基礎字元,需要觀察密碼並確定密碼使用的最小基礎金鑰空間。如果使用abcd作為密碼,只需要使用a-z作為基礎字元。也就是說,只需要使用從a到z的小寫英文字母嘗試破解密碼。如果使用abcd1作為密碼,就需要使用a-z的字母和0到9的數字(基礎字元增加到36個)。如果使用abcD作為密碼,就需要使用az的小寫英文字母和AZ的大寫英文字母,基礎字元是52個。如果使用abcD1,那麼基礎字元就是62個,增加!或&字元可以使基礎字元增加到76個。如果還使用了[或~字元,那麼最終的基礎字元就是95個。

顯然現在不得不做一些假設,除此之外也沒有其他方法。得到攻擊者必須使用的基礎字元很有意義,再結合得到的輸入密碼的結構資訊,將使之更有意義。但因為現在做的事情是衡量密碼的強度,不希望得到有利的任何東西,所以假設所有情況都對破解者有利。假設破解者不得不做一些假設,比如他知道一些本人知道的東西(聽起來有點古怪)。為此,將基礎字元分成幾個組,分別是a-zA-Z09!=,剩下的其他字元形成一個組。如果輸入A,那麼不得不假設你的密碼可能也使用了小寫字母。由於考慮了小寫字母的情況,因此單獨的A也增加到52種可能性。如果使用a1,那麼09的數字也要加進來,基礎字元就是36(26+10)個。如果是A1,那麼基礎字元是62(52+10)個。單個!也有76種可能性,所以A!的基礎字元就是76個。單個[95種可能性,所以A[的基礎字元就是96個。最後,如果使用Ab44!作為密碼,那麼整個金鑰空間就是76^5+76^4+76^3+76^2+76^1,總共2 569 332 380種密碼組合。作為參考,通常將使用的基礎字元分成以下幾組,每個組都有它們對應的金鑰空間:

Base 10: 0123456789
Base 26: abcdefghijklmnopqrstuvwxyz
Base 36: abcdefghijklmnopqrstuvwxyz 0123456789
Base 52: abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUV
–– WXYZ
Base 62: abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUV
–– WXYZ 0123456789
Base 76: abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUV
–– WXYZ 0123456789 [email protected]#$%^&*()-=_+
Base 95: abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUV
–– WXYZ 0123456789 [email protected]#$%^&*()-=_+ []\"{}j;':,./<>?'_

以上分組只是組織字元集合的方式,分組的根據是筆者見過的不同攻擊者的實現方法,以及人們使用特殊字元的習慣。人們總是趨向於將鍵盤上最上面一排的特殊字元連在一起使用,就像T$i*D3這樣,作為對比的T$i]D3就很少用。黑客手冊通常關注以ASCII順序列舉密碼,從不提針對特殊字元的順序檢查,但此處以普通人建立密碼的方式考慮如何分組基礎字元,儘管是由計算機進行破解。沒有絕對正確的方法,也不存在某種黑客一定會照著做的規則。但是當考慮如何應用密碼策略時,就需要知道這些事情。對此必須重點關注,筆者沒有忘記A-Z和0-9的組合,只是沒有發現所有大寫字母和數字的價值,因為一般很少看到此類密碼(譯者注:全部由大寫字母和數字組成的密碼)。筆者看到的密碼一般都是小寫字母和數字,這是因為預設情況下很多人都使用這樣的密碼。這並不是說密碼不可能是大寫字母和數字的組合,這裡選擇的是包含了62個基礎字元的集合,大寫字母和數字組合的情況與小寫字母和數字組合的情況在數學處理上一樣。所以,只有當真的確定金鑰空間就是大寫字母加數字時,才可以選擇對應的基礎字元集合。

現在可以看看圖1顯示的密碼強度結果(這個應用程式是本人編寫的,包含在本書的下載資料中)。

圖1裁剪過的密碼強度計算程式的執行截圖

根據選擇的基礎字元集合的資訊,決定使用最高級別的工業破解標準(Class F),也就是每秒鐘進行100萬次破解嘗試。這是很大的數字,再次強調一下,總是選擇最糟糕的情況,這樣才能知道密碼到底多強壯。以外還會計算對整個金鑰空間進行密碼測試所需要的時間,並顯示這個時間。在前面的例子中,使用過Ab44!作為密碼,為破解這個密碼,對整個金鑰空間進行的測試耗時為2.56933238秒!這個結果提醒我們,儘管這個密碼被認為是相對複雜的密碼,有大寫字母、小寫字母、數字和特殊字元,但是2.5秒真的不算多。當然,大多數破解者的業餘裝置每秒只能發起幾百上千次嘗試,但是必須確定,這只是此類分析方法展示出來的力量的冰山一角。

不要再告訴使用者使用強壯的密碼(不管這意味著什麼)了,相反,要求他們的密碼必須能夠經受起一定時間長度(由你決定)的Class F攻擊。這種方法使我們有了衡量的基礎。再看看圖1,使用aaaaaaNotGood作為密碼,它由13個大小寫混合的字元組成。使用Class F強度的攻擊破解這個密碼的金鑰空間,需要耗時650 000年,看起來很強壯,對吧?

好吧,這個想法讓人開始思考,從破解者的角度看,這個問題限制了所能看到的事情。現在,650 000年對大家來說是最好的情況(對破解者來說是最糟糕的情況),因為這代表了對整個金鑰空間的列舉。所以這時候,筆者決定編寫演算法來計算破解實際密碼需要的列舉次數,而不是整個金鑰空間。筆者編寫的演算法能正常工作,但速度不是特別快,現在使用Will Fischer想出來的公式,這個公式比較好,在這個問題上,Will給了我很大的幫助,再次感謝。

因為我們已經知道密碼,基本上逐列計算每一列從a開始列舉到這個字母所需要的迭代次數,計算的基礎是定義在記憶體裡的帶索引字串,索引字串從a開始。使用字串可以很容易確定當前是什麼字元,對於密碼中的每個字元,一列一列地處理。最後按列計算出總的列舉次數,再與Class F進行對比,計算出時間。這樣的話,對整個金鑰空間列舉需要650 000年,對aaaaaaNotGood列舉就減少到12 637.66年。減少這麼多時間的原因是直接從6個小寫字母a開始列舉。如果將密碼的第一個字母換成H,需要計算的時間就從12 637.66年(398 541 262 291 912 000 000次列舉組合)變成421 660.40年(13 297 482 476 338 200 000 000次列舉組合)。這就是從a變成H的巨大差異,對整個金鑰空間的列舉也增加到20 724 145 598 800 400 000 000次,使用Class F的破解需要657 158.35年時間。

從實際應用的角度看,應該能夠選擇自己的破解級別(聽起來就像選學校),如果選擇從最糟糕的情況構建密碼強度,選擇每秒鐘10億次列舉,那就真的不用擔心那些來自慢平臺的攻擊。當然,可以看到因選擇自己的破解級別而帶來的價值。

使用這個例子是為了讓你能夠看到來自真實世界的攻擊是什麼樣子,畢竟美國國家安全域性不會試圖獲取每個人的資料。當沒有選擇密碼強度,並且還想知道通常每天要面對多少自攻擊者的風險時,以上示例還是有價值的。舉個例子,可以看看圖2中顯示的來自Cisco產品的截圖。

圖2 開發人員提供的使用者介面可以顯式地限制密碼的長度和複雜度

在上述產品中,必須建立管理使用者,但是注意提示資訊:“你的密碼必須小於8個字元,並且不能包含空格和特殊字元。”在這種情況下,使用者被迫使用蹩腳的密碼,即便選擇對破解者最不利的情況,也就是每秒10 000(印表機都有可能使用空閒時間做到這一點)Class A攻擊,最多需要61個小時就可以破解62個基礎字元(a-z、A-Z0-9)的金鑰空間。對於類似AaZzaa99這樣的密碼,只需26個小時即可破解。當考慮這種情況時,使用使用者定義的破解級別就更顯得意義重大。

這也給了你深入瞭解安全明確的其他限制和預設密碼的機會。舉個例子,我們曾經對郵件列表的成員關係提醒做過快速搜尋,這個查詢將提供對存檔電子郵件的各種連結,郵件列表的管理員沒有考慮他們的提醒程式如何通知使用者,包含有完整使用者名稱和密碼的資訊也被存放到這個列表中並且通過你最喜歡的搜尋引擎建立索引。因為這些都是明文,所以也不需要破解。可以看到大多數都是簡單的由8個小寫字母組成的隨機密碼。如果對整個金鑰空間進行暴力破解,即使採用最慢的破解級別,也只需要217秒,大約是聽完AC/DC樂隊的“Have a Drink on Me”所需要的時間。

字典攻擊和其他攻擊方法

精英型別的黑客首先要說的是,“如果從字母Z開始,並且倒退著來呢?”好吧,如果這是你認為合理,並且關注的事情,那就倒著列舉。就算按照字母位置倒著列舉字元,甚至可以一切都從M開始,這個公式依然能以相同的方式工作。當然,攻擊者也可能決定從中間字母開始,他們抱著萬分之一的希望,期望你也是這麼做的,還有許多諸如此類的方法,總之,這是導致收益遞減的引數。

筆者認為,同樣的道理也適用於字典攻擊,我和Will把這稱為“香蕉狗綜合徵”。可以使用包含10000個單詞的字典攻擊那些對安全一無所知的人,他們通常使用bananadog等單詞作為密碼。假設把這些詞兩兩組合在一起(產生含有1億個單詞的詞典),即便這些人聰明地使用bananadog作為密碼,攻擊者也只需要毫秒級的時間就可以破解。暴力破解bananadog的金鑰空間需要大約94.11分鐘,在這段時間裡,攻擊者可以列舉大約5 646 683 826 134個最多由9個小寫字母組成的密碼。對於任何人,bananadog攻擊總是被限制在總數為1億個的可能密碼中,並且會錯過類似bannaadig這樣的密碼。攻擊者想要得到密碼,他們不達目的誓不罷休。正因為這樣,筆者並不擔心以bananadog作為密碼的人,如果你是這種型別的人,我懷疑你根本不可能讀本書。

Will提出一種很好的觀點,就是如果作為攻擊者只需要花費幾毫秒的時間就可以從字典攻擊中獲得那個未知的密碼,有什麼理由不這麼做嗎?筆者完全贊同這種觀點,作為這個邏輯的延伸,需要在密碼強度檢查工具中增加一些合理的檢查,去除類似bananadog這樣的“雜草”。如果準備這麼做,就必須先做幾個不同級別的假設,這意味著實際應用的字典攻擊的邏輯是非常主觀的,但是當前的實現方法圍繞著暴力破解的順序(關於從a到z、從A到Z、從0到9,等等)所做的假設是公平的。需要將這些詞連線起來,給類似BananaDog這樣的密碼評分嗎?需要做三次連線,給類似DogBananaDog這樣的密碼評分嗎?如果是BananaDog1,又怎麼處理?或是B4n4n4D0g呢?筆者發現字典攻擊的邏輯太隨機了。理論上做這個檢查是可以的,但是並不知道如何以工具的形式實現;所以把這個問題放在了次要位置上,任何人如果有好的建議,請直接聯絡[email protected],我都會回覆的。

筆者認為這種方法比其他衡量密碼強度的方法更有價值,圖3給出了一個未公開的測試網站作為演示示例。

 

圖3密碼強度計量器顯示由20z組成的密碼得分是0,非常脆弱

這個系統根據一系列滿足最低要求的規則對強度進行評級,顯然20z是非常脆弱的密碼。但是如果計算一下,就會發現要破解這個金鑰空間需要20 725 274 851 017 800 000 000 000 000次列舉,需要657 194 154 332.12年的時間。在圖4中,給出了另一個不同的密碼。

 

圖4 密碼強度計量器顯示:由有大寫字母、小寫字母和數字組成的5字元密碼獲得了50%

的評分,並被評價為Good

重申一下,這個假設就是因為Aa!1_20z更復雜,是個強壯的密碼,但是用數學來衡量,只需要2 569 332 380次列舉就可以破解這個密碼的金鑰空間,需要的時間是2.56933238秒。這就是為什麼一直提醒大家使用密語而不是密碼,因為可以很容易地記住這個密碼。但是每次輸入Aa!1_的時候,不得不反覆檢查,確認沒有輸錯。所以現在有了量化的標準來衡量密碼的複雜性,問題是密碼可以安全多少年?

檢視程式碼

下面就是密碼檢查工具的C#原始碼:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Forms;

namespace PasswordChecker
{
public partial class frmPassword : Form
{

// Assumptions:
// lower is only lower
// upper is only upper
// numbers are only numbers
// Bangs assumes upper and lower and numbers
// Spec assumes upper lower numbers and Bang

public static int iLength;
public static double uCombinations;
public static double uPerSecond;
public static string sPassword;
public static int iBase = 0;
public static int iaz = 0;
public static int iAZ = 0;
public static int iNum = 0;
public static int iBang = 0;
public static int iSpec = 0;
public static string brute = "abcdefghijklmnopqrstuvwxyz" +
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" +
"[email protected]#$%^&*()-=+" +
" []\""+"{}j;': " +",./<>?'  ”;

public static double dSeconds;
public static double dMinutes;
public static double dHours;
public static double dDays;
public static double dYears;

public frmPassword()
{
InitializeComponent();
uPerSecond = 1000000000;
uCombinations = 0;
txtPerSecond.Text = "1,000,000,000";
txtPassword.Focus();
}

private void frmPassword_Load(object sender, EventArgs e)
{
}

private void txtPassword_TextChanged(object sender,EventArgs e)
{
GetBase();
}

public void GetBase()
{
sPassword = txtPassword.Text;
iLength = txtPassword.TextLength;
iaz = 0; iAZ = 0; iNum = 0; iBang = 0; iSpec = 0;
dSeconds = 0; dHours = 0; dDays = 0; dYears = 0;
uCombinations = 0;

MatchCollection az = Regex.Matches(sPassword, @"[a-z]");
MatchCollection AZ = Regex.Matches(sPassword, @"[A-Z]");
MatchCollection Num = Regex.Matches(sPassword, @"[0-9]");
MatchCollection Bang = Regex.Matches(sPassword, @"[[email protected]#$%
–– ^&*\(\)\-_=+]");
MatchCollection Spec = Regex.Matches(sPassword, @"[\[\]
–– \{\}\;\:\'\,\.\<\>\/\j\\\'\ \?\ ]\""");

if (az.Count > 0) {iaz = 26;}
if (AZ.Count > 0) { iAZ = 26; }
if (Num.Count > 0) { iNum = 10; }

if (Bang.Count > 0)
{
iBang = (26 + 26 + 10 + 14);


iaz = 0;
iAZ = 0;
iNum = 0;
}
if (Spec.Count > 0)
{
iSpec = (26 + 26 + 10 + 14 + 20);
iBang = 0;
iaz = 0;
iAZ = 0;
iNum = 0;
}

iBase = iaz + iAZ + iNum + iBang + iSpec;
txtBase.Text = "Dervies " + Convert.ToString(iBase) + "
–– of 96";
txtLength.Text = Convert.ToString(sPassword.Length);

for (int i = 1; i <= sPassword.Length; i++)
{
uCombinations = uCombinations + System.Math.Pow(iBase, i);
}

txtCombinations.Text = Convert.ToString(uCombinations);

dSeconds = uCombinations / uPerSecond;
dMinutes = dSeconds / 60;
dHours = dSeconds / 60;
dDays = dHours / 24;
dYears = dDays / 365;

txtMinutes.Text = string.Format("{0:n}", dMinutes);
txtHours.Text = string.Format("{0:n}", dHours);
txtDays.Text = string.Format("{0:n}", dDays);
txtYears.Text = string.Format("{0:n}", dYears);
}
}
}