暗号コラム

暗号コラム

第6回 パスワード解析:総当り篇

2016.05.26
東京システムハウス

パスワードは無効です。短すぎます。

皆さんは1日何回メールを送信しますか? その内、添付ファイルを添えるのは何回ありますか?

社外の方にドキュメントや画像を送るという場面はよくあります。 その内容は、通勤中に見かけた野良猫があくびをしている写真ではなく 仕事で使うような重要なドキュメントだったり資料だったりします。 安全に送付する方法としていくつか方法はありますが、 「パスワードつきZipファイルに固めて送る」という方法がよく使われる手段ではないでしょうか。 よく「長さ8文字で半角英数で大文字小文字そして数字を1つ以上含む」といった指定がされますが、 何故このような指定をする必要があるのでしょうか。 これはパスワードつきZipファイルへの攻撃の1つである総当り攻撃を防ぐために必要です。 今回は、実際にZipファイルに総当り攻撃を行うプログラムを作成して 何故「長さ8文字で半角英数で大文字小文字そして数字を1つ以上含む」と指定がされるのかを実感してみましょう。

実際にプログラミングしてみよう

Zipのパスワード解析ツールとしてJohn the Ripperのような有名なツールもありますが、 自分の手を動かしながら総当りパスワード解析を行うプログラムを書いてみましょう。 鈴木の好きなプログラミング言語であるPythonで書きます。

まずはZIPファイルの開け閉めに関する部分です。 PythonでZipファイルを扱うzipfileモジュールがあります。 ZipFile.extractallのオプション引数pwdにパスワードをバイナリ列として渡します。 パスワードが一致しない場合は、RuntimeErrorが送出されます。

バイナリ列を渡すのは面倒なので、使いやすいように関数として書いてみます。

def extract_zip(path_to_zipfile, password=""):
    """"パスワードを入力してZIPファイルを解凍する

    引数passwordのデフォルト値はパスワードなしに該当する。
    """
    with zipfile.ZipFile(path_to_zipfile, 'r') as zf:
        zf.extractall(dest, pwd=password.encode())

PythonプログラムでZipファイルの解析ができるようになりました。

extract_zip("hoge.zip")
extract_zip("encrypted_hoge.zip", password="hoge")

のように使います。

では早速・・・と行きたいところですが、パスワードを1つ1つ手入力するのは面倒ですよね。 候補となるパスワードの生成から入力までもプログラムで自動化しましょう。

候補となるパスワードを生成する機能を追加してみましょう。 Pythonの標準モジュールであるitertoolsを使います。 itetools.productは引数として渡したイテラブルオブジェクトの直積をたどるイテレータを生成します。 キーワード引数repeatに数を渡すことでイテラブルオブジェクト自身の直積をたどるイテレータを生成します。 itertools.product("abc", repeat=2)とすると、abcの中から重複を含む2つの文字のタプルが返されます。

import itertools

for word in itertools.product("abc", repeat=2):
    print(word)

実行結果は次のようになります。

('a', 'a')
('a', 'b')
('a', 'c')
('b', 'a')
('b', 'b')
('b', 'c')
('c', 'a')
('c', 'b')
('c', 'c')

たしかに、欲しかったものが出力されています。 後は、"abc"の代わりに生成する文字列を渡せばすべてのパスワードの組み合わせが生成できそうですね。 先程のextract_zipと組み合わせてパスワードの解析を行います。

def crack_password(path_to_zipfile, target_chars, length):
    """"総当りでパスワードを入力する"""
    target_iter = itertools.chain([""], itertools.product(target_chars, repeat=length))
    for challenge in target_iter:
        try:
            password = "".join(challenge).encode()
            extract_zip(path_to_zipfile, challenge)
            print('PASS:', "".join(challenge))
            break
        except RuntimeError:
            pass

target_iterに対象となる文字からなる全パスワードをたどるイテレータを代入します。 あとは、1つ1つパスワードを入力して暗号化されたZipファイルが解凍できるかを確かめます。 パスワードが一致しない場合は、RuntimeErrorが送出されるため、try-except文でキャッチし、 次の文字を試すように仕掛けます。

使いやすくなるように、標準モジュールのargparseを使いつつちょっとしたコマンドラインツールを作ってみましょう。 ファイル名はzip_password_cracker.pyとしてみましょう。

import argparse
import itertools
import os
import string
import sys
import zipfile


def create_parser():
    """コマンドライン引数のパーサーを作成する"""
    parser = argparse.ArgumentParser(description='Password Cracker for ZIP archived file.',
                                     epilog="If you don't use optional arguments, target set is upper case and lower case (A-Za-z).")
    parser.add_argument("zipfile", action='store', help='zipfile')
    parser.add_argument("length", action='store', type=int, default=5, help='the length of password. Default value is 5.')
    parser.add_argument("-d", "--digits", action='store_true', help='append digits to target set.')
    parser.add_argument("-l", "--lower", action='store_true', help='append lower case to target set.')
    parser.add_argument("-p", "--punctuation", action='store_true', help='append punctuation to target set.')
    parser.add_argument("-u", "--upper", action='store_true', help='append upper case to target set.')
    parser.add_argument("-w", "--white", action='store_true', help='append whitespace to target set.')
    return parser


def create_target_strings(args):
    """コマンドライン引数から解析対象となる文字列を組み立てる"""
    target_chars = ""
    if not any((args.digits, args.lower, args.punctuation, args.upper, args.white)):
        target_chars = string.ascii_lowercase + string.ascii_uppercase
    else:
        if args.digits:
            target_chars += string.digits
        if args.lower:
            target_chars += string.ascii_lowercase
        if args.punctuation:
            target_chars += string.punctuation
        if args.upper:
            target_chars += string.ascii_uppercase
        if args.white:
            target_chars += string.whitespace
    return target_chars


def extract_zip(args, target_chars):
    """"パスワードを入力してZIPファイルを解凍する

    引数passwordのデフォルト値はパスワードなしに該当する。
    """
    with zipfile.ZipFile(args.zipfile, 'r') as zf:
        zf.extractall(os.path.splitext(sys.argv[1])[0], pwd=password.encode())


def crack_password(args, target_chars):
    """"総当りでパスワードを入力する"""
    target_iter = itertools.chain([""], itertools.product(target_chars, repeat=args.length))
    for challenge in target_iter:
        try:
            password = "".join(challenge).encode()
            extract_zip(args, challenge)
            print('PASS:', "".join(challenge))
            break
        except KeyboardInterrupt:
            print("KeyboardInterrupt")
            break
        except RuntimeError:
            pass


def main():
    """一連の動作を順に実行する"""

    parser = create_parser()
    args = parser.parse_args()
    target_chars = create_target_strings(args)
    crack_password(args, target_chars)


if __name__ == '__main__':
    main()

使い方は、python zip_password_cracker.py --helpとすると表示されます。

$> python zip_password_cracker.py --help
usage: zip_password_cracker.py [-h] [-d] [-l] [-p] [-u] [-w] zipfile length

Password Cracker for ZIP archived file.

positional arguments:
  zipfile            zipfile
  length             the length of password. Default value is 5.

optional arguments:
  -h, --help         show this help message and exit
  -d, --digits       append digits to target set.
  -l, --lower        append lower case to target set.
  -p, --punctuation  append punctuation to target set.
  -u, --upper        append upper case to target set.
  -w, --white        append whitespace to target set.

If you don't use optional arguments, target set is upper case and lower case (A-Za-z).

いい感じになりました。 オプションで解析対象の文字列を制御できるのはイカしていますね。

$> python zip_password_cracker.py encrypted_zipfile.zip 4

とすれば長さ4のアルファベット大文字小文字からなるパスワードを総当りで解析できます。 すばらしいですね。

...で、そのツールはパスワード解析において何のメリットがあるとお考えですか?

さあコマンドラインツールさえあれば世の中のZipファイルは俺のものだ! という野望をもし抱かせてしまったのであればまことに申し訳ありません。 総当りでパスワード解析を行えばいつかはわかりますが、問題はいつわかるのか、ということです。 確かに、4文字ぐらいだと現実的な時間で破られてしまうかもしれません。

パスワードがアルファベット小文字大文字、数字からなる場合を考えてみましょう。 使用できるは62文字あります。

  • パスワードが1文字の場合、考えられる可能性は62通りですね。
  • 2文字の場合、1文字で26通り、2文字目で26通りそれぞれ可能性があるので 62 × 62 = 3844通りです。
  • 3文字の場合、1文字目から3文字目までそれぞれにおいて62通りの可能性があるので62 × 62 × 62 = 238328通りです。

これらの例からもわかるように、パスワードの長さが長くなるほど急激に増えるのです。 文字の種類 c の 長さ n のパスワードのすべての組み合わせは cnとなります。 パスワードの組み合わせは長さに関する指数関数的に急激に増えてしまうのです。

例えば、「長さ8文字で半角英数で大文字小文字そして数字を1つ以上含む」の場合、 文字の種類は62種類、長さは短くても8となりますので 628 = 218340105584896、約200兆通りもあります。 1つのパターンを1ミリ秒でチェックできたとしても約7000年かかります。 国の一般会計予算よりも多い全ての組み合わせをすべてチェックするのは非現実です。 膨大な時間をかけて解読した結果、撮影者が通勤中に見かけた野良猫があくびをしている写真だった日には目も当てられません。

このように、全通りを試す方法は確実な方法ではありますが実行するのは難しいのです。 「長さ8文字で半角英数で大文字小文字そして数字を1つ以上含む」とすれば、総当り攻撃に対抗できます。 他のZipファイルのパスワード解析の手段はあるのか?次回はもう少し現実的な攻撃手法を検討してみます。