即使今天一支普通智慧型手機都有多顆 CPU 可用,CPU 在同一時間能做的事仍然有限。只要攻擊者能用極少的力氣與頻寬吃掉 CPU 資源,就可能造成阻斷服務。做法有很多,本節談其中兩種:利用演算法複雜度,以及找出可從外部控制的密碼學系統參數。
演算法複雜度(Algorithmic Complexity)#
所有電腦演算法都有對應的運算成本,代表為了讓某個輸入產生預期輸出所需的工作量。演算法要做的工愈多,佔用處理器的時間就愈長。理想上演算法應該不論輸入為何都花費固定時間,但現實鮮少如此。
有些演算法會隨著輸入參數的數量增加而變得特別昂貴。以排序演算法氣泡排序(Bubble Sort)為例,它檢查緩衝區中的每一組相鄰值,若左邊比右邊大就交換,使較大的值逐步「冒泡」到緩衝區尾端,直到整個緩衝區排序完成。
def bubble_sort(int[] buf)
{
do
{
bool swapped = false;
int N = len(buf);
for(int i = 1; i < N - 1; ++i)
{
if(buf[i-1] > buf[i])
{
// Swap values
swap( buf[i-1], buf[i] );
swapped = true;
}
}
} while(swapped == false);
}這個演算法的工作量正比於緩衝區中的元素個數 N。最好情況是所有元素本來就已排序好,只需掃過一遍,共 N 次迭代;最壞情況是緩衝區以反向排序,此時排序過程需要重複 N² 次。
若攻擊者能指定大量反向排序的值,這次排序的運算成本就會變得非常可觀,足以吃滿 100% 的 CPU 處理時間,造成阻斷服務。
真實世界的案例:包括 PHP 與 Java 在內的一些程式環境,其雜湊表(hash table)實作所用的演算法在最壞情況下需要 N² 次操作。雜湊表這種資料結構會把值與另一個值(例如文字名稱)當作鍵綁在一起:鍵先用一個簡單演算法算出雜湊值,再據此決定值要放進哪個桶(bucket)。那個 N² 演算法用在「把新值插入桶中」的環節;理想上不同鍵的雜湊值極少碰撞,桶的大小就很小。但只要精心構造一組雜湊值相同、鍵值卻不同的鍵,攻擊者僅需送出寥寥數個請求,就能對網頁伺服器之類的網路服務造成阻斷服務。
Big-O 表示法
Big-O 表示法是描述運算複雜度的常見方式,代表演算法複雜度的上界。以下由低到高列出常見的複雜度:
| 表示法 | 說明 |
|---|---|
O(1) | 常數時間;演算法永遠花費相同的時間。 |
O(log N) | 對數;最壞情況正比於輸入數量的對數。 |
O(N) | 線性時間;最壞情況正比於輸入數量。 |
O(N²) | 平方;最壞情況正比於輸入數量的平方。 |
O(2^N) | 指數;最壞情況正比於 2 的 N 次方。 |
要記得這些是最壞情況的值,未必代表真實世界的複雜度。話說回來,只要掌握了特定演算法的細節(例如氣泡排序),攻擊者就很有機會刻意觸發它的最壞情況。
可設定的密碼學參數(Configurable Cryptography)#
雜湊演算法之類的密碼學原語(primitive)處理過程也會產生可觀的運算負擔,尤其是在處理身分驗證憑證時。
資安界的鐵則是:密碼在儲存前必須先用密碼學摘要演算法雜湊過。這把密碼轉換成一個幾乎不可能反推回原文的雜湊值,即使雜湊外洩,也很難還原出原始密碼。但攻擊者仍可以猜密碼、算出雜湊,一旦猜中的密碼雜湊後吻合,原始密碼就破了。為了緩解這個問題,通常會把雜湊運算重複執行多次,藉此提高攻擊者的運算需求——可惜這同時也提高了應用程式自身的運算成本,而這正好可能變成阻斷服務的破口。
漏洞會在兩種情況下成立:雜湊演算法所需時間隨輸入大小呈指數成長,或者演算法的迭代次數可以由外部指定。多數密碼學演算法的耗時與輸入大致成線性關係;但若你能指定迭代次數又沒有合理上限,處理時間就可以任由攻擊者拉長。
def process_authentication()
{
string username = read_string();
string password = read_string();
int iterations = read_int(); /* 迭代次數由網路指定,沒有上限 */
for(int i = 0; i < interations; ++i)
{
password = hash_password(password);
}
return check_user_password(username, password);
}攻擊者顯然可以給出一個極大的迭代次數,讓 CPU 資源被長時間大量佔用——若雜湊演算法本身運算複雜,效果更明顯。
另一個典型的「用戶端可設定密碼學參數」的例子是公開/私密金鑰的處理。像 RSA 這類演算法的安全性建立在分解大型公開金鑰值的運算成本上:金鑰值愈大,加解密就愈耗時,產生新金鑰對所需的時間也愈長。