之前想分析awvs12版,但是Windows下有vmp保护,后来发现linux下的没保护但是没时间弄就扔那里了,直到前几天梦神发了我一篇awvs破解的文章,惊奇的发现作者直接跳过了脱壳这最难的一步,原来官方的demo版没加壳(附下载地址Windows和linux)…迷惑行为,既然提起了就继续分析吧
文件说明
awvs从v11开始,不再使用windows+explorer+Jscript+access
,而是使用跨平台+python+nodejs+postgresql
架构,前者脚本直接放在文件系统里且使用twofish+zlib
加密压缩的方式存储,解密方式及相关浅析可查看之前的文章,后者打开安装目录可以明显看出技术架构:
1 2 3 4 5 6 7
| opsrv.exe # 主服务,负责前端与任务下发 xxx.pyd # 编译的python库文件,opsrv用到的python依赖 node && xxx.bin && xxx.node # nodejs运行时及其快照,模块 wvsc # 扫描主程序,为C++编写的内置V8的二进制文件 chromium # 爬虫,由nodejs控制 postgresql && sqlalchemy # 数据库 dir # 其他目录存放的扫描的中间,结果,日志文件
|
提取脚本
首先我最想要的当然还是它的脚本,它以前的版本是js编写的,现在根据上面分析要么python要么js,而又存在wvsc_blob.bin
这个v8特征的快照,感觉新版依然是js编写的,node处理js插件有两种主流方式,要么直接在js外套个壳后直接存储在二进制文件里,要么使用v8编译为机器码再存放,如果是后者就很凉凉,所以先直接在前端下发任务后attach wvsc进程看看内存,搜索if等字符串能明显看到插件的js代码,那么应该是前者!按套路开始静态分析,先ida查看字符串发现:
有10.5插件分析经验就知道这明显是对脚本预处理的模式串,相关函数输入应该就是明文代码了,调试分析该函数发现sub_1400B54B0
的第三个参数是脚本名,第二个参数作为输出参数,将存储脚本代码:
经过分析确认了该函数的作用就是通过脚本名获取明文脚本,且上层会通过它循环获取所有脚本及依赖,所以直接hook该函数即可拿到所有脚本:
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 35 36 37 38 39 40 41 42
| import os
class DumpScriptsHook(DBG_Hooks): root_path = r'C:\Users\betamao\Desktop\scripts'
def dbg_process_start(self, pid, tid, ea, name, base, size): print('{} run in {:x} and the pid is {}'.format(name, base, pid)) self.func_start = base + 0xb54b0 self.func_end = self.func_start + 0x491 add_bpt(self.func_start, 0, BPT_ENABLED | BPT_UPDMEM) add_bpt(self.func_end, 0, BPT_ENABLED | BPT_UPDMEM)
def dbg_suspend_process(self): ev_ea = get_event_ea() if ev_ea == self.func_start: self.current_out = cpu.rdx script_name_str = cpu.r8 script_name_size = get_dword(script_name_str + 8) script_addr = get_qword(script_name_str) self.script_name = get_bytes(script_addr, script_name_size).replace('/', '.') elif ev_ea == self.func_end: addr = get_qword(self.current_out + 8) size = get_dword(self.current_out + 16) path = os.path.join(self.root_path, self.script_name) res = savefile(path, 0, addr, size) if res == 0: print('save file {} failed'.format(self.script_name))
continue_process()
try: if dump_hook: dump_hook.unhook() except: pass finally: dump_hook = DumpScriptsHook() dump_hook.hook() load_and_run_plugin('python', 3)
|
这样可能由于未触发到某分支而未载入全部插件,也可以用frida勾取该函数,利用postgresql里面的插件数据主动dump,不再赘述。
【注:opsrv为python installer打包的,直接解包就能拿到里面的前端调度代码,不多叙述】
脚本分析
插件
从16年到20年awvs的脚本变化还是很大的,首先v10.5版本应该是ES5版本,而v12.0用的应该是ES6,新版的代码看着明显舒服多了!!!从格式上说,旧版是新建一个ScriptContext对象,在其内部执行插件,而新版是直接使用类似模块一样的封装方式得到函数再直接调用的:
1 2 3
| (function (exports, require, module){ module content; }); (function(scriptArg, scanState){ script content; });
|
因此可以直接加载插件:
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 35 36 37 38 39 40
| const SCRIPT_TEMPLATE = '(function(scriptArg, scanState){\n{CODE}\n});'; const sourceCache = {}; const scriptsFuncCache = {};
function readScriptContent(path) { let abPath = path; if (endsWith(path, '.inc')) { abPath = nodePath.join(BASE_DIR, 'Scripts/Includes/', path); } if (sourceCache[abPath]) { return sourceCache[abPath]; } let rawCode = ''; let text = nodeFs.readFileSync(abPath, {encoding: 'utf8'}); let rePat = new RegExp(`(?:/// )?#(include) ['"]?([^'";]+)['"]?;\\s*$`); for (let row of split(text, '\n')) { let reRes = rePat.exec(row); if (reRes !== null) { let includePath = reRes[2]; rawCode += readScriptContent(includePath); } } rawCode += text; sourceCache[abPath] = rawCode; return rawCode; }
function loadScript(path, forceReload?: boolean) { if (scriptsFuncCache[path] && !forceReload) { return scriptsFuncCache[path]; } let rawCode = readScriptContent(path); let code = SCRIPT_TEMPLATE.replace('{CODE}', rawCode); try{ scriptsFuncCache[path] = new vm.Script(code).runInThisContext(); }catch (e) { console.log(e); } return scriptsFuncCache[path]; }
|
如上将插件封装,通过传参的方式执行函数,另外lib目录下的代码会被ax.loadModule
函数加载,仔细观察其实就是使用了require
加载。特别的,可以发现dump出的文件里有个legacy.js
,它算是一个中间代理,之前花了大量时间去分析的类与函数在这里都有(杀了我就现在👨🔫),从逻辑上说我们不关心底层代码是怎样实现的,awvs底层是用C++实现的,我们可以直接搜索functionTemplate
和classTemplate
找到其结构,那么接下来的工作就是构造scriptArg, scanState
这两个参数了。
对比
通过对比发现12.0的Script目录下没有NetWork类的插件,其他目录下插件大多修改的report
部分,部分插件逻辑有小的改动,新增了约70个插件,删除了约15个插件,值得注意的是新增了ESI注入
,Java反序列化
,python反序列化
,Python代码注入
等新型通用检测,另外在其他目录新增了约130个插件,包括主动扫描插件,被动爬虫插件等。
基于nodejs实现运行环境
漏洞扫描模块由框架和扫描插件组成,现在已经有了js编写的插件,只需要再加上框架就行了。存在两种方向:
- 使用熟悉的Python实现,使用js2py将插件转换为python脚本。
- 使用陌生的js实现,不必转换插件。
上面已经提到原版的awvs使用的是v8引擎,类库由legacy.js
提供,其内部使用C++实现,为了快速实现fuzz能力,这里使用nodejs作为运行时,内部使用js实现,其优点如下:
- 统一使用js编写,没有两种高级语言之间的相互转换,代码量少,直观容易维护。
- nodejs类似python拥有大量的三方库,减少开发工作量。
- web fuzz不同于主动扫描部分,它具有目标通用,执行频率高等特点,一次编写后不需要频繁新增插件,所以开发难度相对于poc扫描部分没那么重要,更重要的扫描速度与资源消耗上的优势,采用v8引擎的nodejs在执行性能上直追C,而且node提供丰富的profile性能调优工具,能直接把瓶颈放在IO上。
- 调试测试:nodejs有成熟的单元测试框架(mocha+should),并且利用v8的inspection可以方便的进行调试,这能加快排错速度,并且利用简单的单元测试可以减少协作导致的改动引发BUG。
- 可以直接将js编译为机器码或wasm便于代码保护。
实现细节
工作内容:
- 实现legacy涉及的底层库,并对每一个功能编写单元测试。
- 增加调度功能。
结尾
我认为插件是核心,有了插件是能自己实现一套扫描器的,即框架部分,在漏扫模块awvs是使用自己内嵌v8来实现运行时的,这样无疑更加高效,不过对于我们这种玩玩闹闹其实用nodejs就好了,因为它提供丰富的库呀~