見出し画像

VBAのパスワードクラッキングで学ぶ共通鍵暗号とハッシュ関数

.xls .xlsm, .xla, .xlamのパスワードを外す手法は古くから周知の事実として受け止められている。しかし、外すだけならもっと簡単な方法があるが、せっかくなのであえてパスワードをクラッキングすることで共通鍵暗号とハッシュ関数について学ぼう。 

まず、xlsやxlsmをzipしたxlディレクトリ下のvbaProject.binファイルをhex editorで開くとDPB=ではじまる文字列が見つかる。
残念ながら私はWindowsに対してアレルギー反応を持っているためExcelを持っておらず、かわりに下のブログの値をそのまま使う事にする。
https://medium.com/walmartglobaltech/vba-project-locked-project-is-unviewable-4d6a0b2e7cac

DPB="94963888C84FE54FE5B01B50E59251526FE67A1CC76C84ED0DAD653FD058F324BFD9D38DED37"

まず、これに何が書かれているか見る事にしよう。そのためにまずXORビット演算について学ぶ必要がある。
XORはビット演算の一種で

1 XOR 1 = 0
1 XOR 0 = 1
0 XOR 1 = 1
0 XOR 0 = 0

上記の様に定義される。
XOR (以下より^で表す)の特徴として

A^A = 0
A^0 = A
A^B = B^A
(A^B)^C = A^(B^C)

の様な性質を持っている。
これはつまりByteというデータをKeyという「鍵」で暗号化と復号化をする時

Byte^Key = Byte_Encrypted
Byte_Encrypted^Key = (Byte^Key)^Key 
                           = Byte^(Key^Key) 
                           = Byte^0 
                           = Byte

と"同じ"鍵を使って暗号化と復号化を非常に高速で行う事ができ、送られるデータを逐次暗号化する様なストリーミングに非常によく使われる。(ストリームサイファーと呼ぶ。対してデータを一括して暗号化する物をブロックサイファーと呼ぶ。)

では、実際にvbaの暗号化された情報を復号化してみよう。
まず1バイト目の0x94は暗号化されていないシード値でこの値を使って2バイト目と3バイト目が暗号化されている。 
とりあえずここでは
Byte[0] = Seed = 0x94
と置く。
2バイト目と3バイト目の値はそれぞれ0x96と0x38でそれぞれSeedの値を使って暗号化されたバージョンとプロジェクトキーである。つまり0x94^Byteでそれぞれ復号化でき、バージョン=0x02, プロジェクトキー=0xACとわかる。

Byte_Enc[1] = Version_Enc = Seed^Version = 0x96
Byte[1] = Version = Seed^Version_Enc = 0x94^0x96 = 0x02
Byte_Enc[2] = ProjectKey_Enc = Seed^ProjectKey = 0x38
Byte[2] = ProjectKey = Seed^ProjectKey_Enc = 0x94^0x38 = 0xAC

そして、それ以降は以下のpseudo codeの様に順次暗号化されている。

for(i=3; i<len(DPB); i++){
        Byte_Enc[i] = Byte[i]^((Byte_Enc[i-2]+Byte[i-1]) AND 0xFF)
}

少々複雑に見えるが全くそんなことはない。ここではつまり
((Byte_Enc[i-2]+Byte[i-1]) AND 0xFF)
という鍵を使って1 byteずつ暗号化していってるだけである。そして、そのkeyは一つ前のbyteと二つ前に暗号化されたbyteを足し合わせているだけである。(AND 0xFFの部分は単に足し合わせた数が1 byteを超えない様に制限しているだけである。)
つまり、復号化する時、暗号化されたByte_Encと算出したkeyのXORを演算してやれば良い。つまり

for(i=3; i<len(DPB); i++){
        Byte[i] = Byte_Enc[i]^((Byte_Enc[i-2]+Byte[i-1]) AND 0xFF)
}

 となる。4バイト目を計算する時、幸いな事に二つ前に暗号化された値と一つ前に復号化した値が分かっているので、

i = 3
key = ((Byte_Enc[1]+Byte[2]) AND 0xFF) 
Byte[2] = ProjectKey = 0xAC
Byte_Enc[1] = Version_Enc = 0x96
key = (0xAC+0x96) AND 0xFF = 0x42
Byte[3] = 0x88^0x42 = 0xCA

となる。そしてそのまま5バイト目、6バイト目と繰り返していく。(データの長さの部分だけlittle endianになっているが、とりあえず割愛する。)
このまま手動で続けるわけにもいかないので10分ほどでpythonでスクリプトを書いて上の値を打ち込んでみたら以下の情報が得られた。

encrypted value: 94963888C84FE54FE5B01B50E59251526FE67A1CC76C84ED0DAD653FD058F324BFD9D38DED37
seed : 94
version : 02
project_key : AC
Ignored: CACA
len: 0000001D
data: FFFFFFFFDD9377A71FF4C687CF12931AAAD24075EC4F83C9342074AA00
unicode: ÿÿÿÿÝw§ôÆϪÒ@uìOÉ4

ここで重要なのは
data: FFFFFFFFDD9377A71FF4C687CF12931AAAD24075EC4F83C9342074AA00
である。しかし、このhexの値を文字列に変えても
unicode: ÿÿÿÿÝw§ôÆϪÒ@uìOÉ4 tª
と意味のない文字の羅列になる。
では次に、このデータをパスワードに変換するためにハッシュ関数を見ていこう。

ハッシュ関数とは一方通行の関数で入力の長さに関わらず、必ず決まった長さのアウトプット(ハッシュ値)を返す関数である。
実際の例を見ていこう。まず、'a'という一文字と文章をSHA-1 (Secure Hash Algorithm)に入れた出力が以下である。

>>> sha1('a')
'86F7E437FAA5A7FCE15D1DDCB9EAEAEA377667B8'

>>> sha1('Common sense is the collection of prejudices acquired by age eighteen.')
'93BB5C12846BF1FC853082C3A47F3FB52711ACCA'

SHA-1は入力したものの長さに関わらず、必ず160 bit (20 byte)のデータを返す。
そして、少しでも違う値を入れると全く違ったデータが帰ってくる。これによってハッシュ値から元のデータを求める事は困難を極める。

>>> sha1('make America great again')
'2F349190BFE7C7132A1254A04D68555A42627484'

>>> sha1('Make America great again')
'7E8B96692624320011297BF7D9427EBA1C6ECAF9'

つまり、これは復号化して使うものではなく入力した値をハッシュ化して、保存されているハッシュと比べるために使う。身近なものだとログインパスワードやブロックチェーンで使われている。
では、先ほどの
data: FFFFFFFFDD9377A71FF4C687CF12931AAAD24075EC4F83C9342074AA00
に戻ろう。
まず最初の1バイトは単にパスワードで保護されているか否かなので無視しできる。そして、それ以降の3バイト(24 bit)
Grbit = 0xFFFFFF = 11111111 11111111 11111111
は、それぞれのビットそれ以降の24バイト
DD9377A71FF4C687CF12931AAAD24075EC4F83C9342074AA (ちなみに最後の1倍とはここまでというnullなので無視する)
がそのまま使われるか0x00になるかのフラグである。この場合、全て1なので全てそのまま使われる。
そして、その初めの4バイトが
salt = DD9377A7
といって平文のパスワードに付け足す事によって同じパスワードでもこの値によってハッシュ値が変わる様にするためのものだ。
そして最後の20バイト
1FF4C687CF12931AAAD24075EC4F83C9342074AA
これがパスワードのハッシュである。
お気づきだろうか。20バイトということは160 bit, つまりこのパスワードハッシュはSHA-1でハッシュ値が出されている。つまり

SHA-1(Password+salt) = 1FF4C687CF12931AAAD24075EC4F83C9342074AA

では、実際にこのパスワードをクラッキングしてみよう。
ちょっと待て、という読者もいるかもしれない。先ほど私は「ハッシュ値から元の値を求めることはできない」といったはずだ。ではどの様に求めるのか。答えは単純で総当たりで試せばいいだけである。

とりあえず今回はこれをJohn The Ripperに入れてみよう。

画像1

1秒とかからずパスワード=testと判明した。
パスワードを作る時、長くしろとか数字や記号を入れる様に推奨されるのはこのためである。

とりあえず、今回あえて簡単にクラックできる物を選んだが、本来セキュリティはもっと堅牢であるのでそこまで怖がらなくて良い。長くなったが、これを通して暗号やセキュリティに対する興味や理解が深まれば幸いだと思う。

この記事が気に入ったらサポートをしてみませんか?