🙆

ZeroLogon脆弱性について理解する

2022/11/15に公開

nri-secure様の記事がとても分かりやすく、これ以上のものは書けないと思いますのでもっとよく知りたい方は御査収ください。

書こうと思った背景

マルウェアの教科書を読んでいたら、bussterlordというハンドルネームを持つ有名なロシア人のIABについて取り上げられていました。

イニシャルアクセスブローカー(Initial Access Broker, IAB)は、サイバー攻撃を行う際の最初のステップである、標的への不正アクセス手段を提供する存在です。

https://twitter.com/bank_security/status/1362681075059752962?lang=en

そんな数多くの企業に攻撃を仕掛けたbassterlordさんがロシアのポータルサイトに現れ、初心者用の攻撃マニュアルを残していったという情報を今更知りました(遅)

ロシア系の学科出身として血が騒いだのでさっそくマニュアルを見つけ出して来ました。

訳すとManual for works with network from Basserlord
...みたいな感じになりますが、このpdfの中に史上最悪レベルの脆弱性とも言われるZeroLogonについての記載があったので、復習もかねてZerologonがどういう脆弱性なのかを解説したいと思います。

ZeroLogonとは

Active Directoryの認証プロトコルであるNetLogon Remote Protocolに存在する脆弱性です。

NetLogonのSpecifictionはMicrosoftの公式ページにあります。
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-nrpc/ff8f970f-3e37-40f7-bd4b-af7336e4792f

問題となる箇所

3.1.4.1 Session-Key Negotiationが攻撃に用いられる箇所となります。

全体の流れとしては以下になります。

  1. bindcallにserverがbind_ackを無事返してきたと仮定します。

  2. clientはまずclientChallengeという任意の64bitのデータをサーバに送信します
    ([request] NetrServerReqChallenge())

  3. serverはclientChallengeを受け取ると、そのresponseとしてserverChallengeを帰します。
    ([Response] NetrServerReqChallenge())

  4. clientとserverだけが知っている情報(例えばuser password)等を使って、SessionKeyを作成します。

  1. 「4.」で導出したSession Keyを鍵として、「1.」で送信したclientChallengeを暗号化し、再度サーバへ送信します。
    ([request] NetrServerAuthenticate)

  2. サーバ側もpassword,challenge data等から同じsession keyを導出しているので、再度送られてきた暗号化されたclient Challengeをsession keyで複合し、 「1.」で送られてきたclientChallengeと同じものであれば認証成功となります。


「4.」の「Session Key」を用いてclientChallengeを再度暗号化する部分で、AES CFB8というブロック暗号が採用されています。

こちらの記事に分かりやすいCFB8の擬似コードがありましたので、記事を御参照ください

IV = Randomized 16 bytes string
key = Predefined Key
Proc:
 input = IV + plaintext[:16]
 output = AES_op(input, key)
 input_2 = output[0]
 output_2 = XOR(plaintext, input_2)[0]
 plaintext = output_2 + plaintext[1:]
 repeat Proc
cypher = plaintext

この部分のどこが問題なのかというと、NetLogonの実装では本来ランダムであるはずの「iv(initial vector)」が「0に固定」となってしまっています。

それを踏まえてもう一度上の擬似コードを見てみます。

  • SessionKeyはセッション中は変わらない = AES_opの出力結果が変わるのは、inputが変更されたときだけ

  • ラウンドiのinputはIV(16 byte)と平文(8 byte)を足したものの[i..i+16]byteとなる

  • clientChallengeはclient側が任意に選んで良いので、ivが0(16byte)、plaintextが0(8bytes)の状況は簡単に作り出せる。

  • そうなると、初回のAES_opのinputは24byteの0となる。

  • 平文(clientChallenge)は全て0なので、初回の出力値が0だった場合、そのxorも0となり、plaintextは8byteの0のままである

なので、ivが0固定の場合、もし初回のSession Keyによる暗号化の結果の1byte目が0になると最終的に生成される暗号文はすべて0 = 00000000

となってしまいます。

初回の暗号文の1byteが0になる確率は2^8 = 256分の1です。

実際に試してみる

本脆弱性を簡単に試せるpython scriptがありますが、
windowsが手元に無く環境を作るのが億劫大変なので

ivが0の場合cfb8の出力結果暗号文は本当に0になるのかを試します。

main.rs
use aes::cipher::{AsyncStreamCipher, KeyIvInit};
use rand::Rng;
type Aes128Cfb8Enc = cfb8::Encryptor<aes::Aes128>;

fn main() {
    // 問題のinitial vector
    let iv = [0u8; 16];
    // 平文。自由に選択できるclientChallenge
    let plaintext = [0u8; 8];
    let wanted_ciphertext = [0u8; 8];

    let mut i = 0;
    let mut j = 0;
    // 確率分母の400倍の試行回数
    while i != 256 * 400 {
        let mut buf = plaintext.to_vec();
        let mut key = [0u8; 16];
        rand::thread_rng().fill(&mut key[..]);
        Aes128Cfb8Enc::new(&key.into(), &iv.into()).encrypt(&mut buf);

        if buf == wanted_ciphertext {
            i += 1;
        }
        j += 1;
    }
    println!("{} : {}", i, j);
}

26223414 / 102400 = 256.088027344

なので256回に1回は出力された暗号文が"00000000"と一致しているようです。

期待通りの出力が得られました。

おわりに

ZeroLogon脆弱性については2020年8月にMicrosoftからセキュリティパッチが出ていますが、
全てのドメインコントローラに適用する必要があり、未だに対応されていないシステムが存在するようです。
なので依然として有効に作用する環境は探せば見つかる気がします。ただし本脆弱性を用いたexploitは法律に抵触すると思われるので、自分の管理するリソース以外で試すのは止めたほうがいいと思います。

今回は簡単ではありますがZerologonの仕組みについて取り上げました。
最後まで呼んでいただきありがとうございました。

Discussion