整理資料時發現幾個 zip 文件的密碼忘記了,於是嘗試用python暴力破解
首先是讀取和解壓zip文件,使用 zipfile 庫
import zipfile
z = zipfile.ZipFile(r'./file.zip')
z.extractall(pwd=password.encode('utf-8'))
定義一個密碼元字符串,每次從里面取出一些字符,比如:
meta_str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
隨機密碼
使用 random.sample 生成指定長度的密碼,然后出現過的密碼放入一個 set
choiced_set = set()
rand_str = ''.join(random.sample(meta_str, 4))
if rand_str not in choiced_set:
choiced_set.add(rand_str)
為了不斷的產生密碼並嘗試解壓,此處應該有循環,那么將嘗試解壓和生成密碼封閉為函數:
import zipfile
import random
meta_str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
choiced_set = set()
def guess(z, password):
try:
z.extractall(pwd=password.encode('utf-8'))
print('guessed pwd: {}'.format(password))
return True
except Exception:
print('invalid password [{}]'.format(password))
return False
def main():
z = zipfile.ZipFile(r'./file.zip')
while True:
rand_str = ''.join(random.sample(meta_str, 4))
if rand_str not in choiced_set:
choiced_set.add(rand_str)
if guess(z, rand_str):
raise Exception('found')
此方法代碼里固定寫了只支持4個字符長度,若密碼不是4個字符,則會無止境的循環下去,顯然不科學。
為了支持n位密碼字符,需對代碼稍加修改:
- 增加n位字符, for x in range(n) 可以解決
- n位密碼長度的中止條件
密碼個數
由於將生成的密碼全部放入set,所以只需要判斷set的長度,就知道當前生成了多少密碼。
而密碼字符可以重復,所以'abcd' 可生成的2位密碼有:
'aa', 'ab', 'ac', 'ad',
'ba', 'bb', 'bc', 'bd',
'ca', 'cb', 'cc', 'cd',
'da', 'db', 'dc', 'dd,
相當於對字符串'abcd' 和 'abcd' 作笛卡爾積,那么一共是 len('abcd') * len('abcd') = 16 個密碼,
所以n位密碼的個數就是 len(meta_str) ** n
random.sample 不支持重復字符,替換為 numpy.random.choice(sq, n, replace=True)
引入 numpy 並修改 main 函數:
def main():
z = zipfile.ZipFile(r'./file.zip')
meta_str_len = len(meta_str)
meta_str_list = list(meta_str)
while True:
for n in range(1, 5):
while len(choiced_set) != meta_str_len ** n:
rand_str = ''.join(np.random.choice(meta_str_list, n, replace=True))
if rand_str not in choiced_set:
choiced_set.add(rand_str)
if guess(z, rand_str):
raise Exception('found')
choiced_set.clear()
耗時
以上代碼雖然可以正常運行,但是解一個4位長度的密碼需要計算 62 * 62 * 62 * 62 = 14776336 次,而且隨着 choiced_set 的增長,生成非重復密碼的概率越來越低,幾乎是一個不可能完成的任務。
字母順序密碼
如果按順序生成密碼,總有一個密碼能滿足條件,比如: 元字符'abcd'生成2位密碼,是一個笛卡爾積,其實就是一個字符全排列的過程,所以這里可以使用深度優先算法生成字符
調用 dfs_str(0) 即可不斷生成 4 位長度的密碼
一個簡單的栗子:
def guess(v):
if v == 'abcd':
print('guessed pwd: {}'.format(v))
return True
else:
print('invalid pwd: {}'.format(v))
return False
meta_str = 'abcd1234'
chars = []
def dfs_str(step):
if step >= 4:
if guess(''.join(chars)):
raise Exception('found')
return
for ch in meta_str:
chars.append(ch)
dfs_str(step + 1)
chars.pop()
密碼生成器
使用這種方式 guess 函數處於深度優先搜索算法中,耦合緊密,那么有沒有辦法一次生成一個密碼,然后傳給 guess 呢?當然是有的,這時候輪到生成器出場了
實現一個密碼生成器,只需要一些小的改動,在函數里增加 yield 關鍵字
def dfs_str_gen(step):
if step >= 4:
yield ''.join(chars)
return
for ch in meta_str:
chars.append(ch)
yield from dfs_str_gen(step + 1)
chars.pop()
密碼生成器類
上面的代碼已經限定了只能生成 4 位長度的密碼,然而我們的密碼可能是 1 2 3 4 5 甚至8位以上,難道每次都要手動修改嗎?或者是在 dfs_str_gen 里傳入參數?
當然傳參可以,離我的目標很近了,然而更好的辦法是包裝成一個 class, 初始化時傳入想要生成密碼的長度:
class PasswordGenerator:
meta_str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
def __init__(self, n=4):
self.maxlen = n
self.endstr = self.meta_str[-1] * self.maxlen # 標記循環結束字符串
self.chars = []
self.g = self._proxy_gen()
def _dfs_str(self, step):
if step >= self.maxlen:
yield ''.join(self.chars)
return
for ch in self.meta_str:
self.chars.append(ch)
yield from self._dfs_str(step + 1)
self.chars.pop()
def _proxy_gen(self):
while True:
yield from self._dfs_str(0)
def __iter__(self):
return self
def __next__(self):
s = self.g.send(None)
if s == self.endstr:
self.g.close()
return s
調用 PasswordGenerator(n) 即可生成 n 位長度的密碼
itertools 生成器
寫了這么多,其實標准庫 itertools 已經有了解決方案
import itertools
meta_str = 'abcdefg'
itertools.product(meta_str, repeat=4)