乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

パスワード

今回はZIPとかPDFのパスワードを
総当たりで開けに行くやつのご紹介です

解説動画はこちら




ファイルにパスワード掛かっていて開けられないよーーー
どうにかならない?

そんな時ありますよね!!!!

そんな時はプログラムで突破すればいい

今回は
ZIPやPDFのパスワードを破るプログラム
についての解説です。


 
ライブラリのインストール

Google Colab上で
ZIPやPDFを扱うのに必要な
ライブラリのインストールです。
pip install pyzipper
pip install pypdf
pip install reportlab



パスワード付きファイルの用意

パスワード付きのZIP , PDFファイルを
作成しておきます。

今回のパスワードは
a12
として
PDFファイルや
ZIP用のファイルを先に作っておきます。

lock_password = "a12"

from reportlab.pdfgen import canvas
from PIL import Image

# PDFを作成
pdf_filename = "original.pdf"
c = canvas.Canvas(pdf_filename)
c.drawString(100, 750, "Hello, this is a sample PDF file.")
c.drawString(100, 730, "This PDF was created using Python and reportlab.")
c.save()

# ZIP用の内包ファイルを用意
with open("secret.txt","w") as _w:
    _w.write("This is a secret message")

Image.new("RGB", (100, 100), (255, 255, 255)).save("image.png")


パスワード付きのZIPファイルを作るコード
import pyzipper

# パスワード付きzipファイルを作成する
def create_protected_zip(file_list, zip_name, password):
    with pyzipper.AESZipFile(zip_name, 'w', compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES) as zf:
        zf.setpassword(password.encode("utf-8"))
        for file in file_list:
            zf.write(file)
    print(f"[+] パスワード付きZIPを作成: {zip_name}")

create_protected_zip(["secret.txt", "image.png"], "protected.zip", lock_password)


PDFにパスワードをかけるコード
from pypdf import PdfReader, PdfWriter

# パスワード付きpdfファイルを作成する
def create_protected_pdf(input_pdf, output_pdf, password):
    reader = PdfReader(input_pdf)
    writer = PdfWriter()
    
    for page in reader.pages:
        writer.add_page(page)

    writer.encrypt(password)  # パスワードを設定
    with open(output_pdf, "wb") as f:
        writer.write(f)
    print(f"[+] パスワード付きPDFを作成: {output_pdf}")

create_protected_pdf("original.pdf", "protected.pdf", lock_password)




ZIPとPDFファイルのパスワードを破るコード


ZIPとPDFに対応
総当たりでパスワードを当てにいくものです
一応辞書にも対応しています。
import itertools
import string
import zipfile
import pyzipper
from pypdf import PdfReader

def extract_zip(file_path, password):
    """ ZIPファイルの解凍 (ZIPCrypto & AES-256対応) """
    password_bytes = password.encode("utf-8")  # バイト列に変換
    try:
        with zipfile.ZipFile(file_path) as zf:
            zf.extractall(pwd=password_bytes)
        print(f"[+] ZIP のパスワード発見: {password}")
        return True
    except (RuntimeError, NotImplementedError):
        pass

    try:
        with pyzipper.AESZipFile(file_path) as zf:
            zf.setpassword(password_bytes)
            zf.extractall()
        print(f"[+] PDF のパスワード発見: {password}")
        return True
    except Exception as e:
        return False

def try_password(file_path, file_type, password):
    """ ZIP または PDF のパスワードを試す """
    try:
        if file_type == "zip":
            return extract_zip(file_path, password)
        elif file_type == "pdf":
            reader = PdfReader(file_path)
            if reader.decrypt(password) == 0:
                raise ValueError("Incorrect password")
            print(f"[+] PDF のパスワード発見: {password}")
            return True
    except Exception:
        return False

def password_cracker(file_path, mode="brute", password_list=None, max_length=4):
    """ ZIP/PDFファイルのパスワードを解析 """
    if file_path.endswith(".zip"):
        file_type = "zip"
    elif file_path.endswith(".pdf"):
        file_type = "pdf"
    else:
        print("[-] サポートされていないファイル形式です")
        return

    print(f"[*] {file_path} のパスワード解析開始 (モード: {mode})")

    count = 0

    # **辞書攻撃**
    if mode == "dictionary" and password_list:
        with open(password_list, "r", encoding="utf-8") as file:
            for password in file:
                count += 1
                if try_password(file_path, file_type, password.strip()):
                    print(f"[+] 試行回数: {count}")
                    return

    # **総当たり攻撃**
    elif mode == "brute":
        characters = string.ascii_lowercase + string.digits  # "abcdefghijklmnopqrstuvwxyz0123456789"
        for length in range(1, max_length + 1):
            for password in itertools.product(characters, repeat=length):
                count += 1
                if try_password(file_path, file_type, "".join(password)):
                    print(f"[+] 総当たり攻撃試行回数: {count}")
                    return

    print("[-] パスワードが見つかりませんでした")

使い方は
# 実行例(ZIP)
password_cracker("protected.zip", mode="brute", max_length=3)
[*] protected.zip のパスワード解析開始 (モード: brute)
[+] PDF のパスワード発見: a12
[+] 総当たり攻撃試行回数: 2333

こんな感じで、開けることができます。

max_length
を増やせば、大きな桁数のパスワードにも対応

文字種を増やしたい場合は
コード上の変数 characters
これに文字列を追加してください。




おまけ


文字種と文字数による
パスワードの強度についてです。

今回は文字種の数が 36種類
パスワード文字数が3桁でした

文字種36個の時の
パスワード桁数による最大試行回数は
以下のようになります。


パスワードの長さによる最大試行回数 : 文字数 01 :  0000000000000036
パスワードの長さによる最大試行回数 : 文字数 02 :  0000000000001296
パスワードの長さによる最大試行回数 : 文字数 03 :  0000000000046656
パスワードの長さによる最大試行回数 : 文字数 04 :  0000000001679616
パスワードの長さによる最大試行回数 : 文字数 05 :  0000000060466176
パスワードの長さによる最大試行回数 : 文字数 06 :  0000002176782336
パスワードの長さによる最大試行回数 : 文字数 07 :  0000078364164096
パスワードの長さによる最大試行回数 : 文字数 08 :  0002821109907456
パスワードの長さによる最大試行回数 : 文字数 09 :  0101559956668416
パスワードの長さによる最大試行回数 : 文字数 10 :  3656158440062976



一般的なPCであれば7-8桁くらいまでなら
開けられそうですね


これ以上となると
少し工夫が必要になってきます。

まあ、会社で使うパスワードは
6桁くらいまでに抑えておいた方が
いざというとき開けられなくなるんで
いいかもしれません


今回はパスワードが開けられなくて困った際の
パスワード開けプログラムについてでした

今日はここまでです
それでは。

先日、個人情報の入った
USBメモリの紛失事件が有りました

パスワードが気になっちゃって仕方ないです


解説動画はこちら



さて先日起きた事件ですが
尼崎市が、市民分約46万人分の個人情報が入った
USBメモリを紛失し
個人情報流出の恐れがあるという問題です。


会見では、パスワードについて聞かれたときに
「英数字13桁のパスワードを設定している、
解読するのは難しいのかなと考えている」等と
返答してしまいました。

USBメモリには
全市民の住民基本台帳データ46万517件
住民税に関する情報36万573件
生活保護や児童手当受給世帯の銀行口座情報合計7万6026件
非課税世帯等臨時特別給付金の対象世帯情報合計8万2716件
を保存していたそうです。


それでは「英数字13桁のパスワード」が
何通り有るのかを考えてみましょう。

文字種を用意してみよう
英小文字 aからzまでの26文字
英大文字 AからZまでの26文字
数字   0から9までの10文字
Python言語ではstringライブラリで用意できます。
import string

suji = string.digits
komoji = string.ascii_lowercase
omoji = string.ascii_uppercase
eiji = komoji + omoji
eisuji = suji + eiji
kigou = string.punctuation

print(suji)
print(komoji)
print(omoji)
print(eiji)
print(eisuji)
print(kigou)
0123456789
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
0123456789abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~


英数文字を含めた13桁のパスワードは
何通りでしょうか、計算してみましょう。

パターン1:
英文字が小文字だけor大文字だけ使用されている場合

l1 = len(komoji + suji)

print(l1)
print('{:,}'.format(l1 ** 13))
print('{:e}'.format(l1 ** 13))
36
170,581,728,179,578,208,256
1.705817e+20


パターン2:
英文字が大文字も小文字も使用されている場合

l2 = len(eisuji)

print(l2)
print('{:,}'.format(l2 ** 13))
print('{:e}'.format(l2 ** 13))
62
200,028,539,268,669,788,905,472
2.000285e+23


どちらも桁が多すぎてピンとこないですね

1垓7058京1728兆1795億7820万8256
2000垓2853京9268兆6697億8890万5472

という感じになりました。

英数字13桁のパターンは
2000垓通り以上になります。


もしこれが仮に1秒間1兆回攻撃出来たとしたら
どれくらいで解けるでしょうか
計算してみましょう。


パターン1:
英文字が小文字だけor大文字だけ使用されている場合

l1 = len(komoji + suji)
times = l1 ** 13 // 1000000000000

print('{:,}秒'.format(times))
print('{:,}分'.format(times//60))
print('{:,}時間'.format(times//60//60))
print('{:,}日'.format(times//60//60//24))
print('{:,}年'.format(times//60//60//24//365))
170,581,728秒
2,843,028分
47,383時間
1,974日
5年


パターン2:
英文字が大文字も小文字も使用されている場合

l2 = len(eisuji)
times = l2 ** 13 // 1000000000000

print('{:,}秒'.format(times))
print('{:,}分'.format(times//60))
print('{:,}時間'.format(times//60//60))
print('{:,}日'.format(times/60//60//24))
print('{:,}年'.format(times//60//60//24//365))
200,028,539,268秒
3,333,808,987分
55,563,483時間
2,315,145.0日
6,342年


英数字13桁のパターン
2000垓通りを1秒間1兆回で
ブルートフォースアタックすると
6342年ほどで解読できる可能性があります。

ここからは、秒間1兆回で
色々な文字種と文字数で
どれくらい時間がかかるのか
計算してみましょう。


import pandas as pd
columns = ['数字のみ','英小文字のみ','英小数字','英字','英数字','英数字記号']

l1 = len(suji)
l2 = len(komoji)
l3 = len(komoji + suji)
l4 = len(eiji)
l5 = len(eiji + suji)
l6 = len(eiji + suji + kigou)
tmp_l = [l1,l2,l3,l4,l5,l6]
print(l1,l2,l3,l4,l5,l6)

df = pd.DataFrame(data = [tmp_l],columns=columns,index=['文字数'])

at = 10000 ** 3
day = 60*60*24

for i in range(6,16):
    tmp = []
    for l in tmp_l:
        a = l ** i // at //day
        if a>=365:
            b = '{0}年'.format(a//365)
        else:
            b = '{0}日'.format(a)
        tmp.append(b)
    tmp_df = pd.DataFrame(data = [tmp],columns=columns,index=['{0}文字'.format(i)])
    df = pd.concat([df,tmp_df],axis=0)

df
スクリーンショット 2022-07-02 16.54.36

こんな感じになりました。

やはり記号を用いると
グンと安全性が高まりますね

このUSBメモリであれば
6400年ほどは破られない可能性がありますが
辞書を用いた攻撃の場合
これよりももっと早く
解かれてしまう可能性があるので
油断は禁物ですね

パスワードを設定する場合は
文字種別も記号まで使い
16文字以上など桁数も多くしておくことを
おススメしたいと思います。

今回はここまでです
それでは。

今回はブルートフォースアタックに挑戦です。

解説動画はこちら



ブルートフォースアタックを知らない方のために
手口を紹介すると
総当たりでパスワードの文字列を生成して
突破を試みる方法です。

この手のプログラムは
慣れれば小学生でも行けちゃうやつです。

早速コードを作ってみましょう。

まずはパスワード用の文字列を
用意するところからです。

stringライブラリを用いると
沢山文字を入力しなくても
文字列を用意する事ができます。
import string

chars = string.ascii_letters + string.digits

print(chars)
print(len(chars))
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
62


続いて記号も組み合わせてみます。
import string

chars = string.ascii_letters + string.digits
chars += '/*-+.,!#$%&()~|_'

print(chars)
print(len(chars))
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/*-+.,!#$%&()~|_
78

記号まで入れると結構な文字種になりますね。

この文字列を使ってランダムでパスワードを
生成してみましょう。

randomライブラリを用いる方法と
secretsライブラリを用いる方法があります。

import string,random

size = 10
chars = string.ascii_letters + string.digits
chars += '/*-+.,!#$%&()~|_'
password = ''.join(random.choices(chars,k=size))

print(password)
DSyzlv.ec3
import string,secrets

size = 10
chars = string.ascii_letters + string.digits
chars += '/*-+.,!#$%&()~|_'
password = ''.join(secrets.choice(chars) for i in range(size))

print(password)

Axu_#,OXf!


ランダムではなく総当たり式で
パスワードを生成するには
itertoolsのproductでデカルト積を作ります。

ここでは文字種が多いので
数字だけでやっていますが
記号含めの文字列でやることもできます。

文字種が多いほど莫大な
組み合わせになります。

import string,random,itertools

size = 3
chars = string.digits

for ch in itertools.product(chars,repeat=size):
    password = ''.join(ch)
    print(password)
・・・
399
400
401
402
・・・


さて、パスワードが用意できたところで
ZIPファイルのパスワードを
総当たりで破ってみましょう。

あらかじめZIPファイルは生成しておくこととして
パスワードは「otupy」にしておきました。

文字種が多いと組み合わせが多すぎて
実験は大変すぎるので
文字は8種類、文字数5文字の
パスワード破りを試します。

import zipfile

file_path = 'data/otupy.txt.zip'
size = 5
chars = 'abcotupy'
count = 0

with zipfile.ZipFile(file_path , 'r') as zf:
    for i in range(100000):
        # パスワードはバイト型で有る必要がある
        pwd = bytes(''.join(random.choices(chars,k=size)),'UTF-8')
        try:
            zf.extractall(path='.', pwd=pwd)
            print('success : password  : {}'.format(pwd))
            break
        except Exception as e:
            count +=1

print('tried passwords : ' , count)
success : password  : b'otupy'
tried passwords :  36993

だいたい、数万回の試行で
パスワードが破れるようです。

この文字種と文字数なので
これだけの時間で終わりますが


78種類
10文字のパスワードだと
8335775831236199424通りも
パスワード候補が有るので
滅多に破られることはありません。

あらかじめ辞書を用意して置いたり
文字種を限定しておくことで
ロック解除する側は探索時間を
短くする事ができます。

逆に対処する側は
パスワードの文字数は最低でも8文字
文字種の組み合わせも70種類以上にしておけば
候補は576480100000000通りになるので
破られにくくなります。

ただし、攻撃する側も
サーバーを複数台用意したり
マルチコアに対応するコードを書く事で
1秒あたりのアタック回数を増やす事が
できてしまいます。

そこで対処する側では
短時間でのパスワード突破失敗を検知したり
何回か失敗したらロックをかけるような
機構を組み込んでおく必要があります。

日頃からこういった攻撃に対する
防衛策は講じておく必要がありますね。

ZIPのパスワードくらいであれば
忘れてしまっても
総当たりで何日か放置しておけば
破れると思いますので
気になる方は試してみてください。

本日はここまでです
それでは。

このページのトップヘ