关于stac和clac的进一步细节及EFLAGS

发布于:2025-03-17 ⋅ 阅读:(9) ⋅ 点赞:(0)

一、背景

在之前的博客 内核态代码直接使用用户态数据的注意事项_内核态如何打开用户态文件-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 一节。

#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 一节。

#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 的文档):

bit18也叫做EFLAGS.AC:

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