mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
synced 2024-08-23 01:10:04 +00:00
![Wander Lairson Costa](/assets/img/avatar_default.png)
The opt_num field is controlled by user mode and is not currently
validated inside the kernel. An attacker can take advantage of this to
trigger an OOB read and potentially leak information.
BUG: KASAN: slab-out-of-bounds in nf_osf_match_one+0xbed/0xd10 net/netfilter/nfnetlink_osf.c:88
Read of size 2 at addr ffff88804bc64272 by task poc/6431
CPU: 1 PID: 6431 Comm: poc Not tainted 6.0.0-rc4 #1
Call Trace:
nf_osf_match_one+0xbed/0xd10 net/netfilter/nfnetlink_osf.c:88
nf_osf_find+0x186/0x2f0 net/netfilter/nfnetlink_osf.c:281
nft_osf_eval+0x37f/0x590 net/netfilter/nft_osf.c:47
expr_call_ops_eval net/netfilter/nf_tables_core.c:214
nft_do_chain+0x2b0/0x1490 net/netfilter/nf_tables_core.c:264
nft_do_chain_ipv4+0x17c/0x1f0 net/netfilter/nft_chain_filter.c:23
[..]
Also add validation to genre, subtype and version fields.
Fixes: 11eeef41d5
("netfilter: passive OS fingerprint xtables match")
Reported-by: Lucas Leong <wmliang@infosec.exchange>
Signed-off-by: Wander Lairson Costa <wander@redhat.com>
Signed-off-by: Florian Westphal <fw@strlen.de>
450 lines
10 KiB
C
450 lines
10 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
|
|
#include <linux/module.h>
|
|
#include <linux/kernel.h>
|
|
|
|
#include <linux/capability.h>
|
|
#include <linux/if.h>
|
|
#include <linux/inetdevice.h>
|
|
#include <linux/ip.h>
|
|
#include <linux/list.h>
|
|
#include <linux/rculist.h>
|
|
#include <linux/skbuff.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/tcp.h>
|
|
|
|
#include <net/ip.h>
|
|
#include <net/tcp.h>
|
|
|
|
#include <linux/netfilter/nfnetlink.h>
|
|
#include <linux/netfilter/x_tables.h>
|
|
#include <net/netfilter/nf_log.h>
|
|
#include <linux/netfilter/nfnetlink_osf.h>
|
|
|
|
/*
|
|
* Indexed by dont-fragment bit.
|
|
* It is the only constant value in the fingerprint.
|
|
*/
|
|
struct list_head nf_osf_fingers[2];
|
|
EXPORT_SYMBOL_GPL(nf_osf_fingers);
|
|
|
|
static inline int nf_osf_ttl(const struct sk_buff *skb,
|
|
int ttl_check, unsigned char f_ttl)
|
|
{
|
|
struct in_device *in_dev = __in_dev_get_rcu(skb->dev);
|
|
const struct iphdr *ip = ip_hdr(skb);
|
|
const struct in_ifaddr *ifa;
|
|
int ret = 0;
|
|
|
|
if (ttl_check == NF_OSF_TTL_TRUE)
|
|
return ip->ttl == f_ttl;
|
|
if (ttl_check == NF_OSF_TTL_NOCHECK)
|
|
return 1;
|
|
else if (ip->ttl <= f_ttl)
|
|
return 1;
|
|
|
|
in_dev_for_each_ifa_rcu(ifa, in_dev) {
|
|
if (inet_ifa_match(ip->saddr, ifa)) {
|
|
ret = (ip->ttl == f_ttl);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
struct nf_osf_hdr_ctx {
|
|
bool df;
|
|
u16 window;
|
|
u16 totlen;
|
|
const unsigned char *optp;
|
|
unsigned int optsize;
|
|
};
|
|
|
|
static bool nf_osf_match_one(const struct sk_buff *skb,
|
|
const struct nf_osf_user_finger *f,
|
|
int ttl_check,
|
|
struct nf_osf_hdr_ctx *ctx)
|
|
{
|
|
const __u8 *optpinit = ctx->optp;
|
|
unsigned int check_WSS = 0;
|
|
int fmatch = FMATCH_WRONG;
|
|
int foptsize, optnum;
|
|
u16 mss = 0;
|
|
|
|
if (ctx->totlen != f->ss || !nf_osf_ttl(skb, ttl_check, f->ttl))
|
|
return false;
|
|
|
|
/*
|
|
* Should not happen if userspace parser was written correctly.
|
|
*/
|
|
if (f->wss.wc >= OSF_WSS_MAX)
|
|
return false;
|
|
|
|
/* Check options */
|
|
|
|
foptsize = 0;
|
|
for (optnum = 0; optnum < f->opt_num; ++optnum)
|
|
foptsize += f->opt[optnum].length;
|
|
|
|
if (foptsize > MAX_IPOPTLEN ||
|
|
ctx->optsize > MAX_IPOPTLEN ||
|
|
ctx->optsize != foptsize)
|
|
return false;
|
|
|
|
check_WSS = f->wss.wc;
|
|
|
|
for (optnum = 0; optnum < f->opt_num; ++optnum) {
|
|
if (f->opt[optnum].kind == *ctx->optp) {
|
|
__u32 len = f->opt[optnum].length;
|
|
const __u8 *optend = ctx->optp + len;
|
|
|
|
fmatch = FMATCH_OK;
|
|
|
|
switch (*ctx->optp) {
|
|
case OSFOPT_MSS:
|
|
mss = ctx->optp[3];
|
|
mss <<= 8;
|
|
mss |= ctx->optp[2];
|
|
|
|
mss = ntohs((__force __be16)mss);
|
|
break;
|
|
case OSFOPT_TS:
|
|
break;
|
|
}
|
|
|
|
ctx->optp = optend;
|
|
} else
|
|
fmatch = FMATCH_OPT_WRONG;
|
|
|
|
if (fmatch != FMATCH_OK)
|
|
break;
|
|
}
|
|
|
|
if (fmatch != FMATCH_OPT_WRONG) {
|
|
fmatch = FMATCH_WRONG;
|
|
|
|
switch (check_WSS) {
|
|
case OSF_WSS_PLAIN:
|
|
if (f->wss.val == 0 || ctx->window == f->wss.val)
|
|
fmatch = FMATCH_OK;
|
|
break;
|
|
case OSF_WSS_MSS:
|
|
/*
|
|
* Some smart modems decrease mangle MSS to
|
|
* SMART_MSS_2, so we check standard, decreased
|
|
* and the one provided in the fingerprint MSS
|
|
* values.
|
|
*/
|
|
#define SMART_MSS_1 1460
|
|
#define SMART_MSS_2 1448
|
|
if (ctx->window == f->wss.val * mss ||
|
|
ctx->window == f->wss.val * SMART_MSS_1 ||
|
|
ctx->window == f->wss.val * SMART_MSS_2)
|
|
fmatch = FMATCH_OK;
|
|
break;
|
|
case OSF_WSS_MTU:
|
|
if (ctx->window == f->wss.val * (mss + 40) ||
|
|
ctx->window == f->wss.val * (SMART_MSS_1 + 40) ||
|
|
ctx->window == f->wss.val * (SMART_MSS_2 + 40))
|
|
fmatch = FMATCH_OK;
|
|
break;
|
|
case OSF_WSS_MODULO:
|
|
if ((ctx->window % f->wss.val) == 0)
|
|
fmatch = FMATCH_OK;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (fmatch != FMATCH_OK)
|
|
ctx->optp = optpinit;
|
|
|
|
return fmatch == FMATCH_OK;
|
|
}
|
|
|
|
static const struct tcphdr *nf_osf_hdr_ctx_init(struct nf_osf_hdr_ctx *ctx,
|
|
const struct sk_buff *skb,
|
|
const struct iphdr *ip,
|
|
unsigned char *opts,
|
|
struct tcphdr *_tcph)
|
|
{
|
|
const struct tcphdr *tcp;
|
|
|
|
tcp = skb_header_pointer(skb, ip_hdrlen(skb), sizeof(struct tcphdr), _tcph);
|
|
if (!tcp)
|
|
return NULL;
|
|
|
|
if (!tcp->syn)
|
|
return NULL;
|
|
|
|
ctx->totlen = ntohs(ip->tot_len);
|
|
ctx->df = ntohs(ip->frag_off) & IP_DF;
|
|
ctx->window = ntohs(tcp->window);
|
|
|
|
if (tcp->doff * 4 > sizeof(struct tcphdr)) {
|
|
ctx->optsize = tcp->doff * 4 - sizeof(struct tcphdr);
|
|
|
|
ctx->optp = skb_header_pointer(skb, ip_hdrlen(skb) +
|
|
sizeof(struct tcphdr), ctx->optsize, opts);
|
|
if (!ctx->optp)
|
|
return NULL;
|
|
}
|
|
|
|
return tcp;
|
|
}
|
|
|
|
bool
|
|
nf_osf_match(const struct sk_buff *skb, u_int8_t family,
|
|
int hooknum, struct net_device *in, struct net_device *out,
|
|
const struct nf_osf_info *info, struct net *net,
|
|
const struct list_head *nf_osf_fingers)
|
|
{
|
|
const struct iphdr *ip = ip_hdr(skb);
|
|
const struct nf_osf_user_finger *f;
|
|
unsigned char opts[MAX_IPOPTLEN];
|
|
const struct nf_osf_finger *kf;
|
|
int fcount = 0, ttl_check;
|
|
int fmatch = FMATCH_WRONG;
|
|
struct nf_osf_hdr_ctx ctx;
|
|
const struct tcphdr *tcp;
|
|
struct tcphdr _tcph;
|
|
|
|
memset(&ctx, 0, sizeof(ctx));
|
|
|
|
tcp = nf_osf_hdr_ctx_init(&ctx, skb, ip, opts, &_tcph);
|
|
if (!tcp)
|
|
return false;
|
|
|
|
ttl_check = (info->flags & NF_OSF_TTL) ? info->ttl : 0;
|
|
|
|
list_for_each_entry_rcu(kf, &nf_osf_fingers[ctx.df], finger_entry) {
|
|
|
|
f = &kf->finger;
|
|
|
|
if (!(info->flags & NF_OSF_LOG) && strcmp(info->genre, f->genre))
|
|
continue;
|
|
|
|
if (!nf_osf_match_one(skb, f, ttl_check, &ctx))
|
|
continue;
|
|
|
|
fmatch = FMATCH_OK;
|
|
|
|
fcount++;
|
|
|
|
if (info->flags & NF_OSF_LOG)
|
|
nf_log_packet(net, family, hooknum, skb,
|
|
in, out, NULL,
|
|
"%s [%s:%s] : %pI4:%d -> %pI4:%d hops=%d\n",
|
|
f->genre, f->version, f->subtype,
|
|
&ip->saddr, ntohs(tcp->source),
|
|
&ip->daddr, ntohs(tcp->dest),
|
|
f->ttl - ip->ttl);
|
|
|
|
if ((info->flags & NF_OSF_LOG) &&
|
|
info->loglevel == NF_OSF_LOGLEVEL_FIRST)
|
|
break;
|
|
}
|
|
|
|
if (!fcount && (info->flags & NF_OSF_LOG))
|
|
nf_log_packet(net, family, hooknum, skb, in, out, NULL,
|
|
"Remote OS is not known: %pI4:%u -> %pI4:%u\n",
|
|
&ip->saddr, ntohs(tcp->source),
|
|
&ip->daddr, ntohs(tcp->dest));
|
|
|
|
if (fcount)
|
|
fmatch = FMATCH_OK;
|
|
|
|
return fmatch == FMATCH_OK;
|
|
}
|
|
EXPORT_SYMBOL_GPL(nf_osf_match);
|
|
|
|
bool nf_osf_find(const struct sk_buff *skb,
|
|
const struct list_head *nf_osf_fingers,
|
|
const int ttl_check, struct nf_osf_data *data)
|
|
{
|
|
const struct iphdr *ip = ip_hdr(skb);
|
|
const struct nf_osf_user_finger *f;
|
|
unsigned char opts[MAX_IPOPTLEN];
|
|
const struct nf_osf_finger *kf;
|
|
struct nf_osf_hdr_ctx ctx;
|
|
const struct tcphdr *tcp;
|
|
struct tcphdr _tcph;
|
|
bool found = false;
|
|
|
|
memset(&ctx, 0, sizeof(ctx));
|
|
|
|
tcp = nf_osf_hdr_ctx_init(&ctx, skb, ip, opts, &_tcph);
|
|
if (!tcp)
|
|
return false;
|
|
|
|
list_for_each_entry_rcu(kf, &nf_osf_fingers[ctx.df], finger_entry) {
|
|
f = &kf->finger;
|
|
if (!nf_osf_match_one(skb, f, ttl_check, &ctx))
|
|
continue;
|
|
|
|
data->genre = f->genre;
|
|
data->version = f->version;
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
return found;
|
|
}
|
|
EXPORT_SYMBOL_GPL(nf_osf_find);
|
|
|
|
static const struct nla_policy nfnl_osf_policy[OSF_ATTR_MAX + 1] = {
|
|
[OSF_ATTR_FINGER] = { .len = sizeof(struct nf_osf_user_finger) },
|
|
};
|
|
|
|
static int nfnl_osf_add_callback(struct sk_buff *skb,
|
|
const struct nfnl_info *info,
|
|
const struct nlattr * const osf_attrs[])
|
|
{
|
|
struct nf_osf_user_finger *f;
|
|
struct nf_osf_finger *kf = NULL, *sf;
|
|
int err = 0;
|
|
|
|
if (!capable(CAP_NET_ADMIN))
|
|
return -EPERM;
|
|
|
|
if (!osf_attrs[OSF_ATTR_FINGER])
|
|
return -EINVAL;
|
|
|
|
if (!(info->nlh->nlmsg_flags & NLM_F_CREATE))
|
|
return -EINVAL;
|
|
|
|
f = nla_data(osf_attrs[OSF_ATTR_FINGER]);
|
|
|
|
if (f->opt_num > ARRAY_SIZE(f->opt))
|
|
return -EINVAL;
|
|
|
|
if (!memchr(f->genre, 0, MAXGENRELEN) ||
|
|
!memchr(f->subtype, 0, MAXGENRELEN) ||
|
|
!memchr(f->version, 0, MAXGENRELEN))
|
|
return -EINVAL;
|
|
|
|
kf = kmalloc(sizeof(struct nf_osf_finger), GFP_KERNEL);
|
|
if (!kf)
|
|
return -ENOMEM;
|
|
|
|
memcpy(&kf->finger, f, sizeof(struct nf_osf_user_finger));
|
|
|
|
list_for_each_entry(sf, &nf_osf_fingers[!!f->df], finger_entry) {
|
|
if (memcmp(&sf->finger, f, sizeof(struct nf_osf_user_finger)))
|
|
continue;
|
|
|
|
kfree(kf);
|
|
kf = NULL;
|
|
|
|
if (info->nlh->nlmsg_flags & NLM_F_EXCL)
|
|
err = -EEXIST;
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* We are protected by nfnl mutex.
|
|
*/
|
|
if (kf)
|
|
list_add_tail_rcu(&kf->finger_entry, &nf_osf_fingers[!!f->df]);
|
|
|
|
return err;
|
|
}
|
|
|
|
static int nfnl_osf_remove_callback(struct sk_buff *skb,
|
|
const struct nfnl_info *info,
|
|
const struct nlattr * const osf_attrs[])
|
|
{
|
|
struct nf_osf_user_finger *f;
|
|
struct nf_osf_finger *sf;
|
|
int err = -ENOENT;
|
|
|
|
if (!capable(CAP_NET_ADMIN))
|
|
return -EPERM;
|
|
|
|
if (!osf_attrs[OSF_ATTR_FINGER])
|
|
return -EINVAL;
|
|
|
|
f = nla_data(osf_attrs[OSF_ATTR_FINGER]);
|
|
|
|
list_for_each_entry(sf, &nf_osf_fingers[!!f->df], finger_entry) {
|
|
if (memcmp(&sf->finger, f, sizeof(struct nf_osf_user_finger)))
|
|
continue;
|
|
|
|
/*
|
|
* We are protected by nfnl mutex.
|
|
*/
|
|
list_del_rcu(&sf->finger_entry);
|
|
kfree_rcu(sf, rcu_head);
|
|
|
|
err = 0;
|
|
break;
|
|
}
|
|
|
|
return err;
|
|
}
|
|
|
|
static const struct nfnl_callback nfnl_osf_callbacks[OSF_MSG_MAX] = {
|
|
[OSF_MSG_ADD] = {
|
|
.call = nfnl_osf_add_callback,
|
|
.type = NFNL_CB_MUTEX,
|
|
.attr_count = OSF_ATTR_MAX,
|
|
.policy = nfnl_osf_policy,
|
|
},
|
|
[OSF_MSG_REMOVE] = {
|
|
.call = nfnl_osf_remove_callback,
|
|
.type = NFNL_CB_MUTEX,
|
|
.attr_count = OSF_ATTR_MAX,
|
|
.policy = nfnl_osf_policy,
|
|
},
|
|
};
|
|
|
|
static const struct nfnetlink_subsystem nfnl_osf_subsys = {
|
|
.name = "osf",
|
|
.subsys_id = NFNL_SUBSYS_OSF,
|
|
.cb_count = OSF_MSG_MAX,
|
|
.cb = nfnl_osf_callbacks,
|
|
};
|
|
|
|
static int __init nfnl_osf_init(void)
|
|
{
|
|
int err = -EINVAL;
|
|
int i;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(nf_osf_fingers); ++i)
|
|
INIT_LIST_HEAD(&nf_osf_fingers[i]);
|
|
|
|
err = nfnetlink_subsys_register(&nfnl_osf_subsys);
|
|
if (err < 0) {
|
|
pr_err("Failed to register OSF nsfnetlink helper (%d)\n", err);
|
|
goto err_out_exit;
|
|
}
|
|
return 0;
|
|
|
|
err_out_exit:
|
|
return err;
|
|
}
|
|
|
|
static void __exit nfnl_osf_fini(void)
|
|
{
|
|
struct nf_osf_finger *f;
|
|
int i;
|
|
|
|
nfnetlink_subsys_unregister(&nfnl_osf_subsys);
|
|
|
|
rcu_read_lock();
|
|
for (i = 0; i < ARRAY_SIZE(nf_osf_fingers); ++i) {
|
|
list_for_each_entry_rcu(f, &nf_osf_fingers[i], finger_entry) {
|
|
list_del_rcu(&f->finger_entry);
|
|
kfree_rcu(f, rcu_head);
|
|
}
|
|
}
|
|
rcu_read_unlock();
|
|
|
|
rcu_barrier();
|
|
}
|
|
|
|
module_init(nfnl_osf_init);
|
|
module_exit(nfnl_osf_fini);
|
|
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_ALIAS_NFNL_SUBSYS(NFNL_SUBSYS_OSF);
|