
Traducción al español de los CVE-2022-1015 y 1016 descubiertos y documentados por David.
This README.md is a translation of David's blog. David found CVEs 1015 and 1016 in the Linux kernel. You can visit his website to read the original document.
Here are his social media links:
Published on April 2, 2022.
These issues should be exploitable in the default configurations of the newest versions of Ubuntu and RHEL. I wrote my proof of concept (PoC) for CVE-2022-1015 targeting kernel version 5.16-rc3 on Arch Linux.
This document is aimed at people who have a basic understanding of the Linux kernel in terms of functionality and security. I tried to make this document friendly to those lacking knowledge of the networking stack to make it accessible to everyone.
Here is a reading guide:
In mid-February, Google's security program announced that they would continue their kCTF bounty program, offering bounties ranging from $31,337 to $91,337 for a Linux kernel exploit that can escalate privileges to root from unprivileged processes in an nsjail sandbox.
Being a poor student, this obviously caught my attention. This was my first time looking for a "real world" vulnerability, but in my adventures playing CTF with my team, I have become familiar with the Linux kernel in terms of security. After hours and hours with very little close to nothing progress (but with greater knowledge about Linux) I managed to find some vulnerabilities in the nf_tables module.
Sadly, at the end of the day, I realized that this module was not included in Google's kCTF rules (so I did not get any bounty for these two vulnerabilities). But obviously, I still reported them and wrote an LPE (Local Privilege Escalation) exploit for CVE-2022-1015.
Alright, so you have decided that you are going to find some vulnerabilities in Linux. Now what? Linux is a gigantic project, and it is quite easy to not see the forest for the trees (you focus so much on details that you lose sight of what is really important, you don't have an overview of the situation). To make matters worse, many parts are undocumented and you need to read a lot of code to understand what is going on.
I started by trying to get a detailed perspective of the Linux security model. Finding a bug is one thing; but finding a good bug is another. After all, not all bugs are created equal:
FS_USERNS_MOUNT, in which case you can mount them in a user namespace.CAP_SYS_ADMIN or CAP_NET_ADMIN.
/proc/config.gz. Modules can be built-in (=y) or compiled separately and loaded at runtime (=m)./proc/modules and , but they are not always reliable, as modules can be dynamically loaded into the kernel ( ).These restrictions help us to know the limits of the filesystems in which we can look for vulnerabilities. I think it is a good idea to take your time trying to plan your attack on your desired target.
I have already learned my lesson about the previous point. As I mentioned, the nf_tables module was not loaded on the instance presented by kCTF. I could have realized this from the start and saved myself the disappointment :p. On the other hand, you probably wouldn't be reading this blog right now if I had realized earlier, I guess things turned out okay after all.
An explanation why COS, Google's container-optimized Linux fork, did not have nf_tables can be found here and here.
After evaluating the above points, I decided that my best path to start was probably to look at the networking source code. Many of the interesting features there require CAP_NET_ADMIN, but as I mentioned, this is not really a problem. On the contrary, I suspect that components requiring special capabilities are generally less secure, as kernel developers may have a false sense of security.
I also made an effort to choose the filesystem I wanted to learn more about; this way, even if you don't find any bugs you will still learn a lot of interesting things.
I investigated many networking filesystems, but didn't find anything significant. After navigating the net/ subdirectory, I came across the nf_tables module. It seemed a bit complex, so I decided to take some time to learn about it.
Netfilter (net/netfilter) is a fairly large networking subsystem in the kernel. In summary, netfilter places hooks across the networking modules that other modules can register handlers with. When a hook is reached, control is delegated to those handlers, and they can operate on their respective network packet structure. Handlers can accept, drop, and modify packets.
After a few hours of browsing the nf_tables API (net/netfilter/nf_tables_api.c) to start understanding exactly how it works, I decided to take a look at the logical validation of the registers that the user sends, and I found some suspicious behavior. After thinking about whether I was going crazy or not, I wrote a small PoC (proof of concept) to try to trigger the vulnerability I found: a vulnerability known as OOB or out-of-bounds, which allows reading and writing stack memory.
After finding a way to leak kernel addresses, taking control of the instruction pointer was quite easy. After a bit of ROP (Return-Oriented Programming), the root shell became a reality.
Whenever an expression's init routine needs to parse a register from a netlink user message, the nft_parse_register_load or nft_parse_register_store routine is called depending on whether it is a source register or a destination register. I added some comments:```c
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
/* Given a netlink attribute and the length
* that is required to read the requested data,
* write a register index to `sreg` or return
* an error on failure. */
u32 reg;
int err;
reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
return err;
/* Write resulting index to the nft_expr.data structure. */
*sreg = reg;
return 0;
}
static unsigned int nft_parse_register(const struct nlattr attr) { / Convert a register to an index in nft_regs */
unsigned int reg;
/* Get specified register from netlink attribute */
reg = ntohl(nla_get_be32(attr));
switch (reg) {
/* If it's 0 to 4 inclusive,
* it's an OG 16-byte register and we need to
* multiply the index by 4 (4*4=16) */
case NFT_REG_VERDICT...NFT_REG_4:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
/* Else we subtract 4, since we need to account
* for the OG registers above. */
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
/* So supplied values of 1, 2, 3, 4 map to
* OG 16-byte registers, with indices 4, 8,
* 12, 16
* Supplied values of 5, 6, 7 overlap the verdict,
* 8,9,10,11 overlap with OG register 1
* 12,13,14,15 overlap with OG register 2
* etc. */
}
static int nft_validate_register_load(enum nft_registers reg, unsigned int len) { /* We can never read from the verdict register, * so bail out if the index is 0,1,2,3 */ if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) return -EINVAL;
/* Invalid operation, bail out */
if (len == 0)
return -EINVAL;
/* If there would be an OOB access whenever
* `reg` is taken as index and `len` bytes are read,
* bail out.
* sizeof_field(struct nft_regs, data) == 0x50 */
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
return -ERANGE;
return 0;
}
The `*_store` variants are virtually identical, except that they allow writing to *verbdict* under some conditions.
After reviewing the last validation, something is really out of place here:```c
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
This seems to be an integer overflow, don't you think? If we can make reg contain some value multiplied by 4 that generates an overflow when added to len, we can satisfy the conditions. In nft_parse_register_load, the last valuable byte of reg is still written to the pointer u8 *sreg, falling into our nft_expr which is later used as an index.```c
*sreg = reg;
¿De verdad podemos? `reg` es un `enum nft_registers` en la validación de la rutina, de todas formas. Podemos pasar valores que tengan un rango entre `0x00000001` hasta `0xfffffffb` inclusive, el rango de `nft_parse_register`; pero ¿será `reg` un valor de 32 bits en `nft_validate_register_load`? Se sabe que los compiladores podrían encoger los *enum types* si un tipo más pequeño puede representar todos los valores. Vamos a obtener una segunda opinión.
Obtenido del manual de GCC:```
The integer type compatible with each enumerated type (C90 6.5.2.2, C99 and C11 6.7.2.2).
Normally, the type is unsigned int if there are no negative values
in the enumeration, otherwise int. If -fshort-enums is specified,
then if there are negative values it is the first
of signed char, short and int that can represent all the values,
otherwise it is the first of unsigned char, unsigned short and unsigned int
that can represent all the values.
On some targets, -fshort-enums is the default; this is determined by the ABI.
TL;DR? It depends on the ABI and the possible optimization level. I could not find any concrete evidence of whether this option is enabled by default in Linux builds.
But the assembler never lies. Let's take a look:```objdump.x86asm
0000000000001b60 <nft_parse_register_load>:
1b60: e8 00 00 00 00 call 1b65 <nft_parse_register_load+0x5>
1b65: 55 push rbp
1b66: 8b 47 04 mov eax,DWORD PTR [rdi+0x4]
1b69: 0f c8 bswap eax
1b6b: 89 c7 mov edi,eax
1b6d: 8d 48 fc lea ecx,[rax-0x4]
1b70: c1 e7 04 shl edi,0x4
1b73: 48 89 e5 mov rbp,rsp
1b76: c1 ef 02 shr edi,0x2
1b79: 83 f8 04 cmp eax,0x4
1b7c: 89 f8 mov eax,edi
1b7e: 0f 47 c1 cmova eax,ecx
1b81: 85 d2 test edx,edx
1b83: 74 13 je 1b98 <nft_parse_register_load+0x38>
1b85: 83 f8 03 cmp eax,0x3
1b88: 76 0e jbe 1b98 <nft_parse_register_load+0x38>
1b8a: 8d 14 82 lea edx,[rdx+rax*4]
1b8d: 83 fa 50 cmp edx,0x50
1b90: 77 0d ja 1b9f <nft_parse_register_load+0x3f>
1b92: 88 06 mov BYTE PTR [rsi],al
1b94: 5d pop rbp
1b95: 31 c0 xor eax,eax
1b97: c3 ret
1b98: b8 ea ff ff ff mov eax,0xffffffea
1b9d: 5d pop rbp
1b9e: c3 ret
1b9f: b8 de ff ff ff mov eax,0xffffffde
1ba4: 5d pop rbp
1ba5: c3 ret
The function calls are aligned quite well. The important operations are at `1b8a`:```objdump.x86asm
lea edx, [rdx+rax*4]
cmp edx, 0x50
ja 1b9f <nft_parse_register_load+0x3f>
mov BYTE PTR [rsi], al
rax is the result of ntf_parse_register, rdx is the provided len, and rsi is the pointer sreg. We've already cleared our doubts.
nft_parse_register_store exhibits the same behavior. As long as the registers live on the stack, our OOB vulnerability will obviously be relative to the stack. This is good, because with a bit of luck, we will be able to overwrite and return memory directly.
To give an example of a vulnerable entry, a register of 0xfffffffb and a length of 0x20 will evaluate 0xfffffffb * 4 + 0x20 = 0x0c < 0x50. After validation, (u8)0xfffffffb = 0xfb will be written to *sreg.
Although there is a problem: are there expressions that allow us to use a length that can cause an overflow when the addition is performed? After a bit of research, I found that nft_bitwise and nft_payload allow you to input your own length, from 0x00 to 0xff. Many other expressions seem to have static lengths that are very small.
So far this looks promising. The next step is to take these exploit primitives and use them.
If we can define the type of power our exploit can give us, exploiting this vulnerability should be easier. So, bear with me while we look at a bit of arithmetic.
There are three points we can use for our overflow for the register multiplication, since it is multiplied by 4 = 2^2: 2^32 - 1, 2^31 - 1 and 2^30 - 1 (respectively 0xffffffff, 0x7fffffff, and 0x3fffffff). These values can decrease until we add our maximum allowed length, after being multiplied by four this will not result in an overflow. Another point to note is that we cannot use values greater than 0xfffffffb, as mentioned earlier.
Given a specific length, the least significant byte values that can allow an overflow using this length will form our range of OOB indices that we can use.
After all, it does not matter which overflow points are used. Take for example the following values with an LSB (least significant bit) of 0xf0:```
0xfffffff0 * 4 = 0xffffffc0
0x7ffffff0 * 4 = 0xffffffc0
0x3ffffff0 * 4 = 0xffffffc0
From now on, we will use register values close to `0x7fffffff`.
Previously we have discussed `nft_payload` and `nft_bitwise`. Some properties of these expressions are:
* `nft_payload` can only perform *OOB* writes, while `nft_bitwise` can perform *OOB* writes and reads.
* `nft_payload` can perform *OOB* writes of up to `0xff` bytes of arbitrary data.
* `nft_bitwise` can actually only write up to `0x40` bytes of arbitrary data and can only read `0x40` bytes of data located in the register space *stack*.
* `nft_bitwise` requires an `sreg` and a `dreg`, which need to pass validation with the same length value.
* We only have `0x40` bytes of register space, so we want to either read or write from the register space, but we cannot pass the validation with a length greater than `0x40`.
We can use a larger length value for `nft_bitwise`, but that means `sreg` and `dreg` need to be out of bounds, which would not be very useful for our purposes. So, for now we will work with a length of `0x40`.
With all this in mind, what types of *exploits* can we use?
`nft_bitwise` has a maximum length of `0x40`. This means that the register value multiplied by four should be at least `0xffffffc0`. The largest value we can get by multiplying by four is `0xfffffffb`, and since `0xfffffffb + 0x40 = 0x3b <= 0x50` this will pass validation.
`0x7ffffff0 * 4 = 0xffffffc0`: the lower limit is `0xf0`.
`0x7fffffff * 4 = 0xfffffffb`: the upper limit is `0xff`.
Translating to [*byte offsets*](https://en.wikipedia.org/wiki/Offset_(computer_science)):```
0xc1 * 4 = 0x304
0xeb * 4 + 0xff = 0x4ab
nft_payload can write out-of-bounds through the offsets [0x304, 0x4ab] from struct nft_regs.
Now that all of this is clarified, what actually is on the stack at these offsets?
The nft_do_chain routine can be called through many code paths. There are many factors that will change the shape of the stack before the stack frame of nft_do_chain:
Whether the chain hook is an input or output.
The protocol we are using.
I think you can get many variations of call stacks using different combinations of protocols, interfaces, and hook locations. For now we will use a chain hook configured as an output with a UDP packet.

Stack layout and out-of-bounds reach in nft_do_chain when a sent UDP packet hits a hook configured as output
To be able to create a stable exploit we will first have to leak the kernel image base address.
The kernel image base address has 9 bits of entropy, meaning there are 512 different positions where the kernel can be loaded. Depending on your attack scenario, there is a 1 in 512 chance that the attack will work correctly; but it would be better if we could achieve a more stable exploit.
The simplest step is to try to use our out-of-bounds read capability obtained via nft_bitwise to copy some of the stack data to our registers. Since the total interval we can read has a length of 0x7c bytes, there is a fairly good chance that the kernel address is there.

nft_bitwise out-of-bounds reach
Today is our day! There are two:``` gef➤ x/bx 0xffffffff815b49c1 0xffffffff815b49c1 <import_iovec+49>: 0xc9 gef➤ x/bx 0xffffffff819ac3ec 0xffffffff819ac3ec <copy_msghdr_from_user+92>: 0xba
Writing this to the registers is one thing, but extracting them is another. After investigating, it seems there is no easy way to directly read the registers while `nft_do_chain` is executing.
In my original report to [email protected], I was informed about the `nft_dynset` expression by a netfilter maintainer, which supports [*dynamic sets*](https://en.wikipedia.org/wiki/Dynamic_set) that can act as a kind of database that can write and read across different `nft_do_chain` executions. Apparently, `nft_payload` also has the ability to write to the packet itself, I did not realize this.
Instead, I decided to continue with my [*side-channel attack*](https://en.wikipedia.org/wiki/Side-channel_attack). Due to the nature of `nf_tables`, you can cause side effects. In fact, you could say they are not even side effects, but primary effects.
Creating rules that drop or accept the packet based on the value of the kernel memory address we are copying, we can gradually deduce what the value is by examining whether the packets we sent were also received.
1. Create a UDP socket that receives packets on `127.0.0.1:9999`:
* It should receive packets on a different thread.
* A message should be sent back for each packet it receives.
2. Add a rule that:
1. Copies the kernel address to the registers with `nft_bitwise`.
2. Uses `nft_cmp_expr` to compare the address to a constant.
3. Drop a packet if the evaluated comparison is true.
3. Send a UDP packet to `127.0.0.1:9999`
1. We can determine a bit of information about the kernel address based on whether we receive a message back.
4. Repeat 2 and 3 with the appropriate values until you have enough information to determine the information by itself.

There are still some caveats. For example, the packet we receive could also be dropped without any warning. To mitigate this, we can add noise reduction, for which we will need a *base chain* and an *auxiliary regular chain*.
*Rule in base chain:*
| # | Expression | Arguments | Comment |
| --- | --------------------- | ------------------------------------------------------------------------------------------------------- |:---------------------------------------------------------------------------------------------------- |
| 0 | `nft_payload` | base=NFT_PAYLOAD_TRANSPORT_HEADER<br/>offset=offsetof(udphdr, dport)<br/>len=sizeof_field(udphdr, dport) | Write the packet's destination port to register 8. |
| 1 | `nft_cmp_expr` | op=NFT_CMP_EQ<br/>sreg=8<br/>data=9999 | Compare the destination port to `9999`, and return `NFT_BREAK` if the result is not equal. |
| 2 | `nft_payload` | base=NFT_PAYLOAD_INNER_HEADER<br/>offset=0<br/>len=8 | Write the first eight bytes of the packet to register 8. |
| 3 | `nft_cmp_expr` | op=NFT_CMP_EQ<br/>sreg=8<br/>data=0xdeadbeef0badc0de | Compare the first eight bytes to the magic value, and return `NFT_BREAK` if not equal. |
| 4 | `nft_immediate_expr` | verdict=NFT_JUMP<br/>chain=aux_chain | Since the rule is still evaluating, the conditions must match, and call our *auxiliary chain*. |
*Rule in auxiliary chain:*
| # | Expression | Arguments | Comment |
| --- | ---------------- | ---------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0 | `nft_bitwise` | op=NFT_BITWISE_RSHIFT<br/>data=SHIFT_AMT<br/>dreg=OOB_OFFSET<br/>sreg=8 | Write the kernel address to the registers using the out-of-bounds read, shifted by `SHIFT_AMT` bits to get the desired address byte into the correct register. |
| 1 | `nft_cmp` | op=NFT_CMP_GT<br/>sreg=ADDRESS_OFFSET<br/>data=COMPARAND | Compare the kernel address byte with `COMPARAND`, return `NFT_BREAK` if this result is not equal. |
| 2 | `nft_immediate` | verdict=NFT_DROP | Drop the packet if the address byte is greater than `COMPARAND`. |
By checking the destination port and comparing the first eight inner header bytes to a magic value, we can trigger the side effects for the packets we want.
Dynamically changing `COMPARAND` we can do a binary search to find the kernel address byte in `0(log(n))` time. Dynamically changing `SHIFT_AMT` to the next multiples of eight we can move to the next memory byte and start again.
#### 4.3.1 Filter pseudo-code
A bit of python code to filter the memory address. The funny thing is that I could have easily implemented this in python. Remember that you don't always have to write your exploits for a kernel in C :p```python
'''
Asumimos que un hilo secundario está recibiendo
paquetes UDP en 127.0.0.1:9999 y todo lo relacionado
con nf_tables ya está configurado
p. ej. table, base y auxiliary chain
'''
def leak_byte(pos):
s = socket.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
s.settimeout(200) # 200ms debería ser más que suficiente
s.bind(("127.0.0.1", 1234))
# buscar los límites
low = 0, high = 255
while True:
mid = (low + high) // 2
# si encontramos el valor, lo regresamos
if low == high:
s.close()
return mid
set_leak_rule(SHIFT_AMT=pos*8, COMPARAND=mid)
# Enviar el paquete y activar la auxiliary chain
s.sendto(pack(0xdeadbeef0badc0de), ("127.0.0.1", 9999))
# El hilo secundario regresa a 127.0.0.1:1234
res = s.recvfrom(0x2000)
if not res:
'''
nuestro paquete fue soltado
ya que no se regresó nada en los 200ms
lo que significa que
byte to leak >= mid
el byte a filtrar es mayor o igual a mid (127)
'''
low = mid
else:
'''
[sanity check o prueba de cordura]
se usa para evaluar rápidamente si
el valor a calcular es siquiera posible
https://es.wikipedia.org/wiki/Prueba_de_cordura
'''
if res != b"MSG_OK":
print("Something went wrong")
return None
'''
Nuestro paquete fue aceptado, lo que
significa que
byte to leak < mid
byte a filtrar es menor a mid (127)
'''
high = mid - 1
leak_bytes = lambda: [leak_byte(i*8) for i in range(4)]
Now that we have the leak, arbitrary code execution should be very easy. The out-of-bounds write of nft_payload should be able to write a RoP chain attack for the stack, right?
Nope. We weren't very lucky, at least on this particular kernel. The out-of-bounds write of nft_payload almost entirely aligns with the stack frame of the udp_sendmsg routine. The address of udp_sendmsg is at offset +0x2f8 relative to the registers; this location is too low to be reached with nft_payload or nft_bitwise (we can start writing at offset +0x304, so close...). The inet_sendmsg address is located at offset +0x4a8. Technically we can reach it (and overwrite the lower three bytes), but there is a stack canary (a technique used to detect a stack buffer overflow before malicious code execution can occur) at address +0x0458 that we also need to overwrite to achieve this. This would obviously crash the kernel, so doing this is not an option.
I managed to use this method on another kernel build, but it seems that trying to do the same for the kernel I am using for this blog will be a bit more difficult.
Now, perhaps we can do some contrived stack frame hacking to overwrite the local variables in udp_sendmsg. We could also try to overwrite the verdict chain pointer, using a register value e.g. 0x7fffff00 (I think this could be a cool technique; considering the challenge).
Let's try changing the base chain hook we used. We were using an output chain, what if we change it to an input one

Diagram of the out-of-bounds scope in nft_do_chain if a sent UDP packet reaches the input hook
This looks a bit better! We can overwrite the return address of the __netif_receive_skb_one_core frame (offset +0x328), which returns to __netif_receive_skb. Since it is relatively close to the height of our nft_payload out-of-bounds scope, we can have our OOB (out-of-bounds) index point directly to this return address, bypassing the stack canary at offset +0x310. The offset +0x328 translates to index 0xca.
To trigger the overwrite of the return address, we create a new input chain in the table, and add a rule with an nft_payload that writes 0xff bytes from the inner header of the packet to index 0xca. Then we send a packet with the payload, and boom.

🥳 🥳 🥳 🥳 🥳
/proc/kallsymsrequest_module