首页
社区
课程
招聘
[原创][PWN] Linux中的pkeys安全机制及绕过
发表于: 2025-12-4 20:03 1430

[原创][PWN] Linux中的pkeys安全机制及绕过

2025-12-4 20:03
1430

小记:做?CTF时遇到了daimi师傅出的弥达斯之触(f70K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6L8r3!0$3k6h3y4@1k6W2)9J5k6h3y4F1i4K6u0r3k6$3q4E0k6i4y4Q4x3V1j5J5i4K6u0r3j5$3S2S2L8r3I4W2L8X3N6W2M7#2)9K6c8X3W2V1i4K6y4p5x3e0f1K6i4K6t1&6i4@1g2r3i4@1u0o6i4K6S2o6i4@1f1^5i4K6R3H3i4K6R3K6i4@1f1$3i4K6W2r3i4@1p5#2i4@1f1@1i4@1u0m8i4K6R3$3pkeys保护机制,查找资料时发现网上没什么资料对pwn中的pkeys作讲解,所以记录一下

(Memory Protection Keys for Userspace,PKU,亦 即PKEYs)内存保护键提供了一种强制实施基于页的保护机制,可以快速调整某些内存区域的执行权限,而不是像传统的mprotect那样,触发页表项的修改,从而导致TLB(快速查找缓存)刷新,影响性能。

先来说说传统的mprotect(),它的工作机制是通过修改目标内存区域的页表项(PTE)的权限位实现内存保护,属于进程全局事件,会导致TLB的刷新,因此切换权限成本高。

pkeys是通过在页表中写入静态保护键标识,真正权限定义在线程局部寄存器中,需要修改目标内存区域的权限只需要修改寄存器就行了,作用局限于线程,不涉及页表和TLB的修改,权限切换非常高效。

x86_64架构中,每个页表中,将 4 个先前保留的位专门用于一个“保护键”,从而提供16 个可能的键

每个键的保护由一个 per-CPU 用户可访问寄存器 (PKRU) 定义。每个 PKRU 都是一个 32 位寄存器,为 16 个键中的每个键存储两位访问禁用和写入禁用)。也就是说PKRU寄存器存储16个保护键的权限,每个保护键占用2位,可以组合为4种权限,每个保护键对应的权限设置被称为一个控制集

举个栗子:
假设有一个线程,它的内存中有三个不同的区域,并且这三个区域被分别分配了不同的保护键:

然后,线程的PKRU 寄存器中可能存储的内容是:

有两条特殊指令管理保护键的读写:

RDPKRU:用于读取当前线程的保护键权限设置。通过这条指令,CPU 将返回一个32位值,包含当前线程的所有保护键的权限设置。

WRPKRU:用于更新当前线程的保护键权限设置。通过这条指令,操作系统或应用程序可以修改特定保护键的访问权限。

PKRU 是与线程绑定的,也就是说,不同的线程可以有不同的保护键权限设置。由于 PKRU 寄存器是 per-CPU 的,每个线程的保护权限都可以在不同的 CPU 上独立设置。同样,当一个线程在不同的 CPU 核心上切换时,操作系统会保证 PKRU 寄存器的状态(即保护键权限)正确地切换,以确保内存访问控制的一致性。

保护键主要应用于内存页的访问控制。在数据访问时,Pkey 的权限会被强制执行,但它对指令获取(如程序代码段的访问)没有影响。这意味着x86下的Pkeys 只对用户空间内存的读写权限有控制作用,无法对代码段执行权限进行限制

arm64就简单介绍一下,在arm64下,Pkeys 在每个页表项中使用 3 位来编码一个“保护键索引”,从而提供 8 个可能的键:

每个键的保护由一个 per-CPU 用户可写系统寄存器 (POR_EL0) 定义。这是一个 64 位寄存器,用于编码每个保护键索引的读、写和执行覆盖权限。

arm64下实现的pkeys也是线程独立的,但是arm64_pkeys的保护键权限不仅适用于数据控制访问,也适用于程序代码的执行权限

有 3 个系统调用直接与 pkeys 交互:

这个系统调用用来分配一个新的保护键(Pkey)。

返回分配的保护键的 ID,成功时返回保护键的 ID,失败时返回负数。

这个调用会释放之前分配的保护键。

成功时返回 0,失败时返回负数。

这个调用可以修改指定内存区域的保护权限,并将其与指定的保护键关联。

成功时返回 0,失败时返回负数。

下面用一道题介绍CTF PWN中pkeys的使用和绕过

源码:

这里面用到了两个pkeys相关的系统调用:

先看这段代码:

正常 pkey_mprotect(addr,len,prot,pkey) 应用于有效内存,但这里给的地址无效

当 addr 无效时,pkey_mprotect 不会设置页权限,但仍然更新 pkru 中对应 pkey 的权限位

所以这里是随机修改不同 pkey 的读/写权限,使得未来分配的 pkey 权限是不可预测的

再看这段:

其内部随机选择 pkey,而当后面flag 所在内存执行这句:

会从1~15随机选取一个pkey,而这 15 个 key 此时权限早已被 the_curse_of_midas() 随机污染

所以当_mprotect绑定页时,权限不可预测,大多数情况下不能直接读,限制了读取flag

看这里:

7 =

此时页表层面权限是RWX,但PKRU层面是根据pkey附加限制覆盖访问权限的,所以最后大概率不能直接读flag

那么如何绕过呢?

之前提到有两条特殊指令管理保护键的读写,分别是RDPKRUWRPKRU,其中WRPKRU可以修改特定保护键的访问权限。

在题目给的libc库中寻找这条指令:

在这里插入图片描述
偏移是0x126256

WRPKRU的使用约束是 ECX 和 EDX 必须为 0。如果不满足,结果未定义或者会 #UD(非法指令行为)

PKRU权限表如下:

而参照这个表,我们肯定想修改目标区域权限为00(可读可写),而在EAX(装载 PKRU 的值)里写一个0,EAX = 0x00000000
直接就可以把全部pkey权限都变成00

知道这些就可以写exp了

先看fmt的偏移
在这里插入图片描述

0x1的偏移是8

在这里插入图片描述

用这条绕过pie(其实没必要,顺手绕一下),fmt偏移是11,程序基址偏移是0x153B

在这里插入图片描述
在这里插入图片描述

这条泄露libc,fmt偏移是29,libc偏移是0x29e40

在根目录放个flag测试文件

exp

在输入后打个断点,看到此时pkru寄存器的值是0x55555554

在这里插入图片描述
被改成了0x0

在这里插入图片描述

成功绕过并输出flag

在这里插入图片描述

exp小贴士:可以注意到payload中在覆盖掉rbp后,又加了一个p64(rdi+1)(=ret),这是为了保证栈对齐

在调用libc时,调用点的RSP必须是16字节对齐(即进入callRSP%16 == 8),这是为了满足 System V ABI 对齐规则

非常感谢daimi师傅的帮助

0000 → Pkey 0
0001 → Pkey 1
0010 → Pkey 2
1111 → Pkey 15
0000 → Pkey 0
0001 → Pkey 1
0010 → Pkey 2
1111 → Pkey 15
00:无权限
01:只读(写入禁用)
10:无访问(访问禁用)
11:读写(无禁用)
00:无权限
01:只读(写入禁用)
10:无访问(访问禁用)
11:读写(无禁用)
区域 A(保护键 Pkey 0):只读(只能读取,不能写入)
区域 B(保护键 Pkey 1):可读可写(可以读取,也可以写入)
区域 C(保护键 Pkey 2):禁止访问(既不能读取,也不能写入)
区域 A(保护键 Pkey 0):只读(只能读取,不能写入)
区域 B(保护键 Pkey 1):可读可写(可以读取,也可以写入)
区域 C(保护键 Pkey 2):禁止访问(既不能读取,也不能写入)
Pkey 0:只读
Pkey 1:可读可写
Pkey 2:禁止访问
Pkey 0:只读
Pkey 1:可读可写
Pkey 2:禁止访问
000 → Pkey 0
001 → Pkey 1
010 → Pkey 2
011 → Pkey 3
100 → Pkey 4
101 → Pkey 5
110 → Pkey 6
111 → Pkey 7
000 → Pkey 0
001 → Pkey 1
010 → Pkey 2
011 → Pkey 3
100 → Pkey 4
101 → Pkey 5
110 → Pkey 6
111 → Pkey 7
int pkey_alloc(unsigned long flags, unsigned long init_access_rights);
int pkey_alloc(unsigned long flags, unsigned long init_access_rights);
int pkey_free(int pkey);
int pkey_free(int pkey);
int pkey_mprotect(unsigned long start, size_t len, unsigned long prot, int pkey)
int pkey_mprotect(unsigned long start, size_t len, unsigned long prot, int pkey)
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <sys/mman.h>
#include <bpf_insn.h>
#include <linux/filter.h>
#include <sys/prctl.h>
#include <seccomp.h>
#include <stddef.h>
 
// __attribute__((constructor))
void the_curse_of_midas() {
    struct sock_filter filter[] = {
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
        // 加载系统调用号到寄存器
         
        // BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 6, 0),
        // BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execveat, 5, 0),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_open, 4, 0),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 3, 0),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mmap, 2, 0),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mprotect, 1, 0),
         
        // 只允许上述调用
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
         
        //其他全kill
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    };
    struct sock_fprog prog = {
        .len = sizeof(filter)/sizeof(filter[0]),
        .filter = filter,
    };
 
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl(NO_NEW_PRIVS)");
    }
 
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) != 0) {
        perror("prctl(SECCOMP)");
    }
 
    asm(
        "mov rdx, 0xf;"
        "begin:;"
        "dec rdx;"
        "jz end;"
        "mov rax, 0x14a;"//这里是pkey_mprotect()
        "xor edi, edi;"
        "mov rsi, 1;"
        "syscall;"
        "jmp begin;"
        "end:;"
    );
}
 
//自定义pkey,在这里实现了pkeys
int _mprotect(void *addr, __int64_t len, int prot)
{
    int pkey = (rand()%15)+1;//随机生成pkey 1~15
    asm volatile(
        "mov rax, %4;"  // syscall number
        "mov rdi, %0;"  // 1st arg: addr
        "mov rsi, %1;"  // 2nd arg: len
        "mov rdx, %2;"  // 3rd arg: prot
        "mov r10d, %3;" // 4th arg: pkey
        "syscall;"//这里是pkey_alloc()(0x149)
        :
        : "r"(addr), "r"(len), "r"(prot), "r"(pkey), "i"(0x149)
        : "rax", "rdi", "rsi", "rdx", "r10", "rcx", "r11", "memory"
    );
}
 
void init(void *secret)
{
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
    srand(((long long)rand())>>24);
    int fd = open("/flag", 0, 0);
    read(fd, secret, 0x100);
    close(fd);
 
    the_curse_of_midas();
    _mprotect(secret, 0x1000, 7);
}
 
char buf[0x100];
int main()
{
    void *secret_of_midas = mmap(0x1000, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
    init(secret_of_midas);
 
    printf("远道而来的年轻人,你也是来寻找弥达斯的秘密吗?\n");
    read(0, buf, 20);
    printf(buf);
    printf("噢不,噢不,你太心急了。\n");
    _mprotect(secret_of_midas, 0x1000, 1);
    sleep(1);
 
    printf("那些关于黄金的诅咒,正在这片古老土地的血管里涌动。\n");
    sleep(1);
 
    printf("你应当保持警惕。\n");
    sleep(1);
 
    printf("那么,告诉我你的名字,我将引领你去往神圣之地:\n");
    read(0, buf, 0x100);
    printf("有趣。去你想去的地方吧。\n");
    asm("mov rbp, %0;"
        :
        : "r"(buf)
    );
}
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <sys/mman.h>
#include <bpf_insn.h>
#include <linux/filter.h>
#include <sys/prctl.h>
#include <seccomp.h>
#include <stddef.h>
 
// __attribute__((constructor))
void the_curse_of_midas() {
    struct sock_filter filter[] = {
        BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
        // 加载系统调用号到寄存器
         
        // BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execve, 6, 0),
        // BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_execveat, 5, 0),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_open, 4, 0),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_openat, 3, 0),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mmap, 2, 0),
        BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mprotect, 1, 0),
         
        // 只允许上述调用
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
         
        //其他全kill
        BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    };
    struct sock_fprog prog = {
        .len = sizeof(filter)/sizeof(filter[0]),
        .filter = filter,
    };
 
    if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
        perror("prctl(NO_NEW_PRIVS)");
    }
 
    if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) != 0) {
        perror("prctl(SECCOMP)");
    }
 
    asm(
        "mov rdx, 0xf;"
        "begin:;"
        "dec rdx;"
        "jz end;"
        "mov rax, 0x14a;"//这里是pkey_mprotect()
        "xor edi, edi;"
        "mov rsi, 1;"
        "syscall;"
        "jmp begin;"
        "end:;"
    );
}
 
//自定义pkey,在这里实现了pkeys
int _mprotect(void *addr, __int64_t len, int prot)
{
    int pkey = (rand()%15)+1;//随机生成pkey 1~15
    asm volatile(
        "mov rax, %4;"  // syscall number
        "mov rdi, %0;"  // 1st arg: addr
        "mov rsi, %1;"  // 2nd arg: len
        "mov rdx, %2;"  // 3rd arg: prot
        "mov r10d, %3;" // 4th arg: pkey
        "syscall;"//这里是pkey_alloc()(0x149)
        :

传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 14
支持
分享
最新回复 (2)
雪    币: 5630
活跃值: (9442)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
好文章
2025-12-5 09:12
0
雪    币: 1224
活跃值: (1358)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
pkeys是啥,学习一下
2025-12-5 14:19
0
游客
登录 | 注册 方可回帖
返回