使用 WPAD/PAC 和 JScript 在 win11 中远程执行代码1
开发
了解 JScript VAR 和字符串
由于在这篇博文的其余部分中,我们将大量讨论 JScript VAR 和字符串,因此在深入了解这些漏洞的工作原理之前先描述这些内容是很有用的。
JScript VAR 是一个 24 字节(在 64 位版本上)结构,它表示一个 JavaScript 变量,并且本质上与此 MSDN 文章中描述的 VARIANT 数据结构相同。在大多数情况下(足以跟踪漏洞利用),它的内存布局如下所示:
抵消 |
尺寸 |
描述 |
---|---|---|
0 |
2 |
变量类型,3 表示整数,5 表示双精度,8 表示字符串等。 |
8 |
8 |
根据类型,立即数或指针 |
16 |
8 |
大多数类型未使用 |
例如,我们可以用 VAR 表示一个双精度数,在前 2 个字节中写入 5(表示双精度类型),后跟偏移 8 处的实际双精度值。最后 8 个字节将不使用,但它们如果从该 VAR 复制另一个 VAR 的值,则将被复制。
JScript 字符串是类型为 8 的 VAR 类型和偏移量 8 处的指针。指针指向此处(https://msdn.microsoft.com/en-us/library/windows/desktop/ms221069(v=vs.85%29.aspx)描述的 BSTR 结构。在 64 位版本上,BSTR 布局如下所示:
抵消 |
尺寸 |
描述 |
---|---|---|
0 |
4 |
没用过 |
4 |
4 |
以字节为单位的字符串长度,不包括最后的空字符 |
8 |
长度+2 |
字符串字符(16 位)后跟一个空字符 |
String VAR 直接指向字符数组,这意味着,要获得 String 的长度,需要将指针减 4 并从那里读取长度。请注意,BSTR 由 OleAut32.dll 处理并分配在单独的堆上(即与用于其他 JScript 对象的堆不同)。
释放 BSTR 也与大多数对象不同,因为在调用 SysFreeString 时,它不是直接释放 BSTR,而是首先将字符串放入由 OleAut32.dll 控制的缓存中。这个机制在 JavaScript 中的堆风水中有详细描述。
第 1 阶段:信息泄漏
infoleak 的目的是获取我们完全控制其内容的内存中字符串的地址。在这一点上,我们不会泄露任何可执行模块地址,这将在稍后发布。相反,我们的目标是击败高熵堆随机化,并使漏洞利用的第二阶段可靠,而无需使用堆喷射。
对于信息泄漏,我们将在 RegExp.lastParen 中使用这个错误。要了解这个错误,让我们首先仔细看看 jscript!RegExpFncObj 的内存布局,它对应于 JScript RegExp 对象。在偏移量 0xAC RegExpFncObj 包含 20 个整数的缓冲区。实际上这些是 10 对整数:对的第一个元素是输入字符串的开始索引,第二个元素是结束索引。每当带有 RegExp 参数的 RegExp.test、RegExp.exec 或 String.search 遇到捕获组(RegExp 语法中的括号)时,匹配的开始和结束索引都存储在这里。显然,缓冲区中只有 10 个匹配项的空间,因此只有前 10 个匹配项存储在此缓冲区中。
但是,如果 RegExp.lastParen 被调用并且有超过 10 个捕获组,RegExpFncObj::LastParen 会很乐意使用捕获组的数量作为缓冲区的索引,从而导致越界读取。这是一个 PoC:
var r= new RegExp(Array(100).join('()'));
''.search(r);
警报(RegExp.lastParen);
2 个索引(我们称它们为start_index和end_index )在缓冲区边界之外读取,因此可以任意大。假设第一次越界访问不会导致崩溃,如果这些索引中的值大于输入字符串的长度,那么将发生第二次越界访问,这允许我们读取a 在输入字符串的范围之外。像这样越界读取的字符串内容将在一个可以检查的字符串变量中返回给调用者。
我们将要使用第二次越界读取,但首先我们需要弄清楚如何将受控数据放入start_index和end_index 。好在看RegExpFncObj的布局,在索引缓冲区结束后还有我们控制的数据:RegExp.input值。通过将 RegExp.input 设置为整数值并使用由 41 组空括号组成的 RegExp,当 RegExp.lastParen 被调用时,start_index将为 0,而end_index将是我们写入 RegExp.input 的任何值。
如果我们让一个输入字符串与一个被释放的字符串相邻,那么通过读取输入字符串的边界,我们可以获得堆元数据,例如指向其他空闲堆段的指针(红黑中的Left,Right和Parent节点堆块树,请参阅Windows 10 段堆内部了解更多信息)。图 1 显示了信息泄漏时的相关对象。
图 1:堆信息泄漏布局
我们使用 20000 字节长的字符串作为输入,以便它们不会被分配到低碎片堆上(LFH 只能用于 16K 字节或更小的分配),因为 LFH 的堆元数据不同并且不包括Windows 10 段堆中的有用指针。此外,LFH 引入了随机性,这会影响我们将输入字符串放置在已释放字符串旁边的能力。
通过从返回的字符串中读取堆元数据,我们可以获得一个已释放字符串的地址。然后,如果我们分配一个与释放的字符串大小相同的字符串,它可能会被放置在这个地址,我们就实现了我们的目标,即我们知道我们控制其内容的字符串的内存地址。
整个信息泄露过程如下所示:
- 分配 1000 个 10000 个字符的字符串(注意:10000 个字符 == 20000 个字节)。
- 每隔一秒免费一次。
- 触发信息泄漏错误。使用剩余的字符串之一作为输入字符串并读取 20080 个字节。
- 分析泄漏的字符串并获取指向已释放字符串之一的指针。
- 使用特制内容分配 500 个与已释放字符串(10000 个字符)长度相同的字符串。
特制琴弦的内容现阶段不重要,但在下一阶段会很重要,所以会在此进行说明。另请注意,通过检查堆元数据,我们可以轻松确定进程正在使用哪个堆实现(段堆与 NT 堆)。
图像 2 和 3 显示了在信息泄漏前后使用堆历史查看器创建的堆可视化。绿色条纹代表分配的块(被字符串占用),灰色条纹代表分配的块,然后被稍后再次分配的释放(我们释放并在触发信息泄漏错误后重新分配的stings),白色条纹代表从未分配的数据(守卫页)。您可以看到随着时间的流逝如何分配字符串,然后释放其中一半(灰色),稍后再次分配(条纹变为绿色)。
我们可以看到,每 3 次这样大小的分配后都会有保护页。我们的漏洞利用永远不会真正触及任何这些保护页面(它读取的数据太少超出了字符串的末尾),但在 1/3 的情况下,在输入字符串之后不会有空闲字符串infoleak,因此预期的堆元数据将丢失。然而,我们可以很容易地检测到这种情况,或者使用另一个输入字符串触发 infoleak 错误,或者静默中止漏洞利用(注意:到目前为止,我们没有触发任何内存损坏)。
图 2:堆图:显示堆随时间的演变
图 3:泄露指向字符串的指针的分步说明。
第 2 阶段:溢出
在漏洞利用的第 2 阶段,我们将使用这个堆溢出漏洞在 Array.sort 中。如果 Array.sort 的输入数组中的元素数大于 Array.length / 2,JsArrayStringHeapSort(如果未指定比较函数则由 Array.sort 调用)将分配一个相同大小的临时缓冲区作为当前数组中的元素数(注意:可以小于array.lenght)。然后它将尝试检索从 0 到 Array.length 的每个数组索引的相应元素,如果该元素存在,则将其添加到缓冲区并转换为字符串。如果数组在 JsArrayStringHeapSort 的生命周期内没有改变,这将正常工作。但是,JsArrayStringHeapSort 将数组元素转换为可以触发 toString() 回调的字符串。如果在其中一个 toString() 回调中元素被添加到之前未定义的数组中,
为了更好地理解这个错误及其可利用性,让我们仔细看看我们将溢出的缓冲区的结构。已经提到该数组将具有与当前输入数组中的元素数相同的大小(准确地说,它将是元素数 + 1)。数组的每个元素的大小将是 48 字节(在 64 位构建中),具有以下结构:
抵消 |
尺寸 |
描述 |
---|---|---|
0 |
8 |
将偏移量 16 处的原始 VAR 转换为字符串后指向字符串 VAR 的指针 |
8 |
4 |
当前元素的索引 (int) |
16 |
24 |
VAR 保存原始数组元素 |
40 |
4 |
int 0 或 1 取决于偏移 16 处的 VAR 类型 |
在 JsArrayStringHeapSort 期间,检索索引 < array.length 的数组的每个元素,如果定义了该元素,则会发生以下情况:
- 数组元素在偏移量 16 处读入 VAR
- 原始的 VAR 被转换为字符串 VAR。指向字符串 VAR 的指针被写入偏移量 0。
- 在偏移量 8 处,写入数组中当前元素的索引
- 根据原始 VAR 类型,在偏移量 40 处写入 0 或 1
看临时缓冲区的结构,很多我们并没有直接控制。如果数组成员是一个字符串,那么在偏移量 0 和 24 处我们将有一个指针,当取消引用时,在偏移量 8 处包含另一个指向我们控制的数据的指针。然而,这比在大多数情况下对我们有用的间接级别要大一级。
但是,如果数组的成员是双精度数,那么在偏移量 24(对应于原始 VAR 的偏移量 8)处,该数字的值将被写入,并且它直接在我们的控制之下。如果我们创建一个与在阶段 1 中获得的指针具有相同双精度表示的数字,那么我们可以使用溢出来用指向我们直接控制的内存的指针覆盖缓冲区结束后某处的指针。
现在问题变成了,我们可以用这种方式覆盖什么来推进漏洞利用。如果我们仔细研究对象在 JScript 中是如何工作的,那么其中一个可能的答案就会出现。
每个对象(更具体地说,一个 NameList JScript 对象)都有一个指向哈希表的指针。这个哈希表只是一个指针数组。当访问 Object 的成员元素时,将计算元素名称的哈希值。然后,取消引用对应于哈希最低位的偏移量的指针。这个指针指向一个对象元素的链表,并且遍历这个链表,直到我们到达一个与请求元素同名的元素。如图 4 所示。
图 4:JScript 对象元素内部
推荐阅读
-
使用 WPAD/PAC 和 JScript 在 win11 中远程执行代码
-
使用 WPAD/PAC 和 JScript 在 win11 中远程执行代码1
-
利用 WPAD/PAC 和 JScript 在 Windows 10 中远程执行代码
-
使用 WPAD/PAC 和 JScript3 在 win11 中远程执行代码
-
Android 开发中 nodpi、xhdpi、hdpi、mdpi、ldpi 的概念 - 术语和概念 屏幕尺寸 屏幕的物理尺寸,基于屏幕的对角线长度(如 2.8 英寸、3.5 英寸)。 简而言之,安卓系统将所有屏幕尺寸简化为三大类:大、普通和小。 程序可以为这三种屏幕尺寸提供三种不同的布局选项,然后系统会以合适的方式将布局选项呈现到相应的屏幕上,这个过程不需要程序员用代码进行干预。 屏幕纵横比 屏幕的物理长度与物理宽度之比。程序只需使用系统提供的资源分类器 long(长)和 notlong(不长),就能为具有特定长宽比的屏幕提供配制材料。 分辨率 屏幕的像素总数。请注意,分辨率并不意味着长宽比,尽管在大多数情况下,分辨率表示为 "宽度 x 长度"。在安卓系统中,程序一般不直接处理分辨率。 密度 根据屏幕分辨率,沿屏幕宽度和长度排列的像素数量。 密度较低的屏幕在长度和宽度方向上的像素都相对较少,而密度较高的屏幕通常会在同一区域内排列很多甚至非常非常多的像素。屏幕的密度非常重要;例如,一个界面元素(如按钮)的长度和宽度以像素为单位,在低密度屏幕上会显得很大,但在高密度屏幕上就会显得很小。 独立于密度的像素(DIP)是指程序用来定义界面元素的抽象意义上的像素。它作为一个与实际密度无关的单位,帮助程序员构建布局方案(界面元素的宽度、高度和位置)。 与密度无关的像素在逻辑上与像素密度为 160 DPI 的屏幕上的像素大小相同,而 160 DPI 是安卓平台默认的显示设备。在运行时,平台会以目标屏幕的密度为基准,"透明 "地处理所有所需的 DIP 缩放操作。要将与密度无关的像素转换为屏幕像素,可以使用一个简单的公式:像素 = DIP * (密度 / 160)。例如,在 240 DPI 的屏幕上,1 个 DIP 等于 1.5 个物理像素。强烈建议使用 DIP 来定义程序界面的布局,因为这样可以确保用户界面在所有分辨率的屏幕上都能正常显示。 为了简化程序员在面对各种分辨率时的麻烦,也为了让各种分辨率的平台都能直接运行这些程序,Android 平台将所有屏幕以密度和分辨率作为分类方式,分别分为三类:- 三大尺寸:大、普通、小;- 三种不同密度:高(hdpi)、中(mdpi)和低(ldpi)。DPI 表示 "每英寸点数",即每英寸的像素数。如果需要,程序可以为不同的屏幕尺寸提供不同的资源(主要是布局),为不同的屏幕密度提供不同的资源(主要是位图)。除此之外,程序无需对屏幕尺寸或密度进行任何额外处理。执行时,平台会根据屏幕本身的尺寸和密度特性自动加载相应的资源,并将其从逻辑像素(DIP,用于定义界面布局)转换为屏幕上的物理像素。
-
Java 类加载器的作用 - 简介:类加载器是 Java™ 中一个非常重要的概念。类加载器负责将 Java 类的字节码加载到 Java 虚拟机中。本文首先详细介绍了 Java 类加载器的基本概念,包括代理模型、加载类的具体过程和线程上下文类加载器等。然后介绍了如何开发自己的类加载器,最后介绍了类加载器在 Web 容器和 OSGi™ 中的应用。 类加载器是 Java 语言的一项创新,也是 Java 语言广受欢迎的重要原因之一。它允许将 Java 类动态加载到 Java 虚拟机中并执行。类加载器从 JDK 1.0 开始出现,最初是为了满足 Java Applets 的需求而开发的,Java Applets 需要从远程位置下载 Java 类文件并在浏览器中执行。现在,类加载器已广泛应用于网络容器和 OSGi。一般来说,Java 应用程序的开发人员不需要直接与类加载器交互;Java 虚拟机的默认行为足以应对大多数情况。但是,如果遇到需要与类加载器交互的情况,而您又不太了解类加载器的机制,就很容易花费大量时间调试异常,如 ClassNotFoundException 和 NoClassDefFoundError。本文将详细介绍 Java 的类加载器,帮助读者深入理解 Java 语言中的这一重要概念。下面先介绍一些基本概念。 类加载器的基本概念 顾名思义,类加载器用于将 Java 类加载到 Java 虚拟机中。一般来说,Java 虚拟机以如下方式使用 Java 类:Java 源程序(.java 文件)经 Java 编译器编译后转换为 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码并将其转换为 java.lang 实例。每个实例都用来表示一个 Java 类。通过该实例的 newInstance 方法创建该类的对象。实际情况可能更加复杂,例如,Java 字节代码可能是由工具动态生成或通过网络下载的。 基本上,所有类加载器都是 java.lang.ClassLoader 类的实例。下面将详细介绍这个 Java 类。 java.lang.ClassLoader 类简介 java.lang.ClassLoader 类的基本职责是根据给定类的名称为其查找或生成相应的字节码,然后根据这些字节码定义一个 Java 类,即 java.lang.Class 类的实例。除此之外,ClassLoader 还负责加载 Java 应用程序所需的资源,如图像文件和配置文件。不过,本文只讨论它加载类的功能。为了履行加载类的职责,ClassLoader 提供了许多方法,其中比较重要的方法如表 1 所示。下文将详细介绍这些方法。 表 1.与加载类相关的 ClassLoader 方法
-
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中的