一、背景
在之前的博客 内核态代码直接使用用户态数据的注意事项_内核态如何打开用户态文件-CSDN博客 里,我们x86平台上在内核态里使用用户态数据的相关细节,即需要使用stac和clac函数来打开/关闭内核态访问用户态数据的权限,这里说是权限,其实指的是管控,也就是x86上SMAP特性,防止内核逻辑意外修改用户态数据,仅仅在copy_from_user/copy_to_user等一些地方临时允许内核态逻辑来访问用户态数据。在arm64下,也有类似的机制,如下图里的:

这里不展开说arm64的,但是可以得知的是,不同的硬件平台一些重要的特性都会覆盖的,如arm64的smap功能在armv8.4-A架构及更高版本中会引入。
回到x86平台,在之前的博客 内核态代码直接使用用户态数据的注意事项-CSDN博客 里,我们自己调用了stac和clac,在stac和clac的中间使用了memcpy使用到了用户态数据,但是其实会遇到一个warning的提示,我们在下面第二章里,展开说明。然后我们在第三章里,把与stac和clac有关的在做上下文切换时如何保存和恢复的逻辑做说明。
二、stac和clac之间使用memcpy会导致编译warning
如下图,如果在stac和clac之间使用memcpy:

就会导致编译时如下的warning:

提示"call to fortify_panic() with UACCESS enabled"。
2.1 在编译时会使用objtool,在进行validate_call函数时会进行UACCESS使能的检查
如下图可以看到,在objtool的源码里,有对指令进行uaccess的验证,如果当前这条指令不是uaccess安全的话,会进行报错:

编译时是可以获悉当前是否在代码里使用了stac这样的打开内核态代码访问用户态数据的管控。
2.2 memcpy里会进行检查,如果检查不过会触发fortify_panic
在objdump -S导出的vmlinux.txt里,可以搜到如下判断和触发fortify_panic的逻辑:

上图里的代码:
if ((p_size != SIZE_MAX && p_size < size) ||
源码里就一处:

是在fortify-string.h里的fortify_memcpy_chk函数里:

而fortify_memcpy_chk函数是在__fortify_memcpy_chk里使用到:

是在fortify-string.h里定义了memcpy,使用到了上图里的__fortify_memcpy_chk宏:

三、stac和clac会写EFLAGS
我们知道x86下的EFLAGS在做上下文切换时会进行保存和恢复。内核里是用pt_regs这个结构体来保存和恢复上下文切换时所需要的prev和next的寄存器。有关pt_regs的更多的细节介绍,见之前的博客 监测各个核上cpu上的线程是内核线程还是用户线程,处于内核态还是用户态的方法_怎么查看线程是用户tai还是内核态-CSDN博客里的 3.1 一节。
下面 3.1 一节,我们先贴出测试源码和测试情况,在 3.2 一节里进行相关分析。
3.1 测试源码和测试结果
3.1.1 测试用的内核模块源码
这一节的内核模块源码是改写自 内核态代码直接使用用户态数据的注意事项-CSDN博客 博客里的 2.1 一节。
cpp
#include <linux/module.h>
#include <linux/capability.h>
#include <linux/sched.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/ctype.h>
#include <linux/seq_file.h>
#include <linux/poll.h>
#include <linux/types.h>
#include <linux/ioctl.h>
#include <linux/errno.h>
#include <linux/stddef.h>
#include <linux/lockdep.h>
#include <linux/kthread.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/wait.h>
#include <linux/init.h>
#include <asm/atomic.h>
#include <trace/events/workqueue.h>
#include <linux/sched/clock.h>
#include <linux/string.h>
#include <linux/mm.h>
#include <linux/interrupt.h>
#include <linux/tracepoint.h>
#include <trace/events/osmonitor.h>
#include <trace/events/sched.h>
#include <trace/events/irq.h>
#include <trace/events/kmem.h>
#include <linux/ptrace.h>
#include <linux/uaccess.h>
#include <asm/processor.h>
#include <linux/sched/task_stack.h>
#include <linux/nmi.h>
#include <asm/apic.h>
#include <linux/version.h>
#include <linux/sched/mm.h>
#include <asm/irq_regs.h>
#include <linux/kallsyms.h>
#include <linux/kprobes.h>
#include <linux/stop_machine.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zhaoxin");
MODULE_DESCRIPTION("Module for debug pf error code tasks.");
MODULE_VERSION("1.0");
#define TESTPFERRORCODE_NODE_TYPEMAGIC 0x73
#define TESTPFERRORCODE_IO_BASE 0x10
char _testmem[102400];
typedef struct testpferrorcode_ioctl_memcpy {
void* memstart;
int memsize;
} testpferrorcode_ioctl_memcpy;
#define TESTPFERRORCODE_IOCTL_MEMCPY \
_IOWR(TESTPFERRORCODE_NODE_TYPEMAGIC, TESTPFERRORCODE_IO_BASE + 1, testpferrorcode_ioctl_memcpy)
unsigned long get_eflags(void) {
unsigned long eflags;
asm volatile (
"pushf\n\t" // 将 EFLAGS 压入栈
"pop %0\n\t" // 将 EFLAGS 弹出到 eflags 变量
: "=r" (eflags) // 输出约束,eflags 变量作为输出
: // 没有输入
: // 没有被修改的寄存器
);
return eflags;
}
static long testpferrorcode_proc_ioctl(struct file *i_pfile, u32 i_cmd, long unsigned int i_arg)
{
switch (i_cmd) {
case TESTPFERRORCODE_IOCTL_MEMCPY:
{
void __user* parg = (void __user*)i_arg;
testpferrorcode_ioctl_memcpy mem;
if (copy_from_user(&mem, parg, sizeof(mem))) {
printk("copy_from_user failed\n");
return -EFAULT;
}
{
unsigned long eflags = get_eflags();
printk("before stac eflags=0x%llx\n", eflags);
}
stac();
{
unsigned long eflags = get_eflags();
printk("after stac eflags=0x%llx\n", eflags);
}
{
printk("before run memcpy\n");
for (int i = 0; i < mem.memsize; i++) {
*(_testmem + i) = *((char*)(mem.memstart + i));
}
printk("after run memcpy\n");
}
//memcpy(_testmem, mem.memstart, mem.memsize);
clac();
printk("after clac()\n");
return 0;
}
default:
return -EINVAL;
}
return 0;
}
static int testpferrorcode_proc_open(struct inode *i_pinode, struct file *i_pfile)
{
return 0;
}
static int testpferrorcode_proc_release(struct inode *i_inode, struct file *i_file)
{
return 0;
}
typedef struct testpferrorcode_env {
struct proc_dir_entry* testpferrorcode;
} testpferrorcode_env;
static testpferrorcode_env _env;
static const struct proc_ops testpferrorcode_proc_ops = {
.proc_read = NULL,
.proc_write = NULL,
.proc_open = testpferrorcode_proc_open,
.proc_release = testpferrorcode_proc_release,
.proc_ioctl = testpferrorcode_proc_ioctl,
};
#define PROC_TESTPFERRORCODE_NAME "testpferrorcode"
static int __init testpferrorcode_init(void)
{
_env.testpferrorcode = proc_create(PROC_TESTPFERRORCODE_NAME, 0666, NULL, &testpferrorcode_proc_ops);
return 0;
}
static void __exit testpferrorcode_exit(void)
{
remove_proc_entry(PROC_TESTPFERRORCODE_NAME, NULL);
}
module_init(testpferrorcode_init);
module_exit(testpferrorcode_exit);
3.1.2 上层的测试程序
这一节的上层测试代码是改写自 内核态代码直接使用用户态数据的注意事项-CSDN博客 博客里的 2.2 一节。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
#include <sys/types.h>
#include <sys/ioctl.h>
using namespace std;
#define TESTPFERRORCODE_NODE_TYPEMAGIC 0x73
#define TESTPFERRORCODE_IO_BASE 0x10
typedef struct testpferrorcode_ioctl_memcpy {
void* memstart;
int memsize;
} testpferrorcode_ioctl_memcpy;
#define TESTPFERRORCODE_IOCTL_MEMCPY \
_IOWR(TESTPFERRORCODE_NODE_TYPEMAGIC, TESTPFERRORCODE_IO_BASE + 1, testpferrorcode_ioctl_memcpy)
int main(int i_argc, char* i_argv[]) {
if (i_argc != 2) {
printf("should be two parameters!\n");
return -1;
}
int convertfd = open("/proc/testpferrorcode", O_RDWR);
testpferrorcode_ioctl_memcpy mem;
int size = atoi(i_argv[1]);
if (size == 4) {
int testa = 3;
int testsize = sizeof(testa);
mem.memstart = &testa;
mem.memsize = testsize;
}
else {
printf("size=%d\n", size);
mem.memstart = malloc(size);
printf("memstart=0x%llx\n", mem.memstart);
//memset(mem.memstart, 0, size);
mem.memsize = size;
}
if (ioctl(convertfd, TESTPFERRORCODE_IOCTL_MEMCPY, &mem) < 0) {
perror("ioctl");
return 0;
}
return 1;
}
3.1.3 测试步骤和测试结果
测试步骤是先insmod 3.1.1 里编出后的ko:

然后运行 3.1.2 里编出的程序,执行方式如下:

看dmesg里的日志里下图里的红色框里的部分:

可以看到在执行stac前后,eflags的值是有变更的。
3.2 原理分析
3.2.1 eflags的获取方法
eflags是x86下的一个寄存器,是状态标志寄存器。可以通过如下方法来获取eflags的值:

如上图,逻辑还是比较简单的,即通过pushf把eflags压入栈,然后再通过pop弹出它的值到一个寄存器里,作为函数返回值返回。
3.2.2 stac和clac会写EFLAGS的bit18
如下图的dmesg打印可以看到stac是写的EFLAGS的bit18(0x246->0x40246):

改写的是下图里的红色框出的bit位(下图是取自资源 https://download.csdn.net/download/weixin_42766184/90348566?spm=1001.2014.3001.5503 的文档):


该EFLAGS.AC与SMAP功能的相关注释如下:
