Giter Site home page Giter Site logo

heap-exploitation-in-real-world's Introduction

[TOC]

概述

本文将以多个真实的堆相关的漏洞以及一些高质量的研究文章为例,介绍在真实漏洞场景下的堆利用技巧,主要是介绍各种现实漏洞场景的堆布局技巧。

vuln obj : 存在漏洞的对象,比如存在堆溢出的对象。

target obj: 漏洞利用时需要篡改的对象,比如利用堆溢出时,我们想覆盖的对象。

当来利用堆溢出的时候,我们需要找一个 gadget object 来进行利用

image-20211006121340597

常用的 gadget object 类型:

  1. 有函数指针,虚表指针,可用于劫持控制流。
  2. 对象中有 size/length 字段,修改这个字段可以导致后续使用对象时发送越界。
  3. 对象中的指针在后面的逻辑中会被用于读、写内存等操作,利用程序的逻辑可以完成一些漏洞利用的原语。

此外利用堆漏洞非常重要的是进行堆布局,为了进行堆布局我们需要找到外部输入可控的内存操作原语,搜索内存操作原语时需要关注三个要素:

  1. 内存分配的大小是否可控
  2. 内存中的内容是否可控
  3. 分配到的内存的生命周期是否可控,即我们能否控制其申请和释放的时机

然后根据不同的内存控制原语采用合适方式进行堆布局,常见搜索内存操作原语的思路是在代码中找含有malloc,new,realloc,std::vector,std::string的调用点,然后判断是否可以由外部输入触发。

作者提到进行堆布局的时候,可以选用占位式堆喷,如图所示:image-20211006123541144

此外为了提升溢出到 gadget 的概率我们可以先把内存碎片清理了,然后再进行占位式堆喷,这样 vuln obj 和 gadget obj 大概率会相邻分配。

如果分配 gadget object 的时候会有一些小对象的分配从而导致影响内存布局,我们可以先在堆上占位一些小的内存块,然后在分配 gadget 前释放一些小的内存块,这样小对象就会落在之前占位的小内存块中,避免影响 vuln obj 和 gadget object 之间的布局。

image-20211006123820942

Assuming the gadget object is the one which allocates the unwanted allocations, one way to deal with this issue is to do the following:

  1. First, at the beginning of the exploit, we allocate a bunch of smaller blocks, let’s say 100 blocks of 0x100 bytes each
  2. We then allocate all the placeholder sets (表示一组相邻的 overflowable 和 gadget 占位对象)
  3. Then, for each placeholder set, we first free the gadget placeholder.
  4. Then we free a few of the small filter allocations. These will be added to the free bins
  5. Then we allocate the gadget itself, the smaller unwanted allocations will use the filter allocations we just freed, and the gadget itself will fall on the placeholder.

漏洞是一个整数溢出导致的堆溢出

case FOURCC('t', 'x', '3', 'g'):
{
    uint32_t type;
    const void *data;
    size_t size = 0;
    if (!mLastTrack->meta->findData(
            kKeyTextFormatData, &type, &data, &size)) {
        size = 0;
    }
    if (SIZE_MAX - chunk_size <= size) { // <---- attempt to prevent overflow 
        return ERROR_MALFORMED;
    }
    uint8_t *buffer = new uint8_t[size + chunk_size];
    if (size > 0) {
        memcpy(buffer, data, size);
    }
    if ((size_t)(mDataSource->readAt(*offset, buffer + size, chunk_size))
            < chunk_size) {
        delete[] buffer;
        buffer = NULL;
        return ERROR_IO;
    }

在分配内存前有一个检查,但由于 chunk_size 在 32 位系统下下也是 uint64_t 的,所以当 chunk_size > SIZE_MAX 时(比如 0xFFFFFFFFFFFFFFFF),会通过检查,然后下面分配的时候就会整数溢出。

从文件中解析 chunk_size 的代码如下

status_t MPEG4Extractor::parseChunk(off64_t *offset, int depth) {
    uint32_t hdr[2];
    if (mDataSource->readAt(*offset, hdr, 8) < 8) {
        return ERROR_IO;
    }
    uint64_t chunk_size = ntohl(hdr[0]);
    uint32_t chunk_type = ntohl(hdr[1]);
    off64_t data_offset = *offset + 8;

    if (chunk_size == 1) {
        if (mDataSource->readAt(*offset + 8, &chunk_size, 8) < 8) {
            return ERROR_IO;
        }
        chunk_size = ntoh64(chunk_size); // 从文件中获取 8 字节的 chunk_size

因此通过这个漏洞我们可以实现一个堆溢出,vuln obj 的大小可控,溢出的内容可控,由于是整数溢出后面可能会拷贝大量的数据(比如 0xFFFFFFFFFFFFFFFF),最终可能会由于访问到没有映射的内存导致进程崩溃。

因此,利用这种类型的整数溢出漏洞,我们需要找到一些路径,让溢出发生后,不会拷贝过量的数据,对于这个漏洞来说我们可能有以下两种方式:

  1. 内存分配后,程序会调用 readAt 从文件中读取 chunk_size 的数据,如果能让 readAt 返回值小于 chunk_size (比如控制文件大小),就能避免溢出过量的数据。
  2. 通过覆盖在具体数据拷贝之前调用的函数指针完成利用,从而避免了溢出过量数据。

该漏洞的利用采取的是第二种方式,即溢出到 mDataSource 对象 (其类型 MPEG4DataSource ),然后把该对象的虚表劫持,从而劫持 readAt 这个虚函数调用。

那么利用该漏洞的 target obj 就是 mDataSource 对象,如果要利用该漏洞,buffer 和 mDataSource 的内存布局如下:

image-20211006145738908

下面就介绍如何利用程序中的逻辑来整理堆的布局,实现上述的布局,首先需要分析源码定位 vuln obj 和 target obj 的分配方式,当解析到 stbl 标签时,会分配 mDataSource 对象(target obj),当解析 tx3g 标签时就会分配 vuln obj 并触发溢出。

然后再看看程序用的内存分配器的一些特点,其使用的是 jemalloc ,jemalloc的大概思路是把内存块分成大小相同的 object,然后返回给申请者,类似于内核的 slub 分配器。

由于堆分配器的实现,堆在初始情况下先分配的对象的地址会比后分配的对象的地址小,这时如果直接先分配 target obj 的话,vuln obj 就会在 target obj 的后面,因此无法溢出到 target obj.

image-20211002221526076

假如先分配 vuln obj 的话,由于漏洞触发点的内存分配和数据溢出是一起发生的,也是无法覆盖到 mDataSource

或者我们可以利用jemalloc的LIFO特性,先把后面的块释放了,然后把前面的块释放了,然后再先分配 target obj 后分配 vuln obj,也可以实现上述的布局。

image-20211004111532366

exploit 里面的思路是采用占位对象,然后通过控制占位对象的分配与释放,来完成布局,示意图如下:

image-20211002205439796

  1. 首先分配两个占位对象 A 和 B,且 A 和 B 的前后都是被分配出去的内存,然后释放B,再利用 stbl 标签分配 target 对象,这样 mDataSource 就会落在 B 的位置。
  2. 然后释放 A ,并利用 tx3g 标签触发漏洞,分配buffer到 A 的位置,就能完成我们需要的布局,这样一溢出就可以覆盖到 mDataSource 对象。

要完成上述布局,关键点在于需要找到可以通过用户输入(mp4文件)控制 分配/释放 的代码逻辑,在 libstagefright 中我们可以利用 avcChvcC 来进行占位,通过这两个标签的特点为:

  1. 分配任意大小的内存,内存中的内容可控(从文件读取)
  2. 内存释放的时机可以通过输入文件中的特定标签控制

利用 avcChvcC 来占位的示例图如下:

image-20211002210810170

为了稳定的完成堆布局,还需要解决内存碎片的问题,上述布局成立的前提是 avcC 和 hvcC 对象是相邻的,如果内存中存在内存碎片的话,这两个对象是有可能不相邻的。

image-20211004113906179

为了解决这个问题常用的方式是首先把之前的内存碎片清掉(即大量分配内存,把之前堆中碎片的内存块申请完),之后再申请内存的时候,我们分配到的内存块的顺序就会符合我们的预期了。

image-20211004115112728

exploit 中使用的是 pssh 标签来清理内存碎片,原因是程序在解析 pssh 标签时会根据标签中的数据分配内存,内存的大小和内容可控,且分配出来的内存在整个文件解析完成后才会被释放,因此直接在输入文件中放置多个 pssh 标签就可以完成内存碎片的清理工作。

完成堆布局后现在就可以溢出到 mDataSource 对象 (其类型 MPEG4DataSource ),接下来的利用思路就是覆盖对象的虚表,把虚表劫持到我们伪造的需要,然后对象进行虚函数调用的时候就可以劫持pc。

image-20211004120703444

目前的问题是如何知道 伪造的虚表 的地址,获取地址一般有两种方式:

  1. 找一个信息泄漏漏洞,或者利用漏洞构造信息泄漏,从而可以拿到堆的地址,然后把虚表指针指向堆上的可控数据位置即可。
  2. 利用堆喷射技术,在某个固定的地址布置伪造的虚表数据,然后把虚表指针指向该位置即可。

这里使用的是堆喷射技术,以32位系统为例,堆喷射的思路大概是由于进程虚拟地址空间的大小限制,当程序分配大量内存时(比如 0xff000 字节时), 尽管存在随机化,我们依然可以以极大的概率在某个具体的地址(0xf7500000)上布置我们的数据。

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

#include <unistd.h>
#include <sys/mman.h>

#define ALLOC_SIZE  0xff000
#define ALLOC_COUNT 0x1

int main(int argc, char** argv) {
  int i = 0;
  char* min_ptr = (char*)0xffffffff;
  char* max_ptr = (char*)0;
  
  for (i = 0; i < ALLOC_COUNT; ++i) {
    char* ptr = mmap(NULL, ALLOC_SIZE, 
                     PROT_READ | PROT_WRITE | PROT_EXEC, 
                     MAP_PRIVATE | MAP_ANONYMOUS,
                     -1, 0);
    if (ptr < min_ptr) {
      fprintf(stderr, "new min: %p\n", ptr);
      min_ptr = ptr;
    }
    if (ptr + ALLOC_SIZE > max_ptr) {
      fprintf(stderr, "new max: %p\n", ptr + ALLOC_SIZE);
      max_ptr = ptr + ALLOC_SIZE;
    }
    
    memset(ptr, '\xcc', ALLOC_SIZE);
  }
  
  fprintf(stderr, "finished min: %p max %p\n", min_ptr, max_ptr);
  ((void(*)())0xf7500000)();
}

利用大量的内存分配可以让进程的某个地址存放我们的数据,数据的内容如何控制呢,按照我的理解,我们在进行堆喷的时候尽量按页对齐去分配,然后以页为单位进行数据布局,就可以实现在可预测的地址,布置可控的数据,具体的偏移和布局还需要调试确认。

exploit 中的堆喷射思路如下

image-20211004135942470

小结

该漏洞的利用非常经典,主要的特点如下:

  1. 触发整数溢出后,通过劫持后续可能会使用的虚函数调用来避免整数溢出后的内存拷贝访问到不可访问的内存.
  2. 通过分析程序对文件的解析流程,找到了内存申请和释放的原语(avcC 和 hvcC),随后使用这两个标签进行占位并完成了 vuln obj 和 target obj 的布局。
  3. 为了增大占位成功的概率,使用了 pssh 标签分配大量对象,清理内存碎片。
  4. 为了在没有信息泄露的情况下劫持虚表,利用 pssh 进行堆喷射,在可预测的地址处放置 rop gadget.

主要可以学习的思路有:

  1. 触发漏洞后,要思考后续的步骤,比如后续要覆盖什么,为了完成覆盖,需要什么样的内存布局。
  2. 然后去程序中查找是否存在内存控制的原语,比如内存申请/释放原语,申请的大小、内存的内容、释放的时机是否可控等。
  3. 然后就根据堆分配器的特性来尝试堆布局。
  4. 堆喷射在32位下还是很有用的,64位下在一些情况下面也是可以用的。
  5. 占位型堆布局和内存碎片的清理也非常的经典。

漏洞是8字节的溢出, vuln obj 的大小可控。

img

作者通过不断调整 vuln obj 的大小来触发漏洞,然后分析 crash 的上下文,最后在32字节的run中找到了合适的 target obj (fixed_queue_t),该对象中有个函数指针,可以用于劫持控制流。

为了实现控制 pc,还利用堆喷在可预测的地址上布置了数据,布置的过程主要靠尝试和调试。

image-20211004144202896

小结

该漏洞利用最直接借鉴的**是通过不断尝试 vuln obj 的大小,来找到潜在的 target obj,这种思路比较有通用性,也可以减少人工搜索 target obj 的工作量,甚至会发现一些比较神奇的对象。

还有就是 jemalloc 的堆喷可以通过观察 regions 的内存布局来提升成功率。

这组漏洞利用包含两个漏洞,一个信息泄露漏洞(CVE-2020-3847)和一个堆溢出漏洞(CVE-2020-3848)。

CVE-2020-3847 的成因是内存申请的大小和访问内存时的偏移检查有问题,漏洞触发流程:

  1. 首先发一个请求,让 ServiceAttributeResults 分配 16 字节的内存
  2. 然后再发一个请求,设置一个比较大的偏移,就可以 泄露出 ServiceAttributeResults 后面的内存数据

CVE-2020-3848 的漏洞成因是分配内存时分配的是 0x20 字节,但是拷贝数据时最多可以拷贝 0xff 字节。

为了实现 Zero Click 的漏洞利用,作者经过分析发现只能通过创建 SDP 连接来让目标分配内存,且一个设备最多只能创建 30 个 SDP 连接,创建 30 个 SDP 连接后实际会分配的对象和关系如下图

image.png

随后作者通过释放其中的几个 SDP 连接对象,并结合信息泄露漏洞来检查当前堆布局是否可以用于利用,即vuln obj 后面是否存在可以用于利用的对象。

大概思路应该是通过一些内存的申请释放看能否让 vuln obj 后面放置可利用的对象。

小结

主要启示在于:

  1. 在审计代码的时候,要注意审计内存申请和使用不是在同一次请求、解析过程中处理的情况,两者的不一致就有可能导致漏洞。
  2. 该漏洞利用的堆布局有点撞运气的感觉,不过由于有信息泄露,我们可以多次尝试,直到符合预期的堆布局出现再触发堆溢出漏洞。
  3. 有限的堆操作原语(只能控制某些特定对象的申请于释放)也是非常有用的。

这篇文章涉及3个漏洞,本节主要涉及其中2个被利用的漏洞,即 CVE-2020-12352 和 CVE-2020-12351,比较有意思的是作者在编写 CVE-2020-12352 的 POC 时,触发了 CVE-2020-12351.

CVE-2020-12351 是一个栈变量未初始化漏洞导致的信息泄露,作者通过随机的发送一些报文进行尝试,最终可以通过该漏洞泄露出 内核和堆的地址。

CVE-2020-12352 是一个类型混淆漏洞,漏洞的成因是把 struct amp_mgr 对象当作了 struct sock 对象传入了 sk_filter 函数进行处理。

经过分析,通过控制 sock 结构体 (即被类型混淆的 amp_mgr 对象)的 sk_filter 指针可以完成漏洞利用, struct sock 的结构体布局如下:

// pahole -E -C sock --hex bluetooth.ko
struct sock {
	struct sock_common {
		...
		short unsigned int skc_family;       /*  0x10   0x2 */
		...
	} __sk_common; /*     0  0x88 */
	...
	struct sk_filter *         sk_filter;   /* 0x110   0x8 */

可以看到 sk_filter 字段位于 sock 结构体偏移 0x110 处,但是 struct amp_mgr 的大小为 0x70,由于 slub 分配器的特性, amp_mgr 结构体实际会在 kmalloc-128 处分配,所以 sk_filter 字段实际位于 amp_mgr 结构体后面第二个内存块中,如下图所示:

image-20211004225218604

amp_mgr 分配在 kmalloc-128 中,当 amp_mgr 被当作 sock 结构传入 sk_filter 函数处理时,其访问 sk->sk_filter 时,实际访问的是图中标红区域。

因此目前的问题是如何控制 fake sk_filter 字段,作者的思路是首先分配 amp_mgr ,然后再 kmalloc-128 中分配两块内存,从而控制 fake sk_filter 字段.

上述思路的关键是需要找到可以远程从 kmalloc-128 中分配内存的原语,常见思路是在协议栈代码中搜索内存申请函数(比如 kmalloc, kzalloc等),不过作者没有找到上述的原语,最终是利用 kmemdup 操作实现的内存申请,代码如下

// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/net/bluetooth/a2mp.c
static int a2mp_getampassoc_rsp(struct amp_mgr *mgr, struct sk_buff *skb,
				struct a2mp_cmd *hdr)
{
	...
	u16 len = le16_to_cpu(hdr->len);
	...
	assoc_len = len - sizeof(*rsp);
	...
	ctrl = amp_ctrl_lookup(mgr, rsp->id);
	if (ctrl) {
		u8 *assoc;

		assoc = kmemdup(rsp->amp_assoc, assoc_len, GFP_KERNEL);
		if (!assoc) {
			amp_ctrl_put(ctrl);
			return -ENOMEM;
		}

		ctrl->assoc = assoc;
		ctrl->assoc_len = assoc_len;
		ctrl->assoc_rem_len = assoc_len;
		ctrl->assoc_len_so_far = 0;

		amp_ctrl_put(ctrl);
	}
	...
}

大概堆布局思路如下:

image-20211004231646521

  1. 首先利用 kmemdup 的原语大量分配内存,清理之前 slab 中的碎片
  2. 然后分配 amp_mgr ,之后再分配一些 ctrl->assoc,最后触发漏洞就可以控制 fake sk_filter 的值。

不过看 exploit 里面的堆布局思路,貌似是直接先分配多个 ctrl->assoc,然后释放掉最后一个 assocamp_mgr,最后重连,就有一定概率会达到上面的布局,从而完成利用。

堆布局相关代码片段:

image-20211004232601841

小结

  1. 搜索内存操作原语时,间接调用内存申请的函数也是需要关注的。
  2. 做漏洞利用时有一定概率也是可以接受的,比如堆布局,即使无法达到100%,能提升一点概率是一点,或者如果概率可以接受的话也可以直接随机着来。

文章中利用的漏洞是固件在解除 TDLS 连接时 由于解析 FT IE 时没有检查长度导致的堆溢出漏洞

image-20211005104449068

因此我们就有了一个堆溢出漏洞,vuln obj 的大小是 256 字节,下面就需要找被溢出的对象,以及寻找堆布局的方式。

作者首先逆向分析了固件中的堆分配器,然后通过 固件代码 patch、内存dump等手段实现了一个堆的可视化工具,可以跟踪固件的内存申请和释放、可视化某个时刻的堆内存布局:

after_setup.png

图中红色表示正在使用的内存块,灰色表示处于 free 状态的内存块。

  • 图中第一行表示 初始的堆状态,可以看到这里有一块很大的空闲内存.
  • 第二行表示 创建 TDLS 连接后的堆状态,有很多内存被使用了,值得注意的是此时堆中还有两块特别标注的空闲内存块,即 0x1f03fc (大小为 0x11c )和 0x1f2108 (大小为 0x124).

然后我们如果尝试解除 TDLS 连接并发送恶意的FT IE触发漏洞,由于堆分配器的实现,此时 malloc(256) 会分配到 0x1f03fc 这块内存,我们也就可以溢出到 0x1f0520 后面的块。

此外经过作者的不断尝试发现,堆状态非常稳定,每次 创建/解除 TDLS 连接 时堆的状态基本是一样的,这样如果能在这个堆状态下完成利用,exploit的稳定性应该也是比较高的,不过如果程序中有一些可用于堆布局的原语(内存申请、释放),可能可以改变这个内存布局,不过作者没有尝试,而是在当前内存状态下完成了利用。

因此 vuln obj0x1f03fc 这块内存,target obj0x1f0520 后面的内存块。

对于堆溢出的利用常见的思路有两种:

  1. 修改堆块头的元数据,比如 size,或者处于释放状态的块中的 next 指针
  2. 修改堆中对象的数据,比如对象的长度字段,堆中的指针等,然后利用程序对对象的操作来进行后续的利用

vuln objtarget obj 的内存 dump 如下:

截图来自 2017-03-13 12:39:21.png

可以看到 target obj 的头不由两个看着像指针的值,不过经过修改尝试发现这两个指针好像没有人用,因此目前只能尝试修改内存块的 头部字段,由于 target obj 所在内存块是 inuse 状态,所以头部字段中只有 size 字段是有效的。

glibc 的堆溢出 中常用的方式是修改 size 字段来构造重叠的堆块,这里也是这样的思路,由于目标系统中堆的使用情况比较复杂, 作者采取的方式是,写脚本自动化地测试,即尝试把 target obj 的 size 字段修改成不同的值,然后检查操作完成后的堆状态。

经过测试,作者发现把 target objsize 字段覆盖为 72 并断开 TDLS 连接,堆中出现了重叠的堆块:

重叠.png

可以看到在断开 TDLS 连接后,0x1f072c 这个 0 字节的 chunk 位于一个大的空闲块中间。

创建重叠堆块后,还需要一个控制内存分配的原语,用于分配到重叠的块,然后修改它的 size 和 next 指针,从而实现任意地址写,固件处理 action 帧的代码中就存在这样的逻辑:

image-20211005153123077

A 是一个全局变量,利用 A 的分配和申请逻辑,可以获取一个生命周期、大小、内容可控的内存控制原语

最后利用堆分配的 best-fit 特性,修改 chunknext 指针,实现把一个已分配的块链接到空闲块的链表里面,然后实现任意地址写,最后的利用手法如下:

首先利用任意地址写,往一个初始化阶段分配的堆块中布置 shellcode(由于没有地址随机化,系统启动过程中分配的一些堆内存的位置会固定),然后修改 堆中定时器的函数指针,最后等待定时器到期,执行 shellcode.

exploit (2).png

选择把处于使用中的堆块链入空闲块链表的原因是:处于使用的块的 next 指针为0,这样就不会导致内存分配器在分配内存时由于遍历链表导致崩溃。

小结

  1. 堆利用的时候,通过调试、打印、dump等手段查看 vuln obj 附件的内存状态,以及触发漏洞前后的内存使用情况,可以很好辅助漏洞利用、定位漏洞利用的方案。
  2. 对于比较复杂、不熟悉的场景可以尝试暴力枚举,来探测可利用的方案。

漏洞成因是 sudo 在解析命令行参数时存在堆溢出,vuln obj 的大小和内容可控。

目前问题就是寻找 target obj,作者的思路是fuzz,通过随机选择 vuln obj 的大小、溢出的大小、setlocale 涉及的相关环境变量(用于控制溢出前的内存块布局),最后通过对 crash 的上下文去重,发现了三种可用于利用的场景,本节将介绍通过覆盖 service_user 完成利用的过程。

首先通过调整 poc 和调试,能够发现在堆上确实是存在 service_user 结构,不过调试发现其和 vuln obj 的偏移过大,直接去尝试覆盖 service_user 程序会由于中间的一些数据被覆盖而崩溃,且两者的偏移也不固定。

image-20211005172559810

为了解决这个问题,可以利用 setlocale 函数中的 内存分配/释放 操作进行堆布局,让 user_args 落在 service_user 的前面。

setlocale 函数在解析环境变量(比如LC_CTYPE, LC_MESSAGES, LC_TIME等)时,会申请内存并把环境变量的值拷贝到申请的内存中,申请的内存在函数退出前会被释放。

通过利用 setlocale 函数的逻辑和堆分配器的机制,我们提前在堆中创建一些 free 的堆块,让 service_user 分配到其中的一个空闲块,这样在漏洞触发时就能在 service_user 前面留一个 free 堆块,这时我们将 user_args 分配到 service_user 前面就可以稳定溢出service_user 了,大概流程图如下所示:

image-20211005174844655

小结

  1. 通过 fuzz 来探测可能用于利用的内存状态值得学习。
  2. 通过提前在堆里面凿一些洞(空闲的堆块)进行堆布局的思路非常新奇。

漏洞是 Zoom 在进行密钥协商时存在一个堆溢出,堆溢出的情况如下:

  1. vuln obj 的大小固定 1040 字节
  2. 溢出的大小和内容可控

漏洞利用的环境是 win10 + 32位的 Zoom 软件。

首先利用堆溢出实现信息泄露,常见的思路是分配一些带 length 字段的对象,然后覆盖length字段从而 leak 出一些数据,经过一番分析没有发现能够回显的这种对象。

最后发现,可以给目标 Zoom 客户端发送特定的请求,让对端 Zoom 向我们的服务器发起 https 请求,请求的 url 部分可控,有种 SSRF 的感觉。

接下来就是利用这个 bug 进行信息泄露,具体来说就是利用溢出把 URL 末尾的 \x00 覆盖掉,然后让目标客户端向我们的服务的发起请求,就可以泄露 URL 后面的内存数据了。

这里选择泄露的对象是 TLS1_PRF_PKEY_CTX,这个对象是 openssl 进行 TLS 协商时创建的,对象中存在指向 DLL 的指针,通过泄露这个指针可以可以拿到 DLL 的基地址。

image-20211006105537780

如果堆布局按照预期就能在我们的服务器收到泄露的数据

img

不过由于 LFH 分配器的随机化和内存状态的不确定性,成功率还是比较低,作者采取了一些措施来提升成功率:

  1. 创建多个服务器的连接,用于不断的进行TLS协商,目的是在 LFH 的桶中分配大量 TLS1_PRF_PKEY_CTX,这个过程中即使对象被释放也没关系,因为释放之后内存中的指针不会被清理,也能进行leak,这样就可以增大布局的概率。
  2. 在覆盖的过程中,可能由于内存布局的不一致导致 vuln obj 和 url 之间存在一些带指针的对象(比如某些类的实例),作者的处理方式是首先通过堆喷在某个地址(比如 0x0d0d0d0d)放置数据,然后用 0x0d0d0d0d 覆盖对象,这样即使覆盖到了不正确的对象也不会导致进程崩溃,从而可以多次尝试。

image-20211006111507328

完成信息泄露后,下一步就是找一个对象控制 PC, 这一步使用的是 FileWrapperImpl 该对象会在客户端被呼叫响铃的时候被不断的创建和删除,因此使用堆溢出覆盖其虚表即可劫持控制流。

image-20211006112040501

为了劫持虚表还需要进行堆喷,攻击者可以发起请求让目标客户端加载 gif 图片,而且没有大小限制,利用这个机制就可以进行堆喷。

第二部的过程中也会存在很多不确定性,比如溢出 vuln obj 的过程中可能会覆盖到堆中的其他类的虚表,作者的解决方案如下:

  1. 不断尝试溢出、调试搜集在利用过程中可能会触发的场景,在虚表、rop 中进行适配,比如 A 类调用的时候可能用的是虚表偏移 0x20 的函数指针,我们就在 0x20 的函数指针布置适合 A 的利用 rop.
  2. 不断溢出,每次溢出的大小递增,避免出现一次溢出太多导致不稳定。

小结

  1. 利用客户端的 url 请求机制进行 leak 也是一个思路。
  2. 处理堆布局随机的情况可以多次尝试,为了能够多次尝试还需要合理控制溢出数据,避免溢出失败导致进程 Crash.
  3. 针对不同的溢出场景,在虚表中进行适配思路非常不错!

漏洞是在解析图片时存在整数溢出导致的堆溢出

img

首先根据 width, height, output_components 计算分配内存的大小,然后从文件中一段一段读入内存,如果 width * height * output_components 发生整数溢出,后面从文件中读取内容时就会溢出。

之前提到过利用整数溢出漏洞需要找到终止拷贝的条件,或者在访问到非法地址前劫持控制流,这里采用的是覆盖拷贝过程中会用到的函数指针,在循环访问到非法地址前劫持控制流。

作者通过分析循环中的一些函数调用,发现在 jpeg_read_scanlines 函数和子函数里面会使用 cinfo 结构体中的一些的函数指针

struct jpeg_decompress_struct { 
       ......
       struct jpeg_d_main_controller *main;  <<-- there’s a function pointer here
       struct jpeg_d_coef_controller *coef; <<-- there’s a function pointer here
       struct jpeg_d_post_controller *post; <<-- there’s a function pointer here
       ......
};

target obj 找到了不过由于没有找到合适的内存控制原语,没法进行完美的堆布局,作者最终采取的策略是通过调试和观察漏洞触发前后的堆状态,来搜索可能用于利用的 vuln obj 大小。

首先通过分析知道 cinfo 分配在 0x5000run 中,然后查看 cinfo 前面的一些run中的使用情况,发现可以通过分配 0xe0 的内存,然后往后溢出即可覆盖 cinfo .

image-20211005205941293

img

原文作者到这里就没继续了,这种方式有点撞运气的成分,如果进程在之前有些其他分配就可能会导致偏移不一致,不过原文中也提到了,我们可以溢出多一点总是能够溢出到函数指针的。

小结

  1. 通过修改循环中的函数指针来防止整数溢出后的Crash也是比较经典。
  2. 有时候实在没有堆控制原语时,也需要多调试、分析,查看 target obj 和 vuln obj 的附件是否有可以用于利用的内存块。

漏洞利用链包含两个漏洞:

  1. malloc 未初始化导致的信息泄露。
  2. vrend_resource 堆溢出,vuln obj 的大小可控,溢出的大小内容可控。

利用未初始化漏洞的思路是堆喷带有函数指针的结构体,然后释放这些结构体,最后触发未初始化漏洞并获取未初始化的内存内容,从而获取 virglrenderer 库的地址。

image-20211006131035948

获取 libc 的地址的思路是申请大块内存,内存中就会包含libc 的地址

image-20211006131050470

堆溢出的利用思路是堆喷多个 vrend_resource 对象,然后利用溢出修改下一个 vrend_resource 对象的指针,从而实现任意地址写

溢出前,两个 vrend_resource 对象的布局

image-20211006131203472

溢出后,第二个 vrend_resource 对象的指针修改为任意地址,然后利用程序的逻辑就可以实现任意地址写

image-20211006131255077

小结

  1. 利用未初始化漏洞进行内存泄露的思路值得学习,比如堆喷带指针的结构,然后利用未初始化漏洞获取地址。
  2. 通过堆喷 vuln obj ,然后溢出下一个 vuln obj 的指针来进行任意地址写,得到启示是做利用的时候可以先看 vuln obj 是否就可以作为 target obj.

主要就是介绍如何在 ios 的内核里面进行占位式堆布局

具体思路是先清理堆中的内存碎片,然后用多个 victim object 包着一个 vuln object ,这样就能提升溢出到 victim object 的成功率,如下图所示

image-20211006144548152

其中: V 表示 victim object, P 表示用于占位的对象,后面溢出的时候就会把 P 释放,然后分配 vuln object.

为了进一步增加漏洞利用的成功率,可以多创建几个用于漏洞利用的区域,多次触发漏洞来提升成功率,如下图所示:

image-20211006144923461

小结

通过在 vuln obj 前后多包裹几个 victim object 和 创建多个漏洞利用区域来提升成功率的思路值得借鉴。

Samsung NPU kmalloc overflow & vmalloc oob

漏洞位于 __config_session_info 中,通过 ioctl 触发。

int __config_session_info(struct npu_session *session)
{
    ...
    ret = __pilot_parsing_ncp(session, &temp_IFM_cnt, &temp_OFM_cnt, &temp_IMB_cnt, &WGT_cnt);

    temp_IFM_av = kcalloc(temp_IFM_cnt, sizeof(struct temp_av), GFP_KERNEL);
    temp_OFM_av = kcalloc(temp_OFM_cnt, sizeof(struct temp_av), GFP_KERNEL);
    temp_IMB_av = kcalloc(temp_IMB_cnt, sizeof(struct temp_av), GFP_KERNEL);
    ...
    ret = __second_parsing_ncp(session, &temp_IFM_av, &temp_OFM_av, &temp_IMB_av, &WGT_av);
    ...
}

session 在 open 驱动时创建,驱动和用户态进程会使用共享内存进行数据交互,__pilot_parsing_ncp 首先从共享内存中提取出 temp_xxx_cnt,然后根据提取出的数量字段调用 kcalloc 分配内存,最后调用 __second_parsing_ncp 从共享内存中拷贝数据。

问题在于 __pilot_parsing_ncp__second_parsing_ncp 会从共享内存中提取两次 temp_xxx_cnt

如果通过条件竞争(起一个线程不断修改 共享内存中的值),让第二次取出的 temp_xxx_cnt 的值比一次的大就会导致后面的溢出。

下面介绍三种利用方式。

这篇文章作者使用的是一个 ION buffer 的内存越界加漏洞

    address_vector_index = (mv + i)->address_vector_index;
    // address_vector_index 可控, av 指向 ION 内存
    weight_offset = (av + address_vector_index)->m_addr;
    (av + address_vector_index)->m_addr = weight_offset + ncp_daddr;

这里的 ION buffer 分配在 VMALLOC 区域,那么首先就需要找到要越界写的 target obj,作者在文中提到内核开启 CONFIG_VMAP_STACK 选项后,进程(task)的内核栈也会从 VMALLOC 区分配,因此作者采用使用用户态进程的内核栈作为 target obj,因此大概的布局思路如下:

A diagram showing an ION buffer mapped directly before a userspace thread's kernel stack, with a guard page in between. The address_vector_offset is so large that it points past the end of the ION buffer and into the stack.

VMALLOC 区域分配内存时,如果没有指定 NO_GUARD_PAGE 参数就会在分配的内存后面加上一个不可访问的页作为 grard page.

预期布局如上图,在存在溢出的 ION buffer 后面是 进程的内核栈,然后触发漏洞修改内核栈的数据完成漏洞利用(比如 ROP)。

下面介绍 VMALLOC 区域的内存分配/释放机制,以及内存布局的方式。在 Linux 内核的虚拟地址空间中由 [VMALLOC_START, VMALLOC_END] 组成的区域称为 VMALLOC 区域,用于分配虚拟地址连续但是物理地址不一定连续的内存, VMALLOC 区域中已经分配出去的虚拟地址空间通过 vm_area 结构体表示,如下图所示:

image-20211009221024328

所有的 vm_area 通过链表和红黑树组织,在进行内存分配时,内核会从低地址往高地址遍历 vm_area 搜索其中处于空闲状态的地址区间,当 空闲空间大小 大于 申请大小 时就会把这个虚拟地址区间分配出去,类似于 ptmalloc 中 unsorted bin 的分配机制。

image-20211009222641521

申请到虚拟地址空间后会调用 alloc_page 一次一页地向伙伴系统申请页面,比如请求分配 10 页,就会调用10alloc_page

在释放 VMALLOC 中的内存时,首先会把虚拟地址空间中的物理地址释放回伙伴系统,但是对应的虚拟地址空间不会立马被释放,而是会被标记为 unpurged vm_area ,当所有 unpurged vm_area 的页面数大于一个阈值(ubuntu 20.10 上是 40000 页左右)时才会对这些 unpurged vm_area 进行回收,在 unpurged vm_area 被回收前其对应的虚拟地址是不可被申请的,这个特点在后面进行内存布局的时候需要考虑。

image-20211009224800882

还有一个比较重要的特性是在 VMALLOC 区域分配内存默认会在申请的页面后面加一页 guard page,用于防止溢出,除非指定 NO_GUARD_PAGE 标记,对于这个漏洞而言 vuln obj (ION buffer) 和 target obj (task kernel stack) 的后面都会带一个 guard page ,由于该漏洞是 OOB ,所以只需要在计算偏移时注意即可。

为了进行内存布局,首先需要找到能够在 VMALLOC 区进行内存分配/释放的原语,通过一番搜索和咨询,作者发现可以利用 binder 驱动的 mmap 接口来在 VMALLOC 区域中申请和释放内存,不过这里有一个限制,即一次最多只能申请 4 MiB 的内存。

一开始作者以IOS内核漏洞利用的思路尝试进行布局,大概思路如下:

image-20211010103646244

  1. 首先分配一大块内存,然后利用 fork 喷射大量的进程的内核栈,这样 large alloc 前后就会被填满进程的内核栈。
  2. 然后释放掉刚刚申请的大内存
  3. 再次分配一些内核栈,然后分配 ION buffer,然后再分配一些内核栈,这样在大内存中就会形成上图所示的布局,即在 ION buffer 的前后包裹了进程的内核栈
  4. 然后触发漏洞就能修改进程的内核栈了。

但是这里有一个小问题就是当释放 VMALLOC 区间中的内存时,如果 unpurged vm_area 的页面总数小于阈值,则这段虚拟地址空间不会被回收,所以也就不能完成上述的第3步操作,然后作者就放弃这种布局思路。

不过我感觉,只要用 mmap 的接口分多次分配大量的地址空间,然后触发释放,就能达到阈值,这时应该也能完成上述的布局。

最后的布局思路:

  1. 首先利用 binder 驱动的 mmap 接口,在 vmalloc 区域中分配内存,目的是把内存区间中的空隙给补上,之后的内存分配就会在一个空闲的大内存区域中分配。
  2. 通过 fork 在 vmalloc 区分配大量内核栈
  3. 分配 ION buffer
  4. 再次通过 fork 分配大量内核栈

A diagram showing an ION buffer mapped directly before a userspace thread's kernel stack, with a guard page in between. The address_vector_offset is so large that it points past the end of the ION buffer and into the stack.

然后访问 /proc/vmallocinfo 就可以看到 ION buffer 的后面跟着一些 内核栈。

image-20211009232212150

然后就通过修改内核栈里面的数据,实现了信息泄露和ROP.

小结

作者的思路非常清晰,拿到一个 vmalloc 堆的越界写漏洞,首先就是寻找 target obj,最终选择修改同处于 vmalloc 栈中的 进程内核栈 来进行提权。

在进行堆布局时作者一开始计划采用他之前在 IOS 上经常使用的布局方式(占位式布局),不过由于 vmalloc 区域在释放的时候采取的延时释放机制,作者放弃了这种方式,而是选用了相对比较直接的布局方式,即先填充内存碎片,然后分配 vuln obj, 然后分配 target obj,最后触发漏洞即可。

为了进行堆布局,作者首先是分析了 npu 驱动本身是否存在 vmalloc 区域的内存控制原语,最后是用的 binder 驱动中的逻辑完成的布局,不过这种首先从相对熟悉的代码中搜索需要的东西的思路也值得借鉴。

这篇文章的的利用思路和p0的思路差不多,不过这里没有采取复杂的内存布局策略,而是先用 fork 喷射大量的内核栈,然后分配 ION buffer ,然后再次分配大量内核栈,最后就可以实现布局。

...
  
atomic_int *wait_count;

int parent_pipe[2];
int child_pipe[2];
int trig_pipe[2];

void *read_sleep_func(void *arg){
    atomic_fetch_add(wait_count, 1);
    syscall(__NR_read, trig_pipe[0], 0x41414141, 0x13371337, 0x42424242, 0x43434343);

    return NULL;
}

...
  
int main(int argc, char *argv[]) {
  	...
    pipe(parent_pipe);
    pipe(child_pipe);
    pipe(trig_pipe);
  	...
    *wait_count = 0;
    int par_pid = 0;
    if (!(par_pid = fork())) {
        for (int i = 0; i < 0x2000; i++) {
            int pid = 0;
            if (!(pid = fork())){
                read_sleep_func(NULL);
                return 0;
            }
        }
        return 0;
    }
  	...
    if(leak(0xeec8) != 0x41414141){
        write(trig_pipe[1], "A", 1); // child process kill
        for (int i = ion_fd; i < 0x3ff; i++) {
            close(i);
        }
        munmap(ncp_page, 0x7000);
        goto retry;
    }
  	...

img

前面两篇文章都是利用 vmalloc 区域的越界写漏洞来完成的漏洞利用,本文使用的漏洞是由于竞争导致的 kmalloc 内存的溢出,vuln obj 的大小、溢出的大小可控,溢出内容部分可控。

漏洞代码的大概逻辑如下:

  1. 首先获取 temp_IFM_cnt ,然后分配 temp_av 数组,数组中成员个数为 temp_IFM_cnt
  2. 获取 WGT_cnt , 然后分配 addr_info 数组,数组中成员个数为 WGT_cnt
  3. 然后会再次取 temp_IFM_cntWGT_cntaddr_infotemp_av 中数组中的每个成员赋值。

如果第 3 步取的 xxx_cnt 大于前面用于数组内存分配的值时就会导致溢出。

不过由于结构体中的一些成员不可控,所以需要仔细梳理结构体中可以控制的成员以及结构体的大小,为后续的利用做准备

struct temp_av 结构体大小为 64 字节

offset - size - name 
 0 - 4 - index : Semi controlled, values are restricted
 4 - 4 - hole : Untouched (compiler gap)
 8 - 8 - size : Least significant 4 bytes are controlled, rest is zeroed
16 - 4 - memory_type: Untouched
20 - 4 - hole : Untouched (compiler gap)
24 - 8 - vaddr: Untouched
32 - 8 - daddr: Untouched
40 - 4 - pixelf_format: Controlled
44 - 4 - width: Controlled
48 - 4 - hieght: Controlled
52 - 4 - channels: Controlled
56 - 4 - strize: Zeroed
60 - 4 - cstride: Untouched

struct temp_av 结构体大小为 56 字节

offset - size - name 
 0 - 4 - memory_type : Zeroed
 4 - 4 - av_index : Semi controlled, values are restricted
 8 - 8 - vaddr : Kernel pointer into the ION buffer at controlled offset
16 - 8 - daddr : Bus address of the ION buffer at controlled offset
24 - 8 - size : Least significant 4 bytes are controlled, rest is zeroed
32 - 4 - pixelf_format: Untouched
36 - 4 - width: Untouched
40 - 4 - hieght: Untouched
44 - 4 - channels: Untouched
48 - 4 - strize: Untouched
52 - 4 - cstride: Untouched

因此我们漏洞的能力如下:

  1. 通过控制数组元素个数,可以控制vuln obj 的大小。
  2. 溢出的大小可控。
  3. 由于结构体的部分成员不可控,会导致溢出的内容部分不可控,这个在选择 target obj 时需要非常注意。

之后作者通过在 kmalloc 和 kfree 增加日志来追踪内存的使用情况,最后发现 kmalloc-128 会被频繁使用,于是觉得让 vuln obj 和 target obj 都落在 kmalloc-256 中,即在 kmalloc-256 中进行溢出。

为了实现稳定的漏洞利用,作者首先决定寻找用于占位的对象来实现稳定的堆布局,布局思路如下:

image-20211010221138477

这里使用的布局对象是 timerfd_ctx

完成布局后,作者开始寻找 target obj,他首先在 npu 的代码搜索,后面使用 codeql 来搜索,不过最后找到的对象都可用,原因是由于溢出数据中不可控的部分会把 目标对象 的一些关键成员破坏导致内核 panic.

搜索 target obj 的 codeql 脚本如下

import cpp

// Check if the function is set as a fielf of 
// struct file_operations
predicate isFopsHandler(Function f) {
  exists(Initializer i |
    i.getDeclaration().(Variable).getUnspecifiedType().hasName("file_operations") and
    f = i.getExpr().getAChild().(Access).getTarget())
}

// Can the function be called from system call handler
// or from a file_operations callback
predicate isSysHandler(Function f) {
  f.getName().matches("sys_%")
  or
  isFopsHandler(f)
  or 
  // Apply this recursively to previous functions in the control flow
  isSysHandler(f.getACallToThisFunction().getControlFlowScope())
}

// Return the name of the calling function
string getSysHandler(Function f) {
  (f.getName().matches("sys_%") and result = f.getName())
  or
  (isFopsHandler(f) and result = f.getName())
  or 
  result = getSysHandler(f.getACallToThisFunction().getControlFlowScope())
}


from FunctionCall fc, Function f, string sys
where
  // Match kmalloc family allocations
  fc.getTarget().getName().regexpMatch("k[a-z]*alloc") and
  // With a given size
  (fc.getArgument(0).getValue().toInt() > 128 and fc.getArgument(0).getValue().toInt() <= 256) and 
  // Reachable from system call or file operation
  isSysHandler(fc.getControlFlowScope()) and sys = getSysHandler(fc.getControlFlowScope())

select fc, "Callsite of fitting K*alloc: " + fc.getTarget().getName() + "(" + fc.getArgument(0).getValue().toString() + ") by " + fc.getControlFlowScope().getName() + " from " + sys

由于 三星 的内核中没有开启 CONFIG_SLAB_FREELIST_HARDENED ,最终的利用思路是通过覆盖 slab 中空闲 object 的 next 指针来让 ION buffer 链接到 slab 的 freelist 中,然后让 pipe_buffer 分配到 ION buffer ,此时就可以通过修改 ION buffer 来修改 pipe_buffer ,最后通过修改 pipe_buffer 的 page 指针实现物理内存的任意读写。

Pipe Buffer

小结

首先作者在拿到这样一个条件竞争导致的堆溢出时,对竞争可能出现的情况进行了分析,发现即使竞争失败也不会导致严重的后果(比如内核panic),这样就表示我们可以不断触发竞争直至竞争成功,保证了漏洞利用的成功率。

接下来作者对漏洞的原语进行了分析,搞清楚我们通过漏洞能实现的能力,比如溢出的大小,溢出的内容之后部分可控,以及可控的区域,这样对后续的漏洞利用打下了很好的基础。

后面寻找占位对象,进行占位式布局,寻找受害者对象等一系列思路都非常的流畅、清晰,这就提示我们在进行漏洞利用的时候需要搞清楚当下我们拥有的能力,接下来的目标,以及为了实现目标我们需要的东西和实施的方案,漏洞利用就是一环扣一环,需要时刻保持清晰的思路,一步一步的进行。

最后漏洞利用的思路也是非常有意思,利用溢出数据中 ION buffer 的地址覆盖 释放状态的内存块 的 next 指针,实现把 ION buffer 链入 slab 中,然后分配 pipe_bufferION buffer 中,然后用户态通过控制 ION buffer 的值控制了 pipe_buffer 结构体。

由于 page 指针指向的其实时 vmemmap 数组中的成员,所以通过修改 page 指针到 vmemmap 数组的其他位置就可以利用 pipe_buffer 的逻辑实现任意物理内存的读写。

漏洞成因是由于内核将 size_t 强转为 int 类型使用,导致检查绕过,最终实现向前越界写,vuln obj 是通过 vmalloc 分配的,触发漏洞时需要让 vuln obj 足够大 (比如 2GB, 即 0x80000000 如果当作 int 型使用是最小负数),然后就能往 -2GB-10B 写入 //deleted 字符串。

示意代码如下:

#include <stdio.h>
int sequoia_test(char* buffer, int len, char* value, int value_len)
{
    printf("buffer:%p, len:%d\n", buffer, len);
    char* p = buffer + len;
    len -= value_len;

    if(len < 0)
        return -1;
    p -= value_len;
    printf("new buffer:%p, len:%d, delta:%p\n", p,len, buffer - p);
    memcpy(p, value, value_len);
}

int main()
{
    size_t sz = 0x80000000;
    char* buf = malloc(sz);
    sequoia_test(buf, sz, "//deleted", 10);
}

运行结果

$ ./test   
buffer:0x7fba536a7010, len:-2147483648
new buffer:0x7fb9d36a7006, len:2147483638, delta:0x8000000a
segmentation fault  ./test

分析清楚漏洞的能力后,作者下一步就是寻找 target obj,通过分析之前的研究文章,发现 ebpf 的 jit 代码也是在 vmalloc 区域进行分配,所以作者的思路就是在 ebpf 生成代码后,修改代码页权限为不可写前把ebpf的代码给修改了,然后后利用 ebpf 完成内核的任意地址读写。

为了实现漏洞利用,作者进行堆布局的思路如下:

  1. 首先通过 bind 不同长度的目录,调用 vmalloc 分配各种大小的内存,目的是把内存中的碎片都分配出来,这样后面分配时就会在一块新的空间中分配(vmalloc 区域的尾部)
  2. 然后分配依次分配两个 1G 的内存 和 一个 2 G 的内存,这3个内存会顺序布局
  3. 然后释放第一个 1 G 的内存,由于内存大小比较大,会超过vmalloc回收地址空间的阈值,所以这个地址空间会被释放,接着大量分配 ebpf ,并利用 USERFAULTFD 让 ebpf 阻塞在修改代码页权限前。
  4. 触发漏洞,修改 ebpf 的代码

image-20211013215241811

又是经典的占位式布局!

heap-exploitation-in-real-world's People

Contributors

hac425xxx avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.