附件下载: https://pwnable.tw/static/chall/netatalk.tgz + https://pwnable.tw/static/libc/libc-18292bd12d37bfaf58e8dded9db7f1f5da1192cb.so
耗时大概 1.5 天,总体来说是非常好的一次调试与复现,不止学到了一些利用及调试技巧,也对思路的扩展非常有帮助。
漏洞的发现过程在作者的 Exploiting an 18 Year Old Bug. A Write-up for CVE-2018–1160 | by Jacob Baines 中写的非常清楚,非常精彩。你也可以在 Netatalk CVE-2018-1160的发现与利用_c01dkit的博客-CSDN博客 找到翻译版。
这个洞在作者的 BLOG 中提到只能在 -no-pie
的 NAS 上利用。但是 hitcon 2019 出题的 DDAA 佬在 HITCON CTF 2019 Pwn 371 Netatalk (ddaa.tw) 给出了利用思路,简而言之就是利用 fork 的性质,即子进程不会改变 memory layout —— 换言之,ASLR 只起到了非常微小的作用(笑),如此一来,我们就可以通过侧信道暴露出一个合法地址,再进行利用。
0x01 环境搭建
环境搭建是极其困难的一件事,大概前半天都在琢磨环境上。
直接说最后的解决方案吧:根据 libc 版本用 skysider/pwndocker: A docker environment for pwn in ctf (github.com) 抄抄改改做了一下,然后丢 docker 里运行了。内核版本没有办法解决,wsl2 尝试了很久也没有编译成功 4.9.0
的内核。但是由于内核只影响 mmap 出 chunk 的 offset,所以我们实际上在本地可以忽略这一点。
FROM ubuntu:18.04
RUN dpkg --add-architecture i386 && \
apt-get -y update && \
apt install -y \
libc6:i386 \
libc6-dbg:i386 \
libc6-dbg \
lib32stdc++6 \
g++-multilib \
cmake \
ipython3 \
vim \
net-tools \
iputils-ping \
libffi-dev \
libssl-dev \
python3-dev \
python3-pip \
build-essential \
ruby \
ruby-dev \
tmux \
strace \
ltrace \
nasm \
wget \
gdb \
gdb-multiarch \
gdbserver \
netcat \
socat \
git \
patchelf \
gawk \
file \
python3-distutils \
bison \
rpm2cpio cpio \
zstd
COPY afpd /
COPY afp.conf /
COPY libatalk.so.18 /
USER 0000:0000
EXPOSE 5566
EXPOSE 1234
ENV LD_LIBRARY_PATH=/
CMD ./afpd -d -F ./afp.conf
sudo docker rm pwnable
sudo docker rmi pwnable:cve-2018-1160
sudo docker build . -t pwnable:cve-2018-1160
sudo docker run -dit -p 5566:5566 -p 1234:1234 --name pwnable pwnable:cve-2018-1160
0x02 漏洞分析
在 IDA 中打开 afpd
,观察字符串我们可以知道 netatalk
的版本,下载源代码可以直接分析。
在这之前,或许你还需要了解一下 Data Stream Interface - Wikipedia
根据 blog,我们直接定位到漏洞函数处。
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->commands
是用户可控的,而在 memcpy
操作中,大小就是我们可控的 dsi->commands[i]
之后,服务器会尝试将 dsi->server_quantum
reply 回来。
观察 DSI 结构体
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->commands[i]
是一个 uint8
类型的值,我们可以最大写入 0xff
个字节,也就是可以控制 attn_quantum
, datasize
, server_quantum
, serverID
, clientID
, commands
以及部分的 data
(DSI_DATASIZ
= 65536
)
同时,我们还可以在这里注意到一件事
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
的内存大小是由 dsi->server_quantum
决定的,而这个值默认是 1M
,大于 128k
,也就会由 mmap 分配到 libc
附近。
然后让我们来看看对 commands
还有什么操作。
static ssize_t buf_read(DSI *dsi, uint8_t *buf, size_t count) // buf: dsi->commands
{
ssize_t len;
LOG(log_maxdebug, logtype_dsi, "buf_read(%u bytes)", count);
if (!count)
return 0;
len = from_buf(dsi, buf, count); /* 1. */
if (len)
return len; /* 2. */
len = readt(dsi->socket, buf, count, 0, 0); /* 3. */
LOG(log_maxdebug, logtype_dsi, "buf_read(%u bytes): got: %d", count, len);
return len;
}
static size_t from_buf(DSI *dsi, uint8_t *buf, size_t count)
{
size_t nbe = 0;
if (dsi->buffer == NULL)
/* afpd master has no DSI buffering */
return 0;
LOG(log_maxdebug, logtype_dsi, "from_buf: %u bytes", count);
nbe = dsi->eof - dsi->start;
if (nbe > 0) {
nbe = MIN((size_t)nbe, count);
memcpy(buf, dsi->start, nbe);
dsi->start += nbe;
if (dsi->eof == dsi->start)
dsi->start = dsi->eof = dsi->buffer;
}
LOG(log_debug, logtype_dsi, "from_buf(read: %u, unread:%u , space left: %u): returning %u",
dsi->start - dsi->buffer, dsi->eof - dsi->start, dsi->end - dsi->eof, nbe);
return nbe;
}
我们可以看到,在接收到消息后,它会首先被写到 commands
对应的指针中。
之后,如果 header 中的 command
为 2,即 DSICommand
的话,还会通过一个 afp_switch
的全局跳转表来传递并运行该表上的函数。
case DSIFUNC_WRITE: /* FPWrite and FPAddIcon */
function = (u_char) dsi->commands[0];
if ( afp_switch[ function ] != NULL ) {
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_AFPFUNC_DONE(function, (char *)AfpNum2name(function));
LOG(log_debug, logtype_afpd, "==> Finished AFP command: %s -> %s",
AfpNum2name(function), AfpErr2name(err));
dsi->flags &= ~DSI_RUNNING;
} else {
LOG(log_error, logtype_afpd, "(write) bad function %x", function);
dsi->datalen = 0;
err = AFPERR_NOOP;
}