Help us understand the problem. What is going on with this article?

CTF/ALPC を利用した権限昇格の脆弱性 CVE-2019-1162

はじめに

CVE-2019-1162 とは、Google Project Zero の Tavis Ormandy 氏が見つけ、2019 年 8 月の Patch Tuesday で修正がリリースされた脆弱性です。非管理者ユーザーから SYSTEM 権限まで昇格して、メモリへの書き込み (Arbitrary Memory Write) とコードの実行 (Remote Code Execution) が可能です。 危険ですね。コードは全て公開されているので、手元で試すのは簡単です。

CVE - CVE-2019-1162
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1162

Project Zero: Down the Rabbit-Hole...
https://googleprojectzero.blogspot.com/2019/08/down-rabbit-hole.html

ブログを読んでいたら面白そうだったので、自分で手を動かして理解してみることにしました。

CTF プロトコルの発見

Tavis Ormandy 氏の物語は、特定の String Message (0xC000-FFFF の範囲にある Window Message。このとき見つかったのは 0xc046 と 0xc047。) は Low IL プロセスから Medium IL プロセスに送信可能であることに気づいたところから始まります。String Message 0xc046 を登録していたのは MSCTF.dll であり、ここで彼は何か感じるものがあったらしくこのように書いています。まあここまでは私でも来れます。

I decided it would be worth it to spend a couple of weeks reverse engineering CTF to understand the security properties.

CTF を解析した結果分かったのは、CTF サーバーとも言える ctfmon.exe という High IL で動いているプロセスがデスクトップ セッション毎に ALPC ポートを開いていて、そのポートに対していい感じの ALPC メッセージを送ると、サーバーから任意の CTF クライアントに対して String Message が送られる仕組みになっているらしい、ということです。この仕組みによって、ChangeWindowMessageFilterEx を介さずに Low から Medium/High IL プロセスへメッセージを送ることができることが分かりました。

そもそも ALPC 関連の API も公開されていなかったような気がしますが、彼は CTF が使っている ALPC メッセージのパラメーターの構造とその意味を解析することに成功しています。具体的にどうやって解析したのかまでは書かれていません。私がやるなら MSCTF!CCtfClientPort::ProcessMessage あたりの関数を頑張って読む、とかでしょうか。IDA Pro があれば簡単にできる・・・のだろうか。CTF プロトコルを解析しろ、というタスクが降ってくれば頑張れるような気もしますが、ちょっと興味を持っただけだと途中ですぐ諦めてしまうだろうなぁ。2 週間どころか、半年ぐらいは余裕でかかりそうです。

CTF プロトコルでは、クライアント側からターゲットのスレッド ID と、メッセージの種類、そのパラメーターを送ることができます。例えば、1 つのメッセージでターゲット スレッド内で COM オブジェクトを作成し、次のメッセージで、作成したオブジェクトに対して任意のメソッドを任意のパラメーターを指定して実行することができます。そして衝撃なのは、その時に権限のチェックが一切存在しないことです。

Firstly, there is no access control whatsoever!

Any application, any user - even sandboxed processes - can connect to any CTF session. Clients are expected to report their thread id, process id and HWND, but there is no authentication involved and you can simply lie.

Secondly, there is nothing stopping you pretending to be a CTF service and getting other applications - even privileged applications - to connect to you.

Even when working as intended, CTF could allow escaping from sandboxes and escalating privileges. This convinced me that my time would be well spent reverse engineering the protocol.

この動き、以前書いた WNF の動きにかなり近いですね。

Subscribe a notification from WNF - Qiita
https://qiita.com/msmania/items/aaba5f3bddec10c245c8

BlackHat で発表した時点で Alex Ionescu 氏は、WNF の存在と、Fuzzer を使って幾つかのプロセスをクラッシュさせる方法を見つけたぐらいでした。CTF プロトコルについても、ここまでの時点でかなりの成果を挙げていると思うのですが、Ormandy 氏はさらに踏み込んで調査します。

MSCTF!CTipProxy::Reconvert による Remote Code Execution

1 つのパラメーターをマーシャリングして適当なメソッドを呼び出すと、ターゲット内でそのメソッドが呼ばれたときのパラメーター近傍のメモリの構造は以下のようになります。ちなみにパラメーターの数を増やすと、構造が微妙に変わります。

01.png

ブログの中では、各メソッドを呼び出して例外が発生したときのコールスタックから、MSCTF!CTipProxy::Reconvert を使うとパラメーターとジャンプ先の値を制御できる、という結論に達しています。コールスタックなどを一個一個目視して判断したのだと思いますが、このへんの地味な作業は経験が物を言うんでしょうね。私はたぶんヒント無しだと無理。

MSCTF!CTipProxy::Reconvert を呼び出すと、MSCTF!CTipProxy::FindContextPtr 経由で indirect call を実行します。この indirect call のジャンプ先が poi(buffer + 40) になるので、ここから任意のアドレスを実行できます。さらに、call の時点で rcx は buffer - 0xb0 のアドレスを保持しているので、多少の制限はありますがパラメーターも制御できます。

ただし、ptr [MSCTF!_guard_dispatch_icall_fptr] は ntdll!LdrpDispatchUserCallTarget を経由するので、CFG のバイパスは必要です。彼に言わせると "In practice, CFG is a very weak mitigation. Ignoring the many known limitations, even the simplest Windows programs have hundreds of thousands of whitelisted indirect branch targets." らしいですが。

Indirect call までのアセンブリとデータの流れは以下の通りです。

MSCTF!CTipProxy::Reconvert:
00007ffc`7560c5f0 48895c2408      mov     qword ptr [rsp+8],rbx
00007ffc`7560c5f5 55              push    rbp
00007ffc`7560c5f6 56              push    rsi
00007ffc`7560c5f7 57              push    rdi
00007ffc`7560c5f8 488bec          mov     rbp,rsp
00007ffc`7560c5fb 4883ec60        sub     rsp,60h
00007ffc`7560c5ff 488bfa          mov     rdi,rdx  <---- rdx = buffer-b0
                                                         poi(rdx) = buffer-80
                                                         poi(rdx-38) = buffer+10
00007ffc`7560c602 488bd9          mov     rbx,rcx
00007ffc`7560c605 4885d2          test    rdx,rdx
00007ffc`7560c608 750a            jne     MSCTF!CTipProxy::Reconvert+0x24 (00007ffc`7560c614)

MSCTF!CTipProxy::Reconvert+0x1a:
00007ffc`7560c60a bb03400080      mov     ebx,80004003h
00007ffc`7560c60f e992000000      jmp     MSCTF!CTipProxy::Reconvert+0xb6 (00007ffc`7560c6a6)

MSCTF!CTipProxy::Reconvert+0x24:
00007ffc`7560c614 4883c1b0        add     rcx,0FFFFFFFFFFFFFFB0h
00007ffc`7560c618 e8878cffff      call    MSCTF!CTipProxy::FindContextPtr (00007ffc`756052a4)

  MSCTF!CTipProxy::FindContextPtr:
  00007ffc`756052a4 4053            push    rbx
  00007ffc`756052a6 4883ec20        sub     rsp,20h
  00007ffc`756052aa 488bd9          mov     rbx,rcx
  00007ffc`756052ad 4c8bc2          mov     r8,rdx  <----
  00007ffc`756052b0 33c9            xor     ecx,ecx
  00007ffc`756052b2 48894c2438      mov     qword ptr [rsp+38h],rcx
  00007ffc`756052b7 4885d2          test    rdx,rdx
  00007ffc`756052ba 7438            je      MSCTF!CTipProxy::FindContextPtr+0x50 (00007ffc`756052f4)

  MSCTF!CTipProxy::FindContextPtr+0x18:
  00007ffc`756052bc 488b02          mov     rax,qword ptr [rdx] <---- rax = buffer-80
  00007ffc`756052bf 498bc8          mov     rcx,r8              <---- rcx = buffer-b0
                                                                      poi(rcx-38) = buffer+10
  00007ffc`756052c2 488d542438      lea     rdx,[rsp+38h]
  00007ffc`756052c7 488b80c0000000  mov     rax,qword ptr [rax+0C0h] <---- rax = poi(buffer+40)
  00007ffc`756052ce ff15cc670500    call    qword ptr [MSCTF!_guard_dispatch_icall_fptr (00007ffc`7565baa0)]

msvcrt!_init_time による Arbitrary Memory Write

次のステップとして、RCE までの連鎖を発生させるデータ構造を作ります。そのためには任意のアドレスに書き込みができる必要があり、前述の buffer+40 経由で最初に実行するコードとして msvcrt!_init_time を使います。どうやって msvcrt!_init_time を見つけたのかという話ですが、CFG のホワイトリストを地道にチェックしただけのようです。MSCTF!CTipProxy::Reconvert を見つける作業はスクリプトを使って多少自動化していたようですが、このフェーズは完全に手作業っぽいです。

MSCTF!CTipProxy::Reconver の indirect call からmsvcrt!_init_time に飛んできた時点で、rcx は buffer-0xb0 のアドレスを保持しています。アセンブリを辿ると、一回の msvcrt!_init_time で buffer+a8 の 1 バイトをデクリメントできることが分かります。条件として、buffer-90 の 1 バイト領域が 0 である必要があります。したがって、近傍が全て 0 で埋まっている領域を見つけておけば、デクリメントを繰り返すことで任意の値にメモリを書き換えることができます。このような領域は多数ありますが、ここでは kernel32.dll のデータ領域 (.data セクション) のオフセット +0x1008 のところを使います。

msvcrt!_init_time:
00007ffc`74ff1d30 48895c2408      mov     qword ptr [rsp+8],rbx
00007ffc`74ff1d35 48896c2410      mov     qword ptr [rsp+10h],rbp
00007ffc`74ff1d3a 57              push    rdi
00007ffc`74ff1d3b 4883ec20        sub     rsp,20h
00007ffc`74ff1d3f 83792000        cmp     dword ptr [rcx+20h],0   <--- check poi(buffer-b0+20)
00007ffc`74ff1d43 488d2d86e00500  lea     rbp,[msvcrt!_lc_time_c (00007ffc`7504fdd0)]
00007ffc`74ff1d4a 488bf9          mov     rdi,rcx                 <---  rdi = buffer-b0
00007ffc`74ff1d4d 744b            je      msvcrt!_init_time+0x6a (00007ffc`74ff1d9a) --> jump 74ff1d9a

msvcrt!_init_time+0x6a:
00007ffc`74ff1d9a 488bdd          mov     rbx,rbp

msvcrt!_init_time+0x6d:
00007ffc`74ff1d9d 488b8758010000  mov     rax,qword ptr [rdi+158h] <---- rax = poi(buffer-b0+158)
                                                                             = poi(buffer+a8)
00007ffc`74ff1da4 483bc5          cmp     rax,rbp
00007ffc`74ff1da7 7407            je      msvcrt!_init_time+0x80 (00007ffc`74ff1db0)

msvcrt!_init_time+0x79:
00007ffc`74ff1da9 f0ff8860010000  lock dec dword ptr [rax+160h]  <---- decrement poi(buffer+a8)+160

ALPC メッセージに渡すバッファーの +40 に msvcrt!_init_time、+a8 にデクリメントしたいアドレス - 0x160 をセットして MSCTF!CTipProxy::Reconvert を呼ぶだけです。

02.png

combase!CStdProxyBuffer_CF_AddRef/MSCTF!CCompartmentEventSink::OnChange によるパラメーター乗っ取り

メモリが自由に書き換えられるようになりました。ひとまずのゴールは、LoadLibrary にジャンプさせて任意の DLL をロードすることです。既に任意のアドレスを実行できるようになっているので LoadLibrary を実行することはできますが、MSCTF!CTipProxy::Reconvert の indirect call では、パラメーターを完全に乗っ取ることができていません。

どうやって見つけたのか謎ですが、combase!CStdProxyBuffer_CF_AddRef と MSCTF!CCompartmentEventSink::OnChange を 2 回ずつ連鎖させることで、最終段階の indirect call ではジャンプ先と第一パラメーターを完全に乗っ取ることができます。それぞれの連鎖におけるステップは以下の通りです。どうやってこんなのに気づくのか意味不明ですね。アクロバティックすぎる。

combase!CStdProxyBuffer_CF_AddRef:
00007ffc`75fae370 488b49c8        mov     rcx,qword ptr [rcx-38h] <---- rcx = buffer+10
00007ffc`75fae374 488b01          mov     rax,qword ptr [rcx]     <---- rax = object
00007ffc`75fae377 488b4008        mov     rax,qword ptr [rax+8]   <---- rax = poi(object+8) = Func2
00007ffc`75fae37b 48ff250e5c0700  jmp     qword ptr [combase!__guard_dispatch_icall_fptr (00007ffc`76023f90)]
                                          --> MSCTF!CCompartmentEventSink::OnChange

MSCTF!CCompartmentEventSink::OnChange:
00007ffc`75623010 488b4130        mov     rax,qword ptr [rcx+30h] <---- rax = poi(buffer+10+30) = Func1
00007ffc`75623014 488b4938        mov     rcx,qword ptr [rcx+38h] <---- rcx = poi(buffer+10+38) = object+38
00007ffc`75623018 48ff25818a0300  jmp     qword ptr [MSCTF!_guard_dispatch_icall_fptr (00007ffc`7565baa0)]
                                          --> combase!CStdProxyBuffer_CF_AddRef

combase!CStdProxyBuffer_CF_AddRef:
00007ffc`75fae370 488b49c8        mov     rcx,qword ptr [rcx-38h] <---- rcx = poi(object+38-38) = object
00007ffc`75fae374 488b01          mov     rax,qword ptr [rcx]     <---- rax = object
00007ffc`75fae377 488b4008        mov     rax,qword ptr [rax+8]   <---- rax = poi(object+8) = Func2
00007ffc`75fae37b 48ff250e5c0700  jmp     qword ptr [combase!__guard_dispatch_icall_fptr (00007ffc`76023f90)]
                                          --> MSCTF!CCompartmentEventSink::OnChange

MSCTF!CCompartmentEventSink::OnChange:
00007ffc`75623010 488b4130        mov     rax,qword ptr [rcx+30h] <---- rax = poi(object+30) = Func3
00007ffc`75623014 488b4938        mov     rcx,qword ptr [rcx+38h] <---- rcx = poi(object+38)
00007ffc`75623018 48ff25818a0300  jmp     qword ptr [MSCTF!_guard_dispatch_icall_fptr (00007ffc`7565baa0)]
                                          --> LoadLibraryA

ここから逆算すると必要なデータ構造が分かるので、前述した msvcrt!_init_time によって 1 バイトずつちまちまとデクリメントをすることを繰り返して作っておきます。必要なデータ構造は以下の通りです。

03.png

おわりに

久々に exploit を追いかけてみました。

Ormandy 氏はこの脆弱性用に ctftool という簡易スクリプト エンジンを作っており、その README に DLL Injection までの仕組みが紹介されています。どう動くかは書かれていますが、combase!CStdProxyBuffer_CF_AddRef などをどうやって見つけたかは書いてないんですよね。そこが気になります。なお本記事は、ctftool に含まれる ctf-exploit-common-win10.ctf をデバッグしながら書きました。

taviso/ctftool: Interactive CTF Exploration Tool
https://github.com/taviso/ctftool

8 月の修正でどのように直したかは見ていませんが、Microsoft の Advisory で MSCTF については触れられていないので、ALPC 側を直したんでしょうかね。8 月のパッチは必ず当てておきましょう。

CVE-2019-1162 | Windows ALPC Elevation of Privilege Vulnerability
https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1162

WNF でも、頑張って探せば RCE できる気にさせられます。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away