如何用神经网络来恢复破损的密码?,神经网络破损,未经许可,禁止转载!英文


本文由 编橙之家 - renlytime 翻译,dylan 校稿。未经许可,禁止转载!
英文出处:neupy.com。欢迎加入翻译组。

在这份教程中,我们将会学习到如何使用神经网络来恢复破损的密码。如果你对 DiscreteHopfield 网络算法不是很了解的话,你可以通过阅读这篇 教程 来加强了解。

在开始实验之前,我们需要先设置一个种子参数使所有实验结果都可以重现。但是,没有这个参数程序也能正常工作。

Python
import random
import numpy as np

np.random.seed(0)
random.seed(0)

如果所安装的Python版本或库版本过低,导致你无法重现上面的结果,你可以自行安装我们在这份教程中所需要的库。

Python
>>> import neupy
>>> neupy.__version__
'0.1.0'
>>> import numpy
>>> numpy.__version__
'1.9.2'
>>> import platform
>>> platform.python_version()
'3.4.3'

所有的代码同样在Python2.7的环境中编译通过。

数据转换

在建立用于存储和恢复密码的神经网络之前,我们需要先转换输入输出的数据。但是,仅仅对其编码是不够的,我们应当为输入数据设置一个固定长度以确保字符串之间的长度一致。我们也要预先决定使用什么编码方式,为了简单易懂,我们选择ASCII码。现在让我们来定义用于将数据转换成二进制列表的函数吧。

Python
def str2bin(text, max_length=30):
    if len(text) > max_length:
        raise ValueError("Text can't contains more "
                         "than {} symbols".format(max_length))

    text = text.rjust(max_length)

    bits_list = []
    for symbol in text:
        bits = bin(ord(symbol))
        # Cut `0b` from the beggining and fill with zeros if they
        # are missed
        bits = bits[2:].zfill(8)
        bits_list.extend(map(int, bits))

    return list(bits_list)

这个函数需要传入两个参数,第一个是我们需要对其编码的字符串,第二个则为设定传入字符串的最大长度。只有传入字符串的长度小于最大长度,程序才会继续执行。

然我们来看看 str2bin 的 输出情况

Python
>>> str2bin("test", max_length=5)
[0, 0, 1, 0, 0, 0, 0, 0, 0, ... ]
>>> len(str2bin("test", max_length=5))
40

ASCII 编码使用八个字节来存放一个字符,而我们这里将每个字符串最大长度设置为5。所以我们的向量长度等于40。从第一个输出结果可以看到,第一组八个符号 00100000 在 ASCII 表中表示空格。

在完成密码恢复过程的预处理之前,我们得到的总是一个 二进制的的列表。所以在我们将数据存到神经网络里面之前,我们需要在定义个函数用来将列表中二进制转回字符串。(差不多与前一个函数功能相反)

Python
def chunker(sequence, size):
    for position in range(0, len(sequence), size):
        yield sequence[position:position + size]

def bin2str(array):
    characters = []
    for binary_symbol_code in chunker(array, size=8):
        binary_symbol_str = ''.join(map(str, binary_symbol_code))
        character = chr(int(binary_symbol_str, base=2))
        characters.append(character)
    return ''.join(characters).lstrip()

如果函数的返回值为 ‘test’ 则说明我们的函数正确运行。

Python
>>> bin2str(str2bin("test", max_length=5))
'test'

注意!如果二进制列表中是以空格开始,那么在转换时这些空格将会被删去,我们就假定密码不会以空格开始吧。

将密码保存到神经网络中

Now we are ready to save the password into the network. For this task we are going to define another function that create network and save password inside of it. Let’s define this function and later we will look at it step by step.

我们已经做好将密码保存至神经网络中的准备了。现在我们将会针对这个过程在定义一个函数去创建神经网络并将密码存入其中。现在先来看看这个函数吧,稍后我们将会对其逐步分析。

Python
import numpy as np
from neupy import algorithms

def save_password(real_password, noise_level=5):
    if noise_level < 1:
        raise ValueError("`noise_level` must be equal or greater than 1.")

    binary_password = str2bin(real_password)
    bin_password_len = len(binary_password)

    data = [binary_password]

    for _ in range(noise_level):
        # The farther from the 0.5 value the less likely
        # password recovery
        noise = np.random.binomial(1, 0.55, bin_password_len)
        data.append(noise)

    dhnet = algorithms.DiscreteHopfieldNetwork(mode='sync')
    dhnet.train(np.array(data))

    return dhnet

如果你已经读过 Discrete Hopfield Network tutorial , 你就会知道如果我们只添加一个向量到神经网络中,则会复制这个向量,或者会将整个矩阵逆转。为了保障安全性,我们可以在网络中添加一些噪声。我们可以再添加一个 noise_level 的参数到函数中。这个参数将会控制随机产生的二进制向量数量。 使用二项分布将每次迭代产生噪音向量的概率控制在55%。然后我们就可将噪音顶点和传过来的密码存入矩阵,最后我们将所有数据存入 Discrete Hopfield Network 中。

然后函数将会返回一个训练过的网络供以后使用。

但是我们为什要使用随进二进制向量来代替随机解码过的字符串呢?问题在于两个向量往往非常相似。现在可以来检查下两种方法,然后比较他们的  Hamming 距离。但是在开始之前,我们需要定义一个用来比较向量距离的函数。

Python
import string
import random

def hamming_distance(left, right):
    left, right = np.array(left), np.array(right)
    if left.shape != right.shape:
        raise ValueError("Shapes must be equal")
    return (left != right).sum()

def generate_password(min_length=5, max_length=30):
    symbols = list(
        string.ascii_letters +
        string.digits +
        string.punctuation
    )
    password_len = random.randrange(min_length, max_length + 1)
    password = [np.random.choice(symbols) for _ in range(password_len)]
    return ''.join(password)

此外,你还能看到 generate_password 这个我们将会用来帮助测试的函数。让我们来检查两个随机产出的密码向量之间的 Hamming 距离吧。

Python
>>> hamming_distance(str2bin(generate_password(20, 20)),
...                  str2bin(generate_password(20, 20)))
70

正如你所看到的那样,两个随机产生的向量的相似度非常高(大约 70% 字节都是一样的(((100 * (240 – 70) / 240))))。但是将我们随机产生的密码和二进制的向量进行比较,就能很容易发现不同。

Python
>>> hamming_distance(str2bin(generate_password(20, 20)),
...                  np.random.binomial(1, 0.55, 240))
134

Hamming 距离比上一个来得要大。 有大约55%的字节不一致。

这种差距可以帮助我们简化恢复从神经网络输入的向量这一过程。所以我们使用随机产生二进制向量取代随机产生字符。

当然,存入没有经过随机生成的噪音向量要好比将随机产生的密码转换成二进制向量要好,但是如果你使用错误的随机输入模式可能会覆盖掉正确的向量。

从神经网络中恢复密码

现在我们可以定义一个函数来从神经网络中恢复密码了。

Python
def recover_password(dhnet, broken_password):
    test = np.array(str2bin(broken_password))
    recovered_password = dhnet.predict(test)

    if recovered_password.ndim == 2:
        recovered_password = recovered_password[0, :]

    return bin2str(recovered_password)

函数需要两个参数,第一个参数用来指定一个网络模板,用来恢复破损的密码。而第二个参数是指破损的密码的字符串。

终于可以来测试从神经网络中恢复密码啦!

Python
>>> my_password = "$My%Super^Secret*^&Passwd"
>>> dhnet = save_password(my_password, noise_level=12)
>>> recover_password(dhnet, "-My-Super-Secret---Passwd")
'$My%Super^Secret*^&Passwd'
>>> _ == my_password
True
>>>
>>> recover_password(dhnet, "-My-Super")
'x19`xa0x04?0?1x14#?0?4E2erx1e?0?4e#2m4jVx07PqsCwd'
>>>
>>> recover_password(dhnet, "Invalid")
'x02 x1d`x80$?0?0x1c#?0?2E?0?4eòx0e?0?4e§:/$êx04x07@5sCu$'
>>>
>>> recover_password(dhnet, "MySuperSecretPasswd")
'$My%Super^Secret*^&Passwd'
>>> _ == my_password
True

现在一切运行正常,即使在代码多次运行之后你也不会有任何问题出现。神经网络现在能够自行产生字符串。这个字符串除了一些符号而外,几乎和我们的密码一模一样。这个问题是因为当神经网络在输入的时候产生了局部最优解。我们不能在运算最优解的时候预防这种情况。你可以通过阅读 tutorial about Discrete Hopfield Network 来了解这个问题的其他细节。

使用Monte Carlo进行测试

然我们来通过随机产生的密码来测试我们的程序吧。我们现在可以使用 Monte Carlo 来进行测试, 每一次我们都产生随机密码然后尝试恢复这个密码。

Python
import pprint
from operator import itemgetter
from collections import OrderedDict

def cutword(word, k, fromleft=False):
    if fromleft:
        return (word[-k:] if k != 0 else '').rjust(len(word))
    return (word[:k] if k != 0 else '').ljust(len(word))

n_times = 10000
cases = OrderedDict([
    ('exclude-one', (lambda x: x - 1)),
    ('exclude-quarter', (lambda x: 3 * x // 4)),
    ('exclude-half', (lambda x: x // 2)),
    ('just-one-symbol', (lambda x: 1)),
    ('empty-string', (lambda x: 0)),
])
results = OrderedDict.fromkeys(cases.keys(), 0)

for _ in range(n_times):
    real_password = generate_password(min_length=25, max_length=25)

    for casename, func in cases.items():
        n_letters = func(len(real_password))
        broken_password = cutword(real_password, k=n_letters,
                                  fromleft=True)

        dhnet = save_password(real_password, noise_level=11)
        recovered_password = recover_password(dhnet, broken_password)

        if recovered_password != real_password:
            results[casename] += 1

print("Number of fails for each test case:")
pprint.pprint(results)

在你提交之后,你的输出结果应该和下面一致。(如果你是一步一步跟着教程做的话)

Python
Number of fails for each test case:
{'exclude-one': 11,
 'exclude-quarter': 729,
 'exclude-half': 5823,
 'just-one-symbol': 9998,
 'empty-string': 10000}

着这个测试中我们可以在神经网络尝试恢复同一个密码时发现两种解。这不是太好,但这个取决与我们在网络中所存储的噪音。随机化并不能给我们完美的结果。有时他甚至能从一个空字符串中恢复出密码,但这种情况并不多见。

最后,每一次迭代我们都总左边截取密码并将用空格填充,现在我们来试试从右边来截取密码,然后我们会得到以下结果。

Python
Number of fails for each test case:
{'exclude-one': 17,
 'exclude-quarter': 705,
 'exclude-half': 5815,
 'just-one-symbol': 9995,
 'empty-string': 10000}

结果和前面很像

另一个有趣的测试会发生在放你想要用空格来替代字符时。

Python
Number of fails for each test case:
{'exclude-one': 14,
 'exclude-quarter': 749,
 'exclude-half': 5760,
 'just-one-symbol': 9998,
 'empty-string': 10000}

还是和前面的结果很像

最后,我们不用空格来替代字符,我这次将其删去,这次结果不是太好。

Python
Number of fails for each test case:
{'exclude-one': 3897,
 'exclude-quarter': 9464,
 'exclude-half': 9943,
 'just-one-symbol': 9998,
 'empty-string': 9998}

我猜在第一种情况下(不含一),我们只是很幸运,在删去一个字符的同时并没有使其他更多的字符被改变。所以,删除字符并不是一个好主意。

所有需要实验的函数你都能在 github 找到。

潜在问题

这里有些可能会在 Discrete Hopfile Network 发生的问题

1. 我们在最后一个实验中我们发现字符的位移要比字符的缺失更难恢复,所以最好用空格代替缺失的字符。
2. 是可能从空字符串中恢复出密码的。
3. 相似的二进制码代表不同的字符是个大问题,有时你会发现两个不同的字符在转换为二进制后只有一个字节不同。我们可以使用 One Hot Encoder 来解决这个问题。但是这样会给我们带来更多的问题。举个例子,我们使用一个包含 94 个字符的字符列表作为密码,如果你对每一个字符都进行解码,我们将会得到一个有 93 个0和一个有效数字组成的向量。问题在于我们总是需要一个有效数字来恢复密码,不过这个发生的可能性很低。

总结

尽管存在一些问题,使用神经网络来恢复密码是确实可行的。通过 Monte Carlo 实验我们可以知道只有少数的字符存在不能正确恢复的可能性。

在你充分了解这个程序的限制以后,即使只是简单的神经网络也可以发挥出强大的功能。

下载代码

你可以从 github repository 看到所有的代码和测试。

评论关闭