ブログ未満のなにか

ブログなのか誰にも分からない

Midnight Sun CTF 2019 Quals writeup

はじめに

Gissa2、 Hfsipc、 Hfs-dosを解いた。 どれも良い問題だったと思う。

Gissa2

stack overflowがあり、 canaryがないのでropができる。 しかしseccomp filterによってシステムコールに制限がかかっている。 設定されるフィルターは以下の通りで、 open/openat/ execve/execeatが塞がれておりフラグを開いたりシェルを立ち上げることができない。

 line  CODE  JT   JF      K
=================================
 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 0x00000002  if (A != open) goto 0006
 0005: 0x06 0x00 0x00 0x00000000  return KILL
 0006: 0x15 0x00 0x01 0x00000038  if (A != clone) goto 0008
 0007: 0x06 0x00 0x00 0x00000000  return KILL
 0008: 0x15 0x00 0x01 0x00000039  if (A != fork) goto 0010
 0009: 0x06 0x00 0x00 0x00000000  return KILL
 0010: 0x15 0x00 0x01 0x0000003a  if (A != vfork) goto 0012
 0011: 0x06 0x00 0x00 0x00000000  return KILL
 0012: 0x15 0x00 0x01 0x0000003b  if (A != execve) goto 0014
 0013: 0x06 0x00 0x00 0x00000000  return KILL
 0014: 0x15 0x00 0x01 0x00000055  if (A != creat) goto 0016
 0015: 0x06 0x00 0x00 0x00000000  return KILL
 0016: 0x15 0x00 0x01 0x00000101  if (A != openat) goto 0018
 0017: 0x06 0x00 0x00 0x00000000  return KILL
 0018: 0x15 0x00 0x01 0x00000142  if (A != execveat) goto 0020
 0019: 0x06 0x00 0x00 0x00000000  return KILL
 0020: 0x06 0x00 0x00 0x7fff0000  return ALLOW

フィルターをバイパスする方法は単純でx32の方のシステムコールを呼ぶ。 システムコールを呼ぶ際に設定するシステムコール番号を+0x40000000すると、 x32として扱われる。 x32でopenを呼ぶ場合は、 システムコール番号は2+0x40000000で良い。 この時上記のseccomp filterでのsys_numberの制限に引っかかることはない。

x64におけるx32でのシステムコールについては下記のリンクに詳細がある。

RFD: x32 ABI system call numbers [LWN.net]

exploit

システムコールの制限は回避可能なので、 フラグを開いて中身を読むだけである。 Midnight Sun CTF 2019 Quals Gissa2 · GitHub

Hfsipc

Linuxのkernel exploit。 脆弱なLKMがロードされているので、 これを攻略する。 ioctlで諸々の操作ができ、 kernel heap上にオブジェクトを作ったり破棄したり編集したりできる。 扱うオブジェクトの構造は以下のようになっていた。 どのオブジェクトを操作するのかをkeyで指定する。 bufは、 オブジェクトを作成するときに指定するサイズで確保したkmalloc()の返り値が入る。 sizeにそのサイズが入る。

struct obj {
    long key;
    char* buf;
    long size;
};

ioctl()経由で、 オブジェクトのbufの中身を読み書きできる。 書き込む際に、 作成したサイズよりも1byte多く書き込むことができるのでoff-by-one overflowとなっている。

exploit

linux kernelで使用されるSLUB allocatorでは、 解放済みのチャンクがリンクドリストで繋がっている。 off-by-one overflowを利用すると、 リンクドリストのポインタの最下位1byteを書き換えることができる。 これを利用して、 kmalloc()で取得する領域をuserlandに強制することができる。 あとは適当にkernelspaceのアドレスをリークして、 modprobe_pathを改変してrootを取った。

Midnight Sun CTF 2019 Quals Hfsipc · GitHub

Hfs-dos

改変されたCOMMAND.COMが動作するFreeDOSCOMMAND.COMでは、 入力した文字列に対応する文字列を返すようになっている。 削除文字(0x7f)を入力すると、 こちらからの入力を保存するバッファへのポインタをデクリメントすることができる。 このデクリメントに制限がなく、 COMMAND.COMのtext領域まで持っていくことができる。 古い時代のアーキテクチャ(i8086)で動作している、 かつ古のOSであるため、 メモリの保護機構など存在せずtext領域でも書き換えることが可能となっている。

exploit

jmpする先を改変し、 文字列FLAG1をFLAG2に改変した。 jmpする先は、 COMMAND.COMが最初にFLAG1を表示する関数群へと変えており、FLAG1をFLAG2にしているので、 2つ目のフラグを得られる。

Midnight Sun CTF 2019 Quals Hfs-dos · GitHub

Midnight Sun CTF Finals Flitbip writeup

はじめに

一人writeup advent calendarの7日目です。 1日1問分のwriteupを目標に頑張っていきます。 7日目の問題は、Midnight Sun CTF Finalsで出題された「Flitbip」。 初めての人にオススメです。

カーネルの情報 (セキュリティ機構など)

カーネルのバージョンは、4.17。 smep, smap、kaslr、kptiが無効になっている。

/ $ uname -a
Linux (none) 4.17.0 #1 Fri Jun 15 18:23:33 CEST 2018 x86_64 GNU/Linux
/ $ cat /proc/cpuinfo | grep flags
flags       : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm nopl cpuid pni cx16 hypervisor lahf_lm svm 3dnowprefetch vmmcall

解析

この問題では脆弱性を持つシステムコールが新しく追加されている。 親切にも、新しいシステムコールソースコードが存在するので、カーネルのバイナリを解析する必要は特にない。 システムコールは、引数で指定したアドレスの指定したビットを反転できる。 しかし反転できる回数に制限があり、flit_countがMAXFLIT(=1)以上の場合ビットを反転させることができない。

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/syscalls.h>

#define MAXFLIT 1

#ifndef __NR_FLITBIP
#define FLITBIP 333
#endif

long flit_count = 0;
EXPORT_SYMBOL(flit_count);

SYSCALL_DEFINE2(flitbip, long *, addr, long, bit)
{
        if (flit_count >= MAXFLIT)
        {
                printk(KERN_INFO "flitbip: sorry :/\n");
                return -EPERM;
        }

        *addr ^= (1ULL << (bit));
        flit_count++;

        return 0;
} 

exploit

kaslrがないため、カーネルがロードされいているアドレスは固定である。 よって、.text、.data、.bssといった領域が存在するアドレスは固定となっている。

まずは、そのままでは1度しかビット反転させることができないので、flit_countの最上位ビットを反転させ、ビット反転の回数の条件を解決する。 最上位ビットを反転させて1を立てることで、flit_countは負数となり、符号付の比較を突破でき、複数回のビット反転が可能になる。

任意の箇所に複数回のビット反転が行えるので、書き込み可能でグローバルな領域に存在する関数テーブルを書き換えていく。 最近のLinuxカーネルexploit問に対するテクニック集3 - HackMD によると、n_tty_opsという関数テーブルが書き込み可能でグローバルである。 smepが無効となっているので、ユーザ空間に諸々の処理を行う関数へとn_tty_opsのreadを向けさせる。

root権限を取るまでの流れだが、基本に忠実にcredのuid系の値を全て0で書き換える方針を取った。 current_taskは、現在実行しているタスクのtask_structを指していており、グローバル変数である。 current_task->cred->uidといった感じで参照していくことで、uidを書き換えられる。

gist.github.com

f:id:hama7230:20181219233317p:plain

最近のCTFで出題されるglibc heap問で個人的によく使うテクニックについて

はじめに

これは CTF Advent Calendar 2018 - Adventar の8日目の記事です。 7日目は、@bata_24 さんの 「WCTF 2018 - klist Writeup - HackMD 」でした。

この記事は、タイトル通りに私が個人的に「よく使うなぁ・他のwriteupでよく見るなぁ」と感じたテクニックの紹介です。 ですが紹介なので、glibc heapの挙動について詳細な説明は特にしないです。 テクニックが何故通用するのかは各自でソースコード読んで理解してください。 また、glibcのバージョンによっては通用しないものもあるので注意してください。 テクニックの名前自体はクソ適当です。

fastbin attack with 0x70 chunk

概要

結構前から汎用的に使われているテクニック。 fastbinに繋がれたチャンクをmalloc()などで取り出す時に、チェックが甘いことに起因する。 libcのdata, bssにあるデータを利用して、その付近のメモリをmalloc()で取得するテクニック。

使用条件

  • UAFやheap bofなどで、free済みのチャンクのfdを任意に制御できる
  • 0x59から0x68までのサイズでmalloc()を呼べる
  • libcのアドレスが分かっている(これが絶対かは問題による)

やり方

x64のlinuxでは、libcなどのライブラリが配置されるアドレスは、0x7fXX_XXXXXXXXとなっている。 アドレスをズラして見ると、0x7fと見ることができ、fastbinに繋げられるchunkのsizeとしてvalidになる。 つまり、fastbin関連のチェックをすり抜けられる。

__malloc_hookをターゲットとした例を説明する。 今回の__malloc_hookが存在するアドレスは、0x7ffff7dd1b10となっている。 それよりも前のメモリがどうなっているか調べると、いくつか0x7fXX_XXXXXXXXとなる値が存在する。 これを数byteズラして見ると、0x7fをサイズとして利用できる。

__malloc_hookのアドレス
gdb-peda$ p &__malloc_hook
$3 = (void *(**)(size_t, const void *)) 0x7ffff7dd1b10 <__malloc_hook>

適当に数byte前を見てみる
gdb-peda$ x/8gx 0x7ffff7dd1b10-0x20
0x7ffff7dd1af0 <_IO_wide_data_0+304>: 0x00007ffff7dd0260  0x0000000000000000
0x7ffff7dd1b00 <__memalign_hook>: 0x00007ffff7a92e20  0x00007ffff7a92a00
0x7ffff7dd1b10 <__malloc_hook>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1b20 <main_arena>:  0x0000000000000000  0x0000000000000000

適当にズラして0x7fとする
gdb-peda$ x/8gx 0x7ffff7dd1b10-0x20-3
0x7ffff7dd1aed <_IO_wide_data_0+301>: 0xfff7dd0260000000  0x000000000000007f
0x7ffff7dd1afd: 0xfff7a92e20000000  0xfff7a92a0000007f
0x7ffff7dd1b0d <__realloc_hook+5>:    0x000000000000007f  0x0000000000000000
0x7ffff7dd1b1d: 0x0000000000000000  0x0000000000000000

今、0x70のチャンクがfastbinに繋がっている。 このチャンクのfdに先ほどのアドレス(0x7ffff7dd1aed)を書き込み、リストに繋げる。 そしてmalloc()していくと、その領域を取ることができる。

sizeが0x70のchunkが1つ存在しfreeされて、fastbinに繋がっている。
gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x602000 --> 0x0
(0x80)     fastbin[6]: 0x0
                  top: 0x6020e0 (size : 0x20f20)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0

そのchunkのfdを、uafなどで上書きできたと想定して、先ほどのアドレスを書き込む
gdb-peda$ set {long}0x602010=0x7ffff7dd1aed

エラーと言われるが気にしない
gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x602000 --> 0x7ffff7dd1aed (size error (0x78)) --> 0xfff7a92e20000000 (invaild memory)
(0x80)     fastbin[6]: 0x0
                  top: 0x6020e0 (size : 0x20f20)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0

先ほどのメモリがmalloc()で返ってくる
gdb-peda$ call malloc(0x68)
$5 = (void *) 0x602010
gdb-peda$ call malloc(0x68)
$6 = (void *) 0x7ffff7dd1afd

unsotedbin attack to libc data region

概要

これも結構前からあるテクニック。 unsortedbin attackは、結果として任意の箇所に0x7fXX_XXXXXXXXとなる値を書き込むことができるが、どこに対して書き込むのかという話。 stdin/stdoutのバッファのポインタを書き換えて、バッファの範囲を変えると良い。

前提条件

  • unsortedbin attackができる
  • libcのアドレスが分かっている

やり方

よくやる/見るのが、stdinが持つbufのポインタに対して。 pwnで出題されるバイナリは、入出力のバッファリングを無効にしているケースが多い。 無効といっても、stdin/stdoutが持つメンバである_shortbufをバッファとして使用している。 scanf()などが呼ばれると、_shortbufにデータが書き込まれている。 バッファを指すポインタをunsortedbin attackで上書きして、バッファと見なす範囲を広げて不正に上書きできる状態を作り出すのが、このテクニックの肝である。

例を上げて流れを説明する。 例では、stdinの_IO_write_endをunsortedbin attackで上書きする。

setbuf(stdin, NULL)を実行した後のstdinの状態は以下のようになっている。 read/write用のバッファは、stdinの内部に持っている_shortbufを指している。

gdb-peda$ x/32gx 0x7ffff7dd18e0
0x7ffff7dd18e0 <_IO_2_1_stdin_>:  0x00000000fbad208b  0x00007ffff7dd1963
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1900 <_IO_2_1_stdin_+32>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1910 <_IO_2_1_stdin_+48>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1920 <_IO_2_1_stdin_+64>:   0x00007ffff7dd1964  0x0000000000000000
0x7ffff7dd1930 <_IO_2_1_stdin_+80>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1940 <_IO_2_1_stdin_+96>:   0x0000000000000000  0x0000000000000000
0x7ffff7dd1950 <_IO_2_1_stdin_+112>:  0x0000000000000000  0xffffffffffffffff
0x7ffff7dd1960 <_IO_2_1_stdin_+128>:  0x0000000000000000  0x00007ffff7dd3790
0x7ffff7dd1970 <_IO_2_1_stdin_+144>:  0xffffffffffffffff  0x0000000000000000
0x7ffff7dd1980 <_IO_2_1_stdin_+160>:  0x00007ffff7dd19c0  0x0000000000000000
0x7ffff7dd1990 <_IO_2_1_stdin_+176>:  0x0000000000000000  0x0000000000000000
0x7ffff7dd19a0 <_IO_2_1_stdin_+192>:  0x0000000000000000  0x0000000000000000
0x7ffff7dd19b0 <_IO_2_1_stdin_+208>:  0x0000000000000000  0x00007ffff7dd06e0
0x7ffff7dd19c0 <_IO_wide_data_0>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd19d0 <_IO_wide_data_0+16>:  0x0000000000000000  0x0000000000000000

gdb-peda$ p *(struct _IO_FILE *) 0x7ffff7dd18e0
$5 = {
  _flags = 0xfbad208b,
  _IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_buf_end = 0x7ffff7dd1964 <_IO_2_1_stdin_+132> "",
  _IO_save_base = 0x0,
  _IO_backup_base = 0x0,
  _IO_save_end = 0x0,
  _markers = 0x0,
  _chain = 0x0,
  _fileno = 0x0,
  _flags2 = 0x0,
  _old_offset = 0xffffffffffffffff,
  _cur_column = 0x0,
  _vtable_offset = 0x0,
  _shortbuf = "",
  _lock = 0x7ffff7dd3790 <_IO_stdfile_0_lock>,
  _offset = 0xffffffffffffffff,
  _codecvt = 0x0,
  _wide_data = 0x7ffff7dd19c0 <_IO_wide_data_0>,
  _freeres_list = 0x0,
  _freeres_buf = 0x0,
  __pad5 = 0x0,
  _mode = 0x0,
  _unused2 = '\000' <repeats 19 times>
}

今、0x110byteのサイズを持つチャンクがunsortedbinに繋がっている。 このチャンクのbkを書き換えて、malloc(0x100)を実行すると、unsortedbin attackが発生する。 stdinの内部に書き込まれた0x7ffff7dd1b78をバッファの終端として使用するようになるため、大きくlibcの中身に書き込める状態を作れた。

unsortedbinが1つ存在している。
gdb-peda$ parseheap
addr                prev                size                 status              fd                bk
0x602000            0x0                 0x110                Freed     0x7ffff7dd1b78    0x7ffff7dd1b78

bkを上書きする
gdb-peda$ set {long}0x602018=0x7ffff7dd1910

gdb-peda$ parseheap
addr                prev                size                 status              fd                bk
0x602000            0x0                 0x110                Freed     0x7ffff7dd1b78    0x7ffff7dd1910

malloc()を呼んで、unsortedbin attackを起こす
gdb-peda$ call malloc(0x100)
$7 = (void *) 0x602010

stdinの状態を確認すると、_IO_buf_endが書き換わっている。
gdb-peda$ x/20gx 0x7ffff7dd18e0
0x7ffff7dd18e0 <_IO_2_1_stdin_>:  0x00000000fbad208b  0x00007ffff7dd1963
0x7ffff7dd18f0 <_IO_2_1_stdin_+16>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1900 <_IO_2_1_stdin_+32>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1910 <_IO_2_1_stdin_+48>:   0x00007ffff7dd1963  0x00007ffff7dd1963
0x7ffff7dd1920 <_IO_2_1_stdin_+64>:   0x00007ffff7dd1b78  0x0000000000000000

gdb-peda$ p *(struct _IO_FILE *) 0x7ffff7dd18e0
$8 = {
  _flags = 0xfbad208b,
  _IO_read_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_read_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_read_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_ptr = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_write_end = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_buf_base = 0x7ffff7dd1963 <_IO_2_1_stdin_+131> "",
  _IO_buf_end = 0x7ffff7dd1b78 <main_arena+88> "\200!`",

_IO_str_jumps

stdin/stdoutなどにはvtableのポインタがあり、制御を奪取する際の狙い目である。 そのためかポインタが正常なものかチェックが入るようになった。 回避方法として、_IO_str_jumpsは手軽で汎用性が高い。

詳しくは以下のリンク先を参照

veritas501.space

おわりに

ということで、以上が個人的によく使うテクニックでした。

明日は、@N4NU さんの「各種OSのUserlandにおけるPwn入門」です。 windowslinux以外のosについても紹介されそうなタイトルで、楽しみですね。

あと宣伝ですが、1人1日1writeup advent calendarというのを細々とやっています。 そちらも良かったら読んでいってください。 ただ既にwriteupのストックはないし、修論などで禿げそうなのでadvent calendarは失敗しました。

hama.hatenadiary.jp

0CTF 2017 Finals cred_jar writeup

はじめに

一人writeup advent calendarの6日目です。 1日1問分のwriteupを目標に頑張っていきます。 6日目の問題は、0CTF 2017 Finalsで出題された「cred_jar」。 race conditionを起点としたkernel exploit問題で初めての人にオススメです。

カーネルの情報 (セキュリティ機構など)

カーネルのバージョンが4.12で、smep, kaslrが有効になっている。 kernel exploitでよく使われる/dev/ptmxが存在しない。

% cat boot.sh
#!/bin/bash

qemu-system-x86_64 -initrd rootfs -kernel bzImage -append 'console=hvc0 root=/dev/ram rdinit=/linuxrc oops=panic panic=1 quiet kaslr' -m 128M -chardev stdio,id=k33n -device virtio-serial -device virtconsole,chardev=k33n -display none -monitor none -smp cores=1,threads=1 -cpu kvm64,+smep

[in qemu]
/ $ uname -a
Linux (none) 4.12.0-rc1 #25 SMP Fri May 26 16:51:03 CST 2017 x86_64 GNU/Linux
/ $ cat /proc/cpuinfo | grep flags
flags       : fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl cpuid pni cx16 hypervisor smep
/ $ ls /dev/
console   cred_jar

解析

攻略対象のデバイス/dev/cred_jar。 デバイスに対する操作は以下のように定義されている。

struct file_operations cred_jar_fops = {
    .open    = cred_jar_open,
    .release = cred_jar_release,
    .read    = cred_jar_read,
    .write   = cred_jar_write,
    .ioctl   = cred_jar_ioctl
};

cred_jar_ioctl()がメインとなっている箇所で、指定するcmdによって下の構造体jarに関する操作が行える。 行える処理は、0x7401:alloc、0x7402:get、0x7403:get id、0x7404:change id、の4つである。 file構造体のprivate_dataで、jarへのポインタを保存している。

allocは、指定したサイズとjar構造体の大きさの合計でkamlloc()して領域を確保する。 確保したら、メンバを適切に初期化して、リンクドリストに繋ぎ、private_dataにポインタを入れる。

id指定で、jarを選択する。指定したidを持つjarが存在したら、private_dataにポインタを入れる。 一度getすると、他のjarを選び直すことはできない。 read/writeは、選択したjarのbufを読み書きできる (bofはない)。

get idとchange idはそのままの処理。get idは選択しているjarのidを得られる。change idは選択しているjarのidを変更できる。

refcountはそのjarへの参照数である。 allocした時点で、参照数は1。 getされたら参照数は増えて、jarを選択しているfdがclose()されたら参照数は減る。

struct jar {
    int refcount;
    int id;
    struct jar *next;
    struct jar *prev;
    unsigned long size;
    char buf;
};

exploit

getはget_cred_jar_ctx()が実行され、close()するときにはput_cred_jar_ctx()が実行される。

struct jar* get_cred_jar_ctx(int id) {
    struct jar* ptr;
    mutex_lock(&ctx_lock);

    if ( ctx_list != &ctx_list ) {
        ptr = ctx_list;
        while(1) {

            if (ptr == &ctx_list)
                break;
            if (ptr->id == id ) {
                _InterlockedIncrement(ptr->count);
                break;
            }
                        
            ptr = ptr-next;
        }
    }
    ptr = 0LL;
    mutex_unlock(&ctx_lock);
    return ptr;
}

int put_cred_jar_ctx(struct jar *jar) {
    if (jar) {
        if (!_InterlockedDecrement(jar->count)) {
            mutex_lock(&ctx_lock);
            // ここにunlinkの処理
            mutex_unlock(&ctx_lock);
            kzfree(a1);
        }
    }
    return 0;
}

put_cred_jar_ctx()は、refcountをデクリメントし0になった場合 (他からも参照されていない状態)、kfree()を実行する。 しかし、refcountの確認をした後にmutex_lock()が走るので、race conditionとなる可能性がある。

対してget_cred_jar_ctx()は、まずmutex_lock()が走る。 ロック後に、指定したidを持つjarをリストを辿って検索する。 指定のidを持つjarが見つかったら、refcountをインクリメントする。 そしてreturnする前にmutex_unlock()が走る。

put_cred_jar_ctx()とget_cred_jar_ctx()を上手く使うことで、参照している状態にあるjarをkfree()することができる。

race conditionが発生してkUAFとなる具体的な流れを詳しく説明する。 ここで、get_cred_jar_ctx()にて目的のidを持つjar Xを探索している途中に、jar Xを参照するfdがclose()されてput_cred_jar_ctx()が実行されたと仮定する。 jar Xはclose()したfdだけが参照している状態、つまりrefcountは1であるとする。

get_cred_jar_ctx()でmutex_lock()が実行されているが、put_cred_jar_ctx()のmutex_lock()は_InterlockedDecrement()後にあるため、jar Xを参照するrefcountのデクリメント自体は問題なく実行される。jar Xのrefcountは元々1だったので、デクリメントされて0になる。jar Xをリンクドリストから外しkfree()を実行する方向へ分岐が進むが、mutex_lock()があるため、get_cred_jar_ctx()の処理が終わるまで、put_cred_jar_ctx()を実行しているスレッドは待機する。 get_cred_jar_ctx()ではjar Xを見つけて、mutex_unlock()が走り、file->private_dataにjar Xへのポインタが格納される。 もう片方のput_cred_jar_ctx()では、mutex_lock()の停止から復帰してkfree()が実行される。 しかしjar Xへの参照はget側で残ったままなので、kUAFとなる。

雑に説明すると、同じjarに対して、スレッドAでget_cred_jar_ctx()、スレッドBでput_cred_jar_ctx()を実行すると、スレッドAでgetしたjarがなぜか知らないうちにkfree()されているという感じ。

SLUBアロケータでは、kfree()したチャンクの先頭8byteはfree済みのチャンクを繋ぐためのポインタになっている。 そのためkUAFが起きた場合、idには設定したものと異なる値が入っているので、idを確認して違う値だった場合はrace conditionに成功してkUAFが起きたと判断できる。

race conditionを起こしてkUAFが起こせるので、別のオブジェクトをラップさせてroot権限を取っていく。 まずはカーネルのアドレスのリークを狙う。 今回のカーネルは、/dev/ptmxがいないので、共有メモリのshmget()を呼んだときに割り当てられるfile構造体を使用した。 file構造体はkmalloc-256から取られるので、alloc_cred_jar()に渡すサイズは256-0x20にする。(関連する部分のコードは次のリンク先に Linux source code: ipc/shm.c (v4.12) - Bootlin, Linux source code: fs/file_table.c (v4.12) - Bootlin)_

file構造体のf_opからカーネルのベースアドレスを求めることができ、さらにf_opを書き換えれば制御の奪取が可能である。 smapは無効なので、ユーザ空間に偽のfile_operationsを用意できる。

struct file {
    union {
        struct llist_node  fu_llist;
        struct rcu_head    fu_rcuhead;
    } f_u;
    struct path        f_path;
    struct inode       *f_inode;   /* cached value */
    const struct file_operations  *f_op; // このポインタの値を知ればカーネルのベースアドレスが求まるし、書き換えれば制御が奪える。

    以下省略

偽のfile_operationsのioctlに、ユーザ空間へとstackをpivotするガジェットを入れてropした。 pivot先に目的のrop chainを用意しておきcommit_creds(prepare_kernel_commit(0))を実行して、root権限を取った。

gist.github.com

f:id:hama7230:20181205190753j:plain

CODE GRAY CTF sured writeup

はじめに

一人writeup advent calendarの5日目です。1日1問分のwriteupを目標に頑張っていきます。 5日目の問題は、CODE GRAY CTFで出題された「sured」。race conditionを使った問題で初めての人にオススメです。

初期調査

セキュリティ機構は以下の通りで、特筆すべきところはcanaryがなくstackでbofがあれば、簡単に制御を奪えること。

[root@ubuntu] ~/ctf/CODEGLAY/sured
# checksec ./23016_sured
[*] '/root/ctf/CODEGLAY/sured/23016_sured'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

解析

実行するとこちらからの入力を受け付けており、入力した文字列を返してくる。 LEAVEという文字列を送ると処理が終了する。

[root@ubuntu] ~/ctf/CODEGLAY/sured
# ./23016_sured
Hello, visitor. Leave your comment!
comment:
hoge
Your comment: hoge
comment:
hogehoge
Your comment: hogehoge
comment:
LEAVE
Your comment: LEAVE
Goodbye visitor!

このバイナリは、起動してからスレッドを1つ作成している。 メインをスレッドA、作成したもう片方をスレッドBとすると、それぞれのスレッドは以下の処理を行う。

スレッドAは、標準入力から文字列を読み込む。 読み込んだ文字列はstd::stringとして保存され、文字列の長さをbss領域に保存する。 文字列の読み込みと長さの保存をループで繰り返すようになっている。

スレッドBでは、入力された文字列がLEAVEで始まるかどうか確認している。 bssに保存されている文字列の長さによって、文字列をstackにコピーする処理が異なっている。 長さが0x100byte以下なら、長さ分だけコピーし、0x100byteより大きいなら、0x100byteだけコピーする。 コピーされた文字列がLEAVEで始まる場合、全体の処理を終える。

重要な処理だけのコードは以下のようになっていた。

std::string buf;
int n;

int thread(void) {
    char dest[0x100];
    do {
        char* data = buf.data();
        if (n <= 0x100) {
            memcpy(dest, data, n);
        } else {
            qmemcpy(dest, data, 0x100);
        }
    } while(strncmp(dest, "LEAVE", 5));
    return 1;
}

int main(void) {
    int result = 0;
    // threadを開始する処理、threadの返り値がresultに入る。
    std::thread::_M_start_thread(); ?????

    std::cout << "Hello, visitor. Leave your comment!" << std::endl;

    while(result) {
        std::cout << "comment: " << std::endl;
        std::cin >> buf;
        n = buf.length();
        std::cout << "Your comment: " << buf.data() << std::endl;
    }
    std::cout << "Goodbye visitor!" << std::endl;
    return 0;
}

exploit

各スレッドでは文字列とその長さを扱っているが、それぞれの処理で文字列に対するロックがないためrace conditionが発生する。 race conditonを起こすには以下のようにして長い文字列と短い文字列を交互に送信する。

  1. はじめに0x100byte以下の短い文字列を入力する。
  2. スレッドBで長さのチェックが行われる。入力文字列が0x100byte以下であるため、文字列の長さnでmemcpy()が実行される方へと分岐する。
  3. 長さのチェックが終わってからmemcpy()が実行されるまでの間に、0x100byteを超える長い文字列を入力する
  4. チェック後のnが書き換わり0x100byteを超えた値で、memcpy()が実行される。
  5. stackに用意された配列は0x100byteなので、それ以上の大きさだとstack overflowとなる。

race conditionが上手く行くかどうかネットワークの状況により、成功率は高くない。 リモートではたまたま1度だけ上手く行った時にフラグを取ることができた。

CODE GRAY CTF sured · GitHub

[root@ubuntu] ~/ctf/CODEGLAY/sured
# python exploit.py
[*] '/root/ctf/CODEGLAY/sured/23016_sured'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[+] Starting local process './23016_sured': pid 12741
[*] Pwning
[*] Switching to interactive mode
Hello, visitor. Leave your comment!
comment:
Your comment: 1
comment:
Your comment: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx$
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
comment:

(長いので省略)

Your comment: LEAVE
comment:
$ id
uid=0(root) gid=0(root) groups=0(root)

フラグ: FLAG{h4v3_4_v4ri0us_p3r5p3ct1v3_v13w_c4n_f17d_bu9}

Sharif CTF 2018 kdb writeup

はじめに

一人writeup advent calendarの4日目です。1日1問分のwriteupを目標に頑張っていきます。 4日目の問題は、Sharif CTF 2018で出題された「kdb」。kUAF (kernel Use After Free) を起点としたkernel exploit問題で初めての人にオススメです。

カーネルの情報 (セキュリティ機構など)

配布ファイルは以下の通り。 run.shを中身を見てみると、smep、kaslrが有効になっている。

% ls
bzImage rootfs.cpio run.sh* 

% cat run.sh
#!/bin/sh

qemu-system-x86_64 -cpu kvm64,+smep -m 64M -kernel ./bzImage -initrd ./rootfs.cpio -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" -smp cores=2,threads=1,sockets=1 -monitor /dev/null -nographic 2>/dev/null

解析

攻略対象は/dev/kdb/dev/kdbに対する操作はopenやreleaseなどが定義されているが、ioctl()がメインの処理となっている。 ioctl(fd, cmd, args);と言った感じでユーザ空間から呼んでやると以下の処理を実行する。 cmdによって実行する内容が異なっている。

  • 0x13371338: alloc_buf()を呼んでbufを新規作成。argsは先頭20byteは作成されたbufのnameに書き込まれる。次の8byteはsizeで、kmalloc()を呼ぶ時の引数となっている。

  • 0x13371339: find_cbuf()でbufの探索。argsは先頭20byteで指定した文字列と合致するnameを持つbufを探す。argsの次の8byteは使われない。そのまた次の8byteがユーザ空間のポインタとなっている。copy_to_user()で見つけたbufのptrの中身をsize分だけ書き出す。

  • 0x1337133a: 0x13371339の逆の処理で、copy_from_user()で書き込む。

  • 0x1337133d: find_cbuf()で探索したbufが存在した場合、free_buf()で解放する。
  • 0x1337133f: argsにはname[0x20]; user_ptr; len;ように30byte分設定しておく。find_cbuf()でbufを探索し、見つかったbufのsizeが、lenより小さい場合、kfree(ptr)してから新しくkmalloc(len)で領域を確保する。その後copy_from_user()で書き込む。lenが超えていない場合は、元のptrにそのままcopy_from_user()で書き込む。

使用される構造体は以下の通り。双方向のリンクドリストとなっている。

struct buf {
    char name[0x20];
    char* ptr;
    long size;
    struct buf *next;
    struct buf *prev;
};

exploit

ioctl(0x1337133F)には、kfree()した後にポインタを適切に処理せずに終了するパスが存在する。 つまり、ioctl()を呼ぶ時の引数を工夫することで、kUAFがおこせる。 具体的には、まず引数で設定するサイズはオブジェクトのサイズよりも大きくしてkfree()を実行させる。 その後、書き込む元のユーザランドのポインタとサイズの合計がユーザ空間内に収まっているかチェックが行われる。 このチェックに引っかかるとkfree()したポインタはそのままでretrunするためkUAFとなる。

この問題環境はkaslrが有効なので、まずはカーネル空間のアドレスをリークする必要がある。 取得した領域は初期化されず、さらに取得した領域から自由に読み込みができるので、未初期化の空間からカーネル空間のアドレスを特定できそうなアドレスを得る。 具体的には、/dev/ptmxのオープン時に作成されるtty_struct構造体がもつメンバであるopsのアドレスからカーネル空間に関するリークができる。 opsはptm_unix98_opsを指しており、これはカーネル空間のdata(?)を指している。 そのため、そのアドレスからカーネルがマップされているベースのアドレスが計算できる。

リーク後は、ropでcommit_creds(prepare_kernel_commit(0));を実行後にswapgs+iretqでユーザ空間へ戻り、シェルを起動する。 このropの流れは、今までのwriteupのものと同じなので詳細については省略する。

gist.github.com

f:id:hama7230:20181120215120p:plain

NCSTISC 2018 babydriver writeup

はじめに

一人writeup advent calendarの3日目です。1日1問分のwriteupを目標に頑張っていきます。 3日目の問題は、NCSTISC CTF 2018で出題された「babydriver」。kUAF (kernel Use After Free) を起点としたkernel exploit問題で初めての人にオススメです。

カーネルの情報 (セキュリティ機構など)

配布されるファイルは以下の通り。boot.shが起動スクリプトで、起動するとqemuが立ち上がる。 boot.shの中身を見ると、smepが有効になっている。また、起動してカーネルのバージョンを確認すると、4.4系であった。

% ls
boot.sh* bzImage* rootfs.cpio*

% cat  boot.sh
#! /bin/sh

qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep

[In qemu]
/ $ uname -a
Linux (none) 4.4.72 #1 SMP Thu Jun 15 19:52:50 PDT 2017 x86_64 GNU/Linux

/ $ cat /proc/cpuinfo | grep flags
flags           : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx lm constant_tsc nopl pni cx16 x2apic hypervisor smep

解析

rootfs.cpioを展開すると、/lib/modules/4.4.72/babydriver.koに攻略対象となるカーネルモジュールがある。 babydriver_init()がモジュールの初期化関数で、/dev/babydevを作成し登録している。 デバイスに対する動作は、以下の通りとなっている。

  • babyopen : ユーザから/dev/babydevをopen()すると呼ばれる。カネールモジュールのbss領域にあるbabydev_structを初期化する (構造は以下の通り)。device_bufにkmem_cache_alloc_trace()で得られたポインタを入れ、device_buf_lenに0x40を入れる。kmem_cache_alloc_trace()で取得されるチャンクのサイズは0x40となっている。

  • babyrelease :/dev/babydevをclose()すると呼ばれる。kfree(babydev_struct.device_buf)を呼ぶ。

  • babywrite : /dev/babydevにwrite()すると呼ばれる。device_buf_lenがwrite()で書き込むサイズよりも大きい場合、copy_from_user(device_buf, user_buf, len)が呼ばれる。

  • babyread : /dev/babydevにread()すると呼ばれる。device_buf_lenがread()で読み込むサイズよりも大きい場合、copy_to_user(user_buf, device_buf, len)が呼ばれる。

  • babyioctl : /dev/babydevにioctl()すると呼ばれる。cmdが0x10001で呼び出すと処理が実行される。ioctl(fd, 0x10001, size)といった感じで呼ぶと、元々のbabydev_struct.device_bufをkfree()して、新しくkmalloc(size)で取得されたポインタを入れる。device_buf_lenにはsizeが入る。

struct babydev_struct {
    char* device_buf;
    long device_buf_len;
}

exploit

babydev_strudtはモジュールのbssで管理されており、/dev/babydevを複数回開いてそれぞれのfd経由で呼び出しても同じ箇所を使用する。 そこで/dev/babydevを2度open()し、片方をclose()する。 すると、close()する際にbabyrelease()が呼ばれて、babydev_struct.device_bufはkfree()されるが、もう片方のfd経由ではbabydev_struct.device_bufを参照している状態にあり、kUAFとなる。

kUAFを起こせるで、適当なオブジェクトを被せて制御を奪っていく。 今回は/dev/ptmxが存在するので、これを使う。 /dev/ptmxを開いた時、tty_struct構造体用の領域がkmalloc()で取得される。 このtty_struct構造体にはopsというメンバを持っており、これはtty_operationsで定義される関数テーブルとなっている。opsを書き換えることで、制御が奪える。 tty_struct構造体は、kmalloc-256に該当するオブジェクトなので、ioctl()で事前にサイズを256byteに変更しておく。

// https://elixir.bootlin.com/linux/v4.4.72/source/include/linux/tty.h#L259
struct tty_struct {
    int    magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops; // これを書き換えたい
    int index;

    あとは省略
}

今回はsmapが無効なので、偽のtty_operationsをユーザ空間に用意できる。 kUAFで重複している/dev/ptmxが指すopsメンバを、ユーザ空間に用意した偽のものを指すように書き換える。

root権限を取るまでの流れは、基本に忠実にropで実行してく。 ユーザ空間に予め偽のstackとrop chainを用意して、そこへpivotさせる。 ropでは、commit_creds(prepare_kernel_cred(0))を呼び出し、swapgs+iretqでユーザ空間へ復帰し、シェルを起動する。 pivotする先は、0x01740100で固定なのであらかじめmmapでその近辺を確保しておく。

kUAFで重複している/dev/ptmxopsを書き換えたままなので、そのままだとroot権限のコマンドを1回実行したらカーネルパニックとなってしまう (実際になった)。 なので、不正に上書きしたopsを正常なものに書き戻しておく必要がある。 exploit内では、一度正常な状態のttyを保存してから不正な状態に書き換えている。 そして上手くroot権限が取れた後に、正常な状態に書き戻している。

gist.github.com

参照URL