├── Makefile ├── LICENSE ├── README.md ├── andes_export_filldir_readdir.patch └── hidefs.c /Makefile: -------------------------------------------------------------------------------- 1 | obj-m += hidefs.o 2 | 3 | all: 4 | make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules 5 | 6 | clean: 7 | make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean 8 | 9 | 10 | ### In case I ever publish the implementation that uses Khook to hook into functions instead of Kprobes (which has a blacklist and WONT let me access the readdir family of functions!) 11 | 12 | #obj-m += hidefs.o 13 | 14 | # Include the KHOOK Makefile 15 | # include $(PWD)/khook/Makefile.khook 16 | # 17 | # # Add KHOOK goals and flags 18 | # $(MODNAME)-y += $(KHOOK_GOALS) 19 | # ccflags-y += $(KHOOK_CCFLAGS) 20 | # ldflags-y += $(KHOOK_LDFLAGS) 21 | 22 | # all: 23 | # make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules 24 | # 25 | # clean: 26 | # make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean 27 | # 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, xplshn 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # (1) HideFS - A kernel module that allows hiding files in __ANY__ filesystem 2 | 3 | I was inspired by gobohide, but I didn't like the following about GoboHide: 4 | 5 | 1. Not a module, it is ALWAYS enabled 6 | 2. Uses Netlink, instead of just exposing a character device that can be interfaced with shell, C, or anything else 7 | 3. Bugs out in 6.1+ 8 | 4. The license 9 | 10 | So I decided to start from scratch 11 | 12 | # (2) HideFS - Interface: 13 | - /sys/kernel/hidefs/hide: write here the filepaths which you wish to hide, (1 path per line) 14 | - /sys/kernel/hidefs/unhide: write here the filepaths which you wish to unhide, (1 path per line) 15 | - /sys/kernel/hidefs/list: the list of currently hidden files, (1 path per line) 16 | 17 | # (3) Limitations (help wanted ; we need ports to i386, arm64, armv7) 18 | - Architecture support: 19 | 20 | > Currently, only amd64 is supported. We could support other architectures if I had access to other hardware or hardware capable enough to emulate other architectures. In order to support other architectures, we'd just have to wrap the kprobes-related code around macros. 21 | 22 | # (4) TODOs 23 | - Benchmark, `a-utils/walk`, 100 runs, without the kernel module. Then again, 100 runs, with the kernel module. 24 | - Use dynamic memory management, and benchmark once again. 25 | - Fork 9front, add a compatibility layer with Linux, implement +200 POSIX system calls + linux's, then check if this module works, if so, break it, because 9front is a sacred zen space. (may actually be done in the near future, I just want 9front + Chromium || Ladybird) 26 | 27 |
28 | 29 | > [!NOTE] 30 | > This module tracks the current generation of the kernel (6.1 and onwards), support for non-current generations is just sheer luck. 31 | 32 |
33 | 34 | # ⑨ Show & tell 35 | 36 | ![A screenshot that shows off: the build process for hidefs, the loading process, and how it is used to hide various FHS paths like /bin, /lib, /usr, etc, on a that has those paths linked to /System/Variable, /System/Libraries, etc.](https://github.com/user-attachments/assets/13918b3d-d620-487c-8a4c-c3778fa8a285) 37 | -------------------------------------------------------------------------------- /andes_export_filldir_readdir.patch: -------------------------------------------------------------------------------- 1 | --- linux-6.10.6/fs/readdir.c 2 | +++ ./readdir.c 3 | @@ -180,7 +180,7 @@ 4 | int result; 5 | }; 6 | 7 | -static bool fillonedir(struct dir_context *ctx, const char *name, int namlen, 8 | +bool fillonedir(struct dir_context *ctx, const char *name, int namlen, 9 | loff_t offset, u64 ino, unsigned int d_type) 10 | { 11 | struct readdir_callback *buf = 12 | @@ -216,6 +216,7 @@ 13 | buf->result = -EFAULT; 14 | return false; 15 | } 16 | +EXPORT_SYMBOL(fillonedir); 17 | 18 | SYSCALL_DEFINE3(old_readdir, unsigned int, fd, 19 | struct old_linux_dirent __user *, dirent, unsigned int, count) 20 | @@ -259,7 +260,7 @@ 21 | int error; 22 | }; 23 | 24 | -static bool filldir(struct dir_context *ctx, const char *name, int namlen, 25 | +bool filldir(struct dir_context *ctx, const char *name, int namlen, 26 | loff_t offset, u64 ino, unsigned int d_type) 27 | { 28 | struct linux_dirent __user *dirent, *prev; 29 | @@ -307,6 +308,7 @@ 30 | buf->error = -EFAULT; 31 | return false; 32 | } 33 | +EXPORT_SYMBOL(filldir); 34 | 35 | SYSCALL_DEFINE3(getdents, unsigned int, fd, 36 | struct linux_dirent __user *, dirent, unsigned int, count) 37 | @@ -347,7 +349,7 @@ 38 | int error; 39 | }; 40 | 41 | -static bool filldir64(struct dir_context *ctx, const char *name, int namlen, 42 | +bool filldir64(struct dir_context *ctx, const char *name, int namlen, 43 | loff_t offset, u64 ino, unsigned int d_type) 44 | { 45 | struct linux_dirent64 __user *dirent, *prev; 46 | @@ -390,6 +392,7 @@ 47 | buf->error = -EFAULT; 48 | return false; 49 | } 50 | +EXPORT_SYMBOL(filldir64); 51 | 52 | SYSCALL_DEFINE3(getdents64, unsigned int, fd, 53 | struct linux_dirent64 __user *, dirent, unsigned int, count) 54 | @@ -437,7 +440,7 @@ 55 | int result; 56 | }; 57 | 58 | -static bool compat_fillonedir(struct dir_context *ctx, const char *name, 59 | +bool compat_fillonedir(struct dir_context *ctx, const char *name, 60 | int namlen, loff_t offset, u64 ino, 61 | unsigned int d_type) 62 | { 63 | @@ -474,6 +477,7 @@ 64 | buf->result = -EFAULT; 65 | return false; 66 | } 67 | +EXPORT_SYMBOL(compat_fillonedir); 68 | 69 | COMPAT_SYSCALL_DEFINE3(old_readdir, unsigned int, fd, 70 | struct compat_old_linux_dirent __user *, dirent, unsigned int, count) 71 | @@ -511,7 +515,7 @@ 72 | int error; 73 | }; 74 | 75 | -static bool compat_filldir(struct dir_context *ctx, const char *name, int namlen, 76 | +bool compat_filldir(struct dir_context *ctx, const char *name, int namlen, 77 | loff_t offset, u64 ino, unsigned int d_type) 78 | { 79 | struct compat_linux_dirent __user *dirent, *prev; 80 | @@ -558,6 +562,7 @@ 81 | buf->error = -EFAULT; 82 | return false; 83 | } 84 | +EXPORT_SYMBOL(compat_filldir); 85 | 86 | COMPAT_SYSCALL_DEFINE3(getdents, unsigned int, fd, 87 | struct compat_linux_dirent __user *, dirent, unsigned int, count) 88 | -------------------------------------------------------------------------------- /hidefs.c: -------------------------------------------------------------------------------- 1 | #define pr_fmt(fmt) "%s: " fmt, __func__ 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | //#ifdef DEBUG 18 | #define dbg_print(fmt, ...) pr_info(fmt, ##__VA_ARGS__) 19 | //#else 20 | //#define dbg_print(fmt, ...) /* No-op */ 21 | //#endif 22 | 23 | // Because this module is meant to be a replacement for GoboHide, nothing else. 24 | #define MAX_HIDDEN_PATHS 64 25 | #define MAX_PATH_LENGTH 64 26 | #define MAX_NAME_LENGTH 64 27 | 28 | // Hidden paths entry structure 29 | struct hidden_entry { 30 | char path[MAX_PATH_LENGTH]; 31 | u64 ino; 32 | char name[MAX_NAME_LENGTH]; 33 | struct list_head list; 34 | }; 35 | 36 | // Global state for hidden paths 37 | static DEFINE_MUTEX(hidden_paths_mutex); 38 | static LIST_HEAD(hidden_paths_list); 39 | static int hidden_paths_count = 0; 40 | 41 | // Sysfs kobject 42 | static struct kobject *hidefs_kobj; 43 | 44 | /* For each probe you need to allocate a kprobe structure */ 45 | static struct kprobe kp = { 46 | .symbol_name = "filldir64", 47 | }; 48 | 49 | /* Function to check if a file is hidden */ 50 | static bool is_hidden(u64 ino, const char *name) 51 | { 52 | struct hidden_entry *entry; 53 | bool hidden = false; 54 | 55 | mutex_lock(&hidden_paths_mutex); 56 | list_for_each_entry(entry, &hidden_paths_list, list) { 57 | if (entry->ino == ino && strcmp(entry->name, name) == 0) { 58 | hidden = true; 59 | break; 60 | } 61 | if (entry->ino == 1 && strcmp(entry->name, name) == 0) { // Special case for pseudo-filesystems 62 | hidden = true; 63 | break; 64 | } 65 | } 66 | mutex_unlock(&hidden_paths_mutex); 67 | 68 | return hidden; 69 | } 70 | 71 | /* kprobe pre_handler: called just before the probed instruction is executed */ 72 | static int __kprobes handler_pre(struct kprobe *p, struct pt_regs *regs) 73 | { 74 | char *filename = (char *)regs->si; 75 | u64 ino = regs->r8; 76 | 77 | dbg_print( 78 | "<%s> p->addr = 0x%p, ip = %lx, rdi=%lx, rsi=%s, flags = 0x%lx, ino = %llu\n", 79 | p->symbol_name, p->addr, regs->ip, regs->di, (char *)regs->si, 80 | regs->flags, (unsigned long long)ino); 81 | 82 | if (is_hidden(ino, filename)) { 83 | dbg_print("Hiding file/path: %s (matches hidden entry)\n", filename); 84 | strcpy((char *)regs->si, "\x00"); 85 | } 86 | 87 | return 0; 88 | } 89 | 90 | static const unsigned char* basename(const char* path) { 91 | char* r = strrchr(path, '/'); 92 | if (r) { 93 | r++; 94 | } 95 | return r; 96 | } 97 | 98 | // Helper function to register a hidden path 99 | static int register_hidden_path(const char *kernel_path) 100 | { 101 | struct hidden_entry *entry; 102 | struct path path; 103 | int err; 104 | 105 | if (!capable(CAP_SYS_ADMIN)) 106 | return -EPERM; 107 | 108 | // Trim trailing whitespace 109 | size_t path_len = strlen(kernel_path); 110 | char trimmed_path[MAX_PATH_LENGTH]; 111 | strncpy(trimmed_path, kernel_path, sizeof(trimmed_path)); 112 | while (path_len > 0 && (trimmed_path[path_len-1] == ' ' || trimmed_path[path_len-1] == '\n')) { 113 | trimmed_path[path_len-1] = '\0'; 114 | path_len--; 115 | } 116 | 117 | // Check for duplicate paths and maximum paths 118 | mutex_lock(&hidden_paths_mutex); 119 | 120 | if (path_len >= MAX_PATH_LENGTH) { 121 | mutex_unlock(&hidden_paths_mutex); 122 | return -ENAMETOOLONG; 123 | } 124 | 125 | if (hidden_paths_count >= MAX_HIDDEN_PATHS) { 126 | mutex_unlock(&hidden_paths_mutex); 127 | return -ENOSPC; 128 | } 129 | 130 | // Check path validity 131 | err = kern_path(trimmed_path, LOOKUP_DIRECTORY, &path); 132 | if (err) { 133 | mutex_unlock(&hidden_paths_mutex); 134 | return err; 135 | } 136 | 137 | // Check if path already exists 138 | struct hidden_entry *existing_entry; 139 | list_for_each_entry(existing_entry, &hidden_paths_list, list) { 140 | if (strcmp(existing_entry->path, trimmed_path) == 0) { 141 | path_put(&path); 142 | mutex_unlock(&hidden_paths_mutex); 143 | return 0; // Path already registered, return 0 144 | } 145 | } 146 | 147 | // Create and insert new entry 148 | entry = kmalloc(sizeof(struct hidden_entry), GFP_KERNEL); 149 | if (!entry) { 150 | path_put(&path); 151 | mutex_unlock(&hidden_paths_mutex); 152 | return -ENOMEM; 153 | } 154 | 155 | strncpy(entry->path, trimmed_path, MAX_PATH_LENGTH); 156 | entry->ino = path.dentry->d_inode->i_ino; 157 | strncpy(entry->name, (strcmp(path.dentry->d_name.name, "/") == 0) ? basename(entry->path) : path.dentry->d_name.name, MAX_NAME_LENGTH); 158 | list_add_tail(&entry->list, &hidden_paths_list); 159 | 160 | hidden_paths_count++; 161 | path_put(&path); 162 | mutex_unlock(&hidden_paths_mutex); 163 | 164 | pr_info("Registered hidden path (%s), (%llu), name (%s)\n", entry->path, (unsigned long long)entry->ino, entry->name); 165 | return 0; 166 | } 167 | 168 | // Helper function to unregister a hidden path 169 | static int unregister_hidden_path(const char *kernel_path) 170 | { 171 | struct hidden_entry *entry, *tmp; 172 | 173 | if (!capable(CAP_SYS_ADMIN)) 174 | return -EPERM; 175 | 176 | mutex_lock(&hidden_paths_mutex); 177 | 178 | list_for_each_entry_safe(entry, tmp, &hidden_paths_list, list) { 179 | if (strcmp(entry->path, kernel_path) == 0) { 180 | list_del(&entry->list); 181 | kfree(entry); 182 | hidden_paths_count--; 183 | mutex_unlock(&hidden_paths_mutex); 184 | 185 | pr_info("Unregistered hidden path %s\n", kernel_path); 186 | return 0; 187 | } 188 | } 189 | 190 | mutex_unlock(&hidden_paths_mutex); 191 | return 0; // Path not registered, return 0 192 | } 193 | 194 | // Sysfs write handler for hide 195 | static ssize_t hide_store(struct kobject *kobj, struct kobj_attribute *attr, 196 | const char *buf, size_t count) 197 | { 198 | int ret = 0; 199 | int paths_hidden = 0; 200 | 201 | // Iterate over each newline-separated path in the buffer 202 | char *buf_copy = kstrdup(buf, GFP_KERNEL); 203 | if (!buf_copy) 204 | return -ENOMEM; 205 | 206 | char *token = strsep(&buf_copy, "\n"); 207 | while (token) { 208 | // Skip empty tokens 209 | if (strlen(token) == 0) { 210 | token = strsep(&buf_copy, "\n"); 211 | continue; 212 | } 213 | 214 | char kernel_path[MAX_PATH_LENGTH]; 215 | 216 | // Copy the current path to kernel_path 217 | strncpy(kernel_path, token, MAX_PATH_LENGTH - 1); 218 | kernel_path[MAX_PATH_LENGTH - 1] = '\0'; 219 | 220 | pr_info("Attempting to hide path: %s\n", kernel_path); 221 | 222 | // Register the path 223 | ret = register_hidden_path(kernel_path); 224 | if (ret == -ENOSPC) { 225 | pr_warn("Hidding more than 64 paths means unhiding the paths you hid first\n"); 226 | ret = 0; // Continue processing other paths 227 | } else if (ret != 0) { 228 | pr_err("Failed to hide path: %s, error: %d\n", kernel_path, ret); 229 | kfree(buf_copy); 230 | return ret; 231 | } 232 | 233 | paths_hidden++; 234 | token = strsep(&buf_copy, "\n"); 235 | } 236 | 237 | kfree(buf_copy); 238 | // Return count on success 239 | return count; 240 | } 241 | 242 | // Sysfs write handler for unhide 243 | static ssize_t unhide_store(struct kobject *kobj, struct kobj_attribute *attr, 244 | const char *buf, size_t count) 245 | { 246 | char *buf_copy = kstrdup(buf, GFP_KERNEL); 247 | if (!buf_copy) 248 | return -ENOMEM; 249 | 250 | char *token = strsep(&buf_copy, "\n"); 251 | int ret = 0; 252 | 253 | while (token) { 254 | char kernel_path[MAX_PATH_LENGTH]; 255 | 256 | // Copy the current path to kernel_path 257 | strncpy(kernel_path, token, MAX_PATH_LENGTH - 1); 258 | kernel_path[MAX_PATH_LENGTH - 1] = '\0'; 259 | 260 | pr_info("Attempting to unhide path: %s\n", kernel_path); 261 | 262 | // Unregister the path 263 | ret = unregister_hidden_path(kernel_path); 264 | if (ret != 0) { 265 | pr_err("Failed to unhide path: %s, error: %d\n", kernel_path, ret); 266 | kfree(buf_copy); 267 | return ret; 268 | } 269 | 270 | token = strsep(&buf_copy, "\n"); 271 | } 272 | 273 | kfree(buf_copy); 274 | // Return count on success 275 | return count; 276 | } 277 | 278 | // Sysfs read handler for list 279 | static ssize_t list_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf) 280 | { 281 | struct hidden_entry *entry; 282 | ssize_t offset = 0; 283 | 284 | mutex_lock(&hidden_paths_mutex); 285 | 286 | list_for_each_entry(entry, &hidden_paths_list, list) { 287 | size_t len = strlen(entry->path); 288 | 289 | // Check if we have room to write this path 290 | if (offset + len + 1 > PAGE_SIZE) 291 | break; 292 | 293 | // Copy path to buffer with null terminator 294 | strcpy(buf + offset, entry->path); 295 | offset += len + 1; 296 | } 297 | 298 | mutex_unlock(&hidden_paths_mutex); 299 | return offset; 300 | } 301 | 302 | // Sysfs attributes 303 | static struct kobj_attribute hide_attr = { 304 | .attr = { 305 | .name = "hide", 306 | .mode = 0666, 307 | }, 308 | .store = hide_store, 309 | }; 310 | 311 | static struct kobj_attribute unhide_attr = { 312 | .attr = { 313 | .name = "unhide", 314 | .mode = 0666, 315 | }, 316 | .store = unhide_store, 317 | }; 318 | 319 | static struct kobj_attribute list_attr = { 320 | .attr = { 321 | .name = "list", 322 | .mode = 0444, 323 | }, 324 | .show = list_show, 325 | }; 326 | 327 | // Attribute group 328 | static struct attribute *hidefs_attrs[] = { 329 | &hide_attr.attr, 330 | &unhide_attr.attr, 331 | &list_attr.attr, 332 | NULL, 333 | }; 334 | static struct attribute_group hidefs_attr_group = { 335 | .attrs = hidefs_attrs, 336 | }; 337 | 338 | static int __init file_hide_init(void) 339 | { 340 | int ret; 341 | 342 | // Create sysfs entry 343 | hidefs_kobj = kobject_create_and_add("hidefs", kernel_kobj); 344 | if (!hidefs_kobj) 345 | return -ENOMEM; 346 | 347 | ret = sysfs_create_group(hidefs_kobj, &hidefs_attr_group); 348 | if (ret) { 349 | kobject_put(hidefs_kobj); 350 | return ret; 351 | } 352 | 353 | // Setup kprobe 354 | kp.pre_handler = handler_pre; 355 | ret = register_kprobe(&kp); 356 | if (ret < 0) { 357 | pr_err("register_kprobe failed, returned %d\n", ret); 358 | sysfs_remove_group(hidefs_kobj, &hidefs_attr_group); 359 | kobject_put(hidefs_kobj); 360 | return ret; 361 | } 362 | 363 | pr_info("filldir64: %px\n", kp.addr); 364 | pr_info("Hidefs module loaded successfully\n"); 365 | return 0; 366 | } 367 | 368 | static void __exit file_hide_exit(void) 369 | { 370 | struct hidden_entry *entry, *tmp; 371 | 372 | // Unregister kprobe 373 | unregister_kprobe(&kp); 374 | 375 | // Remove sysfs entries 376 | sysfs_remove_group(hidefs_kobj, &hidefs_attr_group); 377 | kobject_put(hidefs_kobj); 378 | 379 | // Free allocated memory for hidden paths 380 | mutex_lock(&hidden_paths_mutex); 381 | list_for_each_entry_safe(entry, tmp, &hidden_paths_list, list) { 382 | list_del(&entry->list); 383 | kfree(entry); 384 | } 385 | mutex_unlock(&hidden_paths_mutex); 386 | 387 | pr_info("Hidefs module unloaded successfully\n"); 388 | } 389 | 390 | module_init(file_hide_init); 391 | module_exit(file_hide_exit); 392 | 393 | MODULE_AUTHOR("xplshn"); 394 | MODULE_DESCRIPTION("hidefs"); 395 | MODULE_LICENSE("Dual BSD/GPL"); // I would love for this to be 3BSD OR/AND ISC, but I cannot do that! The kernel and its license sucks. :( // I wish making a oBSD distro was as easy as making a Linux one. 396 | --------------------------------------------------------------------------------