[Python] パスワード付きのZipファイルを無理やり開く
はじめに
ハードディスクの整理をしていたら謎の.zipファイルを発見しました。
結構な容量があるので何か大事なものだったのかもしれませんが、パスワードで保護されていて開くことが出来ませんでした。
そこでPythonと7-Zipを使い、パスワードを総当たりで無理やり開くことにしました。
環境
Python 3.10.4 (tags/v3.10.4:9d38120, Mar 23 2022, 23:13:41) [MSC v.1929 64 bit (AMD64)]
7-Zip 21.07 (x64) : Copyright (c) 1999-2021 Igor Pavlov : 2021-12-26
コード
コード
import os
import os.path
import itertools
import subprocess
import argparse
import re
import json
import Decorator
Zip = r"C:\Program Files\7-Zip\7z.exe"
ASCII = r""" !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~"""
Error1 = "Cannot open the file as archive"
class Settings:
def __init__(self, archive="", progress="", chars="", minLength=0, maxLength=0, start=0):
self.archive = archive
self.progress = progress
self.chars = chars
self.minLength = minLength
self.maxLength = maxLength
self.start = start
def joinIter(iter):
return "".join(map(str, iter))
# list(map(joinIter, itertools.product(string, repeat=n)))
def getString(i, length, chars=ASCII, cl=0):
if cl < 1:
cl = len(chars)
q = i
string = ""
for _ in itertools.repeat(None, length):
q, r = divmod(q, cl)
string += chars[r]
return string
def extract(archive, password):
# -p{password}だと "などが入力されると挙動がおかしくなることがあるので"-sccUTF-8"を設定してsubprocessのinputでパスワードを渡している
result = subprocess.run([Zip, "t", archive, "-y", "-sccUTF-8"], input=password, encoding="utf-8", stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if Error1 in result.stderr:
raise ValueError(result.stderr)
return result.returncode
def saveProgress(settings, password):
with open(settings.progress, "w", encoding="utf-8") as file:
st = settings.__dict__
del st["progress"]
st["lastWord"] = password
if st["start"] > 0:
st["start"] -= 1
json.dump(st, file, indent=2)
def loadProgress(args):
with open(args.progress, "r", encoding="utf-8") as file:
result = json.load(file)
if result["archive"] != args.archive or result["chars"] != args.chars:
error = f"""Setting is Mismatch\n{result["archive"]}\n{args.archive}\n{result["chars"]}\n{args.chars}"""
raise ValueError(error)
del result["lastWord"]
result["progress"] = args.progress
return Settings(**result)
def getArgs(args):
if args.cont and os.path.exists(args.progress):
return loadProgress(args)
else:
return Settings(args.archive, args.progress, args.chars, args.min, args.max, 0)
@Decorator.stopwatchPrintSE
def searchPassword(settings):
try:
charsLength = len(settings.chars)
for length in range(settings.minLength, settings.maxLength + 1):
end = charsLength**length
formatLength = len(str(end))
print(f"Password length={length}, trials={end}: {settings.chars} ({charsLength})")
for i in range(settings.start, end):
password = getString(i, length, settings.chars)
result = extract(settings.archive, password)
print(f"\r{i:>{formatLength+1}}: {password:>{length+1}} : ret={result}", end="")
if result == 0:
print(f"\nPassword is {password}")
return "success"
print("\n")
return "unknown"
except KeyboardInterrupt:
print("\n")
settings.minLength = length
settings.start = i
saveProgress(settings, password)
return "interrupt"
def argumentParser():
parser = argparse.ArgumentParser()
parser.add_argument("archive", help="archive file")
parser.add_argument("-c", "--cont", action="store_true", help="continue")
parser.add_argument("-p", "--progress", default="", help="path to save progress if interrupted.")
parser.add_argument("-chars", default=ASCII, help="charcters")
parser.add_argument("-min", type=int, default=1, help="minimum length")
parser.add_argument("-max", type=int, default=8, help="maximum length")
parser.add_argument("-a", "--showArgument", action="store_true", help="show arguments.")
args = parser.parse_args()
args.archive = os.path.abspath(args.archive)
if args.progress == "":
args.progress = os.path.splitext(args.archive)[0] + ".json"
else:
args.progress = os.path.abspath(args.progress)
return args
if __name__ == "__main__":
args = argumentParser()
if args.showArgument:
print(args)
if not os.path.exists(args.archive):
print(f"File not found. {args.archive}")
exit(1)
settings = getArgs(args)
result = searchPassword(settings)
if result != "interrupt" and os.path.exists(args.progress):
os.remove(args.progress)
パスワードを試すと言うよりかZipファイルを開ける為に、7-Zipをsubprocess
経由で呼び出しています。
7-Zipにコマンドラインからパスワードを渡すのに-p
コマンドオプションがあります。
-p
経由で渡すと、"
などが含まれているパスワードを上手く渡せない事があります。
その為、以下のようにパスワードを標準入力(input
)経由で渡しています。
そして、念のために7-Zipにコンソールからの入力をUTF-8
として認識させるように、
-sccUTF-8
コマンドオプションを設定しています。
subprocess.run([Zip, "t", archive, "-y", "-sccUTF-8"], input=password, encoding="utf-8", stdout=subprocess.PIPE, stderr=subprocess.PIP)
簡単な使い方
コードのファイル名を"openZip.py"とした場合は、
py openZip.py "archiveFileName.zip"
など実行すると時間がかかりますがZipファイルを開くことが出来ます。
残念ながら、庭に草が生えるぐらい遅いのでCtrl-C
(KeyboardInterrupt
)で中断した場合は、そこから再開できるようにしています。
-p
で中断ファイルのパスを指定していない場合は、自動でarchiveFileName.json
とアーカイブファイル名と同じJSONファイルが作成されます。
自動で作成されたファイルを使って再開したい場合は、
py openZip.py "archiveFileName.zip" -c
とすれば、再開できます。
明示的に中断ファイルを指定したければ、
py openZip.py "archiveFileName.zip" -p "archiveFileName.json" -c
などとします。
-chars
オプションは、パスワードに含まれる(試行する)文字を制限したい場合に指定します。
例えば、
-chars "abcdefghijklmnopqrstuvwxyz"
とするとパスワードとして小文字のアルファベットの組み合わせしか試しません。
-min
オプションは、試行するパスワードの最小文字数です。
-max
オプションは、試行するパスワードの最大文字数です。
例えば、3~4文字の間を試したい場合は、
py openZip.py "archiveFileName.zip" -min 3 -max 4
と指定します
参考
余談
PyPyとCPythonの実行速度比較
PyPyを使ったことがなかったので、このコードを使って雑に速度比較をしてみました。
PyPy: 1118.677 sec.
CPython: 503.840 sec.
と圧倒的にCPythonの方が高速でした。
PyPyで明らかに処理が速くなるものも有るんですがね。
軽く検証した所、PyPyが遅い一番の原因は、subprocess
の実行が原因のようです。
subprocess
の呼び出しをしないテストコードを書いてみると、両者ともに圧倒的に速くなり、若干PyPyが遅いぐらいになります。
つまり、PyPyは、subprocess
の実行に倍ぐらい時間が掛かるようです。
一般的にPyPyは、関数呼び出しが遅いといわれているようです。
関数呼び出しを無くすと両者ともに速くなるのですが、PyPyの方がより速くなりました。
結局、このコードを高速化するには、処理の肝であるsubprocess
を何とかするしかないようです。
マルチスレッドやマルチプロセスに対応させるのが良いと思いますが、それはまた別の機会があれば試してみるつもりです。
searchPassword: 21:49:58
Password length=1, trials=26: abcdefghijklmnopqrstuvwxyz (26)
25: z : ret=2
Password length=2, trials=676: abcdefghijklmnopqrstuvwxyz (26)
675: zz : ret=2
Password length=3, trials=17576: abcdefghijklmnopqrstuvwxyz (26)
12846: cat : ret=0
Password is cat
searchPassword: 22:08:36(1118.677 sec.)
searchPassword: 22:45:01
Password length=1, trials=26: abcdefghijklmnopqrstuvwxyz (26)
25: z : ret=2
Password length=2, trials=676: abcdefghijklmnopqrstuvwxyz (26)
675: zz : ret=2
Password length=3, trials=17576: abcdefghijklmnopqrstuvwxyz (26)
12846: cat : ret=0
Password is cat
searchPassword: 22:53:25(503.840 sec.)
別のモジュールを試してみる
subprocess
が遅いのならば、モジュールを使って改善できないかと検証してみました。
zipfile
Pythonの標準ライブラリです。
".zip"でなおかつ暗号化方式が"ZipCrypto"がならばこれで十分でした。
速度もびっくりするぐらい速いです。
(というよりsubprocess
が遅すぎるのでしょうが)
下記の様に、extract()
を置き換えれば動作します。
ただし、以下の欠点で採用しませんでした。
欠点は、".7z"などに対応しないことと暗号化形式が"AES-256"ならば開けない事です。
そして、zipfile
の使い方を何か間違っている可能性がありますが、設定したパスワード以外で開けてしまう事がある事です。
これは、圧縮した時に、空のファイルを圧縮したのが原因かもしれませんが
import zipfile
def extract(path, password):
with zipfile.ZipFile(path) as archive:
try:
archive.extractall(pwd=password.encode("utf-8"))
return 0
except Exception as e:
# print(e)
return 1
py7zr
検索して出てきたので試してみましたが、適当に使った限りだとsubprocess
よりも遅くなりました。
また、パスワードを試しているうちに例外が発生してしまいます。
ドキュメント通りに使っているはずなのですが、原因が不明です。
原因を特定できれば良かったのですが、遅くなるのでは意味がないので採用を見送りました。
そもそも開くだけならばこれで十分なのですが、今回の要件とは合いませんでした。
".7z"などに対応できるのは、魅力的なんですがね。
patool
ソースを見た限り内部でsubprocess
で7zipを呼んでいるようなので採用はしませんでした。
rarfile
".rar"用で".zip"では使えないようなので、不採用です。
Discussion