├── 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 | 
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 |
--------------------------------------------------------------------------------