NFLabs. エンジニアブログ

セキュリティやソフトウェア開発に関する情報を発信する技術者向けのブログです。

macOSの暗号化zipの話の続き

はじめに

こんにちは。事業推進部でOffensive Teamを担当する永井です。

今回はアドベントカレンダーの11日目として、前回投稿した「macOSの暗号化zipファイルはパスワードなしで解凍できる」という記事に寄せられたコメントのうち、特筆すべきものをピックアップして回答していきます。

前回の記事を読んでいない方や、もう覚えてないという方は是非前回の記事を見てから続きを読んでいただければと思います。

Q. 正解するまでbkcrackを回さなくてもzip内のCRC32値と比較すれば良いのでは?

はい、その通りです。

筆者が前回の記事を書いている時には完全に失念していましたが、zip内にはファイル破損を検出するためにCRC32形式のハッシュ値が含まれています。そのため、bkcrackを正解パターンを引くまで都度回さなくても簡単に正解の.DS_Storeを見つけ出すことができます。

実際に試してみます。

まず、検証用に.DS_Storeを意図的に暗号化zipの中に入れ込み、zipinfoコマンドを使用して.DS_StoreのCRC32値を取得します。

$ zipinfo -v abc.zip .DS_Store | grep CRC
  32-bit CRC value (hex):                         fc44dc14

上記コマンドにより、サンプルzipファイル内の.DS_StoreのCRC32値はfc44dc14であることがわかります。

次に、暗号化zip内の.DS_Storeと、同じ名前のファイル (1個) を管理する.DS_Storeを用意します。(区別のためtest_storeと表記します)

そして以下のようなスクリプトを使用し、test_store内のX座標とY座標を動かしながら正解のCRC32値と一致するX座標とY座標を割り出します。
(.DS_Storeでは座標情報はIlocblob\x00\x00\x00\x10の後に存在しています)

import sys
import struct
import binascii

CHECK_BIT_NUM = 8

def calc_crc32(check):
    crc32 = (binascii.crc32(check) & 0xFFFFFFFF)
    return "%08x" % crc32

def find_pos(data, crc32):
    beacon = b"Ilocblob\x00\x00\x00\x10"
    split_data = data.split(beacon)
    first_block = split_data[0] + beacon
    last_block = split_data[1][8:]
    for y in range(2**CHECK_BIT_NUM):
        for x in range(2**CHECK_BIT_NUM):
            check_str = b"".join([first_block, struct.pack(">I", x), struct.pack(">I", y) + last_block])
            if crc32 == calc_crc32(check_str):
                print(f"Found: x={hex(x)} y={hex(y)}")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print(f"Usage: {sys.argv[0]} ds_store_file crc32")
        exit()
    with open(sys.argv[1], "rb") as f:
        s = f.read()
        find_pos(s, sys.argv[2])

上記を動かすと以下のように正解のCRC32値にマッチする座標情報を出力することができます。

$ python3 detect_pos.py test_store fc44dc14
Found: x=0x4a y=0x2e

後はtest_storeのX座標とY座標を上記の座標に書き換えれば、暗号化zip内の.DS_Storeと同じ内容のファイルが出来上がるため、既知平文攻撃を利用して暗号化zipを解凍することができます。

CRC32値の比較による今回の手法は、座標1つごとにbkcrackを回す前回の手法よりも高速であり、筆者の環境 (前回の記事と同じ) では65536パターンを総当たりするのに要した時間は0.1秒未満でした。

暗号化zipを解凍するには正解の.DS_Storeを取得した後にbkcrackを1回実行する必要がありますが、それでも (筆者環境のスペックでは) 最大85秒程度で平文ファイルを取得することができます。

前回の手法では最大で64日かかっていたことを考えると、圧倒的な速度です。

.DS_Storeが管理するファイルが2個の場合

ここまで早いのであれば、.DS_Storeが管理するファイルが2個のパターンでも現実的な時間で既知平文攻撃および暗号化zipの解凍を行うことができます。

.DS_Storeが管理するファイルが複数ある場合、.DS_Store内では管理するファイルの情報が名前順で格納されるようになっており、各ファイルの位置情報もそれぞれ格納されることになります。
また、条件は不明ですが、.DS_Storeが管理するファイルが複数ある場合は、Y座標の後の7〜8バイト目が00 00になる時とFF FFになる時の2パターンが存在するようです。

以下の画像は2つのディレクトリにfirst.txtとsecond.txtファイルをそれぞれ設置した際の.DS_Storeを比較したものですが、X座標とY座標の違いだけでなく、second.txtのY座標の後の7〜8バイト目が00 00FF FFで異なっています。

f:id:nfl_n2:20211122133845p:plain
.DS_Storeファイルの比較

すなわち.DS_Storeがファイルを2個管理しており、かつ管理するファイル名が同一の場合の.DS_Storeのパターン数は以下になります。

(28 * 28 * 2) * (28 * 28 * 2) = 234 = 17179869184

よって234パターン分の.DS_Storeを使ってzipファイルに埋め込まれているCRC32値との比較を行えば、正解の.DS_Storeを見つけ出すことができます。

234という数は一見膨大なように思えますが、CRC32値の比較は非常に高速であるため、筆者環境では約240分で全パターン分の比較を行うことができました。

すなわち、暗号化zip内の.DS_Storeがファイルを2個管理している場合 (≒暗号化zip内に.DS_Storeとファイルが2個だけのフォルダがある場合) でも、寝て起きたら終わっている程度の速度感で暗号化zipの解凍を行うことができます。

Q. 解凍できる条件には「zip内に含まれるファイル名を知っていること」が必要なのでは?

いいえ。前提条件としては必要ありません。

暗号化zip内のファイル名は暗号化されないため、中にどのような名前のファイルが含まれるかはパスワードを知らなくても確認することができます。

確認するためには様々な手法がありますが、例えばunzipコマンドを使用した場合は以下のように確認することができます。

$ unzip -l poc.zip           
Archive:  poc.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
     6148  11-21-2021 22:31   .DS_Store
        5  11-21-2021 22:30   first.txt
       15  11-21-2021 22:30   second.txt
---------                     -------
     6168                     3 files

ただし、.DS_Storeの状態は完全にリアルタイムにディレクトリの状態を反映しているわけではないため、反映が完了する前に暗号化zipが作られた場合、暗号化zip内のファイル構成と.DS_Storeが管理するファイル名が一致しない場合がありえます。

Q. .DS_Storeなんて使わなくてもファイルヘッダを使えば既知平文攻撃できるのでは?

基本的にはNoです。

既知平文攻撃に使用する平文とは、正確には「パスワードなしで圧縮した際の圧縮後のバイト列」であるため、圧縮前のファイルのヘッダを知っていたとしても基本的には既知平文攻撃に利用することはできません。

ファイルヘッダを既知平文攻撃に利用できるのは、圧縮率が0%の無圧縮暗号化zipを作成した場合に限られるため、特定のシチュエーションを除けばファイルヘッダを使って既知平文攻撃を行うことはできません。

Q. zipに固めてから暗号化zipにすればファイル名がわからないから安心だよね?

いいえ。危険ですのでやめた方が良いです。

先程、無圧縮暗号化zipの場合はファイルヘッダを使用して既知平文攻撃を行うことができると述べましたが、zipに固めてから暗号化zipを作成した場合、それ以上圧縮ができず無圧縮状態になることが多々あります。

その場合、zipファイルのファイルヘッダを利用して既知平文攻撃を行うことで、パスワードなしで解凍が可能となります。

実際にやってみます。

f:id:nfl_n2:20211122135551p:plain
暗号化zip内へのzipファイルの格納

上記は.DS_Storeとfirst.txt, second.txtの3つのファイルを一度plain.zipというzipファイルにしてから、再度encrypted.zipという名前で暗号化zipにした際のスクショになります。

注目すべきは上記画像の末尾で、括弧の中の表記がdeflatedではなくstoredと書かれています。この場合、plain.zipは無圧縮状態で暗号化されるため、zipファイルのヘッダを使って既知平文攻撃を行うことができてしまいます。

なお、既存の暗号化zip内のファイルが無圧縮かどうかはunzip -vコマンドで確認することもできます。

f:id:nfl_n2:20211122135835p:plain
unzipコマンドによる圧縮形式の確認

既知平文攻撃を成功させるには、少なくとも12バイトの既知なバイト列が必要となります。

そこで、zipファイルのヘッダの先頭部分の10バイト (基本的に固定な箇所) と、ヘッダの中で基本的に00になる箇所を組み合わせてbkcrackを実行してみます。

f:id:nfl_n2:20211122140020p:plain
bkcrackを用いた解凍の様子

すると、約2時間ほどで上記のようにマスターキーが2種類見つかり、後半のキーを使用してplain.zipを取得することができました。

なお、.DS_Storeを用いた際と比べてbkcrackに時間がかかるのは、既知平文のバイト数が少ないためです。

また、マスターキーが複数見つかっているのも既知平文のバイト数が少ないためであり、指定した条件を満たすキーが複数存在することが原因となります。とはいえ大量のキーが見つかることはないため、bkcrackが表示するマスターキーを全て試せばその中の1つのキーで解凍に成功します。

このように、暗号化zipの中にzipを入れ込んでしまうと、.DS_Storeの有無によらずパスワードなしで解凍できてしまうことがあるため、非常に危険です。

Q. .DS_Store以外にも既知平文攻撃に使えるファイルがあるのでは?

汎用性を問わなければ多数あると思います。一般的にはロゴファイルや公開されている資料等が既知平文攻撃の事例としてよく紹介されますが、それ以外にも例えば.gitディレクトリの中のdescriptionファイルは基本的に以下の内容になっているので、既知平文攻撃に利用することができます。
Unnamed repository; edit this file 'description' to name the repository.

(そもそも公開リポジトリのソースコードが含まれていれば、そのソースコードを既知平文として使うこともできますが)

また、.DS_Store以外ではなく.DS_Storeネタですが、実はmacOSでは特定の動作をした場合に、何も管理するファイルがない、謂わば無を管理する.DS_Storeが生成されることがあり、既知平文攻撃に利用できることが新たにわかりました。

通常、macOSでディレクトリを作っただけでは、.DS_Storeがディレクトリ内に生成されることはありません。そのため、一見すると無を管理する.DS_Storeなどありえないように思えます。

しかし、Finderの表示モードがリストモードの場合、以下の挙動を行うと特殊な.DS_Storeが生成されます。

  1. 空のディレクトリに何かしらのファイルを生成する
  2. 生成したファイルをドラッグ&ドロップで他のディレクトリへ移動する

この際生成される.DS_Storeファイルには、管理するファイルの名前や座標の情報が含まれていないことから、ファイルのパターンは一意になります。
この.DS_Storeのハッシュ値 (SHA-1) はdf2fbeb1400acda0909a32c1cf6bf492f1121e07になっているはずです。

もしもこの.DS_Storeファイルが暗号化zipの中に含まれてしまうと、既知のファイルが暗号化zip内に存在することとなり、既知平文攻撃が成立してしまいます。(よって、ディレクトリ内に.DS_Storeのみがある場合はこの手法が使える可能性が高い)

まとめ

今回の記事は前回と比べ、かなり技術的な内容となりましたが、まとめると以下になります。

  • CRC32値を利用することで前回の手法が大幅に高速化され、.DS_Storeが管理するファイル数が1個の時は瞬時に暗号化zipを解凍できるようになった
  • 同じくCRC32値を利用することで、.DS_Storeが管理するファイル数が2個の場合でも短時間で暗号化zipを解凍できるようになった
  • .DS_Storeが管理するファイル数が0個の場合、.DS_Storeが一意になるため暗号化zipを解凍できることがわかった
  • (.DS_Storeとは関係がないが) 暗号化zipの中にzipがあり、無圧縮状態になっていれば暗号化zipを解凍できる

以上です。
アドベントカレンダーの12日目はMJによる振り返りに関する話とのことです。