我被攻击了谷歌应用引擎:java字节码的解剖漏洞利用

2021-05-07 00:37:59

回到大学,我对Java字节码非常感兴趣。当我在2013年在谷歌进行实习时,我对谷歌应用引擎的Java版本的安全性持怀疑态度,并获得了我实习的最后一周进行了迷你红色团队锻炼,试图闯入App Engine。这是我如何找到漏洞的故事,并开发出漏洞突破应用引擎沙箱,并在Google服务器上获得任意代码执行。

我持怀疑态度的原因之一是Java的安全跟踪记录。 Java在尝试使用其applet模型进行过程中的编程语言中是不寻常的,其中,可信和不受信任的代码在同一语言运行时运行。

在JavaScript和Webassembly接管世界之前,在世界上接管了世界的网站作者,即想要包括非体现交互性的网站作者必须依赖浏览器插件。 Sun进入FRAY是Java Applet,一个系统允许网站作者在其网站上包含预编译的Java Classfiles。当用户视图嵌入页面时,浏览器将该代码发送到安装在用户计算机上的Java虚拟机(JVM)以进行执行。

为了保持安全,Java使用权限系统来控制运行代码可能而且无法做到的。默认情况下,使用所有权限执行桌面应用程序,而Java applet使用非常有限的策略,以防止访问用户本地文件的内容。

不幸的是,小猫仍然困扰着安全漏洞。一个问题是,大多数Java运行时库本身都在Java中实现。受信任和不受信任的代码在同一VM中并排运行,唯一的是将它们分离为权限系统和可见性修饰符(公共,受保护,私人等)

这意味着JVM或标准库中的任何位置都有易于成为安全漏洞。此外,攻击表面很大。 Java 7运行时包括超过17,000多个类,很多地方才能蠕动。

但是,App Engine的作业实际上在大多数方面都比Java applet的工作更难。与applet类似,不受信任的java字节码正在沙箱中执行,只是在谷歌的服务器上而不是用户的计算机上。但是,App Engine也希望让程序员轻松制作内容,这意味着他们将它们与桌面应用的方式相同,以尽可能最大。这意味着App Enable允许访问Applet的所有危险API,以出于安全原因禁止小程序的所有危险API。

App Engine与applet相比它的一件事是用户使用的API是白名单,因此默认情况下,您无法在运行时库中调用所有17,000个类,这大大减少了攻击表面。然而,白名单包括所有有趣的核心API,如反射和类加载器,使安全困难。

为了解决这个问题,App引擎使用静态字节码重写和动态包装器功能的组合。 API分为三组以供安全目的。一些apis,如java / lang / string是无害的,始终安全地呼叫。其他人总是被禁止的,就像在运行时中的随机17,000级,从未意图由最终用户使用。但是,最有趣的是第三种类型 - API,就像反射一样,其安全取决于它们的方式。

Java Reflection API允许您动态调用在运行时指定的名称和类型的函数。这使得静态分析是不可能的,因为没有办法判断实际上被调用的函数只是看代码。但是,App Engine无法只是禁止反思,因为它在合法的Java库中普遍存在。

解决方案是在执行之前透明地重写所有提交的代码。字节码重写传递通过App引擎团队编写的包装功能静态替换对反射API的所有呼叫。这些包装器功能将检查运行时调用哪些函数,允许它,阻止它,或酌情返回另一个包装器。在实现中存在很多复杂性,因为它需要对用户透明并且如果例如,仍然可以工作。反射API本身通过反射执行,但我认为它是正确完成的。

同样,ClassLoader API允许您在运行时定义新类,通过使用要加载的新类的代码传递一组字节。这是一个粗略的Java等同于Python或JavaScript等语言的臭名昭着的eval功能。同样,ClassLoaders有合法的用途,但他们也完全击败了静态分析。因此,字节码重写替换所有自定义类加载器,其中包装函数在加载之前在运行时执行在运行时在运行时进行重写步骤。

为了适当地插入这些包装器,App引擎使用流行的开源字节码操作库ASM在实际执行它之前转换用户的代码。 (字节码重写也是如此之类的事情,如确保适当处理请求超时,但这是我们目的重要的安全包装。)

注意:ASM是一个开源库,可以解析和重新序列化Java类文件,并提供用于分析和更改字节码的API,然后再次写入。实际的字节码重写逻辑是应用引擎团队的封闭来源和自定义。但是,要简化事物,我有时会将App Engine的字节码重写通行证称为“ASM”,即使该代码实际上不是ASM库的一部分。

1)用户将二进制类文件上传到App Engine Server2)ClassFiles由ASM - &gt解析; (代码内存表示的ASM'秒)3)使用ASM-&GT的各种代码转换; (ASM' Sminitized Code的内存表示)4)ASM将代码写回二进制C类文件5)在JVM-&gt上执行ASM输出的二进制文件; (JVM'代码的内存表示中)

如上所述,API包装非常复杂。但是,我决定只是假设它都是正确的实现,而且因为我懒惰,更感兴趣地使用我对低级Java字节码的了解而感到困难,而不是打扰那里的虫子。此外,实际包装代码坐在Google的服务器上,因此如果在那里有漏洞,则真正的攻击者只能通过试验和错误找到它们。

注意:此假设结果不完全正确。一年一或两个以后,App引擎从发现一个危险的API方法的外部安全研究员收到了漏洞报告,他们忘了正确包装。

相反,我决定在开源ASM库中寻找错误(更现实),以便完全绕过字节码重写。关键是上述流程中的步骤3和5之间的间隙。

假设正确地实现了字节码重写,有一种不变的是,步骤3的输出总是安全的。但是,步骤3的输出不直接执行。相反,ASM将Sminitized代码的内存内存表示到二进制类文件,然后将二进制文件传递给重新解析并执行它的JVM。这意味着ASM认为它是如何向文件写出的任何区别以及JVM如何解析,这意味着我们可能会绕过字节码转换,从而绕过安全检查。

因此,我审查了开源ASM序列化代码来查找错误。然而,我对漏洞的第一个想法不是在ASM源中发现的东西,而是没有在源头中的东西。

Java有一个强大的向后兼容性故事。您可以在第一个版本的Java中编译类文件,并在今天的JVM上运行它们,通常仍然工作。事实上,即使没有重新编译也可以自由互操作,即使没有重新编译,你可以写入古代二进制文件的代码,并将其呼叫回复等。

为了使Java ClassFile格式向后兼容,每个类文件都以一个版本字段始于告诉JVM如何解析类文件的其余部分。粗略地对应于Java版本的版本号,除了它们莫名其妙地启动了45的编号,并且具有早期版本的一些奇怪性。 Java 1.02(Java的第一个稳定版本)使用了字节码版本45.3,Java 5为版本49.0,Java 6为50.0,Java 7为51.0等。

虽然第一个稳定版本的Java使用了字节码版本45.3,但JVM实际上将接受ClassFiles,版本从45.0开始。此外,JVM中有一个未记录的功能,当版本为45.0 - 45.2时,它在JVM中有一个未记录的特征,在那里,当版本为45.0 - 45.2时,它会略微不同地分析代码属性。

由于此功能完全没有记录(我只发现它偶然发现它,看着JVM源代码)​​,因此处理它的唯一字节码工具是我写的。其他所有对待预约45.3类的类文件,与正常(45.3+)类文件相同。即使是Oracle自己的javap工具也没有处理此操作,所以它不成熟,既不是ASM。

在普通类文件中,代码属性的堆栈,当地人和代码长度字段分别具有2,2和4个字节的长度,而是在一个45.3 pre-45.3类中,JVM希望它们为1,1和2代替字节。通常情况下,这意味着ASM产生的45.3类ClassFile在JVM上运行时,asm将崩溃,因为JVM在解析并拒绝时遇到垃圾数据。

但是,如果您非常小心,则可以构建与2,2,4宽度解析的类有效的类文件,并且在用1,1,2解析时也有效,但在每种情况下被解析为不同的代码。这意味着可以在实际JVM上运行时,可以制作执行一条代码的类文件,并在用逆向工程工具查看时显示完全不同的虚假代码,如下所示。

注意:此功能稍后在2019年10月删除。似乎有人提交了一个错误报告,要求javap添加对45.3 pre-45.3类文件的支持,而是他们决定完全从JVM中删除功能。

不幸的是,这个问题在App Enginn中没有利用,因为字节码重写恰好将版本设置为至少49.0。这不是出于安全原因所做的,但由于重写有时添加了使用版本49.0功能的代码,因此他们决定无条件将ClassFile版本更新到最少49.0。幸运的是,它没有长时间找到另一个,甚至在ASM中更简单的错误。

每当Java字节码需要引用字符串时,字符串数据都被存储为两个字节长度字段,然后是字符串数据的多个字节。但是,asm在写出长度字段时不做任何溢出检查(或者在2013年未备份)。因此,如果询问ASM要写出来,例如,一个65536字节字符串,而不是抛出错误,它将只是默默地溢出并将0写出来,后跟那些65536字节的字符串数据。然后,当JVM解析ClassFile时,它将看到一个0个长度字符串,然后继续打开它,以解析它在下一步中的任何一个要解析的内容,从字符串数据的那些65536字节开始。

这是一个非常有希望的虫子,但它并不足以利用自己。问题是,我们没有任何方法可以直接生产这样的超大字符串。回想一下,流程如下:

1)用户将二进制类文件上传到App Engine Server2)ClassFiles由ASM - &gt解析; (代码内存表示的ASM'秒)3)使用ASM-&GT的各种代码转换; (ASM' Sminitized Code的内存表示)4)ASM将代码写回二进制C类文件5)在JVM-&gt上执行ASM输出的二进制文件; (JVM'代码的内存表示中)

我们想要以某种方式确保在我们进入步骤4时确保在Sunitized代码的内存表示中有一个长字符串。但是,我们没有任何直接控制输入到步骤4.我们直接控制的唯一的东西是步骤1,我们上传到服务器的文件。

由于我们在步骤1中发送到App Engine的输入是一组二进制类文件,而ClassFiles不能表示超过65535个字节长的字符串,这意味着步骤2和3中的所有字符串是< = 65535字节,并且实际上没有办法在步骤4中实际触发溢出错误呢?

诀窍是我们上传的文件永远不会被JVM所看到的。这意味着它们不一定是严格有效的类文件。我们可以上传我们喜欢的任何文件,只要它看起来就像ASM的类文件来解析它一样。导致在ASM字符串处理中发现的第二个错误。

在ClassFile格式中,常量字符串数据存储在Mutf-8编码中。这与普遍存在的UTF-8编码相同,具有两个微小的差异。第一个(星形字符被存储为代理对)对我们的目的无关紧要。但是,第二个区别在于,在UTF-8编码中,空字符如单个空字节存储。在umf-8相反,它们被编码为两个字节序列,同样的方式是CodePoints U + 0080-07FF在普通UTF-8中处理。这具有以下优点:Mutf-8编码的字符串永远不会包含文字空字节,允许使用C字符串函数。

ASM当然是在写出ClassFile时编码Mutf-8中的所有内容(否则它根本无法工作)。但是,ASM的ClassFile Parser在它将接受的内容更加自由。它将接受文字字节和正确的两个字节编码,并将其转换为内存代码表示中的常量字符串(存储为使用UTF-16编码的普通Java字符串)。

这意味着如果输入ASM一个几乎有效的类文件,除了包含字符串中的文字空字节,可以触发溢出错误。例如,如果输入类文件包含一串长度32768,其中数据是32768空字节,则解析器将转换为内存中的32768个空字符串。然后,当它将其写入Classfile时,它将将内存字符串编码为MUTF-8,导致编码为32768 * 2 = 65536字节长。当它尝试写入OUT时,长度字段将溢出到0。

现在我们发现了一个漏洞,有问题是如何实际编写利用的问题。我们将不得不仔细创建一个(无效)类,这些类文件似乎是无害的,而且在ASM转换后,将由JVM作为恶意代码重新解析。

显然,这样做是一种痛苦,所以我们希望尽可能更容易地将绝对最小的代码放在利用ClassFile中。因此,我决定在我的Exproit类中放置一个最小的自定义类加载器,然后可以从“安全”代码中调用以加载任意代码并继续剥削过程。

公共类主{public静态void main(string [] args)抛出throwable {exploit e = new exploit(); Byte [] Payload_ByTecode = / *字节对于有效载荷类文件* /; class payload_class = e。 sneakylead(payload_bytecode);方法m = payload_class。 getmethod(" doevilstuff"); m。调用(null);公共类exproit扩展了ClassLoader {Public Class Sneakylearoad(Byte [] B){返回此内容。定义(b,0,b。长);公共类有效载荷{public static void doevilstuff(){// evill your will}}

在这里,我们有三个类:主要,利用和有效载荷。其中,利润是我们唯一需要用手绕过ASM的类,它包含最少的代码。

主类是所有其他设置代码的位置。我们可以在普通的Java中写它,因为它是安全的亚马逊的角度。所有它所做的就是呼吁我们的利用课程。 Exploit类包含一个类加载器和从任意字节加载类的方法。通常,ASM将重写SneakyLexCoad方法,调用类加载器API的ASM包装器,这反过来将在运行时应用于运行时重写的ASM字节码重新编号传递到Sneakyleak。但是,由于漏洞类绕过了字节码重写,因此没有添加包装器,因此Sneakyleak允许我们加载任意代码而不再次通过ASM。

然后,主类调用SneakyleAkoad来加载有效载荷类,我们可以在其中放置其余的代码,以便在剥削后要做任何事情。由于ASM从首先看到它,因此有效载荷类也可以用普通的Java编写。

注意:我在2013年的原始黑客中没有任何文件,因此此处看到的所有代码都是为了这个博客文章而重新创建,并且类似但与原始漏洞代码相似。这里所示的骨架特别简化。例如,在实际的应用引擎中,您正在编写Web服务器,因此应用程序入口点是Web请求处理程序,而我在此显示的内容使用命令行应用程序中的main()方法。

不幸的是,上面的轮廓并不完全起作用。我们仍然必须与许可制度争辩。

回想一下,Java权限系统旨在使用预防小程序读取您的本地文件的内容。所有Java代码都有一个具有各种权限的关联安全策略。默认情况下,桌面应用程序运行所有权限,而Java applet默认情况下运行java applet以非常严格的一组权限运行。

当然,App Engine团队决定通过使用最严格的权限设置的用户代码来使用权限系统作为深度测量的防御。即使我们绕过ASM字节码重写,我们仍然有一个事实,我们没有实际做得很多。

幸运的是,事实证明很容易绕过许可系统。关键是某些权限,例如ClassLoader权限,呈现所有其他权限无关紧要。这是因为当您定义自定义类加载器时,您可以选择将哪些权限分配给您加载的任何类,并且您可以授予加载的类甚至可以自己授予权限。这不是一个安全错误 - 这是正式记录的行为。

对于Java applet,这并不重要,因为applet在第一个位置没有授予ClassLoader权限。但是,App Engine使用户能够使用自定义类加载器。它们通过使用字节码重写来插入包装器并强制执行安全检查来使其安全。但是,从JVM的角度来看,包裹的代码仍然最终将ClassLoader称为引擎盖下。这意味着在授予用户代码的少数权限中,它们必须授予ClassLoader权限,并且只纯粹地依赖于ASM以确保用户定义的类加载器的安全性。

这意味着如果我们设法绕过ASM并定义一个不受限制的类加载器,它也是绕过权限系统的微不足道。 但是,我们仍然必须添加代码以实际执行此操作。 我们将从Exproit类开始,在那里我们想要尽可能少代码。 幸运的是,设置加载类的权限只是添加保护域参数的问题(加上API也需要的名称参数)。 我们刚刚通过参数,让主类进行实际创建保护域的大量提升。 公共类Exploit扩展了ClassLoader {公共类Sneakyleykoad(String Name,Byte [] B,PresenceDomain PD){返回此。 defineclass(名称,b,0,b。长度,pd); }} 接下来,我们更新主域以创建具有Allpermission并通过它的保护域。没有 ......