利用patch绕过kextload对内核签名的检测

0x00 摘要

​ 分析CVE-2016-1757的EXP过程中,发现EXP通过对kextload程序的patch,绕过了OSX对驱动扩展数字签名的检测,成功加载未签名的驱动扩展。

0x01 数字签名与内核扩展

OSX系统在加载内核扩展时要求必须拥有苹果的开发者签名。在开发过程中,在自己的调试环境中可以通过修改参数加载没有签名的驱动,进行开发与调试。

1.1 OS X 10.11之后版本

​ 在OS X 10.11中引入的Rootless机制之后只需要关闭Rootless机制就可以加载没有签名的驱动扩展。细节可以参考这一篇文章。OS X 10.11中Rootless的实现与解释以及关闭方法

1.2 OS X 10.11之前版本

​ 在OS X 10.11之前的版本需要修改内核启动参数,并重启系统

1
2
sudo nvram boot-args="kext-dev-mode=1"
sudo reboot

0x02 Patch详细分析

​ 通过对kextload程序patch前后的对比可以发现,总共对两个函数进行了patch。

ida_bindiff

2.1 sub_100020c9(check_root)

​ 通过bindiff发现,sub_100020c9函数改变非常的大(rename成了check_root,不一定完全正确),patch前后对比如下图所示:

check_root

该函数内容被完全替换,直接返回0。

通过ida看一下替换前该函数的伪代码,可以大致了解该函数的作用。

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
__int64 __usercall check_root@<rax>(__int64 a1@<rax>)
{
signed int v1; // ebx@3
__int64 v3; // [sp-4h] [bp-10h]@1

v3 = a1;
HIDWORD(v3) = 0;
if ( bootstrap_look_up(*(_DWORD *)bootstrap_port_ptr, "com.apple.KernelExtensionServer", (mach_port_t *)&v3 + 1) )
{
if ( geteuid() )
{
OSKextLog(0LL, 113LL, "Can't contact kextd; must run as root to load kexts.");
v1 = 77;
}
else
{
v1 = 0;
OSKextLog(0LL, 115LL, "Can't contact kextd; attempting to load directly into kernel.");
}
}
else
{
byte_100004808 = 1;
v1 = 0;
}
if ( HIDWORD(v3) )
mach_port_deallocate(*(_DWORD *)mach_task_self__ptr, HIDWORD(v3));
return (unsigned int)v1;
}

可以看到返回0的情况有两种:

  • 能够获取到与com.apple.KernelExtensionServer通信的port
  • 没有获取到port,但是拥有root权限。

逻辑比较简单,应该就是为后面的内核扩展加载检测一下权限和资源。patch后直接返回0。

1
2
3
4
__int64 __usercall check_root@<rax>(__int64 a1@<rax>)
{
return 0;
}

2.2 sub_1000023bc

​ 该函数相似度有99%,查看patch后的前后对比的具体细节,如下图所示:

bindiff_check_sign

通过nop指令,干掉了对sub_100000979函数的调用。直接通过xor指令清零eax

查看sub_100000979的伪代码,看看该函数究竟做了些什么。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
__int64 __fastcall check_sign(__int64 a1, char a2, char a3)
{

char v3; // r13@1
char v4; // r15@1
__int64 v5; // r12@1
int v6; // ebx@1
__int64 v7; // rax@2
__int64 v8; // rax@2
__int64 v9; // r14@2
__int64 v10; // rax@5
__CFString *v11; // rdi@5
__int64 v12; // rsi@10
__int64 v14; // [sp+8h] [bp-38h]@1
__int64 v15; // [sp+10h] [bp-30h]@1

v3 = a3;
v4 = a2;
v5 = a1;
v15 = 0LL;
v14 = 0LL;
v6 = -67061;
if ( a1 )
{
LODWORD(v7) = OSKextGetURL();
LODWORD(v8) = CFURLCopyAbsoluteURL(v7);
v9 = v8;
if ( v8 )
{
if ( SecStaticCodeCreateWithPath(v8, 0LL, &v15) || !v15 )
goto LABEL_26;
LODWORD(v10) = OSKextGetIdentifier(a1);
v11 = &cfstr_AnchorApple;
if ( !(unsigned __int8)CFStringHasPrefix(v10, &cfstr_Com_apple_) )
v11 = &cfstr_AnchorAppleGen;
if ( SecRequirementCreateWithString(v11, 0LL, &v14) || !v14 )
{
LABEL_26:
OSKextLog(0LL, 17LL, "Memory allocation failure.");
v6 = -67061;
}
else
{
if ( v3 )
v12 = 536870913LL;
else
v12 = 1073741825LL;
//*******************************
//签名检测
//*******************************
v6 = SecStaticCodeCheckValidity(v15, v12);
if ( v4 && v6 && (unsigned __int8)sub_100000B19(v5, v9, 1LL) )
v6 = 0;
}
CFRelease(v9);
if ( v15 )
CFRelease(v15);
}
else
{
OSKextLog(0LL, 17LL, "Memory allocation failure.");
v6 = -67061;
}
if ( v14 )
CFRelease(v14);
}
return (unsigned int)v6;
}

可以看到最核心的就是调用了SecStaticCodeCheckValidity这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
@function SecStaticCodeCheckValidity
Performs static validation on the given SecStaticCode object. The call obtains and
verifies the signature on the code object. It checks the validity of all
sealed components (including resources, if any). It validates the code against
a SecRequirement if one is given. The call succeeds if all these conditions
are satisfactory. It fails otherwise.
*/


/*蹩脚的翻译一下
@function SecStaticCodeCheckValidity
认证传入的SecStaticCode对象的合法性,该函数调用获取并检测数字签名的合法性。
认证内容包含了所有的组件(如果存在资源文件同样会检测)。
如果提供了加密的内容,也会对加密部分进行认证。
当所有条件满足时函数调用返回成功。
*/

这是该函数在头文件中的定义。

0x03 小结

​ 经过测试确实可以绕过数字签名的检测成功加载了未签名的内核扩展。

​ 通过对kextloadpatch,绕过两个检测函数,从而达到了绕过OSX系统对驱动扩展的签名检测,结合CVE-2016-1757root权限执行任意代码,从而实现了,普通用户可以加载任意未签名的内核扩展。

0x04 勘误

​ 由于不够严谨,遗漏了一个patch的点,这里补上。更详细的原理在exploit的分析文章中可以看到。

4.1 sub_1000023BC

sub_1000023BC函数中的对sub_085c的调用被patch修改绕过签名验证。

1
2
3
4
5
6
7
8
__text:0000000100002802                 mov     esi, 1
__text:0000000100002807 mov rdi, r15
__text:000000010000280A mov edx, r12d
__text:000000010000280D call sub_10000085C
__text:0000000100002812 test eax, eax
__text:0000000100002814 mov r12, r14
__text:0000000100002817 mov r14, [rbp+var_CD0]
__text:000000010000281E jnz loc_1000028FA

patch后代码如下:

1
2
3
4
5
6
7
8
9
10
11
__text:000000010000277E                 mov     r14, r12
__text:0000000100002781 movzx r12d, [rbp+var_CB9]
__text:0000000100002789 mov esi, 1
__text:000000010000278E mov rdi, r15
__text:0000000100002791 mov edx, r12d
__text:0000000100002794 nop
__text:0000000100002795 nop
__text:0000000100002796 nop
__text:0000000100002797 xor eax, eax
__text:0000000100002799 mov ebx, eax
__text:000000010000279B test ebx, ebx

bindiif如下图所示:

勘误1

ps:

这是我的学习分享博客http://turingh.github.io/

欢迎大家来探讨,不足之处还请指正。

文章目录
  1. 1. 0x00 摘要
  2. 2. 0x01 数字签名与内核扩展
    1. 2.1. 1.1 OS X 10.11之后版本
    2. 2.2. 1.2 OS X 10.11之前版本
  3. 3. 0x02 Patch详细分析
    1. 3.1. 2.1 sub_100020c9(check_root)
    2. 3.2. 2.2 sub_1000023bc
  4. 4. 0x03 小结
  5. 5. 0x04 勘误
    1. 5.1. 4.1 sub_1000023BC
  6. 6. ps:
,