在Pwn2Own Tokyo 2020比赛上,有2个团队攻破了群晖DS418Play型号的NAS设备,其中DEVCORE团队利用一个堆溢出漏洞在设备上实现了代码执行。根据ZDI的公告,漏洞存在于Netatalk组件中,在解析DSI结构体时由于缺乏对某个长度字段的适当校验,在后续进行拷贝时会出现堆溢出。目前群晖已发布了补丁,该漏洞的触发相对比较简单,参考@Angelboy的分享,在本地环境中完成了对漏洞的利用。
/* 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. */
structdsi_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 */ };
/* 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; // (1) controllable }
afpd开启的保护机制,以及运行时的部分地址空间布局如下。可以看到,dsi->commands的地址为0x7ffff7edf000,在其下方有一段大小为0x1b000的空间,其对应Thread Local Storage(TLS),里面保存了tls相关的结构体、TLS destructors、线程局部变量和线程的main arena指针等信息。
/* Call all functions registered with `atexit' and `on_exit', in the reverse of the order in which they were registered perform stdio cleanup, and terminate program execution with STATUS. */ void attribute_hidden __run_exit_handlers (int status, struct exit_function_list **listp, bool run_list_atexit) { /* First, call the TLS destructors. */ #ifndef SHARED if (&__call_tls_dtors != NULL) #endif __call_tls_dtors ();
/* We do it this way to handle recursive calls to exit () made by the functions registered with `atexit' and `on_exit'. We call everyone on the list and use the status value in the last exit (). */ // ...
在__call_tls_dtors()中,其会遍历tls_dtor_list列表,针对每个dtor_list,在(5)处调用func(cur->obj)。如果能伪造tls_dtor_list,则可以指定其func字段和obj字段,从而实现控制流劫持,且第1个参数可控。另外,在(4)处涉及到pointer demangle,后面会进行说明。根据tls_dtor_list的定义可知,其是一个带有关键字__thread的静态变量,而__thread的作用是告诉编译器将其放入Thread Local Storage(TLS)中,对应前面提到的dsi->commands下方的内存空间。
// elf/dl-tls.c /* The generic dynamic and local dynamic model cannot be used in statically linked applications. */ void * __tls_get_addr (GET_ADDR_ARGS) { dtv_t *dtv = THREAD_DTV ();
if (__glibc_unlikely (dtv[0].counter != GL(dl_tls_generation))) return update_get_addr (GET_ADDR_PARAM);
void *p = dtv[GET_ADDR_MODULE].pointer.val;
if (__glibc_unlikely (p == TLS_DTV_UNALLOCATED)) return tls_get_addr_tail (GET_ADDR_PARAM, dtv, NULL);
return (char *) p + GET_ADDR_OFFSET; }
// sysdeps/x86_64/nptl/tls.h /* Return the address of the dtv for the current thread. */ # define THREAD_DTV() \ ({ struct pthread *__pd; \ THREAD_GETMEM (__pd, header.dtv); })
/* Read member of the thread descriptor directly. */ # define THREAD_GETMEM(descr, member) \ ({ __typeof (descr->member) __value; \ if (sizeof (__value) == 1) \ asmvolatile ("movb %%fs:%P2,%b0" \ : "=q" (__value) \ : "0" (0), "i" (offsetof (struct pthread, member))); \ elseif (sizeof (__value) == 4) \ asmvolatile ("movl %%fs:%P1,%0" \ : "=r" (__value) \ : "i" (offsetof (struct pthread, member))); \ else \ { \ if (sizeof (__value) != 8) \ /* There should not be any value with a size other than 1, \ 4 or 8. */ \ abort (); \ \ asmvolatile ("movq %%fs:%P1,%q0" \ : "=r" (__value) \ : "i" (offsetof (struct pthread, member))); \ } \ __value; })
/* Thread descriptor data structure. */ structpthread { union { #if !TLS_DTV_AT_TP /* This overlaps the TCB as used for TLS without threads (see tls.h). */ tcbhead_t header; #else struct { int multiple_threads; int gscope_flag; # ifndef __ASSUME_PRIVATE_FUTEX int private_futex; # endif } header; #endif
// sysdeps/x86_64/nptl/tls.h typedefstruct { void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */ dtv_t *dtv; void *self; /* Pointer to the thread descriptor. */ int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; // ... } tcbhead_t;
/* ------------------------------------------- afp over dsi. this never returns. */ voidafp_over_dsi(AFPObj *obj) { // ... /* get stuck here until the end */ while (1) { // ... /* Blocking read on the network socket */ cmd = dsi_stream_receive(dsi);
if (cmd == 0) { // ... } switch(cmd) { case DSIFUNC_CLOSE: // DSICloseSession LOG(log_debug, logtype_afpd, "DSI: close session request"); afp_dsi_close(obj); LOG(log_note, logtype_afpd, "done"); exit(0); // ...
// sysdeps/x86_64/nptl/tls.h typedefstruct { void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */ dtv_t *dtv; void *self; /* Pointer to the thread descriptor. */ int multiple_threads; int gscope_flag; uintptr_t sysinfo; uintptr_t stack_guard; uintptr_t pointer_guard; // ... } tcbhead_t;
本文基于群晖DSM6.1.7-15284版本,对Pwn2Own Tokyo 2020比赛上DEVCORE团队使用的堆溢出漏洞进行了分析。漏洞的触发相对比较简单,漏洞的利用思路则参考了@Angelboy的议题《Your NAS is not your NAS !》,介绍通过伪造tls_dtor_list来实现代码执行的目的,并对其中的一些关键点如寻找内容可控的地址空间、stack_guard泄露、pointer demangle等进行了细致分析。之前对tls_dtor_list这块不太了解,在完成漏洞利用的过程中学到了很多,感兴趣地可以自己动手搭建环境试试。