イントロ Link to this heading

こんにちは、ニートです。

1 週間程前、Intel は第 11 世代 Core プロセッサ (コードネーム: Tiger Lake) を正式に発表しました。 なんか GPU がどうのこうのとか Wi-Fi6 がどうのとかあるらしいですが、CPU よく知らないので知りません。

但し、重要な事としてこのアーキテクチャは Intel CET という機能を実装しています。 そしてこの CET は皆が大好きな exploit technique である ROP を殺してしまうと言われています。

本エントリでは、今更ながら Intel CET の概要について触れ、これによって今まで成功してきた exploit が Tiger Lake 以降の Intel CPU に於いてどうなってしまうのかを見てみたいと思います。

Intel CET 概観 Link to this heading

もともと Intel CET が発表されたのは、もう 3 年以上前のことである。何度かの延期を経て、2020 年末に Tiger Lake 搭載端末が発売されると言われている。 Tiger Lake が実装している機能の内今回注目するのは、Intel CET のみである。

Intel CET: Control-flow Enforcement Technology は、端的に言うと「ROP を殺す機構」である。 大別して Shadow StackIndirect Branch Tracking という 2 つの要素から構成されている。 その結果として、「スタックフレーム中の Return Address の保護」と「jmp/call に用いる free branch の保護」を実現する。

以下では、CET を構成する 2 大要素について見ていく。 尚、以下の記述は参考 Intel SDM の specification に大きく依っている(発売していない以上正解は仕様書にしか無い)。


(追記: 2020.09.12)

同様の考えでで ROP を防ぐ方法は、過去にも RAP という名前で 10 年近く前から提唱されている。 CET はこれをアーキテクチャレベルで実装している。RAP については以下を参考のこと。


Shadow Stack Link to this heading

Shadow Stack はスタックフレーム中に保存してある RA: return address を保護する。

Shadow Stack は従来の関数スタックに追加で別のスタックを確保される。 以降は call 命令の度に従来のスタックと Shadow Stack の両方に RA を push する。 そして関数から ret する際にはやはり従来のスタックと Shadow Stack の両方から値を pop する。 ここで pop した値が同じであるならば、スタックフレーム中の RA が書き換えられていないことが保証されるというわけである。 尚、ここで pop した値が異なる場合にはプロセッサは #CP: Control Protection Exception という例外を通知する。

尚、CPL3間でのジャンプならば述べたとおりだが、CPL3 と CPL012 間でのジャンプの場合には Shadow Stack は使用されず、代わりに MSR が用いられる。Shadow Stack は CPL 毎に初期化され、CPL の切り替わり時に通常のスタックのように該当する CPL のスタックに切り替わる(SSP: Shadow Stack Pointerが書き換えられる)。

Shadow Stack の switch Link to this heading

Shadow Stack の切り替えには RSTORSSP / SAVEPREVSSP という一対の命令が用いられる。

RSTORSSP は現在の Shadow Stack から切り替え先の Shadow Stack に SSP を書き換える命令である。 同時に、新しい Shadow Stack の最も上に有る shadow stack restore token の正当性を確認する。 shadow stack restore token は、それが push されている Shadow Stack のアドレスを示しているはずである(下位 2bit を除く)。そうでなければやはり#CP例外を通知する。 正当性を確認した後はその shadow stack restore token を遷移元である Shadow Stack の SSP で書き換えて previous SSP token とする。

SAVEPREVSSP は以前の Shadow Stack へと SSP を切り替える命令である。 現在の Shadow Stack のてっぺんから previous SSP token を pop し、これが示すアドレスに shadow stack restore token (previous SSP token である自分と 1bit 目を除き同じ)を push する。

https://software.intel.com/sites/default/files/managed/4d/2a/control-flow-enforcement-technology-preview.pdf

https://software.intel.com/sites/default/files/managed/4d/2a/control-flow-enforcement-technology-preview.pdf

https://software.intel.com/sites/default/files/managed/4d/2a/control-flow-enforcement-technology-preview.pdf

https://software.intel.com/sites/default/files/managed/4d/2a/control-flow-enforcement-technology-preview.pdf

じゃあ、ShadowStack を書き換えればいいじゃん? Link to this heading

不可能である。

CET の実装に伴い、ページテーブルに Shadow Stack という属性が追加された。 これによって Shadow Stack とマークされたページに対するアクセスは control flow を変える命令からしか不可能になった。 mov / xsave 等の命令によるアクセスは SEGV となる。

スタックだろ? アンダーフローさせちまえよ Link to this heading

不可能である。

前述したように Shadow Stack を操作し得る命令は限られている。 これらの命令を呼んでスタックをアンダーフローさせるには、 RIP を操作して該当命令を複数回呼んで pop を繰り返す必要が有る。 しかし、そもそもにこの RIP を操作することがCETによってできなくなっている。 まさに R.I.P. ってね….

Indirect Branch Tracking Link to this heading

CPU は indirect jump/ call 命令を追跡する為に状態を保持する。 jmp / call 命令の前にはIDLE 状態であり、これらの命令が呼ばれると WAIT_FOR_ENDBRANCH 状態に切りかわる。 WAIT_FOR_ENDBRANCH 状態の時に ENDBR32/64 命令以外が実行されると、#CP例外を通知する。 換言すれば、jmp/call による遷移先は endbr32/64 でなければならないということである。

ハードウェアは Indirect Branch Tracking のために CPL3 用とそれ以外用として 2 つの状態機械を構成する。 どちらの状態機械を用いるかは、その時の CPL に依存する。 CPL3 と CPL012 間での追跡については多少煩雑であるため、参考 C を参照されたい。

尚、endbr32/64 命令は CET 非対応アーキテクチャに於いてはただの NOP となる。 また、対応機であっても有効化されていない場合にはただの NOP として扱われる。 更に、対応機且つ有効化されていたとしても状態機械の状態評価に使われる以外は NOP と同じで環境に全く影響しない。

既に gcc 君は対応するコードを吐いてくれる。

じゃあ endbr64 から始まるアドレスに不正に JMP することはできるんだろ? Link to this heading

Yes.

RA を書き換えて Shadow Stack に引っかかったりしないような場合、 例えば関数テーブルを書き換えるような場合には Indirect Branch Tracking には引っかからない。 但し、chain を組むことはできないため単発でできるようなことに限られてしまう。

CET が有効になる条件 Link to this heading

まずは CR4 レジスタの CET bit(24th) がオンになっていることが必要である。

その上で Shadow Stack や Indirect Branch Tracking は MSR によって別々に機能制限をすることができ、 例えば IA32_U_CETCPR=3 の際の CET のコンフィグを行うことができる。 この値は cpuid によって知ることができる。

実際に Intel CET 上で ROP をしようとしてみる Link to this heading

Intel SDEによってTiger Lakeをエミュレートする。

まずは、参考 C からtar.bzファイルをダウンロードしてきて展開する。 基本的にはそれだけだが、Linux に於いては ptrace できる範囲が通常 1: restricted ptrace (非 root は子プロセスのみ・root は任意)になっているため、 以下で 0: classic ptrace permissions にする必要が有る。(次の reboot まで有効)

.sh
1echo 0 > /proc/sys/kernel/yama/ptrace_scope

まずは Shadow Stack の働きをチェックする。使用するプログラムは以下の通り:

.c
 1#include<stdio.h>
 2#include<stdlib.h>
 3
 4char mora[] = "     ` (#!    `   `....        ...,......TN.`\n      j@`  ``.JgHH=?\"\"W%`     TB\"7!(\"TY` db.\n    `-#!     _ue    ...      `.de.......  .M;\n   ` dD      .dMY9_T\"TMb      ?MY?!_??T#_  dR`\n     W]    `  j#      dD      .Wl     j@`` J@   < I am Winner!!!\n     W]  `    .N+   .(#!       ?N.. .(H'  .W%\n     db. +#WNx _TBD`?=     `     ?!(\"=   .d$\n     .Me Wm(d9       . .. .-. .,        .dD\n      .T\\ _!`       .TH#WHB7HMYWH%     .\"!\n";
 5
 6void win(void){
 7  printf("%s", mora);
 8  exit(1);
 9}
10
11void evil(void){
12  char buf[0x10];
13  printf("I am evil moratorium.\n");
14
15  *(unsigned long*)(buf+0x28) = (unsigned long)((char*)&win + 0x4);
16}
17
18
19int main(int argc, char *argv[])
20{
21  printf("Start\n");
22
23  evil();
24  printf("CET is winner...\n");
25
26  return 0;
27}

evil() 関数に於いて自身の RAwin()+4 に書き換えている。

これを 非 CET 環境 (Ubuntu20.04 glibc2.32 kernel5.4.0-47-generic)と CET 環境に於いて動かした場合のそれぞれの結果は以下である:

落ちている。ROP が死んだ…

SDE では -debug オプションをつけることで勝手に gdb server を建ててくれるため容易にデバッグができる。 見てみたところ、win() の最初の命令を実行する直前で落ちている。 SEGVで落ちてる理由は、知らん、なんやコレ。

エラーを見てみると以下のようになっている:

Shadow Stack は 0x40120fret に於いて 0x40119a(main) を pop したが、 通常のスタックからは 0x401234(win) を pop したためにエラーになったという旨である。 ROP は、CET によって殺されてしまったようだ…


また、同様にして Indirect Branch Tracking に引っかかるような以下のコードも考える:

.c
 1#include<stdio.h>
 2#include<stdlib.h>
 3
 4char mora[] = "     ` (#!    `   `....        ...,......TN.`\n      j@`  ``.JgHH=?\"\"W%`     TB\"7!(\"TY` db.\n    `-#!     _ue    ...      `.de.......  .M;\n   ` dD      .dMY9_T\"TMb      ?MY?!_??T#_  dR`\n     W]    `  j#      dD      .Wl     j@`` J@   < I am Winner!!!\n     W]  `    .N+   .(#!       ?N.. .(H'  .W%\n     db. +#WNx _TBD`?=     `     ?!(\"=   .d$\n     .Me Wm(d9       . .. .-. .,        .dD\n      .T\\ _!`       .TH#WHB7HMYWH%     .\"!\n";
 5void (*fp)();
 6
 7void win(void){
 8  printf("%s", mora);
 9  exit(1);
10}
11
12void f1(void){
13  printf("In f1\n");
14  return;
15}
16
17void f2(void){
18  printf("In f2\n");
19  return;
20}
21
22void evil(void){
23  char buf[0x10];
24  printf("I am evil moratorium.\n");
25
26  fp = ((char*)f2) + 4; // skip endbr
27  fp();
28  *(unsigned long*)(buf+0x28) = (unsigned long)((char*)&win + 0x4);
29}
30
31int main(int argc, char *argv[])
32{
33  printf("Start\n");
34
35  fp = f1;
36  evil();
37  printf("CET is winner...\n");
38
39  return 0;
40}

関数ポインタを書き換えているが、その際に endbr を飛ばしている。 この状態での CET/非 CET 環境での実行結果は以下の通り:

Control flow ENDBRANCH error detected だそうだ。

Bypass Link to this heading

わからん。 思いつかない。 でも DEP が実装されたと思ったら ROP ができたわけだから、CET が実装されたらなんか exploit 出てくるやろ。頭いい人、頼んだわ。

アウトロ Link to this heading

悲しいね…

参考 Link to this heading