awvs v12.0 插件分析

之前想分析awvs12版,但是Windows下有vmp保护,后来发现linux下的没保护但是没时间弄就扔那里了,直到前几天梦神发了我一篇awvs破解的文章,惊奇的发现作者直接跳过了脱壳这最难的一步,原来官方的demo版没加壳(附下载地址Windowslinux)…迷惑行为,既然提起了就继续分析吧

文件说明

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
# coding=utf-8
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; });
/// #include constants.inc;

因此可以直接加载插件:

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++实现的,我们可以直接搜索functionTemplateclassTemplate找到其结构,那么接下来的工作就是构造scriptArg, scanState这两个参数了。

对比

通过对比发现12.0的Script目录下没有NetWork类的插件,其他目录下插件大多修改的report部分,部分插件逻辑有小的改动,新增了约70个插件,删除了约15个插件,值得注意的是新增了ESI注入Java反序列化python反序列化Python代码注入等新型通用检测,另外在其他目录新增了约130个插件,包括主动扫描插件,被动爬虫插件等。

基于nodejs实现运行环境

漏洞扫描模块由框架和扫描插件组成,现在已经有了js编写的插件,只需要再加上框架就行了。存在两种方向:

  1. 使用熟悉的Python实现,使用js2py将插件转换为python脚本。
  2. 使用陌生的js实现,不必转换插件。

上面已经提到原版的awvs使用的是v8引擎,类库由legacy.js提供,其内部使用C++实现,为了快速实现fuzz能力,这里使用nodejs作为运行时,内部使用js实现,其优点如下:

  1. 统一使用js编写,没有两种高级语言之间的相互转换,代码量少,直观容易维护。
  2. nodejs类似python拥有大量的三方库,减少开发工作量。
  3. web fuzz不同于主动扫描部分,它具有目标通用,执行频率高等特点,一次编写后不需要频繁新增插件,所以开发难度相对于poc扫描部分没那么重要,更重要的扫描速度与资源消耗上的优势,采用v8引擎的nodejs在执行性能上直追C,而且node提供丰富的profile性能调优工具,能直接把瓶颈放在IO上。
  4. 调试测试:nodejs有成熟的单元测试框架(mocha+should),并且利用v8的inspection可以方便的进行调试,这能加快排错速度,并且利用简单的单元测试可以减少协作导致的改动引发BUG。
  5. 可以直接将js编译为机器码或wasm便于代码保护。

实现细节

工作内容:

  1. 实现legacy涉及的底层库,并对每一个功能编写单元测试。
  2. 增加调度功能。

结尾

我认为插件是核心,有了插件是能自己实现一套扫描器的,即框架部分,在漏扫模块awvs是使用自己内嵌v8来实现运行时的,这样无疑更加高效,不过对于我们这种玩玩闹闹其实用nodejs就好了,因为它提供丰富的库呀~