As a researcher, it’s important to add new techniques and software to your bug hunting methodology. A year ago, I started using CodeQL for my own research on open source projects and decided to compile the Linux kernel with it and try my luck.
For those who haven’t come across it before, CodeQL is an analysis engine that allows you to run queries on code. From a security perspective, this can allow you to find vulnerabilities purely by describing what they look like. CodeQL will then go off and find all instances of that vulnerability.
I’d had a passing thought about overflows that I wanted to take a quick look at between research projects, namely, looking at locations in which a 16-bit variable was passed to kmalloc
. My thinking was that 16-bits would be easier to realistically overflow than a 32-bit or 64-bit number.
The query itself is basic and isn’t aimed at finding actual overflows, just looking for interesting kmalloc
calls as a starting point for a larger query:
import cpp from FunctionCall fc // Select all Function Calls where fc.getTarget().getName() = "kmalloc" // Where the target function is called kmalloc and fc.getArgument(0).getType().getSize() = 2 // and the supplied size argument is a 16-bit int select fc, fc.getLocation() // Select the call location and the string of the location to know what file it’s in
This returned 60 results. After briefly looking over a few, one result stood out above the rest:
static bool tipc_crypto_key_rcv(struct tipc_crypto *rx, struct tipc_msg *hdr) // (1) { struct tipc_crypto *tx = tipc_net(rx->net)->crypto_tx; struct tipc_aead_key *skey = NULL; u16 key_gen = msg_key_gen(hdr); u16 size = msg_data_sz(hdr); // (2) u8 *data = msg_data(hdr); /* ... */ /* Allocate memory for the key */ skey = kmalloc(size, GFP_ATOMIC); // (3) if (unlikely(!skey)) { pr_err("%s: unable to allocate memory for skey\n", rx->name); goto exit; } /* Copy key from msg data */ skey->keylen = ntohl(*((__be32 *)(data + TIPC_AEAD_ALG_NAME))); // (4) memcpy(skey->alg_name, data, TIPC_AEAD_ALG_NAME); memcpy(skey->key, data + TIPC_AEAD_ALG_NAME + sizeof(__be32), skey->keylen); // (5) /* Sanity check */ if (unlikely(size != tipc_aead_key_size(skey))) { // (6) kfree(skey); skey = NULL; goto exit; } /* ... */ }
What struck me as interesting is that this seems to be a function for parsing received data (1) and doesn’t appear to have any validation on the size (4) (5) obtained from the body of the message (2) until after it’s already copied (6). It also appears that the copied size could be different to the allocated size (3). This looked like a clear-cut kernel heap buffer overflow.
Transparent Inter-Process Communication (TIPC) is a protocol that allows nodes in a cluster to communicate with each other in a way that can optimally handle a large number of nodes remaining fault tolerant.
In order to keep this section brief, this post will focus on the key components. For a more detailed and high-level description of the actual TIPC protocol, including the various ways messaging is performed and how Service Tracking works, it’s best to refer to the official sourceforge page.
The protocol is implemented in a kernel module packaged with all major Linux distributions. When loaded by a user, it can be used as a socket and can be configured on an interface using netlink (or using the userspace tool tipc
, which will perform these netlink calls) as an unprivileged user.
TIPC can be configured to operate on top of a bearer protocol such as Ethernet or UDP (in the latter case, the kernel listens on port 6118 for incoming messages from any machine). Since a low privileged user is unable to create raw ethernet frames, setting the bearer to UDP makes it easier to write a local exploit for.
Although TIPC is used on top of these protocols, it has a separate addressing scheme whereby nodes can choose their own addresses.
The TIPC protocol works in a way transparent to the user. All message construction and parsing is performed in the kernel. Each TIPC message has a common header format and some message-specific headers (hence the variable total size of the header).
The most important parts of the common header for this vulnerability are the ‘Header Size’ –the actual header size shifted to the right by two bits– and the ‘Message Size’ –the entire TIPC message taking into account the header size:
These two sizes are validated by the tipc_msg_validate
function.
bool tipc_msg_validate(struct sk_buff **_skb) { struct sk_buff *skb = *_skb; struct tipc_msg *hdr; int msz, hsz; /* ... */ hsz = msg_hdr_sz(buf_msg(skb)); if (unlikely(hsz < MIN_H_SIZE) || (hsz > MAX_H_SIZE)) return false; /* ... */ hdr = buf_msg(skb); /* ... */ msz = msg_size(hdr); if (unlikely(msz < hsz)) return false; if (unlikely((msz - hsz) > TIPC_MAX_USER_MSG_SIZE)) return false; if (unlikely(skb->len < msz)) return false; TIPC_SKB_CB(skb)->validated = 1; return true; }
The Message Size is correctly validated as greater than the Header Size, the payload size is validated against the maximum user message size, and the message size is validated against the actual received packet length.
In September 2020, a new user message type was introduced called MSG_CRYPTO
, which allows peers to send cryptographic keys (at the moment, only AES GCM appears to be supported). This is part of the 2021 TIPC roadmap.
The body of the message has the following structure:
struct tipc_aead_key { char alg_name[TIPC_AEAD_ALG_NAME]; unsigned int keylen; /* in bytes */ char key[]; };
Where TIPC_AEAD_ALG_NAME
is a macro for 32. When this message is received, the TIPC kernel module needs to copy this information into storage for that node:
/* Allocate memory for the key */ skey = kmalloc(size, GFP_ATOMIC); /* ... */ /* Copy key from msg data */ skey->keylen = ntohl(*((__be32 *)(data + TIPC_AEAD_ALG_NAME))); memcpy(skey->alg_name, data, TIPC_AEAD_ALG_NAME); memcpy(skey->key, data + TIPC_AEAD_ALG_NAME + sizeof(__be32), skey->keylen);
The size used to allocate is the same as the size of the message payload (calculated from the Header Size being subtracted from the Message Size). The name of the key algorithm is copied and the key itself is then copied as well.
As mentioned above, the Header Size and the Message Size are both validated against the actual packet size. So while these values are guaranteed to be within the range of the actual packet, there are no similar checks for either the keylen
member of the MSG_CRYPTO
message or the size of the key algorithm name itself (TIPC_AEAD_ALG_NAME
) against the message size. This means that an attacker can create a packet with a small body size to allocate heap memory, and then use an arbitrary size in the keylen
attribute to write outside the bounds of this location:
This vulnerability can be exploited both locally and remotely. While local exploitation is easier due to greater control over the objects allocated in the kernel heap, remote exploitation can be achieved thanks to the structures that TIPC supports.
As for the data being overwritten, at first glance it may look like the overflow will have uncontrolled data, since the actual message size used to allocate the heap location is verified. However, a second look at the message validation function shows that it only checks that the message size in the header is within the bounds of the actual packet. That means that an attacker could create a 20 byte packet and set the message size to 10 bytes without failing the check:
if (unlikely(skb->len < msz)) return false;
In order to aid in fixing the issue quickly, I drafted a patch idea along with the report. After some very helpful discussion with one person from the Linux Foundation and one of the TIPC maintainers, the following patch was decided on:
net/tipc/crypto.c | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/net/tipc/crypto.c b/net/tipc/crypto.c index c9391d38d..dc60c32bb 100644 --- a/net/tipc/crypto.c +++ b/net/tipc/crypto.c @@ -2285,43 +2285,53 @@ static bool tipc_crypto_key_rcv(struct tipc_crypto *rx, struct tipc_msg *hdr) u16 key_gen = msg_key_gen(hdr); u16 size = msg_data_sz(hdr); u8 *data = msg_data(hdr); + unsigned int keylen; + + /* Verify whether the size can exist in the packet */ + if (unlikely(size < sizeof(struct tipc_aead_key) + TIPC_AEAD_KEYLEN_MIN)) { + pr_debug("%s: message data size is too small\n", rx->name); + goto exit; + } + + keylen = ntohl(*((__be32 *)(data + TIPC_AEAD_ALG_NAME))); + + /* Verify the supplied size values */ + if (unlikely(size != keylen + sizeof(struct tipc_aead_key) || + keylen > TIPC_AEAD_KEY_SIZE_MAX)) { + pr_debug("%s: invalid MSG_CRYPTO key size\n", rx->name); + goto exit; + } spin_lock(&rx->lock); if (unlikely(rx->skey || (key_gen == rx->key_gen && rx->key.keys))) { pr_err("%s: key existed <%p>, gen %d vs %d\n", rx->name, rx->skey, key_gen, rx->key_gen); - goto exit; + goto exit_unlock; } /* Allocate memory for the key */ skey = kmalloc(size, GFP_ATOMIC); if (unlikely(!skey)) { pr_err("%s: unable to allocate memory for skey\n", rx->name); - goto exit; + goto exit_unlock; } /* Copy key from msg data */ - skey->keylen = ntohl(*((__be32 *)(data + TIPC_AEAD_ALG_NAME))); + skey->keylen = keylen; memcpy(skey->alg_name, data, TIPC_AEAD_ALG_NAME); memcpy(skey->key, data + TIPC_AEAD_ALG_NAME + sizeof(__be32), skey->keylen); - /* Sanity check */ - if (unlikely(size != tipc_aead_key_size(skey))) { - kfree(skey); - skey = NULL; - goto exit; - } - rx->key_gen = key_gen; rx->skey_mode = msg_key_mode(hdr); rx->skey = skey; rx->nokey = 0; mb(); /* for nokey flag */ - exit: + exit_unlock: spin_unlock(&rx->lock); + exit: /* Schedule the key attaching on this crypto */ if (likely(skey && queue_delayed_work(tx->wq, &rx->work, 0))) return true; -- 2.31.1
This patch moves the size validation to take place before the copy has taken place instead of after it. I’ve also added a size overflow check along with additional checks for the minimum packet size and the supplied key size.
As this vulnerability was discovered within a year of its introduction into the codebase, TIPC users should ensure that their Linux kernel version is not between 5.10-rc1 and 5.15.
The vulnerability research that SentinelLabs conducts allows us to protect users on a global scale by identifying and fixing vulnerabilities before malicious actors do. In the case of TIPC, the vulnerability was caught within a year of its introduction into the codebase. While TIPC itself isn’t loaded automatically by the system but by end users, the ability to configure it from an unprivileged local perspective and the possibility of remote exploitation makes this a dangerous vulnerability for those that use it in their networks. What is more concerning is that an attacker that exploits this vulnerability could execute arbitrary code within the kernel, leading to a complete compromise of the system.
19 Oct 2021 - SentinelLabs supplied the initial vulnerability report to the Kernel.org team
19 Oct 2021 - Greg K.H. responds and adds the TIPC maintainers to the email thread
21 Oct 2021 - The patch is finalised
25 Oct 2021 - The patch is added to lore.kernel.org
29 Oct 2021 - The patch is added to the mainline repository
31 Oct 2021 - The patch is now officially under 5.15
04 Nov 2021 - SentinelLabs publicly disclose details of the vulnerability