zipファイルのパスワード解析ソフトを自作してみた(c++)

zipファイルのパスワード解析ソフトを自作してみた - bogamp’s blog
以前pythonでzipファイルのパスワード解析ソフトを作ったが、今度はc++で作ってみた。

#include <iostream>
#include <fstream>
#include <stdio.h>
#include <zip.h>
#include <vector>
#include <string>
#include <zlib.h>
#include <cstring>
#include <memory>

std::vector<std::string> read_file(std::string filename){//パスワードリストのファイルを読み込む関数
	std::string line;
	std::vector<std::string> content;
	content.reserve(1000000);//ここは無くても良いが、行数文だけ先に領域確保しておけば少し速くなる
	std::ifstream passfile(filename);
	if(passfile.is_open()){
		while(getline(passfile,line)){
			content.push_back(line);
		}
		passfile.close();
	}
	return content;
}

long calcrc(char data[],int len){//crc32は、zipファイルの誤り検出用の数字。これが一致すればファイル内容が正しいと確認できる。
	const void* voidata = data;
	long res = crc32(0L, NULL, 0);
	res = crc32(res, static_cast<const Bytef*>(voidata), len);//libzipにはcrc32を計算できる関数は無いようなので、zlibの関数を使う
	return res;
} 

int main(int argc, char *argv[]){
	auto passlist = read_file(argv[2]);//パスワードリストのファイルを読み込む
	auto archive = zip_open(argv[1],0,NULL);//zipファイルを開く
	zip_file_t * zfile = NULL;
	try{
		auto passnum = passlist.size();

		zip_stat_t stat;
		zip_stat_init(&stat);
		zip_stat_index(archive,0,0,&stat);//すべてのファイルのパスワードが同一だと仮定して、ここではindexが0のファイルを対象にする。
		auto filesize = stat.size;//ファイルサイズ
                long filecrc = (long)stat.crc;

		auto filetext = std::make_unique<char[]>(filesize);//読み込んだファイル内容を格納するためにメモリ確保

		for(int i=0;i<passnum;++i){
			auto pass = passlist.at(i);
			zfile = zip_fopen_index_encrypted(archive,0,0,pass.c_str());//openに失敗した場合、エラーを投げるのではなくNULLを返す仕様らしい
			if(zfile != NULL){
				int res =  zip_fread(zfile,filetext.get(),filesize);//resには読み込んだ文字列のサイズを返す。よってfilesizeと同じならおそらく成功したと判定できる。
				if(res==filesize){
					if(filecrc == calcrc(filetext.get(),res)){//crc32を計算してチェック
						std::cout << "answer : " + pass << std::endl;
						break;
					}
				}
				zip_fclose(zfile);
			}
		}
		zip_close(archive);
	}catch(...){
		if(zfile != NULL){
			zip_fclose(zfile);
		}
		zip_close(archive);
	}
}

参考:https://libzip.org/documentation/
libzipとzlibを使っているので、g++でコンパイル時は-lzipと-lzをリンクオプションでつける。

間違ったパスワードのときはzip_fopen_index_encryptedが必ず失敗してくれるなら簡単なのだが、zipの使用上無理らしい(試してみると1%くらいはopenに成功してしまう)ので、ファイル内容を読み込んでチェックする必要がある。
さすがにpythonよりはずいぶん速くなった。
C++初心者なのであんまり自信はないですが、スマートポインタとvectorを使っているので、メモリリークとかは多分ないはず…

pip で一括アップグレード(Linux)

pipには一括アップグレード機能がないみたいなので、コマンドを書いてみた。

pip list --outdated | cut -f 1 -d " " | tail -n +3 | while read line; do pip install -U $line; done

pipにライブラリのリストを直接渡すと、依存関係等でエラーが起きたときにそこで止まってしまう(けっこう頻繁に起こる)。
だからループを回して一つずつライブラリの名前を渡すようにした。

sudo pythonだとimportでエラーが出る

これはsys.pathがsudoを使う場合と使わない場合とで違うかららしい(pathが通ってないのでimportでエラーが出る)。

import sys
print(sys.path)

をsudoした場合としない場合のときで比較して、通ってないpathを確認。

sys.path.append(/home/usr/.local/lib/python3.10/site-packages)
#上で確認したpathを追加する

みたいにすればimportできるようになる(毎回リセットされるので、sudoを使うソースコードの最初に書いておく)。

~/.bashrcにpythonpathを追加する方法は、sudoを使うと反映されなくなるので意味がないみたいです。

userscriptでリダイレクト

tampermonkeyというアドオンで、ユーザースクリプトが作れるということを知ったので作ってみた。
Tampermonkey – 🦊 Firefox (ja) 向け拡張機能を入手


redditを見るとき、少し動作が重く感じられるので、
teddit
というコピーサイトで代わりに見たい。redditのページを開いたとき、自動的にtedditにリダイレクトされるようにしてみる。

// ==UserScript==
// @name         teddit redirect
// @namespace    http://tampermonkey.net/
// @version      0.1
// @author       You
// @description  redirect reddit to teddit
// @match        https://www.reddit.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=mozilla.org
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
     let redir = window.location.href;
     window.location.replace(redir.replace("www.reddit.com","teddit.net"));
})();

tedditは元のredditと同じ構造になっているので、URLの最初のほうを少し変えてリダイレクトするだけで良い。

py2exeでpythonコードをexe化


追記

この記事で書いたsetup.pyを使う方法は、今後使えなくなる可能性があります(version 0.13.0.0で削除)。
公式の推奨手順は以下のリンクを参照のこと
https://github.com/py2exe/py2exe/blob/master/docs/migration.md
https://github.com/py2exe/py2exe/blob/master/docs/py2exe.freeze.md




エラーが出たので書いておきます。


windowspythonをダウンロード
ここでMicrosoftストアからダウンロードするとうまく行かないようです(自分でPATHを通せばいいんだろうけど、py2exeにPATHを通しただけではダメで、他にどこにPATHを通せばいいのか分からなかった)。

python公式ページからインストーラーをダウンロードし、インストール時に"Add Python 3.x to PATH"にチェックを入れるとうまく行きます。


・setup.pyを作成
exe化したいpyファイルと同じディレクトリに、"setup.py"という名前のファイルを作る。

from distutils.core import setup
import py2exe
#ここで、pyファイル内でimportしたモジュールもimportする必要がある

setup(console=['(exe化したいファイル).py'])
#GUIがある場合は「console=」ではなく「windows=」と書く

少しハマった点があったので書いておきました。


・実行

python setup.py py2exe

これで上手くいけば、作成されたdistフォルダ内にexeファイルがあるはずです。



なお、近々この方法は使えなくなるらしいです。
GitHub - py2exe/py2exe: Create standalone Windows programs from Python code
Using a setup.py script or the builder

Using a setup.py script with py2exe is deprecated. Please adapt your scripts to use the new freeze API. This interface will be removed in the next major release.

The build_exe (or -m py2exe) CLI was removed in version 0.13.0.0.

tkinterでGUIを作った

zipファイルのパスワード解析ソフトを自作してみた - bogamp’s blog
この前作ったpythonのコードにtkinterGUIを追加してみた。

import sys
import pyzipper
from tkinter import *
from tkinter import filedialog
from tkinter import ttk
global zipf
global listf
global caution
zipf = ""
listf = ""
def startcrack():
  flag = 0
  try:
    z = pyzipper.AESZipFile(zipf,"r",compression=pyzipper.ZIP_DEFLATED,encryption=pyzipper.WZ_AES)
  except:
	  res = "エラー"
	  answer(res)
	  sys.exit()
  with open(listf) as file:
    for line in file:
        print(line)
        try:
          z.extractall(pwd=str.encode(line.rstrip()))
          flag = 1
          break
        except:
          pass
    if flag == 1:
      ans = '成功しました。パスワードは\n' + line.rstrip() + '\nです。'
    else:
      ans = 'パスワードを発見できませんでした。'
    answer(ans)
 
def answer(ans):
	answer = Tk()
	answer.title('解析結果')
	frame_ans = ttk.Frame(answer)
	label_ans = ttk.Label(text=ans)
	frame_ans.pack()
	label_ans.pack()
	answer.mainloop()
def caut():
	global caution
	caution = Tk()
	caution.title('エラー')
	frame_caution = ttk.Frame(caution,height=20,width=30)
	label_caution = ttk.Label(text='選択されていない\nファイルがあります',foreground="#ff0000")
	button_caution = ttk.Button(text="OK",command=lambda: [caution.destroy(),main()])
	frame_caution.pack()
	label_caution.pack(side=TOP)
	button_caution.pack(side=TOP)
	caution.mainloop()

def main():
	root= Tk()
	root.title('パスワード解析')
	frame1 = ttk.Frame(root,padding = 50)
	t = StringVar()
	filebutton=ttk.Button(
	    frame1,text='解析するファイルを選ぶ',command=lambda: filebutton_clicked())
	listbutton=ttk.Button(
       frame1,
       text='パスワードリストを選ぶ',
       command=lambda: listbutton_clicked())
	button1=ttk.Button(
       frame1,
       text='実行',
       command=lambda: print(button1_clicked(root)))
	frame1.pack()
	filebutton.pack(side=TOP)
	listbutton.pack(side=TOP)
	button1.pack(side=TOP)
	root.mainloop()
	
def filebutton_clicked():
    global zipf
    zipf = filedialog.askopenfilename(initialdir='~/')
def listbutton_clicked():
	 global listf
	 listf = filedialog.askopenfilename(initialdir='~/')
def button1_clicked(root):
	if zipf == "" or listf == "":
		 root.destroy()
		 caut()
	else:
		root.destroy()
		startcrack()
		
main()

処理中に「処理中です」みたいな画面を出すには、threadingで並行処理する必要があるらしい。面倒なので無しにした。
グローバル変数に頼りすぎかも?ちゃんと作るならオブジェクト指向的な書き方をするべきなのかもしれない。

しかしGUIを作るのってけっこう面倒ですね…

zipファイルのパスワード解析ソフトを自作してみた

プログラミングの勉強として、簡単に書けそうなパスワード解析ソフトを作ってみた。

辞書攻撃をするので、パスワードリストが必要。
英語で調べるとたくさん見つかるけど、自分は
hashcat-rules-collection/README.md at main · n0kovo/hashcat-rules-collection · GitHub
にあるWordlist tests - Google Sheetsを参考にした。

日本人向けのパスワードリストは
Index of /pub/wordlists/languages/Japanese
くらいしか見つからなかった。約10万語のリストで、非日本語圏でも上位にあるようなパスワードは省かれているよう

シェルスクリプトLinux
7zipを使って復号化しているので、7zやtar.gzやrarも解析できる。ただしzipと比べると処理には時間がかかる

#!/bin/sh

file=$1
cat passwordlist.txt | while read line
do
    7z e -y "-p$line" $file > /dev/null 2>&1
    if [ "$?" -eq 0 ]
    then
	echo  "パスワードは $line です。"
        break
    fi
done


python
自前の環境だと、スピードは↑のシェルスクリプトの10倍くらい。
復号化にはpyzipper
GitHub - danifus/pyzipper: Python zipfile extensions
を使用。暗号化方式がAESではない場合は別のモジュールを使う必要があるかも

import sys
import pyzipper
args = sys.argv
file = args[1]
z = pyzipper.AESZipFile(file,"r",compression=pyzipper.ZIP_DEFLATED,encryption=pyzipper.WZ_AES)
with open('passwordlist.txt') as file:
    for line in file:
        try:
          z.extractall(pwd=str.encode(line.rstrip()))
          break
        except:
          pass
    print("パスワードは ",line.rstrip()," です。")

C++で書いたらもっと速くなるかもと思ったけど、zipのパスワードを復号化できるライブラリが分からず断念。


有名なjohn the ripper
https://github.com/openwall/john
というパスワードクラッカーでは、まずファイルからハッシュを取り出し、それとパスワードを照合するので、もっと速い解析が可能なよう(具体的にどのような仕組みになっているのかは分からなかった)。実際に試してみたところ、↑のpythonの20倍ほどのスピードが出た。


追記

↑のpythonのコードを、一行ずつファイルから読み込んで処理する方法からいったんキューに読み込む方法に変えてみたら、それだけで10倍以上のスピードになった。ここまで変わるとは…

import sys
import pyzipper
from collections import deque
args = sys.argv
comp = args[1]
WordlistFilename="wordlist"
z = pyzipper.AESZipFile(comp,"r",compression=pyzipper.ZIP_DEFLATED,encryption=pyzipper.WZ_AES)
queue=deque()
with open(WordlistFilename) as w:
	while True:
		line=w.readline()
		if not line:
			break
		queue.append(line.rstrip())
for i in range(len(queue)):
	password=queue.popleft()
	try:
		z.extractall(pwd=str.encode(password))
		print("パスワードは ",password," です。",i+1,"回の試行で成功しました。")
		break
	except:
		pass
del queue

工夫すればもっと速くなるかも?


追記2

docker+pypyで動かしたら、コード一切変えずにそのままで3倍速になった。すごい…


追記3

C++でlibzipを使って書いてみた記事。さすがに速いです。
bogamp.hatenablog.com