三年前分析的一个漏洞,最近又温习一遍,这个flash中混淆漏洞的鼻祖,10年最经典的漏洞。
漏洞触发原因
该漏洞主要因为avm对返回的类没有进行校验,通过修改swf文件,实现Ref类和Origin类的混淆。
Poc如下,可以看到poc一共由三个类组成:
PoC_Main
Original_Class
Real_Ref_Class
PoC_Main类中首先初始化Real_Ref_Class类,之后调用Original_Class的static_func1方法,该方法会返回一个Original_Class对象,并通过该对象调用方法normal_func1()
Original_Class中包含两个方法,static_func1,返回一个Original_Class对象,normal_func1,不做任何操作
Real_Ref_Class类中包含一个函数func1,该函数返回一个uint类型的变量。
漏洞实际上上将对类Origin_Class的操作混淆成对Real_Ref_Class类的操作,为了达到这个效果,需要修改编译好的swf文件。
将上图中的070102修改成070103,实际上就是修改了avm运行时需要使用到的编号。
运行样本之后,flash崩溃,windbg断下结果如下,可以看到在mov操作的时,eax中的值为41414141,该地址非法,从而导致崩溃,而41414141正是我们可控的数据,崩溃不远处正好有call eax可以导致程序执行,而导致漏洞触发的地址位于寄存器eax中,通过反编译窗口可以发现eax来自于04c1ff9a地址的call eax这个函数。
直接在返回地址下断,重新加载运行,连续断下五次之后,进入对应的call eax,在函数快返回时,调用指令call edi,该函数最终会返回41414141
如下图所示:
实际上此处下断的函数call eax即为Poc_Main中语句var obj:Original_Class = Original_Class.static_func1()对应的jit代码,但此时由于swf的修改导致Original_Class.static_func1()这句代码中的Original_Class变成了Real_Ref_Class。
通过修改可以导致Main在实际的调用过程中将Real_Ref_Class误以为Original_Class,由于avm虚拟机的工作机制,导致main中的处理都是基于Original_Class类生成的,故而Original_Class中函数的返回类型决定了Main可以接受什么样的类型,如Original_class中的函数A返回uint,Main就必须接受uint的类型,但是此时如果Real_Ref_Class中的对应A返回为一个String的话,Main就会将这个String当做uint对象处理。
泄露基地址
修改poc将var obj:Original_Class = Original_Class.static_image();修改为
var obj:uint = Original_Class.static_image();
由于main中的接受的类型变成了uint,因此Original_Class对应返回的类型也要修改成uint,因为Original_Classs虽然被替换了,但是其决定了对应的调用返回的类型。
此时在Real_Ref_Class中修改代码,将string换成一个ByteArray对象。
这样的结果就是在Main函数中我们通过漏洞将一个ByteArray的对象当做uint对象来处理,正常情况下这样的做法在编码上是无法通过的,在Main中直接调用uint的方法toString即可获取ByteArray的地址,如下图所示:
此时我们获取了该ByteArray的地址,通过该地址我们可以尝试获取flash player的基地址,从而绕过aslr,下图即为生成的ByteArray在内存中的对象在Bytearray+10+10的位置即为对应的内容,如此处的41414141,为我们ByteArray中的内容。
而01eb4f40处的值00572310即为对应的虚函数,该地址在flash player中的偏移是固定的,获取该值,通过简单的数学运算即可获取对应的基址,此处需要涉及到一个问题,如何读取01eb4f40中的虚函数值。
这就需要用到ascript中的Number类,通过new Number(address)即可进行读取,但是对于Number来说只接受整形对象的参数,此时就需要再次出发该漏洞将我们的uint类型混淆成整形对象(前面获取ByteArray的时候实际是将ByteArray混淆成了uint)
对于actionscript中的所有变量都是一个32位的对象,其中末四位决定了该对象的类型,如下图所示。
如此处我们生成的ByteArray第地址就是01eb4f41,最后一个为001,即一个object类型,而我们的目标是将其混淆为110类型。
如下图所示首先触发漏洞获取ByteArray对象,将该地址与&0xFFFFFFF8做预算,将末尾的类型标志去掉,由于此时为uint,通过toString函数将其装换成String对象,再次出发漏洞,将其混淆成整形对象,之后通过Number读取对应的值。
Real_Ref_Class中定义函数static_strToint,该函数接受一个String类型的参数,并即将其的做|0x000000007操作,目的是转换成以110为结尾的整形对象。
在Origin_Class中定义对应的伪函数,我们知道该类中的函数返回值决定了Real_Ref_Class中的真实类型,由于此处希望按Real_Ref_Class中static_strToint定义的整形返回,因此此处在Origin_Class中不定义返回的类型 ,这样Main就默认接受调用函数返回的类型。
运行之后,泄露出的地址如下。
这样通过读取ByteArray的虚函数地址,即可获取对应的flash player基址,通过读取ByteArray+10的地址,即可获取对应的shellcode的地址(将shellcode部署到ByteArray中)
在Main中,获取bytearray中的shellcode地址。
整个ByteArray的内存详细结构如下。
最后泄露出的flash player基址,shellcode地址如下。
控制eip
漏洞触发时会eax+0x48指向的指针
修改代码,在指向bytearray内容的指针前填充64长度的字符,这样触发时即可获取eip的执行权。
修改main中的代码,假设需要执行的shellcode的指针为eip,
执行之后如下,获取对应的地址call [41424344]。
构建dep
此时通过漏洞获取了flash的基址,获取了可执行权限,获取shellcode的地址,通过获取的基址可以构建rop链来绕过dep,这样每次利用运行时获取对应的基址,之后更新rop链的地址即可绕过aslr的限制,理论上可以直接通过mona生成dep链,但是这个地方有一个需要注意的地方就是,和一般的栈溢出不同,我们的dep链在bytearray中,而bytearray对象在堆上,这就导致rop链中gaaget模块连接起来的重要引出esp不存在了,本质上rop就是利用ret指令会直接将esp中的内容放到eip执行来使整个rop链运行起来,如下图所示:获取eip时esp为001be2fc,而实际可控的bytearray的内容在0247f000中,为此需要调整esp,使其指向rop的地址,为rop链制造可运行的“堆栈”环境。
此时一般最直接的方式其实是xchg指令,直接通过该指令交换esp,和ecx的值(触发时ecx正好指向我们可控的bytearray中shellcode的地址),通过mona进行指令的搜索,发现12条指令,但是并不是所有指令的可以使用这个地方的指令其实是有一个要求的,即交换之后esp需要进行加操作,因为本身xchg指令是有长度的。
经过挑选之后选择的交换gatage为以下,可以看到此处有一个add esp,8的操作,注意此处我们的gatage并不会运行到ret,而是会在add esp,8之后进行一个jmp操作,可能会问,为什么不找直接ret的gatage,没办法,这条是唯一可行的了。
由于此处是jmp,我们需要到00d99460的地方去看看是否有ret,运气不错,虽然jmp了一下,但是00d99460位置只有三句指令,这个地方也需要注意,eax来自esp+14的地址,即我们可控的位置,在00d99460的指令序列中有一个and [eax+24],0EFFFFFFF操作,这里需要保证[eax+24]这出的地址可写可读,写了个脚本跑了下flash的地址空间,发现没有类型的地址
因此此处直接选了一个第地址的50f78,可以看到改地址+24的地方的内存属性可读可写。
这样重新调整过esp的值之后就可以构造rop链了,首先搜取指令用于对virtualprotect函数参数进行布置,搜索的指令如下。
为什么按这样的指令序列进行搜索,直接来看看flash中virtualprotect函数调用,即可发现原因了。
整个rop链如下:
在Real_Ref_Class中定义函数ropfordep,该函数接受flash player的基址和shellcode,之后按上述的结构将rop中的地址更新。
同样编写shellcode函数,该函数保存对应的calc的shellcode,此处注意编码,ByteArray是小端机的排序,所以和内存中的shellcode是相反的,当然你也可以把这个地方的ByteArray在返回的时候转成String,这样的的话就和内存中一致了。
Real_Rel_Class中对应的伪函数。
调试运行,进入rop链中
此时esp设置成功。
堆栈调整之后,esp执行堆上对应的ByteArray中的rop链
参数布置
此时shellcode只有读写权。
调用virtualprotect,将shellcode的地址设置为可执行。
运行之后,shellcode可执行
Calc运行,熟悉的计算器。
转载请注明出处