世界を動かす技術を、日本語で。

base64エンコードされたJSON、証明書、プライベートキーの検出

概要

  • base64エンコードされたJSON や証明書の見分け方を解説
  • 先頭の文字列から エンコード内容 を推測する方法
  • PEM形式証明書 ・秘密鍵の識別例
  • 注意点 や誤検出パターンも紹介
  • シンプルで便利な 小技 の共有

base64エンコード文字列から内容を推測する技

  • homelabで 暗号化ファイル を調査
  • ファイル内に key_provider.pbkdf2.password_key のようなbase64文字列を発見
  • 同僚のアドバイスで base64デコード を試した結果、 JSON形式 の鍵情報を取得
  • 例:
    • echo "eyJzYW..." | base64 -d
    • 出力:{"salt":"...","iterations":600000,"hash_function":"sha512","key_length":32}
  • JSONがbase64エンコード されている場合、先頭がeyで始まるケースが多い
    • ey{"のbase64表現
  • 追加例:
    • echo "{\"" | base64eyIK
    • echo "{\"s" | base64eyJzCg==
    • 先頭がeyなら base64 JSON の可能性が高い

PEM証明書や秘密鍵のbase64判別

  • 証明書や秘密鍵 (PEM形式)は-----BEGIN CERTIFICATE-----などで始まる
  • これをbase64エンコードすると先頭がLSになる
    • echo -en "-----BEGIN CERTIFICATE-----" | base64LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t
  • LS---(ハイフン3つ)のbase64表現
  • 秘密鍵やその他PEMデータ も同様に判別可能

注意点と誤検出パターン

  • YAMLファイル---(ドキュメント開始記号)も同じくLS0tLS0Kで始まる
    • echo "---\n" | base64LS0tLS0K
  • PEM形式以外 のデータもLSで始まる場合があるため、 完全な判別法ではない
  • base64の先頭文字列 だけで内容を断定せず、必要に応じて デコードで確認 推奨

まとめと小技の活用

  • base64エンコード文字列の先頭 を見るだけで、ある程度 中身を推測 できる
  • eyJSONLSPEM証明書/秘密鍵やYAML
  • 完全な判定法ではない が、 素早い調査やデバッグ に有効
  • Davide、Denis、tyzbit によるシンプルかつ便利なテクニック

Hackerたちの意見

Base64の準固定ポイントがあるよ: $ echo -n Vm0 | base64 Vm0w これは一文字ずつ無限に拡張できるけど、常に何らかのサフィックスが付くんだ。

サフィックスは入力に応じて長くなるから、どんどん面白くなくなっていくよ。(出力は必ず入力の8/6のサイズになるから、サフィックスは常に長さに33%を追加するんだ。)

参考までに、クワジ固定点をゼロから生成するプログラム:

#!/usr/bin/env python3
import base64

def len_common_prefix(a, b):
    assert len(a) == length
    return tmp[:length]

print(tmp[:l].decode('ascii'), tmp[l:].decode('ascii'), sep='\v')
# バッファの終わりを超えてスライスすると、Pythonでは安全に切り捨てられる。
start = tmp[:l*4//3+4]
# TODO これって理想的なの?
if __name__ == '__main__':
    final = calculate_quasi_fixed_point(b'\0', 80)
    print(final.decode('ascii'))

最終的にはこれが生成される: Vm0wd2QyUXlVWGxWV0d4V1YwZDRWMVl3WkRSV01WbDNXa1JTVjAxV2JETlhhMUpUVmpBeFYySkVUbGho

逆に言うと、それは尾を食うユニクインって呼ぶの?

これ、あんまり好きじゃないな。無駄に感じる。JWTも同じことしてるし。この例でも、文字列(ソルト)を二重にBase64エンコードしてる。jsonのようなものが本当にないのが残念だね。みんなそれを使って書けるのに。protobufみたいなものが、スキーマなしで書きやすくて読みやすければいいのに。

構造を説明するヘッダーにn*field_countを犠牲にする必要があるね。許可されるタイプも定義しないと。

すべてがそれを話し、書くことができる。ASN.1は超いい感じだよ -- すべてがそれを話し、ツールも素晴らしい(逃げて隠れる)

これの何が悪いの?Base64の目的はデータ、特にバイナリデータを限られたASCII文字のセットにエンコードして、テキストベースのプロトコルでの送信を可能にすることなんだ。暗号ライブラリでもオブfuscationツールでもないよ。機密データをBase64でエンコードしたり、JWTペイロードに機密データを含めたりするのは、最初に暗号化されていない限り避けるべきだよ。

本当に残念だけど、jsonのようなものは他にないんだよね。messagepackやcborはjsonに似てるけど(スキーマなしで、似たようなプリミティブ型)、バイナリデータもサポートしてる。bsonも似たような選択肢だね。どれも多くの言語で実装があって、大きな成熟したプロジェクトで使われてるよ。

protobufみたいなものがスキーマなしで書きやすくて読みやすかったらいいのにね。もし汎用的なバイナリの階層型長さ値エンコーディングが欲しいなら、https://en.wikipedia.org/wiki/Interchange_File_Format を考えたことある?広くサポートされているIFFライブラリがあるわけじゃなくて、フォーマットがシンプルすぎて、言語にバイト配列型があれば、バグのないIFFエンコーダ/デコーダを5分で実装できるんだ。 (だから、JSONやXMLのライブラリのような汎用的なIFFメタフォーマットライブラリがないんだよね。「あまりにもシンプルすぎて、みんなが私のライブラリに依存するのを面倒にしたくない」から、みんな自分のIFFベースの具体的なファイルフォーマットのパーサーやジェネレーターの一部としてIFFエンコーディング/デコーディングを実装してる。)IFFは何に使われてるかというと?AIFF、RIFF(だからWAV、AVI、ANI、そして驚くべきことにWebP)、JPEG2000、PNG [ちょっとした調整あり]… • それに、ISO Base Media File Format(「BMFF」)という子孫メタフォーマットもあって、これによってMP4、MOV、HEIF/HEICも汎用的なIFFパーサーで解析できるよ(ただし、BMFF特有のパーサーを使わないと、チャンクボディからメタデータフィールドを取り出すのを見逃すかもしれないけど)。 • それから、https://en.wikipedia.org/wiki/Extensible_Binary_Meta_Languag...(「EBML」)という代替案もあって、これは基本的にIFFだけど、TLVの「型」と「長さ」の部分を可変長整数でエンコードしてる(https://matroska-org.github.io/libebml/specs.htmlを参照)。これは現在、Matroska(MKV)フォーマットのメタフォーマットとして使われてるよ。ちょっと複雑すぎて、独立した汎用コーデックライブラリ(https://github.com/Matroska-Org/libebml)もある。もしディスクに構造化されたバイナリデータをダンプしたいなら、IFFチャンクをダンプ/エクスポート/送信ロジックの中で手動で生成するのが一番だと思う。例えば、printfの呼び出しの中でCSVを手動で出力するのと同じように。「これはIFFベースのフォーマットです」と言うか、.iff拡張子を付けるか、application/x-iffとして送信すれば、エコシステムはそれで動くはずだよ。(JSONと同じように、IFFチャンクに説明的な名前を付ければ、みんなは文脈からチャンクが「何を意味するか」を推測できると思うよ。スキーマドキュメントは必要ない。)

仕事でJWTサポートライブラリを作ったよ(https://github.com/geldata/gel-rust/tree/master/gel-jwt)。JWTは全部「eyyyyyy」って頭の中で聞こえるって確認できるよ。

ええ、笑った

JWTは頭の中で「えぇぇぇ」って聞こえる。「おい、API開けよ、俺だよ」

"またお前か、パンクZIP?" ゼップファイルの最初の数バイトを見たときに。

すべての証明書が「みいいいいい」って聞こえるのと同じだね。

おい、俺はJSONだ!

Base64エンコードされたJSONは見分けられるよ。PEMフォーマット(-----BEGIN [CERTIFICATE|CERTIFICATE REQUEST|PRIVATE KEY|X509 CRL|PUBLIC KEY]-----で始まる)は、ボディ内で既にBase64になってる。ヘッダーとフッターはASCIIで、エンコードすべきじゃない[0](主張へのリンクはないから、PEMに似た別のフォーマットがあるかも?)プライベートキーは、繰り返しのテキストシーケンスで始まるか(またはヘッダーもエンコードされたPEMフォーマットを使ってる場合)でない限り、見分けられないよ。[0]: https://datatracker.ietf.org/doc/html/rfc7468

他に注意すべきbase64のプレフィックスはMIだね。MIはすべてのASN.1 DERエンコードされたオブジェクトに共通なんだ(標準エンコーディングのすべての公開鍵と秘密鍵、すべての証明書、すべてのCRL)ほとんどすべてのオブジェクトがSEQUENCE(0x30タグバイト)に続いて長さを示すバイト(上位ニブル0x8)だから。MIIはすごく一般的で、2バイトの長さを持つSEQUENCEを導入するからね。

指摘してくれてありがとう!ブログ記事に訂正を追加したよ。

Base64エンコードや16進エンコードされたasn1を一回見すぎたら、マトリックスのあのシーンを信じるようになった。オペレーターがターミナルでマトリックスからの生ストリームを見て、そこに何かを見ているシーンね。

数年前、m4を使わずにsendmail.cfの大部分を手で読み書きできる人たちのグループにいたんだ。他の人たちも当時メールサーバーを扱っていて、まるで超能力みたいに扱ってたよ。

なんか、若い子が私が16進数ストリームからASCII文字列を読み取れたことに驚いていたのを思い出す。私たちおじさんたちはいろいろ見てきたからね。

ここにいる誰かが考えたことがないなら(「今日のラッキー1万人」?)、ASCIIの構造には意図的なものがたくさんあって、それがバイナリや16進数で簡単にわかるよ。最初のニブル(16進数の桁)はチャート内の位置を示してて、だいたい2 = 句読点、3 = 数字、4 = 大文字、6 = 小文字みたいな感じ。数字(最初のニブル3)の場合、数字の値は2番目のニブルの値と等しい。句読点(最初のニブル2)の場合、句読点は伝統的なアメリカのキーボードレイアウトでシフトを押して2番目のニブルの数字を押したときに得られる文字。大文字(最初のニブル4、次に最初のニブル5にオーバーフロー)の場合、2番目のニブルはアルファベット内の文字の順序位置。だから41 = A(文字#1)、42 = B(文字#2)、43 = C(文字#3)。小文字も同じように6から始まって、61 = a(文字#1)、62 = b(文字#2)、63 = c(文字#3)って感じ。トリッキーなのは、最初のニブル5にオーバーフローする文字(文字#16のP)や、最初のニブル7にオーバーフローする文字(文字#16のp)。そこでは、実際に文字の位置に16を足してから2番目のニブルと組み合わせる必要があるか、「文字#0x10、文字#0x11、文字#0x12...」みたいに考える必要があるかもしれないけど、これは直感的じゃない人もいるかもね。ASCIIにはもっと構造やパターンがあって、それはすべて意図的で、意味のあるビット操作を促進するためにあるんだ。例えば、大文字を小文字に変換するのは単に32を足すだけ、または0x00100000との論理和を取るだけ。小文字を大文字に変換するのは32を引くか、0x11011111との論理積を取るだけ。ASCIIの16進数ダンプを読むときは、最初の印刷可能な文字(0x20)が皮肉にも空白、つまりスペース文字であることを知っておくと便利だよ。

いい知識だね、でもなんでそうなってるのか説明してみて。{"はASCII 01111011、00100010。Base64は3バイト×8ビット=24ビットを取って、その24ビットのシーケンスを6ビットずつ4つの部分に分けて、それぞれを0-63の間の数字に変換するんだ。もしビットが足りない場合(2バイト=16ビットしかないのに、18ビット必要)、0でパディングする。もちろん実際には、最後の2ビットはJSON文字列の3番目の文字から取られることになるけど、それは可変だよ。最初の6ビットは011110で、10進数では30。次の6ビットは110010で、10進数では50。最後の4ビットは0010。これを00でパディングすると001000になり、8になる。エンコーディングテーブル(https://base64.guru/learn/base64-characters)を使うと、30はe、50はy、8はIになる。これが「ey」だよ。コンピュータサイエンスの人たちが今はあまり好奇心がないのは面白いね。このブログ記事は表面を触れただけで、説明には入ってない。

これを「今のコンピュータサイエンスの人たちが好奇心がない象徴」と考えるのにはかなり躊躇するよ。著者がこの発見の文脈で書いているときに、そこまで深く関与していなかっただけかもしれないし、パターンが存在すること以上に彼らにとって実際には重要ではないからね。

CSの卒業生って、何かが実際にどう動いてるかをスキップしちゃって、抽象的な部分に満足してることが多いよね。

聴衆はもうそれがなぜ機能するか理解してると思うけど、こういうことには比較的小さなニーモニックのセットがあるっていうのが面白いんだよね。「eyJ」はJSON用、「LS0」はダッシュ(PEMエンコーディング)用、「MII」はPEM内のDERペイロード用、とか。長いことやってるけど、今日まで気づいたのは「MII」だけだったな。

それって、作者の興味についての飛躍だね。根本的な理由があまりにも明白だから、言及する価値がないと感じた可能性もある。自分はbase64エンコーディングがどう動くか知ってるけど、作者が指摘したパターンには気づかなかった。読んだ瞬間に理由がわかったけど、作者がもっと深く説明すべきだとは思わなかったな。

作者はJSONが何か説明してないね。ターゲットオーディエンスには明らかだから、説明は必要ないってことだね。

数学的に言うと、base64は生の入力の3文字ごとに4文字のbase64出力が得られるようになってるんだ。このブロックは互いに独立して考えられるよ。例えば、「Hello world」という文字列で、次のようなbase64変換ができる。* "Hel" -> "SGVs" * "lo " -> "bG8g" * "wor" -> "d29y" * "ld" -> "bGQ=" これらのエンコードされたブロックを連結すれば、最終的なエンコードされた文字列「SGVsbG8gd29ybGQ="ができるよ。(最後の部分がイコールで終わってるのに注意してね。これは入力が3文字未満だから、4文字の出力を得るためにパディングを適用する必要があるからなんだ。その一部は3番目の数字にもエンコードされてる。)これはbase64の動作の副産物であって、意図されたものではないことに注意が必要だよ。私の理解では、ASCII文字を取って、それを16進数(基数16)に変換すると、結果の16進数は常に2桁になる、というのと似てる。元の文字が大きな文字列の一部であっても、同じ2桁になるんだ。この場合、3つの基数256の数字は4つの基数64の数字に変換され、同じように6つの基数16の数字に変換される。

ちょっと指摘だけど、asciiはbase128だよ。最大のascii値は0x7fだから、これはhexダンプを見ているときの目印になる。

ところで、これがLLMがbase64を結構うまくデコード/エンコードできる理由だと思う。MCPが提供するツールなしでも、他の言語を読むのと同じように「読む」ことができるから。ウェブ上のほとんどのエンコードされたbase64は、デコードされたバージョンが一緒にあるしね。

1213486160を思い出すな。[1] それとは別に、これが暗号化されたOpenTofuの状態だって理解するのに、かなりの時間を費やしちゃった。テラフォームの状態に見えすぎたけど、完全には一致しなかった。これが私が仕事で多くの時間を費やしていることを教えてくれるね。これは、状態を読むことはできないけど、暗号文を観察することで変化や成長を観察できるという、興味深い状況かもしれない。多分大丈夫だけど、面白いね。1: https://rachelbythebay.com/w/2016/02/21/malloc/

直接的には関係ないけど、EBCDICやクレジットカードの位置データフォーマットを即座に解読できるおじいさんを知ってる。時々、彼はその理由をうまく説明できない「感覚」を持っていて、でも正確に値や名前、他のデータを知ってた。彼がログや他の場所でVISAやMASTERのトランザクションを即座にデコードするのを見るのはすごかったよ。

それって、今まで聞いた中で一番ニッチなパーティートリックだね。

これらのログにクレジットカードの詳細(番号やCVVなど)が含まれてないことを願うよ。もし含まれてたら、この情報を記録してる会社はVisaやMCとトラブルになるかも。追記:もうちょっと深く見てみたら、これらの[0]ファイルのことを言ってるのかな? [0]: https://docs.helix.q2.com/docs/card-transaction-file

監査中に、ライブログを画面で見たことあるよ。言うまでもなく、最初の監査は通らなかった(そのログは黒塗りにすべきだった)。