2014年2月24日月曜日

Lokiの脆弱性について

Samsung Galaxy S4やLG端末で有効な署名なしカーネルを焼くのに必要な
Lokiの脆弱性について元の記事を翻訳してみました。

要約すると、boot.imgからカーネルイメージとramdiskイメージをそれぞれ指定の
アドレスに読み出し、署名チェックを行ってから、有効であれば起動するという
流れになっているので、カーネルイメージ領域にカーネルとramdiskイメージを
くっつけて書き出し、ramdiskイメージにabootを書き換えるシェルコードを
置くというものになってます。

元の記事はこちら
http://blog.azimuthsecurity.com/2013/05/exploiting-samsung-galaxy-s4-secure-boot.html

翻訳はこちら
2013年4月に発売された、サムスン製Glaxy S4は発売後1ヶ月で1000万台を販売し、
今年最も売れたスマートフォンの一つに数えられることだろう。
大半のモデルがユーザー自らが自作カーネルの動作やシステムの改変を可能にしているのに対し、
AT&TとVerizonはそのような改変ができないモデルを販売している。
そこで、このブートローダーロックの仕組みと署名チェックをバイパスし、
署名なしカーネルやリカバリーが動作可能となる脆弱性について説明しよう。

AT&TとVerizonのモデルはQualcomm製のAPQ8064Tチップセットを使用している。
前回のブログで解説したMotorola製のブートローダーと同様にQualcommはQfuseという
信頼性の高い起動手順を導入している。
つまり、起動手順の各段階でQfuseを利用して暗号化チェックを行っている。
ハードウェアに関する起動手順の初期段階が完了してから、サムスン製のAPPSBLが動作する。
Glaxy S4のブートローダーロックとアンロックにはブート領域とリカバリー領域の
強制署名チェック有無の違いがある。

aboot(ブートローダー領域の名前から)はオープンソースの"lk"プロジェクトを参考に作られており、
手間のかかるリバースエンジニアリングの時間を節約することができた。
"lk"とabootを見比べることにより、署名チェックとLinuxカーネル起動のコードを見分けられた。

ブート領域とリカバリー領域に含まれているLinuxカーネルとramdiskを読み出し、検証し、
起動させるロジックはboot_linux_from_mmc()に記載されている。
まず、AndroidOSのLinuxカーネルとramdiskが含まれているブート領域かAndroidリカバリーシステムの
カーネルとramdiskが含まれているリカバリー領域を起動するかを決定する。
次に、適当な領域の最初のページをeMMCから物理メモリに読み出す。


  if (!boot_into_recovery) {
    index = partition_get_index("boot");
    ptn = partition_get_offset(index);
    if (ptn == 0) {
      dprintf(CRITICAL, "ERROR: No boot partition found\n");
      return -1;
    }
  }
  else {
    index = partition_get_index("recovery");
    ptn = partition_get_offset(index);
    if (ptn == 0) {
      dprintf(CRITICAL, "ERROR: No recovery partition found\n");
      return -1;
    }
  }

  if (mmc_read(ptn + offset, (unsigned int *) buf, page_size)) {
    dprintf(CRITICAL, "ERROR: Cannot read boot image header\n");
    return -1;
  }


上記のコードは、"lk"のロジックをそのまま流用している。
次に、ヘッダーを含むブート領域の健全性チェックを行った後、ブート領域のヘッダーに
記載されているアドレスにカーネルとramdiskを読み込む。


  hdr = (struct boot_img_hdr *)buf;

  image_addr = target_get_scratch_address();
  kernel_actual = ROUND_TO_PAGE(hdr->kernel_size, page_mask);
  ramdisk_actual = ROUND_TO_PAGE(hdr->ramdisk_size, page_mask) + 0x200;
  imagesize_actual = (page_size + kernel_actual + ramdisk_actual);
 
  memcpy(image_addr, hdr, page_size);
 
  offset = page_size;

  /* Load kernel */
  if (mmc_read(ptn + offset, (void *)hdr->kernel_addr, kernel_actual)) {
    dprintf(CRITICAL, "ERROR: Cannot read kernel image\n");
    return -1;
  }

  memcpy(image_addr + offset, hdr->kernel_addr, kernel_actual);

  offset += kernel_actual;

  /* Load ramdisk */
  if (mmc_read(ptn + offset, (void *)hdr->ramdisk_addr, ramdisk_actual)) {
    dprintf(CRITICAL, "ERROR: Cannot read ramdisk image\n");
    return -1;
  }

  memcpy(image_addr + offset, hdr->ramdisk_addr, ramdisk_actual);

  offset += ramdisk_actual;


これは、本質的には"lk"のコードと同一であり、ブートイメージを単位毎にimage_addrで
指し示す位置にコピーしている。
最終的に、イメージ全体の署名チェックを行う。
署名チェックが正しければ、カーネルは起動し、誤っていれば、改竄されているという
ワーニングを表示し、起動に失敗する。


  if (check_sig(boot_into_recovery))
  {
    if (!is_engineering_device())
    {
      dprintf("kernel secure check fail.\n");
      print_console("SECURE FAIL: KERNEL");
      while (1)
      {
        /* Display tampered screen and halt */
        ...
      }
    }
  }
 
  /* Boot the Linux kernel */
  ...


is_engineering_device()関数は単にテスト機か製品機かを示すチップセットID(ハード的に変更不可な値)から
起動手順の初期に設定されるグローバル変数の値を返却する。
check_sig()関数はabootが署名チェックの為にオープンソースのRSA実装を使用していることが分かる。
ブートローダーはaboot内のRSA-2048の公開鍵を使用してブートイメージの署名を解読し、
ブートイメージのSHA1ハッシュコードと比較する。
ブートイメージを改造すると異なるSHA1ハッシュコードが得られるので、RSA-2048をハックするか、
特別なSHA1ハッシュコードを生成するか、サムスンの秘密鍵を利用しない限りは署名済みブートイメージは作成できない。

聡明な読者は上記のプログラムコードを見てお気づきだろう。
上記で示された手順は、まずブートイメージのヘッダーで示されたアドレスにカーネルとramdiskを読み出し、
読み出しが完了してから署名チェックが行われている。
その為、信頼できないデータが含まれていても、署名チェックよりもeMMCからの読み出しを優先して行う。
結果、aboot自身を含めたカーネル又はramdiskを物理メモリに読み込むようなヘッダーの値を含む悪意のある
ブートイメージを書き込むことができる。

この流れは、まったく単純である。
abootのcheck_sig()関数の物理アドレスと等しいアドレスにramdiskをロードするブートイメージを用意した。
改変されたブートイメージでは、ramdiskが配置される位置にシェルコードを埋め込んだ。
root権限でブート領域を作成したブートイメージで置き換えた。
abootがeMMCデバイスからramdiskを読み込み、check_sig()関数をシェルコードで置き換えた。
シェルコードは単純に、ブートイメージのヘッダーに正しい値を書き込み、正しいメモリ位置に
カーネルとramdiskをコピーし、署名チェックの正常終了を意味する「0」を返却する。
結果、abootは起動処理を継続し、署名なしのカーネルとramdiskで起動する。

意見や思いつきを交換し、手助けしてくれたralekdevに感謝する。