使用python对SHA-1(哈希值)进行再次加密,防止黑客的爆破

By王诗琦

Feb 16, 2023 , , ,

关于哈希的一点点

黑客经常窃取用户登录和密码数据的整个资料库,因此 ,哈希是存储密码等敏感信息的首选方式。

哈希与加密不同,因为它们不存储数据。相反 ,构成散列的数字是计算运行的结果,无论它是您正在散列的是什么,无论是密码还是整个文件。这用于确保您下载的文件与您要下载的文件匹配,或确认用户输入的密码与他们註册的密码相匹配。

根据您要散列的文件或密码的大小,像SHA-1或MD5这样的散列将採用您正在散列的数据的固定块,并逐块对其进行复杂计算,直到达到最终值。此值是一个非常长的数字,设计为唯一的,以便通过比较哈希值来验证一个文件是否与另一个文件匹配。如果哈希值不同,那么关于该文件的某些内容已被更改。

这很好,因为如果用户输入的密码不是他们选择的密码,则哈希值将完全不同。因此,开发人员只需要存储哈希,因为只要用户需要登录,他们就可以输入密码来创建新哈希以与存储的哈希进行比较。

例如,我将nullbyte散列为以下SHA-1值。您可以在sha1-online.com上创建自己的SHA-1哈希,亲眼看看它的外观。

32c0ced56f1fe08583bdb079d85a35a81995018c

SHA-1哈希的一个问题

不幸的是 ,对于开发人员而言 ,并非所有哈希都是相同的存储密码。对于像SHA-1这样的哈希,有一些问题使用SHA-1保存密码不是一个理想的解决方案。

要突出显示一个,每次使用SHA-1散列相同的单词时 ,它会生成完全相同的散列。虽然这是设计的,但您可以简单地进行大量猜测并将它们全部哈希到SHA-1中,然后快速比较哈希以获得SHA-1哈希派生自的密码。因为SHA-1设计得很快,所以这个过程需要很短的时间,这使得它更容易暴力破解。

有一些解决方案,其中最受欢迎的是添加盐。salt是一串文本,您可以在对其进行散列之前将其添加到密码中。一个例子是将单词salt添加到密码nullbyte。虽然我们从上面知道了nullbyte的SHA-1值,但是nullbytesalt或saltnullbyte的散列将完全不同。这有帮助,但如果盐不是每个用户,那么找出盐并不太难,你又回到了同样的问题。

Bcrypt如何帮助使哈希变得更加安全

一个更好的解决方案是添加一个随机盐,并且有一个哈希算法是为了存储密码而创建的,正是考虑到了这一点。

Bcrypt不仅故意阻止暴力破坏,它还为它生成的每个哈希添加了一个随机盐。因此,即使它们是使用完全相同的密码制作的,也不会有两个bcrypt哈希值相同。要检查对bcrypt哈希的猜测,您必须使用bcrypt函数,该函数将密码猜测和哈希作为参数,并返回它们是否匹配的结果。

为了展示这些不同的哈希是如何工作的,我编写了一些Python来将任何密码转换为SHA-1,MD5和bcrypt哈希。

importhashlib,bcrypt

#Demonstratesthedifferencebetweentwotypesofhashing,SHA1andBcrypt

password=input("Inputthepasswordtohash

>")

print("

SHA1:

")

foriinrange(3):

setpass=bytes(password,'utf-8')

hash_object=hashlib.sha1(setpass)

guess_pw=hash_object.hexdigest()

print(guess_pw)

print("

MD5:

")

foriinrange(3):

setpass=bytes(password,'utf-8')

hash_object=hashlib.md5(setpass)

guess_pw=hash_object.hexdigest()

print(guess_pw)

print("

BCRYPT:

")

foriinrange(3):

hashed=bcrypt.hashpw(setpass,bcrypt.gensalt(10))

如下所示 ,MD5和SHA-1哈希值完全相同,但每次生成时bcrypt哈希值都会发生变化。对于开发人员来说,bcrypt显然是更好的选择。但是如果我们碰巧使用SHA-1或MD5哈希密码资料库,我们怎么能真正去强制哈希呢?

/Users/skickar/venv/untitled10/bin/python/Users/skickar/Desktop/TestSHA1.py

Inputthepasswordtohash

>nullbyte

SHA1:

32c0ced56f1fe08583bdb079d85a35a81995018c

32c0ced56f1fe08583bdb079d85a35a81995018c

32c0ced56f1fe08583bdb079d85a35a81995018c

MD5:

5f804b61f8dcf70044ad8c1385e946a8

5f804b61f8dcf70044ad8c1385e946a8

5f804b61f8dcf70044ad8c1385e946a8

BCRYPT:

b'$2b$10$Z1WVDUi50fmqyrpw19rIyOLPIKVUFeh7HO0FfQi1MbKjyxyduG2WS'

b'$2b$10$F.vehMYSUh/6zmTR/VY2quTnPfzPDcIdHTfZpb8twqjRIIIEFcbUW'

b'$2b$10$pZyptPPDHrnIgpU7wTW2nu4cfGAUS65kcGZb6FMC7KmYwJmuwSoLO'

为Brute-ForceSHA-1构建Python3程序

成长为黑客的一部分是学习编写自己的工具。首先,您的工具将很简单并解决小问题,但随着您获得经验,您将能够实现越来越多。当你开始使用时,像C++这样的强类型程式语言对于初学者来说很难理解,但Python3是一种灵活的 ,适合初学者的语言,它可以让我们轻松地抽象思想和构建原型。

我们今天要编写的简单程序将有助于实践黑客创建利用漏洞的工具的方式。在这个例子中,SHA-1很容易受到强制攻击,因为你可以将两个哈希值进行比较,所以我们将编写一个程序来完成它。

要编写任何程序,您需要写出程序需要遵循的步骤才能成功。这个列表可能看起来有点长,但它可以压缩,你应该尽可能具体地了解事情需要工作的方式 ,以获得你想要的输出。我更喜欢使用白板或MindMupp等在线流程图制作者来绘制这些程序从开始到结束的流程。

当您完成步骤时,您可以开始跳转到伪代码,这是您按顺序放置顺序中的步骤的地方,这种方式是可读的,但更接近实际表达代码的方式。编写这个伪代码后,您可以逐行填写代码,在发生错误时纠正错误,并观察程序的每一步开始形成并相互交互。

你需要继续做什么

要遵循本指南,您需要一台可以使用Python3的计算机。Python3与之前版本的Python有许多不同之处,因此您应该确保获得正确的版本。您可以通过多种方式安装Python3。在Linux中 ,您可以键入以下内容来安装Python3。

aptinstallpython3

您将需要一个Python3IDE(集成开发环境)。这些程序可以帮助您编写,测试和试验代码。特别是,我推荐Jetbrains的PyCharm。此外,专业版可免费提供给学生,如果您恰好符合条件,这绝对值得。

不要错过:破解密码所需的原则和技术

为了使一切正常工作 ,我们需要导入一些库。我们将使用urllib,urlopen和hashlib库来使这个代码能够从远程URL打开文件,并将哈希密码猜测转换为SHA-1。要包含它们,请在IDE中创建一个新的Python3文件,并在第一行中键入以下内容。

fromurllib.requestimporturlopen,hashlib

这将导入所需的库,确保程序的其余部分可以访问这些库。如果您需要在计算机上安装任何这些库来运行此脚本,通常可以使用pipinstall,然后使用所需库的名称。

接下来,您可以下载我为此示例编写的Python程序。为此,请打开终端窗口并键入以下三个命令以下载脚本,更改为其目录,并列出其中的文件。

gitclone

cdSHA1cracker

ls

步骤1

从用户处获取SHA-1哈希值

对于第一个命令 ,我们需要从用户那里获取我们想要破解的哈希。为此,我们可以使用输入功能,该功能将向用户显示提示并允许他们输入响应。

在Python中,我们可以将此响应存储在变量中,而无需事先做任何事情。这是因为Python不像C++,它要求你在开头声明一切。我们可以创建变量来保存我们想要的数据。

我们将变量命名为sha1hash,因为我们将在其中存储SHA-1哈希。我们可以输入它来创建变量,然后我们需要分配用户的响应来填充该变量。在Python中,equals(=)符号并不意味着它在比较某些内容以查看它是否相等。这实际上是用两个等号(==)来完成的。等号符号更多是Python中的命令,左侧的变量被赋予等号右侧的数据。

我们将分配用户类型的任何内容,因此我们将调用输入函数,这也允许我们将显示给用户的文本放在两个括号中。为了告诉Python我们想要列印一个字符串或一组字符,我们也会附上我们在引号中输入的内容。最终结果应如下所示:

sha1hash=input("Pleaseinputthehashtocrack.

>")

当我们运行它时,会出现一个提示「请输入要破解的哈希」。在此之后,我们看到一个「新行」符号,它是一个反斜槓(\)和一个n。这意味着跳转到新行。最后,我放了一个>符号,这样用户就可以在新行上输入他们的响应。当我们运行文件NBspecial.py时,结果如下所示。

Dell:SHA1crackerskickar$

Dell:SHA1crackerskickar$python3NBspecial.py

Pleaseinputthehashtocrack

>_

一旦用户输入一个哈希值,它就会保存在sha1hash变量中,以便稍后在程序中使用。

步骤2

打开一个完整的密码猜测文件

接下来,我们要打开许多常用密码的列表。我们将使用我们示例中10,000个最常用密码的列表,这是一个託管在GitHub上的纯文本文件。您可以使用其他列表,例如在线泄露的密码或使用Mentalist或Crunch制作的密码。

不要错过:使用泄露的密码资料库来创建暴力Word列表

对于我们正在使用的文件,我们将再次将其分配给变量,这次称为LIST_OF_COMMON_PASSWORDS。要打开文件,我们将使用一个名为urlopen的函数,它允许我们轻松打开此文本文件并告诉Python正确的编码类型。使用以下格式。

urlopen('TheURLYouWantToOpen').read()

这将使用read方法打开引号括起来的URL,这意味着我们要从文件中读取文本。为了确保str()函数知道它正在使用什么,我们还将在此函数之后添加一个命令和'utf-8'来告诉程序我们正在使用UTF-8文本编码。

我们将再次将数据保存为字符串,并且为了防止出现任何问题,我们可以通过首先将其「转换」为字符串来确保我们放入变量的数据是一个字符串。这意味着尝试将数据更改为另一种类型,并且可以将整数转换为字符串,将字符串转换为字节以及所需的任何其他类型的数据类型。为此,我们将键入str(),然后将要转换为括号内的字符串的数据包括在内。最终结果应如下所示。

LIST_OF_COMMON_PASSWORDS=str(urlopen('').read(),'utf-8')

在这一行中,我们打开从远程URL中选择的文本文件,将其编码为UTF-8文本文件,然后将该数据保存到名为LIST_OF_COMMON_PASSWORDS的字符串中。

步骤3

从密码列表中进行猜测

现在,我们需要解决一个有趣的问题。虽然我们知道文本文件中有10,000个密码,但程序不知道有多少密码,因此我们需要为密码文件中的每个猜测创建一些代码来运行一次。

不要错过:如何制定一个好的密码破解策略)

为此,我们将使用一个名为for循环的结构。一个用于循环是在编程中非常基本的概念,看起来是这样的:

for[anindividualguess]in[thevariablethatguessisin]:[dothis]

这意味着,对于我们创建的变量中的猜测数量,以保存最后一步中的所有猜测(在本例中为10,000) ,我们将执行下面的操作。在实践中,这意味着我们将从猜测列表中进行猜测,做任何动作,然后跳起来抓住下一个猜测,直到我们用尽新的猜测来尝试。

我们可以将每个猜测的变量命名为我们想要的任何东西,但为了清楚起见 ,我将其命名为猜测。只需对LIST_OF_COMMON_PASSWORDS中的x表示即可。

我们需要解决的最后一个问题是告诉程序如何将大量密码分解为单独的密码猜测。我们使用的密码列表用新行分隔密码 ,因此我们可以使用新行字符将LIST_OF_COMMON_PASSWORDS分成单独的猜测。

为了实现这一点,我们可以将.split()添加到LIST_OF_COMMON_PASSWORDS变量的末尾,并将新行(即'\n')的代码放入括号中。最终结果如下所示。

forguessinLIST_OF_COMMON_PASSWORDS.split('

'):

此代码将从我们之前创建的LIST_OF_COMMON_PASSWORDS变量中获取一个密码,在该行的末尾停止。它将运行与列表中的密码一样多的次数,除非我们告诉它在接下来的步骤中表现不同。

步骤4

哈希猜测我们从密码列表中取出

在这里,我们需要创建一个新变量来保存我们从列表中提取的密码猜测的哈希版本。当我们这样做时,它应该创建一个相同的哈希,如果我们使用相同的密码,用于创建用户在第一步中提供的哈希。如果它在下一步中匹配,我们就会知道我们找到了密码。

我们将变量命名为包含猜测hashedGuess的哈希版本。接下来,在我们能够对从密码列表中提取的猜测进行哈希之前,我们需要做一些准备工作。要转换字符串变量,我们将guess调用为bytes对象。这是必要的,因为SHA-1函数仅适用于字节对象,而不适用于字符串。

幸运的是,将字符串转换为字节很容易。我们可以通过将第一步中的用户输入转换为字符串的相同的一般方式来实现。该公式如下所示。在这种情况下,我们将猜测为字节 ,文本编码为UTF-8。

bytes(StringToTurnIntoBytes,'EncodingOfString')

现在我们有了guess的字节版本,我们可以使用以下代码将其转换为SHA-1哈希。

hashlib.sha1(BytesToHash).hexdigest()

那是做什么的?我们从hashlib函数调用SHA-1哈希并散列我们放在括号中的bytes变量。由于SHA-1的工作方式,我们可以不断添加内容,但是为了列印SHA-1哈希的当前值,我们将.hexidigest()添加到最后。

在最终代码中,我们将散列猜测的值分配给变量HashedGuess。

hashedGuess=hashlib.sha1(bytes(guess,'utf-8')).hexdigest()

现在我们将密码猜测保存为散列,我们可以将此猜测与原始SHA-1散列进行比较以直接破解。

步骤5

将哈希密码与原始哈希进行比较

在这一步中,我们需要告诉程序如果哈希匹配该怎么做。为此,我们将使用一个名为if语句的简单语句。

一个如果语句的工作有点像的说法,但检查的情况看,如果是执行代码的下一部分之前如此。如果条件为真,则可以告诉程序执行一个操作,如果为false,则执行另一个操作。

Python中if语句的通用公式如下。

if[someconditiontocheckistrue]:

[dowhateverthiscodesaystodo]

对于我们的用例,我们想确定散列猜测是否与用户给我们的原始散列相匹配,因此我们可以使用==符号来确定它们是否相等。我们要评估的语句是hashesGuess是否等于sha1hash,我们保留原始哈希的变量。在我们的代码中,这是一个简单的语句。

ifhashedGuess==sha1hash:

现在我们已经建立了这种比较,我们将不得不向程序解释在我们期望的三种情况下要做什么:匹配,不匹配或列表中没有更多的密码来猜测。

步骤6

设置条件逻辑

在此步骤中,我们将解释如果哈希匹配或不匹配该怎么做。因为前面的语句询问如果这两者相等会怎么做,我们的第一条指令将是如果密码猜测的哈希与原始密码匹配该怎么做。

如果是这种情况,我们找到了密码,正确的做法是列印出正确的密码并退出程序。如果我们不退出,即使我们找到了与SHA-1哈希匹配的密码,循环也会继续。为此,我们只需键入以下内容即可。

print("Thepasswordis",str(guess))

这将列印引号内的所有内容,然后添加已成功猜到的当前密码的字符串版本。重要的是我们列印guess变量而不是hashedGuess变量,因为hashedGuess版本只会给我们另一个SHA-1哈希。在这种情况下,我们还将该变量强制转换为字符串,以便Python可以很好地列印它而不会出现错误。在列印完之后,我们只需要包含quit()来关闭程序,因为我们已经有了密码!

不要错过:如何使用Hashcat快速破解密码

如果hashedGuess和sha1hash变量不匹配,我们需要解释要做什么。我们可以使用elif语句添加该部分语句。Elif或「else

if」告诉程序如果条件不同则该怎么做。我们在这种情况下的下一个测试声明如下。

hashedGuess!=sha1hash

该语句询问两个变量是否相等,用!=符号表示。如果这是真的,或者换句话说,如果两个哈希值不相等且密码猜测错误,我们需要告诉用户猜测失败,然后回到循环顶部抓住一个新密码。

为此,我们将执行与之前相同的操作,只需使用print()函数列印出一条消息。在这条消息中,我们会说:「密码猜测」,[猜测],「不匹配,尝试下一个......」。最终结果应如下所示。

print("Thepasswordis",str(guess))

quit()

elifhashedGuess!=sha1hash:

print("Passwordguess",str(guess),"doesnotmatch ,tryingnext...")

这段代码解释了如果猜测是正确的,该怎么做,如果猜测不匹配该怎么办,但如果我们找不到匹配怎么办?如果我们确定我们已经用尽了密码列表并且没有更多的猜测可以尝试,那么我们可以向用户提供更多信息,而不仅仅是退出。

步骤7

告诉程序如果没有匹配该怎么办

如果我们一直在这个循环中找不到匹配项,那么循环将结束,因为从密码猜测列表中将没有任何进一步的东西。我们将让用户知道我们没有成功,而不是仅仅通过在循环外部放置一个print语句而突然退出程序。这样,如果找到密码,最终的列印功能将永远不会执行,因为我们之前添加的quit()函数在我们获得正确的密码时结束程序。

那么我们如何将这个陈述置于循环之外呢?在Python中,空格非常重要,因此我们可以将它放在一个新行上,而不是缩进它,如下例所示。

forguessinLIST_OF_COMMON_PASSWORDS.split('

'):

hashedGuess=hashlib.sha1(bytes(guess,'utf-8')).hexdigest()

ifhashedGuess==sha1hash:

print("Thepasswordis",str(guess))

quit()

elifhashedGuess!=sha1hash:

print("Passwordguess",str(guess),"doesnotmatch,tryingnext...")

print("Passwordnotindatabase,we'llgetthemnexttime.")

Python首先执行for循环,然后计算if和elif语句,并且只有在循环结束时才执行最终的print函数,因为它在for循环之外。

这个列印功能很简单,不包含任何变量,只是一个字符串,让用户知道我们没有在列表中找到匹配的密码。

print("Passwordnotindatabase,we'llgetthemnexttime.")

在最后一行中,我们有一个功能齐全的SHA-1强制执行程序,让我们运行它!首先,我们得到一个提示,要求SHA-1哈希破解。我会给它哈希cbfdac6008f9cab4083784cbd1874f76618d2a97来测试它。

Dell:SHA1crackerskickar$python3NBspecial.py

Pleaseinputthehashtocrack.

>cbfdac6008f9cab4083784cbd1874f76618d2a97

按返回后,脚本开始工作。

Passwordguess171717doesnotmatch,tryingnext...

Passwordguesspanzerdoesnotmatch,tryingnext...

Passwordguesslincolndoesnotmatch,tryingnext...

Passwordguesskatanadoesnotmatch,tryingnext...

Passwordguessfirebirddoesnotmatch,tryingnext...

Passwordguessblizzarddoesnotmatch,tryingnext...

Passwordguessa1b2c3d4doesnotmatch,tryingnext...

Passwordguesswhitedoesnotmatch ,tryingnext...

Passwordguesssterlingdoesnotmatch,tryingnext...

Passwordguessredheaddoesnotmatch ,tryingnext...

Thepasswordispassword123

Dell:SHA1crackerskickar$_

就这样,我们找到了用于创建哈希的密码,允许我们反转「单向」SHA-1哈希。

Python是有趣和强大的,让我们缩短它

通过一些简单的Python3知识,我们能够编写一个简单的脚本来查找仅在11行中派生哈希的密码。您可以在下面看到没有评论的整个代码。通过Python的一些巧妙的格式化,我们可以使它更紧凑(但更难以阅读或理解)并且只用三行代码执行所有这些。

fromurllib.requestimporturlopen,hashlib

sha1hash=input("Pleaseinputthehashtocrack.

>")

LIST_OF_COMMON_PASSWORDS=str(urlopen('').read(),'utf-8')

forguessinLIST_OF_COMMON_PASSWORDS.split('

'):

hashedGuess=hashlib.sha1(bytes(guess,'utf-8')).hexdigest()

ifhashedGuess==sha1hash:

print("Thepasswordis",str(guess))

quit()

elifhashedGuess!=sha1hash:

print("Passwordguess",str(guess),"doesnotmatch,tryingnext...")

print("Passwordnotindatabase,we'llgetthemnexttime.")

这可以通过在我们用于导入库的同一行上获取哈希来破解,并通过使用称为三元运算符的东西将for和if语句压缩成一行来实现。通常,这些的格式如下,并且可以根据需要添加到其中。

ifelseif

在我们的脚本中,我们将使用的格式如下:

ifelseifelse

应用这些更改后,我们可以压缩我们的代码,如下例所示。

fromurllib.requestimporturlopen,hashlib;origin=input("InputSHA1hashtocrack

>")

forpasswordinstr(urlopen('').read(),'utf-8').split('

'):

[print("Thepasswordis",str(password)),quit()]if(hashlib.sha1(bytes(password,'utf-8')).hexdigest())==originelseprint("Passwordnotindatabase,we'llgetthemnexttime.")ifpassword==""elseprint("Passwordguess",str(password),"doesnotmatch,tryingnext...")

虽然对于没有评论的Python新手来说这很难理解,但是只需通过编写程序并查找快捷方式,就可以将Python从粗略的想法简化为一些简洁的代码行。

如果你想将它放到一行,你可以简单地将它包装在一个exec()函数中,并为每个新的换行添加新的行(\n)字符。为什么你会这样做,我不确定,但能够在需要时压缩程序是有用的。