小米路由器4-从开始到放弃

小米路由器4-从开始到放弃

之前入手了一台小米路由器4,想着来分析一下这款路由器,结果发现每一步都不好做,里面做了很多防护措施,最后由于水平有限,选择了放弃。不过这里还是把整个过程记录一下,由于文章是边做边记录的,所以结构上会有点杂乱。

路由器信息

路由器型号:小米路由器4

后台界面:miwifi.com,192.168.31.1

固件版本:2.26.145

固件的在官网上就可以下载到

初步分析

在下载的固件的基础上对小米路由器进行了一些分析,但是binwalk解压出来的内容不全面,且没有找到cgi的具体实现。同时路由器是通过OPENWRT架构实现的,httpd服务集成在了busybox中,在网上也有其源码,分析了其权限校验等实现,没有发现问题。

利用Burp抓与路由器交互的数据包,在其请求的url中确实发现了cgi的字样,说明路由器运行时确实有cgi,只不过固件中没有发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /cgi-bin/luci/api/xqsystem/login HTTP/1.1
Host: miwifi.com
Proxy-Connection: keep-alive
Content-Length: 125
Accept: */*
Origin: http://miwifi.com
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Referer: http://miwifi.com/cgi-bin/luci/web/home
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: Hm_lvt_fe14c89e4d8b8c2e3038dd2b388a108f=1552889726; Hm_lpvt_fe14c89e4d8b8c2e3038dd2b388a108f=1552889728; __guid=236905885.1063846128660310800.1552890281870.1318; psp=admin|||2|||0; monitor_count=19

username=admin&password=61723585ffcf5700ff42a4cf1b68b74ea1f320cb&logtype=2&nonce=0_34%3Af3%3A9a%3A67%3A9a%3A94_1552891294_315

同时在返回的数据包里面发现了如下的一些信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
HTTP/1.1 200 OK
Server: nginx
Date: Tue, 31 Jul 2018 04:58:43 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Cache-Control: no-cache
Expires: Thu, 01 Jan 1970 00:00:01 GMT
MiCGI-Switch: 1 1
//看这个字段
MiCGI-Client-Ip: 192.168.31.21
MiCGI-Host: 192.168.31.1
MiCGI-Http-Host: 192.168.31.1
MiCGI-Server-Ip: 192.168.31.1
MiCGI-Server-Port: 80
MiCGI-Status: CGI
MiCGI-Preload: no
Content-Length: 111842

其中192.168.31.21地址是提供cgi服务的客户端?

接着通过NMap扫描了路由器开放的端口信息

192.168.31.1开放的端口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Starting Nmap 7.01 ( https://nmap.org ) at 2019-04-08 10:03 CST
Nmap scan report for bogon (192.168.31.1)
Host is up (1.0s latency).
Not shown: 989 closed ports
PORT STATE SERVICE
53/tcp open domain
80/tcp open http
82/tcp filtered xfer
514/tcp filtered shell
4003/tcp filtered pxc-splr-ft
5003/tcp filtered filemaker
8080/tcp open http-proxy
8192/tcp open sophos
8193/tcp open sophos
8383/tcp open m2mservices
8899/tcp open ospf-lite

Nmap done: 1 IP address (1 host up) scanned in 107.41 seconds

192.168.31.21开放的端口如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜  _miwifi_r4_firmware_f1bbb_2.26.145.bin.extracted nmap 192.168.31.21

Starting Nmap 7.01 ( https://nmap.org ) at 2019-04-08 10:29 CST
Nmap scan report for 192.168.31.21
Host is up (1.0s latency).
Not shown: 988 closed ports
PORT STATE SERVICE
135/tcp open msrpc
139/tcp open netbios-ssn
514/tcp filtered shell
902/tcp open iss-realsecure
912/tcp open apex-mesh
1025/tcp open NFS-or-IIS
1026/tcp open LSA-or-nterm
1027/tcp open IIS
1031/tcp open iad2
1034/tcp open zincite-a
1914/tcp filtered elm-momentum
2869/tcp open icslap

Nmap done: 1 IP address (1 host up) scanned in 56.69 seconds

对于哪些端口可能存在问题,需要先知道是那些程序提供这些服务的,需要在获取到shell之后专门进行研究。

通过UART获取路由器shell

为了知道cgi接口的具体实现,需要获取到路由器的shell,在进一步进行分析,所以这里直接通过uart来获取路由器的shell。

找到uart调试口

拆开路由器后发现了4个并排的小孔,感觉这就是板子上的uart口,但是没有发现每个口对应的到底是TX/RX/GND/VCC中的哪一个:

仔细观察四个孔的下面有四个序号,分别是:

1
2
3
4
4: White/TX
3: Black/GND
2: Gree/RX
1:

猜想这就是上面的四个孔对应的编号,用什么方法确定呢?

根据参考文章中的描述,将黑色表笔接到IoT设备板子上的电路接口上固定不动,红色表笔依次在四个接口上进行通断测试,如果万用表有蜂鸣声,我们就可以判断此接口是GND接口。使用万用表,将黑色的线插入COM口,红色的线插入V/Ω口,调整旋钮到测量二极管通断处。接着把黑色指针放到供电口,也就是供电线与路由器相接的地方,把红色指针放到从上往下的第二个口。这时发现万用表发出了蜂鸣声,放到其他的口却没有,说明第二个口就是GND,即四个编号刚好对应了四个口。

注意:还有其他的几种方法来判定uart口,可以参考给出的参考链接里面的文章

接线如下图所示:

将黑色表笔放在供电处,红色表笔依次在四个口处试探:

发现放在第二个孔处时,万用表发出了轰鸣声:

ttl转usb

按照:
TX–>RX,Rx–>TX,GND–>GND
的方式连接uart口和ttl转usb口的线路

连接好之后插入电脑上的usb口,在电脑上查看串口设备,总是发现不了我们插入的设备!!!

是因为没有安装驱动,所以才识别不了我们的设备?
使用驱动精灵,发现有三个设备异常,其中第一个和第三个应该和我们遇到的问题相关:

安装驱动后重启电脑,发现能够识别出来串口设备了(其他几个usb转ttl设备均能够正确识别):

利用xshell连接设备

按照下图所示配置好xshell:

给路由器插上电源,可以看到路由器输出的信息,但是并不能往其中输入数据:

所以从uart获取shell这条路就被堵住了,还是从固件本身的分析入手吧。

挂载ubi文件系统

在固件解压出来的结果中,可以发现还有一个后缀名为.ubi的文件没有用到,而且该文件的大小和另一个文件的大小基本一致,再结合参考链接中的一些分析文章中都提到了挂载ubi文件系统,所以这里进行一个尝试,来挂载一下该ubi文件系统。

github上有现成的工具,直接安装即可使用:
https://github.com/jrspruitt/ubi_reader/

按照github上面给的步骤即可正常安装,直接使用命令进行提取发现出现错误:

结合链接中提到的,ubireader对文件的校验比较严格,必须补齐每一个块内容,当最后一个块内容没填充满或是有多余的内容,会提示错误信息。

这里发现最后一个块里面的内容确实超出了应有的部分,最后一个块正常情况下在0xC60000处就应该结束了:

所以这里采取的做法是删除掉前面和多出来的数据一样大小的数据:

然后使用命令进行提取:
ubireader_extract_images 338A20-2.ubi

进入到./ubifs-root/338A20-2.ubi目录下面,可以看到生成了一个后缀为ubifs的文件,直接双击发现能够打开该文件:

里面存在着大量的后缀为.lua的文件:

这个文件系统是可以直接解压出来的,可以进一步分析了!!!

进一步分析

解压出来新得到的文件系统,在文件系统中执行:

find ./ -name “*httpd*“

可以看到找到了许多包含该名称的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
./etc/init.d/uhttpd
./etc/init.d/sysapihttpd
./etc/rc.d/S50uhttpd
./etc/rc.d/S21sysapihttpd
./etc/rc.d/K60sysapihttpd
./etc/sysapihttpd
./etc/sysapihttpd/sysapihttpd.conf
./etc/config/uhttpd
./userdisk/sysapihttpd
./lib/upgrade/keep.d/uhttpd
./lib/upgrade/keep.d/sysapihttpd
./usr/sbin/uhttpd
./usr/sbin/sysapihttpd
./usr/lib/lua/luci/sgi/uhttpd.lua
./usr/lib/uhttpd_lua.so

其中uhttpd文件比较引人注意,把/usr/sbin/uhttpd放入IDA中进行分析,分析后发现该程序就是cgi处理程序,在busybox中的httpd里把调用cgi的各个参数设置好,然后在该程序中进行处理

该程序中最关键的就是函数uh_cgi_request,该函数先分配了一大块内存,然后fork出了一个子进程,在子进程的一个循环中中获取了传进来的各个参数,最后调用了execl来执行脚本:

搜索了一下相关资料,uhttpd也是一个开源的web服务器,可以参考网上的源码分析。

通过本篇资料,知道uhttpd的配置文件为/etc/config/uhttpd,脚本/etc/init.d/uhttpd用来控制服务的运行状态,会在OpenWrt启动的时候启动。

查看uhttpd的配置文件,可以发现有我们关注的信息,包括lua_prefixlua_handler以及cgi_prefix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	# CGI url prefix, will be searched in docroot.
# Default is /cgi-bin
option cgi_prefix /cgi-bin

# List of extension->interpreter mappings.
# Files with an associated interpreter can
# be called outside of the CGI prefix and do
# not need to be executable.
# list interpreter ".php=/usr/bin/php-cgi"
# list interpreter ".cgi=/usr/bin/perl"

# Lua url prefix and handler script.
# Lua support is disabled if no prefix given.
option lua_prefix /luci
option lua_handler /usr/lib/lua/luci/sgi/uhttpd.lua

结合uhttpd的源码,可以知道lua将请求分为了三类,分别是CGI请求文件请求lua请求。其中cgi请求会进入到uh_cgi_request函数进行处理,文件请求会进入uh_file_request进行处理,而lua请求则会进入到lua_request中处理。同时通过源码会发现,程序在分发请求时,会最先判断是否是lua请求,如果是的话就会进入对应的处理函数进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static bool uh_dispatch_request(struct client *cl, struct http_request *req) {
struct path_info *pin;
#ifdef HAVE_CGI
struct interpreter *ipr = NULL;
#endif
struct config *conf = cl->server->conf;

#ifdef HAVE_LUA
// 可以看到这里最先判断的是lua请求
/* Lua request? */
if (conf->lua_state && uh_path_match(conf->lua_prefix, req->url)) {
return conf->lua_request(cl, conf->lua_state);
} else
#endif

#ifdef HAVE_UBUS
/* ubus request? */
if (conf->ubus_state && uh_path_match(conf->ubus_prefix, req->url)) {
return conf->ubus_request(cl, conf->ubus_state);
} else
#endif

/* dispatch request */
if ((pin = uh_path_lookup(cl, req->url)) != NULL) {
/* 检查请求路径权限 */
if (!pin->redirected && uh_auth_check(cl, req, pin)) {
#ifdef HAVE_CGI
/* 是否在CGI前缀目录下的文件或者文件后缀是否有关联的解析器 */
if (uh_path_match(conf->cgi_prefix, pin->name) ||
(ipr = uh_interpreter_lookup(pin->phys)) != NULL) {
return uh_cgi_request(cl, pin, ipr);
}
...
...

从uhttpd的二进制程序中也能找到判断lua请求的部分:

固件中自己实现的部分主要有cgilua这两部分,其他部分都是框架中提供的,所以接下来主要从这两部分进行分析。

分析lua脚本部分

打开/usr/lib/lua/luci/sgi/uhttpd.lua,发现该文件是一个二进制文件,而不是直接可见的lua脚本

这应该是lua脚本编译后得到的二进制程序,尝试反编译该二进制程序,得到脚本对应的源码

github上有一个luadec的项目,专门用来反编译lua程序的,按照官方的流程编译出错

1
2
3
4
5
6
7
8
9
10
11
12
13
gcc -O2 -Wall -DLUA_USE_LINUX   -c -o lua.o lua.c
In file included from lua.h:16:0,
from lua.c:15:
luaconf.h:275:31: fatal error: readline/readline.h: No such file or directory
compilation terminated.
<builtin>: recipe for target 'lua.o' failed
make[2]: *** [lua.o] Error 1
make[2]: Leaving directory '/home/em/Desktop/software/luadec/lua-5.1/src'
Makefile:99: recipe for target 'linux' failed
make[1]: *** [linux] Error 2
make[1]: Leaving directory '/home/em/Desktop/software/luadec/lua-5.1/src'
Makefile:56: recipe for target 'linux' failed
make: *** [linux] Error 2

安装一下下列的程序即可

sudo apt-get install libreadline-dev

又报如下错误:

解决方法是:sudo apt-get install libncurses-dev

随后便可成功编译

如何对编译后的lua程序进行分析?

https://blog.csdn.net/liujiayu2/article/details/81942010

https://blog.csdn.net/liutianshx2012/article/details/44786207

https://www.52pojie.cn/thread-697540-1-1.html

向逆向大佬请教了一下,大佬说luadec只能反汇编,反编译会失败,而且这应该是加密之后的文件。在路由器里面应该有解密操作,不然这个脚本跑不起来

每一个.lua脚本里面都有Fate/Z这样的标志位:

这种标志位的匹配,解密比较一般有两种:比较Fate字符串和Fate的十六进制。所以这里全局搜索一下这个字符串,还真的发现了一个可疑的文件

看该文件的名字可以知道这属于lua脚本执行过程中用到的一个so库,会不会是在这个so库文件中进行的解密操作?逆向分析一下

分析liblua.so.5.1.5库

在IDA中的字符串窗口中搜索字符串,发现了lua文件中的标志字段,Fate/Z前后的\x1B字节也都和lua文件中保持一致

搜索该字符串的交叉引用,函数sub_15a00引用了该字符串。该函数主要进行的操作为,先把8个字节的标志字段拷贝到第一个参数指向的内存处,接着往8字节标志字段后面写入8个字节的数据

lua解释器是有源码的,可以结合源码进行分析。这篇博客中提到lua的执行流程是从luaL_loadfile开始的,该函数用来加载lua文件,加载完之后再开始执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...
}
c = getc(lf.f);
if (c == '#') { /* Unix exec. file? */
lf.extraline = 1;
while ((c = getc(lf.f)) != EOF && c != '\n') ; /* skip first line */
if (c == '\n') c = getc(lf.f);
}
// 如果读取的第一个字符和LUA_SIGNATURE的第一个字节相同,表示是一个可执行的文件
if (c == LUA_SIGNATURE[0] && filename) { /* binary file? */
lf.f = freopen(filename, "rb", lf.f); /* reopen in binary mode */
if (lf.f == NULL) return errfile(L, "reopen", fnameindex);
/* skip eventual `#!...' */
while ((c = getc(lf.f)) != EOF && c != LUA_SIGNATURE[0]) ;
lf.extraline = 0;
}
ungetc(c, lf.f);
status = lua_load(L, getF, &lf, lua_tostring(L, -1));
readstatus = ferror(lf.f);
if (filename) fclose(lf.f); /* close file (even in case of errors) */
if (readstatus) {
lua_settop(L, fnameindex); /* ignore results from `lua_load' */
return errfile(L, "read", fnameindex);
}
lua_remove(L, fnameindex);
...

将源码和二进制程序进行对应,即可发现源码中的LUA_SIGNATURE就是我们之前发现的标志字段1B 46 61 74 65 2F 5A 1BLUA_SIGNATURE[0]对应的就是\x1B

通过对uhttpd的逆向分析,发现在加载lua脚本的时候并没有进行解密操作,而是开发人员直接对lua解释器的源码进行了魔改,导致编译得到的结果和正常情况下得到的结果不一样。要弄清楚它修改了哪些地方,需要做的分析工作量太大了,而且即使分析出来(后来逆向师傅分析了一下,它是对lua的解释器进行了修改,他把原有的指令交换了一下顺序,而且自己还增加了一条指令,不知道是干啥用的),得到的反编译后的结果可读性比直接看正常程序汇编代码的可读性更差,在这种情况下很难分析其lua脚本是怎么实现的,也就很难找出来漏洞。所以分析lua脚本这条路基本上行不通!

分析cgi程序

uhttpd将请求分为了三类,其中lua脚本这一类请求行不通,于是尝试继续分析cgi程序这一类请求的处理过程。

为了知道有哪些cgi请求,这里直接访问路由器后台管理界面,通过抓包的方式来获取有哪些cgi请求。把管理员后台的所有接口都抓了一遍后,发现全部都是使用的luci来实现的,没有找到使用cgi实现的地方。

全局搜索包含cgi-bin字符串内容的html等文件,发现这些访问的都是lua接口(包含有/luci):

所以这条路也行不通!

难点

  • 固件采用OpenWRT,http服务集成在busybox中,较难发现问题
  • 固件解包未找到小米自己实现的功能部分在哪里
  • uart调试无法发送数据,获取不了shell
  • 拆开路由器发现其flash等都受到了保护
  • 提取出来固件,发现lua脚本经过了编译,而且和正常编译的文件还不相同
  • 对lua脚本的解释器从源码上进行了修改,逆向分析起来困难
  • 没有找到通过cgi实现的功能部分

其实从cve上面搜索小米路由器得到的漏洞数量来看,就可以知道他们的安全性还是做得很好的。自己体验了一下,能更加体会到这一点。还是要先从安全性做的不那么好的产品开始分析,循序渐进的来。

参考链接

https://future-sec.com/iot-security-hardware-debuging.html

https://paper.seebug.org/506/

https://paper.seebug.org/649/

https://www.lotlabs.com/archives/919

http://www.log4cpp.com/smarthome/9.html

https://blog.csdn.net/junyeer/article/details/46761853

https://www.92aq.com/2016/01/20/openwrt-xiaomi-loudong.html

https://bbs.ichunqiu.com/thread-33974-1-1.html

https://blog.securityevaluators.com/show-mi-the-vulns-exploiting-command-injection-in-mi-router-3-55c6bcb48f09

挂载ubi文件系统:
http://www.gandalf.site/2019/01/iotubi.html